Skip to content

Commit

Permalink
feat: support HTTP/SOCKS5 proxy for connections tiny-craft#159
Browse files Browse the repository at this point in the history
  • Loading branch information
tiny-craft committed Feb 22, 2024
1 parent 64ae79f commit 13e80da
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 9 deletions.
64 changes: 55 additions & 9 deletions backend/services/connection_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -21,6 +23,7 @@ import (
"time"
. "tinyrdm/backend/storage"
"tinyrdm/backend/types"
_ "tinyrdm/backend/utils/proxy"
)

type cmdHistoryItem struct {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions backend/types/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"`
}
96 changes: 96 additions & 0 deletions backend/utils/proxy/http.go
Original file line number Diff line number Diff line change
@@ -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)
}
78 changes: 78 additions & 0 deletions frontend/src/components/dialogs/ConnectionDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
},
)
Expand Down Expand Up @@ -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-->
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/langs/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/langs/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,16 @@
"cluster": {
"title": "集群模式",
"enable": "当前为集群节点"
},
"proxy": {
"title": "网络代理",
"type_none": "不使用代理",
"type_system": "使用系统代理设置",
"type_custom": "手动配置代理",
"host": "主机名",
"auth": "使用身份验证",
"usr_tip": "代理授权用户名",
"pwd_tip": "代理授权密码"
}
},
"group": {
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/stores/connections.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ const useConnectionStore = defineStore('connections', {
cluster: {
enable: false,
},
proxy: {
type: 0,
schema: 'http',
addr: '',
port: 0,
auth: false,
username: '',
password: '',
},
}
},

Expand Down

0 comments on commit 13e80da

Please sign in to comment.