diff --git a/internal/rpc/rpc_client.go b/internal/rpc/rpc_client.go index 0bb6b5fb..d3384ac2 100644 --- a/internal/rpc/rpc_client.go +++ b/internal/rpc/rpc_client.go @@ -130,6 +130,10 @@ func (this *RPCClient) SSLCertRPC() pb.SSLCertServiceClient { return pb.NewSSLCertServiceClient(this.pickConn()) } +func (this *RPCClient) SSLPolicyRPC() pb.SSLPolicyServiceClient { + return pb.NewSSLPolicyServiceClient(this.pickConn()) +} + // 构造上下文 func (this *RPCClient) Context(adminId int64) context.Context { ctx := context.Background() diff --git a/internal/web/actions/default/servers/components/ssl/datajs.go b/internal/web/actions/default/servers/components/ssl/datajs.go new file mode 100644 index 00000000..2eadb877 --- /dev/null +++ b/internal/web/actions/default/servers/components/ssl/datajs.go @@ -0,0 +1,60 @@ +package ssl + +import ( + "encoding/json" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" +) + +// 所有相关数据 +type DatajsAction struct { + actionutils.ParentAction +} + +func (this *DatajsAction) Init() { +} + +func (this *DatajsAction) RunGet(params struct{}) { + this.AddHeader("Content-Type", "text/javascript; charset=utf-8") + + { + cipherSuitesJSON, err := json.Marshal(sslconfigs.AllTLSCipherSuites) + if err != nil { + this.ErrorPage(err) + return + } + this.WriteString("window.SSL_ALL_CIPHER_SUITES = " + string(cipherSuitesJSON) + ";\n") + } + { + modernCipherSuitesJSON, err := json.Marshal(sslconfigs.TLSModernCipherSuites) + if err != nil { + this.ErrorPage(err) + return + } + this.WriteString("window.SSL_MODERN_CIPHER_SUITES = " + string(modernCipherSuitesJSON) + ";\n") + } + { + intermediateCipherSuitesJSON, err := json.Marshal(sslconfigs.TLSIntermediateCipherSuites) + if err != nil { + this.ErrorPage(err) + return + } + this.WriteString("window.SSL_INTERMEDIATE_CIPHER_SUITES = " + string(intermediateCipherSuitesJSON) + ";\n") + } + { + sslVersionsJSON, err := json.Marshal(sslconfigs.AllTlsVersions) + if err != nil { + this.ErrorPage(err) + return + } + this.WriteString("window.SSL_ALL_VERSIONS = " + string(sslVersionsJSON) + ";\n") + } + { + clientAuthTypesJSON, err := json.Marshal(sslconfigs.AllSSLClientAuthTypes()) + if err != nil { + this.ErrorPage(err) + return + } + this.WriteString("window.SSL_ALL_CLIENT_AUTH_TYPES = " + string(clientAuthTypesJSON) + ";\n") + } +} diff --git a/internal/web/actions/default/servers/components/ssl/init.go b/internal/web/actions/default/servers/components/ssl/init.go index b89b9695..faecb363 100644 --- a/internal/web/actions/default/servers/components/ssl/init.go +++ b/internal/web/actions/default/servers/components/ssl/init.go @@ -23,6 +23,8 @@ func init() { Get("/downloadKey", new(DownloadKeyAction)). Get("/downloadCert", new(DownloadCertAction)). Get("/downloadZip", new(DownloadZipAction)). + Get("/selectPopup", new(SelectPopupAction)). + Get("/datajs", new(DatajsAction)). EndAll() }) } diff --git a/internal/web/actions/default/servers/components/ssl/selectPopup.go b/internal/web/actions/default/servers/components/ssl/selectPopup.go new file mode 100644 index 00000000..bc6eb470 --- /dev/null +++ b/internal/web/actions/default/servers/components/ssl/selectPopup.go @@ -0,0 +1,68 @@ +package ssl + +import ( + "encoding/json" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" + "github.com/iwind/TeaGo/maps" + timeutil "github.com/iwind/TeaGo/utils/time" + "time" +) + +// 选择证书 +type SelectPopupAction struct { + actionutils.ParentAction +} + +func (this *SelectPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *SelectPopupAction) RunGet(params struct{}) { + // TODO 支持关键词搜索 + // TODO 列出常用的证书供用户选择 + + countResp, err := this.RPC().SSLCertRPC().CountSSLCerts(this.AdminContext(), &pb.CountSSLCertRequest{}) + if err != nil { + this.ErrorPage(err) + return + } + + page := this.NewPage(countResp.Count) + this.Data["page"] = page.AsHTML() + + listResp, err := this.RPC().SSLCertRPC().ListSSLCerts(this.AdminContext(), &pb.ListSSLCertsRequest{ + Offset: page.Offset, + Size: page.Size, + }) + + certConfigs := []*sslconfigs.SSLCertConfig{} + err = json.Unmarshal(listResp.CertsJSON, &certConfigs) + if err != nil { + this.ErrorPage(err) + return + } + this.Data["certs"] = certConfigs + + certMaps := []maps.Map{} + nowTime := time.Now().Unix() + for _, certConfig := range certConfigs { + countServersResp, err := this.RPC().ServerRPC().CountServersWithSSLCertId(this.AdminContext(), &pb.CountServersWithSSLCertIdRequest{CertId: certConfig.Id}) + if err != nil { + this.ErrorPage(err) + return + } + + certMaps = append(certMaps, maps.Map{ + "beginDay": timeutil.FormatTime("Y-m-d", certConfig.TimeBeginAt), + "endDay": timeutil.FormatTime("Y-m-d", certConfig.TimeEndAt), + "isExpired": nowTime > certConfig.TimeEndAt, + "isAvailable": nowTime <= certConfig.TimeEndAt, + "countServers": countServersResp.Count, + }) + } + this.Data["certInfos"] = certMaps + + this.Show() +} diff --git a/internal/web/actions/default/servers/components/ssl/uploadPopup.go b/internal/web/actions/default/servers/components/ssl/uploadPopup.go index 72635f19..8258dc7a 100644 --- a/internal/web/actions/default/servers/components/ssl/uploadPopup.go +++ b/internal/web/actions/default/servers/components/ssl/uploadPopup.go @@ -1,6 +1,7 @@ package ssl import ( + "encoding/json" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" @@ -73,7 +74,7 @@ func (this *UploadPopupAction) RunPost(params struct { } // 保存 - _, err = this.RPC().SSLCertRPC().CreateSSLCert(this.AdminContext(), &pb.CreateSSLCertRequest{ + createResp, err := this.RPC().SSLCertRPC().CreateSSLCert(this.AdminContext(), &pb.CreateSSLCertRequest{ IsOn: params.IsOn, Name: params.Name, Description: params.Description, @@ -91,5 +92,26 @@ func (this *UploadPopupAction) RunPost(params struct { return } + // 查询已创建的证书并返回,方便调用者进行后续处理 + certId := createResp.CertId + configResp, err := this.RPC().SSLCertRPC().FindEnabledSSLCertConfig(this.AdminContext(), &pb.FindEnabledSSLCertConfigRequest{CertId: certId}) + if err != nil { + this.ErrorPage(err) + return + } + certConfig := &sslconfigs.SSLCertConfig{} + err = json.Unmarshal(configResp.CertJSON, certConfig) + if err != nil { + this.ErrorPage(err) + return + } + certConfig.CertData = nil // 去掉不必要的数据 + certConfig.KeyData = nil // 去掉不必要的数据 + this.Data["cert"] = certConfig + this.Data["certRef"] = &sslconfigs.SSLCertRef{ + IsOn: true, + CertId: certId, + } + this.Success() } diff --git a/internal/web/actions/default/servers/server/settings/https/index.go b/internal/web/actions/default/servers/server/settings/https/index.go index a1366726..da2d997f 100644 --- a/internal/web/actions/default/servers/server/settings/https/index.go +++ b/internal/web/actions/default/servers/server/settings/https/index.go @@ -2,12 +2,15 @@ package https import ( "encoding/json" + "errors" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/servers/serverutils" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs" "github.com/iwind/TeaGo/actions" "github.com/iwind/TeaGo/maps" + "github.com/iwind/TeaGo/types" ) type IndexAction struct { @@ -37,10 +40,29 @@ func (this *IndexAction) RunGet(params struct { httpsConfig.IsOn = true } + var sslPolicy *sslconfigs.SSLPolicy + if httpsConfig.SSLPolicyRef != nil && httpsConfig.SSLPolicyRef.SSLPolicyId > 0 { + sslPolicyConfigResp, err := this.RPC().SSLPolicyRPC().FindEnabledSSLPolicyConfig(this.AdminContext(), &pb.FindEnabledSSLPolicyConfigRequest{SslPolicyId: httpsConfig.SSLPolicyRef.SSLPolicyId}) + if err != nil { + this.ErrorPage(err) + return + } + sslPolicyConfigJSON := sslPolicyConfigResp.SslPolicyJSON + if len(sslPolicyConfigJSON) > 0 { + sslPolicy = &sslconfigs.SSLPolicy{} + err = json.Unmarshal(sslPolicyConfigJSON, sslPolicy) + if err != nil { + this.ErrorPage(err) + return + } + } + } + this.Data["serverType"] = server.Type this.Data["httpsConfig"] = maps.Map{ "isOn": httpsConfig.IsOn, "addresses": httpsConfig.Listen, + "sslPolicy": sslPolicy, } this.Show() @@ -51,6 +73,8 @@ func (this *IndexAction) RunPost(params struct { IsOn bool Addresses string + SslPolicyJSON []byte + Must *actions.Must }) { addresses := []*serverconfigs.NetworkAddressConfig{} @@ -59,6 +83,73 @@ func (this *IndexAction) RunPost(params struct { this.Fail("端口地址解析失败:" + err.Error()) } + // TODO 校验addresses + + // 校验SSL + var sslPolicyId = int64(0) + if params.SslPolicyJSON != nil { + sslPolicy := &sslconfigs.SSLPolicy{} + err = json.Unmarshal(params.SslPolicyJSON, sslPolicy) + if err != nil { + this.ErrorPage(errors.New("解析SSL配置时发生了错误:" + err.Error())) + return + } + + sslPolicyId = sslPolicy.Id + + certsJSON, err := json.Marshal(sslPolicy.CertRefs) + if err != nil { + this.ErrorPage(err) + return + } + + hstsJSON, err := json.Marshal(sslPolicy.HSTS) + if err != nil { + this.ErrorPage(err) + return + } + + clientCACertsJSON, err := json.Marshal(sslPolicy.ClientCARefs) + if err != nil { + this.ErrorPage(err) + return + } + + if sslPolicyId > 0 { + _, err := this.RPC().SSLPolicyRPC().UpdateSSLPolicy(this.AdminContext(), &pb.UpdateSSLPolicyRequest{ + SslPolicyId: sslPolicyId, + Http2Enabled: sslPolicy.HTTP2Enabled, + MinVersion: sslPolicy.MinVersion, + CertsJSON: certsJSON, + HstsJSON: hstsJSON, + ClientAuthType: types.Int32(sslPolicy.ClientAuthType), + ClientCACertsJSON: clientCACertsJSON, + CipherSuitesIsOn: sslPolicy.CipherSuitesIsOn, + CipherSuites: sslPolicy.CipherSuites, + }) + if err != nil { + this.ErrorPage(err) + return + } + } else { + resp, err := this.RPC().SSLPolicyRPC().CreateSSLPolicy(this.AdminContext(), &pb.CreateSSLPolicyRequest{ + Http2Enabled: sslPolicy.HTTP2Enabled, + MinVersion: sslPolicy.MinVersion, + CertsJSON: certsJSON, + HstsJSON: hstsJSON, + ClientAuthType: types.Int32(sslPolicy.ClientAuthType), + ClientCACertsJSON: clientCACertsJSON, + CipherSuitesIsOn: sslPolicy.CipherSuitesIsOn, + CipherSuites: sslPolicy.CipherSuites, + }) + if err != nil { + this.ErrorPage(err) + return + } + sslPolicyId = resp.SslPolicyId + } + } + server, _, isOk := serverutils.FindServer(this.Parent(), params.ServerId) if !isOk { return @@ -72,6 +163,10 @@ func (this *IndexAction) RunPost(params struct { } } + httpsConfig.SSLPolicyRef = &sslconfigs.SSLPolicyRef{ + IsOn: true, + SSLPolicyId: sslPolicyId, + } httpsConfig.IsOn = params.IsOn httpsConfig.Listen = addresses configData, err := json.Marshal(httpsConfig) diff --git a/web/public/js/components/server/ssl-config-box.js b/web/public/js/components/server/ssl-config-box.js new file mode 100644 index 00000000..92c7988b --- /dev/null +++ b/web/public/js/components/server/ssl-config-box.js @@ -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)/, "$1") + }, + + // 添加单个套件 + 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: `
| 用HTTP/2 | +
+
+
+
+
+ |
+ |
| 选择证书 | +
+
+
+ 选择或上传证书后HTTPSTLS服务才能生效。
+
+
+
+
+ |
+ |
| TLS最低版本 | ++ + | +|
| 加密算法套件(CipherSuites) | +
+
+
+
+
+
+
+
+
+
+
+
+ 点击可选套件添加。 + |
+ |
| 是否开启HSTS | +
+
+
+
+
+ + 开启后,会自动在响应Header中加入 + Strict-Transport-Security: + ... + max-age={{hsts.maxAge}} + ; includeSubDomains + ; preload + + + 修改 + + + |
+ |
| HSTS包含子域名(includeSubDomains) | +
+
+
+
+
+ |
+ |
| HSTS预加载(preload) | +
+
+
+
+
+ |
+ |
| HSTS生效的域名 | +
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+
+ 如果没有设置域名的话,则默认支持所有的域名。 + |
+ |
| 客户端认证方式 | ++ + | +|
| 客户端认证CA证书 | +
+
+
+
+ 用来校验客户端证书以增强安全性,通常不需要设置。 + |
+ |
暂时还没有相关的证书。
+| 证书说明 | +顶级发行组织 | +域名 | +过期日期 | +引用服务 | +状态 | +操作 | +
|---|---|---|---|---|---|---|
| {{cert.name}}
+
+ CA
+
+ |
+ + {{cert.commonNames[cert.commonNames.length-1]}} + | +
+
+ {{dnsName}}
+
+ |
+ {{certInfos[index].endDay}} | +{{certInfos[index].countServers}} | ++ 已过期 + 有效中 + | ++ 选择 + | +
提醒:HTTP2、证书等信息修改后,可能需要清空浏览器缓存后才能浏览效果。