feat: support ssl connection

This commit is contained in:
tiny-craft 2023-10-15 02:27:23 +08:00
parent 444d0ea199
commit 2db858ba9e
9 changed files with 216 additions and 75 deletions

27
app.go
View File

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

View File

@ -2,10 +2,11 @@ package services
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"errors" "errors"
"fmt" "fmt"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/wailsapp/wails/v2/pkg/runtime"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"log" "log"
"net" "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{ option := &redis.Options{
ClientName: config.Name, ClientName: config.Name,
Addr: fmt.Sprintf("%s:%d", config.Addr, config.Port), 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, DialTimeout: time.Duration(config.ConnTimeout) * time.Second,
ReadTimeout: time.Duration(config.ExecTimeout) * time.Second, ReadTimeout: time.Duration(config.ExecTimeout) * time.Second,
WriteTimeout: time.Duration(config.ExecTimeout) * time.Second, WriteTimeout: time.Duration(config.ExecTimeout) * time.Second,
TLSConfig: tlsConfig,
} }
if sshClient != nil { if sshClient != nil {
option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) { option.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
@ -321,23 +356,6 @@ func (c *connectionService) SaveSortedConnection(sortedConns types.Connections)
return 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 // CreateGroup create a new group
func (c *connectionService) CreateGroup(name string) (resp types.JSResp) { func (c *connectionService) CreateGroup(name string) (resp types.JSResp) {
err := c.conns.CreateGroup(name) err := c.conns.CreateGroup(name)

View File

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

View File

@ -16,6 +16,7 @@ type ConnectionConfig struct {
DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"` DBFilterType string `json:"dbFilterType" yaml:"db_filter_type,omitempty"`
DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"` DBFilterList []int `json:"dbFilterList" yaml:"db_filter_list,omitempty"`
MarkColor string `json:"markColor,omitempty" yaml:"mark_color,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"` SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"`
Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"` Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"`
Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"` Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"`
@ -42,6 +43,13 @@ type ConnectionDB struct {
AvgTTL int `json:"avgTtl,omitempty"` 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 { type ConnectionSSH struct {
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`

View File

@ -2,10 +2,11 @@
import { every, get, includes, isEmpty, map, sortBy, toNumber } from 'lodash' import { every, get, includes, isEmpty, map, sortBy, toNumber } from 'lodash'
import { computed, nextTick, ref, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' 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 useDialog, { ConnDialogType } from 'stores/dialog'
import Close from '@/components/icons/Close.vue' import Close from '@/components/icons/Close.vue'
import useConnectionStore from 'stores/connections.js' import useConnectionStore from 'stores/connections.js'
import { SelectFile } from 'wailsjs/go/services/systemService.js'
/** /**
* Dialog for new or edit connection * Dialog for new or edit connection
@ -82,12 +83,36 @@ const sshLoginType = computed(() => {
return get(generalForm.value, 'ssh.loginType', 'pwd') return get(generalForm.value, 'ssh.loginType', 'pwd')
}) })
const onChoosePKFile = async () => { const onSSHChooseKey = async () => {
const { success, data } = await SelectKeyFile(i18n.t('dialogue.connection.ssh.pkfile_selection_title')) 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) { if (!success) {
generalForm.value.ssh.pkFile = '' generalForm.value.ssl.caFile = ''
} else { } 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 = [] generalForm.value.dbFilterList = []
} }
// trim ssl data
if (!!!generalForm.value.ssl.enable) {
generalForm.value.ssl = {}
}
// trim ssh login data // trim ssh login data
if (generalForm.value.ssh.enable) { if (!!generalForm.value.ssh.enable) {
switch (generalForm.value.ssh.loginType) { switch (generalForm.value.ssh.loginType) {
case 'pkfile': case 'pkfile':
generalForm.value.ssh.password = '' generalForm.value.ssh.password = ''
@ -162,15 +192,16 @@ const onSaveConnection = async () => {
} }
} else { } else {
// ssh disabled, reset to default value // ssh disabled, reset to default value
const { ssh } = connectionStore.newDefaultConnection() generalForm.value.ssh = {}
generalForm.value.ssh = ssh
} }
// trim sentinel data // trim sentinel data
if (!generalForm.value.sentinel.enable) { if (!!!generalForm.value.sentinel.enable) {
generalForm.value.sentinel.master = '' generalForm.value.sentinel = {}
generalForm.value.sentinel.username = '' }
generalForm.value.sentinel.password = ''
if (!!!generalForm.value.cluster.enable) {
generalForm.value.cluster = {}
} }
// store new connection // store new connection
@ -387,6 +418,60 @@ const onClose = () => {
</n-form> </n-form>
</n-tab-pane> </n-tab-pane>
<!-- SSL pane -->
<n-tab-pane :tab="$t('dialogue.connection.ssl.title')" display-directive="show" name="ssl">
<n-form-item label-placement="left">
<n-checkbox v-model:checked="generalForm.ssl.enable" size="medium">
{{ $t('dialogue.connection.ssl.enable') }}
</n-checkbox>
</n-form-item>
<n-form
:model="generalForm.ssl"
:show-require-mark="false"
:disabled="!generalForm.ssl.enable"
label-placement="top">
<n-form-item :label="$t('dialogue.connection.ssl.cert_file')">
<n-input-group>
<n-input
v-model:value="generalForm.ssl.certFile"
:placeholder="$t('dialogue.connection.ssl.cert_file_tip')"
clearable />
<n-button
:focusable="false"
:disabled="!generalForm.ssl.enable"
@click="onSSLChooseCert">
...
</n-button>
</n-input-group>
</n-form-item>
<n-form-item :label="$t('dialogue.connection.ssl.key_file')">
<n-input-group>
<n-input
v-model:value="generalForm.ssl.keyFile"
:placeholder="$t('dialogue.connection.ssl.key_file_tip')"
clearable />
<n-button
:focusable="false"
:disabled="!generalForm.ssl.enable"
@click="onSSLChooseKey">
...
</n-button>
</n-input-group>
</n-form-item>
<n-form-item :label="$t('dialogue.connection.ssl.ca_file')">
<n-input-group>
<n-input
v-model:value="generalForm.ssl.caFile"
:placeholder="$t('dialogue.connection.ssl.ca_file_tip')"
clearable />
<n-button :focusable="false" :disabled="!generalForm.ssl.enable" @click="onSSLChooseCA">
...
</n-button>
</n-input-group>
</n-form-item>
</n-form>
</n-tab-pane>
<!-- SSH pane --> <!-- SSH pane -->
<n-tab-pane :tab="$t('dialogue.connection.ssh.title')" display-directive="show" name="ssh"> <n-tab-pane :tab="$t('dialogue.connection.ssh.title')" display-directive="show" name="ssh">
<n-form-item label-placement="left"> <n-form-item label-placement="left">
@ -435,7 +520,13 @@ const onClose = () => {
<n-input <n-input
v-model:value="generalForm.ssh.pkFile" v-model:value="generalForm.ssh.pkFile"
:placeholder="$t('dialogue.connection.ssh.pkfile_tip')" /> :placeholder="$t('dialogue.connection.ssh.pkfile_tip')" />
<n-button :focusable="false" @click="onChoosePKFile">...</n-button> <n-button
:focusable="false"
:disabled="!generalForm.ssh.enable"
@click="onSSHChooseKey"
clearable>
...
</n-button>
</n-input-group> </n-input-group>
</n-form-item> </n-form-item>
<n-form-item v-if="sshLoginType === 'pkfile'" :label="$t('dialogue.connection.ssh.passphrase')"> <n-form-item v-if="sshLoginType === 'pkfile'" :label="$t('dialogue.connection.ssh.passphrase')">

View File

@ -150,12 +150,12 @@
"ssl": { "ssl": {
"title": "SSL/TLS", "title": "SSL/TLS",
"enable": "Enable SSL/TLS", "enable": "Enable SSL/TLS",
"key_file": "Public Key", "cert_file": "Public Key",
"cert_file": "Private Key", "key_file": "Private Key",
"ca_file": "Authority", "ca_file": "Authority",
"key_file_tip": "Public Key File in PEM format", "cert_file_tip":"Public Key File in PEM format(Cert)",
"cert_file_tip":"Private Key File in PEM format", "key_file_tip": "Private Key File in PEM format(Key)",
"ca_file_tip": "Certificate Authority File in PEM format" "ca_file_tip": "Certificate Authority File in PEM format(CA)"
}, },
"ssh": { "ssh": {
"title": "SSH Tunnel", "title": "SSH Tunnel",
@ -167,8 +167,7 @@
"usr_tip": "SSH Username", "usr_tip": "SSH Username",
"pwd_tip": "SSH Password", "pwd_tip": "SSH Password",
"pkfile_tip": "SSH Private Key File Path", "pkfile_tip": "SSH Private Key File Path",
"passphrase_tip": "(Optional) Passphrase for Private Key", "passphrase_tip": "(Optional) Passphrase for Private Key"
"pkfile_selection_title": "Please Select Private Key File"
}, },
"sentinel": { "sentinel": {
"title": "Sentinel", "title": "Sentinel",

View File

@ -150,12 +150,12 @@
"ssl": { "ssl": {
"title": "SSL/TLS", "title": "SSL/TLS",
"enable": "启用SSL", "enable": "启用SSL",
"key_file": "公钥文件", "cert_file": "公钥文件",
"cert_file": "私钥文件", "key_file": "私钥文件",
"ca_file": "授权文件", "ca_file": "授权文件",
"key_file_tip": "PEM格式公钥文件", "cert_file_tip":"PEM格式公钥文件(Cert)",
"cert_file_tip":"PEM格式私钥文件", "key_file_tip": "PEM格式私钥文件(Key)",
"ca_file_tip": "PEM格式授权文件" "ca_file_tip": "PEM格式授权文件(CA)"
}, },
"ssh": { "ssh": {
"enable": "启用SSH隧道", "enable": "启用SSH隧道",
@ -167,8 +167,7 @@
"usr_tip": "SSH登录用户名", "usr_tip": "SSH登录用户名",
"pwd_tip": "SSH登录密码", "pwd_tip": "SSH登录密码",
"pkfile_tip": "SSH私钥文件路径", "pkfile_tip": "SSH私钥文件路径",
"passphrase_tip": "(可选)SSH私钥密码", "passphrase_tip": "(可选)SSH私钥密码"
"pkfile_selection_title": "请选择私钥文件"
}, },
"sentinel": { "sentinel": {
"title": "哨兵模式", "title": "哨兵模式",

View File

@ -231,6 +231,12 @@ const useConnectionStore = defineStore('connections', {
dbFilterType: 'none', dbFilterType: 'none',
dbFilterList: [], dbFilterList: [],
markColor: '', markColor: '',
ssl: {
enable: false,
certFile: '',
keyFile: '',
caFile: '',
},
ssh: { ssh: {
enable: false, enable: false,
addr: '', addr: '',

View File

@ -26,7 +26,7 @@ var version = "0.0.0"
func main() { func main() {
// Create an instance of the app structure // Create an instance of the app structure
app := NewApp() sysSvc := services.System()
connSvc := services.Connection() connSvc := services.Connection()
prefSvc := services.Preferences() prefSvc := services.Preferences()
prefSvc.SetAppVersion(version) prefSvc.SetAppVersion(version)
@ -54,7 +54,7 @@ func main() {
}, },
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 0}, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 0},
OnStartup: func(ctx context.Context) { OnStartup: func(ctx context.Context) {
app.startup(ctx) sysSvc.Start(ctx)
connSvc.Start(ctx) connSvc.Start(ctx)
}, },
OnBeforeClose: func(ctx context.Context) (prevent bool) { OnBeforeClose: func(ctx context.Context) (prevent bool) {
@ -69,7 +69,7 @@ func main() {
connSvc.Stop(ctx) connSvc.Stop(ctx)
}, },
Bind: []interface{}{ Bind: []interface{}{
app, sysSvc,
connSvc, connSvc,
prefSvc, prefSvc,
}, },