tunli 0.0.25 → 0.0.27
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/README.md
CHANGED
|
@@ -126,6 +126,8 @@ Tunli relies on the following packages:
|
|
|
126
126
|
|
|
127
127
|
For development purposes, you can start the application using nodemon to automatically restart it on file changes:
|
|
128
128
|
|
|
129
|
+
_$ TUNLI_API_SERVER_URL=http://127.0.0.1:10000/api TUNLI_DASHBOARD=off TUNLI_SERVER=http://127.0.0.1:10000 TUNLI_PROXY_URL='http://127.0.0.1:10000/proxy/{{ uuid }}' node client.js register -f
|
|
130
|
+
|
|
129
131
|
```bash
|
|
130
132
|
npm run dev
|
|
131
133
|
```
|
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.
|
|
4
|
+
"version": "0.0.27",
|
|
5
5
|
"main": "bin/tunli",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tunli": "bin/tunli"
|
|
@@ -24,14 +24,15 @@
|
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"axios": "^1.
|
|
27
|
+
"axios": "^1.11.0",
|
|
28
28
|
"blessed": "^0.1.81",
|
|
29
|
-
"chalk": "^5.
|
|
30
|
-
"commander": "^
|
|
31
|
-
"
|
|
29
|
+
"chalk": "^5.5.0",
|
|
30
|
+
"commander": "^14.0.0",
|
|
31
|
+
"qrcode": "^1.5.4",
|
|
32
|
+
"socket.io-client": "^4.8.1"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
|
-
"nodemon": "^3.1.
|
|
35
|
+
"nodemon": "^3.1.10"
|
|
35
36
|
},
|
|
36
37
|
"repository": {
|
|
37
38
|
"type": "git",
|
package/src/cli-app/Dashboard.js
CHANGED
|
@@ -6,6 +6,7 @@ import chalk from "chalk";
|
|
|
6
6
|
import {getLatestVersion} from "#lib/Flow/getLatestVersion";
|
|
7
7
|
import {exec} from 'child_process'
|
|
8
8
|
import {checkGlobalInstallation, checkLocalInstallation} from "#src/utils/npmFunctions";
|
|
9
|
+
import QRCode from 'qrcode'
|
|
9
10
|
|
|
10
11
|
export class Dashboard {
|
|
11
12
|
|
|
@@ -82,6 +83,13 @@ export class Dashboard {
|
|
|
82
83
|
const lastBlockedIp = ref('')
|
|
83
84
|
const availableUpdate = ref('')
|
|
84
85
|
|
|
86
|
+
screen.key('C-q', () => {
|
|
87
|
+
QRCode.toString(forwardingUrl.value, {type: 'terminal'}, (err, url) => {
|
|
88
|
+
if (err) throw err
|
|
89
|
+
screen.newFullScreenModal(url)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
85
93
|
getLatestVersion().then((version) => {
|
|
86
94
|
if (version && version !== packageJson.version) {
|
|
87
95
|
availableUpdate.value = chalk.yellow(`update available (version ${version}, Ctrl-U to update)`)
|
|
@@ -182,6 +190,7 @@ export class Dashboard {
|
|
|
182
190
|
infoList.row(chalk.yellow('Update'), availableUpdate).if(() => availableUpdate)
|
|
183
191
|
infoList.row('Profile', config.profile)
|
|
184
192
|
infoList.row('Config', config.configPath)
|
|
193
|
+
infoList.row('QR-Code', 'Ctrl-Q')
|
|
185
194
|
|
|
186
195
|
if (allowedCidr || deniedCidr) infoList.row('')
|
|
187
196
|
if (allowedCidr) infoList.row('Allowed', allowedCidr)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import blessed from "blessed";
|
|
2
|
+
|
|
3
|
+
const centerContent = (text, boxWidth) => {
|
|
4
|
+
const lines = text.split('\n')
|
|
5
|
+
return lines.map(line => {
|
|
6
|
+
const pad = Math.floor((boxWidth - line.length) / 2)
|
|
7
|
+
return ' '.repeat(Math.max(pad, 0)) + line
|
|
8
|
+
}).join('\n')
|
|
9
|
+
}
|
|
10
|
+
export const newFullScreenModal = (screen, text) => {
|
|
11
|
+
// Backdrop zum Blocken von Inputs „unter“ dem Modal
|
|
12
|
+
const backdrop = blessed.box({
|
|
13
|
+
top: 0, left: 0, width: '100%', height: '100%',
|
|
14
|
+
style: {bg: 'black'},
|
|
15
|
+
mouse: true, clickable: true
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Größe anhand des Inhalts bestimmen (mit Padding & Border)
|
|
19
|
+
const lines = text.split('\n')
|
|
20
|
+
const contentWidth = Math.max(...lines.map(l => l.length))
|
|
21
|
+
const contentHeight = lines.length
|
|
22
|
+
const boxWidth = Math.min(contentWidth + 4, screen.width - 2)
|
|
23
|
+
//const centeredQr = centerContent(text, screen.width - 2) // -2 wegen Padding/Border
|
|
24
|
+
|
|
25
|
+
const qrBox = blessed.box({
|
|
26
|
+
top: 'center',
|
|
27
|
+
left: 'center',
|
|
28
|
+
width: boxWidth,
|
|
29
|
+
height: Math.min(contentHeight + 2, screen.height - 2),
|
|
30
|
+
padding: {left: 1, right: 1},
|
|
31
|
+
tags: false,
|
|
32
|
+
scrollable: true,
|
|
33
|
+
content: text,
|
|
34
|
+
border: 'line',
|
|
35
|
+
style: {
|
|
36
|
+
fg: 'white',
|
|
37
|
+
bg: 'black',
|
|
38
|
+
border: {fg: 'cyan'}
|
|
39
|
+
},
|
|
40
|
+
keys: true,
|
|
41
|
+
mouse: true,
|
|
42
|
+
alwaysScroll: true
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Einfügen und nach vorne bringen
|
|
46
|
+
screen.append(backdrop)
|
|
47
|
+
screen.append(qrBox)
|
|
48
|
+
qrBox.focus()
|
|
49
|
+
screen.render()
|
|
50
|
+
|
|
51
|
+
const close = () => {
|
|
52
|
+
backdrop.detach()
|
|
53
|
+
qrBox.detach()
|
|
54
|
+
screen.render()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Schließen über Tastatur/Click
|
|
58
|
+
qrBox.key(['escape', 'q', 'C-q'], close)
|
|
59
|
+
backdrop.on('click', close)
|
|
60
|
+
|
|
61
|
+
// Bei Resize neu zentriert blessed automatisch; nur neu rendern
|
|
62
|
+
screen.on('resize', () => screen.render())
|
|
63
|
+
}
|
package/src/cli-app/Screen.js
CHANGED
|
@@ -4,6 +4,7 @@ import {Row} from "#src/cli-app/elements/Row";
|
|
|
4
4
|
import {ElementNode} from "#src/cli-app/elements/ElementNode";
|
|
5
5
|
import {arrayRemoveEntry} from "#src/utils/arrayFunctions";
|
|
6
6
|
import {Line} from "#src/cli-app/elements/Line";
|
|
7
|
+
import {newFullScreenModal} from "#src/cli-app/Screen/FullScreenModal";
|
|
7
8
|
|
|
8
9
|
export class Screen extends EventEmitter {
|
|
9
10
|
|
|
@@ -52,6 +53,10 @@ export class Screen extends EventEmitter {
|
|
|
52
53
|
return this.#screen.height
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
newFullScreenModal(text) {
|
|
57
|
+
return newFullScreenModal(this, text)
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
/**
|
|
56
61
|
* @param {string|array} key
|
|
57
62
|
* @param {keypressEventListener} callback
|
package/src/lib/Flow/proxyUrl.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {securedHttpClient} from "#lib/HttpClient";
|
|
2
|
-
import {
|
|
2
|
+
import {TUNLI_PROXY_URL} from "#lib/defs";
|
|
3
|
+
import {replaceTemplatePlaceholders} from "#src/utils/stringFunctions";
|
|
3
4
|
|
|
4
5
|
export const requestNewProxyUrl = async (token) => {
|
|
5
6
|
|
|
@@ -10,7 +11,9 @@ export const requestNewProxyUrl = async (token) => {
|
|
|
10
11
|
process.exit(1)
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
return
|
|
14
|
+
return replaceTemplatePlaceholders(TUNLI_PROXY_URL, {
|
|
15
|
+
uuid: data
|
|
16
|
+
})
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export const renewProxyUrlRegistration = async (proxyUrl, token) => {
|
package/src/lib/defs.js
CHANGED
|
@@ -2,9 +2,12 @@ import {resolve} from "path";
|
|
|
2
2
|
import {homedir} from "os";
|
|
3
3
|
|
|
4
4
|
export const SERVER_HOST = 'tunli.app'
|
|
5
|
+
|
|
6
|
+
export const TUNLI_PROXY_URL = process.env.TUNLI_PROXY_URL ?? 'https://{{ uuid }}.tunli.app'
|
|
7
|
+
|
|
5
8
|
export const CONFIG_DIR_NAME = '.tunli'
|
|
6
9
|
|
|
7
|
-
export const AUTH_SERVER_URL = 'https://api.tunli.app'
|
|
10
|
+
export const AUTH_SERVER_URL = process.env.TUNLI_API_SERVER_URL ?? 'https://api.tunli.app'
|
|
8
11
|
|
|
9
12
|
export const GLOBAL_CONFIG_DIR = resolve(homedir(), CONFIG_DIR_NAME);
|
|
10
13
|
export const CONFIG_FILENAME = 'default.json'
|
|
@@ -32,3 +32,28 @@ export const padEndIgnoreControlChars = (string, maxLength, fillString = ' ', au
|
|
|
32
32
|
const padding = ''.padEnd(maxLength - stringWithoutCC.length, fillString)
|
|
33
33
|
return `${string}${padding}`
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Replaces template placeholders in a string with corresponding values from a replacements object.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} template - The string containing placeholders in the format {{ placeholder }}.
|
|
40
|
+
* @param {object} replacements - The object containing replacement values. Nested placeholders can be accessed using dot notation.
|
|
41
|
+
*/
|
|
42
|
+
export const replaceTemplatePlaceholders = (template, replacements) => {
|
|
43
|
+
|
|
44
|
+
return template.replace(/{{\s*([^{}|]+)\s*(\|[^{}]+)*\s*}}/ig, (match, placeholder) => {
|
|
45
|
+
|
|
46
|
+
placeholder = placeholder.trim()
|
|
47
|
+
const keys = placeholder.split('.')
|
|
48
|
+
|
|
49
|
+
let value = replacements
|
|
50
|
+
for (const part of keys) {
|
|
51
|
+
if (!value) {
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
value = value [part]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return value ?? ''
|
|
58
|
+
})
|
|
59
|
+
}
|