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