@wwkit/sshproxy 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bluesliu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # sshproxy
2
+
3
+ SOCKS5 代理管理器(CLI + 本地 Web 管理页)。通过本地 SSH 隧道(autossh)提供 SOCKS5 代理,全局命令为 `sshproxy`。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add -g @wwkit/sshproxy
9
+ # 或
10
+ npm i -g @wwkit/sshproxy
11
+ ```
12
+
13
+ ## 配置
14
+
15
+ 全局配置:`~/.config/sshproxy/config.json5`。安装时(postinstall 钩子)自动把默认模板复制到该位置,用户可直接编辑;未复制到也可首次运行时自动生成。
16
+
17
+ ```json5
18
+ {
19
+ socks5: { host: "127.0.0.1", port: 1080 },
20
+ host: "", port: 22, user: "", password: "", timeout: 30,
21
+ web: { host: "127.0.0.1", port: 1081 }
22
+ }
23
+ ```
24
+
25
+ 支持 `{env:XXX}` 占位符与环境变量覆盖(`SSHPROXY_CONFIG_DIR` 指定配置目录、`SSHPROXY_CONFIG_CONTENT` 直接提供配置内容)。
26
+
27
+ 配置加载由 `@wwkit/shared` 的多层加载器实现:**内置默认 → `SSHPROXY_CONFIG_CONTENT` 注入 → 用户 `config.json5`** 三层深合并(用户优先),并对结果统一解析 `{env:XXX}` 占位符。
28
+
29
+ ### 系统内置常量(contains.json5)
30
+
31
+ 路径/SSH 密钥/autossh 参数/系统代理/调试/Web 轮询等**内部常量**内置于 `src/contains.json5`(随包发布,系统级、用户不可改)。运行时始终使用内置值,不与用户配置合并。
32
+
33
+ ## 命令
34
+
35
+ 命令按功能模块划分。
36
+
37
+ ### 隧道(SOCKS5 服务)
38
+
39
+ ```bash
40
+ sshproxy tunnel on # 启动 SSH 隧道(SOCKS5,默认 127.0.0.1:1080)
41
+ sshproxy tunnel off # 停止隧道
42
+ ```
43
+
44
+ `tunnel on` 已运行不会重复启动;首次运行会自动完成初始化(SSH 密钥/公钥/ssh config/autossh)。
45
+
46
+ ### 系统全局代理
47
+
48
+ ```bash
49
+ sshproxy proxy on # 开启系统全局代理(隧道未运行则先启动)
50
+ sshproxy proxy off # 关闭系统全局代理
51
+ ```
52
+
53
+ ### 代理 Chrome
54
+
55
+ ```bash
56
+ sshproxy chrome on # 以代理模式启动 Chrome(隧道未运行则先启动)
57
+ sshproxy chrome off # 关闭代理 Chrome
58
+ ```
59
+
60
+ ### 组合命令
61
+
62
+ ```bash
63
+ sshproxy start # 隧道 + 系统代理,一键全开
64
+ sshproxy stop # 隧道 + 系统代理 + Chrome,一键全关
65
+ sshproxy status # 查看全部状态(隧道/系统代理/Chrome/Web)
66
+ ```
67
+
68
+ ### Web 管理页
69
+
70
+ ```bash
71
+ sshproxy web on # 启动 Web 管理页(后台,默认 http://127.0.0.1:1081)
72
+ sshproxy web off # 关闭 Web 管理页
73
+ ```
74
+
75
+ ### 信息与检查
76
+
77
+ ```bash
78
+ sshproxy debug # 全链路检查(验证代理是否可用)
79
+ sshproxy help # 显示帮助
80
+ ```
81
+
82
+ ## 本地开发调用示例
83
+
84
+ 在 monorepo 根目录或 `packages/sshproxy` 内均可运行。若未全局安装 `sshproxy`,用以下方式调用:
85
+
86
+ ```bash
87
+ # 在 packages/sshproxy 目录内,直接跑源码入口
88
+ node ./bin tunnel on
89
+ node ./bin proxy on
90
+ node ./bin status
91
+
92
+ # 或从 monorepo 根目录,通过 pnpm 过滤到子包
93
+ pnpm --filter @wwkit/sshproxy exec sshproxy status
94
+ ```
95
+
96
+ 本地开发时配置走默认路径 `~/.config/sshproxy/config.json5`;也可用环境变量临时指到测试配置,避免影响真实配置:
97
+
98
+ ```bash
99
+ # 用临时配置目录跑(首次会自动初始化)
100
+ SSHPROXY_CONFIG_DIR=/tmp/sshproxy-dev node ./bin tunnel on
101
+
102
+ # 或直接注入配置内容
103
+ SSHPROXY_CONFIG_CONTENT='{ host: "example.com", user: "root", password: "***" }' \
104
+ node ./bin tunnel on
105
+ ```
106
+
107
+ ## Web 管理页
108
+
109
+ `sshproxy web on` 在后台启动 HTTP 服务(`web off` 关闭),提供 REST API 与静态 `index.html`:
110
+
111
+ - `GET /api/status` — 代理状态
112
+ - `GET /api/config` — 系统内置常量
113
+ - `POST /api/tunnel` `{action:"on"|"off"}` — 隧道启停
114
+ - `POST /api/proxy` `{action:"on"|"off"}` — 系统代理开关
115
+ - `POST /api/chrome` `{action:"on"|"off"}` — Chrome 启停
116
+ - `POST /api/start` / `POST /api/stop` — 一键全开/全关
117
+ - `POST /api/debug` — 全链路检查
118
+
119
+ ## 前置条件
120
+
121
+ - `autossh`(macOS 可用 `brew install autossh`)
122
+ - 系统代理:macOS 使用 `networksetup`;Linux 使用 `ALL_PROXY` 环境变量
123
+ - 代理 Chrome:本机安装 Google Chrome
package/bin/index.js ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { ProxyManager } from '../src/ProxyManager.js'
4
+ import { clientAddr, loadConfig } from '../src/config.js'
5
+
6
+ const args = process.argv.slice(2)
7
+ const cmd = args[0] ?? 'help'
8
+ const sub = args[1] ?? ''
9
+
10
+ async function main() {
11
+ switch (cmd) {
12
+ case 'tunnel':
13
+ return handleTunnel(sub)
14
+ case 'proxy':
15
+ return handleProxy(sub)
16
+ case 'chrome':
17
+ return handleChrome(sub)
18
+ case 'start':
19
+ return handleStart()
20
+ case 'stop':
21
+ return handleStop()
22
+ case 'status':
23
+ return handleStatus()
24
+ case 'debug':
25
+ return handleDebug()
26
+ case 'web':
27
+ return handleWeb(sub)
28
+ case 'help':
29
+ default:
30
+ printHelp()
31
+ }
32
+ }
33
+
34
+ function printResult(result) {
35
+ if (!result) return
36
+ if (result.message) console.log(result.message)
37
+ if (result.ok === false) process.exitCode = 1
38
+ }
39
+
40
+ async function handleTunnel(sub) {
41
+ const manager = new ProxyManager()
42
+ if (sub === 'off') return printResult(await manager.tunnelStop())
43
+ return printResult(await manager.tunnelStart())
44
+ }
45
+
46
+ async function handleProxy(sub) {
47
+ const manager = new ProxyManager()
48
+ if (sub === 'off') return printResult(await manager.proxyOff())
49
+ return printResult(await manager.proxyOn())
50
+ }
51
+
52
+ async function handleChrome(sub) {
53
+ const manager = new ProxyManager()
54
+ if (sub === 'off') return printResult(await manager.chromeStop())
55
+ return printResult(await manager.chromeStart())
56
+ }
57
+
58
+ async function handleStart() {
59
+ const manager = new ProxyManager()
60
+ const result = await manager.start()
61
+ if (result.ok) {
62
+ const [host, port] = clientAddr()
63
+ console.log(`代理地址: SOCKS5 ${host}:${port}`)
64
+ }
65
+ printResult(result)
66
+ }
67
+
68
+ async function handleStop() {
69
+ const manager = new ProxyManager()
70
+ printResult(await manager.stop())
71
+ }
72
+
73
+ function handleStatus() {
74
+ const manager = new ProxyManager()
75
+ const status = manager.getStatus()
76
+ const tunnel = status.tunnel
77
+ if (tunnel && tunnel.running) {
78
+ console.log(`隧道: 运行中 (PID=${tunnel.pid})`)
79
+ } else {
80
+ console.log('隧道: 未运行')
81
+ }
82
+ console.log(`代理地址: SOCKS5 ${status.host}:${status.port}`)
83
+ console.log(`系统代理: ${status.system_proxy.enabled ? '已开启' : '已关闭'}`)
84
+ const chrome = status.chrome
85
+ console.log(`代理 Chrome: ${chrome.running ? `运行中 (PID=${chrome.pid})` : '未运行'}`)
86
+ const web = status.web
87
+ if (web.running) {
88
+ const webCfg = loadConfig().web ?? {}
89
+ console.log(`Web 管理页: 运行中 (PID=${web.pid})`)
90
+ console.log(` 管理页地址: http://${webCfg.host ?? '127.0.0.1'}:${webCfg.port ?? 1081}`)
91
+ } else {
92
+ console.log('Web 管理页: 未运行')
93
+ }
94
+ }
95
+
96
+ async function handleDebug() {
97
+ const manager = new ProxyManager()
98
+ const checks = await manager.debug()
99
+ for (const check of checks) {
100
+ const name = check.name
101
+ const detail = check.detail
102
+ const ok = check.ok
103
+ if (ok === null) {
104
+ console.log(` - ${name}`)
105
+ } else {
106
+ const mark = ok ? '✓' : '✗'
107
+ console.log(` ${mark} ${name}`)
108
+ }
109
+ if (detail) {
110
+ console.log(` ${detail}`)
111
+ }
112
+ if (ok === false) {
113
+ console.log('')
114
+ console.log(`检查中止于: ${name}`)
115
+ return
116
+ }
117
+ }
118
+ console.log('')
119
+ console.log('全部通过,代理可用')
120
+ }
121
+
122
+ async function handleWeb(sub) {
123
+ const manager = new ProxyManager()
124
+ if (sub === 'off') {
125
+ const result = await manager.webStop()
126
+ console.log(result.message)
127
+ return
128
+ }
129
+ const result = await manager.webStart()
130
+ console.log(result.message)
131
+ if (result.ok) {
132
+ const cfg = loadConfig().web ?? {}
133
+ console.log(` 管理页: http://${cfg.host ?? '127.0.0.1'}:${cfg.port ?? 1081}`)
134
+ }
135
+ }
136
+
137
+ function printHelp() {
138
+ console.log(`sshproxy - SSH 代理命令
139
+
140
+ 隧道(SOCKS5 服务):
141
+ sshproxy tunnel 启动 SSH 隧道(= tunnel on)
142
+ sshproxy tunnel on 启动 SSH 隧道
143
+ sshproxy tunnel off 停止隧道
144
+
145
+ 系统全局代理:
146
+ sshproxy proxy 开启系统全局代理(隧道未运行则先启动)
147
+ sshproxy proxy on 开启系统全局代理
148
+ sshproxy proxy off 关闭系统全局代理
149
+
150
+ 代理 Chrome:
151
+ sshproxy chrome 以代理模式启动 Chrome(隧道未运行则先启动)
152
+ sshproxy chrome on 以代理模式启动 Chrome
153
+ sshproxy chrome off 关闭代理 Chrome
154
+
155
+ 一键:
156
+ sshproxy start 隧道 + 系统代理,一键全开
157
+ sshproxy stop 隧道 + 系统代理 + Chrome,一键全关
158
+
159
+ 状态与工具:
160
+ sshproxy status 查看全部状态
161
+ sshproxy debug 全链路检查(验证代理是否可用)
162
+ sshproxy web 启动 Web 管理页(后台,默认 http://127.0.0.1:1081)
163
+ sshproxy web on 启动 Web 管理页
164
+ sshproxy web off 关闭 Web 管理页
165
+ sshproxy help 显示本帮助
166
+
167
+ 配置: ~/.config/sshproxy/config.json5
168
+ `)
169
+ }
170
+
171
+ main()
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@wwkit/sshproxy",
3
+ "version": "1.0.1",
4
+ "author": "bluesliu <langcai163@163.com>",
5
+ "description": "SOCKS5 proxy manager (client SSH tunnel / server remote), CLI + web UI",
6
+ "type": "module",
7
+ "main": "src/index.js",
8
+ "bin": {
9
+ "sshproxy": "./bin/index.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "scripts",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "jest": {
19
+ "transform": {},
20
+ "testMatch": [
21
+ "**/tests/unit/**/*.test.js"
22
+ ]
23
+ },
24
+ "keywords": [
25
+ "socks5",
26
+ "proxy",
27
+ "ssh",
28
+ "autossh",
29
+ "chrome"
30
+ ],
31
+ "license": "MIT",
32
+ "publishConfig": {
33
+ "registry": "https://registry.npmjs.org/",
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "dotenv": "^16.4.5",
38
+ "json5": "^2.2.3",
39
+ "ssh2": "^1.16.0",
40
+ "@wwkit/shared": "1.0.1"
41
+ },
42
+ "devDependencies": {
43
+ "jest": "^29.7.0"
44
+ },
45
+ "scripts": {
46
+ "postinstall": "node scripts/postinstall.js",
47
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache tests/unit",
48
+ "test:all": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache tests/unit",
49
+ "release": "pnpm version patch && pnpm publish"
50
+ }
51
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { createConfigLoader, getXdgConfigDir } from '@wwkit/shared'
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+ const src = path.join(__dirname, '..', 'src', 'config.json5')
9
+
10
+ const loader = createConfigLoader({
11
+ builtinConfigPath: src,
12
+ userConfigDir: getXdgConfigDir('sshproxy'),
13
+ envDirVar: 'SSHPROXY_CONFIG_DIR',
14
+ })
15
+
16
+ if (loader.copyBuiltinConfig()) {
17
+ console.log(`sshproxy: wrote default config to ${loader.userConfigPath}`)
18
+ }
package/src/OSystem.js ADDED
@@ -0,0 +1,131 @@
1
+ import { execFileSync, execFile, spawnSync } from 'node:child_process'
2
+ import path from 'node:path'
3
+ import fs from 'node:fs'
4
+
5
+ import { getResolvedStaticConfig } from './config.js'
6
+
7
+ export function getOsType() {
8
+ if (process.platform === 'win32') return 'windows'
9
+ if (process.platform === 'linux') return 'linux'
10
+ if (process.platform === 'darwin') return 'mac'
11
+ return 'unknown'
12
+ }
13
+
14
+ export function getChromePath() {
15
+ const type = getOsType()
16
+ if (type === 'mac') {
17
+ const p = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
18
+ return fs.existsSync(p) ? p : null
19
+ }
20
+ if (type === 'linux') {
21
+ return which('google-chrome') ?? which('google-chrome-stable')
22
+ }
23
+ if (type === 'windows') {
24
+ for (const base of [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)']]) {
25
+ if (!base) continue
26
+ const candidate = path.join(base, 'Google', 'Chrome', 'Application', 'chrome.exe')
27
+ if (fs.existsSync(candidate)) return candidate
28
+ }
29
+ return null
30
+ }
31
+ return null
32
+ }
33
+
34
+ export function which(cmd) {
35
+ try {
36
+ const out = execFileSync('which', [cmd], { encoding: 'utf-8' }).trim()
37
+ return out || null
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ export function killProcess(name) {
44
+ const type = getOsType()
45
+ if (type === 'mac') {
46
+ spawnSync('killall', ['-m', name])
47
+ return true
48
+ }
49
+ if (type === 'linux') {
50
+ spawnSync('pkill', ['-f', name])
51
+ return true
52
+ }
53
+ if (type === 'windows') {
54
+ spawnSync('taskkill', ['/F', '/IM', `${name}.exe`])
55
+ return true
56
+ }
57
+ return false
58
+ }
59
+
60
+ function getMacNetworkService() {
61
+ const services = getResolvedStaticConfig().os.macNetworkServices
62
+ const fallback = getResolvedStaticConfig().os.macNetworkDefault
63
+ const result = execFileSync('networksetup', ['-listnetworkserviceorder'], { encoding: 'utf-8' })
64
+ for (const line of result.split('\n')) {
65
+ if (services.some((s) => line.includes(s))) {
66
+ const parts = line.split(')')
67
+ if (parts.length >= 2) return parts[1].trim()
68
+ }
69
+ }
70
+ return fallback
71
+ }
72
+
73
+ export function setSocksProxy(host, port) {
74
+ const type = getOsType()
75
+ if (type === 'mac') {
76
+ const service = getMacNetworkService()
77
+ execFileSync('networksetup', ['-setsocksfirewallproxy', service, host, String(port)])
78
+ execFileSync('networksetup', ['-setsocksfirewallproxystate', service, 'on'])
79
+ return true
80
+ }
81
+ if (type === 'linux') {
82
+ process.env.ALL_PROXY = `socks5://${host}:${port}`
83
+ return true
84
+ }
85
+ return false
86
+ }
87
+
88
+ export function unsetSocksProxy() {
89
+ const type = getOsType()
90
+ if (type === 'mac') {
91
+ const service = getMacNetworkService()
92
+ execFileSync('networksetup', ['-setsocksfirewallproxystate', service, 'off'])
93
+ return true
94
+ }
95
+ if (type === 'linux') {
96
+ delete process.env.ALL_PROXY
97
+ return true
98
+ }
99
+ return false
100
+ }
101
+
102
+ export function getSocksProxyStatus() {
103
+ const type = getOsType()
104
+ if (type === 'mac') {
105
+ const service = getMacNetworkService()
106
+ try {
107
+ const out = execFileSync('networksetup', ['-getsocksfirewallproxy', service], { encoding: 'utf-8' })
108
+ const enabled = out.includes('Enabled: Yes')
109
+ return { enabled, detail: out.trim() }
110
+ } catch {
111
+ return { enabled: false, detail: '' }
112
+ }
113
+ }
114
+ if (type === 'linux') {
115
+ const proxy = process.env.ALL_PROXY ?? ''
116
+ return { enabled: Boolean(proxy), detail: proxy ? `ALL_PROXY=${proxy}` : '未设置' }
117
+ }
118
+ return { enabled: false, detail: '不支持当前系统' }
119
+ }
120
+
121
+ export function execCapture(cmd, args, { timeout = 15000 } = {}) {
122
+ return new Promise((resolve) => {
123
+ execFile(cmd, args, { timeout }, (err, stdout, stderr) => {
124
+ if (err) {
125
+ resolve({ code: err.code ?? 1, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })
126
+ } else {
127
+ resolve({ code: 0, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })
128
+ }
129
+ })
130
+ })
131
+ }