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.
- package/LICENSE.md +595 -0
- package/README.md +135 -0
- package/bin/tunli +11 -0
- package/client.js +31 -0
- package/package.json +51 -0
- package/src/cli-app/Dashboard.js +146 -0
- package/src/cli-app/Screen.js +135 -0
- package/src/cli-app/elements/ElementNode.js +97 -0
- package/src/cli-app/elements/Line.js +21 -0
- package/src/cli-app/elements/List/List.js +227 -0
- package/src/cli-app/elements/List/ListCell.js +83 -0
- package/src/cli-app/elements/List/ListColumn.js +52 -0
- package/src/cli-app/elements/List/ListRow.js +118 -0
- package/src/cli-app/elements/Row.js +38 -0
- package/src/cli-app/helper/utils.js +42 -0
- package/src/commands/Action/addDelValuesAction.js +56 -0
- package/src/commands/CommandAuth.js +32 -0
- package/src/commands/CommandClearAll.js +27 -0
- package/src/commands/CommandConfig.js +57 -0
- package/src/commands/CommandHTTP.js +131 -0
- package/src/commands/CommandInvite.js +38 -0
- package/src/commands/CommandRefresh.js +35 -0
- package/src/commands/CommandRegister.js +48 -0
- package/src/commands/Option/DeleteOption.js +6 -0
- package/src/commands/Option/SelectConfigOption.js +52 -0
- package/src/commands/SubCommand/AllowDenyCidrCommand.js +28 -0
- package/src/commands/SubCommand/HostCommand.js +22 -0
- package/src/commands/SubCommand/PortCommand.js +20 -0
- package/src/commands/helper/AliasResolver.js +13 -0
- package/src/commands/helper/BindArgs.js +53 -0
- package/src/commands/helper/SharedArg.js +32 -0
- package/src/commands/utils.js +96 -0
- package/src/config/ConfigAbstract.js +318 -0
- package/src/config/ConfigManager.js +70 -0
- package/src/config/GlobalConfig.js +14 -0
- package/src/config/GlobalLocalShardConfigAbstract.js +76 -0
- package/src/config/LocalConfig.js +7 -0
- package/src/config/PropertyConfig.js +122 -0
- package/src/config/SystemConfig.js +31 -0
- package/src/core/FS/utils.js +60 -0
- package/src/core/Ref.js +70 -0
- package/src/lib/Flow/getCurrentIp.js +18 -0
- package/src/lib/Flow/getLatestVersion.js +13 -0
- package/src/lib/Flow/proxyUrl.js +32 -0
- package/src/lib/Flow/validateAuthToken.js +19 -0
- package/src/lib/HttpClient.js +61 -0
- package/src/lib/defs.js +10 -0
- package/src/net/IPV4.js +139 -0
- package/src/net/http/IncomingMessage.js +92 -0
- package/src/net/http/ServerResponse.js +126 -0
- package/src/net/http/TunliRequest.js +1 -0
- package/src/net/http/TunliResponse.js +1 -0
- package/src/net/http/TunnelRequest.js +177 -0
- package/src/net/http/TunnelResponse.js +119 -0
- package/src/tunnel-client/TunnelClient.js +136 -0
- package/src/utils/arrayFunctions.js +45 -0
- package/src/utils/checkFunctions.js +161 -0
- package/src/utils/cliFunctions.js +62 -0
- package/src/utils/createRequest.js +12 -0
- package/src/utils/httpFunction.js +23 -0
- package/src/utils/npmFunctions.js +27 -0
- package/src/utils/stringFunctions.js +34 -0
- package/types/index.d.ts +112 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {Readable} from "stream";
|
|
2
|
+
import {TunnelResponse} from "#src/net/http/TunnelResponse";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
|
|
6
|
+
class TunnelRequest extends Readable {
|
|
7
|
+
|
|
8
|
+
#socket
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* uuid v4 request/response id
|
|
12
|
+
* @type {string}
|
|
13
|
+
*/
|
|
14
|
+
#requestId
|
|
15
|
+
|
|
16
|
+
constructor({socket, requestId}) {
|
|
17
|
+
|
|
18
|
+
super();
|
|
19
|
+
this.#socket = socket;
|
|
20
|
+
this.#requestId = requestId;
|
|
21
|
+
|
|
22
|
+
this.#bindEvents()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#bindEvents() {
|
|
26
|
+
const events = {
|
|
27
|
+
'request-pipe': this.#onRequestPipe.bind(this),
|
|
28
|
+
'request-pipes': this.#onRequestPipes.bind(this),
|
|
29
|
+
'request-pipe-error': this.#onRequestPipeError.bind(this),
|
|
30
|
+
'request-pipe-end': this.#onRequestPipeEnd.bind(this),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const [event, handler] of Object.entries(events)) {
|
|
34
|
+
this.#socket.on(event, handler);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.once('close', () => {
|
|
38
|
+
for (const [event, handler] of Object.entries(events)) {
|
|
39
|
+
this.#socket.off(event, handler);
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#onRequestPipe(requestId, data) {
|
|
45
|
+
if (this.#requestId === requestId) {
|
|
46
|
+
this.push(data);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#onRequestPipes(requestId, data) {
|
|
51
|
+
if (this.#requestId === requestId) {
|
|
52
|
+
data.forEach((chunk) => {
|
|
53
|
+
this.push(chunk);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#onRequestPipeError(requestId, error) {
|
|
59
|
+
if (this.#requestId === requestId) {
|
|
60
|
+
this.destroy(new Error(error));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#onRequestPipeEnd(requestId, data) {
|
|
65
|
+
if (this.#requestId === requestId) {
|
|
66
|
+
if (data) {
|
|
67
|
+
this.push(data);
|
|
68
|
+
}
|
|
69
|
+
this.push(null);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_read() {
|
|
74
|
+
// No op
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const setupErrorHandlers = (source, target) => {
|
|
79
|
+
const onTunnelRequestError = (e) => {
|
|
80
|
+
source.off('end', onTunnelRequestEnd);
|
|
81
|
+
target.destroy(e);
|
|
82
|
+
};
|
|
83
|
+
const onTunnelRequestEnd = () => {
|
|
84
|
+
target.off('error', onTunnelRequestError);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
source.once('error', onTunnelRequestError);
|
|
88
|
+
source.once('end', onTunnelRequestEnd);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {SocketIoRawRequestObject} req
|
|
94
|
+
* @param {EventEmitter} eventEmitter
|
|
95
|
+
* @param {tunnelClientOptions} options
|
|
96
|
+
*/
|
|
97
|
+
export const forwardTunnelRequestToProxyTarget = (req, eventEmitter, options) => {
|
|
98
|
+
|
|
99
|
+
const tunnelRequest = new TunnelRequest({
|
|
100
|
+
requestId: req.requestId,
|
|
101
|
+
socket: req.tunnelSocket,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const isWebSocket = req.headers.upgrade === 'websocket'
|
|
105
|
+
const localReq = options.protocol === 'https' ? https.request(req) : http.request(req)
|
|
106
|
+
|
|
107
|
+
tunnelRequest.pipe(localReq)
|
|
108
|
+
|
|
109
|
+
setupErrorHandlers(tunnelRequest, localReq)
|
|
110
|
+
|
|
111
|
+
const onLocalResponse = (localRes) => {
|
|
112
|
+
localReq.off('error', onLocalError)
|
|
113
|
+
if (isWebSocket && localRes.upgrade) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const tunnelResponse = new TunnelResponse({
|
|
118
|
+
responseId: req.requestId,
|
|
119
|
+
socket: req.tunnelSocket,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
tunnelResponse.writeHead(
|
|
123
|
+
localRes.statusCode,
|
|
124
|
+
localRes.statusMessage,
|
|
125
|
+
localRes.headers,
|
|
126
|
+
localRes.httpVersion
|
|
127
|
+
)
|
|
128
|
+
localRes.pipe(tunnelResponse)
|
|
129
|
+
if (!isWebSocket) {
|
|
130
|
+
eventEmitter.emit('response', localRes, req)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const onLocalError = (error) => {
|
|
134
|
+
|
|
135
|
+
let statusCode = 500
|
|
136
|
+
let statusMessage = 'UNKNOWN ERROR'
|
|
137
|
+
if (error.code === 'ECONNREFUSED') {
|
|
138
|
+
statusCode = 502
|
|
139
|
+
statusMessage = 'Bad gateway'
|
|
140
|
+
} else if (error.code === 'ECONNRESET') {
|
|
141
|
+
statusCode = 500
|
|
142
|
+
statusMessage = 'Connection reset'
|
|
143
|
+
} else {
|
|
144
|
+
console.error('IMPLEMENT ME')
|
|
145
|
+
console.error(error)
|
|
146
|
+
console.error(error.code)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
eventEmitter.emit('response', {statusCode, statusMessage}, req)
|
|
150
|
+
localReq.off('response', onLocalResponse)
|
|
151
|
+
req.tunnelSocket.emit('request-error', req.requestId, error && error.message)
|
|
152
|
+
tunnelRequest.destroy(error)
|
|
153
|
+
}
|
|
154
|
+
const onUpgrade = (localRes, localSocket, localHead) => {
|
|
155
|
+
if (localHead && localHead.length) {
|
|
156
|
+
localSocket.unshift(localHead)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const tunnelResponse = new TunnelResponse({
|
|
160
|
+
responseId: req.requestId,
|
|
161
|
+
socket: req.tunnelSocket,
|
|
162
|
+
duplex: true,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
tunnelResponse.writeHead(null, null, localRes.headers)
|
|
166
|
+
localSocket
|
|
167
|
+
.pipe(tunnelResponse)
|
|
168
|
+
.pipe(localSocket)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
localReq.once('error', onLocalError)
|
|
172
|
+
localReq.once('response', onLocalResponse)
|
|
173
|
+
|
|
174
|
+
if (isWebSocket) {
|
|
175
|
+
localReq.on('upgrade', onUpgrade)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {Duplex} from "stream";
|
|
2
|
+
|
|
3
|
+
export class TunnelResponse extends Duplex {
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
#socket
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* uuid v4 request/response id
|
|
12
|
+
* @type {string}
|
|
13
|
+
*/
|
|
14
|
+
#responseId
|
|
15
|
+
|
|
16
|
+
constructor({socket, responseId, duplex}) {
|
|
17
|
+
|
|
18
|
+
super();
|
|
19
|
+
this.#socket = socket;
|
|
20
|
+
this.#responseId = responseId;
|
|
21
|
+
|
|
22
|
+
if (duplex) {
|
|
23
|
+
this.#bindEvents()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#onResponsePipe(responseId, data) {
|
|
28
|
+
if (this.#responseId === responseId) {
|
|
29
|
+
this.push(data);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#onResponsePipes(responseId, data) {
|
|
34
|
+
if (this.#responseId === responseId) {
|
|
35
|
+
data.forEach((chunk) => {
|
|
36
|
+
this.push(chunk);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#onResponsePipeError(responseId, error) {
|
|
42
|
+
if (this.#responseId === responseId) {
|
|
43
|
+
this.destroy(new Error(error));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#onResponsePipeEnd(responseId, data) {
|
|
48
|
+
if (this.#responseId === responseId) {
|
|
49
|
+
if (data) {
|
|
50
|
+
this.push(data);
|
|
51
|
+
}
|
|
52
|
+
this.push(null);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#bindEvents() {
|
|
57
|
+
|
|
58
|
+
const events = {
|
|
59
|
+
'response-pipe': this.#onResponsePipe.bind(this),
|
|
60
|
+
'response-pipes': this.#onResponsePipes.bind(this),
|
|
61
|
+
'response-pipe-error': this.#onResponsePipeError.bind(this),
|
|
62
|
+
'response-pipe-end': this.#onResponsePipeEnd.bind(this)
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (const [event, handler] of Object.entries(events)) {
|
|
66
|
+
this.#socket.on(event, handler);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.once('close', () => {
|
|
70
|
+
console.log('CLOSING CALL')
|
|
71
|
+
for (const [event, handler] of Object.entries(events)) {
|
|
72
|
+
this.#socket.off(event, handler);
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_write(chunk, encoding, callback) {
|
|
78
|
+
this.#socket.emit('response-pipe', this.#responseId, chunk);
|
|
79
|
+
this.#socket.io.engine.once('drain', callback)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_writev(chunks, callback) {
|
|
83
|
+
this.#socket.emit('response-pipes', this.#responseId, chunks);
|
|
84
|
+
this.#socket.io.engine.once('drain', callback)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_final(callback) {
|
|
88
|
+
this.#socket.emit('response-pipe-end', this.#responseId);
|
|
89
|
+
this.#socket.io.engine.once('drain', callback)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_destroy(e, callback) {
|
|
93
|
+
if (e) {
|
|
94
|
+
this.#socket.emit('response-pipe-error', this.#responseId, e && e.message);
|
|
95
|
+
this.#socket.io.engine.once('drain', callback)
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
callback();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {number} statusCode
|
|
103
|
+
* @param {string} statusMessage
|
|
104
|
+
* @param {Object} headers
|
|
105
|
+
* @param {string} httpVersion
|
|
106
|
+
*/
|
|
107
|
+
writeHead(statusCode, statusMessage, headers, httpVersion) {
|
|
108
|
+
|
|
109
|
+
this.#socket.emit('response', this.#responseId, {
|
|
110
|
+
statusCode,
|
|
111
|
+
statusMessage,
|
|
112
|
+
headers,
|
|
113
|
+
httpVersion,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_read(size) {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {securedHttpClient} from "#lib/HttpClient";
|
|
2
|
+
import {forwardTunnelRequestToProxyTarget} from "#src/net/http/TunnelRequest";
|
|
3
|
+
import {io} from "socket.io-client";
|
|
4
|
+
import EventEmitter from "node:events";
|
|
5
|
+
import {ref} from "#src/core/Ref";
|
|
6
|
+
import {isValidRemoteAddress} from "#commands/helper/BindArgs";
|
|
7
|
+
import {createRequestFromRaw} from "#src/utils/httpFunction";
|
|
8
|
+
|
|
9
|
+
export class TunnelClient extends EventEmitter {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {tunnelClientOptions}
|
|
13
|
+
*/
|
|
14
|
+
#options
|
|
15
|
+
#socket
|
|
16
|
+
#latency = ref(0)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {tunnelClientOptions} options
|
|
20
|
+
*/
|
|
21
|
+
constructor(options) {
|
|
22
|
+
super()
|
|
23
|
+
this.#options = options
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get latency() {
|
|
27
|
+
return this.#latency
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#bindEvents() {
|
|
31
|
+
|
|
32
|
+
const socket = this.#socket
|
|
33
|
+
const options = this.#options
|
|
34
|
+
|
|
35
|
+
socket.on('connect', () => {
|
|
36
|
+
if (socket.connected) {
|
|
37
|
+
this.emit('tunnel-connection-established')
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
socket.on('connect_error', (e) => {
|
|
42
|
+
this.emit('tunnel-connection-error', e)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
socket.on('disconnect', () => {
|
|
46
|
+
this.emit('tunnel-connection-closed')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
socket.on('request', ( /** string */ requestId, /** SocketIoRawRequestObject */ request) => {
|
|
50
|
+
|
|
51
|
+
const req = createRequestFromRaw(request, requestId, socket)
|
|
52
|
+
request.requestId = requestId
|
|
53
|
+
request.tunnelSocket = socket
|
|
54
|
+
|
|
55
|
+
if (!isValidRemoteAddress(req.remoteAddress, options)) {
|
|
56
|
+
this.emit('blocked', req.remoteAddress)
|
|
57
|
+
req.res.status(401).send(`Access denied for ip ${req.remoteAddress}`)
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isWebSocket = request.headers.upgrade === 'websocket';
|
|
62
|
+
|
|
63
|
+
request.port = options.port;
|
|
64
|
+
request.hostname = options.host; // TODO hostanme : www.foo.bar.de vs host: www.foo.bar:80
|
|
65
|
+
request.rejectUnauthorized = false
|
|
66
|
+
|
|
67
|
+
if (options.origin) {
|
|
68
|
+
request.headers.host = options.origin;
|
|
69
|
+
} else {
|
|
70
|
+
request.headers.host = request.hostname
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!isWebSocket) {
|
|
74
|
+
this.emit('request', request)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
forwardTunnelRequestToProxyTarget(request, this, this.#options)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async #keepAlive() {
|
|
82
|
+
const ping = () => {
|
|
83
|
+
const start = Date.now();
|
|
84
|
+
this.#socket.volatile.emit('ping', () => {
|
|
85
|
+
this.#latency.value = Date.now() - start;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
while (this.#latency.value === 0) {
|
|
90
|
+
ping()
|
|
91
|
+
await new Promise((resolve) => setTimeout((resolve), 100))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setInterval(ping, 5000)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async init(dashboard) {
|
|
98
|
+
|
|
99
|
+
const params = await this.#createParameters(dashboard)
|
|
100
|
+
this.#socket = io(this.#options.server, params)
|
|
101
|
+
this.#bindEvents()
|
|
102
|
+
this.#keepAlive();
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async #createParameters(dashboard) {
|
|
107
|
+
const options = this.#options
|
|
108
|
+
const webSocketCapturePath = await securedHttpClient(options.authToken).get('/capture_path')
|
|
109
|
+
|
|
110
|
+
if (webSocketCapturePath.error) {
|
|
111
|
+
dashboard.destroy()
|
|
112
|
+
if (webSocketCapturePath.error?.message === 'Request failed with status code 401') {
|
|
113
|
+
console.error('missing authorization, check your registration and try again')
|
|
114
|
+
} else {
|
|
115
|
+
console.error('could not connect to remote server. pls try again later')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const initParams = {
|
|
122
|
+
path: webSocketCapturePath.data,
|
|
123
|
+
transports: ['websocket'],
|
|
124
|
+
auth: {
|
|
125
|
+
token: options.authToken,
|
|
126
|
+
},
|
|
127
|
+
extraHeaders: {},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (options.path) {
|
|
131
|
+
initParams.extraHeaders['path-prefix'] = options.path;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return initParams
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes duplicate elements from an array.
|
|
3
|
+
* @param {any[]} value - The array from which to remove duplicates.
|
|
4
|
+
* @returns {any[]|null} - A new array with unique elements, or null if the input is not an array.
|
|
5
|
+
*/
|
|
6
|
+
export const arrayUnique = (value) => {
|
|
7
|
+
if (!Array.isArray(value)) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
return Array.from(new Set(value))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @template A
|
|
15
|
+
* @template B
|
|
16
|
+
* Merges two arrays into one.
|
|
17
|
+
* @param {A[]} arr1 - The first array.
|
|
18
|
+
* @param {B[]} arr2 - The second array.
|
|
19
|
+
* @returns {[A, B]} - A new array containing elements from both input arrays.
|
|
20
|
+
*/
|
|
21
|
+
export const arrayMerge = (arr1, arr2) => {
|
|
22
|
+
arr1 ??= []
|
|
23
|
+
arr2 ??= []
|
|
24
|
+
return [...arr1, ...arr2]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subtracts elements of the second array from the first array.
|
|
29
|
+
* @param {any[]} arr1 - The array from which to subtract elements.
|
|
30
|
+
* @param {any[]} arr2 - The array containing elements to subtract.
|
|
31
|
+
* @returns {any[]} - A new array containing elements from arr1 that are not in arr2.
|
|
32
|
+
*/
|
|
33
|
+
export const arraySub = (arr1, arr2) => {
|
|
34
|
+
arr1 ??= []
|
|
35
|
+
arr2 ??= []
|
|
36
|
+
return arr1.filter(x => !arr2.includes(x))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const arrayRemoveEntry = (array, search) => {
|
|
40
|
+
const delPos = array.indexOf(search)
|
|
41
|
+
if (delPos > -1) {
|
|
42
|
+
array = [...array.slice(0, delPos), ...array.slice(delPos + 1)]
|
|
43
|
+
}
|
|
44
|
+
return array
|
|
45
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {InvalidArgumentError} from "commander"
|
|
2
|
+
import {ipV4} from "#src/net/IPV4";
|
|
3
|
+
import {isSharedArg} from "#commands/helper/SharedArg";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate and format an IPv4 address with CIDR notation.
|
|
7
|
+
* This function checks if the provided value is a valid IPv4 address with or without CIDR suffix.
|
|
8
|
+
* If valid, it returns the address formatted with its CIDR suffix.
|
|
9
|
+
* If invalid, it throws an InvalidArgumentError with an appropriate message.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} value - The IPv4 address (with or without CIDR) to validate.
|
|
12
|
+
* @returns {string} - The validated and formatted IPv4 address with CIDR suffix.
|
|
13
|
+
* @throws {InvalidArgumentError} - If the IPv4 address is not valid.
|
|
14
|
+
*/
|
|
15
|
+
export const checkIpV4Cidr = (value) => {
|
|
16
|
+
const val = ipV4(value)
|
|
17
|
+
if (!val.isValid) {
|
|
18
|
+
throw new InvalidArgumentError('The provided value must be a valid IPv4 address, with or without CIDR notation.')
|
|
19
|
+
}
|
|
20
|
+
return `${val}/${val.cidrSuffix}`
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Checks if the value is an integer.
|
|
24
|
+
* @param {any} value - The value to check.
|
|
25
|
+
* @param {string} [errorMessage='Value must be an integer'] - The error message to throw if validation fails.
|
|
26
|
+
* @returns {number} - The validated integer.
|
|
27
|
+
* @throws {InvalidArgumentError} - If the value is not a valid integer.
|
|
28
|
+
*/
|
|
29
|
+
export const checkInt = (value, errorMessage = 'Value must be an integer') => {
|
|
30
|
+
value = value.toString()
|
|
31
|
+
|
|
32
|
+
if (!/^\d+$/.test(value)) {
|
|
33
|
+
throw new InvalidArgumentError(errorMessage);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return parseInt(value)
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Checks if the value is a valid URL.
|
|
40
|
+
* @param {string} value - The value to check.
|
|
41
|
+
* @returns {string} - The validated URL.
|
|
42
|
+
* @throws {InvalidArgumentError} - If the value is not a valid URL.
|
|
43
|
+
*/
|
|
44
|
+
export const checkUrl = (value) => {
|
|
45
|
+
try {
|
|
46
|
+
return new URL(value).toString()
|
|
47
|
+
} catch (e) {
|
|
48
|
+
throw new InvalidArgumentError('Invalid URL')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Checks if the value is a valid host.
|
|
53
|
+
* @param {Ref|*} valueOrSharedArg - The value or shared argument to check.
|
|
54
|
+
* @param {boolean} [isArgument=false] - Whether the value is an argument.
|
|
55
|
+
* @param {*} [value] - The value to check.
|
|
56
|
+
* @returns {string} - The validated host.
|
|
57
|
+
* @throws {InvalidArgumentError} - If the value is not a valid host.
|
|
58
|
+
*/
|
|
59
|
+
export const checkHost = (valueOrSharedArg, isArgument = false, value) => {
|
|
60
|
+
if (isSharedArg(valueOrSharedArg)) {
|
|
61
|
+
const {url, host} = valueOrSharedArg.value
|
|
62
|
+
if (url) {
|
|
63
|
+
throw new InvalidArgumentError('You must not set a host if the port argument is a URL')
|
|
64
|
+
} else if (host) {
|
|
65
|
+
throw new InvalidArgumentError('You must not set a host argument if the host option has been set')
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
value = valueOrSharedArg
|
|
69
|
+
valueOrSharedArg = undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const check = new URL('http://localhost')
|
|
73
|
+
check.hostname = value
|
|
74
|
+
|
|
75
|
+
if (check.hostname !== value.toLowerCase()) {
|
|
76
|
+
throw new InvalidArgumentError('Invalid host');
|
|
77
|
+
}
|
|
78
|
+
if (isSharedArg(valueOrSharedArg) && !isArgument) {
|
|
79
|
+
valueOrSharedArg.value.host = check.host
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return check.host
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Checks if the value is a valid port.
|
|
86
|
+
* @param {Ref|*} valueOrSharedArg - The value or shared argument to check.
|
|
87
|
+
* @param {boolean} [isArgument=false] - Whether the value is an argument.
|
|
88
|
+
* @param {*} [value] - The value to check.
|
|
89
|
+
* @returns {number|Ref} - The validated port or shared argument with port.
|
|
90
|
+
* @throws {InvalidArgumentError} - If the value is not a valid port.
|
|
91
|
+
*/
|
|
92
|
+
export const checkPort = (valueOrSharedArg, isArgument = false, value) => {
|
|
93
|
+
const handleUrlArg = (value) => {
|
|
94
|
+
try {
|
|
95
|
+
const url = new URL(value)
|
|
96
|
+
|
|
97
|
+
let port = url.port
|
|
98
|
+
const protocol = url.protocol.substring(0, url.protocol.length - 1)
|
|
99
|
+
|
|
100
|
+
if (!port) {
|
|
101
|
+
if (protocol === 'http') {
|
|
102
|
+
port = 80
|
|
103
|
+
} else if (protocol === 'https') {
|
|
104
|
+
port = 443
|
|
105
|
+
}
|
|
106
|
+
url.port = port.toString()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
valueOrSharedArg.value.url = {
|
|
110
|
+
protocol,
|
|
111
|
+
host: url.hostname,
|
|
112
|
+
port: parseInt(port)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return port
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let portFromUrl
|
|
122
|
+
|
|
123
|
+
if (isSharedArg(valueOrSharedArg)) {
|
|
124
|
+
const {url, port, host} = valueOrSharedArg.value
|
|
125
|
+
portFromUrl = isArgument ? handleUrlArg(value) : null
|
|
126
|
+
|
|
127
|
+
if (host && portFromUrl) {
|
|
128
|
+
throw new InvalidArgumentError('You must not set a URL as port argument if the host option is already set')
|
|
129
|
+
} else if (port) {
|
|
130
|
+
if (portFromUrl) {
|
|
131
|
+
throw new InvalidArgumentError('You must not set a port option if the port argument is a URL')
|
|
132
|
+
}
|
|
133
|
+
throw new InvalidArgumentError('You must not set a port option if the port argument is set')
|
|
134
|
+
} else if (url) {
|
|
135
|
+
if (port) {
|
|
136
|
+
throw new InvalidArgumentError('You must not set a port option if the port argument is a URL')
|
|
137
|
+
}
|
|
138
|
+
throw new InvalidArgumentError('Unexpected: You must not set a port argument')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
value = portFromUrl ?? value
|
|
142
|
+
} else {
|
|
143
|
+
value = valueOrSharedArg
|
|
144
|
+
valueOrSharedArg = undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
value = checkInt(value, 'Port must be a valid integer')
|
|
148
|
+
|
|
149
|
+
if (value > 65535) {
|
|
150
|
+
throw new InvalidArgumentError('Port exceeds maximum value of 65535');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isSharedArg(valueOrSharedArg)) {
|
|
154
|
+
if (!portFromUrl) {
|
|
155
|
+
valueOrSharedArg.value.port = value
|
|
156
|
+
}
|
|
157
|
+
return valueOrSharedArg
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return value
|
|
161
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {stdout} from 'node:process';
|
|
2
|
+
import EventEmitter from "node:events";
|
|
3
|
+
import {spawn} from "child_process";
|
|
4
|
+
|
|
5
|
+
export const setCursorVisibility = (visible) => {
|
|
6
|
+
if (visible) {
|
|
7
|
+
stdout.write('\x1B[?25h');
|
|
8
|
+
} else {
|
|
9
|
+
stdout.write('\x1B[?25l');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const clearTerminal = () => {
|
|
14
|
+
stdout.write('\x1B[2J\x1B[0f');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Runs a child process and proxies its output.
|
|
20
|
+
* @param {string} pathToExecutable - Relative or absolute path to the executable script
|
|
21
|
+
* @returns {Promise<number|null>} - Exit code of the child process
|
|
22
|
+
*/
|
|
23
|
+
export const proxyChildProcess = (pathToExecutable) => {
|
|
24
|
+
|
|
25
|
+
const eventEmitter = new EventEmitter()
|
|
26
|
+
const createSpawnProcessPromise = () => {
|
|
27
|
+
const onError = (error) => {
|
|
28
|
+
eventEmitter.emit('close', 1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const onClose = (code) => {
|
|
32
|
+
if (code === null) {
|
|
33
|
+
clearTerminal()
|
|
34
|
+
}
|
|
35
|
+
eventEmitter.emit('close', code)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const onMessage = (message) => {
|
|
39
|
+
if (message === 'restart') {
|
|
40
|
+
child.off('close', onClose)
|
|
41
|
+
child.kill()
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
eventEmitter.emit('spawn')
|
|
44
|
+
}, 10)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const child = spawn('node', [pathToExecutable], {
|
|
49
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'] // Proxy standard output and error to the main process, enable IPC channel
|
|
50
|
+
})
|
|
51
|
+
.on('close', onClose)
|
|
52
|
+
.on('error', onError)
|
|
53
|
+
.on('message', onMessage)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
eventEmitter.on('spawn', () => createSpawnProcessPromise(pathToExecutable))
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
eventEmitter.once('close', resolve)
|
|
60
|
+
eventEmitter.emit('spawn')
|
|
61
|
+
})
|
|
62
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {Socket} from "socket.io-client";
|
|
2
|
+
import {IncomingRequest} from "#lib/Tunnel/IncomingRequest";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {IncomingSocketIoRequest} incomingSocketIoRequest
|
|
6
|
+
* @param {RequestId} requestId
|
|
7
|
+
* @param {Socket} socket
|
|
8
|
+
* @returns {IncomingRequest}
|
|
9
|
+
*/
|
|
10
|
+
export const createRequestFromSocketIoRequest = (incomingSocketIoRequest, requestId, socket) => {
|
|
11
|
+
return new IncomingRequest(incomingSocketIoRequest, requestId, socket)
|
|
12
|
+
}
|