creepercli 1.1.0 → 1.1.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.
- package/README.md +4 -4
- package/bin/creepercli.js +8 -9
- package/package.json +2 -2
- package/src/client.js +41 -41
- package/src/commands/index.js +8 -8
- package/src/commands/monitor.js +7 -7
- package/src/index.js +24 -24
- package/src/repl.js +18 -18
- package/src/session.js +1 -1
- package/src/transfer.js +149 -149
- package/src/update.js +7 -7
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# CreeperCLI
|
|
2
2
|
|
|
3
|
-
> Remote administration CLI for Paper/Spigot servers and BungeeCord/Waterfall or Velocity proxies.
|
|
3
|
+
> Remote administration CLI for Paper/Spigot servers and BungeeCord/Waterfall or Velocity proxies.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/creepercli)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://discord.com/invite/zCkE44hsBR)
|
|
8
8
|
|
|
9
|
-
`creepercli` is the client half of CreeperCLI. It talks to the CreeperCLI server/proxy plugin over a TCP socket and gives you a remote shell: sandboxed filesystem access, console commands through an allowlist, live logs, verified transfers, and monitoring.
|
|
9
|
+
`creepercli` is the client half of CreeperCLI. It talks to the CreeperCLI server/proxy plugin over a TCP socket and gives you a remote shell: sandboxed filesystem access, console commands through an allowlist, live logs, verified transfers, and monitoring.
|
|
10
10
|
|
|
11
11
|
The plugin is required. Install it on the server, then create a user from the server console:
|
|
12
12
|
|
|
@@ -70,7 +70,7 @@ One-shot mode works for scripts: `creepercli ls /plugins`, `creepercli exec list
|
|
|
70
70
|
- The server-side sandbox blocks `..`, symlink, and absolute-path escapes.
|
|
71
71
|
- Passwords are bcrypt-hashed (cost 12). Sessions expire after 15 idle minutes and bind to your IP.
|
|
72
72
|
- Only allowlisted console commands run (`list`, `say *`, `whitelist *`, `restart` by default).
|
|
73
|
-
- The plugin binds `127.0.0.1` by default. Connect through an SSH tunnel, VPN, or private network; do not expose the plaintext port directly to the internet.
|
|
73
|
+
- The plugin binds `127.0.0.1` by default. Connect through an SSH tunnel, VPN, or private network; do not expose the plaintext port directly to the internet.
|
|
74
74
|
|
|
75
75
|
The full threat model lives in the docs: [demonz-development.github.io/creepercli](https://demonz-development.github.io/creepercli/).
|
|
76
76
|
|
|
@@ -82,4 +82,4 @@ The full threat model lives in the docs: [demonz-development.github.io/creepercl
|
|
|
82
82
|
|
|
83
83
|
## License
|
|
84
84
|
|
|
85
|
-
Apache License 2.0. See [LICENSE](LICENSE).
|
|
85
|
+
Apache License 2.0. See [LICENSE](LICENSE).
|
package/bin/creepercli.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
4
3
|
'use strict';
|
|
5
4
|
|
|
6
5
|
const { main } = require('../src/index');
|
|
7
6
|
|
|
8
7
|
main(process.argv.slice(2)).then((code) => {
|
|
9
8
|
if (typeof code === 'number') process.exitCode = code;
|
|
10
|
-
}).catch((err) => {
|
|
11
|
-
if (!err || !err.creepercliReported) {
|
|
12
|
-
console.error(err && err.message ? err.message : String(err));
|
|
13
|
-
}
|
|
14
|
-
process.exitCode = 1;
|
|
15
|
-
});
|
|
9
|
+
}).catch((err) => {
|
|
10
|
+
if (!err || !err.creepercliReported) {
|
|
11
|
+
console.error(err && err.message ? err.message : String(err));
|
|
12
|
+
}
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "creepercli",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Remote administration CLI for Minecraft servers and proxies running CreeperCLI",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Remote administration CLI for Minecraft servers and proxies running CreeperCLI",
|
|
5
5
|
"author": "DemonZDevelopment",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"type": "commonjs",
|
package/src/client.js
CHANGED
|
@@ -6,9 +6,9 @@ const net = require('net');
|
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const { EventEmitter } = require('events');
|
|
8
8
|
const { StringDecoder } = require('string_decoder');
|
|
9
|
-
const { ProtocolError, ERR, PROTOCOL_VERSION } = require('./protocol');
|
|
10
|
-
|
|
11
|
-
const MAX_INCOMING_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
const { ProtocolError, ERR, PROTOCOL_VERSION } = require('./protocol');
|
|
10
|
+
|
|
11
|
+
const MAX_INCOMING_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
12
12
|
|
|
13
13
|
class CreeperClient extends EventEmitter {
|
|
14
14
|
constructor({ host, port, timeoutMs = 300000 }) {
|
|
@@ -29,47 +29,47 @@ class CreeperClient extends EventEmitter {
|
|
|
29
29
|
socket.setNoDelay(true);
|
|
30
30
|
socket.on('connect', () => resolve());
|
|
31
31
|
socket.on('error', (err) => reject(err));
|
|
32
|
-
socket.on('close', () => {
|
|
33
|
-
if (this.socket === socket) this.socket = null;
|
|
34
|
-
this._onClose();
|
|
35
|
-
});
|
|
32
|
+
socket.on('close', () => {
|
|
33
|
+
if (this.socket === socket) this.socket = null;
|
|
34
|
+
this._onClose();
|
|
35
|
+
});
|
|
36
36
|
socket.on('data', (chunk) => this._onData(chunk));
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
_onData(chunk) {
|
|
41
|
-
this.buffer += this._decoder.write(chunk);
|
|
42
|
-
if (Buffer.byteLength(this.buffer, 'utf8') > MAX_INCOMING_BUFFER_BYTES) {
|
|
43
|
-
this._failAll(new ProtocolError(ERR.PAYLOAD_TOO_LARGE, 'Server frame exceeded client buffer limit'));
|
|
44
|
-
this.close();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
40
|
+
_onData(chunk) {
|
|
41
|
+
this.buffer += this._decoder.write(chunk);
|
|
42
|
+
if (Buffer.byteLength(this.buffer, 'utf8') > MAX_INCOMING_BUFFER_BYTES) {
|
|
43
|
+
this._failAll(new ProtocolError(ERR.PAYLOAD_TOO_LARGE, 'Server frame exceeded client buffer limit'));
|
|
44
|
+
this.close();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
47
|
let idx;
|
|
48
48
|
while ((idx = this.buffer.indexOf('\n')) !== -1) {
|
|
49
49
|
const line = this.buffer.slice(0, idx).trim();
|
|
50
50
|
this.buffer = this.buffer.slice(idx + 1);
|
|
51
51
|
if (!line) continue;
|
|
52
52
|
this._handleLine(line);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
55
|
|
|
56
56
|
_handleLine(line) {
|
|
57
57
|
let frame;
|
|
58
58
|
try {
|
|
59
59
|
frame = JSON.parse(line);
|
|
60
|
-
} catch {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (frame.v !== PROTOCOL_VERSION) {
|
|
64
|
-
const entry = this.pending.get(frame.id);
|
|
65
|
-
if (entry) {
|
|
66
|
-
this.pending.delete(frame.id);
|
|
67
|
-
clearTimeout(entry.timer);
|
|
68
|
-
entry.reject(new ProtocolError(ERR.BAD_REQUEST,
|
|
69
|
-
`Unsupported protocol version ${frame.v} (expected ${PROTOCOL_VERSION})`));
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
60
|
+
} catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (frame.v !== PROTOCOL_VERSION) {
|
|
64
|
+
const entry = this.pending.get(frame.id);
|
|
65
|
+
if (entry) {
|
|
66
|
+
this.pending.delete(frame.id);
|
|
67
|
+
clearTimeout(entry.timer);
|
|
68
|
+
entry.reject(new ProtocolError(ERR.BAD_REQUEST,
|
|
69
|
+
`Unsupported protocol version ${frame.v} (expected ${PROTOCOL_VERSION})`));
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
73
|
if (frame.type === 'response') {
|
|
74
74
|
const entry = this.pending.get(frame.id);
|
|
75
75
|
if (!entry) return;
|
|
@@ -92,7 +92,7 @@ class CreeperClient extends EventEmitter {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
request(action, params = {}, { timeoutMs } = {}) {
|
|
95
|
-
if (!this.socket || this.socket.destroyed === true || this.socket.writable === false) {
|
|
95
|
+
if (!this.socket || this.socket.destroyed === true || this.socket.writable === false) {
|
|
96
96
|
return Promise.reject(new ProtocolError(ERR.INTERNAL, 'Connection closed'));
|
|
97
97
|
}
|
|
98
98
|
const id = crypto.randomUUID();
|
|
@@ -102,7 +102,7 @@ class CreeperClient extends EventEmitter {
|
|
|
102
102
|
reject(new ProtocolError(ERR.TIMEOUT, `Request timed out (${action})`));
|
|
103
103
|
}, timeoutMs || this.timeoutMs);
|
|
104
104
|
this.pending.set(id, { resolve, reject, timer });
|
|
105
|
-
const frame = JSON.stringify({ v: PROTOCOL_VERSION, type: 'request', id, action, params });
|
|
105
|
+
const frame = JSON.stringify({ v: PROTOCOL_VERSION, type: 'request', id, action, params });
|
|
106
106
|
try {
|
|
107
107
|
this.socket.write(frame + '\n');
|
|
108
108
|
} catch (err) {
|
|
@@ -113,16 +113,16 @@ class CreeperClient extends EventEmitter {
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
_onClose() {
|
|
117
|
-
this._failAll(new ProtocolError(ERR.INTERNAL, 'Connection closed by server'));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
_failAll(error) {
|
|
121
|
-
for (const { reject, timer } of this.pending.values()) {
|
|
122
|
-
clearTimeout(timer);
|
|
123
|
-
reject(error);
|
|
124
|
-
}
|
|
125
|
-
this.pending.clear();
|
|
116
|
+
_onClose() {
|
|
117
|
+
this._failAll(new ProtocolError(ERR.INTERNAL, 'Connection closed by server'));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_failAll(error) {
|
|
121
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
reject(error);
|
|
124
|
+
}
|
|
125
|
+
this.pending.clear();
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
close() {
|
package/src/commands/index.js
CHANGED
|
@@ -54,21 +54,21 @@ function find(name) {
|
|
|
54
54
|
return COMMANDS.find((c) => c.name === name);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function splitArgs(input) {
|
|
57
|
+
function splitArgs(input) {
|
|
58
58
|
const args = [];
|
|
59
59
|
let cur = '';
|
|
60
60
|
let quote = null;
|
|
61
61
|
let escaped = false;
|
|
62
|
-
for (let i = 0; i < input.length; i++) {
|
|
63
|
-
const ch = input[i];
|
|
62
|
+
for (let i = 0; i < input.length; i++) {
|
|
63
|
+
const ch = input[i];
|
|
64
64
|
if (escaped) {
|
|
65
65
|
cur += ch;
|
|
66
66
|
escaped = false;
|
|
67
|
-
} else if (ch === '\\') {
|
|
68
|
-
const next = input[i + 1];
|
|
69
|
-
const escapable = next != null && (next === '\\' || next === '"' || next === "'" || /\s/.test(next));
|
|
70
|
-
if (escapable) escaped = true;
|
|
71
|
-
else cur += ch;
|
|
67
|
+
} else if (ch === '\\') {
|
|
68
|
+
const next = input[i + 1];
|
|
69
|
+
const escapable = next != null && (next === '\\' || next === '"' || next === "'" || /\s/.test(next));
|
|
70
|
+
if (escapable) escaped = true;
|
|
71
|
+
else cur += ch;
|
|
72
72
|
} else if (quote) {
|
|
73
73
|
if (ch === quote) quote = null;
|
|
74
74
|
else cur += ch;
|
package/src/commands/monitor.js
CHANGED
|
@@ -29,18 +29,18 @@ async function log(ctx, args) {
|
|
|
29
29
|
const res = await ctx.client.request('monitor.log.start', { grep });
|
|
30
30
|
console.log(`Streaming console log (Ctrl+C to stop)${res.grep ? ' filtered by "' + res.grep + '"' : ''}`);
|
|
31
31
|
if (res.buffer && res.buffer.length) console.log(res.buffer.join('\n'));
|
|
32
|
-
let stop = null;
|
|
33
|
-
let stopped = false;
|
|
32
|
+
let stop = null;
|
|
33
|
+
let stopped = false;
|
|
34
34
|
try {
|
|
35
35
|
await new Promise((resolve) => {
|
|
36
36
|
const onLine = (event, data) => {
|
|
37
37
|
if (event === EVENT_LOG_LINE) console.log(data.message);
|
|
38
38
|
};
|
|
39
|
-
ctx.client.on('event', onLine);
|
|
40
|
-
stop = () => {
|
|
41
|
-
if (stopped) return;
|
|
42
|
-
stopped = true;
|
|
43
|
-
ctx.client.removeListener('event', onLine);
|
|
39
|
+
ctx.client.on('event', onLine);
|
|
40
|
+
stop = () => {
|
|
41
|
+
if (stopped) return;
|
|
42
|
+
stopped = true;
|
|
43
|
+
ctx.client.removeListener('event', onLine);
|
|
44
44
|
ctx.client.request('monitor.log.stop').catch(() => {});
|
|
45
45
|
resolve();
|
|
46
46
|
};
|
package/src/index.js
CHANGED
|
@@ -9,17 +9,17 @@ const { startRepl } = require('./repl');
|
|
|
9
9
|
const { runCommand } = require('./commands');
|
|
10
10
|
const { doLogin } = require('./commands/auth');
|
|
11
11
|
const { friendly } = require('./protocol');
|
|
12
|
-
const { checkUpdate } = require('./update');
|
|
13
|
-
const pkg = require('../package.json');
|
|
12
|
+
const { checkUpdate } = require('./update');
|
|
13
|
+
const pkg = require('../package.json');
|
|
14
14
|
|
|
15
|
-
async function main(argv) {
|
|
16
|
-
const flags = parseFlags(argv);
|
|
17
|
-
if (flags.help) {
|
|
18
|
-
printUsage();
|
|
19
|
-
return 0;
|
|
20
|
-
}
|
|
21
|
-
checkUpdate();
|
|
22
|
-
const cfg = applyFlags(loadConfig(), flags);
|
|
15
|
+
async function main(argv) {
|
|
16
|
+
const flags = parseFlags(argv);
|
|
17
|
+
if (flags.help) {
|
|
18
|
+
printUsage();
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
checkUpdate();
|
|
22
|
+
const cfg = applyFlags(loadConfig(), flags);
|
|
23
23
|
const args = flags.args;
|
|
24
24
|
const cmd = args[0];
|
|
25
25
|
|
|
@@ -33,10 +33,10 @@ async function main(argv) {
|
|
|
33
33
|
startRepl(ctx);
|
|
34
34
|
return 0;
|
|
35
35
|
}
|
|
36
|
-
} catch (err) {
|
|
37
|
-
console.error(`\nError: ${friendly(err)}\n`);
|
|
38
|
-
client.close();
|
|
39
|
-
return 1;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(`\nError: ${friendly(err)}\n`);
|
|
38
|
+
client.close();
|
|
39
|
+
return 1;
|
|
40
40
|
} finally {
|
|
41
41
|
if (!ctx.exit && !process.stdout.isTTY) {
|
|
42
42
|
client.close();
|
|
@@ -66,10 +66,10 @@ async function main(argv) {
|
|
|
66
66
|
if (creds) {
|
|
67
67
|
ctx.username = creds.username;
|
|
68
68
|
try {
|
|
69
|
-
const resume = await client.request('auth.resume', { token: creds.token }, { timeoutMs: 15000 });
|
|
70
|
-
ctx.username = resume.username;
|
|
71
|
-
ctx.cwd = resume.cwd || '/';
|
|
72
|
-
session.save({ ...creds, expiresAt: Date.now() + (resume.expiresInSeconds || 0) * 1000 });
|
|
69
|
+
const resume = await client.request('auth.resume', { token: creds.token }, { timeoutMs: 15000 });
|
|
70
|
+
ctx.username = resume.username;
|
|
71
|
+
ctx.cwd = resume.cwd || '/';
|
|
72
|
+
session.save({ ...creds, expiresAt: Date.now() + (resume.expiresInSeconds || 0) * 1000 });
|
|
73
73
|
} catch (err) {
|
|
74
74
|
if (err.code === 'E_SESSION_EXPIRED' || err.code === 'E_UNAUTHORIZED') {
|
|
75
75
|
session.clear();
|
|
@@ -134,7 +134,7 @@ async function connect(cfg) {
|
|
|
134
134
|
console.error(`CREEPER CLI SETUP & TROUBLESHOOTING GUIDE`);
|
|
135
135
|
console.error(`--------------------------------------------------------------------------------\n`);
|
|
136
136
|
console.error(`1. INSTALL THE PLUGIN ON YOUR MINECRAFT SERVER:`);
|
|
137
|
-
console.error(` * Place CreeperCLI-${pkg.version}.jar inside your server's 'plugins/' folder.`);
|
|
137
|
+
console.error(` * Place CreeperCLI-${pkg.version}.jar inside your server's 'plugins/' folder.`);
|
|
138
138
|
console.error(` * Start or restart your server (Paper / Spigot 1.21+).\n`);
|
|
139
139
|
console.error(`2. CREATE YOUR ADMIN USER IN SERVER CONSOLE:`);
|
|
140
140
|
console.error(` * Open your server console (or run in-game as OP):`);
|
|
@@ -147,13 +147,13 @@ async function connect(cfg) {
|
|
|
147
147
|
console.error(` ssh -L ${cfg.port}:127.0.0.1:${cfg.port} user@<your-server-ip>\n`);
|
|
148
148
|
console.error(`--------------------------------------------------------------------------------`);
|
|
149
149
|
console.error(`Documentation: https://github.com/DemonZ-Development/creepercli\n`);
|
|
150
|
-
err.creepercliReported = true;
|
|
151
|
-
throw err;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
150
|
+
err.creepercliReported = true;
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
154
|
|
|
155
155
|
function printUsage() {
|
|
156
|
-
console.log(`CreeperCLI v${pkg.version} — remote Minecraft administration
|
|
156
|
+
console.log(`CreeperCLI v${pkg.version} — remote Minecraft administration
|
|
157
157
|
|
|
158
158
|
Usage:
|
|
159
159
|
creepercli login Authenticate with the server
|
package/src/repl.js
CHANGED
|
@@ -10,9 +10,9 @@ function prompt(ctx) {
|
|
|
10
10
|
return `${ctx.username || '?'}@${ctx.cfg.host}:${ctx.cwd || '/'}> `;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function startRepl(ctx, io = {}) {
|
|
14
|
-
const input = io.input || process.stdin;
|
|
15
|
-
const output = io.output || process.stdout;
|
|
13
|
+
function startRepl(ctx, io = {}) {
|
|
14
|
+
const input = io.input || process.stdin;
|
|
15
|
+
const output = io.output || process.stdout;
|
|
16
16
|
const completions = COMMANDS.map((c) => c.name);
|
|
17
17
|
|
|
18
18
|
function completer(line) {
|
|
@@ -22,9 +22,9 @@ function startRepl(ctx, io = {}) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const rl = readline.createInterface({
|
|
25
|
-
input,
|
|
26
|
-
output,
|
|
27
|
-
terminal: Boolean(output.isTTY),
|
|
25
|
+
input,
|
|
26
|
+
output,
|
|
27
|
+
terminal: Boolean(output.isTTY),
|
|
28
28
|
historySize: 1000,
|
|
29
29
|
completer,
|
|
30
30
|
});
|
|
@@ -47,11 +47,11 @@ function startRepl(ctx, io = {}) {
|
|
|
47
47
|
running = true;
|
|
48
48
|
while (queue.length) {
|
|
49
49
|
const line = queue.shift();
|
|
50
|
-
await runCommand(ctx, line);
|
|
51
|
-
if (ctx.exit) {
|
|
52
|
-
queue.length = 0;
|
|
53
|
-
rl.close();
|
|
54
|
-
return;
|
|
50
|
+
await runCommand(ctx, line);
|
|
51
|
+
if (ctx.exit) {
|
|
52
|
+
queue.length = 0;
|
|
53
|
+
rl.close();
|
|
54
|
+
return;
|
|
55
55
|
}
|
|
56
56
|
rl.setPrompt(prompt(ctx));
|
|
57
57
|
rl.prompt();
|
|
@@ -83,12 +83,12 @@ function startRepl(ctx, io = {}) {
|
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
rl.on('close', async () => {
|
|
87
|
-
await ctx.client.flush();
|
|
88
|
-
ctx.client.close();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
return rl;
|
|
92
|
-
}
|
|
86
|
+
rl.on('close', async () => {
|
|
87
|
+
await ctx.client.flush();
|
|
88
|
+
ctx.client.close();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return rl;
|
|
92
|
+
}
|
|
93
93
|
|
|
94
94
|
module.exports = { startRepl, prompt };
|
package/src/session.js
CHANGED
package/src/transfer.js
CHANGED
|
@@ -4,28 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const crypto = require('crypto');
|
|
8
|
-
const { confirm } = require('./prompts');
|
|
9
|
-
|
|
10
|
-
const MAX_TRANSFER_CHUNK_BYTES = 1024 * 1024;
|
|
11
|
-
|
|
12
|
-
function validateTransferStart(start, expectFileMetadata = false) {
|
|
13
|
-
if (!start || typeof start.transferId !== 'string' || !start.transferId) {
|
|
14
|
-
throw new Error('Server returned an invalid transfer id');
|
|
15
|
-
}
|
|
16
|
-
if (!Number.isInteger(start.chunkSize) || start.chunkSize < 1024
|
|
17
|
-
|| start.chunkSize > MAX_TRANSFER_CHUNK_BYTES) {
|
|
18
|
-
throw new Error('Server returned an invalid transfer chunk size');
|
|
19
|
-
}
|
|
20
|
-
if (expectFileMetadata) {
|
|
21
|
-
if (!Number.isSafeInteger(start.size) || start.size < 0) {
|
|
22
|
-
throw new Error('Server returned an invalid file size');
|
|
23
|
-
}
|
|
24
|
-
if (typeof start.sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(start.sha256)) {
|
|
25
|
-
throw new Error('Server returned an invalid file checksum');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { confirm } = require('./prompts');
|
|
9
|
+
|
|
10
|
+
const MAX_TRANSFER_CHUNK_BYTES = 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
function validateTransferStart(start, expectFileMetadata = false) {
|
|
13
|
+
if (!start || typeof start.transferId !== 'string' || !start.transferId) {
|
|
14
|
+
throw new Error('Server returned an invalid transfer id');
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isInteger(start.chunkSize) || start.chunkSize < 1024
|
|
17
|
+
|| start.chunkSize > MAX_TRANSFER_CHUNK_BYTES) {
|
|
18
|
+
throw new Error('Server returned an invalid transfer chunk size');
|
|
19
|
+
}
|
|
20
|
+
if (expectFileMetadata) {
|
|
21
|
+
if (!Number.isSafeInteger(start.size) || start.size < 0) {
|
|
22
|
+
throw new Error('Server returned an invalid file size');
|
|
23
|
+
}
|
|
24
|
+
if (typeof start.sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(start.sha256)) {
|
|
25
|
+
throw new Error('Server returned an invalid file checksum');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
29
|
|
|
30
30
|
function sha256File(file) {
|
|
31
31
|
return new Promise((resolve, reject) => {
|
|
@@ -49,7 +49,7 @@ function clearProgress() {
|
|
|
49
49
|
process.stdout.write('\r'.padEnd(70) + '\r');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async function cpush(ctx, args) {
|
|
52
|
+
async function cpush(ctx, args) {
|
|
53
53
|
const rest = args.filter((a) => !a.startsWith('--'));
|
|
54
54
|
let [local, remote] = rest;
|
|
55
55
|
if (!local || !remote) throw new Error('Usage: cpush <local-file> <remote-path>');
|
|
@@ -57,20 +57,20 @@ async function cpush(ctx, args) {
|
|
|
57
57
|
const stat = fs.statSync(local);
|
|
58
58
|
if (!stat.isFile()) throw new Error('Local path must be a file');
|
|
59
59
|
const sha = await sha256File(local);
|
|
60
|
-
const start = await ctx.client.request('xfer.push.start', {
|
|
60
|
+
const start = await ctx.client.request('xfer.push.start', {
|
|
61
61
|
path: remote,
|
|
62
62
|
size: stat.size,
|
|
63
63
|
sha256: sha,
|
|
64
|
-
force: args.includes('--force'),
|
|
65
|
-
}, { timeoutMs: 60000 });
|
|
66
|
-
const { transferId, chunkSize } = start;
|
|
67
|
-
let fd = null;
|
|
68
|
-
let transferred = 0;
|
|
69
|
-
let index = 0;
|
|
70
|
-
try {
|
|
71
|
-
validateTransferStart(start);
|
|
72
|
-
fd = fs.openSync(local, 'r');
|
|
73
|
-
const buf = Buffer.alloc(chunkSize);
|
|
64
|
+
force: args.includes('--force'),
|
|
65
|
+
}, { timeoutMs: 60000 });
|
|
66
|
+
const { transferId, chunkSize } = start;
|
|
67
|
+
let fd = null;
|
|
68
|
+
let transferred = 0;
|
|
69
|
+
let index = 0;
|
|
70
|
+
try {
|
|
71
|
+
validateTransferStart(start);
|
|
72
|
+
fd = fs.openSync(local, 'r');
|
|
73
|
+
const buf = Buffer.alloc(chunkSize);
|
|
74
74
|
while (true) {
|
|
75
75
|
const n = fs.readSync(fd, buf, 0, chunkSize, transferred);
|
|
76
76
|
if (n === 0) break;
|
|
@@ -81,99 +81,99 @@ async function cpush(ctx, args) {
|
|
|
81
81
|
}, { timeoutMs: 120000 });
|
|
82
82
|
transferred += n;
|
|
83
83
|
index++;
|
|
84
|
-
showProgress(transferred, stat.size);
|
|
85
|
-
}
|
|
86
|
-
const finish = await ctx.client.request('xfer.push.finish', { transferId, sha256: sha });
|
|
87
|
-
if (!finish || typeof finish.sha256 !== 'string' || finish.sha256.toLowerCase() !== sha) {
|
|
88
|
-
throw new Error('Server reported an unexpected upload checksum');
|
|
89
|
-
}
|
|
90
|
-
clearProgress();
|
|
91
|
-
console.log(`Pushed ${local} -> ${finish.target} (${finish.size} bytes, sha256 ${finish.sha256})`);
|
|
92
|
-
} catch (err) {
|
|
93
|
-
await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
|
|
94
|
-
clearProgress();
|
|
95
|
-
throw err;
|
|
96
|
-
} finally {
|
|
97
|
-
if (fd !== null) fs.closeSync(fd);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
84
|
+
showProgress(transferred, stat.size);
|
|
85
|
+
}
|
|
86
|
+
const finish = await ctx.client.request('xfer.push.finish', { transferId, sha256: sha });
|
|
87
|
+
if (!finish || typeof finish.sha256 !== 'string' || finish.sha256.toLowerCase() !== sha) {
|
|
88
|
+
throw new Error('Server reported an unexpected upload checksum');
|
|
89
|
+
}
|
|
90
|
+
clearProgress();
|
|
91
|
+
console.log(`Pushed ${local} -> ${finish.target} (${finish.size} bytes, sha256 ${finish.sha256})`);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
|
|
94
|
+
clearProgress();
|
|
95
|
+
throw err;
|
|
96
|
+
} finally {
|
|
97
|
+
if (fd !== null) fs.closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
100
|
|
|
101
101
|
async function cpull(ctx, args) {
|
|
102
102
|
let [remote, local] = args.filter((a) => !a.startsWith('--'));
|
|
103
103
|
if (!remote || !local) throw new Error('Usage: cpull <remote-path> <local-file>');
|
|
104
|
-
remote = remote.replace(/\\/g, '/');
|
|
105
|
-
const start = await ctx.client.request('xfer.pull.start', { path: remote });
|
|
106
|
-
const { transferId, size, sha256: expectedSha, chunkSize } = start;
|
|
107
|
-
const part = `${local}.creepercli-part-${process.pid}-${Date.now()}`;
|
|
108
|
-
const hash = crypto.createHash('sha256');
|
|
109
|
-
let fd = null;
|
|
110
|
-
let transferred = 0;
|
|
111
|
-
let index = 0;
|
|
112
|
-
try {
|
|
113
|
-
validateTransferStart(start, true);
|
|
114
|
-
const parent = path.dirname(path.resolve(local));
|
|
115
|
-
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
116
|
-
fd = fs.openSync(part, 'w');
|
|
117
|
-
while (true) {
|
|
104
|
+
remote = remote.replace(/\\/g, '/');
|
|
105
|
+
const start = await ctx.client.request('xfer.pull.start', { path: remote });
|
|
106
|
+
const { transferId, size, sha256: expectedSha, chunkSize } = start;
|
|
107
|
+
const part = `${local}.creepercli-part-${process.pid}-${Date.now()}`;
|
|
108
|
+
const hash = crypto.createHash('sha256');
|
|
109
|
+
let fd = null;
|
|
110
|
+
let transferred = 0;
|
|
111
|
+
let index = 0;
|
|
112
|
+
try {
|
|
113
|
+
validateTransferStart(start, true);
|
|
114
|
+
const parent = path.dirname(path.resolve(local));
|
|
115
|
+
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
116
|
+
fd = fs.openSync(part, 'w');
|
|
117
|
+
while (true) {
|
|
118
118
|
const res = await ctx.client.request('xfer.pull.chunk', { transferId, index }, { timeoutMs: 120000 });
|
|
119
|
-
const data = Buffer.from(res.data || '', 'base64');
|
|
120
|
-
if (data.length === 0) break;
|
|
121
|
-
if (data.length > chunkSize || transferred + data.length > size) {
|
|
122
|
-
throw new Error('Server sent an invalid transfer chunk');
|
|
123
|
-
}
|
|
119
|
+
const data = Buffer.from(res.data || '', 'base64');
|
|
120
|
+
if (data.length === 0) break;
|
|
121
|
+
if (data.length > chunkSize || transferred + data.length > size) {
|
|
122
|
+
throw new Error('Server sent an invalid transfer chunk');
|
|
123
|
+
}
|
|
124
124
|
fs.writeSync(fd, data, 0, data.length);
|
|
125
125
|
hash.update(data);
|
|
126
126
|
transferred += data.length;
|
|
127
|
-
index++;
|
|
128
|
-
showProgress(transferred, size);
|
|
129
|
-
}
|
|
130
|
-
fs.closeSync(fd);
|
|
131
|
-
fd = null;
|
|
132
|
-
if (transferred !== size) {
|
|
133
|
-
throw new Error(`Transfer ended early: expected ${size} bytes, received ${transferred}`);
|
|
134
|
-
}
|
|
135
|
-
const actual = await ctx.client.request('xfer.pull.finish', { transferId, sha256: hash.digest('hex') });
|
|
136
|
-
clearProgress();
|
|
137
|
-
if (!actual || typeof actual.sha256 !== 'string'
|
|
138
|
-
|| actual.sha256.toLowerCase() !== expectedSha.toLowerCase()) {
|
|
139
|
-
throw new Error('Checksum mismatch: transfer aborted');
|
|
140
|
-
}
|
|
141
|
-
replaceFile(part, local);
|
|
142
|
-
console.log(`Pulled ${remote} -> ${local} (${transferred} bytes, sha256 ${actual.sha256})`);
|
|
143
|
-
} catch (err) {
|
|
144
|
-
if (fd !== null) {
|
|
145
|
-
fs.closeSync(fd);
|
|
146
|
-
fd = null;
|
|
147
|
-
}
|
|
148
|
-
await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
|
|
149
|
-
try { fs.unlinkSync(part); } catch {}
|
|
150
|
-
clearProgress();
|
|
151
|
-
throw err;
|
|
152
|
-
} finally {
|
|
153
|
-
if (fd !== null) fs.closeSync(fd);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
127
|
+
index++;
|
|
128
|
+
showProgress(transferred, size);
|
|
129
|
+
}
|
|
130
|
+
fs.closeSync(fd);
|
|
131
|
+
fd = null;
|
|
132
|
+
if (transferred !== size) {
|
|
133
|
+
throw new Error(`Transfer ended early: expected ${size} bytes, received ${transferred}`);
|
|
134
|
+
}
|
|
135
|
+
const actual = await ctx.client.request('xfer.pull.finish', { transferId, sha256: hash.digest('hex') });
|
|
136
|
+
clearProgress();
|
|
137
|
+
if (!actual || typeof actual.sha256 !== 'string'
|
|
138
|
+
|| actual.sha256.toLowerCase() !== expectedSha.toLowerCase()) {
|
|
139
|
+
throw new Error('Checksum mismatch: transfer aborted');
|
|
140
|
+
}
|
|
141
|
+
replaceFile(part, local);
|
|
142
|
+
console.log(`Pulled ${remote} -> ${local} (${transferred} bytes, sha256 ${actual.sha256})`);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
if (fd !== null) {
|
|
145
|
+
fs.closeSync(fd);
|
|
146
|
+
fd = null;
|
|
147
|
+
}
|
|
148
|
+
await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
|
|
149
|
+
try { fs.unlinkSync(part); } catch {}
|
|
150
|
+
clearProgress();
|
|
151
|
+
throw err;
|
|
152
|
+
} finally {
|
|
153
|
+
if (fd !== null) fs.closeSync(fd);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
156
|
|
|
157
157
|
async function csync(ctx, args) {
|
|
158
158
|
let [remoteDir, localDir] = args.filter((a) => a !== '-y' && !a.startsWith('--'));
|
|
159
159
|
if (!remoteDir || !localDir) throw new Error('Usage: csync <remote-dir> <local-dir> [--yes]');
|
|
160
160
|
remoteDir = remoteDir.replace(/\\/g, '/');
|
|
161
|
-
const yes = args.includes('--yes') || args.includes('-y') || !!(ctx.flags && ctx.flags.yes);
|
|
162
|
-
await ctx.client.request('fs.mkdir', { path: remoteDir, parents: true }).catch(() => {});
|
|
163
|
-
|
|
164
|
-
const remoteInfo = await ctx.client.request('fs.info', { path: remoteDir });
|
|
165
|
-
if (!remoteInfo.isDir || typeof remoteInfo.path !== 'string' || !remoteInfo.path.startsWith('/')) {
|
|
166
|
-
throw new Error('Server returned an invalid sync directory');
|
|
167
|
-
}
|
|
168
|
-
remoteDir = remoteInfo.path;
|
|
169
|
-
|
|
170
|
-
const remote = await ctx.client.request('xfer.list', { path: remoteDir }, { timeoutMs: 600000 });
|
|
171
|
-
const remoteMap = new Map();
|
|
172
|
-
for (const e of remote.entries) {
|
|
173
|
-
if (!e.isDir) {
|
|
174
|
-
const rel = relativeRemotePath(remoteDir, e.path);
|
|
175
|
-
if (rel) remoteMap.set(rel, e);
|
|
176
|
-
}
|
|
161
|
+
const yes = args.includes('--yes') || args.includes('-y') || !!(ctx.flags && ctx.flags.yes);
|
|
162
|
+
await ctx.client.request('fs.mkdir', { path: remoteDir, parents: true }).catch(() => {});
|
|
163
|
+
|
|
164
|
+
const remoteInfo = await ctx.client.request('fs.info', { path: remoteDir });
|
|
165
|
+
if (!remoteInfo.isDir || typeof remoteInfo.path !== 'string' || !remoteInfo.path.startsWith('/')) {
|
|
166
|
+
throw new Error('Server returned an invalid sync directory');
|
|
167
|
+
}
|
|
168
|
+
remoteDir = remoteInfo.path;
|
|
169
|
+
|
|
170
|
+
const remote = await ctx.client.request('xfer.list', { path: remoteDir }, { timeoutMs: 600000 });
|
|
171
|
+
const remoteMap = new Map();
|
|
172
|
+
for (const e of remote.entries) {
|
|
173
|
+
if (!e.isDir) {
|
|
174
|
+
const rel = relativeRemotePath(remoteDir, e.path);
|
|
175
|
+
if (rel) remoteMap.set(rel, e);
|
|
176
|
+
}
|
|
177
177
|
}
|
|
178
178
|
if (remote.truncated) console.error('(remote listing truncated)');
|
|
179
179
|
|
|
@@ -227,38 +227,38 @@ async function csync(ctx, args) {
|
|
|
227
227
|
console.log('Sync complete.');
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
function joinRemote(base, rel) {
|
|
230
|
+
function joinRemote(base, rel) {
|
|
231
231
|
const cleanBase = base.replace(/\\/g, '/');
|
|
232
232
|
const cleanRel = rel.replace(/\\/g, '/');
|
|
233
233
|
return (cleanBase.endsWith('/') ? cleanBase : cleanBase + '/') + cleanRel;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function relativeRemotePath(base, absolutePath) {
|
|
237
|
-
const cleanBase = ('/' + base.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')).replace(/^\/$/, '');
|
|
238
|
-
const cleanPath = '/' + absolutePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
239
|
-
if (!cleanBase) return cleanPath.replace(/^\//, '');
|
|
240
|
-
if (cleanPath === cleanBase) return '';
|
|
241
|
-
if (!cleanPath.startsWith(cleanBase + '/')) {
|
|
242
|
-
throw new Error(`Server returned path outside sync root: ${absolutePath}`);
|
|
243
|
-
}
|
|
244
|
-
return cleanPath.slice(cleanBase.length + 1);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function replaceFile(part, destination) {
|
|
248
|
-
if (!fs.existsSync(destination)) {
|
|
249
|
-
fs.renameSync(part, destination);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const backup = `${destination}.creepercli-backup-${process.pid}-${Date.now()}`;
|
|
253
|
-
fs.renameSync(destination, backup);
|
|
254
|
-
try {
|
|
255
|
-
fs.renameSync(part, destination);
|
|
256
|
-
} catch (err) {
|
|
257
|
-
try { fs.renameSync(backup, destination); } catch {}
|
|
258
|
-
throw err;
|
|
259
|
-
}
|
|
260
|
-
try { fs.rmSync(backup, { force: true }); } catch {}
|
|
261
|
-
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function relativeRemotePath(base, absolutePath) {
|
|
237
|
+
const cleanBase = ('/' + base.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')).replace(/^\/$/, '');
|
|
238
|
+
const cleanPath = '/' + absolutePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
239
|
+
if (!cleanBase) return cleanPath.replace(/^\//, '');
|
|
240
|
+
if (cleanPath === cleanBase) return '';
|
|
241
|
+
if (!cleanPath.startsWith(cleanBase + '/')) {
|
|
242
|
+
throw new Error(`Server returned path outside sync root: ${absolutePath}`);
|
|
243
|
+
}
|
|
244
|
+
return cleanPath.slice(cleanBase.length + 1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function replaceFile(part, destination) {
|
|
248
|
+
if (!fs.existsSync(destination)) {
|
|
249
|
+
fs.renameSync(part, destination);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const backup = `${destination}.creepercli-backup-${process.pid}-${Date.now()}`;
|
|
253
|
+
fs.renameSync(destination, backup);
|
|
254
|
+
try {
|
|
255
|
+
fs.renameSync(part, destination);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
try { fs.renameSync(backup, destination); } catch {}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
try { fs.rmSync(backup, { force: true }); } catch {}
|
|
261
|
+
}
|
|
262
262
|
|
|
263
263
|
function walkLocal(dir) {
|
|
264
264
|
const map = new Map();
|
|
@@ -273,13 +273,13 @@ function walkLocal(dir) {
|
|
|
273
273
|
const p = path.join(d, name);
|
|
274
274
|
let st;
|
|
275
275
|
try {
|
|
276
|
-
st = fs.lstatSync(p);
|
|
276
|
+
st = fs.lstatSync(p);
|
|
277
277
|
} catch {
|
|
278
278
|
continue;
|
|
279
279
|
}
|
|
280
280
|
const rel = ((prefix ? prefix + '/' : '') + name).replace(/\\/g, '/');
|
|
281
|
-
if (st.isSymbolicLink()) continue;
|
|
282
|
-
if (st.isDirectory()) walk(p, rel);
|
|
281
|
+
if (st.isSymbolicLink()) continue;
|
|
282
|
+
if (st.isDirectory()) walk(p, rel);
|
|
283
283
|
else if (st.isFile()) map.set(rel, { size: st.size, mtime: Math.floor(st.mtimeMs) });
|
|
284
284
|
}
|
|
285
285
|
}
|
|
@@ -287,6 +287,6 @@ function walkLocal(dir) {
|
|
|
287
287
|
return map;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
module.exports = {
|
|
291
|
-
cpush, cpull, csync, joinRemote, relativeRemotePath, replaceFile, walkLocal, validateTransferStart,
|
|
292
|
-
};
|
|
290
|
+
module.exports = {
|
|
291
|
+
cpush, cpull, csync, joinRemote, relativeRemotePath, replaceFile, walkLocal, validateTransferStart,
|
|
292
|
+
};
|
package/src/update.js
CHANGED
|
@@ -22,14 +22,14 @@ function checkUpdate() {
|
|
|
22
22
|
|
|
23
23
|
if (cache.lastCheck && now - cache.lastCheck < CHECK_INTERVAL_MS) {
|
|
24
24
|
if (cache.latest && isNewerVersion(pkg.version, cache.latest)) {
|
|
25
|
-
console.log(`[Notice] A new version of creepercli (v${cache.latest}) is available! Run "npm install -g creepercli" to update.\n`);
|
|
25
|
+
console.log(`[Notice] A new version of creepercli (v${cache.latest}) is available! Run "npm install -g creepercli" to update.\n`);
|
|
26
26
|
}
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const req = https.get('https://registry.npmjs.org/creepercli/latest', {
|
|
30
|
+
const req = https.get('https://registry.npmjs.org/creepercli/latest', {
|
|
31
31
|
timeout: 2500,
|
|
32
|
-
headers: { 'User-Agent': `creepercli/${pkg.version}` }
|
|
32
|
+
headers: { 'User-Agent': `creepercli/${pkg.version}` }
|
|
33
33
|
}, (res) => {
|
|
34
34
|
if (res.statusCode !== 200) return;
|
|
35
35
|
let body = '';
|
|
@@ -42,7 +42,7 @@ function checkUpdate() {
|
|
|
42
42
|
fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
|
|
43
43
|
fs.writeFileSync(UPDATE_FILE, JSON.stringify({ lastCheck: now, latest }), { mode: 0o600 });
|
|
44
44
|
if (isNewerVersion(pkg.version, latest)) {
|
|
45
|
-
console.log(`[Notice] A new version of creepercli (v${latest}) is available! Run "npm install -g creepercli" to update.\n`);
|
|
45
|
+
console.log(`[Notice] A new version of creepercli (v${latest}) is available! Run "npm install -g creepercli" to update.\n`);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
} catch {
|
|
@@ -50,9 +50,9 @@ function checkUpdate() {
|
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
req.on('error', () => {});
|
|
54
|
-
req.on('socket', (socket) => socket.unref());
|
|
55
|
-
} catch {
|
|
53
|
+
req.on('error', () => {});
|
|
54
|
+
req.on('socket', (socket) => socket.unref());
|
|
55
|
+
} catch {
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|