From 55eaa269708af2ff69a0a9d83ba59b1e48c529d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sun, 23 Jun 2024 02:35:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=9D=A2=E6=9D=BFHTT?= =?UTF-8?q?PS=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/http/controllers/setting_controller.go | 101 ++++++++++++++++----- app/http/requests/setting/https.go | 36 ++++++++ app/http/requests/setting/update.go | 2 - docs/docs.go | 79 +++++++++++++++- docs/swagger.json | 79 +++++++++++++++- docs/swagger.yaml | 49 +++++++++- internal/services/cert.go | 5 +- pkg/acme/acme.go | 67 +------------- pkg/acme/client.go | 4 +- pkg/cert/cert.go | 89 ++++++++++++++++++ routes/api.go | 2 + 11 files changed, 409 insertions(+), 104 deletions(-) create mode 100644 app/http/requests/setting/https.go create mode 100644 pkg/cert/cert.go diff --git a/app/http/controllers/setting_controller.go b/app/http/controllers/setting_controller.go index ca22498b..78454a18 100644 --- a/app/http/controllers/setting_controller.go +++ b/app/http/controllers/setting_controller.go @@ -9,6 +9,7 @@ import ( "github.com/TheTNB/panel/app/models" "github.com/TheTNB/panel/internal" "github.com/TheTNB/panel/internal/services" + "github.com/TheTNB/panel/pkg/cert" "github.com/TheTNB/panel/pkg/io" "github.com/TheTNB/panel/pkg/os" "github.com/TheTNB/panel/pkg/shell" @@ -27,13 +28,12 @@ func NewSettingController() *SettingController { // List // -// @Summary 设置列表 -// @Description 获取面板设置列表 -// @Tags 面板设置 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/list [get] +// @Summary 设置列表 +// @Tags 面板设置 +// @Produce json +// @Security BearerToken +// @Success 200 {object} SuccessResponse +// @Router /panel/setting/list [get] func (r *SettingController) List(ctx http.Context) http.Response { var settings []models.Setting err := facades.Orm().Query().Get(&settings) @@ -77,15 +77,14 @@ func (r *SettingController) List(ctx http.Context) http.Response { // Update // -// @Summary 更新设置 -// @Description 更新面板设置 -// @Tags 面板设置 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Update true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/update [post] +// @Summary 更新设置 +// @Tags 面板设置 +// @Accept json +// @Produce json +// @Security BearerToken +// @Param data body requests.Update true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/setting/update [post] func (r *SettingController) Update(ctx http.Context) http.Response { var updateRequest requests.Update sanitize := SanitizeRequest(ctx, &updateRequest) @@ -213,7 +212,70 @@ func (r *SettingController) Update(ctx http.Context) http.Response { } } - if updateRequest.SSL { + if oldPort != port || oldEntrance != entrance || oldLanguage != updateRequest.Language { + tools.RestartPanel() + } + + return Success(ctx, nil) +} + +// GetHttps +// +// @Summary 获取面板 HTTPS 设置 +// @Tags 面板设置 +// @Produce json +// @Security BearerToken +// @Success 200 {object} SuccessResponse +// @Router /panel/setting/https [get] +func (r *SettingController) GetHttps(ctx http.Context) http.Response { + certPath := facades.Config().GetString("http.tls.ssl.cert") + keyPath := facades.Config().GetString("http.tls.ssl.key") + crt, err := io.Read(certPath) + if err != nil { + return ErrorSystem(ctx) + } + key, err := io.Read(keyPath) + if err != nil { + return ErrorSystem(ctx) + } + + return Success(ctx, http.Json{ + "https": facades.Config().GetBool("panel.ssl"), + "cert": crt, + "key": key, + }) +} + +// UpdateHttps +// +// @Summary 更新面板 HTTPS 设置 +// @Tags 面板设置 +// @Accept json +// @Produce json +// @Security BearerToken +// @Param data body requests.Https true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/setting/https [post] +func (r *SettingController) UpdateHttps(ctx http.Context) http.Response { + var httpsRequest requests.Https + sanitize := SanitizeRequest(ctx, &httpsRequest) + if sanitize != nil { + return sanitize + } + + if httpsRequest.Https { + if _, err := cert.ParseCert(httpsRequest.Cert); err != nil { + return Error(ctx, http.StatusBadRequest, "证书格式错误") + } + if _, err := cert.ParseKey(httpsRequest.Key); err != nil { + return Error(ctx, http.StatusBadRequest, "密钥格式错误") + } + if err := io.Write(facades.App().ExecutablePath("storage/ssl.crt"), httpsRequest.Cert, 0700); err != nil { + return ErrorSystem(ctx) + } + if err := io.Write(facades.App().ExecutablePath("storage/ssl.key"), httpsRequest.Key, 0700); err != nil { + return ErrorSystem(ctx) + } if out, err := shell.Execf("sed -i 's/APP_SSL=false/APP_SSL=true/g' /www/panel/panel.conf"); err != nil { return Error(ctx, http.StatusInternalServerError, out) } @@ -223,9 +285,6 @@ func (r *SettingController) Update(ctx http.Context) http.Response { } } - if oldPort != port || oldEntrance != entrance || oldLanguage != updateRequest.Language || updateRequest.SSL != facades.Config().GetBool("panel.ssl") { - tools.RestartPanel() - } - + tools.RestartPanel() return Success(ctx, nil) } diff --git a/app/http/requests/setting/https.go b/app/http/requests/setting/https.go new file mode 100644 index 00000000..64052010 --- /dev/null +++ b/app/http/requests/setting/https.go @@ -0,0 +1,36 @@ +package requests + +import ( + "github.com/goravel/framework/contracts/http" + "github.com/goravel/framework/contracts/validation" +) + +type Https struct { + Https bool `form:"https" json:"https"` + Cert string `form:"cert" json:"cert"` + Key string `form:"key" json:"key"` +} + +func (r *Https) Authorize(ctx http.Context) error { + return nil +} + +func (r *Https) Rules(ctx http.Context) map[string]string { + return map[string]string{ + "https": "bool", + "cert": "string", + "key": "string", + } +} + +func (r *Https) Messages(ctx http.Context) map[string]string { + return map[string]string{} +} + +func (r *Https) Attributes(ctx http.Context) map[string]string { + return map[string]string{} +} + +func (r *Https) PrepareForValidation(ctx http.Context, data validation.Data) error { + return nil +} diff --git a/app/http/requests/setting/update.go b/app/http/requests/setting/update.go index 3455f372..c4506e88 100644 --- a/app/http/requests/setting/update.go +++ b/app/http/requests/setting/update.go @@ -12,7 +12,6 @@ type Update struct { BackupPath string `form:"backup_path" json:"backup_path"` WebsitePath string `form:"website_path" json:"website_path"` Entrance string `form:"entrance" json:"entrance"` - SSL bool `form:"ssl" json:"ssl"` UserName string `form:"username" json:"username"` Email string `form:"email" json:"email"` Password string `form:"password" json:"password"` @@ -30,7 +29,6 @@ func (r *Update) Rules(ctx http.Context) map[string]string { "backup_path": "required|string:2,255", "website_path": "required|string:2,255", "entrance": `required|regex:^/(\w+)?$|not_in:/api`, - "ssl": "bool", "username": "required|string:2,20", "email": "required|email", "password": "string:8,255", diff --git a/docs/docs.go b/docs/docs.go index e671490e..43fadca0 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2830,6 +2830,66 @@ const docTemplate = `{ } } }, + "/panel/setting/https": { + "get": { + "security": [ + { + "BearerToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "面板设置" + ], + "summary": "获取面板 HTTPS 设置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.SuccessResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "面板设置" + ], + "summary": "更新面板 HTTPS 设置", + "parameters": [ + { + "description": "request", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.Https" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.SuccessResponse" + } + } + } + } + }, "/panel/setting/list": { "get": { "security": [ @@ -2837,7 +2897,6 @@ const docTemplate = `{ "BearerToken": [] } ], - "description": "获取面板设置列表", "produces": [ "application/json" ], @@ -2862,7 +2921,6 @@ const docTemplate = `{ "BearerToken": [] } ], - "description": "更新面板设置", "consumes": [ "application/json" ], @@ -5006,9 +5064,6 @@ const docTemplate = `{ "port": { "type": "integer" }, - "ssl": { - "type": "boolean" - }, "username": { "type": "string" }, @@ -5467,6 +5522,20 @@ const docTemplate = `{ } } }, + "requests.Https": { + "type": "object", + "properties": { + "cert": { + "type": "string" + }, + "https": { + "type": "boolean" + }, + "key": { + "type": "string" + } + } + }, "requests.ImagePull": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 65f6db35..d22a1f4b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2823,6 +2823,66 @@ } } }, + "/panel/setting/https": { + "get": { + "security": [ + { + "BearerToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "面板设置" + ], + "summary": "获取面板 HTTPS 设置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.SuccessResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "面板设置" + ], + "summary": "更新面板 HTTPS 设置", + "parameters": [ + { + "description": "request", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/requests.Https" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.SuccessResponse" + } + } + } + } + }, "/panel/setting/list": { "get": { "security": [ @@ -2830,7 +2890,6 @@ "BearerToken": [] } ], - "description": "获取面板设置列表", "produces": [ "application/json" ], @@ -2855,7 +2914,6 @@ "BearerToken": [] } ], - "description": "更新面板设置", "consumes": [ "application/json" ], @@ -4999,9 +5057,6 @@ "port": { "type": "integer" }, - "ssl": { - "type": "boolean" - }, "username": { "type": "string" }, @@ -5460,6 +5515,20 @@ } } }, + "requests.Https": { + "type": "object", + "properties": { + "cert": { + "type": "string" + }, + "https": { + "type": "boolean" + }, + "key": { + "type": "string" + } + } + }, "requests.ImagePull": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5a0c99c5..0750a128 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -84,8 +84,6 @@ definitions: type: string port: type: integer - ssl: - type: boolean username: type: string website_path: @@ -389,6 +387,15 @@ definitions: path: type: string type: object + requests.Https: + properties: + cert: + type: string + https: + type: boolean + key: + type: string + type: object requests.ImagePull: properties: auth: @@ -2384,9 +2391,44 @@ paths: summary: 更新插件首页显示状态 tags: - 插件 + /panel/setting/https: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controllers.SuccessResponse' + security: + - BearerToken: [] + summary: 获取面板 HTTPS 设置 + tags: + - 面板设置 + post: + consumes: + - application/json + parameters: + - description: request + in: body + name: data + required: true + schema: + $ref: '#/definitions/requests.Https' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/controllers.SuccessResponse' + security: + - BearerToken: [] + summary: 更新面板 HTTPS 设置 + tags: + - 面板设置 /panel/setting/list: get: - description: 获取面板设置列表 produces: - application/json responses: @@ -2403,7 +2445,6 @@ paths: post: consumes: - application/json - description: 更新面板设置 parameters: - description: request in: body diff --git a/internal/services/cert.go b/internal/services/cert.go index a003a34c..c431db0f 100644 --- a/internal/services/cert.go +++ b/internal/services/cert.go @@ -12,6 +12,7 @@ import ( requests "github.com/TheTNB/panel/app/http/requests/cert" "github.com/TheTNB/panel/app/models" "github.com/TheTNB/panel/pkg/acme" + "github.com/TheTNB/panel/pkg/cert" "github.com/TheTNB/panel/pkg/io" "github.com/TheTNB/panel/pkg/systemctl" ) @@ -54,7 +55,7 @@ func (s *CertImpl) UserStore(request requests.UserStore) error { return errors.New("向 CA 注册账号失败,请检查参数是否正确") } - privateKey, err := acme.EncodePrivateKey(client.Account.PrivateKey) + privateKey, err := cert.EncodeKey(client.Account.PrivateKey) if err != nil { return errors.New("获取私钥失败") } @@ -97,7 +98,7 @@ func (s *CertImpl) UserUpdate(request requests.UserUpdate) error { return errors.New("向 CA 注册账号失败,请检查参数是否正确") } - privateKey, err := acme.EncodePrivateKey(client.Account.PrivateKey) + privateKey, err := cert.EncodeKey(client.Account.PrivateKey) if err != nil { return errors.New("获取私钥失败") } diff --git a/pkg/acme/acme.go b/pkg/acme/acme.go index 9bd81071..46be6d2c 100644 --- a/pkg/acme/acme.go +++ b/pkg/acme/acme.go @@ -4,20 +4,17 @@ import ( "context" "crypto" "crypto/ecdsa" - "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" - "crypto/x509" - "encoding/pem" "errors" - "fmt" "net/http" - "strings" "github.com/mholt/acmez/v2" "github.com/mholt/acmez/v2/acme" "go.uber.org/zap" + + "github.com/TheTNB/panel/pkg/cert" ) const ( @@ -77,7 +74,7 @@ func NewPrivateKeyAccount(email string, privateKey string, CA string, eab *EAB) return nil, err } - key, err := parsePrivateKey([]byte(privateKey)) + key, err := cert.ParseKey(privateKey) if err != nil { return nil, err } @@ -102,36 +99,6 @@ func NewPrivateKeyAccount(email string, privateKey string, CA string, eab *EAB) return &Client{Account: account, zClient: client}, nil } -func parsePrivateKey(key []byte) (crypto.Signer, error) { - keyBlockDER, _ := pem.Decode(key) - if keyBlockDER == nil { - return nil, errors.New("invalid PEM block") - } - - if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") { - return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type) - } - - if parse, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil { - return parse, nil - } - - if parse, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil { - switch parse.(type) { - case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: - return parse.(crypto.Signer), nil - default: - return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key) - } - } - - if parse, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil { - return parse, nil - } - - return nil, errors.New("解析私钥失败") -} - func generatePrivateKey(keyType KeyType) (crypto.Signer, error) { switch keyType { case KeyEC256: @@ -149,34 +116,6 @@ func generatePrivateKey(keyType KeyType) (crypto.Signer, error) { return nil, errors.New("未知的密钥类型") } -func EncodePrivateKey(key crypto.Signer) ([]byte, error) { - var pemType string - var keyBytes []byte - switch key := key.(type) { - case *ecdsa.PrivateKey: - var err error - pemType = "EC" - keyBytes, err = x509.MarshalECPrivateKey(key) - if err != nil { - return nil, err - } - case *rsa.PrivateKey: - pemType = "RSA" - keyBytes = x509.MarshalPKCS1PrivateKey(key) - case ed25519.PrivateKey: - var err error - pemType = "ED25519" - keyBytes, err = x509.MarshalPKCS8PrivateKey(key) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("未知的密钥类型 %T", key) - } - pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} - return pem.EncodeToMemory(&pemKey), nil -} - func getClient(CA string) (acmez.Client, error) { logger, err := zap.NewProduction() if err != nil { diff --git a/pkg/acme/client.go b/pkg/acme/client.go index 462c860e..8962c724 100644 --- a/pkg/acme/client.go +++ b/pkg/acme/client.go @@ -7,6 +7,8 @@ import ( "github.com/libdns/libdns" "github.com/mholt/acmez/v2" "github.com/mholt/acmez/v2/acme" + + "github.com/TheTNB/panel/pkg/cert" ) type Certificate struct { @@ -61,7 +63,7 @@ func (c *Client) ObtainSSL(ctx context.Context, domains []string, keyType KeyTyp if err != nil { return Certificate{}, err } - pemPrivateKey, err := EncodePrivateKey(certPrivateKey) + pemPrivateKey, err := cert.EncodeKey(certPrivateKey) if err != nil { return Certificate{}, err } diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go new file mode 100644 index 00000000..3ba8c764 --- /dev/null +++ b/pkg/cert/cert.go @@ -0,0 +1,89 @@ +package cert + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "strings" +) + +func ParseCert(crt string) (x509.Certificate, error) { + certBlock, _ := pem.Decode([]byte(crt)) + if certBlock == nil { + return x509.Certificate{}, errors.New("invalid PEM block") + } + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return x509.Certificate{}, err + } + + return *cert, nil +} + +func ParseKey(key string) (crypto.Signer, error) { + keyBlockDER, _ := pem.Decode([]byte(key)) + if keyBlockDER == nil { + return nil, errors.New("invalid PEM block") + } + + if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") { + return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type) + } + + if parse, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil { + return parse, nil + } + + if parse, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil { + switch parse.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: + return parse.(crypto.Signer), nil + default: + return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key) + } + } + + if parse, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil { + return parse, nil + } + + return nil, errors.New("解析私钥失败") +} + +func EncodeCert(cert x509.Certificate) ([]byte, error) { + pemCert := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} + return pem.EncodeToMemory(&pemCert), nil +} + +func EncodeKey(key crypto.Signer) ([]byte, error) { + var pemType string + var keyBytes []byte + switch key := key.(type) { + case *ecdsa.PrivateKey: + var err error + pemType = "EC" + keyBytes, err = x509.MarshalECPrivateKey(key) + if err != nil { + return nil, err + } + case *rsa.PrivateKey: + pemType = "RSA" + keyBytes = x509.MarshalPKCS1PrivateKey(key) + case ed25519.PrivateKey: + var err error + pemType = "ED25519" + keyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("未知的密钥类型 %T", key) + } + pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} + return pem.EncodeToMemory(&pemKey), nil +} diff --git a/routes/api.go b/routes/api.go index ff654e57..16a85614 100644 --- a/routes/api.go +++ b/routes/api.go @@ -199,6 +199,8 @@ func Api() { settingController := controllers.NewSettingController() r.Get("list", settingController.List) r.Post("update", settingController.Update) + r.Get("https", settingController.GetHttps) + r.Post("https", settingController.UpdateHttps) }) r.Prefix("system").Middleware(middleware.Jwt()).Group(func(r route.Router) { controller := controllers.NewSystemController()