实现HTTPS配置

This commit is contained in:
GoEdgeLab
2020-10-01 16:01:04 +08:00
parent 76575a9e47
commit 140d73420c
13 changed files with 1129 additions and 3 deletions

View File

@@ -0,0 +1,483 @@
Vue.component("ssl-config-box", {
props: ["v-ssl-policy", "v-protocol"],
created: function () {
let that = this
setTimeout(function () {
that.sortableCipherSuites()
}, 100)
},
data: function () {
let policy = this.vSslPolicy
if (policy == null) {
policy = {
id: 0,
isOn: true,
certRefs: [],
certs: [],
clientCARefs: [],
clientCACerts: [],
clientAuthType: 0,
minVersion: "TLS 1.1",
hsts: null,
cipherSuitesIsOn: false,
cipherSuites: [],
http2Enabled: true
}
} else {
if (policy.certRefs == null) {
policy.certRefs = []
}
if (policy.certs == null) {
policy.certs = []
}
if (policy.clientCARefs == null) {
policy.clientCARefs = []
}
if (policy.clientCACerts == null) {
policy.clientCACerts = []
}
if (policy.cipherSuites == null) {
policy.cipherSuites = []
}
}
let hsts = policy.hsts
if (hsts == null) {
hsts = {
isOn: false,
maxAge: 0,
includeSubDomains: false,
preload: false,
domains: []
}
}
return {
policy: policy,
// hsts
hsts: hsts,
hstsOptionsVisible: false,
hstsDomainAdding: false,
addingHstsDomain: "",
hstsDomainEditingIndex: -1,
// 相关数据
allVersions: window.SSL_ALL_VERSIONS,
allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(),
modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES,
intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES,
allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES,
cipherSuitesVisible: false,
// 高级选项
moreOptionsVisible: false
}
},
watch: {
hsts: {
deep: true,
handler: function () {
this.policy.hsts = this.hsts
}
}
},
methods: {
// 删除证书
removeCert: function (index) {
let that = this
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () {
that.policy.certRefs.$remove(index)
that.policy.certs.$remove(index)
})
},
// 选择证书
selectCert: function () {
let that = this
teaweb.popup("/servers/components/ssl/selectPopup", {
width: "50em",
height: "30em",
callback: function (resp) {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
}
})
},
// 上传证书
uploadCert: function () {
let that = this
teaweb.popup("/servers/components/ssl/uploadPopup", {
height: "28em",
callback: function (resp) {
teaweb.success("上传成功", function () {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
})
}
})
},
// 更多选项
changeOptionsVisible: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
// 格式化时间
formatTime: function (timestamp) {
return new Date(timestamp * 1000).format("Y-m-d")
},
// 格式化加密套件
formatCipherSuite: function (cipherSuite) {
return cipherSuite.replace(/(AES|3DES)/, "<var style=\"font-weight: bold\">$1</var>")
},
// 添加单个套件
addCipherSuite: function (cipherSuite) {
if (!this.policy.cipherSuites.$contains(cipherSuite)) {
this.policy.cipherSuites.push(cipherSuite)
}
this.allCipherSuites.$removeValue(cipherSuite)
},
// 删除单个套件
removeCipherSuite: function (cipherSuite) {
let that = this
teaweb.confirm("确定要删除此套件吗?", function () {
that.policy.cipherSuites.$removeValue(cipherSuite)
that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) {
return !that.policy.cipherSuites.$contains(v)
})
})
},
// 清除所选套件
clearCipherSuites: function () {
let that = this
teaweb.confirm("确定要清除所有已选套件吗?", function () {
that.policy.cipherSuites = []
that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy()
})
},
// 批量添加套件
addBatchCipherSuites: function (suites) {
var that = this
teaweb.confirm("确定要批量添加套件?", function () {
suites.$each(function (k, v) {
if (that.policy.cipherSuites.$contains(v)) {
return
}
that.policy.cipherSuites.push(v)
})
})
},
/**
* 套件拖动排序
*/
sortableCipherSuites: function () {
var box = document.querySelector(".cipher-suites-box")
Sortable.create(box, {
draggable: ".label",
handle: ".icon.handle",
onStart: function () {
},
onUpdate: function (event) {
}
})
},
// 显示所有套件
showAllCipherSuites: function () {
this.cipherSuitesVisible = !this.cipherSuitesVisible
},
// 显示HSTS更多选项
showMoreHSTS: function () {
this.hstsOptionsVisible = !this.hstsOptionsVisible;
if (this.hstsOptionsVisible) {
this.changeHSTSMaxAge()
}
},
// 监控HSTS有效期修改
changeHSTSMaxAge: function () {
var v = this.hsts.maxAge
if (isNaN(v)) {
this.hsts.days = "-"
return
}
this.hsts.days = parseInt(v / 86400)
if (isNaN(this.hsts.days)) {
this.hsts.days = "-"
} else if (this.hsts.days < 0) {
this.hsts.days = "-"
}
},
// 设置HSTS有效期
setHSTSMaxAge: function (maxAge) {
this.hsts.maxAge = maxAge
this.changeHSTSMaxAge()
},
// 添加HSTS域名
addHstsDomain: function () {
this.hstsDomainAdding = true
this.hstsDomainEditingIndex = -1
let that = this
setTimeout(function () {
that.$refs.addingHstsDomain.focus()
}, 100)
},
// 修改HSTS域名
editHstsDomain: function (index) {
this.hstsDomainEditingIndex = index
this.addingHstsDomain = this.hsts.domains[index]
this.hstsDomainAdding = true
let that = this
setTimeout(function () {
that.$refs.addingHstsDomain.focus()
}, 100)
},
// 确认HSTS域名添加
confirmAddHstsDomain: function () {
this.addingHstsDomain = this.addingHstsDomain.trim()
if (this.addingHstsDomain.length == 0) {
return;
}
if (this.hstsDomainEditingIndex > -1) {
this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain
} else {
this.hsts.domains.push(this.addingHstsDomain)
}
this.cancelHstsDomainAdding()
},
// 取消HSTS域名添加
cancelHstsDomainAdding: function () {
this.hstsDomainAdding = false
this.addingHstsDomain = ""
this.hstsDomainEditingIndex = -1
},
// 删除HSTS域名
removeHstsDomain: function (index) {
this.cancelHstsDomainAdding()
this.hsts.domains.$remove(index)
},
// 选择客户端CA证书
selectClientCACert: function () {
let that = this
teaweb.popup("/servers/components/ssl/selectPopup?isCA=1", {
width: "50em",
height: "30em",
callback: function (resp) {
that.policy.clientCARefs.push(resp.data.certRef)
that.policy.clientCACerts.push(resp.data.cert)
}
})
},
// 上传CA证书
uploadClientCACert: function () {
let that = this
teaweb.popup("/servers/components/ssl/uploadPopup?isCA=1", {
height: "28em",
callback: function (resp) {
teaweb.success("上传成功", function () {
that.policy.clientCARefs.push(resp.data.certRef)
that.policy.clientCACerts.push(resp.data.cert)
})
}
})
},
// 删除客户端CA证书
removeClientCACert: function (index) {
let that = this
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () {
that.policy.clientCARefs.$remove(index)
that.policy.clientCACerts.$remove(index)
})
}
},
template: `<div>
<h4>SSL/TLS相关配置</h4>
<input type="hidden" name="sslPolicyJSON" :value="JSON.stringify(policy)"/>
<table class="ui table definition selectable">
<tbody>
<tr>
<td class="title">用HTTP/2</td>
<td>
<div class="ui checkbox">
<input type="checkbox" value="1" v-model="policy.http2Enabled"/>
<label></label>
</div>
</td>
</tr>
<tr>
<td>选择证书</td>
<td>
<div v-if="policy.certs != null && policy.certs.length > 0">
<div class="ui label small" v-for="(cert, index) in policy.certs">
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}} &nbsp; <a href="" title="删除" @click.prevent="removeCert()"><i class="icon remove"></i></a>
</div>
<div class="ui divider"></div>
</div>
<div v-else>
<span class="red">选择或上传证书后<span v-if="vProtocol == 'https'">HTTPS</span><span v-if="vProtocol == 'tls'">TLS</span>服务才能生效。</span>
<div class="ui divider"></div>
</div>
<button class="ui button tiny" type="button" @click.prevent="selectCert()">选择已有证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadCert()">上传新证书</button>
</td>
</tr>
<tr>
<td>TLS最低版本</td>
<td>
<select v-model="policy.minVersion" class="ui dropdown auto-width">
<option v-for="version in allVersions" :value="version">{{version}}</option>
</select>
</td>
</tr>
</tbody>
<more-options-tbody @change="changeOptionsVisible"></more-options-tbody>
<tbody v-show="moreOptionsVisible">
<!-- 加密套件 -->
<tr>
<td>加密算法套件<em>CipherSuites</em></td>
<td>
<div class="ui checkbox">
<input type="checkbox" value="1" v-model="policy.cipherSuitesIsOn" />
<label>是否要自定义</label>
</div>
<div v-show="policy.cipherSuitesIsOn">
<div class="ui divider"></div>
<div class="cipher-suites-box">
已添加套件({{policy.cipherSuites.length}})
<div v-for="cipherSuite in policy.cipherSuites" class="ui label tiny" style="margin-bottom: 0.5em">
<input type="hidden" name="cipherSuites" :value="cipherSuite"/>
<span v-html="formatCipherSuite(cipherSuite)"></span> &nbsp; <a href="" title="删除套件" @click.prevent="removeCipherSuite(cipherSuite)"><i class="icon remove"></i></a>
<a href="" title="拖动改变顺序"><i class="icon bars handle"></i></a>
</div>
</div>
<div>
<div class="ui divider"></div>
<span v-if="policy.cipherSuites.length > 0"><a href="" @click.prevent="clearCipherSuites()">[清除所有已选套件]</a> &nbsp; </span>
<a href="" @click.prevent="addBatchCipherSuites(modernCipherSuites)">[添加推荐套件]</a> &nbsp;
<a href="" @click.prevent="addBatchCipherSuites(intermediateCipherSuites)">[添加兼容套件]</a>
<div class="ui divider"></div>
</div>
<div class="cipher-all-suites-box">
<a href="" @click.prevent="showAllCipherSuites()"><span v-if="policy.cipherSuites.length == 0">所有</span>可选套件({{allCipherSuites.length}}) <i class="icon angle" :class="{down:!cipherSuitesVisible, up:cipherSuitesVisible}"></i></a>
<a href="" v-if="cipherSuitesVisible" v-for="cipherSuite in allCipherSuites" class="ui label tiny" title="点击添加到自定义套件中" @click.prevent="addCipherSuite(cipherSuite)" v-html="formatCipherSuite(cipherSuite)" style="margin-bottom:0.5em"></a>
</div>
<p class="comment" v-if="cipherSuitesVisible">点击可选套件添加。</p>
</div>
</td>
</tr>
<!-- HSTS -->
<tr v-show="vProtocol == 'https'">
<td :class="{'color-border':hsts.isOn}">是否开启HSTS</td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="hstsOn" v-model="hsts.isOn" value="1"/>
<label></label>
</div>
<p class="comment">
开启后会自动在响应Header中加入
<span class="ui label small">Strict-Transport-Security:
<var v-if="!hsts.isOn">...</var>
<var v-if="hsts.isOn"><span>max-age=</span>{{hsts.maxAge}}</var>
<var v-if="hsts.isOn && hsts.includeSubDomains">; includeSubDomains</var>
<var v-if="hsts.isOn && hsts.preload">; preload</var>
</span>
<span v-if="hsts.isOn">
<a href="" @click.prevent="showMoreHSTS()">修改<i class="icon angle" :class="{down:!hstsOptionsVisible, up:hstsOptionsVisible}"></i> </a>
</span>
</p>
</td>
</tr>
<tr v-show="hsts.isOn && hstsOptionsVisible">
<td class="color-border">HSTS包含子域名<em>includeSubDomains</em></td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="hstsIncludeSubDomains" value="1" v-model="hsts.includeSubDomains"/>
<label></label>
</div>
</td>
</tr>
<tr v-show="hsts.isOn && hstsOptionsVisible">
<td class="color-border">HSTS预加载<em>preload</em></td>
<td>
<div class="ui checkbox">
<input type="checkbox" name="hstsPreload" value="1" v-model="hsts.preload"/>
<label></label>
</div>
</td>
</tr>
<tr v-show="hsts.isOn && hstsOptionsVisible">
<td class="color-border">HSTS生效的域名</td>
<td colspan="2">
<div class="names-box">
<span class="ui label tiny" v-for="(domain, arrayIndex) in hsts.domains" :class="{blue:hstsDomainEditingIndex == arrayIndex}">{{domain}}
<input type="hidden" name="hstsDomains" :value="domain"/> &nbsp;
<a href="" @click.prevent="editHstsDomain(arrayIndex)" title="修改"><i class="icon pencil"></i></a>
<a href="" @click.prevent="removeHstsDomain(arrayIndex)" title="删除"><i class="icon remove"></i></a>
</span>
</div>
<div class="ui fields inline" v-if="hstsDomainAdding" style="margin-top:0.8em">
<div class="ui field">
<input type="text" name="addingHstsDomain" ref="addingHstsDomain" style="width:16em" maxlength="100" placeholder="域名比如example.com" @keyup.enter="confirmAddHstsDomain()" @keypress.enter.prevent="1" v-model="addingHstsDomain" />
</div>
<div class="ui field">
<button class="ui button tiny" type="button" @click="confirmAddHstsDomain()">确定</button>
&nbsp; <a href="" @click.prevent="cancelHstsDomainAdding()">取消</a>
</div>
</div>
<div class="ui field" style="margin-top: 1em">
<button class="ui button tiny" type="button" @click="addHstsDomain()">+</button>
</div>
<p class="comment">如果没有设置域名的话,则默认支持所有的域名。</p>
</td>
</tr>
<!-- 客户端认证 -->
<tr>
<td>客户端认证方式</td>
<td>
<select name="clientAuthType" v-model="policy.clientAuthType" class="ui dropdown auto-width">
<option v-for="authType in allClientAuthTypes" :value="authType.type">{{authType.name}}</option>
</select>
</td>
</tr>
<tr>
<td>客户端认证CA证书</td>
<td>
<div v-if="policy.clientCACerts != null && policy.clientCACerts.length > 0">
<div class="ui label small" v-for="(cert, index) in policy.clientCACerts">
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}} &nbsp; <a href="" title="删除" @click.prevent="removeClientCACert()"><i class="icon remove"></i></a>
</div>
<div class="ui divider"></div>
</div>
<button class="ui button tiny" type="button" @click.prevent="selectClientCACert()">选择已有证书</button> &nbsp;
<button class="ui button tiny" type="button" @click.prevent="uploadClientCACert()">上传新证书</button>
<p class="comment">用来校验客户端证书以增强安全性,通常不需要设置。</p>
</td>
</tr>
</tbody>
</table>
<div class="ui margin"></div>
</div>`
})

322
web/public/js/date.tea.js Normal file
View File

@@ -0,0 +1,322 @@
/**
* Tea.Date 对象
*
* @class Tea.Date
*/
/**
* Tea.Date构造器。使用方法如<br/>
* var date = new Tea.Date();<br/>
* var date = new Tea.Date("Y-m-d H:i:s");<br/>
* var date = new Tea.Date("Y-m-d H:i:s", 1169226085);
*
* @constructor Tea.Date
* @param String format 时间格式为可选参数目前支持O,r,Y,y,L,M,m,n,F,t,w,D,l,d,z,H,i,s,j,h,G,g,a,A等字符。
* @param int time 时间戳,为可选参数
*/
Tea.Date = function (format, time) {
var date = new Date();
if (typeof(format) == "undefined") {
format = "r";
}
if (typeof(time) != "undefined") {
time = parseInt(time, 10);
date.setTime(time);
}
//parse char
this.get = function (chr) {
if ((chr >= "a" && chr <= "z") || (chr >= "A" && chr <= "Z")) {
var func = "_parse_" + chr;
if (this[func]) {
return this[func]();
}
}
return chr;
};
/**
* 根据提供的格式取得对应的时间格式
*
* @method parse
* @param String format
*/
this.parse = function (format) {
var result = "";
if (format.length > 0) {
for (var i=0; i<format.length; i++) {
var chr = format.charAt(i);
result += this.get(chr);
}
}
return result;
};
/**
* 设置某一时间为某值
*
* @method set
* @param String type 时间选项,如 d 表示天,Y 表示年,H 表示小时,等等。
* @param int value 新的值
*/
this.set = function (type, value) {
value = parseInt(value, 10);
switch (type) {
case "d":
date.setDate(value);
break;
case "Y":
date.setFullYear(value);
break;
case "H":
case "G":
date.setHours(value);
break;
case "i":
date.setMinutes(value);
break;
case "s":
date.setSeconds(value);
break;
case "m":
case "n":
date.setMonth(value - 1);
break;
}
};
//timezone
this._parse_O = function () {
var hours = (Math.abs(date.getTimezoneOffset()/60)).toString();
if (hours.length == 1) {
hours = "0" + hours;
}
return "+" + hours + "00";
};
this._parse_r = function () {
return this.parse("D, d M Y H:i:s O");
};
//parse year
this._parse_Y = function () {
return date.getFullYear().toString();
};
this._parse_y = function () {
var y = this._parse_Y();
return y.substr(2);
};
this._parse_L = function () {
var y = parseInt(this.parse("Y"));
if (y%4 ==0 && (y%100 > 0 || y%400 == 0)) {
return "1";
}
return "0";
};
//month
this._parse_m = function () {
var n = this._parse_n();
if (n.length < 2) {
n = "0" + n;
}
return n;
};
this._parse_n = function () {
return (date.getMonth() + 1).toString();
};
this._parse_t = function () {
var t = 32 - new Date(this.get("Y"), this.get("m") - 1 , 32).getDate();
return t;
};
this._parse_F = function () {
var n = parseInt(this.parse("n"));
var months = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
return months[n];
};
this._parse_M = function () {
var n = parseInt(this.parse("n"));
var months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
return months[n];
};
//week
this._parse_w = function () {
return date.getDay().toString();
};
this._parse_D = function () {
var w = parseInt(this._parse_w());
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
return days[w];
};
this._parse_l = function () {
var w = parseInt(this._parse_w());
var days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
return days[w];
};
//day
this._parse_d = function () {
var j = this._parse_j();
if (j.length < 2) {
j = "0" + j;
}
return j;
};
this._parse_j = function () {
return date.getDate().toString();
};
this._parse_W = function () {
var _date = new Tea.Date();
_date.set("m", 1);
_date.set("d", 1);
var w = parseInt(_date.parse("w"));
var m = parseInt(this.parse("m"), 10);
var total = 0;
for (var i=1; i<m; i++) {
var date2 = new Tea.Date();
date2.set("m", i);
var t = parseInt(date2.parse("t"));
total += t;
}
total += parseInt(this.parse("d"), 10);
var w2 = parseInt(this.parse("w"));
total = total - w2 + (w - 1);
var weeks = 0;
if (w2 != 0) {
weeks = (total/7 + 1).toString();
}
else {
weeks = (total/7).toString();
}
if (weeks.length == 1) {
weeks = "0" + weeks;
}
return weeks;
};
this._parse_z = function () {
var m = parseInt(this.parse("m"), 10);
var total = 0;
for (var i=1; i<m; i++) {
var date2 = new Tea.Date();
date2.set("m", i);
var t = parseInt(date2.parse("t"));
total += t;
}
total += parseInt(this.parse("d"), 10) - 1;
return total;
};
//minute
this._parse_i = function () {
var i = date.getMinutes().toString();
if (i.length < 2) {
i = "0" + i;
}
return i;
};
//second
this._parse_s = function () {
var s = date.getSeconds().toString();
if (s.length < 2) {
s = "0" + s;
}
return s;
};
//hour
this._parse_H = function () {
var H = this._parse_G();
if (H.length < 2) {
H = "0" + H;
}
return H;
};
this._parse_G = function () {
return date.getHours().toString();
};
this._parse_h = function () {
var h = this._parse_g();
if (h.length < 2) {
h = "0" + h;
}
return h;
};
this._parse_g = function () {
var g = parseInt(this._parse_G(), 10);
if (g > 12) {
g = g - 12;
}
return g.toString();
};
//time
this._parse_U = function () {
return this.time().toString();
};
//am/pm
this._parse_a = function () {
var hour = this.parse("H");
return (hour<12)?"am":"pm";
};
this._parse_A = function () {
return this.parse("a").toUpperCase();
};
/**
* 取得当前时间对应的时间戳,代表了从 1970 年 1 月 1 日开始计算到 Date 对象中的时间之间的秒数
*
* @method time
* @return int
*/
this.time = function () {
return Math.round(date.getTime()/1000);
};
/*
* 将该对象转换成字符串格式
*
* @method toString
* @return String 该对象的字符串表示形式
*/
this.toString = function () {
return this.parse(format);
};
};
Tea.Date.toTime = function (dateStr) {
if (arguments.length == 1) {
return Date.parse(dateStr);
} else if (arguments.length == 3) {
arguments[1] = parseInt(arguments[1], 10) - 1;
return (new Date(arguments[0], arguments[1], arguments[2])).time();
}
};
Number.prototype.dateFormat = function (format) {
var date = new Tea.Date(format, this * 1000);
return date.toString();
};
Date.prototype.format = function (format) {
return new Tea.Date(format, this.getTime()).toString();
};