@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.
@@ -0,0 +1,435 @@
1
+ import fs from 'node:fs'
2
+ import net from 'node:net'
3
+ import path from 'node:path'
4
+ import { spawn, spawnSync } from 'node:child_process'
5
+
6
+ import { execCapture, getChromePath, getSocksProxyStatus, killProcess, setSocksProxy, unsetSocksProxy, which } from './OSystem.js'
7
+ import { clientAddr, getProxyConfig, getResolvedStaticConfig, sshAlias } from './config.js'
8
+ import { initClient } from './init.js'
9
+
10
+ const _chromePidFile = () => getResolvedStaticConfig().paths.chromePidFile
11
+ const _webPidFile = () => getResolvedStaticConfig().paths.webPidFile
12
+ const _pidFile = () => getResolvedStaticConfig().paths.pidFile
13
+ const _sshDir = () => getResolvedStaticConfig().paths.sshDir
14
+ const _sshConfigFile = () => getResolvedStaticConfig().paths.sshConfigFile
15
+ const _keyCandidates = () => getResolvedStaticConfig().ssh.keyCandidates
16
+ const _autosshArgs = () => getResolvedStaticConfig().tunnel.autosshArgs
17
+ const _curlUrl = () => getResolvedStaticConfig().debug.curlUrl
18
+ const _curlTimeoutMs = () => getResolvedStaticConfig().debug.curlTimeoutMs
19
+
20
+ const _lock = (() => {
21
+ let chain = Promise.resolve()
22
+ return {
23
+ async run(fn) {
24
+ let release
25
+ const gate = new Promise((resolve) => { release = resolve })
26
+ const prev = chain
27
+ chain = chain.then(() => gate)
28
+ await prev
29
+ try {
30
+ return await fn()
31
+ } finally {
32
+ release()
33
+ }
34
+ },
35
+ }
36
+ })()
37
+
38
+ export class ProxyManager {
39
+ config() {
40
+ const cfg = getProxyConfig()
41
+ const [host, port] = clientAddr(cfg)
42
+ return { host, port }
43
+ }
44
+
45
+ getStatus() {
46
+ const cfg = this.config()
47
+ const pid = this._readPid()
48
+ const tunnel = { running: pid != null && this._isAlive(pid), pid }
49
+ const sys = getSocksProxyStatus()
50
+ return {
51
+ host: cfg.host,
52
+ port: cfg.port,
53
+ tunnel,
54
+ system_proxy: { enabled: sys.enabled, detail: sys.detail },
55
+ chrome: this.chromeStatus(),
56
+ web: this.webStatus(),
57
+ }
58
+ }
59
+
60
+ async tunnelStart() {
61
+ return _lock.run(() => this._doTunnelStart())
62
+ }
63
+
64
+ async tunnelStop() {
65
+ return _lock.run(() => this._doTunnelStop())
66
+ }
67
+
68
+ async proxyOn() {
69
+ return _lock.run(() => this._doProxyOn())
70
+ }
71
+
72
+ async proxyOff() {
73
+ return _lock.run(() => this._doProxyOff())
74
+ }
75
+
76
+ async start() {
77
+ return _lock.run(async () => {
78
+ const tunnel = await this._doTunnelStart()
79
+ if (!tunnel.ok) return tunnel
80
+ const proxy = await this._doProxyOn()
81
+ if (!proxy.ok) return proxy
82
+ return { ok: true, message: '代理已全部开启(隧道 + 系统代理)' }
83
+ })
84
+ }
85
+
86
+ async stop() {
87
+ return _lock.run(() => {
88
+ const result = { ok: true, message: '' }
89
+ const tunnel = this._doTunnelStop()
90
+ if (tunnel.message) result.message += tunnel.message + ' '
91
+ const sys = getSocksProxyStatus()
92
+ if (sys.enabled) {
93
+ unsetSocksProxy()
94
+ result.message += '系统代理已关闭 '
95
+ }
96
+ const chromeMsg = this._stopChrome().message
97
+ if (chromeMsg) result.message += chromeMsg
98
+ if (!result.message.trim()) result.message = '代理未在运行'
99
+ return result
100
+ })
101
+ }
102
+
103
+ // ---------- 原子内部实现(不含锁,供组合命令复用) ----------
104
+
105
+ async _doTunnelStart() {
106
+ const cfg = this.config()
107
+ const ok = await this._startTunnel(cfg.host, cfg.port)
108
+ if (!ok) return { ok: false, message: '隧道启动失败' }
109
+ return { ok: true, message: `隧道已启动 (SOCKS5 ${cfg.host}:${cfg.port})` }
110
+ }
111
+
112
+ _doTunnelStop() {
113
+ const result = { ok: true, message: '' }
114
+ const pid = this._readPid()
115
+ if (pid && this._isAlive(pid)) {
116
+ try {
117
+ process.kill(pid, 'SIGTERM')
118
+ result.message = `隧道已停止 (PID=${pid})`
119
+ } catch {
120
+ result.message = '隧道停止失败'
121
+ }
122
+ }
123
+ this._cleanupPid()
124
+ if (!result.message) result.message = '隧道未在运行'
125
+ return result
126
+ }
127
+
128
+ async _doProxyOn() {
129
+ const cfg = this.config()
130
+ if (!this._isProxyRunning()) {
131
+ const ok = await this._startTunnel(cfg.host, cfg.port)
132
+ if (!ok) return { ok: false, message: '隧道未运行,代理未开启' }
133
+ }
134
+ setSocksProxy(cfg.host, cfg.port)
135
+ return { ok: true, message: `系统代理已开启 (${cfg.host}:${cfg.port})` }
136
+ }
137
+
138
+ _doProxyOff() {
139
+ const sys = getSocksProxyStatus()
140
+ if (!sys.enabled) return { ok: false, message: '系统代理未开启' }
141
+ unsetSocksProxy()
142
+ return { ok: true, message: '系统代理已关闭' }
143
+ }
144
+
145
+ async debug() {
146
+ const cfg = this.config()
147
+ const host = cfg.host
148
+ const port = cfg.port
149
+ const alias = sshAlias(port)
150
+ const checks = [
151
+ { name: 'autossh 已安装', fn: () => this._checkAutossh() },
152
+ { name: `SSH config 有 ${alias}`, fn: () => this._checkSshConfig(alias) },
153
+ { name: 'SSH 密钥存在', fn: () => this._checkSshKey() },
154
+ { name: '代理进程在运行', fn: () => this._checkProxyRunning() },
155
+ { name: `${port} 端口在监听`, fn: () => this._checkPort(port) },
156
+ { name: '网络连通性测试', fn: () => this._checkConnectivity(host, port) },
157
+ ]
158
+ const results = []
159
+ let criticalOk = true
160
+ for (const { name, fn } of checks) {
161
+ if (!criticalOk && name === '网络连通性测试') {
162
+ results.push({ name, ok: null, detail: '前置检查未通过,跳过连通性测试' })
163
+ continue
164
+ }
165
+ const [ok, detail] = await fn()
166
+ results.push({ name, ok: Boolean(ok), detail })
167
+ if (!ok) criticalOk = false
168
+ }
169
+ return results
170
+ }
171
+
172
+ chromeStatus() {
173
+ const pid = this._readPid(_chromePidFile())
174
+ if (pid && this._isAlive(pid)) {
175
+ return { running: true, pid }
176
+ }
177
+ const { host, port } = this.config()
178
+ const pids = this._findPgrepPids(`chrome.*--proxy-server=socks5://${host}:${port}`)
179
+ if (pids.length > 0) {
180
+ return { running: true, pid: pids[0] }
181
+ }
182
+ this._cleanupPid(_chromePidFile())
183
+ return { running: false, pid: null }
184
+ }
185
+
186
+ async chromeStart() {
187
+ return _lock.run(async () => {
188
+ const cfg = this.config()
189
+ if (!this._isProxyRunning()) {
190
+ const ok = await this._startTunnel(cfg.host, cfg.port)
191
+ if (!ok) {
192
+ return { ok: false, message: '代理隧道启动失败' }
193
+ }
194
+ }
195
+ const chromePath = getChromePath()
196
+ if (!chromePath) {
197
+ return { ok: false, message: '未找到 Chrome,请确认安装路径' }
198
+ }
199
+ killProcess('Google Chrome')
200
+ const cmd = [
201
+ chromePath,
202
+ `--proxy-server=socks5://${cfg.host}:${cfg.port}`,
203
+ '--disable-features=SecureDns',
204
+ ]
205
+ const proc = this._spawnDetached(cmd[0], cmd.slice(1))
206
+ this._writePid(proc.pid ?? 0, _chromePidFile())
207
+ return { ok: true, message: 'Chrome 已以代理模式启动', pid: proc.pid }
208
+ })
209
+ }
210
+
211
+ _spawnDetached(command, args) {
212
+ const proc = spawn(command, args, {
213
+ stdio: 'ignore',
214
+ detached: true,
215
+ })
216
+ proc.unref()
217
+ return proc
218
+ }
219
+
220
+ async chromeStop() {
221
+ return _lock.run(() => this._stopChrome())
222
+ }
223
+
224
+ _stopChrome() {
225
+ const result = { ok: true, message: '' }
226
+ const pid = this._readPid(_chromePidFile())
227
+ let killed = false
228
+ if (pid && this._isAlive(pid)) {
229
+ try {
230
+ process.kill(pid, 'SIGTERM')
231
+ killed = true
232
+ } catch {
233
+ /* ignore */
234
+ }
235
+ }
236
+ const { host, port } = this.config()
237
+ for (const p of this._findPgrepPids(`chrome.*--proxy-server=socks5://${host}:${port}`)) {
238
+ if (p === pid) continue
239
+ try {
240
+ process.kill(p, 'SIGTERM')
241
+ killed = true
242
+ } catch {
243
+ /* ignore */
244
+ }
245
+ }
246
+ this._cleanupPid(_chromePidFile())
247
+ result.message = killed ? '代理 Chrome 已关闭' : '代理 Chrome 未在运行'
248
+ return result
249
+ }
250
+
251
+ // ---------- Web 管理页 ----------
252
+
253
+ _webServerPath() {
254
+ return path.join(path.dirname(new URL(import.meta.url).pathname), 'web', 'server.js')
255
+ }
256
+
257
+ async webStart() {
258
+ return _lock.run(() => {
259
+ const pid = this._readPid(_webPidFile())
260
+ if (pid && this._isAlive(pid)) {
261
+ return { ok: false, message: 'Web 管理页已在运行' }
262
+ }
263
+ const proc = spawn(process.execPath, [this._webServerPath()], {
264
+ stdio: 'ignore',
265
+ detached: true,
266
+ env: { ...process.env },
267
+ })
268
+ proc.unref()
269
+ this._writePid(proc.pid ?? 0, _webPidFile())
270
+ return { ok: true, message: 'Web 管理页已启动(后台)', pid: proc.pid }
271
+ })
272
+ }
273
+
274
+ async webStop() {
275
+ return _lock.run(() => {
276
+ const result = { ok: true, message: '' }
277
+ const pid = this._readPid(_webPidFile())
278
+ if (pid && this._isAlive(pid)) {
279
+ try {
280
+ process.kill(pid, 'SIGTERM')
281
+ result.message = 'Web 管理页已关闭'
282
+ } catch {
283
+ result.message = 'Web 管理页关闭失败'
284
+ }
285
+ }
286
+ this._cleanupPid(_webPidFile())
287
+ if (!result.message) result.message = 'Web 管理页未在运行'
288
+ return result
289
+ })
290
+ }
291
+
292
+ webStatus() {
293
+ const pid = this._readPid(_webPidFile())
294
+ if (pid && this._isAlive(pid)) {
295
+ return { running: true, pid }
296
+ }
297
+ this._cleanupPid(_webPidFile())
298
+ return { running: false, pid: null }
299
+ }
300
+
301
+ // ---------- 隧道 ----------
302
+
303
+ async _startTunnel(host, port) {
304
+ const pid = this._readPid()
305
+ if (pid && this._isAlive(pid)) return true
306
+ if (await this._isPortBusy(port)) return false
307
+ const initLines = await initClient()
308
+ if (initLines.some((l) => l.includes('✗'))) return false
309
+ const alias = sshAlias(port)
310
+ const proc = spawn('autossh', [..._autosshArgs(), alias], {
311
+ stdio: ['ignore', 'ignore', 'pipe'],
312
+ })
313
+ const stderr = []
314
+ proc.stderr.on('data', (chunk) => stderr.push(String(chunk)))
315
+ const code = await new Promise((resolve) => {
316
+ proc.on('error', () => resolve(1))
317
+ proc.on('exit', (c) => resolve(c ?? 1))
318
+ })
319
+ if (code !== 0) return false
320
+ const proxyPid = this._findProxyPid(port)
321
+ if (!proxyPid) return false
322
+ this._writePid(proxyPid)
323
+ return true
324
+ }
325
+
326
+ _isProxyRunning() {
327
+ const pid = this._readPid()
328
+ return pid != null && this._isAlive(pid)
329
+ }
330
+
331
+ // ---------- 检查原语 ----------
332
+
333
+ _checkAutossh() {
334
+ const autossh = which('autossh')
335
+ if (autossh) {
336
+ return [true, autossh]
337
+ }
338
+ return [false, '未安装,请执行: brew install autossh']
339
+ }
340
+
341
+ _checkSshConfig(alias) {
342
+ const configPath = _sshConfigFile()
343
+ if (!fs.existsSync(configPath)) return [false, `${configPath} 不存在`]
344
+ const content = fs.readFileSync(configPath, 'utf-8')
345
+ if (!content.includes(`Host ${alias}`)) {
346
+ return [false, `缺少 ${alias} 配置,请执行 sshproxy tunnel on`]
347
+ }
348
+ return [true, '']
349
+ }
350
+
351
+ _checkSshKey() {
352
+ const sshDir = _sshDir()
353
+ for (const name of _keyCandidates()) {
354
+ if (fs.existsSync(path.join(sshDir, name))) return [true, '']
355
+ }
356
+ return [false, `${sshDir}/ 下无密钥文件,请执行: ssh-keygen -t ed25519`]
357
+ }
358
+
359
+ _checkProxyRunning() {
360
+ const pid = this._readPid()
361
+ if (pid && this._isAlive(pid)) return [true, `PID=${pid}`]
362
+ return [false, '代理未运行,请执行: sshproxy tunnel on']
363
+ }
364
+
365
+ async _checkPort(port) {
366
+ if (await this._isPortBusy(port)) return [true, `127.0.0.1:${port}`]
367
+ return [false, `${port} 端口未监听,代理可能未正确启动`]
368
+ }
369
+
370
+ async _checkConnectivity(host, port) {
371
+ const result = await execCapture('curl', ['--socks5-hostname', `${host}:${port}`, _curlUrl(), '-I'], { timeout: _curlTimeoutMs() })
372
+ if (result.code === 0) return [true, '']
373
+ return [false, `请求失败 (exit=${result.code})`]
374
+ }
375
+
376
+ // ---------- PID 文件 ----------
377
+
378
+ _readPid(filePath = _pidFile()) {
379
+ try {
380
+ return parseInt(fs.readFileSync(filePath, 'utf-8').trim(), 10)
381
+ } catch {
382
+ return null
383
+ }
384
+ }
385
+
386
+ _writePid(pid, filePath = _pidFile()) {
387
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
388
+ fs.writeFileSync(filePath, String(pid))
389
+ }
390
+
391
+ _cleanupPid(filePath = _pidFile()) {
392
+ try {
393
+ fs.unlinkSync(filePath)
394
+ } catch {
395
+ /* ignore */
396
+ }
397
+ }
398
+
399
+ _isAlive(pid) {
400
+ try {
401
+ process.kill(pid, 0)
402
+ return true
403
+ } catch {
404
+ return false
405
+ }
406
+ }
407
+
408
+ _isPortBusy(port) {
409
+ return new Promise((resolve) => {
410
+ const s = net.createServer()
411
+ s.once('error', () => resolve(true))
412
+ s.once('listening', () => s.close(() => resolve(false)))
413
+ s.listen(port, '127.0.0.1')
414
+ })
415
+ }
416
+
417
+ _findProxyPid(port = null) {
418
+ const alias = port ? sshAlias(port) : 'client-proxy'
419
+ let pids = this._findPgrepPids(`autossh.*-N.*-f.*${alias}`)
420
+ if (pids.length === 0) pids = this._findPgrepPids(`ssh.*-N.*${alias}`)
421
+ return pids.length > 0 ? pids[0] : null
422
+ }
423
+
424
+ _findPgrepPids(pattern) {
425
+ const result = spawnSync('pgrep', ['-f', pattern])
426
+ if (result.status !== 0) return []
427
+ const pids = []
428
+ for (const line of String(result.stdout).trim().split('\n')) {
429
+ const pid = parseInt(line.trim(), 10)
430
+ if (Number.isNaN(pid)) continue
431
+ if (pid !== process.pid) pids.push(pid)
432
+ }
433
+ return pids
434
+ }
435
+ }
@@ -0,0 +1,132 @@
1
+ import { Client } from 'ssh2'
2
+ import path from 'node:path'
3
+
4
+ import { loadConfig } from './config.js'
5
+
6
+ export class SshClient {
7
+ constructor() {
8
+ const cfg = loadConfig()
9
+ this.host = cfg.host ?? ''
10
+ this.port = Number(cfg.port ?? 22)
11
+ this.user = cfg.user ?? ''
12
+ this.password = cfg.password ?? ''
13
+ this.timeout = Number(cfg.timeout ?? 30)
14
+ }
15
+
16
+ connect() {
17
+ return new Promise((resolve, reject) => {
18
+ const client = new Client()
19
+ const conn = {
20
+ host: this.host,
21
+ port: this.port,
22
+ username: this.user,
23
+ timeout: this.timeout * 1000,
24
+ readyTimeout: this.timeout * 1000,
25
+ }
26
+ if (this.password) conn.password = this.password
27
+ client.on('ready', () => resolve(client))
28
+ client.on('error', (err) => reject(err))
29
+ client.connect(conn)
30
+ })
31
+ }
32
+
33
+ async checkConnection() {
34
+ try {
35
+ const client = await this.connect()
36
+ try {
37
+ await this.execOnClient(client, 'echo ok')
38
+ return true
39
+ } finally {
40
+ client.end()
41
+ }
42
+ } catch {
43
+ return false
44
+ }
45
+ }
46
+
47
+ async execute(command) {
48
+ let client
49
+ try {
50
+ client = await this.connect()
51
+ const result = await this.execOnClient(client, command)
52
+ return [result.code, result.stdout, result.stderr]
53
+ } catch (err) {
54
+ return [1, '', String(err)]
55
+ } finally {
56
+ if (client) client.end()
57
+ }
58
+ }
59
+
60
+ execOnClient(client, command) {
61
+ return new Promise((resolve, reject) => {
62
+ client.exec(command, (err, stream) => {
63
+ if (err) return reject(err)
64
+ let stdout = ''
65
+ let stderr = ''
66
+ stream.on('data', (chunk) => { stdout += String(chunk) })
67
+ stream.stderr.on('data', (chunk) => { stderr += String(chunk) })
68
+ stream.on('close', (code) => {
69
+ resolve({ code: code ?? 0, stdout, stderr })
70
+ })
71
+ })
72
+ })
73
+ }
74
+
75
+ async upload(localPath, remotePath) {
76
+ let client
77
+ try {
78
+ client = await this.connect()
79
+ const sftp = await new Promise((resolve, reject) => {
80
+ client.sftp((err, sftp) => (err ? reject(err) : resolve(sftp)))
81
+ })
82
+ await this.mkdirs(sftp, path.posix.dirname(remotePath))
83
+ await new Promise((resolve, reject) => {
84
+ sftp.fastPut(localPath, remotePath, (err) => (err ? reject(err) : resolve()))
85
+ })
86
+ return true
87
+ } catch {
88
+ return false
89
+ } finally {
90
+ if (client) client.end()
91
+ }
92
+ }
93
+
94
+ async download(remotePath, localPath) {
95
+ let client
96
+ try {
97
+ client = await this.connect()
98
+ const sftp = await new Promise((resolve, reject) => {
99
+ client.sftp((err, sftp) => (err ? reject(err) : resolve(sftp)))
100
+ })
101
+ await new Promise((resolve, reject) => {
102
+ sftp.fastGet(remotePath, localPath, (err) => (err ? reject(err) : resolve()))
103
+ })
104
+ return true
105
+ } catch {
106
+ return false
107
+ } finally {
108
+ if (client) client.end()
109
+ }
110
+ }
111
+
112
+ mkdirs(sftp, remoteDir) {
113
+ const dirs = remoteDir.split('/').filter(Boolean)
114
+ let p = ''
115
+ return dirs.reduce((chain, d) => {
116
+ p += `/${d}`
117
+ return chain.then(() => this.statOrMkdir(sftp, p))
118
+ }, Promise.resolve())
119
+ }
120
+
121
+ statOrMkdir(sftp, dirPath) {
122
+ return new Promise((resolve) => {
123
+ sftp.stat(dirPath, (err) => {
124
+ if (err) {
125
+ sftp.mkdir(dirPath, (mkdirErr) => resolve(mkdirErr ? Promise.reject(mkdirErr) : resolve()))
126
+ } else {
127
+ resolve()
128
+ }
129
+ })
130
+ })
131
+ }
132
+ }
package/src/config.js ADDED
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import JSON5 from 'json5'
6
+ import dotenv from 'dotenv'
7
+ import { createConfigLoader, getXdgConfigDir } from '@wwkit/shared'
8
+
9
+ // ============================================================
10
+ // 业务配置(用户可调整;默认值存于 src/config.json5)
11
+ // ============================================================
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
+ dotenv.config({ path: path.join(__dirname, '..', '.env'), quiet: true })
15
+ const DEFAULT_CONFIG_PATH = path.join(__dirname, 'config.json5')
16
+
17
+ export function defaultConfig() {
18
+ const raw = fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf-8')
19
+ return JSON5.parse(raw)
20
+ }
21
+
22
+ export function getConfigDir() {
23
+ const envDir = process.env.SSHPROXY_CONFIG_DIR
24
+ if (envDir) return envDir
25
+ return getXdgConfigDir('sshproxy')
26
+ }
27
+
28
+ export function getConfigPath() {
29
+ return path.join(getConfigDir(), 'config.json5')
30
+ }
31
+
32
+ const _loader = createConfigLoader({
33
+ builtinConfigPath: DEFAULT_CONFIG_PATH,
34
+ userConfigDir: getConfigDir(),
35
+ envContentVar: 'SSHPROXY_CONFIG_CONTENT',
36
+ envDirVar: 'SSHPROXY_CONFIG_DIR',
37
+ })
38
+
39
+ export function loadConfig() {
40
+ return _loader.getConfig()
41
+ }
42
+
43
+ // ============================================================
44
+ // 静态配置(系统内置常量,存于 src/contains.json5,用户不可改)
45
+ // ============================================================
46
+
47
+ const DEFAULT_STATIC_PATH = path.join(__dirname, 'contains.json5')
48
+
49
+ export function defaultStaticConfig() {
50
+ const raw = fs.readFileSync(DEFAULT_STATIC_PATH, 'utf-8')
51
+ return JSON5.parse(raw)
52
+ }
53
+
54
+ export function getStaticConfig() {
55
+ return defaultStaticConfig()
56
+ }
57
+
58
+ // ============================================================
59
+ // 通用(由 @wwkit/shared 提供)
60
+ // ============================================================
61
+
62
+ export { deepMerge, resolveEnvVars } from '@wwkit/shared'
63
+
64
+ export function getProxyConfig() {
65
+ const cfg = loadConfig()
66
+ return cfg.proxy ?? cfg
67
+ }
68
+
69
+ export function clientAddr(cfg = getProxyConfig()) {
70
+ const c = cfg.socks5 ?? {}
71
+ return [c.host ?? '127.0.0.1', c.port ?? 1080]
72
+ }
73
+
74
+ export function sshAlias(port) {
75
+ const s = getStaticConfig()
76
+ return `${s.ssh.aliasPrefix}-${port}`
77
+ }
78
+
79
+ // ============================================================
80
+ // 静态配置派生(封装默认值与路径展开)
81
+ // ============================================================
82
+
83
+ function expandHome(p) {
84
+ return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p
85
+ }
86
+
87
+ export function getResolvedStaticConfig() {
88
+ const s = getStaticConfig()
89
+ return {
90
+ paths: {
91
+ stateDir: expandHome(s.paths.stateDir),
92
+ pidFile: path.join(expandHome(s.paths.stateDir), s.paths.pidFile),
93
+ chromePidFile: path.join(expandHome(s.paths.stateDir), s.paths.chromePidFile),
94
+ webPidFile: path.join(expandHome(s.paths.stateDir), s.paths.webPidFile),
95
+ sshDir: expandHome(s.paths.sshDir),
96
+ sshConfigFile: path.join(expandHome(s.paths.sshDir), s.paths.sshConfigFile),
97
+ },
98
+ ssh: { ...s.ssh },
99
+ tunnel: { ...s.tunnel },
100
+ os: { ...s.os },
101
+ debug: { ...s.debug },
102
+ web: { ...s.web },
103
+ }
104
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ socks5: {
3
+ host: '127.0.0.1',
4
+ port: 1080,
5
+ },
6
+ host: '{env:SSH_HOST}',
7
+ port: 22,
8
+ user: '{env:SSH_USER}',
9
+ password: '{env:SSH_PASSWORD}',
10
+ timeout: 30,
11
+ web: {
12
+ host: '127.0.0.1',
13
+ port: 1081,
14
+ },
15
+ }