diff --git a/internal/web/actions/default/servers/certs/init.go b/internal/web/actions/default/servers/certs/init.go index fe8c6c7b..89739b55 100644 --- a/internal/web/actions/default/servers/certs/init.go +++ b/internal/web/actions/default/servers/certs/init.go @@ -21,6 +21,7 @@ func init() { Data("leftMenuItem", "cert"). Get("", new(IndexAction)). GetPost("/uploadPopup", new(UploadPopupAction)). + GetPost("/uploadBatchPopup", new(UploadBatchPopupAction)). Post("/delete", new(DeleteAction)). GetPost("/updatePopup", new(UpdatePopupAction)). Get("/certPopup", new(CertPopupAction)). diff --git a/internal/web/actions/default/servers/certs/uploadBatchPopup.go b/internal/web/actions/default/servers/certs/uploadBatchPopup.go new file mode 100644 index 00000000..b0c4b1a0 --- /dev/null +++ b/internal/web/actions/default/servers/certs/uploadBatchPopup.go @@ -0,0 +1,171 @@ +// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn . + +package certs + +import ( + "bytes" + "crypto/tls" + "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/actions" + "github.com/iwind/TeaGo/types" + "io" + "mime/multipart" + "strings" +) + +type UploadBatchPopupAction struct { + actionutils.ParentAction +} + +func (this *UploadBatchPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *UploadBatchPopupAction) RunGet(params struct{}) { + this.Show() +} + +func (this *UploadBatchPopupAction) RunPost(params struct { + UserId int64 + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + defer this.CreateLogInfo("批量上传证书") + + var files = this.Request.MultipartForm.File["certFiles"] + if len(files) == 0 { + this.Fail("请选择要上传的证书和私钥文件") + return + } + + // 限制每次上传的文件数量 + const maxFiles = 10_000 + if len(files) > maxFiles { + this.Fail("每次上传最多不能超过" + types.String(maxFiles) + "个文件") + return + } + + type certInfo struct { + filename string + data []byte + } + + var certDataList = []*certInfo{} + var keyDataList = [][]byte{} + + var failMessages = []string{} + for _, file := range files { + func(file *multipart.FileHeader) { + fp, err := file.Open() + if err != nil { + failMessages = append(failMessages, "文件"+file.Filename+"读取失败:"+err.Error()) + return + } + + defer func() { + _ = fp.Close() + }() + + data, err := io.ReadAll(fp) + if err != nil { + failMessages = append(failMessages, "文件"+file.Filename+"读取失败:"+err.Error()) + return + } + + if bytes.Contains(data, []byte("CERTIFICATE-")) { + certDataList = append(certDataList, &certInfo{ + filename: file.Filename, + data: data, + }) + } else if bytes.Contains(data, []byte("PRIVATE KEY-")) { + keyDataList = append(keyDataList, data) + } else { + failMessages = append(failMessages, "文件"+file.Filename+"读取失败:文件格式错误,无法识别是证书还是私钥") + return + } + }(file) + } + + if len(failMessages) > 0 { + this.Fail("发生了错误:" + strings.Join(failMessages, ";")) + return + } + + // 自动匹配 + var pairs = [][2][]byte{} // [] { cert, key } + var keyIndexMap = map[int]bool{} // 方便下面跳过已匹配的Key + for _, cert := range certDataList { + var found = false + for keyIndex, keyData := range keyDataList { + if keyIndexMap[keyIndex] { + continue + } + + _, err := tls.X509KeyPair(cert.data, keyData) + if err == nil { + found = true + pairs = append(pairs, [2][]byte{cert.data, keyData}) + keyIndexMap[keyIndex] = true + break + } + } + if !found { + this.Fail("找不到" + cert.filename + "对应的私钥") + return + } + } + + // 组织 CertConfig + var pbCerts = []*pb.CreateSSLCertsRequestCert{} + for _, pair := range pairs { + certData, keyData := pair[0], pair[1] + + var certConfig = &sslconfigs.SSLCertConfig{ + IsCA: false, + CertData: certData, + KeyData: keyData, + } + err := certConfig.Init(nil) + if err != nil { + this.Fail("证书验证失败:" + err.Error()) + return + } + + var certName = "" + if len(certConfig.DNSNames) > 0 { + certName = certConfig.DNSNames[0] + if len(certConfig.DNSNames) > 1 { + certName += "等" + types.String(len(certConfig.DNSNames)) + "个域名" + } + } + + pbCerts = append(pbCerts, &pb.CreateSSLCertsRequestCert{ + IsOn: true, + Name: certName, + Description: "", + ServerName: "", + IsCA: false, + CertData: certData, + KeyData: keyData, + TimeBeginAt: certConfig.TimeBeginAt, + TimeEndAt: certConfig.TimeEndAt, + DnsNames: certConfig.DNSNames, + CommonNames: certConfig.CommonNames, + }) + } + + _, err := this.RPC().SSLCertRPC().CreateSSLCerts(this.AdminContext(), &pb.CreateSSLCertsRequest{ + UserId: params.UserId, + SSLCerts: pbCerts, + }) + if err != nil { + this.ErrorPage(err) + return + } + + this.Data["count"] = len(pbCerts) + this.Success() +} diff --git a/web/views/@default/servers/certs/index.html b/web/views/@default/servers/certs/index.html index b9d1d2c8..0d1ce6a5 100644 --- a/web/views/@default/servers/certs/index.html +++ b/web/views/@default/servers/certs/index.html @@ -11,6 +11,7 @@ 30天内过期({{count30Days}}) | [上传证书] + [批量上传]
diff --git a/web/views/@default/servers/certs/index.js b/web/views/@default/servers/certs/index.js index df2603fe..0358e502 100644 --- a/web/views/@default/servers/certs/index.js +++ b/web/views/@default/servers/certs/index.js @@ -11,6 +11,16 @@ Tea.context(function () { }) } + // 批量上传证书 + this.uploadBatch = function () { + teaweb.popup("/servers/certs/uploadBatchPopup", { + height: "30em", + callback: function () { + window.location.reload() + } + }) + } + // 删除证书 this.deleteCert = function (certId) { let that = this diff --git a/web/views/@default/servers/certs/updatePopup.html b/web/views/@default/servers/certs/updatePopup.html index 10561801..6f7df21c 100644 --- a/web/views/@default/servers/certs/updatePopup.html +++ b/web/views/@default/servers/certs/updatePopup.html @@ -25,7 +25,7 @@ 输入证书内容选择证书文件 - +

[输入内容上传文件]。文件内容中通常含有"-----BEGIN CERTIFICATE-----"类似的信息,可以直接拖动证书文件到输入框,留空表示不修改

@@ -33,7 +33,7 @@ 输入私钥内容选择私钥文件 - +

[输入内容上传文件]。文件内容中通常含有"-----BEGIN RSA PRIVATE KEY-----"类似的信息,可以直接拖动证书文件到输入框,留空表示不修改

diff --git a/web/views/@default/servers/certs/uploadBatchPopup.html b/web/views/@default/servers/certs/uploadBatchPopup.html new file mode 100644 index 00000000..e89caa0f --- /dev/null +++ b/web/views/@default/servers/certs/uploadBatchPopup.html @@ -0,0 +1,25 @@ +{$layout "layout_popup"} + +

批量上传证书

+ + + + + + + + + + + + +
选择要上传的证书和私钥文件 * + +

点击后在弹出的文件选择框中支持多选。

+
所属用户 + +

可选项,指定证书所属的用户;指定用户后,上传的证书管理员无法在管理系统查看,只能在用户系统查看。

+
+ 上传 + +
\ No newline at end of file diff --git a/web/views/@default/servers/certs/uploadBatchPopup.js b/web/views/@default/servers/certs/uploadBatchPopup.js new file mode 100644 index 00000000..cbe031f3 --- /dev/null +++ b/web/views/@default/servers/certs/uploadBatchPopup.js @@ -0,0 +1,30 @@ +Tea.context(function () { + this.isRequesting = false + this.userId = 0 + + this.before = function () { + this.isRequesting = true + } + + this.done = function () { + this.isRequesting = false + } + + this.successUpload = function (resp) { + if (resp.data.count > 0) { + let msg = "html:成功上传" + resp.data.count + "个证书" + if (this.userId > 0) { + msg += "
由于你选择了证书用户,所以只有此用户才能在用户系统中查看到这些证书。" + } + teaweb.success(msg, function () { + NotifyPopup(resp) + }) + return + } + NotifyPopup(resp) + } + + this.changeUserId = function (userId) { + this.userId = userId + } +}) \ No newline at end of file diff --git a/web/views/@default/servers/certs/uploadPopup.html b/web/views/@default/servers/certs/uploadPopup.html index 660a127c..c745ffba 100644 --- a/web/views/@default/servers/certs/uploadPopup.html +++ b/web/views/@default/servers/certs/uploadPopup.html @@ -24,7 +24,7 @@ 输入证书内容选择证书文件 * - +

[输入内容上传文件]。文件内容中通常含有"-----BEGIN CERTIFICATE-----"类似的信息,可以直接拖动证书文件到输入框

@@ -32,7 +32,7 @@ 输入私钥内容选择私钥文件 * - +

[输入内容上传文件]。文件内容中通常含有"-----BEGIN RSA PRIVATE KEY-----"类似的信息,可以直接拖动私钥文件到输入框