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/wailsapp/wails/v2/pkg/runtime"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/net/proxy"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
@ -21,6 +23,7 @@ import (
|
|||
"time"
|
||||
. "tinyrdm/backend/storage"
|
||||
"tinyrdm/backend/types"
|
||||
_ "tinyrdm/backend/utils/proxy"
|
||||
)
|
||||
|
||||
type cmdHistoryItem struct {
|
||||
|
@ -54,9 +57,34 @@ func (c *connectionService) Start(ctx context.Context) {
|
|||
}
|
||||
|
||||
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 {
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
sshConfig = &ssh.ClientConfig{
|
||||
User: config.SSH.Username,
|
||||
Auth: []ssh.AuthMethod{ssh.Password(config.SSH.Password)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
|
@ -84,11 +112,7 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
|
|||
return nil, errors.New("invalid login type")
|
||||
}
|
||||
|
||||
var err error
|
||||
sshClient, err = ssh.Dial("tcp", fmt.Sprintf("%s:%d", config.SSH.Addr, config.SSH.Port), sshConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sshAddr = fmt.Sprintf("%s:%d", config.SSH.Addr, config.SSH.Port)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
return sshClient.Dial(network, addr)
|
||||
return dialer.Dial(network, addr)
|
||||
}
|
||||
option.ReadTimeout = -2
|
||||
option.WriteTimeout = -2
|
||||
|
|
|
@ -27,6 +27,7 @@ type ConnectionConfig struct {
|
|||
SSH ConnectionSSH `json:"ssh,omitempty" yaml:"ssh,omitempty"`
|
||||
Sentinel ConnectionSentinel `json:"sentinel,omitempty" yaml:"sentinel,omitempty"`
|
||||
Cluster ConnectionCluster `json:"cluster,omitempty" yaml:"cluster,omitempty"`
|
||||
Proxy ConnectionProxy `json:"proxy,omitempty" yaml:"proxy,omitempty"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
|
@ -76,3 +77,12 @@ type ConnectionSentinel struct {
|
|||
type ConnectionCluster struct {
|
||||
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 = {}
|
||||
}
|
||||
|
||||
// trim cluster data
|
||||
if (!!!generalForm.value.cluster.enable) {
|
||||
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
|
||||
const { success, msg } = await connectionStore.saveConnection(
|
||||
isEditMode.value ? editName.value : null,
|
||||
|
@ -248,6 +262,7 @@ watch(
|
|||
pairs.push({ db: parseInt(db), alias: alias[db] })
|
||||
}
|
||||
aliasPair.value = pairs
|
||||
generalForm.value.proxy.auth = !isEmpty(generalForm.value.proxy.username)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -723,6 +738,69 @@ const pasteFromClipboard = async () => {
|
|||
<!-- label-placement="top">-->
|
||||
<!-- </n-form>-->
|
||||
</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>
|
||||
|
||||
<!-- test result alert-->
|
||||
|
|
|
@ -269,6 +269,16 @@
|
|||
"cluster": {
|
||||
"title": "Cluster",
|
||||
"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": {
|
||||
|
|
|
@ -269,6 +269,16 @@
|
|||
"cluster": {
|
||||
"title": "集群模式",
|
||||
"enable": "当前为集群节点"
|
||||
},
|
||||
"proxy": {
|
||||
"title": "网络代理",
|
||||
"type_none": "不使用代理",
|
||||
"type_system": "使用系统代理设置",
|
||||
"type_custom": "手动配置代理",
|
||||
"host": "主机名",
|
||||
"auth": "使用身份验证",
|
||||
"usr_tip": "代理授权用户名",
|
||||
"pwd_tip": "代理授权密码"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
|
|
|
@ -196,6 +196,15 @@ const useConnectionStore = defineStore('connections', {
|
|||
cluster: {
|
||||
enable: false,
|
||||
},
|
||||
proxy: {
|
||||
type: 0,
|
||||
schema: 'http',
|
||||
addr: '',
|
||||
port: 0,
|
||||
auth: false,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue