tunli 0.0.19

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.
Files changed (63) hide show
  1. package/LICENSE.md +595 -0
  2. package/README.md +135 -0
  3. package/bin/tunli +11 -0
  4. package/client.js +31 -0
  5. package/package.json +51 -0
  6. package/src/cli-app/Dashboard.js +146 -0
  7. package/src/cli-app/Screen.js +135 -0
  8. package/src/cli-app/elements/ElementNode.js +97 -0
  9. package/src/cli-app/elements/Line.js +21 -0
  10. package/src/cli-app/elements/List/List.js +227 -0
  11. package/src/cli-app/elements/List/ListCell.js +83 -0
  12. package/src/cli-app/elements/List/ListColumn.js +52 -0
  13. package/src/cli-app/elements/List/ListRow.js +118 -0
  14. package/src/cli-app/elements/Row.js +38 -0
  15. package/src/cli-app/helper/utils.js +42 -0
  16. package/src/commands/Action/addDelValuesAction.js +56 -0
  17. package/src/commands/CommandAuth.js +32 -0
  18. package/src/commands/CommandClearAll.js +27 -0
  19. package/src/commands/CommandConfig.js +57 -0
  20. package/src/commands/CommandHTTP.js +131 -0
  21. package/src/commands/CommandInvite.js +38 -0
  22. package/src/commands/CommandRefresh.js +35 -0
  23. package/src/commands/CommandRegister.js +48 -0
  24. package/src/commands/Option/DeleteOption.js +6 -0
  25. package/src/commands/Option/SelectConfigOption.js +52 -0
  26. package/src/commands/SubCommand/AllowDenyCidrCommand.js +28 -0
  27. package/src/commands/SubCommand/HostCommand.js +22 -0
  28. package/src/commands/SubCommand/PortCommand.js +20 -0
  29. package/src/commands/helper/AliasResolver.js +13 -0
  30. package/src/commands/helper/BindArgs.js +53 -0
  31. package/src/commands/helper/SharedArg.js +32 -0
  32. package/src/commands/utils.js +96 -0
  33. package/src/config/ConfigAbstract.js +318 -0
  34. package/src/config/ConfigManager.js +70 -0
  35. package/src/config/GlobalConfig.js +14 -0
  36. package/src/config/GlobalLocalShardConfigAbstract.js +76 -0
  37. package/src/config/LocalConfig.js +7 -0
  38. package/src/config/PropertyConfig.js +122 -0
  39. package/src/config/SystemConfig.js +31 -0
  40. package/src/core/FS/utils.js +60 -0
  41. package/src/core/Ref.js +70 -0
  42. package/src/lib/Flow/getCurrentIp.js +18 -0
  43. package/src/lib/Flow/getLatestVersion.js +13 -0
  44. package/src/lib/Flow/proxyUrl.js +32 -0
  45. package/src/lib/Flow/validateAuthToken.js +19 -0
  46. package/src/lib/HttpClient.js +61 -0
  47. package/src/lib/defs.js +10 -0
  48. package/src/net/IPV4.js +139 -0
  49. package/src/net/http/IncomingMessage.js +92 -0
  50. package/src/net/http/ServerResponse.js +126 -0
  51. package/src/net/http/TunliRequest.js +1 -0
  52. package/src/net/http/TunliResponse.js +1 -0
  53. package/src/net/http/TunnelRequest.js +177 -0
  54. package/src/net/http/TunnelResponse.js +119 -0
  55. package/src/tunnel-client/TunnelClient.js +136 -0
  56. package/src/utils/arrayFunctions.js +45 -0
  57. package/src/utils/checkFunctions.js +161 -0
  58. package/src/utils/cliFunctions.js +62 -0
  59. package/src/utils/createRequest.js +12 -0
  60. package/src/utils/httpFunction.js +23 -0
  61. package/src/utils/npmFunctions.js +27 -0
  62. package/src/utils/stringFunctions.js +34 -0
  63. package/types/index.d.ts +112 -0
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # Tunli
2
+
3
+ Tunli is a Node.js application that creates HTTP tunnels to make local software projects accessible over the internet.
4
+ This is particularly useful for developing and testing applications that need to be reachable from anywhere.
5
+
6
+ ## Installation
7
+
8
+ To install Tunli, run the following command:
9
+
10
+ ```bash
11
+ npm install -g tunli
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Tunli can be used from the command line with various commands and options. Here are some of the key commands:
17
+
18
+ ### Display Help
19
+
20
+ ```bash
21
+ tunli --help
22
+ ```
23
+
24
+ ### Configure and View Settings
25
+
26
+ ```bash
27
+ tunli config
28
+ tunli config host localhost
29
+ tunli config port 80
30
+ ```
31
+
32
+ ### Create HTTP Tunnel
33
+
34
+ ```bash
35
+ tunli http [PORT] [HOST]
36
+ ```
37
+
38
+ Example:
39
+
40
+ ```bash
41
+ tunli http localhost:80
42
+ ```
43
+
44
+ ### Registration and Authentication
45
+
46
+ To use Tunli, you need to register and authenticate.
47
+
48
+ #### Register
49
+
50
+ ```bash
51
+ tunli register
52
+ ```
53
+
54
+ #### Authenticate
55
+
56
+ ```bash
57
+ tunli auth <TOKEN>
58
+ ```
59
+
60
+ ### Create an Invitation
61
+
62
+ Generate a shareable registration token:
63
+
64
+ ```bash
65
+ tunli invite
66
+ ```
67
+
68
+ ## Examples
69
+
70
+ Here are some examples of how to use Tunli:
71
+
72
+ - Set the host for the local configuration:
73
+
74
+ ```bash
75
+ tunli config host localhost
76
+ ```
77
+
78
+ - Set the port for the local configuration:
79
+
80
+ ```bash
81
+ tunli config port 80
82
+ ```
83
+
84
+ - Show the local configuration:
85
+
86
+ ```bash
87
+ tunli config
88
+ ```
89
+
90
+ - Show the global configuration:
91
+
92
+ ```bash
93
+ tunli config --global
94
+ ```
95
+
96
+ - Forward HTTP requests to `localhost:80`:
97
+
98
+ ```bash
99
+ tunli http localhost:80
100
+ ```
101
+
102
+ - Create a shareable registration token:
103
+
104
+ ```bash
105
+ tunli invite
106
+ ```
107
+
108
+ - Register this client with a given token:
109
+
110
+ ```bash
111
+ tunli auth <TOKEN>
112
+ ```
113
+
114
+ ## Dependencies
115
+
116
+ Tunli relies on the following packages:
117
+
118
+ - `axios`: ^1.7.2
119
+ - `blessed`: ^0.1.81
120
+ - `chalk`: ^5.3.0
121
+ - `commander`: ^12.1.0
122
+ - `https-proxy-agent`: ^7.0.4
123
+ - `socket.io-client`: ^4.7.5
124
+
125
+ ## Development
126
+
127
+ For development purposes, you can start the application using nodemon to automatically restart it on file changes:
128
+
129
+ ```bash
130
+ npm run dev
131
+ ```
132
+
133
+ ## License
134
+
135
+ Tunli is licensed under the GPL-3.0 License.
package/bin/tunli ADDED
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ ":" //# comment; exec /usr/bin/env node --harmony "$0" "$@"
3
+ import {proxyChildProcess, setCursorVisibility} from '#src/utils/cliFunctions'
4
+ import {exit} from 'node:process'
5
+ import {resolve} from "path";
6
+ import {dirnameFromMeta} from "#src/core/FS/utils";
7
+
8
+ setCursorVisibility(false)
9
+ const exitCode = await proxyChildProcess(resolve(dirnameFromMeta(import.meta), '../client.js'))
10
+ setCursorVisibility(true)
11
+ exit(exitCode)
package/client.js ADDED
@@ -0,0 +1,31 @@
1
+ import {dirnameFromMeta, readJsonFile, searchFileInDirectoryTree} from "#src/core/FS/utils";
2
+ import {program} from 'commander';
3
+ import {createCommandHTTP} from "#commands/CommandHTTP";
4
+ import {createCommandAuth} from "#commands/CommandAuth";
5
+ import {createCommandConfig} from "#commands/CommandConfig";
6
+ import {createCommandRegister} from "#commands/CommandRegister";
7
+ import {createCommandRefresh} from "#commands/CommandRefresh";
8
+ import {argumentAliasResolver} from "#commands/helper/AliasResolver";
9
+ import {createCommandInvite} from "#commands/CommandInvite";
10
+ import {addExamples, addUsage} from "#commands/utils";
11
+
12
+ program
13
+ .name('tunli')
14
+ .description('HTTP tunnel client')
15
+ .option('-v, --version', 'version', () => {
16
+ const packageJson = readJsonFile(searchFileInDirectoryTree('package.json', dirnameFromMeta(import.meta)))
17
+ console.log(`tunli: ${packageJson.version}`)
18
+ process.exit()
19
+ })
20
+
21
+ program.addCommand(createCommandConfig(program))
22
+ program.addCommand(createCommandHTTP(program), {isDefault: true})
23
+ program.addCommand(createCommandRegister(program))
24
+ program.addCommand(createCommandRefresh(program))
25
+ program.addCommand(createCommandInvite(program))
26
+ program.addCommand(createCommandAuth(program))
27
+
28
+ addExamples(program)
29
+ addUsage(program)
30
+
31
+ await program.parseAsync(argumentAliasResolver());
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "tunli",
3
+ "description": "Node.js application for creating HTTP tunnels to make local software projects accessible over the internet.",
4
+ "version": "0.0.19",
5
+ "main": "bin/tunli",
6
+ "bin": {
7
+ "tunli": "bin/tunli"
8
+ },
9
+ "scripts": {
10
+ "dev": "nodemon client.js"
11
+ },
12
+ "keywords": [
13
+ "HTTP tunnel",
14
+ "reverse proxy",
15
+ "remote access",
16
+ "local development",
17
+ "Node.js"
18
+ ],
19
+ "author": "Pascal Pfeifer <pascal@tunli.app>",
20
+ "imports": {
21
+ "#lib/*": "./src/lib/*.js",
22
+ "#commands/*": "./src/commands/*.js",
23
+ "#src/*": "./src/*.js"
24
+ },
25
+ "type": "module",
26
+ "dependencies": {
27
+ "axios": "^1.7.2",
28
+ "blessed": "^0.1.81",
29
+ "chalk": "^5.3.0",
30
+ "commander": "^12.1.0",
31
+ "https-proxy-agent": "^7.0.4",
32
+ "socket.io-client": "^4.7.5"
33
+ },
34
+ "devDependencies": {
35
+ "nodemon": "^3.1.3"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/tunlijs/tunli-client"
40
+ },
41
+ "publishConfig": {
42
+ "registry": "https://registry.npmjs.org"
43
+ },
44
+ "homepage": "https://tunli.app",
45
+ "bugs": {
46
+ "email": "dev@tunli.app",
47
+ "url": "https://github.com/tunlijs/tunli-client/issues"
48
+ },
49
+ "license": "GPL-3.0",
50
+ "readme": "README.md"
51
+ }
@@ -0,0 +1,146 @@
1
+ import {concat, createScreen} from "#src/cli-app/helper/utils";
2
+ import {dirnameFromMeta, readJsonFile, searchFileInDirectoryTree} from "#src/core/FS/utils";
3
+ import {ref} from "#src/core/Ref";
4
+ import {trimEnd} from "#src/utils/stringFunctions";
5
+ import chalk from "chalk";
6
+ import {getLatestVersion} from "#lib/Flow/getLatestVersion";
7
+ import {exec} from 'child_process'
8
+ import {checkGlobalInstallation, checkLocalInstallation} from "#src/utils/npmFunctions";
9
+
10
+ /**
11
+ *
12
+ * @param {TunnelClient} client
13
+ * @param {tunnelClientOptions} options
14
+ * @param {AppConfig} config
15
+ */
16
+ 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
146
+ }
@@ -0,0 +1,135 @@
1
+ import {List} from "#src/cli-app/elements/List/List";
2
+ import EventEmitter from "node:events";
3
+ import {Row} from "#src/cli-app/elements/Row";
4
+ import {ElementNode} from "#src/cli-app/elements/ElementNode";
5
+ import {arrayRemoveEntry} from "#src/utils/arrayFunctions";
6
+ import {Line} from "#src/cli-app/elements/Line";
7
+
8
+ export class Screen extends EventEmitter {
9
+
10
+ #screen
11
+
12
+ /**
13
+ * @type {ElementNode[]}
14
+ */
15
+ #elements = []
16
+
17
+ constructor(screen) {
18
+ super()
19
+ this.#screen = screen
20
+
21
+ for (const proxyFn of ['append']) {
22
+ this[proxyFn] = (...arg) => {
23
+ screen[proxyFn](...arg)
24
+ }
25
+ }
26
+
27
+ const ele = new ElementNode(this)
28
+ ele._cached.height = ele.height
29
+ ele._cached.top = ele.positionTop
30
+ this.#elements.push(ele)
31
+
32
+ this.on('delete-element', (ele) => {
33
+ this.#elements = arrayRemoveEntry(this.#elements, ele)
34
+ })
35
+
36
+ screen.on('keypress', (char, details) => this.emit('keypress', char, /** keypressEventDetails */ details))
37
+ }
38
+
39
+ get screen() {
40
+ return this.#screen
41
+ }
42
+
43
+ get children() {
44
+ return this.#elements
45
+ }
46
+
47
+ get width() {
48
+ return this.#screen.width
49
+ }
50
+
51
+ get height() {
52
+ return this.#screen.height
53
+ }
54
+
55
+ /**
56
+ * @param {string|array} key
57
+ * @param {keypressEventListener} callback
58
+ */
59
+ key(key, callback) {
60
+ this.#screen.key(key, callback)
61
+ }
62
+
63
+ /**
64
+ * @param {string|array} key
65
+ * @param {keypressEventListener} callback
66
+ */
67
+ onceKey(key, callback) {
68
+ this.#screen.onceKey(key, callback)
69
+ }
70
+
71
+ render() {
72
+ this.#screen.render()
73
+ }
74
+
75
+ destroy() {
76
+ this.#screen.destroy()
77
+ }
78
+
79
+ /**
80
+ *
81
+ * @param {ElementNode} ele
82
+ */
83
+ updateElement(ele) {
84
+
85
+ const heightChanged = ele._cached.height === ele.height
86
+ const topChanged = ele._cached.top === ele.positionTop
87
+
88
+ ele._cached.height = ele.height
89
+ ele._cached.top = ele.positionTop
90
+
91
+ if (heightChanged || topChanged) {
92
+ ele.nextElementSibling?.emit('prev-resize', ele)
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @param {...cliListOption} [options]
98
+ * @returns {List}
99
+ */
100
+ list(options) {
101
+ const ele = new List(this)
102
+ ele.init(options)
103
+
104
+ ele._cached.height = ele.height
105
+ ele._cached.top = ele.positionTop
106
+
107
+ this.#elements.push(ele)
108
+ return ele
109
+ }
110
+
111
+ /**
112
+ * @param content
113
+ * @param [top]
114
+ * @param [right]
115
+ * @returns {Row}
116
+ */
117
+ row(content, {top, right} = {}) {
118
+ const ele = new Row(this)
119
+ ele.init(content, {top, right})
120
+ ele._cached.height = ele.height
121
+ ele._cached.top = ele.positionTop
122
+
123
+ this.#elements.push(ele)
124
+ return ele
125
+ }
126
+
127
+ line() {
128
+ const ele = new Line(this)
129
+ ele.init()
130
+ ele._cached.height = ele.height
131
+ ele._cached.top = ele.positionTop
132
+ this.#elements.push(ele)
133
+ return ele
134
+ }
135
+ }
@@ -0,0 +1,97 @@
1
+ import EventEmitter from "node:events";
2
+
3
+ export class ElementNode extends EventEmitter {
4
+
5
+ _cached = {
6
+ top: null,
7
+ height: null
8
+ }
9
+
10
+ /**
11
+ * @type {ElementNode}
12
+ */
13
+ #nextElementSibling
14
+ /**
15
+ * @type {ElementNode}
16
+ */
17
+ #prevElementSibling
18
+ /**
19
+ * @type {Screen}
20
+ */
21
+ #screen
22
+ #height = 0
23
+
24
+
25
+ #detached = false
26
+ #offsetTop = 0;
27
+
28
+ /**
29
+ * @param {Screen} screen
30
+ */
31
+ constructor(screen) {
32
+ super()
33
+ this.#screen = screen
34
+
35
+ if (screen.children.length) {
36
+ const before = screen.children[screen.children.length - 1]
37
+ before.#nextElementSibling = this
38
+ this.#prevElementSibling = before
39
+ }
40
+
41
+ /**
42
+ * prev element postop and height
43
+ */
44
+ this.on('prev-resize', ({positionTop, height}) => {
45
+ this.emit('position-top', positionTop + height)
46
+ this.nextElementSibling?.emit('prev-resize', this)
47
+ })
48
+ }
49
+
50
+ get detached() {
51
+ return this.#detached
52
+ }
53
+
54
+ get nextElementSibling() {
55
+ return this.#nextElementSibling
56
+ }
57
+
58
+ get positionTop() {
59
+ if (this.#prevElementSibling) {
60
+ return this.#prevElementSibling.positionTop + this.#prevElementSibling.height + this.#offsetTop
61
+ }
62
+ return this.#offsetTop
63
+ }
64
+
65
+ set offsetTop(offsetTop) {
66
+ this.#offsetTop = offsetTop ?? 0
67
+ }
68
+
69
+ get screen() {
70
+ return this.#screen
71
+ }
72
+
73
+ get height() {
74
+ return this.#height
75
+ }
76
+
77
+ set height(height) {
78
+ const update = this.#height !== height
79
+ this.#height = height
80
+ if (update) {
81
+ this.emit('height', height)
82
+ }
83
+ }
84
+
85
+ delete() {
86
+
87
+ this.#screen.emit('delete-element', this)
88
+ this.emit('delete', this)
89
+ this.#detached = true
90
+ const next = this.#nextElementSibling
91
+
92
+ if (next) {
93
+ next.#prevElementSibling = this.#prevElementSibling
94
+ this.#nextElementSibling?.emit('prev-resize', this.#prevElementSibling)
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,21 @@
1
+ import {ElementNode} from "#src/cli-app/elements/ElementNode";
2
+ import blessed from "blessed";
3
+
4
+ export class Line extends ElementNode {
5
+
6
+ /**
7
+ * @param {("horizontal", "vertical")} orientation
8
+ */
9
+ init(orientation = 'horizontal') {
10
+
11
+ const line = blessed.line({
12
+ orientation
13
+ })
14
+
15
+ this.height = 1
16
+ this.on('position-top', (val) => line.top = val)
17
+ this.on('height', (val) => line.height = val)
18
+ this.on('delete', (val) => line.detach())
19
+ this.screen.append(line)
20
+ }
21
+ }