diff --git a/backend/app/api/v1/database.go b/backend/app/api/v1/database.go index 5431411d6..41ba2f142 100644 --- a/backend/app/api/v1/database.go +++ b/backend/app/api/v1/database.go @@ -1,6 +1,8 @@ package v1 import ( + "encoding/base64" + "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/constant" @@ -21,6 +23,14 @@ func (b *BaseApi) CreateDatabase(c *gin.Context) { if err := helper.CheckBindAndValidate(&req, c); err != nil { return } + if req.SSL { + key, _ := base64.StdEncoding.DecodeString(req.ClientKey) + req.ClientKey = string(key) + cert, _ := base64.StdEncoding.DecodeString(req.ClientCert) + req.ClientCert = string(cert) + ca, _ := base64.StdEncoding.DecodeString(req.RootCert) + req.RootCert = string(ca) + } if err := databaseService.Create(req); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) @@ -43,6 +53,14 @@ func (b *BaseApi) CheckDatabase(c *gin.Context) { if err := helper.CheckBindAndValidate(&req, c); err != nil { return } + if req.SSL { + clientKey, _ := base64.StdEncoding.DecodeString(req.ClientKey) + req.ClientKey = string(clientKey) + clientCert, _ := base64.StdEncoding.DecodeString(req.ClientCert) + req.ClientCert = string(clientCert) + rootCert, _ := base64.StdEncoding.DecodeString(req.RootCert) + req.RootCert = string(rootCert) + } helper.SuccessWithData(c, databaseService.CheckDatabase(req)) } @@ -173,6 +191,14 @@ func (b *BaseApi) UpdateDatabase(c *gin.Context) { if err := helper.CheckBindAndValidate(&req, c); err != nil { return } + if req.SSL { + cKey, _ := base64.StdEncoding.DecodeString(req.ClientKey) + req.ClientKey = string(cKey) + cCert, _ := base64.StdEncoding.DecodeString(req.ClientCert) + req.ClientCert = string(cCert) + ca, _ := base64.StdEncoding.DecodeString(req.RootCert) + req.RootCert = string(ca) + } if err := databaseService.Update(req); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) diff --git a/backend/app/dto/database.go b/backend/app/dto/database.go index 970f2b58e..cdf1966df 100644 --- a/backend/app/dto/database.go +++ b/backend/app/dto/database.go @@ -227,17 +227,24 @@ type DatabaseSearch struct { } type DatabaseInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Name string `json:"name" validate:"max=256"` - From string `json:"from"` - Type string `json:"type"` - Version string `json:"version"` - Address string `json:"address"` - Port uint `json:"port"` - Username string `json:"username"` - Password string `json:"password"` - Description string `json:"description"` + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name" validate:"max=256"` + From string `json:"from"` + Type string `json:"type"` + Version string `json:"version"` + Address string `json:"address"` + Port uint `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert"` + ClientKey string `json:"clientKey"` + ClientCert string `json:"clientCert"` + SkipVerify bool `json:"skipVerify"` + + Description string `json:"description"` } type DatabaseOption struct { @@ -250,25 +257,39 @@ type DatabaseOption struct { } type DatabaseCreate struct { - Name string `json:"name" validate:"required,max=256"` - Type string `json:"type" validate:"required"` - From string `json:"from" validate:"required,oneof=local remote"` - Version string `json:"version" validate:"required"` - Address string `json:"address"` - Port uint `json:"port"` - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` + Name string `json:"name" validate:"required,max=256"` + Type string `json:"type" validate:"required"` + From string `json:"from" validate:"required,oneof=local remote"` + Version string `json:"version" validate:"required"` + Address string `json:"address"` + Port uint `json:"port"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert"` + ClientKey string `json:"clientKey"` + ClientCert string `json:"clientCert"` + SkipVerify bool `json:"skipVerify"` + Description string `json:"description"` } type DatabaseUpdate struct { - ID uint `json:"id"` - Type string `json:"type" validate:"required"` - Version string `json:"version" validate:"required"` - Address string `json:"address"` - Port uint `json:"port"` - Username string `json:"username" validate:"required"` - Password string `json:"password" validate:"required"` + ID uint `json:"id"` + Type string `json:"type" validate:"required"` + Version string `json:"version" validate:"required"` + Address string `json:"address"` + Port uint `json:"port"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert"` + ClientKey string `json:"clientKey"` + ClientCert string `json:"clientCert"` + SkipVerify bool `json:"skipVerify"` + Description string `json:"description"` } diff --git a/backend/app/model/database.go b/backend/app/model/database.go index 1123bf282..f4d0ff0fc 100644 --- a/backend/app/model/database.go +++ b/backend/app/model/database.go @@ -11,5 +11,12 @@ type Database struct { Port uint `json:"port" gorm:"type:decimal;not null"` Username string `json:"username" gorm:"type:varchar(64)"` Password string `json:"password" gorm:"type:varchar(64)"` - Description string `json:"description" gorm:"type:varchar(256);"` + + SSL bool `json:"ssl"` + RootCert string `json:"rootCert" gorm:"type:longText"` + ClientKey string `json:"clientKey" gorm:"type:longText"` + ClientCert string `json:"clientCert" gorm:"type:longText"` + SkipVerify bool `json:"skipVerify"` + + Description string `json:"description" gorm:"type:varchar(256);"` } diff --git a/backend/app/service/database.go b/backend/app/service/database.go index 60f4125f1..a91e1c5b5 100644 --- a/backend/app/service/database.go +++ b/backend/app/service/database.go @@ -84,7 +84,13 @@ func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool { Port: req.Port, Username: req.Username, Password: req.Password, - Timeout: 6, + + SSL: req.SSL, + RootCert: req.RootCert, + ClientKey: req.ClientKey, + ClientCert: req.ClientCert, + SkipVerify: req.SkipVerify, + Timeout: 6, }); err != nil { return false } @@ -105,7 +111,13 @@ func (u *DatabaseService) Create(req dto.DatabaseCreate) error { Port: req.Port, Username: req.Username, Password: req.Password, - Timeout: 6, + + SSL: req.SSL, + RootCert: req.RootCert, + ClientKey: req.ClientKey, + ClientCert: req.ClientCert, + SkipVerify: req.SkipVerify, + Timeout: 6, }); err != nil { return err } @@ -172,7 +184,14 @@ func (u *DatabaseService) Update(req dto.DatabaseUpdate) error { Port: req.Port, Username: req.Username, Password: req.Password, - Timeout: 300, + + SSL: req.SSL, + ClientKey: req.ClientKey, + ClientCert: req.ClientCert, + RootCert: req.RootCert, + SkipVerify: req.SkipVerify, + + Timeout: 300, }); err != nil { return err } @@ -189,6 +208,10 @@ func (u *DatabaseService) Update(req dto.DatabaseUpdate) error { upMap["port"] = req.Port upMap["username"] = req.Username upMap["password"] = pass - upMap["description"] = req.Description + upMap["ssl"] = req.SSL + upMap["client_key"] = req.ClientKey + upMap["client_cert"] = req.ClientCert + upMap["root_cert"] = req.RootCert + upMap["skip_verify"] = req.SkipVerify return databaseRepo.Update(req.ID, upMap) } diff --git a/backend/app/service/database_mysql.go b/backend/app/service/database_mysql.go index 9e926bded..cf61f1241 100644 --- a/backend/app/service/database_mysql.go +++ b/backend/app/service/database_mysql.go @@ -641,6 +641,11 @@ func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) { dbInfo.Port = databaseItem.Port dbInfo.Username = databaseItem.Username dbInfo.Password = databaseItem.Password + dbInfo.SSL = databaseItem.SSL + dbInfo.ClientKey = databaseItem.ClientKey + dbInfo.ClientCert = databaseItem.ClientCert + dbInfo.RootCert = databaseItem.RootCert + dbInfo.SkipVerify = databaseItem.SkipVerify version = databaseItem.Version } else { diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 9a094d773..79aaaa87d 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -57,6 +57,7 @@ func Init() { migrations.UpdateWebsiteSSL, migrations.AddWebsiteCA, migrations.AddDockerSockPath, + migrations.AddDatabaseSSL, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/v_1_9.go b/backend/init/migration/migrations/v_1_9.go index 0d4a544f6..408bce1f1 100644 --- a/backend/init/migration/migrations/v_1_9.go +++ b/backend/init/migration/migrations/v_1_9.go @@ -45,3 +45,13 @@ var AddDockerSockPath = &gormigrate.Migration{ return nil }, } + +var AddDatabaseSSL = &gormigrate.Migration{ + ID: "20231126-add-database-ssl", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.Database{}); err != nil { + return err + } + return nil + }, +} diff --git a/backend/utils/mysql/client.go b/backend/utils/mysql/client.go index b2ca2a35b..7af24ff06 100644 --- a/backend/utils/mysql/client.go +++ b/backend/utils/mysql/client.go @@ -35,7 +35,12 @@ func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) { if strings.Contains(conn.Address, ":") { conn.Address = fmt.Sprintf("[%s]", conn.Address) } - connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8", conn.Username, conn.Password, conn.Address, conn.Port) + + tlsItem, err := client.ConnWithSSL(conn.SSL, conn.SkipVerify, conn.ClientKey, conn.ClientCert, conn.RootCert) + if err != nil { + return nil, err + } + connArgs := fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8%s", conn.Username, conn.Password, conn.Address, conn.Port, tlsItem) db, err := sql.Open("mysql", connArgs) if err != nil { return nil, err @@ -57,5 +62,11 @@ func NewMysqlClient(conn client.DBInfo) (MysqlClient, error) { Password: conn.Password, Address: conn.Address, Port: conn.Port, + + SSL: conn.SSL, + RootCert: conn.RootCert, + ClientKey: conn.ClientKey, + ClientCert: conn.ClientCert, + SkipVerify: conn.SkipVerify, }), nil } diff --git a/backend/utils/mysql/client/info.go b/backend/utils/mysql/client/info.go index 4e48bdd9f..d4f20e90a 100644 --- a/backend/utils/mysql/client/info.go +++ b/backend/utils/mysql/client/info.go @@ -1,9 +1,13 @@ package client import ( + "crypto/tls" + "crypto/x509" + "errors" "strings" "github.com/1Panel-dev/1Panel/backend/utils/common" + "github.com/go-sql-driver/mysql" ) type DBInfo struct { @@ -14,6 +18,12 @@ type DBInfo struct { Username string `json:"userName"` Password string `json:"password"` + SSL bool `json:"ssl"` + RootCert string `json:"rootCert"` + ClientKey string `json:"clientKey"` + ClientCert string `json:"clientCert"` + SkipVerify bool `json:"skipVerify"` + Timeout uint `json:"timeout"` // second } @@ -114,3 +124,45 @@ func randomPassword(user string) string { } return passwdItem + "@" + common.RandStrAndNum(8) } + +func VerifyPeerCertFunc(pool *x509.CertPool) func([][]byte, [][]*x509.Certificate) error { + return func(rawCerts [][]byte, _ [][]*x509.Certificate) error { + if len(rawCerts) == 0 { + return errors.New("no certificates available to verify") + } + + cert, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return err + } + + opts := x509.VerifyOptions{Roots: pool} + if _, err = cert.Verify(opts); err != nil { + return err + } + return nil + } +} + +func ConnWithSSL(ssl, skipVerify bool, clientKey, clientCert, rootCert string) (string, error) { + if !ssl { + return "", nil + } + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM([]byte(rootCert)); !ok { + return "", errors.New("unable to append root cert to pool") + } + cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey)) + if err != nil { + return "", err + } + if err := mysql.RegisterTLSConfig("cloudsql", &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: skipVerify, + VerifyPeerCertificate: VerifyPeerCertFunc(pool), + }); err != nil { + return "", err + } + return "&tls=cloudsql", nil +} diff --git a/backend/utils/mysql/client/remote.go b/backend/utils/mysql/client/remote.go index 34fe29678..18a0b01f7 100644 --- a/backend/utils/mysql/client/remote.go +++ b/backend/utils/mysql/client/remote.go @@ -23,6 +23,12 @@ type Remote struct { Password string Address string Port uint + + SSL bool + RootCert string + ClientKey string + ClientCert string + SkipVerify bool } func NewRemote(db Remote) *Remote { @@ -224,7 +230,12 @@ func (r *Remote) Backup(info BackupInfo) error { } } fileNameItem := info.TargetDir + "/" + strings.TrimSuffix(info.FileName, ".gz") - dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F") + + tlsItem, err := ConnWithSSL(r.SSL, r.SkipVerify, r.ClientKey, r.ClientCert, r.RootCert) + if err != nil { + return err + } + dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai%s", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F", tlsItem) f, _ := os.OpenFile(fileNameItem, os.O_RDWR|os.O_CREATE, 0755) defer f.Close() @@ -254,7 +265,12 @@ func (r *Remote) Recover(info RecoverInfo) error { _, _ = gzipCmd.CombinedOutput() }() } - dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F") + tlsItem, err := ConnWithSSL(r.SSL, r.SkipVerify, r.ClientKey, r.ClientCert, r.RootCert) + if err != nil { + return err + } + dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai%s", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F", tlsItem) + f, err := os.Open(fileName) if err != nil { return err diff --git a/frontend/src/api/interface/database.ts b/frontend/src/api/interface/database.ts index 47fda5ed2..0e6138b3a 100644 --- a/frontend/src/api/interface/database.ts +++ b/frontend/src/api/interface/database.ts @@ -213,6 +213,13 @@ export namespace Database { port: number; username: string; password: string; + + ssl: boolean; + rootCert: string; + clientKey: string; + clientCert: string; + skipVerify: boolean; + description: string; } export interface SearchDatabasePage { @@ -239,6 +246,13 @@ export namespace Database { port: number; username: string; password: string; + + ssl: boolean; + rootCert: string; + clientKey: string; + clientCert: string; + skipVerify: boolean; + description: string; } export interface DatabaseUpdate { @@ -248,6 +262,13 @@ export namespace Database { port: number; username: string; password: string; + + ssl: boolean; + rootCert: string; + clientKey: string; + clientCert: string; + skipVerify: boolean; + description: string; } export interface DatabaseDelete { diff --git a/frontend/src/api/modules/database.ts b/frontend/src/api/modules/database.ts index b8bec3fea..c92590c16 100644 --- a/frontend/src/api/modules/database.ts +++ b/frontend/src/api/modules/database.ts @@ -101,13 +101,34 @@ export const listDatabases = (type: string) => { return http.get>(`/databases/db/list/${type}`); }; export const checkDatabase = (params: Database.DatabaseCreate) => { - return http.post(`/databases/db/check`, params, TimeoutEnum.T_40S); + let request = deepCopy(params) as Database.DatabaseCreate; + if (request.ssl) { + request.clientKey = Base64.encode(request.clientKey); + request.clientCert = Base64.encode(request.clientCert); + request.rootCert = Base64.encode(request.rootCert); + } + + return http.post(`/databases/db/check`, request, TimeoutEnum.T_40S); }; export const addDatabase = (params: Database.DatabaseCreate) => { - return http.post(`/databases/db`, params, TimeoutEnum.T_40S); + let request = deepCopy(params) as Database.DatabaseCreate; + if (request.ssl) { + request.clientKey = Base64.encode(request.clientKey); + request.clientCert = Base64.encode(request.clientCert); + request.rootCert = Base64.encode(request.rootCert); + } + + return http.post(`/databases/db`, request, TimeoutEnum.T_40S); }; export const editDatabase = (params: Database.DatabaseUpdate) => { - return http.post(`/databases/db/update`, params, TimeoutEnum.T_40S); + let request = deepCopy(params) as Database.DatabaseCreate; + if (request.ssl) { + request.clientKey = Base64.encode(request.clientKey); + request.clientCert = Base64.encode(request.clientCert); + request.rootCert = Base64.encode(request.rootCert); + } + + return http.post(`/databases/db/update`, request, TimeoutEnum.T_40S); }; export const deleteCheckDatabase = (id: number) => { return http.post>(`/databases/db/del/check`, { id: id }); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 58360d751..399223c73 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -391,6 +391,11 @@ const message = { address: 'DB address', version: 'DB version', userHelper: 'The root user or a database user with root privileges can access the remote database.', + ssl: 'Use SSL', + clientKey: 'Client Private Key', + clientCert: 'Client Certificate', + caCert: 'CA Certificate', + skipVerify: 'Ignore Certificate Validity Check', formatHelper: 'The current database character set is {0}, the character set inconsistency may cause recovery failure', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index fe01ec378..54d421e78 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -383,6 +383,11 @@ const message = { address: '數據庫地址', version: '數據庫版本', userHelper: 'root 用戶或者擁有 root 權限的數據庫用戶', + ssl: '使用 SSL', + clientKey: '客户端私钥', + clientCert: '客户端证书', + caCert: 'CA 证书', + skipVerify: '忽略校验证书可用性检测', formatHelper: '當前資料庫字符集為 {0},字符集不一致可能導致恢復失敗', selectFile: '選擇文件', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 84adc44b3..cf091d09e 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -383,6 +383,11 @@ const message = { address: '数据库地址', version: '数据库版本', userHelper: 'root 用户或者拥有 root 权限的数据库用户', + ssl: '使用 SSL', + clientKey: '客户端私钥', + clientCert: '客户端证书', + caCert: 'CA 证书', + skipVerify: '忽略校验证书可用性检测', formatHelper: '当前数据库字符集为 {0},字符集不一致可能导致恢复失败', selectFile: '选择文件', diff --git a/frontend/src/views/database/mysql/remote/operate/index.vue b/frontend/src/views/database/mysql/remote/operate/index.vue index 62d7e8ebd..cc8ca3da6 100644 --- a/frontend/src/views/database/mysql/remote/operate/index.vue +++ b/frontend/src/views/database/mysql/remote/operate/index.vue @@ -54,6 +54,46 @@ v-model.trim="dialogData.rowData!.password" /> + + + +
+ + + + + + + + + + + + +
@@ -128,6 +168,10 @@ const rules = reactive({ port: [Rules.port], username: [Rules.requiredInput], password: [Rules.requiredInput], + + clientKey: [Rules.requiredInput], + clientCert: [Rules.requiredInput], + caCert: [Rules.requiredInput], }); type FormInstance = InstanceType; diff --git a/frontend/src/views/home/status/index.vue b/frontend/src/views/home/status/index.vue index fe7be721a..6aaf46bbc 100644 --- a/frontend/src/views/home/status/index.vue +++ b/frontend/src/views/home/status/index.vue @@ -43,7 +43,7 @@ - + {{ $t('home.mem') }}: {{ $t('home.total') }}: {{ formatNumber(currentInfo.memoryTotal / 1024 / 1024) }} MB @@ -57,10 +57,8 @@ {{ $t('home.percent') }}: {{ formatNumber(currentInfo.memoryUsedPercent) }}% -
- - {{ $t('home.swapMem') }}: - +
+ {{ $t('home.swapMem') }}: {{ $t('home.total') }}: {{ formatNumber(currentInfo.swapMemoryTotal / 1024 / 1024) }} MB