tunli 0.0.24 → 0.0.25

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/bin/tunli CHANGED
@@ -5,7 +5,7 @@ import {exit} from 'node:process'
5
5
  import {resolve} from "path";
6
6
  import {dirnameFromMeta} from "#src/core/FS/utils";
7
7
 
8
- setCursorVisibility(false)
8
+ if (process.env.TUNLI_DASHBOARD !== 'off') setCursorVisibility(false)
9
9
  const exitCode = await proxyChildProcess(resolve(dirnameFromMeta(import.meta), '../client.js'))
10
- setCursorVisibility(true)
10
+ if (process.env.TUNLI_DASHBOARD !== 'off') setCursorVisibility(true)
11
11
  exit(exitCode)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tunli",
3
3
  "description": "Node.js application for creating HTTP tunnels to make local software projects accessible over the internet.",
4
- "version": "0.0.24",
4
+ "version": "0.0.25",
5
5
  "main": "bin/tunli",
6
6
  "bin": {
7
7
  "tunli": "bin/tunli"
@@ -28,7 +28,6 @@
28
28
  "blessed": "^0.1.81",
29
29
  "chalk": "^5.3.0",
30
30
  "commander": "^12.1.0",
31
- "https-proxy-agent": "^7.0.4",
32
31
  "socket.io-client": "^4.7.5"
33
32
  },
34
33
  "devDependencies": {
@@ -7,140 +7,205 @@ import {getLatestVersion} from "#lib/Flow/getLatestVersion";
7
7
  import {exec} from 'child_process'
8
8
  import {checkGlobalInstallation, checkLocalInstallation} from "#src/utils/npmFunctions";
9
9
 
10
+ export class Dashboard {
11
+
12
+ /**
13
+ * @type {TunnelClient}
14
+ */
15
+ #client
16
+ /**
17
+ * @type {tunnelClientOptions}
18
+ */
19
+ #options
20
+ /**
21
+ * @type {AppConfig}
22
+ */
23
+ #config
24
+ /**
25
+ * @type {Screen}
26
+ */
27
+ #screen
28
+ /**
29
+ * @type {Ref}
30
+ */
31
+ #forwardingUrl
32
+
33
+ /**
34
+ * @param {TunnelClient} client
35
+ * @param {tunnelClientOptions} options
36
+ * @param {AppConfig} config
37
+ */
38
+ constructor(client, options, config) {
39
+ this.#client = client
40
+ this.#options = options
41
+ this.#config = config
42
+ }
43
+
44
+ /**
45
+ * @param {string} value
46
+ */
47
+ set forwardingUrl(value) {
48
+ this.#forwardingUrl.value = value
49
+ }
50
+
51
+ /**
52
+ * @return {Screen}
53
+ */
54
+ get screen() {
55
+ return this.#screen
56
+ }
57
+
58
+ destroy() {
59
+ this.#screen.destroy()
60
+ }
61
+
62
+ init() {
63
+
64
+ const options = this.#options
65
+ const config = this.#config
66
+ const client = this.#client
67
+
68
+ const screen = createScreen()
69
+ this.#screen = screen
70
+ const packageJson = readJsonFile(searchFileInDirectoryTree('package.json', dirnameFromMeta(import.meta)))
71
+ const connectionStatus = ref(chalk.yellow('offline'))
72
+ const requestCount = ref(0)
73
+ const connectionDetails = ref('')
74
+ const forwardingUrl = ref(trimEnd(options.server, '/'))
75
+
76
+ this.#forwardingUrl = forwardingUrl
77
+
78
+ const allowedCidr = options.allowCidr.join(', ')
79
+ const deniedCidr = options.denyCidr.join(', ')
80
+
81
+ const blockedCount = ref(0)
82
+ const lastBlockedIp = ref('')
83
+ const availableUpdate = ref('')
84
+
85
+ getLatestVersion().then((version) => {
86
+ if (version && version !== packageJson.version) {
87
+ availableUpdate.value = chalk.yellow(`update available (version ${version}, Ctrl-U to update)`)
88
+
89
+ screen.onceKey('C-u', async (char, details) => {
90
+ availableUpdate.value = chalk.yellow('Updating...')
91
+ let modifier
92
+ if (await checkGlobalInstallation(packageJson.name)) {
93
+ modifier = ' -g'
94
+ } else if (!await checkLocalInstallation(packageJson.name)) {
95
+ availableUpdate.value = chalk.red('Update failed.')
96
+ return
97
+ }
98
+
99
+ const npmUpdateCommand = `npm${modifier} update ${packageJson.name} --registry https://registry.npmjs.org`
100
+
101
+ exec(npmUpdateCommand, (error, stdout, stderr) => {
102
+ if (error || stderr) {
103
+ availableUpdate.value = chalk.red('Update failed. Reason 2.')
104
+ return;
105
+ }
106
+ availableUpdate.value = chalk.green('Update done. Please restart tunli. (Ctrl+R to restart)')
107
+ })
108
+ })
109
+ }
110
+ })
111
+
112
+ screen.row(packageJson.name)
113
+ screen.row('(Ctrl+C to quit)', {
114
+ top: -1, right: 0
115
+ })
116
+
117
+ screen.row("")
118
+
119
+ const infoList = screen.list({minWidth: 30})
120
+
121
+ screen.row('HTTP Requests')
122
+ screen.line()
123
+
124
+ const accessLog = screen.list({
125
+ length: 30, reverse: true, minWidth: [undefined, 30], maxWidth: [undefined, screen.width - 35]
126
+ })
127
+
128
+ client.on('tunnel-connection-established', () => {
129
+ connectionStatus.value = chalk.bold(chalk.green('online'))
130
+ connectionDetails.value = ''
131
+ screen.render()
132
+ })
133
+
134
+ client.on('blocked', ip => {
135
+ blockedCount.value++
136
+ lastBlockedIp.value = ` (${ip})`
137
+ })
138
+ client.on('tunnel-connection-closed', () => {
139
+ connectionStatus.value = chalk.bold(chalk.red('offline'))
140
+ screen.render()
141
+ })
142
+
143
+ client.on('tunnel-connection-error', (error) => {
144
+ if (error.stopPropagation) {
145
+ return
146
+ }
147
+ connectionDetails.value = chalk.bold(chalk.red(` - ${error.message}`))
148
+ screen.render()
149
+ })
150
+
151
+ client.on('response', (res, req) => {
152
+
153
+ const code = res.statusCode
154
+ let rspMsg = `${res.statusCode} ${res.statusMessage}`
155
+ if (code >= 500) {
156
+ rspMsg = chalk.red(rspMsg)
157
+ } else if (code >= 400) {
158
+ rspMsg = chalk.blueBright(rspMsg)
159
+ } else {
160
+ rspMsg = chalk.green(rspMsg)
161
+ }
162
+
163
+ rspMsg = chalk.bold(rspMsg)
164
+
165
+ requestCount.value++
166
+ accessLog.row(req.method, req.path, rspMsg)
167
+ screen.render()
168
+ })
169
+
170
+ screen.key('C-c', (char, details) => {
171
+ process.exit(0);
172
+ })
173
+
174
+ screen.key('C-r', (char, details) => {
175
+ process.send('restart')
176
+ })
177
+
178
+ const target = new URL(`${options.protocol}://${options.host}:${options.port}`)
179
+
180
+ infoList.row('Tunnel', concat(connectionStatus, connectionDetails))
181
+ infoList.row('Version', packageJson.version)
182
+ infoList.row(chalk.yellow('Update'), availableUpdate).if(() => availableUpdate)
183
+ infoList.row('Profile', config.profile)
184
+ infoList.row('Config', config.configPath)
185
+
186
+ if (allowedCidr || deniedCidr) infoList.row('')
187
+ if (allowedCidr) infoList.row('Allowed', allowedCidr)
188
+ if (deniedCidr) infoList.row('Denied', deniedCidr)
189
+ if (allowedCidr || deniedCidr) infoList.row('Blocked', concat(blockedCount, lastBlockedIp))
190
+
191
+ infoList.row('')
192
+ infoList.row('Latency', concat(client.latency, 'ms'))
193
+ infoList.row('Forwarding', concat(forwardingUrl, ` -> ${trimEnd(target.toString(), '/')}`))
194
+ infoList.row('Connections', requestCount)
195
+
196
+ screen.render();
197
+
198
+ return this
199
+ }
200
+ }
201
+
10
202
  /**
11
- *
12
203
  * @param {TunnelClient} client
13
204
  * @param {tunnelClientOptions} options
14
205
  * @param {AppConfig} config
15
206
  */
16
207
  export const initDashboard = (client, options, config) => {
17
-
18
- const screen = createScreen()
19
- const packageJson = readJsonFile(searchFileInDirectoryTree('package.json', dirnameFromMeta(import.meta)))
20
- const connectionStatus = ref(chalk.yellow('offline'))
21
- const requestCount = ref(0)
22
- const connectionDetails = ref('')
23
-
24
- const allowedCidr = options.allowCidr.join(', ')
25
- const deniedCidr = options.denyCidr.join(', ')
26
-
27
- const blockedCount = ref(0)
28
- const lastBlockedIp = ref('')
29
- const availableUpdate = ref('')
30
-
31
- getLatestVersion().then((version) => {
32
- if (version && version !== packageJson.version) {
33
- availableUpdate.value = chalk.yellow(`update available (version ${version}, Ctrl-U to update)`)
34
-
35
- screen.onceKey('C-u', async (char, details) => {
36
- availableUpdate.value = chalk.yellow('Updating...')
37
- let modifier
38
- if (await checkGlobalInstallation(packageJson.name)) {
39
- modifier = ' -g'
40
- } else if (!await checkLocalInstallation(packageJson.name)) {
41
- availableUpdate.value = chalk.red('Update failed.')
42
- return
43
- }
44
-
45
- const npmUpdateCommand = `npm${modifier} update ${packageJson.name} --registry https://registry.npmjs.org`
46
-
47
- exec(npmUpdateCommand, (error, stdout, stderr) => {
48
- if (error || stderr) {
49
- availableUpdate.value = chalk.red('Update failed. Reason 2.')
50
- return;
51
- }
52
- availableUpdate.value = chalk.green('Update done. Please restart tunli. (Ctrl+R to restart)')
53
- })
54
- })
55
- }
56
- })
57
-
58
- screen.row(packageJson.name)
59
- screen.row('(Ctrl+C to quit)', {
60
- top: -1,
61
- right: 0
62
- })
63
-
64
- screen.row("")
65
-
66
- const infoList = screen.list({minWidth: 30})
67
-
68
- screen.row('HTTP Requests')
69
- screen.line()
70
-
71
- const accessLog = screen.list({
72
- length: 30,
73
- reverse: true,
74
- minWidth: [undefined, 30],
75
- maxWidth: [undefined, screen.width - 35]
76
- })
77
-
78
- client.on('tunnel-connection-established', () => {
79
- connectionStatus.value = chalk.bold(chalk.green('online'))
80
- connectionDetails.value = ''
81
- screen.render()
82
- })
83
-
84
- client.on('blocked', ip => {
85
- blockedCount.value++
86
- lastBlockedIp.value = ` (${ip})`
87
- })
88
- client.on('tunnel-connection-closed', () => {
89
- connectionStatus.value = chalk.bold(chalk.red('offline'))
90
- screen.render()
91
- })
92
-
93
- client.on('tunnel-connection-error', (error) => {
94
- connectionDetails.value = chalk.bold(chalk.red(` - ${error.message}`))
95
- screen.render()
96
- })
97
-
98
- client.on('response', (res, req) => {
99
-
100
- const code = res.statusCode
101
- let rspMsg = `${res.statusCode} ${res.statusMessage}`
102
- if (code >= 500) {
103
- rspMsg = chalk.red(rspMsg)
104
- } else if (code >= 400) {
105
- rspMsg = chalk.blueBright(rspMsg)
106
- } else {
107
- rspMsg = chalk.green(rspMsg)
108
- }
109
-
110
- rspMsg = chalk.bold(rspMsg)
111
-
112
- requestCount.value++
113
- accessLog.row(req.method, req.path, rspMsg)
114
- screen.render()
115
- })
116
-
117
- screen.key('C-c', (char, details) => {
118
- process.exit(0);
119
- })
120
-
121
- screen.key('C-r', (char, details) => {
122
- process.send('restart')
123
- })
124
-
125
- const target = new URL(`${options.protocol}://${options.host}:${options.port}`)
126
-
127
- infoList.row('Tunnel', concat(connectionStatus, connectionDetails))
128
- infoList.row('Version', packageJson.version)
129
- infoList.row(chalk.yellow('Update'), availableUpdate).if(() => availableUpdate)
130
- infoList.row('Profile', config.profile)
131
- infoList.row('Config', config.configPath)
132
-
133
- if (allowedCidr || deniedCidr) infoList.row('')
134
- if (allowedCidr) infoList.row('Allowed', allowedCidr)
135
- if (deniedCidr) infoList.row('Denied', deniedCidr)
136
- if (allowedCidr || deniedCidr) infoList.row('Blocked', concat(blockedCount, lastBlockedIp))
137
-
138
- infoList.row('')
139
- infoList.row('Latency', concat(client.latency, 'ms'))
140
- infoList.row('Forwarding', `${trimEnd(options.server, '/')} -> ${trimEnd(target.toString(), '/')}`)
141
- infoList.row('Connections', requestCount)
142
-
143
- screen.render();
144
-
145
- return screen
208
+ const dashboard = new Dashboard(client, options, config)
209
+ dashboard.init()
210
+ return dashboard
146
211
  }
@@ -10,7 +10,48 @@ import {renewProxyUrlRegistration, requestNewProxyUrl} from "#lib/Flow/proxyUrl"
10
10
  import {getCurrentIp} from "#lib/Flow/getCurrentIp";
11
11
  import {arrayUnique} from "#src/utils/arrayFunctions";
12
12
  import {initDashboard} from "#src/cli-app/Dashboard";
13
- import {proxy} from "#lib/Proxy";
13
+ import {md5} from "#src/utils/hashFunctions";
14
+
15
+
16
+ /**
17
+ *
18
+ * @param {AppConfig} config
19
+ * @param {tunnelClientOptions} options
20
+ */
21
+ const computeProxyURL = async (config, options) => {
22
+
23
+ // console.log(new URL(`${options.protocol}://${options.host ?? config.host}:${options.port ?? config.port}`).toString())
24
+
25
+ const targetUrlHash = md5(new URL(`${options.protocol}://${options.host ?? config.host}:${options.port ?? config.port}`))
26
+
27
+ if (config.profile === 'default') {
28
+ let proxyUrl = config.proxyURLs[targetUrlHash]
29
+
30
+ if (proxyUrl) {
31
+ proxyUrl = await renewProxyUrlRegistration(proxyUrl, config.authToken)
32
+ }
33
+
34
+ if (!proxyUrl) {
35
+ proxyUrl = await requestNewProxyUrl(config.authToken)
36
+ config.proxyURLs[targetUrlHash] = proxyUrl
37
+ config.update({proxyURLs: config.proxyURLs})
38
+ }
39
+
40
+ config.proxyURL = proxyUrl
41
+
42
+ console.log(config.proxyURL)
43
+ return
44
+ }
45
+
46
+ if (config.proxyURL) {
47
+ config.proxyURL = await renewProxyUrlRegistration(config.proxyURL, config.authToken)
48
+ }
49
+
50
+ if (!config.proxyURL) {
51
+ config.proxyURL = await requestNewProxyUrl(config.authToken)
52
+ config.update({proxyURL: config.proxyURL})
53
+ }
54
+ }
14
55
 
15
56
  /**
16
57
  * @callback httpCommandExec
@@ -48,15 +89,6 @@ const exec = (configRef, cmd, program) => {
48
89
  process.exit()
49
90
  }
50
91
 
51
- if (config.proxyURL) {
52
- config.proxyURL = await renewProxyUrlRegistration(config.proxyURL, config.authToken)
53
- }
54
-
55
- if (!config.proxyURL) {
56
- config.proxyURL = await requestNewProxyUrl(config.authToken)
57
- options.save ??= true
58
- }
59
-
60
92
  if (isSharedArg(port)) {
61
93
  protocol ??= port.value.url?.protocol
62
94
  host ??= port.value.host ?? port.value.url?.host
@@ -69,6 +101,8 @@ const exec = (configRef, cmd, program) => {
69
101
  options.host ??= host
70
102
  options.protocol ??= protocol ?? 'http'
71
103
 
104
+ await computeProxyURL(config, options)
105
+
72
106
  const save = options.save
73
107
  delete options.save
74
108
 
@@ -100,9 +134,23 @@ const exec = (configRef, cmd, program) => {
100
134
  protocol: options.protocol
101
135
  }
102
136
 
103
- const useDashboard = process.env.DASHBOARD !== 'off'
137
+ const useDashboard = process.env.TUNLI_DASHBOARD !== 'off'
104
138
 
105
139
  const client = new TunnelClient(clientOptions)
140
+
141
+ client.once('tunnel-connection-error', async (error) => {
142
+ error.stopPropagation = true
143
+ if (error.data?.connection_exists) {
144
+ clientOptions.server = await requestNewProxyUrl(config.authToken)
145
+
146
+ if (useDashboard) {
147
+ dashboard.forwardingUrl = clientOptions.server
148
+ }
149
+
150
+ await client.init()
151
+ }
152
+ })
153
+
106
154
  const dashboard = useDashboard ? initDashboard(client, clientOptions, config) : null
107
155
  await client.init(dashboard)
108
156
 
@@ -1,4 +1,4 @@
1
- import {existsSync, writeFileSync} from "fs"
1
+ import {existsSync, readFileSync, writeFileSync} from "fs"
2
2
  import {dirname} from 'path'
3
3
  import {ensureDirectoryExists} from "#src/core/FS/utils";
4
4
  import {PropertyConfig} from "#src/config/PropertyConfig";
@@ -268,6 +268,30 @@ export class ConfigAbstract {
268
268
  return this
269
269
  }
270
270
 
271
+ /**
272
+ * @param {{[p:string]: any}} value
273
+ * @return {ConfigAbstract}
274
+ */
275
+ update(value) {
276
+ ensureDirectoryExists(dirname(this.#path))
277
+
278
+ const isSystem = this.#data === this.#profileData.system
279
+ const profileData = JSON.parse(readFileSync(this.#path, 'utf-8'))
280
+ profileData.system ??= {}
281
+ profileData.profile ??= {}
282
+ profileData.profile[this.profile] ??= {}
283
+
284
+ const updateData = isSystem ? profileData.system : profileData.profile[this.profile]
285
+
286
+ for (const [k, v] of Object.entries(value)) {
287
+ updateData[k] = v
288
+ this[k] = v
289
+ }
290
+
291
+ writeFileSync(this.#path, JSON.stringify(profileData, null, 2) + "\n")
292
+ return this
293
+ }
294
+
271
295
  /**
272
296
  * Use the system configuration.
273
297
  * @returns {ConfigAbstract} - The instance of the configuration.
@@ -1,4 +1,4 @@
1
- import {checkHost, checkInArray, checkIpV4Cidr, checkPort, checkUrl} from "#src/utils/checkFunctions";
1
+ import {checkHost, checkInArray, checkIpV4Cidr, checkMd5, checkPort, checkUrl} from "#src/utils/checkFunctions";
2
2
  import {ConfigAbstract, VISIBILITY_PUBLIC} from "#src/config/ConfigAbstract";
3
3
  import {property} from "#src/config/PropertyConfig";
4
4
  import {ConfigManager} from "#src/config/ConfigManager";
@@ -20,6 +20,19 @@ export class GlobalLocalShardConfigAbstract extends ConfigAbstract {
20
20
  return val.map(checkIpV4Cidr)
21
21
  }
22
22
  }),
23
+ proxyURLs: property({
24
+ defaultValue: {},
25
+ visibility: VISIBILITY_PUBLIC,
26
+ writeable: true,
27
+ type: Array,
28
+ validate(val) {
29
+ const final = {}
30
+ for (const [hash, proxyURL] of Object.entries(val)) {
31
+ final[checkMd5(hash)] = checkUrl(proxyURL)
32
+ }
33
+ return final
34
+ }
35
+ }),
23
36
  proxyURL: property({
24
37
  visibility: VISIBILITY_PUBLIC,
25
38
  writeable: true,
@@ -95,7 +95,7 @@ export class TunnelClient extends EventEmitter {
95
95
  }
96
96
 
97
97
  /**
98
- * @param [dashboard]
98
+ * @param {Dashboard} [dashboard]
99
99
  * @return {Promise<TunnelClient>}
100
100
  */
101
101
  async init(dashboard) {
@@ -107,6 +107,10 @@ export class TunnelClient extends EventEmitter {
107
107
  return this
108
108
  }
109
109
 
110
+ /**
111
+ * @param {Dashboard} dashboard
112
+ * @return {Promise<{path: any, transports: string[], auth: {token: string}, extraHeaders: {}}>}
113
+ */
110
114
  async #createParameters(dashboard) {
111
115
  const options = this.#options
112
116
  const webSocketCapturePath = await securedHttpClient(options.authToken).get('/capture_path')
@@ -170,4 +170,13 @@ export const checkInArray = (val, arr) => {
170
170
  }
171
171
 
172
172
  return val
173
+ }
174
+
175
+ export const checkMd5 = (hash) => {
176
+
177
+ if (!hash?.match(/^[0-9a-f]{32}$/i)) {
178
+ throw new InvalidArgumentError(`Value is not a valid md5 hash`)
179
+ }
180
+
181
+ return hash.toLowerCase()
173
182
  }
@@ -0,0 +1,9 @@
1
+ import {createHash} from 'crypto'
2
+
3
+ /**
4
+ * @param {string|Object<toString>} value
5
+ * @return {string}
6
+ */
7
+ export const md5 = (value) => {
8
+ return createHash('md5').update(value.toString()).digest('hex')
9
+ }
package/types/index.d.ts CHANGED
@@ -32,8 +32,8 @@ export type cliListOption = {
32
32
  minLength?: number
33
33
  length?: number
34
34
  reverse?: boolean
35
- minWidth?: number | Array<number|string|boolean>
36
- maxWidth?: number | Array<number|string|boolean>
35
+ minWidth?: number | Array<number | string | boolean>
36
+ maxWidth?: number | Array<number | string | boolean>
37
37
  }
38
38
 
39
39
  export type profileDump = {}
@@ -53,6 +53,7 @@ export interface AppConfig {
53
53
  host: string
54
54
  authToken: string
55
55
  proxyURL: proxyURL
56
+ proxyURLs: proxyURL[]
56
57
  path: undefined
57
58
  origin: string
58
59
  denyCidr?: ipCidr[]
@@ -70,6 +71,8 @@ export interface AppConfig {
70
71
 
71
72
  save(): this
72
73
 
74
+ update(value: { [p: string]: any }): this
75
+
73
76
  useSystem(): AppConfig
74
77
 
75
78
  use(profile: profileAlias): AppConfig
@@ -113,6 +116,4 @@ export interface keypressEventListener {
113
116
  }
114
117
 
115
118
 
116
- export type tunliProxyOptions = {
117
-
118
- }
119
+ export type tunliProxyOptions = {}