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