diff --git a/app.go b/app.go deleted file mode 100644 index af53038..0000000 --- a/app.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "context" - "fmt" -) - -// App struct -type App struct { - ctx context.Context -} - -// NewApp creates a new App application struct -func NewApp() *App { - return &App{} -} - -// startup is called when the app starts. The context is saved -// so we can call the runtime methods -func (a *App) startup(ctx context.Context) { - a.ctx = ctx -} - -// Greet returns a greeting for the given name -func (a *App) Greet(name string) string { - return fmt.Sprintf("Hello %s, It's show time!", name) -} diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go index 70cdcbf..83fcc55 100644 --- a/backend/services/connection_service.go +++ b/backend/services/connection_service.go @@ -2,10 +2,11 @@ package services import ( "context" + "crypto/tls" + "crypto/x509" "errors" "fmt" "github.com/redis/go-redis/v9" - "github.com/wailsapp/wails/v2/pkg/runtime" "golang.org/x/crypto/ssh" "log" "net" @@ -115,6 +116,39 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O } } + var tlsConfig *tls.Config + if config.SSL.Enable { + // setup tls config + var certs []tls.Certificate + if len(config.SSL.CertFile) > 0 && len(config.SSL.KeyFile) > 0 { + if cert, err := tls.LoadX509KeyPair(config.SSL.CertFile, config.SSL.KeyFile); err != nil { + return nil, err + } else { + certs = []tls.Certificate{cert} + } + } + + var caCertPool *x509.CertPool + if len(config.SSL.CAFile) > 0 { + ca, err := os.ReadFile(config.SSL.CAFile) + if err != nil { + return nil, err + } + caCertPool = x509.NewCertPool() + caCertPool.AppendCertsFromPEM(ca) + } + + if len(certs) <= 0 { + return nil, errors.New("tls config error") + } + + tlsConfig = &tls.Config{ + RootCAs: caCertPool, + InsecureSkipVerify: false, + Certificates: certs, + } + } + option := &redis.Options{ ClientName: config.Name, Addr: fmt.Sprintf("%s:%d", config.Addr, config.Port), @@ -123,6 +157,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O DialTimeout: time.Duration(config.ConnTimeout) * time.Second, ReadTimeout: time.Duration(config.ExecTimeout) * time.Second, WriteTimeout: time.Duration(config.ExecTimeout) * time.Second, + TLSConfig: tlsConfig, } if sshClient != nil { option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) { @@ -321,23 +356,6 @@ func (c *connectionService) SaveSortedConnection(sortedConns types.Connections) return } -// SelectKeyFile open file dialog to select a private key file -func (c *connectionService) SelectKeyFile(title string) (resp types.JSResp) { - filepath, err := runtime.OpenFileDialog(c.ctx, runtime.OpenDialogOptions{ - Title: title, - ShowHiddenFiles: true, - }) - if err != nil { - resp.Msg = err.Error() - return - } - resp.Success = true - resp.Data = map[string]any{ - "path": filepath, - } - return -} - // CreateGroup create a new group func (c *connectionService) CreateGroup(name string) (resp types.JSResp) { err := c.conns.CreateGroup(name) diff --git a/backend/services/system_dialog.go b/backend/services/system_dialog.go new file mode 100644 index 0000000..6f06f43 --- /dev/null +++ b/backend/services/system_dialog.go @@ -0,0 +1,47 @@ +package services + +import ( + "context" + "github.com/wailsapp/wails/v2/pkg/runtime" + "log" + "sync" + "tinyrdm/backend/types" +) + +type systemService struct { + ctx context.Context +} + +var system *systemService +var onceSystem sync.Once + +func System() *systemService { + if system == nil { + onceSystem.Do(func() { + system = &systemService{} + }) + } + return system +} + +func (s *systemService) Start(ctx context.Context) { + s.ctx = ctx +} + +// SelectFile open file dialog to select a file +func (s *systemService) SelectFile(title string) (resp types.JSResp) { + filepath, err := runtime.OpenFileDialog(s.ctx, runtime.OpenDialogOptions{ + Title: title, + ShowHiddenFiles: true, + }) + if err != nil { + log.Println(err) + resp.Msg = err.Error() + return + } + resp.Success = true + resp.Data = map[string]any{ + "path": filepath, + } + return +} diff --git a/backend/types/connection.go b/backend/types/connection.go index 646fc2c..4ea7d05 100644 --- a/backend/types/connection.go +++ b/backend/types/connection.go @@ -16,6 +16,7 @@ type ConnectionConfig struct { DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"` DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"` MarkColor string `json:"markColor,omitempty" yaml:"mark_color,omitempty"` + SSL ConnectionSSL `json:"ssl,omitempty" yaml:"ssl,omitempty"` SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"` Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"` Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"` @@ -42,6 +43,13 @@ type ConnectionDB struct { AvgTTL int `json:"avgTtl,omitempty"` } +type ConnectionSSL struct { + Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` + KeyFile string `json:"keyFile,omitempty" yaml:"keyFile,omitempty"` + CertFile string `json:"certFile,omitempty" yaml:"certFile,omitempty"` + CAFile string `json:"caFile,omitempty" yaml:"caFile,omitempty"` +} + type ConnectionSSH struct { Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` diff --git a/frontend/src/components/dialogs/ConnectionDialog.vue b/frontend/src/components/dialogs/ConnectionDialog.vue index 61ec240..f92f94e 100644 --- a/frontend/src/components/dialogs/ConnectionDialog.vue +++ b/frontend/src/components/dialogs/ConnectionDialog.vue @@ -2,10 +2,11 @@ import { every, get, includes, isEmpty, map, sortBy, toNumber } from 'lodash' import { computed, nextTick, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' -import { ListSentinelMasters, SelectKeyFile, TestConnection } from 'wailsjs/go/services/connectionService.js' +import { ListSentinelMasters, TestConnection } from 'wailsjs/go/services/connectionService.js' import useDialog, { ConnDialogType } from 'stores/dialog' import Close from '@/components/icons/Close.vue' import useConnectionStore from 'stores/connections.js' +import { SelectFile } from 'wailsjs/go/services/systemService.js' /** * Dialog for new or edit connection @@ -82,12 +83,36 @@ const sshLoginType = computed(() => { return get(generalForm.value, 'ssh.loginType', 'pwd') }) -const onChoosePKFile = async () => { - const { success, data } = await SelectKeyFile(i18n.t('dialogue.connection.ssh.pkfile_selection_title')) +const onSSHChooseKey = async () => { + const { success, data } = await SelectFile() + const path = get(data, 'path', '') + if (!isEmpty(path)) { + generalForm.value.ssh.pkFile = path + } +} + +const onSSLChooseCert = async () => { + const { success, data } = await SelectFile() + const path = get(data, 'path', '') + if (!isEmpty(path)) { + generalForm.value.ssl.certFile = path + } +} + +const onSSLChooseKey = async () => { + const { success, data } = await SelectFile() + const path = get(data, 'path', '') + if (!isEmpty(path)) { + generalForm.value.ssl.keyFile = path + } +} + +const onSSLChooseCA = async () => { + const { success, data } = await SelectFile() if (!success) { - generalForm.value.ssh.pkFile = '' + generalForm.value.ssl.caFile = '' } else { - generalForm.value.ssh.pkFile = get(data, 'path', '') + generalForm.value.ssl.caFile = get(data, 'path', '') } } @@ -149,8 +174,13 @@ const onSaveConnection = async () => { generalForm.value.dbFilterList = [] } + // trim ssl data + if (!!!generalForm.value.ssl.enable) { + generalForm.value.ssl = {} + } + // trim ssh login data - if (generalForm.value.ssh.enable) { + if (!!generalForm.value.ssh.enable) { switch (generalForm.value.ssh.loginType) { case 'pkfile': generalForm.value.ssh.password = '' @@ -162,15 +192,16 @@ const onSaveConnection = async () => { } } else { // ssh disabled, reset to default value - const { ssh } = connectionStore.newDefaultConnection() - generalForm.value.ssh = ssh + generalForm.value.ssh = {} } // trim sentinel data - if (!generalForm.value.sentinel.enable) { - generalForm.value.sentinel.master = '' - generalForm.value.sentinel.username = '' - generalForm.value.sentinel.password = '' + if (!!!generalForm.value.sentinel.enable) { + generalForm.value.sentinel = {} + } + + if (!!!generalForm.value.cluster.enable) { + generalForm.value.cluster = {} } // store new connection @@ -387,6 +418,60 @@ const onClose = () => { + + + + + {{ $t('dialogue.connection.ssl.enable') }} + + + + + + + + ... + + + + + + + + ... + + + + + + + + ... + + + + + + @@ -435,7 +520,13 @@ const onClose = () => { - ... + + ... + diff --git a/frontend/src/langs/en.json b/frontend/src/langs/en.json index 81c1cb1..7b0ac7b 100644 --- a/frontend/src/langs/en.json +++ b/frontend/src/langs/en.json @@ -150,12 +150,12 @@ "ssl": { "title": "SSL/TLS", "enable": "Enable SSL/TLS", - "key_file": "Public Key", - "cert_file": "Private Key", + "cert_file": "Public Key", + "key_file": "Private Key", "ca_file": "Authority", - "key_file_tip": "Public Key File in PEM format", - "cert_file_tip":"Private Key File in PEM format", - "ca_file_tip": "Certificate Authority File in PEM format" + "cert_file_tip":"Public Key File in PEM format(Cert)", + "key_file_tip": "Private Key File in PEM format(Key)", + "ca_file_tip": "Certificate Authority File in PEM format(CA)" }, "ssh": { "title": "SSH Tunnel", @@ -167,8 +167,7 @@ "usr_tip": "SSH Username", "pwd_tip": "SSH Password", "pkfile_tip": "SSH Private Key File Path", - "passphrase_tip": "(Optional) Passphrase for Private Key", - "pkfile_selection_title": "Please Select Private Key File" + "passphrase_tip": "(Optional) Passphrase for Private Key" }, "sentinel": { "title": "Sentinel", diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index 131c07d..21dc3d9 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -150,12 +150,12 @@ "ssl": { "title": "SSL/TLS", "enable": "启用SSL", - "key_file": "公钥文件", - "cert_file": "私钥文件", + "cert_file": "公钥文件", + "key_file": "私钥文件", "ca_file": "授权文件", - "key_file_tip": "PEM格式公钥文件", - "cert_file_tip":"PEM格式私钥文件", - "ca_file_tip": "PEM格式授权文件" + "cert_file_tip":"PEM格式公钥文件(Cert)", + "key_file_tip": "PEM格式私钥文件(Key)", + "ca_file_tip": "PEM格式授权文件(CA)" }, "ssh": { "enable": "启用SSH隧道", @@ -167,8 +167,7 @@ "usr_tip": "SSH登录用户名", "pwd_tip": "SSH登录密码", "pkfile_tip": "SSH私钥文件路径", - "passphrase_tip": "(可选)SSH私钥密码", - "pkfile_selection_title": "请选择私钥文件" + "passphrase_tip": "(可选)SSH私钥密码" }, "sentinel": { "title": "哨兵模式", diff --git a/frontend/src/stores/connections.js b/frontend/src/stores/connections.js index 6ba5d58..1c6f0bb 100644 --- a/frontend/src/stores/connections.js +++ b/frontend/src/stores/connections.js @@ -231,6 +231,12 @@ const useConnectionStore = defineStore('connections', { dbFilterType: 'none', dbFilterList: [], markColor: '', + ssl: { + enable: false, + certFile: '', + keyFile: '', + caFile: '', + }, ssh: { enable: false, addr: '', diff --git a/main.go b/main.go index 0991cd0..ec3bc0c 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ var version = "0.0.0" func main() { // Create an instance of the app structure - app := NewApp() + sysSvc := services.System() connSvc := services.Connection() prefSvc := services.Preferences() prefSvc.SetAppVersion(version) @@ -54,7 +54,7 @@ func main() { }, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 0}, OnStartup: func(ctx context.Context) { - app.startup(ctx) + sysSvc.Start(ctx) connSvc.Start(ctx) }, OnBeforeClose: func(ctx context.Context) (prevent bool) { @@ -69,7 +69,7 @@ func main() { connSvc.Stop(ctx) }, Bind: []interface{}{ - app, + sysSvc, connSvc, prefSvc, },