feat: support HTTP/SOCKS5 proxy for connections #159
This commit is contained in:
parent
64ae79f565
commit
13e80da978
|
@ -11,8 +11,10 @@ import (
|
||||||
"github.com/vrischmann/userdir"
|
"github.com/vrischmann/userdir"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -21,6 +23,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
. "tinyrdm/backend/storage"
|
. "tinyrdm/backend/storage"
|
||||||
"tinyrdm/backend/types"
|
"tinyrdm/backend/types"
|
||||||
|
_ "tinyrdm/backend/utils/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cmdHistoryItem struct {
|
type cmdHistoryItem struct {
|
||||||
|
@ -54,9 +57,34 @@ func (c *connectionService) Start(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.Options, error) {
|
func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.Options, error) {
|
||||||
var sshClient *ssh.Client
|
var dialer proxy.Dialer
|
||||||
|
var dialerErr error
|
||||||
|
if config.Proxy.Type == 1 {
|
||||||
|
// use system proxy
|
||||||
|
dialer = proxy.FromEnvironment()
|
||||||
|
} else if config.Proxy.Type == 2 {
|
||||||
|
// use custom proxy
|
||||||
|
proxyUrl := url.URL{
|
||||||
|
Host: fmt.Sprintf("%s:%d", config.Proxy.Addr, config.Proxy.Port),
|
||||||
|
}
|
||||||
|
if len(config.Proxy.Username) > 0 {
|
||||||
|
proxyUrl.User = url.UserPassword(config.Proxy.Username, config.Proxy.Password)
|
||||||
|
}
|
||||||
|
switch config.Proxy.Schema {
|
||||||
|
case "socks5", "socks5h", "http", "https":
|
||||||
|
proxyUrl.Scheme = config.Proxy.Schema
|
||||||
|
default:
|
||||||
|
proxyUrl.Scheme = "http"
|
||||||
|
}
|
||||||
|
if dialer, dialerErr = proxy.FromURL(&proxyUrl, proxy.Direct); dialerErr != nil {
|
||||||
|
return nil, dialerErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sshConfig *ssh.ClientConfig
|
||||||
|
var sshAddr string
|
||||||
if config.SSH.Enable {
|
if config.SSH.Enable {
|
||||||
sshConfig := &ssh.ClientConfig{
|
sshConfig = &ssh.ClientConfig{
|
||||||
User: config.SSH.Username,
|
User: config.SSH.Username,
|
||||||
Auth: []ssh.AuthMethod{ssh.Password(config.SSH.Password)},
|
Auth: []ssh.AuthMethod{ssh.Password(config.SSH.Password)},
|
||||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
@ -84,11 +112,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
||||||
return nil, errors.New("invalid login type")
|
return nil, errors.New("invalid login type")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
sshAddr = fmt.Sprintf("%s:%d", config.SSH.Addr, config.SSH.Port)
|
||||||
sshClient, err = ssh.Dial("tcp", fmt.Sprintf("%s:%d", config.SSH.Addr, config.SSH.Port), sshConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
|
@ -150,9 +174,31 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
||||||
option.Addr = fmt.Sprintf("%s:%d", config.Addr, port)
|
option.Addr = fmt.Sprintf("%s:%d", config.Addr, port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sshClient != nil {
|
|
||||||
|
if len(sshAddr) > 0 {
|
||||||
|
if dialer != nil {
|
||||||
|
// ssh with proxy
|
||||||
|
conn, err := dialer.Dial("tcp", sshAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sc, chans, reqs, err := ssh.NewClientConn(conn, sshAddr, sshConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dialer = ssh.NewClient(sc, chans, reqs)
|
||||||
|
} else {
|
||||||
|
// ssh without proxy
|
||||||
|
sshClient, err := ssh.Dial("tcp", sshAddr, sshConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dialer = sshClient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dialer != 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) {
|
||||||
return sshClient.Dial(network, addr)
|
return dialer.Dial(network, addr)
|
||||||
}
|
}
|
||||||
option.ReadTimeout = -2
|
option.ReadTimeout = -2
|
||||||
option.WriteTimeout = -2
|
option.WriteTimeout = -2
|
||||||
|
|
|
@ -27,6 +27,7 @@ type ConnectionConfig struct {
|
||||||
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"`
|
||||||
|
Proxy ConnectionProxy `json:"proxy,omitempty" yaml:"proxy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connection struct {
|
type Connection struct {
|
||||||
|
@ -76,3 +77,12 @@ type ConnectionSentinel struct {
|
||||||
type ConnectionCluster struct {
|
type ConnectionCluster struct {
|
||||||
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConnectionProxy struct {
|
||||||
|
Type int `json:"type,omitempty" yaml:"type,omitempty"`
|
||||||
|
Schema string `json:"schema,omitempty" yaml:"schema,omitempty"`
|
||||||
|
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
|
||||||
|
Port int `json:"port,omitempty" yaml:"port,omitempty"`
|
||||||
|
Username string `json:"username,omitempty" yaml:"username,omitempty"`
|
||||||
|
Password string `json:"password,omitempty" yaml:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpProxy struct {
|
||||||
|
scheme string // HTTP Proxy scheme
|
||||||
|
host string // HTTP Proxy host or host:port
|
||||||
|
auth *proxy.Auth // authentication
|
||||||
|
forward proxy.Dialer // forwarding Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HttpProxy) Dial(network, addr string) (net.Conn, error) {
|
||||||
|
c, err := p.forward.Dial(network, p.host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.SetDeadline(time.Now().Add(15 * time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqUrl := &url.URL{
|
||||||
|
Scheme: "",
|
||||||
|
Host: addr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create with CONNECT method
|
||||||
|
req, err := http.NewRequest("CONNECT", reqUrl.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Close = false
|
||||||
|
|
||||||
|
// authentication
|
||||||
|
if p.auth != nil {
|
||||||
|
req.SetBasicAuth(p.auth.User, p.auth.Password)
|
||||||
|
req.Header.Add("Proxy-Authorization", req.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// send request
|
||||||
|
err = req.Write(c)
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := http.ReadResponse(bufio.NewReader(c), req)
|
||||||
|
if err != nil {
|
||||||
|
res.Body.Close()
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
c.Close()
|
||||||
|
return nil, fmt.Errorf("proxy connection error: StatusCode[%d]", res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpProxyDialer(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
|
||||||
|
var auth *proxy.Auth
|
||||||
|
if u.User != nil {
|
||||||
|
pwd, _ := u.User.Password()
|
||||||
|
auth = &proxy.Auth{
|
||||||
|
User: u.User.Username(),
|
||||||
|
Password: pwd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hp := &HttpProxy{
|
||||||
|
scheme: u.Scheme,
|
||||||
|
host: u.Host,
|
||||||
|
auth: auth,
|
||||||
|
forward: forward,
|
||||||
|
}
|
||||||
|
return hp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proxy.RegisterDialerType("http", NewHttpProxyDialer)
|
||||||
|
proxy.RegisterDialerType("https", NewHttpProxyDialer)
|
||||||
|
}
|
|
@ -205,10 +205,24 @@ const onSaveConnection = async () => {
|
||||||
generalForm.value.sentinel = {}
|
generalForm.value.sentinel = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trim cluster data
|
||||||
if (!!!generalForm.value.cluster.enable) {
|
if (!!!generalForm.value.cluster.enable) {
|
||||||
generalForm.value.cluster = {}
|
generalForm.value.cluster = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trim proxy data
|
||||||
|
if (generalForm.value.proxy.type !== 2) {
|
||||||
|
generalForm.value.proxy.schema = ''
|
||||||
|
generalForm.value.proxy.addr = ''
|
||||||
|
generalForm.value.proxy.port = 0
|
||||||
|
generalForm.value.proxy.auth = false
|
||||||
|
generalForm.value.proxy.username = ''
|
||||||
|
generalForm.value.proxy.password = ''
|
||||||
|
} else if (!generalForm.value.proxy.auth) {
|
||||||
|
generalForm.value.proxy.username = ''
|
||||||
|
generalForm.value.proxy.password = ''
|
||||||
|
}
|
||||||
|
|
||||||
// store new connection
|
// store new connection
|
||||||
const { success, msg } = await connectionStore.saveConnection(
|
const { success, msg } = await connectionStore.saveConnection(
|
||||||
isEditMode.value ? editName.value : null,
|
isEditMode.value ? editName.value : null,
|
||||||
|
@ -248,6 +262,7 @@ watch(
|
||||||
pairs.push({ db: parseInt(db), alias: alias[db] })
|
pairs.push({ db: parseInt(db), alias: alias[db] })
|
||||||
}
|
}
|
||||||
aliasPair.value = pairs
|
aliasPair.value = pairs
|
||||||
|
generalForm.value.proxy.auth = !isEmpty(generalForm.value.proxy.username)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -723,6 +738,69 @@ const pasteFromClipboard = async () => {
|
||||||
<!-- label-placement="top">-->
|
<!-- label-placement="top">-->
|
||||||
<!-- </n-form>-->
|
<!-- </n-form>-->
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- Proxy pane -->
|
||||||
|
<n-tab-pane :tab="$t('dialogue.connection.proxy.title')" display-directive="show:lazy" name="proxy">
|
||||||
|
<n-radio-group v-model:value="generalForm.proxy.type" name="radiogroup">
|
||||||
|
<n-space size="large" vertical>
|
||||||
|
<n-radio :label="$t('dialogue.connection.proxy.type_none')" :value="0" />
|
||||||
|
<n-radio :label="$t('dialogue.connection.proxy.type_system')" :value="1" />
|
||||||
|
<n-radio :label="$t('dialogue.connection.proxy.type_custom')" :value="2" />
|
||||||
|
<n-form
|
||||||
|
:disabled="generalForm.proxy.type !== 2"
|
||||||
|
:model="generalForm.proxy"
|
||||||
|
:show-require-mark="false"
|
||||||
|
label-placement="top">
|
||||||
|
<n-grid :x-gap="10">
|
||||||
|
<n-form-item-gi :show-label="false" :span="24" path="addr" required>
|
||||||
|
<n-input-group>
|
||||||
|
<n-select
|
||||||
|
v-model:value="generalForm.proxy.schema"
|
||||||
|
:consistent-menu-width="false"
|
||||||
|
:options="[
|
||||||
|
{ value: 'http', label: 'HTTP' },
|
||||||
|
{ value: 'https', label: 'HTTPS' },
|
||||||
|
{ value: 'socks5', label: 'SOCKS5' },
|
||||||
|
{ value: 'socks5h', label: 'SOCKS5H' },
|
||||||
|
]"
|
||||||
|
default-value="http"
|
||||||
|
style="max-width: 100px" />
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.proxy.addr"
|
||||||
|
:placeholder="$t('dialogue.connection.proxy.host')" />
|
||||||
|
<n-text style="width: 40px; text-align: center">:</n-text>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="generalForm.proxy.port"
|
||||||
|
:max="65535"
|
||||||
|
:min="0"
|
||||||
|
:show-button="false"
|
||||||
|
style="width: 200px" />
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :show-label="false" :span="24" path="auth">
|
||||||
|
<n-checkbox v-model:checked="generalForm.proxy.auth" size="medium">
|
||||||
|
{{ $t('dialogue.connection.proxy.auth') }}
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="$t('dialogue.connection.usr')" :span="12" path="username">
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.proxy.username"
|
||||||
|
:disabled="!!!generalForm.proxy.auth"
|
||||||
|
:placeholder="$t('dialogue.connection.proxy.usr_tip')" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi :label="$t('dialogue.connection.pwd')" :span="12" path="password">
|
||||||
|
<n-input
|
||||||
|
v-model:value="generalForm.proxy.password"
|
||||||
|
:disabled="!!!generalForm.proxy.auth"
|
||||||
|
:placeholder="$t('dialogue.connection.proxy.pwd_tip')"
|
||||||
|
show-password-on="click"
|
||||||
|
type="password" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-form>
|
||||||
|
</n-space>
|
||||||
|
</n-radio-group>
|
||||||
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
|
|
||||||
<!-- test result alert-->
|
<!-- test result alert-->
|
||||||
|
|
|
@ -269,6 +269,16 @@
|
||||||
"cluster": {
|
"cluster": {
|
||||||
"title": "Cluster",
|
"title": "Cluster",
|
||||||
"enable": "Serve as Cluster Node"
|
"enable": "Serve as Cluster Node"
|
||||||
|
},
|
||||||
|
"proxy": {
|
||||||
|
"title": "Proxy",
|
||||||
|
"type_none": "No Proxy",
|
||||||
|
"type_system": "System Proxy Configuration",
|
||||||
|
"type_custom": "Manual Proxy Configuration",
|
||||||
|
"host": "Host name",
|
||||||
|
"auth": "Proxy authentication",
|
||||||
|
"usr_tip": "Username for proxy authentication",
|
||||||
|
"pwd_tip": "Password for proxy authentication"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
|
|
|
@ -269,6 +269,16 @@
|
||||||
"cluster": {
|
"cluster": {
|
||||||
"title": "集群模式",
|
"title": "集群模式",
|
||||||
"enable": "当前为集群节点"
|
"enable": "当前为集群节点"
|
||||||
|
},
|
||||||
|
"proxy": {
|
||||||
|
"title": "网络代理",
|
||||||
|
"type_none": "不使用代理",
|
||||||
|
"type_system": "使用系统代理设置",
|
||||||
|
"type_custom": "手动配置代理",
|
||||||
|
"host": "主机名",
|
||||||
|
"auth": "使用身份验证",
|
||||||
|
"usr_tip": "代理授权用户名",
|
||||||
|
"pwd_tip": "代理授权密码"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
|
|
|
@ -196,6 +196,15 @@ const useConnectionStore = defineStore('connections', {
|
||||||
cluster: {
|
cluster: {
|
||||||
enable: false,
|
enable: false,
|
||||||
},
|
},
|
||||||
|
proxy: {
|
||||||
|
type: 0,
|
||||||
|
schema: 'http',
|
||||||
|
addr: '',
|
||||||
|
port: 0,
|
||||||
|
auth: false,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue