2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 11:27:17 +08:00

feat: 添加面板HTTPS接口

This commit is contained in:
耗子
2024-06-23 02:35:25 +08:00
parent 0d769d3400
commit 55eaa26970
11 changed files with 409 additions and 104 deletions

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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

View File

@@ -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("获取私钥失败")
}

View File

@@ -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 {

View File

@@ -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
}

89
pkg/cert/cert.go Normal file
View File

@@ -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
}

View File

@@ -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()