dsh-ssh-workbench 0.0.0-stage → 1.7.0
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/CHANGELOG.md +552 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/README.md +178 -2
- package/SECURITY.md +27 -0
- package/cordis.patch.yml +5 -0
- package/docs/architecture.md +146 -0
- package/docs/execution-model.md +83 -0
- package/docs/images/chat-at-resource.png +0 -0
- package/docs/images/plugin.png +0 -0
- package/docs/images/remote-workbench.png +0 -0
- package/docs/images/workbench.png +0 -0
- package/docs/quickstart.md +106 -0
- package/lib/client.js +4991 -0
- package/lib/core/activity.js +88 -0
- package/lib/core/background.js +69 -0
- package/lib/core/broker.js +802 -0
- package/lib/core/dialect.js +28 -0
- package/lib/core/errors.js +138 -0
- package/lib/core/execution-stats.js +88 -0
- package/lib/core/gate-pwsh.js +88 -0
- package/lib/core/gate.js +572 -0
- package/lib/core/guard.js +123 -0
- package/lib/core/known-hosts.js +74 -0
- package/lib/core/live-executions.js +101 -0
- package/lib/core/local-file-api.js +174 -0
- package/lib/core/localfs.js +381 -0
- package/lib/core/managed-context.js +276 -0
- package/lib/core/mentions.js +216 -0
- package/lib/core/paths.js +104 -0
- package/lib/core/posix-quote.js +4 -0
- package/lib/core/posix-shell.js +89 -0
- package/lib/core/prefs.js +38 -0
- package/lib/core/remote-exec-tool.js +394 -0
- package/lib/core/remote-file-tools.js +559 -0
- package/lib/core/remote-shell-tool.js +310 -0
- package/lib/core/secrets.js +95 -0
- package/lib/core/server-ref.js +61 -0
- package/lib/core/shell-adapters.js +48 -0
- package/lib/core/ssh.js +761 -0
- package/lib/core/store.js +83 -0
- package/lib/core/terminals.js +299 -0
- package/lib/core/win32-adapter.js +141 -0
- package/lib/index.js +409 -0
- package/lib/routes.js +759 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +89 -4
- package/vendor/NOTICE.md +50 -0
- package/vendor/XTERM-LICENSE +21 -0
- package/vendor/node_modules/asn1/LICENSE +19 -0
- package/vendor/node_modules/asn1/README.md +50 -0
- package/vendor/node_modules/asn1/lib/ber/errors.js +13 -0
- package/vendor/node_modules/asn1/lib/ber/index.js +27 -0
- package/vendor/node_modules/asn1/lib/ber/reader.js +262 -0
- package/vendor/node_modules/asn1/lib/ber/types.js +36 -0
- package/vendor/node_modules/asn1/lib/ber/writer.js +317 -0
- package/vendor/node_modules/asn1/lib/index.js +20 -0
- package/vendor/node_modules/asn1/package.json +31 -0
- package/vendor/node_modules/bcrypt-pbkdf/CONTRIBUTING.md +13 -0
- package/vendor/node_modules/bcrypt-pbkdf/LICENSE +66 -0
- package/vendor/node_modules/bcrypt-pbkdf/README.md +45 -0
- package/vendor/node_modules/bcrypt-pbkdf/index.js +556 -0
- package/vendor/node_modules/bcrypt-pbkdf/package.json +15 -0
- package/vendor/node_modules/safer-buffer/LICENSE +21 -0
- package/vendor/node_modules/safer-buffer/Readme.md +156 -0
- package/vendor/node_modules/safer-buffer/dangerous.js +58 -0
- package/vendor/node_modules/safer-buffer/package.json +34 -0
- package/vendor/node_modules/safer-buffer/safer.js +77 -0
- package/vendor/node_modules/tweetnacl/AUTHORS.md +28 -0
- package/vendor/node_modules/tweetnacl/LICENSE +24 -0
- package/vendor/node_modules/tweetnacl/README.md +459 -0
- package/vendor/node_modules/tweetnacl/nacl-fast.js +2388 -0
- package/vendor/node_modules/tweetnacl/nacl.js +1175 -0
- package/vendor/node_modules/tweetnacl/package.json +58 -0
- package/vendor/ssh2/LICENSE +19 -0
- package/vendor/ssh2/lib/Channel.js +295 -0
- package/vendor/ssh2/lib/agent.js +1123 -0
- package/vendor/ssh2/lib/client.js +2176 -0
- package/vendor/ssh2/lib/http-agents.js +84 -0
- package/vendor/ssh2/lib/index.js +44 -0
- package/vendor/ssh2/lib/keygen.js +582 -0
- package/vendor/ssh2/lib/protocol/Protocol.js +2136 -0
- package/vendor/ssh2/lib/protocol/SFTP.js +4052 -0
- package/vendor/ssh2/lib/protocol/constants.js +356 -0
- package/vendor/ssh2/lib/protocol/crypto/poly1305.js +43 -0
- package/vendor/ssh2/lib/protocol/crypto.js +1602 -0
- package/vendor/ssh2/lib/protocol/handlers.js +16 -0
- package/vendor/ssh2/lib/protocol/handlers.misc.js +1285 -0
- package/vendor/ssh2/lib/protocol/kex.js +1908 -0
- package/vendor/ssh2/lib/protocol/keyParser.js +1484 -0
- package/vendor/ssh2/lib/protocol/node-fs-compat.js +115 -0
- package/vendor/ssh2/lib/protocol/utils.js +356 -0
- package/vendor/ssh2/lib/protocol/zlib.js +255 -0
- package/vendor/ssh2/lib/server.js +1380 -0
- package/vendor/ssh2/lib/utils.js +336 -0
- package/vendor/ssh2/package.json +34 -0
- package/vendor/xterm.js +53 -0
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
// TODO:
|
|
2
|
+
// * convert listenerCount() usage to emit() return value checking?
|
|
3
|
+
// * emit error when connection severed early (e.g. before handshake)
|
|
4
|
+
// * add '.connected' or similar property to connection objects to allow
|
|
5
|
+
// immediate connection status checking
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const { Server: netServer } = require('net');
|
|
9
|
+
const EventEmitter = require('events');
|
|
10
|
+
const { listenerCount } = EventEmitter;
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
CHANNEL_OPEN_FAILURE,
|
|
14
|
+
DEFAULT_CIPHER,
|
|
15
|
+
DEFAULT_COMPRESSION,
|
|
16
|
+
DEFAULT_KEX,
|
|
17
|
+
DEFAULT_MAC,
|
|
18
|
+
DEFAULT_SERVER_HOST_KEY,
|
|
19
|
+
DISCONNECT_REASON,
|
|
20
|
+
DISCONNECT_REASON_BY_VALUE,
|
|
21
|
+
SUPPORTED_CIPHER,
|
|
22
|
+
SUPPORTED_COMPRESSION,
|
|
23
|
+
SUPPORTED_KEX,
|
|
24
|
+
SUPPORTED_MAC,
|
|
25
|
+
SUPPORTED_SERVER_HOST_KEY,
|
|
26
|
+
} = require('./protocol/constants.js');
|
|
27
|
+
const { init: cryptoInit } = require('./protocol/crypto.js');
|
|
28
|
+
const { KexInit } = require('./protocol/kex.js');
|
|
29
|
+
const { parseKey } = require('./protocol/keyParser.js');
|
|
30
|
+
const Protocol = require('./protocol/Protocol.js');
|
|
31
|
+
const { SFTP } = require('./protocol/SFTP.js');
|
|
32
|
+
const { writeUInt32BE } = require('./protocol/utils.js');
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
Channel,
|
|
36
|
+
MAX_WINDOW,
|
|
37
|
+
PACKET_SIZE,
|
|
38
|
+
windowAdjust,
|
|
39
|
+
WINDOW_THRESHOLD,
|
|
40
|
+
} = require('./Channel.js');
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
ChannelManager,
|
|
44
|
+
generateAlgorithmList,
|
|
45
|
+
isWritable,
|
|
46
|
+
onChannelOpenFailure,
|
|
47
|
+
onCHANNEL_CLOSE,
|
|
48
|
+
} = require('./utils.js');
|
|
49
|
+
|
|
50
|
+
const MAX_PENDING_AUTHS = 10;
|
|
51
|
+
|
|
52
|
+
class AuthContext extends EventEmitter {
|
|
53
|
+
constructor(protocol, username, service, method, cb) {
|
|
54
|
+
super();
|
|
55
|
+
|
|
56
|
+
this.username = this.user = username;
|
|
57
|
+
this.service = service;
|
|
58
|
+
this.method = method;
|
|
59
|
+
this._initialResponse = false;
|
|
60
|
+
this._finalResponse = false;
|
|
61
|
+
this._multistep = false;
|
|
62
|
+
this._cbfinal = (allowed, methodsLeft, isPartial) => {
|
|
63
|
+
if (!this._finalResponse) {
|
|
64
|
+
this._finalResponse = true;
|
|
65
|
+
cb(this, allowed, methodsLeft, isPartial);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
this._protocol = protocol;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
accept() {
|
|
72
|
+
this._cleanup && this._cleanup();
|
|
73
|
+
this._initialResponse = true;
|
|
74
|
+
this._cbfinal(true);
|
|
75
|
+
}
|
|
76
|
+
reject(methodsLeft, isPartial) {
|
|
77
|
+
this._cleanup && this._cleanup();
|
|
78
|
+
this._initialResponse = true;
|
|
79
|
+
this._cbfinal(false, methodsLeft, isPartial);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class KeyboardAuthContext extends AuthContext {
|
|
85
|
+
constructor(protocol, username, service, method, submethods, cb) {
|
|
86
|
+
super(protocol, username, service, method, cb);
|
|
87
|
+
|
|
88
|
+
this._multistep = true;
|
|
89
|
+
|
|
90
|
+
this._cb = undefined;
|
|
91
|
+
this._onInfoResponse = (responses) => {
|
|
92
|
+
const callback = this._cb;
|
|
93
|
+
if (callback) {
|
|
94
|
+
this._cb = undefined;
|
|
95
|
+
callback(responses);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
this.submethods = submethods;
|
|
99
|
+
this.on('abort', () => {
|
|
100
|
+
this._cb && this._cb(new Error('Authentication request aborted'));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
prompt(prompts, title, instructions, cb) {
|
|
105
|
+
if (!Array.isArray(prompts))
|
|
106
|
+
prompts = [ prompts ];
|
|
107
|
+
|
|
108
|
+
if (typeof title === 'function') {
|
|
109
|
+
cb = title;
|
|
110
|
+
title = instructions = undefined;
|
|
111
|
+
} else if (typeof instructions === 'function') {
|
|
112
|
+
cb = instructions;
|
|
113
|
+
instructions = undefined;
|
|
114
|
+
} else if (typeof cb !== 'function') {
|
|
115
|
+
cb = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < prompts.length; ++i) {
|
|
119
|
+
if (typeof prompts[i] === 'string') {
|
|
120
|
+
prompts[i] = {
|
|
121
|
+
prompt: prompts[i],
|
|
122
|
+
echo: true
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this._cb = cb;
|
|
128
|
+
this._initialResponse = true;
|
|
129
|
+
|
|
130
|
+
this._protocol.authInfoReq(title, instructions, prompts);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
class PKAuthContext extends AuthContext {
|
|
135
|
+
constructor(protocol, username, service, method, pkInfo, cb) {
|
|
136
|
+
super(protocol, username, service, method, cb);
|
|
137
|
+
|
|
138
|
+
this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
|
|
139
|
+
this.hashAlgo = pkInfo.hashAlgo;
|
|
140
|
+
this.signature = pkInfo.signature;
|
|
141
|
+
this.blob = pkInfo.blob;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
accept() {
|
|
145
|
+
if (!this.signature) {
|
|
146
|
+
this._initialResponse = true;
|
|
147
|
+
this._protocol.authPKOK(this.key.algo, this.key.data);
|
|
148
|
+
} else {
|
|
149
|
+
AuthContext.prototype.accept.call(this);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class HostbasedAuthContext extends AuthContext {
|
|
155
|
+
constructor(protocol, username, service, method, pkInfo, cb) {
|
|
156
|
+
super(protocol, username, service, method, cb);
|
|
157
|
+
|
|
158
|
+
this.key = { algo: pkInfo.keyAlgo, data: pkInfo.key };
|
|
159
|
+
this.hashAlgo = pkInfo.hashAlgo;
|
|
160
|
+
this.signature = pkInfo.signature;
|
|
161
|
+
this.blob = pkInfo.blob;
|
|
162
|
+
this.localHostname = pkInfo.localHostname;
|
|
163
|
+
this.localUsername = pkInfo.localUsername;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class PwdAuthContext extends AuthContext {
|
|
168
|
+
constructor(protocol, username, service, method, password, cb) {
|
|
169
|
+
super(protocol, username, service, method, cb);
|
|
170
|
+
|
|
171
|
+
this.password = password;
|
|
172
|
+
this._changeCb = undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
requestChange(prompt, cb) {
|
|
176
|
+
if (this._changeCb)
|
|
177
|
+
throw new Error('Change request already in progress');
|
|
178
|
+
if (typeof prompt !== 'string')
|
|
179
|
+
throw new Error('prompt argument must be a string');
|
|
180
|
+
if (typeof cb !== 'function')
|
|
181
|
+
throw new Error('Callback argument must be a function');
|
|
182
|
+
this._changeCb = cb;
|
|
183
|
+
this._protocol.authPasswdChg(prompt);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Session extends EventEmitter {
|
|
189
|
+
constructor(client, info, localChan) {
|
|
190
|
+
super();
|
|
191
|
+
|
|
192
|
+
this.type = 'session';
|
|
193
|
+
this.subtype = undefined;
|
|
194
|
+
this.server = true;
|
|
195
|
+
this._ending = false;
|
|
196
|
+
this._channel = undefined;
|
|
197
|
+
this._chanInfo = {
|
|
198
|
+
type: 'session',
|
|
199
|
+
incoming: {
|
|
200
|
+
id: localChan,
|
|
201
|
+
window: MAX_WINDOW,
|
|
202
|
+
packetSize: PACKET_SIZE,
|
|
203
|
+
state: 'open'
|
|
204
|
+
},
|
|
205
|
+
outgoing: {
|
|
206
|
+
id: info.sender,
|
|
207
|
+
window: info.window,
|
|
208
|
+
packetSize: info.packetSize,
|
|
209
|
+
state: 'open'
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class Server extends EventEmitter {
|
|
217
|
+
constructor(cfg, listener) {
|
|
218
|
+
super();
|
|
219
|
+
|
|
220
|
+
if (typeof cfg !== 'object' || cfg === null)
|
|
221
|
+
throw new Error('Missing configuration object');
|
|
222
|
+
|
|
223
|
+
const hostKeys = Object.create(null);
|
|
224
|
+
const hostKeyAlgoOrder = [];
|
|
225
|
+
|
|
226
|
+
const hostKeys_ = cfg.hostKeys;
|
|
227
|
+
if (!Array.isArray(hostKeys_))
|
|
228
|
+
throw new Error('hostKeys must be an array');
|
|
229
|
+
|
|
230
|
+
const cfgAlgos = (
|
|
231
|
+
typeof cfg.algorithms === 'object' && cfg.algorithms !== null
|
|
232
|
+
? cfg.algorithms
|
|
233
|
+
: {}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const hostKeyAlgos = generateAlgorithmList(
|
|
237
|
+
cfgAlgos.serverHostKey,
|
|
238
|
+
DEFAULT_SERVER_HOST_KEY,
|
|
239
|
+
SUPPORTED_SERVER_HOST_KEY
|
|
240
|
+
);
|
|
241
|
+
for (let i = 0; i < hostKeys_.length; ++i) {
|
|
242
|
+
let privateKey;
|
|
243
|
+
if (Buffer.isBuffer(hostKeys_[i]) || typeof hostKeys_[i] === 'string')
|
|
244
|
+
privateKey = parseKey(hostKeys_[i]);
|
|
245
|
+
else
|
|
246
|
+
privateKey = parseKey(hostKeys_[i].key, hostKeys_[i].passphrase);
|
|
247
|
+
|
|
248
|
+
if (privateKey instanceof Error)
|
|
249
|
+
throw new Error(`Cannot parse privateKey: ${privateKey.message}`);
|
|
250
|
+
|
|
251
|
+
if (Array.isArray(privateKey)) {
|
|
252
|
+
// OpenSSH's newer format only stores 1 key for now
|
|
253
|
+
privateKey = privateKey[0];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (privateKey.getPrivatePEM() === null)
|
|
257
|
+
throw new Error('privateKey value contains an invalid private key');
|
|
258
|
+
|
|
259
|
+
// Discard key if we already found a key of the same type
|
|
260
|
+
if (hostKeyAlgoOrder.includes(privateKey.type))
|
|
261
|
+
continue;
|
|
262
|
+
|
|
263
|
+
if (privateKey.type === 'ssh-rsa') {
|
|
264
|
+
// SSH supports multiple signature hashing algorithms for RSA, so we add
|
|
265
|
+
// the algorithms in the desired order
|
|
266
|
+
let sha1Pos = hostKeyAlgos.indexOf('ssh-rsa');
|
|
267
|
+
const sha256Pos = hostKeyAlgos.indexOf('rsa-sha2-256');
|
|
268
|
+
const sha512Pos = hostKeyAlgos.indexOf('rsa-sha2-512');
|
|
269
|
+
if (sha1Pos === -1) {
|
|
270
|
+
// Fall back to giving SHA1 the lowest priority
|
|
271
|
+
sha1Pos = Infinity;
|
|
272
|
+
}
|
|
273
|
+
[sha1Pos, sha256Pos, sha512Pos].sort(compareNumbers).forEach((pos) => {
|
|
274
|
+
if (pos === -1)
|
|
275
|
+
return;
|
|
276
|
+
|
|
277
|
+
let type;
|
|
278
|
+
switch (pos) {
|
|
279
|
+
case sha1Pos: type = 'ssh-rsa'; break;
|
|
280
|
+
case sha256Pos: type = 'rsa-sha2-256'; break;
|
|
281
|
+
case sha512Pos: type = 'rsa-sha2-512'; break;
|
|
282
|
+
default: return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Store same RSA key under each hash algorithm name for convenience
|
|
286
|
+
hostKeys[type] = privateKey;
|
|
287
|
+
|
|
288
|
+
hostKeyAlgoOrder.push(type);
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
hostKeys[privateKey.type] = privateKey;
|
|
292
|
+
hostKeyAlgoOrder.push(privateKey.type);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const algorithms = {
|
|
297
|
+
kex: generateAlgorithmList(
|
|
298
|
+
cfgAlgos.kex,
|
|
299
|
+
DEFAULT_KEX,
|
|
300
|
+
SUPPORTED_KEX
|
|
301
|
+
).concat(['kex-strict-s-v00@openssh.com']),
|
|
302
|
+
serverHostKey: hostKeyAlgoOrder,
|
|
303
|
+
cs: {
|
|
304
|
+
cipher: generateAlgorithmList(
|
|
305
|
+
cfgAlgos.cipher,
|
|
306
|
+
DEFAULT_CIPHER,
|
|
307
|
+
SUPPORTED_CIPHER
|
|
308
|
+
),
|
|
309
|
+
mac: generateAlgorithmList(cfgAlgos.hmac, DEFAULT_MAC, SUPPORTED_MAC),
|
|
310
|
+
compress: generateAlgorithmList(
|
|
311
|
+
cfgAlgos.compress,
|
|
312
|
+
DEFAULT_COMPRESSION,
|
|
313
|
+
SUPPORTED_COMPRESSION
|
|
314
|
+
),
|
|
315
|
+
lang: [],
|
|
316
|
+
},
|
|
317
|
+
sc: undefined,
|
|
318
|
+
};
|
|
319
|
+
algorithms.sc = algorithms.cs;
|
|
320
|
+
|
|
321
|
+
if (typeof listener === 'function')
|
|
322
|
+
this.on('connection', listener);
|
|
323
|
+
|
|
324
|
+
const origDebug = (typeof cfg.debug === 'function' ? cfg.debug : undefined);
|
|
325
|
+
const ident = (cfg.ident ? Buffer.from(cfg.ident) : undefined);
|
|
326
|
+
const offer = new KexInit(algorithms);
|
|
327
|
+
|
|
328
|
+
this._srv = new netServer((socket) => {
|
|
329
|
+
if (this._connections >= this.maxConnections) {
|
|
330
|
+
socket.destroy();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
++this._connections;
|
|
334
|
+
socket.once('close', () => {
|
|
335
|
+
--this._connections;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
let debug;
|
|
339
|
+
if (origDebug) {
|
|
340
|
+
// Prepend debug output with a unique identifier in case there are
|
|
341
|
+
// multiple clients connected at the same time
|
|
342
|
+
const debugPrefix = `[${process.hrtime().join('.')}] `;
|
|
343
|
+
debug = (msg) => {
|
|
344
|
+
origDebug(`${debugPrefix}${msg}`);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// eslint-disable-next-line no-use-before-define
|
|
349
|
+
new Client(socket, hostKeys, ident, offer, debug, this, cfg);
|
|
350
|
+
}).on('error', (err) => {
|
|
351
|
+
this.emit('error', err);
|
|
352
|
+
}).on('listening', () => {
|
|
353
|
+
this.emit('listening');
|
|
354
|
+
}).on('close', () => {
|
|
355
|
+
this.emit('close');
|
|
356
|
+
});
|
|
357
|
+
this._connections = 0;
|
|
358
|
+
this.maxConnections = Infinity;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
injectSocket(socket) {
|
|
362
|
+
this._srv.emit('connection', socket);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
listen(...args) {
|
|
366
|
+
this._srv.listen(...args);
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
address() {
|
|
371
|
+
return this._srv.address();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getConnections(cb) {
|
|
375
|
+
this._srv.getConnections(cb);
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
close(cb) {
|
|
380
|
+
this._srv.close(cb);
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
ref() {
|
|
385
|
+
this._srv.ref();
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
unref() {
|
|
390
|
+
this._srv.unref();
|
|
391
|
+
return this;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
Server.KEEPALIVE_CLIENT_INTERVAL = 15000;
|
|
395
|
+
Server.KEEPALIVE_CLIENT_COUNT_MAX = 3;
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class Client extends EventEmitter {
|
|
399
|
+
constructor(socket, hostKeys, ident, offer, debug, server, srvCfg) {
|
|
400
|
+
super();
|
|
401
|
+
|
|
402
|
+
let exchanges = 0;
|
|
403
|
+
let acceptedAuthSvc = false;
|
|
404
|
+
let pendingAuths = [];
|
|
405
|
+
let authCtx;
|
|
406
|
+
let kaTimer;
|
|
407
|
+
let onPacket;
|
|
408
|
+
const unsentGlobalRequestsReplies = [];
|
|
409
|
+
this._sock = socket;
|
|
410
|
+
this._chanMgr = new ChannelManager(this);
|
|
411
|
+
this._debug = debug;
|
|
412
|
+
this.noMoreSessions = false;
|
|
413
|
+
this.authenticated = false;
|
|
414
|
+
|
|
415
|
+
// Silence pre-header errors
|
|
416
|
+
function onClientPreHeaderError(err) {}
|
|
417
|
+
this.on('error', onClientPreHeaderError);
|
|
418
|
+
|
|
419
|
+
const DEBUG_HANDLER = (!debug ? undefined : (p, display, msg) => {
|
|
420
|
+
debug(`Debug output from client: ${JSON.stringify(msg)}`);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const kaIntvl = (
|
|
424
|
+
typeof srvCfg.keepaliveInterval === 'number'
|
|
425
|
+
&& isFinite(srvCfg.keepaliveInterval)
|
|
426
|
+
&& srvCfg.keepaliveInterval > 0
|
|
427
|
+
? srvCfg.keepaliveInterval
|
|
428
|
+
: (
|
|
429
|
+
typeof Server.KEEPALIVE_CLIENT_INTERVAL === 'number'
|
|
430
|
+
&& isFinite(Server.KEEPALIVE_CLIENT_INTERVAL)
|
|
431
|
+
&& Server.KEEPALIVE_CLIENT_INTERVAL > 0
|
|
432
|
+
? Server.KEEPALIVE_CLIENT_INTERVAL
|
|
433
|
+
: -1
|
|
434
|
+
)
|
|
435
|
+
);
|
|
436
|
+
const kaCountMax = (
|
|
437
|
+
typeof srvCfg.keepaliveCountMax === 'number'
|
|
438
|
+
&& isFinite(srvCfg.keepaliveCountMax)
|
|
439
|
+
&& srvCfg.keepaliveCountMax >= 0
|
|
440
|
+
? srvCfg.keepaliveCountMax
|
|
441
|
+
: (
|
|
442
|
+
typeof Server.KEEPALIVE_CLIENT_COUNT_MAX === 'number'
|
|
443
|
+
&& isFinite(Server.KEEPALIVE_CLIENT_COUNT_MAX)
|
|
444
|
+
&& Server.KEEPALIVE_CLIENT_COUNT_MAX >= 0
|
|
445
|
+
? Server.KEEPALIVE_CLIENT_COUNT_MAX
|
|
446
|
+
: -1
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
let kaCurCount = 0;
|
|
450
|
+
if (kaIntvl !== -1 && kaCountMax !== -1) {
|
|
451
|
+
this.once('ready', () => {
|
|
452
|
+
const onClose = () => {
|
|
453
|
+
clearInterval(kaTimer);
|
|
454
|
+
};
|
|
455
|
+
this.on('close', onClose).on('end', onClose);
|
|
456
|
+
kaTimer = setInterval(() => {
|
|
457
|
+
if (++kaCurCount > kaCountMax) {
|
|
458
|
+
clearInterval(kaTimer);
|
|
459
|
+
const err = new Error('Keepalive timeout');
|
|
460
|
+
err.level = 'client-timeout';
|
|
461
|
+
this.emit('error', err);
|
|
462
|
+
this.end();
|
|
463
|
+
} else {
|
|
464
|
+
// XXX: if the server ever starts sending real global requests to
|
|
465
|
+
// the client, we will need to add a dummy callback here to
|
|
466
|
+
// keep the correct reply order
|
|
467
|
+
proto.ping();
|
|
468
|
+
}
|
|
469
|
+
}, kaIntvl);
|
|
470
|
+
});
|
|
471
|
+
// TODO: re-verify keepalive behavior with OpenSSH
|
|
472
|
+
onPacket = () => {
|
|
473
|
+
kaTimer && kaTimer.refresh();
|
|
474
|
+
kaCurCount = 0;
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const proto = this._protocol = new Protocol({
|
|
479
|
+
server: true,
|
|
480
|
+
hostKeys,
|
|
481
|
+
ident,
|
|
482
|
+
offer,
|
|
483
|
+
onPacket,
|
|
484
|
+
greeting: srvCfg.greeting,
|
|
485
|
+
banner: srvCfg.banner,
|
|
486
|
+
onWrite: (data) => {
|
|
487
|
+
if (isWritable(socket))
|
|
488
|
+
socket.write(data);
|
|
489
|
+
},
|
|
490
|
+
onError: (err) => {
|
|
491
|
+
if (!proto._destruct)
|
|
492
|
+
socket.removeAllListeners('data');
|
|
493
|
+
this.emit('error', err);
|
|
494
|
+
try {
|
|
495
|
+
socket.end();
|
|
496
|
+
} catch {}
|
|
497
|
+
},
|
|
498
|
+
onHeader: (header) => {
|
|
499
|
+
this.removeListener('error', onClientPreHeaderError);
|
|
500
|
+
|
|
501
|
+
const info = {
|
|
502
|
+
ip: socket.remoteAddress,
|
|
503
|
+
family: socket.remoteFamily,
|
|
504
|
+
port: socket.remotePort,
|
|
505
|
+
header,
|
|
506
|
+
};
|
|
507
|
+
if (!server.emit('connection', this, info)) {
|
|
508
|
+
// auto reject
|
|
509
|
+
proto.disconnect(DISCONNECT_REASON.BY_APPLICATION);
|
|
510
|
+
socket.end();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (header.greeting)
|
|
515
|
+
this.emit('greeting', header.greeting);
|
|
516
|
+
},
|
|
517
|
+
onHandshakeComplete: (negotiated) => {
|
|
518
|
+
if (++exchanges > 1)
|
|
519
|
+
this.emit('rekey');
|
|
520
|
+
this.emit('handshake', negotiated);
|
|
521
|
+
},
|
|
522
|
+
debug,
|
|
523
|
+
messageHandlers: {
|
|
524
|
+
DEBUG: DEBUG_HANDLER,
|
|
525
|
+
DISCONNECT: (p, reason, desc) => {
|
|
526
|
+
if (reason !== DISCONNECT_REASON.BY_APPLICATION) {
|
|
527
|
+
if (!desc) {
|
|
528
|
+
desc = DISCONNECT_REASON_BY_VALUE[reason];
|
|
529
|
+
if (desc === undefined)
|
|
530
|
+
desc = `Unexpected disconnection reason: ${reason}`;
|
|
531
|
+
}
|
|
532
|
+
const err = new Error(desc);
|
|
533
|
+
err.code = reason;
|
|
534
|
+
this.emit('error', err);
|
|
535
|
+
}
|
|
536
|
+
socket.end();
|
|
537
|
+
},
|
|
538
|
+
CHANNEL_OPEN: (p, info) => {
|
|
539
|
+
// Handle incoming requests from client
|
|
540
|
+
|
|
541
|
+
// Do early reject in some cases to prevent wasteful channel
|
|
542
|
+
// allocation
|
|
543
|
+
if ((info.type === 'session' && this.noMoreSessions)
|
|
544
|
+
|| !this.authenticated) {
|
|
545
|
+
const reasonCode = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
|
|
546
|
+
return proto.channelOpenFail(info.sender, reasonCode);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let localChan = -1;
|
|
550
|
+
let reason;
|
|
551
|
+
let replied = false;
|
|
552
|
+
|
|
553
|
+
let accept;
|
|
554
|
+
const reject = () => {
|
|
555
|
+
if (replied)
|
|
556
|
+
return;
|
|
557
|
+
replied = true;
|
|
558
|
+
|
|
559
|
+
if (reason === undefined) {
|
|
560
|
+
if (localChan === -1)
|
|
561
|
+
reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
|
|
562
|
+
else
|
|
563
|
+
reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (localChan !== -1)
|
|
567
|
+
this._chanMgr.remove(localChan);
|
|
568
|
+
proto.channelOpenFail(info.sender, reason, '');
|
|
569
|
+
};
|
|
570
|
+
const reserveChannel = () => {
|
|
571
|
+
localChan = this._chanMgr.add();
|
|
572
|
+
|
|
573
|
+
if (localChan === -1) {
|
|
574
|
+
reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE;
|
|
575
|
+
if (debug) {
|
|
576
|
+
debug('Automatic rejection of incoming channel open: '
|
|
577
|
+
+ 'no channels available');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return (localChan !== -1);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const data = info.data;
|
|
585
|
+
switch (info.type) {
|
|
586
|
+
case 'session':
|
|
587
|
+
if (listenerCount(this, 'session') && reserveChannel()) {
|
|
588
|
+
accept = () => {
|
|
589
|
+
if (replied)
|
|
590
|
+
return;
|
|
591
|
+
replied = true;
|
|
592
|
+
|
|
593
|
+
const instance = new Session(this, info, localChan);
|
|
594
|
+
this._chanMgr.update(localChan, instance);
|
|
595
|
+
|
|
596
|
+
proto.channelOpenConfirm(info.sender,
|
|
597
|
+
localChan,
|
|
598
|
+
MAX_WINDOW,
|
|
599
|
+
PACKET_SIZE);
|
|
600
|
+
|
|
601
|
+
return instance;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
this.emit('session', accept, reject);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
case 'direct-tcpip':
|
|
609
|
+
if (listenerCount(this, 'tcpip') && reserveChannel()) {
|
|
610
|
+
accept = () => {
|
|
611
|
+
if (replied)
|
|
612
|
+
return;
|
|
613
|
+
replied = true;
|
|
614
|
+
|
|
615
|
+
const chanInfo = {
|
|
616
|
+
type: undefined,
|
|
617
|
+
incoming: {
|
|
618
|
+
id: localChan,
|
|
619
|
+
window: MAX_WINDOW,
|
|
620
|
+
packetSize: PACKET_SIZE,
|
|
621
|
+
state: 'open'
|
|
622
|
+
},
|
|
623
|
+
outgoing: {
|
|
624
|
+
id: info.sender,
|
|
625
|
+
window: info.window,
|
|
626
|
+
packetSize: info.packetSize,
|
|
627
|
+
state: 'open'
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const stream = new Channel(this, chanInfo, { server: true });
|
|
632
|
+
this._chanMgr.update(localChan, stream);
|
|
633
|
+
|
|
634
|
+
proto.channelOpenConfirm(info.sender,
|
|
635
|
+
localChan,
|
|
636
|
+
MAX_WINDOW,
|
|
637
|
+
PACKET_SIZE);
|
|
638
|
+
|
|
639
|
+
return stream;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
this.emit('tcpip', accept, reject, data);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
break;
|
|
646
|
+
case 'direct-streamlocal@openssh.com':
|
|
647
|
+
if (listenerCount(this, 'openssh.streamlocal')
|
|
648
|
+
&& reserveChannel()) {
|
|
649
|
+
accept = () => {
|
|
650
|
+
if (replied)
|
|
651
|
+
return;
|
|
652
|
+
replied = true;
|
|
653
|
+
|
|
654
|
+
const chanInfo = {
|
|
655
|
+
type: undefined,
|
|
656
|
+
incoming: {
|
|
657
|
+
id: localChan,
|
|
658
|
+
window: MAX_WINDOW,
|
|
659
|
+
packetSize: PACKET_SIZE,
|
|
660
|
+
state: 'open'
|
|
661
|
+
},
|
|
662
|
+
outgoing: {
|
|
663
|
+
id: info.sender,
|
|
664
|
+
window: info.window,
|
|
665
|
+
packetSize: info.packetSize,
|
|
666
|
+
state: 'open'
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const stream = new Channel(this, chanInfo, { server: true });
|
|
671
|
+
this._chanMgr.update(localChan, stream);
|
|
672
|
+
|
|
673
|
+
proto.channelOpenConfirm(info.sender,
|
|
674
|
+
localChan,
|
|
675
|
+
MAX_WINDOW,
|
|
676
|
+
PACKET_SIZE);
|
|
677
|
+
|
|
678
|
+
return stream;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
this.emit('openssh.streamlocal', accept, reject, data);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
default:
|
|
686
|
+
// Automatically reject any unsupported channel open requests
|
|
687
|
+
reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE;
|
|
688
|
+
if (debug) {
|
|
689
|
+
debug('Automatic rejection of unsupported incoming channel open'
|
|
690
|
+
+ ` type: ${info.type}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (reason === undefined) {
|
|
695
|
+
reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED;
|
|
696
|
+
if (debug) {
|
|
697
|
+
debug('Automatic rejection of unexpected incoming channel open'
|
|
698
|
+
+ ` for: ${info.type}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
reject();
|
|
703
|
+
},
|
|
704
|
+
CHANNEL_OPEN_CONFIRMATION: (p, info) => {
|
|
705
|
+
const channel = this._chanMgr.get(info.recipient);
|
|
706
|
+
if (typeof channel !== 'function')
|
|
707
|
+
return;
|
|
708
|
+
|
|
709
|
+
const chanInfo = {
|
|
710
|
+
type: channel.type,
|
|
711
|
+
incoming: {
|
|
712
|
+
id: info.recipient,
|
|
713
|
+
window: MAX_WINDOW,
|
|
714
|
+
packetSize: PACKET_SIZE,
|
|
715
|
+
state: 'open'
|
|
716
|
+
},
|
|
717
|
+
outgoing: {
|
|
718
|
+
id: info.sender,
|
|
719
|
+
window: info.window,
|
|
720
|
+
packetSize: info.packetSize,
|
|
721
|
+
state: 'open'
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const instance = new Channel(this, chanInfo, { server: true });
|
|
726
|
+
this._chanMgr.update(info.recipient, instance);
|
|
727
|
+
channel(undefined, instance);
|
|
728
|
+
},
|
|
729
|
+
CHANNEL_OPEN_FAILURE: (p, recipient, reason, description) => {
|
|
730
|
+
const channel = this._chanMgr.get(recipient);
|
|
731
|
+
if (typeof channel !== 'function')
|
|
732
|
+
return;
|
|
733
|
+
|
|
734
|
+
const info = { reason, description };
|
|
735
|
+
onChannelOpenFailure(this, recipient, info, channel);
|
|
736
|
+
},
|
|
737
|
+
CHANNEL_DATA: (p, recipient, data) => {
|
|
738
|
+
let channel = this._chanMgr.get(recipient);
|
|
739
|
+
if (typeof channel !== 'object' || channel === null)
|
|
740
|
+
return;
|
|
741
|
+
|
|
742
|
+
if (channel.constructor === Session) {
|
|
743
|
+
channel = channel._channel;
|
|
744
|
+
if (!channel)
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// The remote party should not be sending us data if there is no
|
|
749
|
+
// window space available ...
|
|
750
|
+
// TODO: raise error on data with not enough window?
|
|
751
|
+
if (channel.incoming.window === 0)
|
|
752
|
+
return;
|
|
753
|
+
|
|
754
|
+
channel.incoming.window -= data.length;
|
|
755
|
+
|
|
756
|
+
if (channel.push(data) === false) {
|
|
757
|
+
channel._waitChanDrain = true;
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (channel.incoming.window <= WINDOW_THRESHOLD)
|
|
762
|
+
windowAdjust(channel);
|
|
763
|
+
},
|
|
764
|
+
CHANNEL_EXTENDED_DATA: (p, recipient, data, type) => {
|
|
765
|
+
// NOOP -- should not be sent by client
|
|
766
|
+
},
|
|
767
|
+
CHANNEL_WINDOW_ADJUST: (p, recipient, amount) => {
|
|
768
|
+
let channel = this._chanMgr.get(recipient);
|
|
769
|
+
if (typeof channel !== 'object' || channel === null)
|
|
770
|
+
return;
|
|
771
|
+
|
|
772
|
+
if (channel.constructor === Session) {
|
|
773
|
+
channel = channel._channel;
|
|
774
|
+
if (!channel)
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// The other side is allowing us to send `amount` more bytes of data
|
|
779
|
+
channel.outgoing.window += amount;
|
|
780
|
+
|
|
781
|
+
if (channel._waitWindow) {
|
|
782
|
+
channel._waitWindow = false;
|
|
783
|
+
|
|
784
|
+
if (channel._chunk) {
|
|
785
|
+
channel._write(channel._chunk, null, channel._chunkcb);
|
|
786
|
+
} else if (channel._chunkcb) {
|
|
787
|
+
channel._chunkcb();
|
|
788
|
+
} else if (channel._chunkErr) {
|
|
789
|
+
channel.stderr._write(channel._chunkErr,
|
|
790
|
+
null,
|
|
791
|
+
channel._chunkcbErr);
|
|
792
|
+
} else if (channel._chunkcbErr) {
|
|
793
|
+
channel._chunkcbErr();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
CHANNEL_SUCCESS: (p, recipient) => {
|
|
798
|
+
let channel = this._chanMgr.get(recipient);
|
|
799
|
+
if (typeof channel !== 'object' || channel === null)
|
|
800
|
+
return;
|
|
801
|
+
|
|
802
|
+
if (channel.constructor === Session) {
|
|
803
|
+
channel = channel._channel;
|
|
804
|
+
if (!channel)
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (channel._callbacks.length)
|
|
809
|
+
channel._callbacks.shift()(false);
|
|
810
|
+
},
|
|
811
|
+
CHANNEL_FAILURE: (p, recipient) => {
|
|
812
|
+
let channel = this._chanMgr.get(recipient);
|
|
813
|
+
if (typeof channel !== 'object' || channel === null)
|
|
814
|
+
return;
|
|
815
|
+
|
|
816
|
+
if (channel.constructor === Session) {
|
|
817
|
+
channel = channel._channel;
|
|
818
|
+
if (!channel)
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (channel._callbacks.length)
|
|
823
|
+
channel._callbacks.shift()(true);
|
|
824
|
+
},
|
|
825
|
+
CHANNEL_REQUEST: (p, recipient, type, wantReply, data) => {
|
|
826
|
+
const session = this._chanMgr.get(recipient);
|
|
827
|
+
if (typeof session !== 'object' || session === null)
|
|
828
|
+
return;
|
|
829
|
+
|
|
830
|
+
let replied = false;
|
|
831
|
+
let accept;
|
|
832
|
+
let reject;
|
|
833
|
+
|
|
834
|
+
if (session.constructor !== Session) {
|
|
835
|
+
// normal Channel instance
|
|
836
|
+
if (wantReply)
|
|
837
|
+
proto.channelFailure(session.outgoing.id);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (wantReply) {
|
|
842
|
+
// "real session" requests will have custom accept behaviors
|
|
843
|
+
if (type !== 'shell'
|
|
844
|
+
&& type !== 'exec'
|
|
845
|
+
&& type !== 'subsystem') {
|
|
846
|
+
accept = () => {
|
|
847
|
+
if (replied || session._ending || session._channel)
|
|
848
|
+
return;
|
|
849
|
+
replied = true;
|
|
850
|
+
|
|
851
|
+
proto.channelSuccess(session._chanInfo.outgoing.id);
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
reject = () => {
|
|
856
|
+
if (replied || session._ending || session._channel)
|
|
857
|
+
return;
|
|
858
|
+
replied = true;
|
|
859
|
+
|
|
860
|
+
proto.channelFailure(session._chanInfo.outgoing.id);
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (session._ending) {
|
|
865
|
+
reject && reject();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
switch (type) {
|
|
870
|
+
// "pre-real session start" requests
|
|
871
|
+
case 'env':
|
|
872
|
+
if (listenerCount(session, 'env')) {
|
|
873
|
+
session.emit('env', accept, reject, {
|
|
874
|
+
key: data.name,
|
|
875
|
+
val: data.value
|
|
876
|
+
});
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
880
|
+
case 'pty-req':
|
|
881
|
+
if (listenerCount(session, 'pty')) {
|
|
882
|
+
session.emit('pty', accept, reject, data);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
case 'window-change':
|
|
887
|
+
if (listenerCount(session, 'window-change'))
|
|
888
|
+
session.emit('window-change', accept, reject, data);
|
|
889
|
+
else
|
|
890
|
+
reject && reject();
|
|
891
|
+
break;
|
|
892
|
+
case 'x11-req':
|
|
893
|
+
if (listenerCount(session, 'x11')) {
|
|
894
|
+
session.emit('x11', accept, reject, data);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
break;
|
|
898
|
+
// "post-real session start" requests
|
|
899
|
+
case 'signal':
|
|
900
|
+
if (listenerCount(session, 'signal')) {
|
|
901
|
+
session.emit('signal', accept, reject, {
|
|
902
|
+
name: data
|
|
903
|
+
});
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
// XXX: is `auth-agent-req@openssh.com` really "post-real session
|
|
908
|
+
// start"?
|
|
909
|
+
case 'auth-agent-req@openssh.com':
|
|
910
|
+
if (listenerCount(session, 'auth-agent')) {
|
|
911
|
+
session.emit('auth-agent', accept, reject);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
break;
|
|
915
|
+
// "real session start" requests
|
|
916
|
+
case 'shell':
|
|
917
|
+
if (listenerCount(session, 'shell')) {
|
|
918
|
+
accept = () => {
|
|
919
|
+
if (replied || session._ending || session._channel)
|
|
920
|
+
return;
|
|
921
|
+
replied = true;
|
|
922
|
+
|
|
923
|
+
if (wantReply)
|
|
924
|
+
proto.channelSuccess(session._chanInfo.outgoing.id);
|
|
925
|
+
|
|
926
|
+
const channel = new Channel(
|
|
927
|
+
this, session._chanInfo, { server: true }
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
channel.subtype = session.subtype = type;
|
|
931
|
+
session._channel = channel;
|
|
932
|
+
|
|
933
|
+
return channel;
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
session.emit('shell', accept, reject);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
break;
|
|
940
|
+
case 'exec':
|
|
941
|
+
if (listenerCount(session, 'exec')) {
|
|
942
|
+
accept = () => {
|
|
943
|
+
if (replied || session._ending || session._channel)
|
|
944
|
+
return;
|
|
945
|
+
replied = true;
|
|
946
|
+
|
|
947
|
+
if (wantReply)
|
|
948
|
+
proto.channelSuccess(session._chanInfo.outgoing.id);
|
|
949
|
+
|
|
950
|
+
const channel = new Channel(
|
|
951
|
+
this, session._chanInfo, { server: true }
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
channel.subtype = session.subtype = type;
|
|
955
|
+
session._channel = channel;
|
|
956
|
+
|
|
957
|
+
return channel;
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
session.emit('exec', accept, reject, {
|
|
961
|
+
command: data
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
break;
|
|
966
|
+
case 'subsystem': {
|
|
967
|
+
let useSFTP = (data === 'sftp');
|
|
968
|
+
accept = () => {
|
|
969
|
+
if (replied || session._ending || session._channel)
|
|
970
|
+
return;
|
|
971
|
+
replied = true;
|
|
972
|
+
|
|
973
|
+
if (wantReply)
|
|
974
|
+
proto.channelSuccess(session._chanInfo.outgoing.id);
|
|
975
|
+
|
|
976
|
+
let instance;
|
|
977
|
+
if (useSFTP) {
|
|
978
|
+
instance = new SFTP(this, session._chanInfo, {
|
|
979
|
+
server: true,
|
|
980
|
+
debug,
|
|
981
|
+
});
|
|
982
|
+
} else {
|
|
983
|
+
instance = new Channel(
|
|
984
|
+
this, session._chanInfo, { server: true }
|
|
985
|
+
);
|
|
986
|
+
instance.subtype =
|
|
987
|
+
session.subtype = `${type}:${data}`;
|
|
988
|
+
}
|
|
989
|
+
session._channel = instance;
|
|
990
|
+
|
|
991
|
+
return instance;
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
if (data === 'sftp') {
|
|
995
|
+
if (listenerCount(session, 'sftp')) {
|
|
996
|
+
session.emit('sftp', accept, reject);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
useSFTP = false;
|
|
1000
|
+
}
|
|
1001
|
+
if (listenerCount(session, 'subsystem')) {
|
|
1002
|
+
session.emit('subsystem', accept, reject, {
|
|
1003
|
+
name: data
|
|
1004
|
+
});
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
debug && debug(
|
|
1011
|
+
`Automatic rejection of incoming channel request: ${type}`
|
|
1012
|
+
);
|
|
1013
|
+
reject && reject();
|
|
1014
|
+
},
|
|
1015
|
+
CHANNEL_EOF: (p, recipient) => {
|
|
1016
|
+
let channel = this._chanMgr.get(recipient);
|
|
1017
|
+
if (typeof channel !== 'object' || channel === null)
|
|
1018
|
+
return;
|
|
1019
|
+
|
|
1020
|
+
if (channel.constructor === Session) {
|
|
1021
|
+
if (!channel._ending) {
|
|
1022
|
+
channel._ending = true;
|
|
1023
|
+
channel.emit('eof');
|
|
1024
|
+
channel.emit('end');
|
|
1025
|
+
}
|
|
1026
|
+
channel = channel._channel;
|
|
1027
|
+
if (!channel)
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (channel.incoming.state !== 'open')
|
|
1032
|
+
return;
|
|
1033
|
+
channel.incoming.state = 'eof';
|
|
1034
|
+
|
|
1035
|
+
if (channel.readable)
|
|
1036
|
+
channel.push(null);
|
|
1037
|
+
},
|
|
1038
|
+
CHANNEL_CLOSE: (p, recipient) => {
|
|
1039
|
+
let channel = this._chanMgr.get(recipient);
|
|
1040
|
+
if (typeof channel !== 'object' || channel === null)
|
|
1041
|
+
return;
|
|
1042
|
+
|
|
1043
|
+
if (channel.constructor === Session) {
|
|
1044
|
+
channel._ending = true;
|
|
1045
|
+
channel.emit('close');
|
|
1046
|
+
channel = channel._channel;
|
|
1047
|
+
if (!channel)
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
onCHANNEL_CLOSE(this, recipient, channel);
|
|
1052
|
+
},
|
|
1053
|
+
// Begin service/auth-related ==========================================
|
|
1054
|
+
SERVICE_REQUEST: (p, service) => {
|
|
1055
|
+
if (exchanges === 0
|
|
1056
|
+
|| acceptedAuthSvc
|
|
1057
|
+
|| this.authenticated
|
|
1058
|
+
|| service !== 'ssh-userauth') {
|
|
1059
|
+
proto.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);
|
|
1060
|
+
socket.end();
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
acceptedAuthSvc = true;
|
|
1065
|
+
proto.serviceAccept(service);
|
|
1066
|
+
},
|
|
1067
|
+
USERAUTH_REQUEST: (p, username, service, method, methodData) => {
|
|
1068
|
+
if (exchanges === 0
|
|
1069
|
+
|| this.authenticated
|
|
1070
|
+
|| (authCtx
|
|
1071
|
+
&& (authCtx.username !== username
|
|
1072
|
+
|| authCtx.service !== service))
|
|
1073
|
+
// TODO: support hostbased auth
|
|
1074
|
+
|| (method !== 'password'
|
|
1075
|
+
&& method !== 'publickey'
|
|
1076
|
+
&& method !== 'hostbased'
|
|
1077
|
+
&& method !== 'keyboard-interactive'
|
|
1078
|
+
&& method !== 'none')
|
|
1079
|
+
|| pendingAuths.length === MAX_PENDING_AUTHS) {
|
|
1080
|
+
proto.disconnect(DISCONNECT_REASON.PROTOCOL_ERROR);
|
|
1081
|
+
socket.end();
|
|
1082
|
+
return;
|
|
1083
|
+
} else if (service !== 'ssh-connection') {
|
|
1084
|
+
proto.disconnect(DISCONNECT_REASON.SERVICE_NOT_AVAILABLE);
|
|
1085
|
+
socket.end();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
let ctx;
|
|
1090
|
+
switch (method) {
|
|
1091
|
+
case 'keyboard-interactive':
|
|
1092
|
+
ctx = new KeyboardAuthContext(proto, username, service, method,
|
|
1093
|
+
methodData, onAuthDecide);
|
|
1094
|
+
break;
|
|
1095
|
+
case 'publickey':
|
|
1096
|
+
ctx = new PKAuthContext(proto, username, service, method,
|
|
1097
|
+
methodData, onAuthDecide);
|
|
1098
|
+
break;
|
|
1099
|
+
case 'hostbased':
|
|
1100
|
+
ctx = new HostbasedAuthContext(proto, username, service, method,
|
|
1101
|
+
methodData, onAuthDecide);
|
|
1102
|
+
break;
|
|
1103
|
+
case 'password':
|
|
1104
|
+
if (authCtx
|
|
1105
|
+
&& authCtx instanceof PwdAuthContext
|
|
1106
|
+
&& authCtx._changeCb) {
|
|
1107
|
+
const cb = authCtx._changeCb;
|
|
1108
|
+
authCtx._changeCb = undefined;
|
|
1109
|
+
cb(methodData.newPassword);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
ctx = new PwdAuthContext(proto, username, service, method,
|
|
1113
|
+
methodData, onAuthDecide);
|
|
1114
|
+
break;
|
|
1115
|
+
case 'none':
|
|
1116
|
+
ctx = new AuthContext(proto, username, service, method,
|
|
1117
|
+
onAuthDecide);
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (authCtx) {
|
|
1122
|
+
if (!authCtx._initialResponse) {
|
|
1123
|
+
return pendingAuths.push(ctx);
|
|
1124
|
+
} else if (authCtx._multistep && !authCtx._finalResponse) {
|
|
1125
|
+
// RFC 4252 says to silently abort the current auth request if a
|
|
1126
|
+
// new auth request comes in before the final response from an
|
|
1127
|
+
// auth method that requires additional request/response exchanges
|
|
1128
|
+
// -- this means keyboard-interactive for now ...
|
|
1129
|
+
authCtx._cleanup && authCtx._cleanup();
|
|
1130
|
+
authCtx.emit('abort');
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
authCtx = ctx;
|
|
1135
|
+
|
|
1136
|
+
if (listenerCount(this, 'authentication'))
|
|
1137
|
+
this.emit('authentication', authCtx);
|
|
1138
|
+
else
|
|
1139
|
+
authCtx.reject();
|
|
1140
|
+
},
|
|
1141
|
+
USERAUTH_INFO_RESPONSE: (p, responses) => {
|
|
1142
|
+
if (authCtx && authCtx instanceof KeyboardAuthContext)
|
|
1143
|
+
authCtx._onInfoResponse(responses);
|
|
1144
|
+
},
|
|
1145
|
+
// End service/auth-related ============================================
|
|
1146
|
+
GLOBAL_REQUEST: (p, name, wantReply, data) => {
|
|
1147
|
+
const reply = {
|
|
1148
|
+
type: null,
|
|
1149
|
+
buf: null
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
function setReply(type, buf) {
|
|
1153
|
+
reply.type = type;
|
|
1154
|
+
reply.buf = buf;
|
|
1155
|
+
sendReplies();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (wantReply)
|
|
1159
|
+
unsentGlobalRequestsReplies.push(reply);
|
|
1160
|
+
|
|
1161
|
+
if ((name === 'tcpip-forward'
|
|
1162
|
+
|| name === 'cancel-tcpip-forward'
|
|
1163
|
+
|| name === 'no-more-sessions@openssh.com'
|
|
1164
|
+
|| name === 'streamlocal-forward@openssh.com'
|
|
1165
|
+
|| name === 'cancel-streamlocal-forward@openssh.com')
|
|
1166
|
+
&& listenerCount(this, 'request')
|
|
1167
|
+
&& this.authenticated) {
|
|
1168
|
+
let accept;
|
|
1169
|
+
let reject;
|
|
1170
|
+
|
|
1171
|
+
if (wantReply) {
|
|
1172
|
+
let replied = false;
|
|
1173
|
+
accept = (chosenPort) => {
|
|
1174
|
+
if (replied)
|
|
1175
|
+
return;
|
|
1176
|
+
replied = true;
|
|
1177
|
+
let bufPort;
|
|
1178
|
+
if (name === 'tcpip-forward'
|
|
1179
|
+
&& data.bindPort === 0
|
|
1180
|
+
&& typeof chosenPort === 'number') {
|
|
1181
|
+
bufPort = Buffer.allocUnsafe(4);
|
|
1182
|
+
writeUInt32BE(bufPort, chosenPort, 0);
|
|
1183
|
+
}
|
|
1184
|
+
setReply('SUCCESS', bufPort);
|
|
1185
|
+
};
|
|
1186
|
+
reject = () => {
|
|
1187
|
+
if (replied)
|
|
1188
|
+
return;
|
|
1189
|
+
replied = true;
|
|
1190
|
+
setReply('FAILURE');
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (name === 'no-more-sessions@openssh.com') {
|
|
1195
|
+
this.noMoreSessions = true;
|
|
1196
|
+
accept && accept();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
this.emit('request', accept, reject, name, data);
|
|
1201
|
+
} else if (wantReply) {
|
|
1202
|
+
setReply('FAILURE');
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
socket.pause();
|
|
1209
|
+
cryptoInit.then(() => {
|
|
1210
|
+
proto.start();
|
|
1211
|
+
socket.on('data', (data) => {
|
|
1212
|
+
try {
|
|
1213
|
+
proto.parse(data, 0, data.length);
|
|
1214
|
+
} catch (ex) {
|
|
1215
|
+
this.emit('error', ex);
|
|
1216
|
+
try {
|
|
1217
|
+
if (isWritable(socket))
|
|
1218
|
+
socket.end();
|
|
1219
|
+
} catch {}
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
socket.resume();
|
|
1223
|
+
}).catch((err) => {
|
|
1224
|
+
this.emit('error', err);
|
|
1225
|
+
try {
|
|
1226
|
+
if (isWritable(socket))
|
|
1227
|
+
socket.end();
|
|
1228
|
+
} catch {}
|
|
1229
|
+
});
|
|
1230
|
+
socket.on('error', (err) => {
|
|
1231
|
+
err.level = 'socket';
|
|
1232
|
+
this.emit('error', err);
|
|
1233
|
+
}).once('end', () => {
|
|
1234
|
+
debug && debug('Socket ended');
|
|
1235
|
+
proto.cleanup();
|
|
1236
|
+
this.emit('end');
|
|
1237
|
+
}).once('close', () => {
|
|
1238
|
+
debug && debug('Socket closed');
|
|
1239
|
+
proto.cleanup();
|
|
1240
|
+
this.emit('close');
|
|
1241
|
+
|
|
1242
|
+
const err = new Error('No response from server');
|
|
1243
|
+
|
|
1244
|
+
// Simulate error for pending channels and close any open channels
|
|
1245
|
+
this._chanMgr.cleanup(err);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const onAuthDecide = (ctx, allowed, methodsLeft, isPartial) => {
|
|
1249
|
+
if (authCtx === ctx && !this.authenticated) {
|
|
1250
|
+
if (allowed) {
|
|
1251
|
+
authCtx = undefined;
|
|
1252
|
+
this.authenticated = true;
|
|
1253
|
+
proto.authSuccess();
|
|
1254
|
+
pendingAuths = [];
|
|
1255
|
+
this.emit('ready');
|
|
1256
|
+
} else {
|
|
1257
|
+
proto.authFailure(methodsLeft, isPartial);
|
|
1258
|
+
if (pendingAuths.length) {
|
|
1259
|
+
authCtx = pendingAuths.pop();
|
|
1260
|
+
if (listenerCount(this, 'authentication'))
|
|
1261
|
+
this.emit('authentication', authCtx);
|
|
1262
|
+
else
|
|
1263
|
+
authCtx.reject();
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
function sendReplies() {
|
|
1270
|
+
while (unsentGlobalRequestsReplies.length > 0
|
|
1271
|
+
&& unsentGlobalRequestsReplies[0].type) {
|
|
1272
|
+
const reply = unsentGlobalRequestsReplies.shift();
|
|
1273
|
+
if (reply.type === 'SUCCESS')
|
|
1274
|
+
proto.requestSuccess(reply.buf);
|
|
1275
|
+
if (reply.type === 'FAILURE')
|
|
1276
|
+
proto.requestFailure();
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
end() {
|
|
1282
|
+
if (this._sock && isWritable(this._sock)) {
|
|
1283
|
+
this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION);
|
|
1284
|
+
this._sock.end();
|
|
1285
|
+
}
|
|
1286
|
+
return this;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
x11(originAddr, originPort, cb) {
|
|
1290
|
+
const opts = { originAddr, originPort };
|
|
1291
|
+
openChannel(this, 'x11', opts, cb);
|
|
1292
|
+
return this;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
forwardOut(boundAddr, boundPort, remoteAddr, remotePort, cb) {
|
|
1296
|
+
const opts = { boundAddr, boundPort, remoteAddr, remotePort };
|
|
1297
|
+
openChannel(this, 'forwarded-tcpip', opts, cb);
|
|
1298
|
+
return this;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
openssh_forwardOutStreamLocal(socketPath, cb) {
|
|
1302
|
+
const opts = { socketPath };
|
|
1303
|
+
openChannel(this, 'forwarded-streamlocal@openssh.com', opts, cb);
|
|
1304
|
+
return this;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
rekey(cb) {
|
|
1308
|
+
let error;
|
|
1309
|
+
|
|
1310
|
+
try {
|
|
1311
|
+
this._protocol.rekey();
|
|
1312
|
+
} catch (ex) {
|
|
1313
|
+
error = ex;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// TODO: re-throw error if no callback?
|
|
1317
|
+
|
|
1318
|
+
if (typeof cb === 'function') {
|
|
1319
|
+
if (error)
|
|
1320
|
+
process.nextTick(cb, error);
|
|
1321
|
+
else
|
|
1322
|
+
this.once('rekey', cb);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
setNoDelay(noDelay) {
|
|
1327
|
+
if (this._sock && typeof this._sock.setNoDelay === 'function')
|
|
1328
|
+
this._sock.setNoDelay(noDelay);
|
|
1329
|
+
|
|
1330
|
+
return this;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
function openChannel(self, type, opts, cb) {
|
|
1336
|
+
// Ask the client to open a channel for some purpose (e.g. a forwarded TCP
|
|
1337
|
+
// connection)
|
|
1338
|
+
const initWindow = MAX_WINDOW;
|
|
1339
|
+
const maxPacket = PACKET_SIZE;
|
|
1340
|
+
|
|
1341
|
+
if (typeof opts === 'function') {
|
|
1342
|
+
cb = opts;
|
|
1343
|
+
opts = {};
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const wrapper = (err, stream) => {
|
|
1347
|
+
cb(err, stream);
|
|
1348
|
+
};
|
|
1349
|
+
wrapper.type = type;
|
|
1350
|
+
|
|
1351
|
+
const localChan = self._chanMgr.add(wrapper);
|
|
1352
|
+
|
|
1353
|
+
if (localChan === -1) {
|
|
1354
|
+
cb(new Error('No free channels available'));
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
switch (type) {
|
|
1359
|
+
case 'forwarded-tcpip':
|
|
1360
|
+
self._protocol.forwardedTcpip(localChan, initWindow, maxPacket, opts);
|
|
1361
|
+
break;
|
|
1362
|
+
case 'x11':
|
|
1363
|
+
self._protocol.x11(localChan, initWindow, maxPacket, opts);
|
|
1364
|
+
break;
|
|
1365
|
+
case 'forwarded-streamlocal@openssh.com':
|
|
1366
|
+
self._protocol.openssh_forwardedStreamLocal(
|
|
1367
|
+
localChan, initWindow, maxPacket, opts
|
|
1368
|
+
);
|
|
1369
|
+
break;
|
|
1370
|
+
default:
|
|
1371
|
+
throw new Error(`Unsupported channel type: ${type}`);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function compareNumbers(a, b) {
|
|
1376
|
+
return a - b;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
module.exports = Server;
|
|
1380
|
+
module.exports.IncomingClient = Client;
|