diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index 5c09a500f..3d1714de1 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -119,6 +119,48 @@ func (b *BaseApi) UpdatePassword(c *gin.Context) { helper.SuccessWithData(c, nil) } +// @Tags System Setting +// @Summary Update system ssl +// @Description 修改系统 ssl 登录 +// @Accept json +// @Param request body dto.SSLUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/ssl/update [post] +// @x-panel-log {"bodyKeys":[ssl],"paramKeys":[],"BeforeFuntions":[],"formatZH":"修改系统 ssl => [ssl]","formatEN":"update system ssl => [ssl]"} +func (b *BaseApi) UpdateSSL(c *gin.Context) { + var req dto.SSLUpdate + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := settingService.UpdateSSL(c, req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags System Setting +// @Summary Load system cert info +// @Description 获取证书信息 +// @Success 200 {object} dto.SettingInfo +// @Security ApiKeyAuth +// @Router /settings/ssl/info [get] +func (b *BaseApi) LoadFromCert(c *gin.Context) { + info, err := settingService.LoadFromCert() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, info) +} + // @Tags System Setting // @Summary Update system port // @Description 更新系统端口 diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index e4ad1fa4d..c3483bf78 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -17,6 +17,8 @@ type SettingInfo struct { ServerPort string `json:"serverPort"` SecurityEntranceStatus string `json:"securityEntranceStatus"` + SSL string `json:"ssl"` + SSLType string `json:"sslType"` SecurityEntrance string `json:"securityEntrance"` ExpirationDays string `json:"expirationDays"` ExpirationTime string `json:"expirationTime"` @@ -40,6 +42,23 @@ type SettingUpdate struct { Value string `json:"value"` } +type SSLUpdate struct { + SSLType string `json:"sslType"` + Domain string `json:"domain"` + SSL string `json:"ssl" validate:"required,oneof=enable disable"` + Cert string `json:"cert"` + Key string `json:"key"` + SSLID uint `json:"sslID"` +} +type SSLInfo struct { + Domain string `json:"domain"` + Timeout string `json:"timeout"` + RootPath string `json:"rootPath"` + Cert string `json:"cert"` + Key string `json:"key"` + SSLID uint `json:"sslID"` +} + type PasswordUpdate struct { OldPassword string `json:"oldPassword" validate:"required"` NewPassword string `json:"newPassword" validate:"required"` diff --git a/backend/app/service/setting.go b/backend/app/service/setting.go index e645787ec..ee1a4cb9f 100644 --- a/backend/app/service/setting.go +++ b/backend/app/service/setting.go @@ -1,8 +1,14 @@ package service import ( + "crypto/tls" + "crypto/x509" "encoding/json" + "encoding/pem" + "fmt" + "os" "strconv" + "strings" "time" "github.com/1Panel-dev/1Panel/backend/app/dto" @@ -12,7 +18,9 @@ import ( "github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/encrypt" + "github.com/1Panel-dev/1Panel/backend/utils/ssl" "github.com/gin-gonic/gin" + "github.com/pkg/errors" ) type SettingService struct{} @@ -23,6 +31,8 @@ type ISettingService interface { UpdateEntrance(value string) error UpdatePassword(c *gin.Context, old, new string) error UpdatePort(port uint) error + UpdateSSL(c *gin.Context, req dto.SSLUpdate) error + LoadFromCert() (*dto.SSLInfo, error) HandlePasswordExpired(c *gin.Context, old, new string) error } @@ -111,6 +121,126 @@ func (u *SettingService) UpdatePort(port uint) error { return nil } +func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error { + if req.SSL == "disable" { + if err := settingRepo.Update("SSL", "disable"); err != nil { + return err + } + if err := settingRepo.Update("SSLType", "self"); err != nil { + return err + } + _ = os.Remove(global.CONF.System.BaseDir + "/1panel/secret/server.crt") + _ = os.Remove(global.CONF.System.BaseDir + "/1panel/secret/server.key") + go func() { + _, err := cmd.Exec("systemctl restart 1panel.service") + if err != nil { + global.LOG.Errorf("restart system failed, err: %v", err) + } + }() + return nil + } + + if _, err := os.Stat(global.CONF.System.BaseDir + "/1panel/secret"); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(global.CONF.System.BaseDir+"/1panel/secret", os.ModePerm); err != nil { + return err + } + } + if err := settingRepo.Update("SSLType", req.SSLType); err != nil { + return err + } + if req.SSLType == "self" { + if len(req.Domain) == 0 { + return fmt.Errorf("load domain failed") + } + if err := ssl.GenerateSSL(req.Domain); err != nil { + return err + } + } + if req.SSLType == "select" { + sslInfo, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID)) + if err != nil { + return err + } + req.Cert = sslInfo.Pem + req.Key = sslInfo.PrivateKey + req.SSLType = "import" + if err := settingRepo.Update("SSLID", strconv.Itoa(int(req.SSLID))); err != nil { + return err + } + } + if req.SSLType == "import" { + cert, err := os.OpenFile("/opt/1panel/secret/server.crt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer cert.Close() + if _, err := cert.WriteString(req.Cert); err != nil { + return err + } + key, err := os.OpenFile("/opt/1panel/secret/server.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + if _, err := key.WriteString(req.Key); err != nil { + return err + } + defer key.Close() + } + if err := checkCertValid(req.Domain); err != nil { + return err + } + if err := settingRepo.Update("SSL", req.SSL); err != nil { + return err + } + go func() { + _, err := cmd.Exec("systemctl restart 1panel.service") + if err != nil { + global.LOG.Errorf("restart system failed, err: %v", err) + } + }() + return nil +} + +func (u *SettingService) LoadFromCert() (*dto.SSLInfo, error) { + ssl, err := settingRepo.Get(settingRepo.WithByKey("SSL")) + if err != nil { + return nil, err + } + if ssl.Value == "disable" { + return &dto.SSLInfo{}, nil + } + sslType, err := settingRepo.Get(settingRepo.WithByKey("SSLType")) + if err != nil { + return nil, err + } + data, err := loadInfoFromCert() + if err != nil { + return nil, err + } + switch sslType.Value { + case "import": + if _, err := os.Stat(global.CONF.System.BaseDir + "/1panel/secret/server.crt"); err != nil { + return nil, fmt.Errorf("load server.crt file failed, err: %v", err) + } + certFile, _ := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.crt") + data.Cert = string(certFile) + + if _, err := os.Stat(global.CONF.System.BaseDir + "/1panel/secret/server.key"); err != nil { + return nil, fmt.Errorf("load server.key file failed, err: %v", err) + } + keyFile, _ := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.key") + data.Key = string(keyFile) + case "select": + sslID, err := settingRepo.Get(settingRepo.WithByKey("SSLID")) + if err != nil { + return nil, err + } + id, _ := strconv.Atoi(sslID.Value) + data.SSLID = uint(id) + } + return data, nil +} + func (u *SettingService) HandlePasswordExpired(c *gin.Context, old, new string) error { setting, err := settingRepo.Get(settingRepo.WithByKey("Password")) if err != nil { @@ -149,3 +279,71 @@ func (u *SettingService) UpdatePassword(c *gin.Context, old, new string) error { _ = global.SESSION.Clean() return nil } + +func loadInfoFromCert() (*dto.SSLInfo, error) { + var info dto.SSLInfo + certFile := global.CONF.System.BaseDir + "/1panel/secret/server.crt" + if _, err := os.Stat(certFile); err != nil { + return &info, err + } + certData, err := os.ReadFile(certFile) + if err != nil { + return &info, err + } + certBlock, _ := pem.Decode(certData) + if certBlock == nil { + return &info, err + } + certObj, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return &info, err + } + var domains []string + if len(certObj.IPAddresses) != 0 { + for _, ip := range certObj.IPAddresses { + domains = append(domains, ip.String()) + } + } + if len(certObj.DNSNames) != 0 { + domains = append(domains, certObj.DNSNames...) + } + return &dto.SSLInfo{ + Domain: strings.Join(domains, ","), + Timeout: certObj.NotAfter.Format("2006-01-02 15:04:05"), + RootPath: global.CONF.System.BaseDir + "/1panel/secret/server.crt", + }, nil +} + +func checkCertValid(domain string) error { + certificate, err := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.crt") + if err != nil { + return err + } + key, err := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.key") + if err != nil { + return err + } + if _, err = tls.X509KeyPair(certificate, key); err != nil { + return err + } + certObj, err := x509.ParseCertificate(certificate) + if err != nil { + return err + } + + if len(certObj.IPAddresses) != 0 { + for _, ip := range certObj.IPAddresses { + if ip.String() == domain { + return nil + } + } + } + if len(certObj.DNSNames) != 0 { + for _, ip := range certObj.DNSNames { + if ip == domain { + return nil + } + } + } + return errors.New("The domain name or ip address does not match") +} diff --git a/backend/configs/system.go b/backend/configs/system.go index 2aa26bc7b..02c73ef97 100644 --- a/backend/configs/system.go +++ b/backend/configs/system.go @@ -2,6 +2,7 @@ package configs type System struct { Port string `mapstructure:"port"` + SSL string `mapstructure:"ssl"` DbFile string `mapstructure:"db_file"` DbPath string `mapstructure:"db_path"` LogPath string `mapstructure:"log_path"` diff --git a/backend/init/hook/hook.go b/backend/init/hook/hook.go index c5affaa61..a2fdc989d 100644 --- a/backend/init/hook/hook.go +++ b/backend/init/hook/hook.go @@ -17,6 +17,11 @@ func Init() { global.LOG.Errorf("load service encrypt key from setting failed, err: %v", err) } global.CONF.System.EncryptKey = enptrySetting.Value + sslSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL")) + if err != nil { + global.LOG.Errorf("load service ssl from setting failed, err: %v", err) + } + global.CONF.System.SSL = sslSetting.Value if _, err := settingRepo.Get(settingRepo.WithByKey("SystemStatus")); err != nil { _ = settingRepo.Create("SystemStatus", "Free") diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index b1d79d7b4..992795602 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -25,7 +25,7 @@ func Init() { migrations.UpdateTableApp, migrations.UpdateTableHost, migrations.UpdateTableWebsite, - migrations.AddEntranceStatus, + migrations.AddEntranceAndSSL, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index f6dc843b5..f16517a26 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -288,8 +288,8 @@ var UpdateTableWebsite = &gormigrate.Migration{ }, } -var AddEntranceStatus = &gormigrate.Migration{ - ID: "20230414-add-entrance-status", +var AddEntranceAndSSL = &gormigrate.Migration{ + ID: "20230414-add-entrance-and-ssl", Migrate: func(tx *gorm.DB) error { if err := tx.Create(&model.Setting{Key: "SecurityEntranceStatus", Value: "disable"}).Error; err != nil { return err @@ -297,6 +297,15 @@ var AddEntranceStatus = &gormigrate.Migration{ if err := tx.Model(&model.Setting{}).Where("key = ?", "SecurityEntrance").Updates(map[string]interface{}{"value": ""}).Error; err != nil { return err } + if err := tx.Create(&model.Setting{Key: "SSLType", Value: "self"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "SSLID", Value: "0"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "SSL", Value: "disable"}).Error; err != nil { + return err + } return tx.AutoMigrate(&model.Website{}) }, } diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index cb2220c87..2ca54f689 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -24,6 +24,8 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) { settingRouter.POST("/update", baseApi.UpdateSetting) settingRouter.POST("/entrance/enable", baseApi.UpdateEntrance) settingRouter.POST("/port/update", baseApi.UpdatePort) + settingRouter.POST("/ssl/update", baseApi.UpdateSSL) + settingRouter.GET("/ssl/info", baseApi.LoadFromCert) settingRouter.POST("/password/update", baseApi.UpdatePassword) settingRouter.POST("/time/sync", baseApi.SyncTime) settingRouter.POST("/monitor/clean", baseApi.CleanMonitor) diff --git a/backend/server/server.go b/backend/server/server.go index c46beebcd..032a06a3b 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -1,8 +1,11 @@ package server import ( + "crypto/tls" "encoding/gob" "fmt" + "net/http" + "os" "time" "github.com/1Panel-dev/1Panel/backend/init/app" @@ -43,22 +46,42 @@ func Start() { rootRouter := router.Routers() address := fmt.Sprintf(":%s", global.CONF.System.Port) - s := initServer(address, rootRouter) - global.LOG.Infof("server run success on %s", global.CONF.System.Port) - if err := s.ListenAndServe(); err != nil { - global.LOG.Error(err) - panic(err) - } -} - -type server interface { - ListenAndServe() error -} - -func initServer(address string, router *gin.Engine) server { - s := endless.NewServer(address, router) + s := endless.NewServer(address, rootRouter) s.ReadHeaderTimeout = 20 * time.Second s.WriteTimeout = 60 * time.Second s.MaxHeaderBytes = 1 << 20 - return s + + if global.CONF.System.SSL == "disable" { + global.LOG.Infof("server run success on %s with http", global.CONF.System.Port) + if err := s.ListenAndServe(); err != nil { + global.LOG.Error(err) + panic(err) + } + } else { + certificate, err := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.crt") + if err != nil { + panic(err) + } + key, err := os.ReadFile(global.CONF.System.BaseDir + "/1panel/secret/server.key") + if err != nil { + panic(err) + } + cert, err := tls.X509KeyPair(certificate, key) + if err != nil { + panic(err) + } + s := &http.Server{ + Addr: address, + Handler: rootRouter, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + } + + global.LOG.Infof("server run success on %s with https", global.CONF.System.Port) + if err := s.ListenAndServeTLS("", ""); err != nil { + global.LOG.Error(err) + panic(err) + } + } } diff --git a/backend/utils/ssl/ssl.go b/backend/utils/ssl/ssl.go new file mode 100644 index 000000000..34f59792d --- /dev/null +++ b/backend/utils/ssl/ssl.go @@ -0,0 +1,112 @@ +package ssl + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "os" + "time" + + "github.com/1Panel-dev/1Panel/backend/global" +) + +func GenerateSSL(domain string) error { + rootPrivateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + ipItem := net.ParseIP(domain) + isIP := false + if len(ipItem) != 0 { + isIP = true + } + + rootTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "1Panel Root CA"}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + BasicConstraintsValid: true, + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + } + if isIP { + rootTemplate.IPAddresses = []net.IP{ipItem} + } else { + rootTemplate.DNSNames = []string{domain} + } + + rootCertBytes, _ := x509.CreateCertificate(rand.Reader, &rootTemplate, &rootTemplate, &rootPrivateKey.PublicKey, rootPrivateKey) + rootCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: rootCertBytes, + } + + interPrivateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + interTemplate := x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "1Panel Intermediate CA"}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + BasicConstraintsValid: true, + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + } + if isIP { + interTemplate.IPAddresses = []net.IP{ipItem} + } else { + interTemplate.DNSNames = []string{domain} + } + + interCertBytes, _ := x509.CreateCertificate(rand.Reader, &interTemplate, &rootTemplate, &interPrivateKey.PublicKey, rootPrivateKey) + interCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: interCertBytes, + } + + clientPrivateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + clientTemplate := x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{CommonName: domain}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + if isIP { + clientTemplate.IPAddresses = []net.IP{ipItem} + } else { + clientTemplate.DNSNames = []string{domain} + } + + clientCertBytes, _ := x509.CreateCertificate(rand.Reader, &clientTemplate, &interTemplate, &clientPrivateKey.PublicKey, interPrivateKey) + clientCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: clientCertBytes, + } + + pemBytes := []byte{} + pemBytes = append(pemBytes, pem.EncodeToMemory(clientCertBlock)...) + pemBytes = append(pemBytes, pem.EncodeToMemory(interCertBlock)...) + pemBytes = append(pemBytes, pem.EncodeToMemory(rootCertBlock)...) + certOut, err := os.OpenFile(global.CONF.System.BaseDir+"/1panel/secret/server.crt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer certOut.Close() + if _, err := certOut.Write(pemBytes); err != nil { + return err + } + + keyOut, err := os.OpenFile(global.CONF.System.BaseDir+"/1panel/secret/server.key", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer keyOut.Close() + if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientPrivateKey)}); err != nil { + return err + } + + return nil +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f2cececf5..a6a96a64c 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -26,7 +26,6 @@ class RequestHttp { ...config.headers, }; } - return { ...config, }; diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 51e7f2f14..d243657fe 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -16,6 +16,8 @@ export namespace Setting { serverPort: number; securityEntranceStatus: string; + ssl: string; + sslType: string; securityEntrance: string; expirationDays: number; expirationTime: string; @@ -35,6 +37,22 @@ export namespace Setting { key: string; value: string; } + export interface SSLUpdate { + ssl: string; + domain: string; + sslType: string; + cert: string; + key: string; + sslID: number; + } + export interface SSLInfo { + domain: string; + timeout: string; + rootPath: string; + cert: string; + key: string; + sslID: number; + } export interface PasswordUpdate { oldPassword: string; newPassword: string; diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index d075cf659..76e7c8df4 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -28,6 +28,13 @@ export const updatePort = (param: Setting.PortUpdate) => { return http.post(`/settings/port/update`, param); }; +export const updateSSL = (param: Setting.SSLUpdate) => { + return http.post(`/settings/ssl/update`, param); +}; +export const loadSSLInfo = () => { + return http.get<Setting.SSLInfo>(`/settings/ssl/info`); +}; + export const handleExpired = (param: Setting.PasswordUpdate) => { return http.post(`/settings/expired/handle`, param); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index c0ee585e4..0b9958449 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -872,6 +872,18 @@ const message = { mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code', mfaHelper3: 'Enter six digits from the app', + https: 'Setting up HTTPS protocol access for the panel can enhance the security of panel access.', + selfSigned: 'Self signed', + selfSignedHelper: + 'It is normal for self-signed certificates to be not trusted by browsers and display a security warning as the certificate is not issued by a trusted third party.', + import: 'Import', + select: 'Select', + domainOrIP: 'Domain/IP:', + timeOut: 'Timeout', + rootCrtDownload: 'Root certificate download', + primaryKey: 'Primary key', + certificate: 'Certificate', + snapshot: 'Snapshot', thirdPartySupport: 'Only third-party accounts are supported', recoverDetail: 'Recover detail', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 5e1812bba..bc1c4a755 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -857,6 +857,17 @@ const message = { password: '密码', path: '路径', + https: '为面板设置 https 协议访问,提升面板访问安全性', + selfSigned: '自签名', + selfSignedHelper: '自签证书,不被浏览器信任,显示不安全是正常现象', + import: '导入', + select: '选择已有', + domainOrIP: '域名/IP:', + timeOut: '过期时间:', + rootCrtDownload: '根证书下载', + primaryKey: '密钥', + certificate: '证书', + snapshot: '快照', thirdPartySupport: '仅支持第三方账号', recoverDetail: '恢复详情', @@ -909,6 +920,8 @@ const message = { mfaHelper1: '下载两步验证手机应用 如:', mfaHelper2: '使用手机应用扫描以下二维码,获取 6 位验证码', mfaHelper3: '输入手机应用上的 6 位数字', + sslDisable: '禁用', + sslDisableHelper: '禁用 https 服务,需要重启面板才能生效,是否继续!', monitor: '监控', enableMonitor: '监控状态', diff --git a/frontend/src/views/setting/safe/index.vue b/frontend/src/views/setting/safe/index.vue index 2c44da8a3..5b325b740 100644 --- a/frontend/src/views/setting/safe/index.vue +++ b/frontend/src/views/setting/safe/index.vue @@ -5,7 +5,7 @@ <el-form :model="form" ref="panelFormRef" v-loading="loading" label-position="left" label-width="180px"> <el-row> <el-col :span="1"><br /></el-col> - <el-col :span="10"> + <el-col :span="16"> <el-form-item :label="$t('setting.panelPort')" :rules="Rules.port" prop="serverPort"> <el-input clearable v-model.number="form.serverPort"> <template #append> @@ -133,6 +133,22 @@ </ul> </el-card> </el-form-item> + + <el-form-item label="https" required prop="ssl"> + <el-switch + @change="handleSSL" + v-model="form.ssl" + active-value="enable" + inactive-value="disable" + /> + <span class="input-help">{{ $t('setting.https') }}</span> + <SSLSetting + :type="form.sslType" + :status="form.ssl" + v-if="sslShow" + style="width: 100%" + /> + </el-form-item> </el-col> </el-row> </el-form> @@ -165,6 +181,7 @@ import { ref, reactive, onMounted } from 'vue'; import { ElForm, ElMessageBox } from 'element-plus'; import { Setting } from '@/api/interface/setting'; import LayoutContent from '@/layout/layout-content.vue'; +import SSLSetting from '@/views/setting/safe/ssl/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue'; import { updateSetting, @@ -174,6 +191,7 @@ import { updatePort, getSystemAvailable, updateEntrance, + updateSSL, } from '@/api/modules/setting'; import i18n from '@/lang'; import { Rules, checkNumberRange } from '@/global/form-rules'; @@ -183,9 +201,12 @@ import { GlobalStore } from '@/store'; const globalStore = GlobalStore(); const loading = ref(false); + const form = reactive({ serverPort: 9999, securityEntranceStatus: 'disable', + ssl: 'disable', + sslType: 'self', securityEntrance: '', expirationDays: 0, expirationTime: '', @@ -200,11 +221,20 @@ const timeoutForm = reactive({ days: 0, }); +const sslShow = ref(); +const oldSSLStatus = ref(); + const search = async () => { const res = await getSettingInfo(); form.serverPort = Number(res.data.serverPort); form.securityEntranceStatus = res.data.securityEntranceStatus; isEntranceShow.value = res.data.securityEntranceStatus === 'enable'; + form.ssl = res.data.ssl; + oldSSLStatus.value = res.data.ssl; + form.sslType = res.data.sslType; + if (form.ssl === 'enable') { + sslShow.value = true; + } form.securityEntrance = res.data.securityEntrance; form.expirationDays = Number(res.data.expirationDays); form.expirationTime = res.data.expirationTime; @@ -323,6 +353,33 @@ const handleEntrance = async () => { } }; +const handleSSL = async () => { + if (form.ssl === 'enable') { + sslShow.value = true; + return; + } + if (form.ssl === oldSSLStatus.value) { + sslShow.value = false; + return; + } + ElMessageBox.confirm(i18n.global.t('setting.sslDisableHelper'), i18n.global.t('setting.sslDisable'), { + confirmButtonText: i18n.global.t('commons.button.confirm'), + cancelButtonText: i18n.global.t('commons.button.cancel'), + type: 'info', + }) + .then(async () => { + sslShow.value = false; + await updateSSL({ ssl: 'disable', domain: '', sslType: '', key: '', cert: '', sslID: 0 }); + MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); + let href = window.location.href; + let address = href.split('://')[1]; + window.open(`http://${address}/`, '_self'); + }) + .catch(() => { + form.ssl = 'enable'; + }); +}; + const onSaveEntrance = async () => { const reg = /^[A-Za-z0-9]{6,10}$/; if ((!reg.test(form.securityEntrance) && form.securityEntrance !== '') || form.securityEntrance === '') { diff --git a/frontend/src/views/setting/safe/ssl/index.vue b/frontend/src/views/setting/safe/ssl/index.vue new file mode 100644 index 000000000..f7d9d8d0b --- /dev/null +++ b/frontend/src/views/setting/safe/ssl/index.vue @@ -0,0 +1,194 @@ +<template> + <div> + <el-card> + <el-form ref="formRef" label-position="top" :model="form" :rules="rules"> + <el-radio-group v-model="sslItemType"> + <el-radio label="self">{{ $t('setting.selfSigned') }}</el-radio> + <el-radio label="select">{{ $t('setting.select') }}</el-radio> + <el-radio label="import">{{ $t('setting.import') }}</el-radio> + </el-radio-group> + <span class="input-help" v-if="sslItemType === 'self'">{{ $t('setting.selfSignedHelper') }}</span> + <div v-if="sslInfo.timeout"> + <el-tag>{{ $t('setting.domainOrIP') }} {{ sslInfo.domain }}</el-tag> + <el-tag style="margin-left: 5px">{{ $t('setting.timeOut') }} {{ sslInfo.timeout }}</el-tag> + <el-button + @click="onDownload" + style="margin-left: 5px" + v-if="sslItemType === 'self'" + type="primary" + link + icon="Download" + > + {{ $t('setting.rootCrtDownload') }} + </el-button> + </div> + + <div v-if="sslItemType === 'import'"> + <el-form-item :label="$t('setting.primaryKey')" prop="key"> + <el-input v-model="form.key" :autosize="{ minRows: 2, maxRows: 6 }" type="textarea" /> + </el-form-item> + <el-form-item class="margintop" :label="$t('setting.certificate')" prop="cert"> + <el-input v-model="form.cert" :autosize="{ minRows: 2, maxRows: 6 }" type="textarea" /> + </el-form-item> + </div> + + <div v-if="sslItemType === 'select'"> + <el-form-item :label="$t('setting.certificate')" prop="sslID"> + <el-select v-model="form.sslID" @change="changeSSl(form.sslID)"> + <el-option + v-for="(item, index) in sslList" + :key="index" + :label="item.primaryDomain" + :value="item.id" + ></el-option> + </el-select> + </el-form-item> + <el-descriptions + class="margintop" + :column="5" + border + direction="vertical" + v-if="form.sslID > 0 && itemSSL" + > + <el-descriptions-item :label="$t('website.primaryDomain')"> + {{ itemSSL.primaryDomain }} + </el-descriptions-item> + <el-descriptions-item :label="$t('website.otherDomains')"> + {{ itemSSL.domains }} + </el-descriptions-item> + <el-descriptions-item :label="$t('ssl.provider')"> + {{ getProvider(itemSSL.provider) }} + </el-descriptions-item> + <el-descriptions-item + :label="$t('ssl.acmeAccount')" + v-if="itemSSL.acmeAccount?.email && itemSSL.provider !== 'manual'" + > + {{ itemSSL.acmeAccount.email }} + </el-descriptions-item> + <el-descriptions-item :label="$t('website.expireDate')"> + {{ dateFormatSimple(itemSSL.expireDate) }} + </el-descriptions-item> + </el-descriptions> + </div> + <el-button style="margin-top: 20px" type="primary" @click="onSaveSSL(formRef)"> + {{ $t('commons.button.saveAndEnable') }} + </el-button> + </el-form> + </el-card> + </div> +</template> +<script lang="ts" setup> +import { Website } from '@/api/interface/Website'; +import { loadSSLInfo } from '@/api/modules/setting'; +import { dateFormatSimple, getProvider } from '@/utils/util'; +import { ListSSL } from '@/api/modules/website'; +import { nextTick, onMounted, reactive, ref } from 'vue'; +import i18n from '@/lang'; +import { MsgSuccess } from '@/utils/message'; +import { updateSSL } from '@/api/modules/setting'; +import { DownloadByPath } from '@/api/modules/files'; +import { Rules } from '@/global/form-rules'; +import { FormInstance } from 'element-plus'; + +const form = reactive({ + ssl: 'enable', + domain: '', + sslType: 'self', + sslID: null as number, + cert: '', + key: '', + rootPath: '', +}); + +const rules = reactive({ + cert: [Rules.requiredInput], + key: [Rules.requiredInput], + sslID: [Rules.requiredSelect], +}); + +const formRef = ref<FormInstance>(); + +const props = defineProps({ + type: { + type: String, + default: 'self', + }, +}); + +const sslInfo = reactive({ + domain: '', + timeout: '', +}); +const sslList = ref(); +const itemSSL = ref(); +const sslItemType = ref('self'); + +const loadInfo = async () => { + await loadSSLInfo().then(async (res) => { + sslInfo.domain = res.data.domain || ''; + sslInfo.timeout = res.data.timeout || ''; + form.cert = res.data.cert; + form.key = res.data.key; + form.rootPath = res.data.rootPath; + if (res.data.sslID) { + form.sslID = res.data.sslID; + const ssls = await ListSSL({}); + sslList.value = ssls.data || []; + changeSSl(form.sslID); + } + }); +}; + +const loadSSLs = async () => { + const res = await ListSSL({}); + sslList.value = res.data || []; +}; + +const changeSSl = (sslid: number) => { + const res = sslList.value.filter((element: Website.SSL) => { + return element.id == sslid; + }); + itemSSL.value = res[0]; +}; + +const onDownload = async () => { + const file = await DownloadByPath(form.rootPath); + const downloadUrl = window.URL.createObjectURL(new Blob([file])); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = downloadUrl; + a.download = 'server.crt'; + const event = new MouseEvent('click'); + a.dispatchEvent(event); +}; + +const onSaveSSL = async (formEl: FormInstance | undefined) => { + if (!formEl) return; + formEl.validate(async (valid) => { + if (!valid) return; + form.sslType = sslItemType.value; + let href = window.location.href; + form.domain = href.split('//')[1].split(':')[0]; + await updateSSL(form).then(() => { + MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); + let href = window.location.href; + let address = href.split('://')[1]; + window.open(`https://${address}/`, '_self'); + }); + }); +}; + +onMounted(() => { + nextTick(() => { + sslItemType.value = props.type; + loadInfo(); + }); + loadSSLs(); +}); +</script> + +<style scoped lang="scss"> +.margintop { + margin-top: 10px; +} +</style>