-
Notifications
You must be signed in to change notification settings - Fork 3
/
socks5.js
425 lines (337 loc) · 15.7 KB
/
socks5.js
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/* socks5.js
*
* Partially implements the SOCKS5 protocol over WebSockets
* The missing parts are around the types of authentication:
* does NOT support GSSAPI, [and has no means for plugging in new ].
*
* Depends on WebSocketStream (which depends on ayepromise and some WebSocket library).
*/
/* TODO
*
*
* [ ] We *should* be sending binary frames (i.e. have websocket frame opcode 0x2 i.e. use Blob()s)
* The SOCKS headers are binary, except for the DOMAINNAME address type, which is presumably ASCII. But after the SOCKS headers, we should get out of the way and use whatever form the user or remote end hands us (getting this right will mostly be an exercise in getting unit tests right)
* [ ] We should be able to parse and unparse IP addresses
since SOCKS transfers them in plain bytes, and the
header we send depends on the format we send the address in.
* [ ] Instead of using Strings everywhere, use integer enum codes and have a single function that does conversion between ints and (network ordered!) byte strings
* [ ] factor the common parts of the protocol in some way that allows implementing all three kinds of APIs is feasible
* [ ] Auth methods need to support callbacks so that when the server prompts; handling this generally is hard! What do we do.. we could call an event handler and demand that it return something? And it doesn't look like either Gnome or Firefox support SOCKS with passwords. Actually, SOCKS with passwords is wildly stupid since SOCKS supports; though in principle SOCKS is extensible with other 'methods' which can do anything: auth, privacy and/or MACing.
* [ ] unit tests!!
* [ ] Perhaps onopen should use.. promises? Backbone.Events?
* [ ] LOGIN auth
* [ ] GSSAPI auth (i feel like this is a wontfix; the spec is dumb to enforce a complicated API into a simple protocol)
* [ ] Make sure that if the underlying WebSocket fallsover that w eknow about it (that is, hook its onclose)
* [ ] Make sure that SOCKS5WebSocket actually supports the complete WebSocket API (how do we do readyState??)
*/
// UMD header
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.SOCKS5 = factory();
}
}(this, function () {
'use strict';
if(!WebSocket) {
var WebSocket = require("ws");
}
if(!WebSocketStream) {
var WebSocketStream = require("./websocketstream.js")
}
/* Utility Routines */
function split_addr(addr) {
// I would rather use the more general 'rsplit()'
// but js doesn't have that and writing it myself is fraught:
//http://stackoverflow.com/questions/958908/how-do-you-reverse-a-string-in-place-in-javascript/16776621#16776621
var split = addr.lastIndexOf(":")
var host = addr.slice(0, split)
var port = addr.slice(split+1)
return [host, port]
}
function join_addr(host, port) {
return host + ":" + port;
}
function build_int(n) {
if(!(0 <= n && n<=0xFF)) throw "Integer out of range. SOCKS integers must fit in one byte"
return String.fromCharCode(n);
}
function SOCKS5(proxy, auth_callback) {
/* This partially-evaluates SOCKS5WebSocket
* doing this requires bending over backwards a bit, because we want to support 'new'
* hence, we basically dynamically create a subclass which closes over 'proxy' and 'auth_callback'
*/
function WebSocket(address, protocols) {
//using SOCKS5WebSocket.call(this, ...) makes
// var C = SOCKS5(...); new C(...) behave properly
SOCKS5WebSocket.call(this, proxy, auth_callback, address, protocols);
}
WebSocket.prototype = SOCKS5WebSocket.prototype;
return WebSocket;
}
function SOCKS5WebSocket(proxy, auth_callback, address, protocols) {
/* implements the SOCKS5 client protocol
* reference: https://tools.ietf.org/html/rfc1928
*
* This class only implements TCP CONNECT SOCKS; SOCKS also allows UDP and even BIND modes, but those types require distinctly different APIs (UDP needs .send() and .onmessage; BIND only allows one remote TCP connection (i.e. it's not a full listen()+accept() implementation) but presumably there should be an intermediate state for "connected to the proxy but no one is connected to us"
*/
var self = this;
self.target = address;
self.remote = { host: null, port: null } //the address of the remote end of the tunnel
self._ws = new WebSocket(proxy)
// Though it is unlikely, I do not trust that TCP or WebSockets will not break up the SOCKS5 messages in funny ways
// Hence, internally I use a WebSocketStream to do the initial setup
// The WebSocketStream is deleted once the SOCKS headers are done.
self._stream = new WebSocketStream(this._ws)
self._stream.onopen = function(evt) {
self._connect()
}
// proxy some WebSocketStream events up unmolested
self._ws.onclose = function(evt) {
self.onclose(evt)
}
self._ws.onerror = function(evt) {
self.onerror(evt)
}
return SOCKS5WebSocket;
}
SOCKS5WebSocket.prototype._validate_version = function(b) {
if(b != this.VERSION) {
throw "Unsupported SOCKS version"
}
}
// B) connect to the SOCKS server
SOCKS5WebSocket.prototype._connect = function() {
var self = this;
return self._negotiate_method()
.then(function(m) { return self._negotiate_auth(m) }) //the wrapping of _negotiate_auth() is because then() suffers from changing what 'this' is
.then(function( ) { return self._negotiate_connection() })
.then(function( ) {
// C) Get out of the way: just forward packets
// we *disable* the WebSocketStream and redirect the onmessage
delete self._stream;
self._ws.onmessage = function(e) { self.onmessage(e) }
self.onopen({/*XXX fill in some sensible event result here */})
// should this step return anything?
})
.fail(function(e) { self.onerror(e) }) //chain exceptions out to the event handler
}
// 1) negotiate an connection method (i.e. an auth method, though encryption and digital signatures are theoretically an option here too);
SOCKS5WebSocket.prototype._negotiate_method = function() {
this._stream.send(this._build_method_selection([this.auth.NONE]))
return this._read_method();
}
SOCKS5WebSocket.prototype._build_method_selection = function(methods) {
// precondition: methods is a subset of this.auth
// XXX this precondition isn't enforced!
var nmethods = methods.length;
return this.VERSION + build_int(nmethods) + methods.join('')
}
SOCKS5WebSocket.prototype._read_method = function() {
var self = this;
return self._stream.recv(1)
.then(function(b) { self._validate_version(b) })
.then(function() { return self._stream.recv(1); })
}
// 2) negotiate the authentication;
// the spec says we "MUST" support GSSAPI, which I'm not going
// to do, and "SHOULD" support user/pass, which I am.
SOCKS5WebSocket.prototype._negotiate_auth = function(method) {
var self = this;
// look up a handler for 'method'
// points: this handler may return a promise, but it also might not
// this handler is run with this = [the SOCKS5]
if(method == this.auth.UNACCEPTABLE) {
//"If the selected METHOD is X'FF', none of the methods listed by the
// client are acceptable, and the client MUST close the connection."
self._stream.close(); //"client MUST close"
//and we error out too, for good measure
throw "SOCKS server rejected all our auth methods."
}
function find_method() {
var s = null;
// .find() didn't work. for(k in self.auth) didn't work.
// maybe things are different under Firefox??
// So I fall back to using a global 's'
Object.keys(self.auth).forEach(function(k) { //XXX this code feels like it probably exists in the stdlib somewhere
if(self.auth[k].charCodeAt(0) == method.charCodeAt(0)) s = k;
})
if(s) return s;
throw "Unknown auth method" //Shouldn't happen but not impossible; a conforming server should only respond with a method in the list we sent
}
var handler = this.authmethods[find_method()]
// Note! handler might here overwrite this._ws here with a further
// wrapper because:
// > If the negotiated method includes encapsulation [...]
// > these requests MUST be encapsulated in the method-
// > dependent encapsulation.
// - <https://tools.ietf.org/html/rfc1928#section-4>
return handler.call(this)
}
SOCKS5WebSocket.prototype.authmethods = {}
SOCKS5WebSocket.prototype.authmethods.NONE = function() {
return;
}
SOCKS5WebSocket.prototype.authmethods.GSSAPI = function() {
throw "NotImplemented"
}
SOCKS5WebSocket.prototype.authmethods.LOGIN = function() {
throw "NotImplemented"
}
// 3) request the actual tunnel
SOCKS5WebSocket.prototype._negotiate_connection = function() {
this._stream.send(this._build_request("CONNECT", this.target)) //hardcoded to "CONNECT"; see the comments near the top
return this._read_reply();
}
SOCKS5WebSocket.prototype._build_request = function(command, address) {
command = command.toUpperCase();
command = this.commands[command]
if(command === undefined) {
throw "Invalid SOCKS command."
}
// XXX for now, address is hardcoded as DOMAINAME
// most DNS resolvers should be able to handle text-formatted IPv4 and IPv6 addresses...
var atype = this.atype.DOMAINNAME
address = split_addr(address)
var host = address[0]
var port = address[1]
// format address as a fortran-style string
if(host.length > 0xFF) {
throw "Target hostname too long to encode."
}
var n = String.fromCharCode(host.length)
host = n + host
// and finally, the port
if(port === null) { //XXX maybe this should be inside of spit_addr
throw "Target port must be specified when using SOCKS5."
}
// "in network octet order"
port = +port; //convert to an integer
port = String.fromCharCode((port & 0xFF00) >> 8) + String.fromCharCode(port & 0xFF)
var m = this.VERSION + command + this.RSV + atype + host + port;
return m
}
SOCKS5WebSocket.prototype._read_reply = function() {
// As a state machine, this process is:
// [ read version ] -> [ read response ] -> [read reserved null byte] -> [error out]
// |-> [error out] |
// v
// [read address type]
// [read ipv4] [read domainname] [readipv6]
// [read port]
//
// Because the message is a fixed size up to reading the address, I avoiding having to kludge
// around this by just saying .recv(4) and then using standard if statements instead of a chain of .recv(1)s
// but because this step comes after the split, it needs to
//the trick here is that promises chain: .then() records the handler you pass it and then returns a new promise which will be fired after that promise completes and finishes the handler
// how do I write branching with promises?
// Promises/A+ makes it easy enough to write a chain of steps and
// get async almost free (the only expense is some repetition: .then().then().then()....)
// MSFT even has an excellent doc on doing this: http://msdn.microsoft.com/en-us/library/windows/apps/Hh700334.aspx
//
// In principle, you should be able to have a promise that represents
// the final result of a branching
// How to express this is escaping me at the moment.
//NB: the non-lint'd indenting is on purpose here!
// The correct indents would distract, because the .then()s
// are basically boilerplate around the real process.
var self = this;
// check the remote server version
return self._stream.recv(1)
.then(function(b) { return self._validate_version(b) })
// parse the response type
.then(function() { return self._stream.recv(1) })
.then(function(b) {
if(b != self.responses.OK) {
throw "SOCKS tunnel refused"
//TODO: give more detailed error message based on what b is
}
})
// check that the 'reserved' byte is actually unused;
// if it's not, we might be not talking to SOCKS
.then(function() { return self._stream.recv(1) }) // NB: Promises/A+ says that you can chain promises: http://promisesaplus.com/#point-49
.then(function(b) {
if(b != self.RSV ) {
throw "Malformed SOCKS reply"
}
})
// determine the length of the next field, which is "bind.addr",
// telling us what our remote host is
.then(function() { return self._stream.recv(1) })
.then(function(b) {
switch(b) {
case self.atype.IPv4: //ipv4: 4 bytes
return self._stream.recv(4).then(function(addr) {
//TODO: parse the bytes into a IP string
self.remote.host = addr;
})
break;
case self.atype.DOMAINNAME: // domain name: a fortran-style string (so we need to read 1 byte to find out the length)
return self._stream.recv(1).then(function(h) {
h = h.charCodeAt(0) //extract the number of bytes to read
self._stream.recv(h).then(function(addr) {
//the string as given is a string
self.remote.host = addr;
})
})
break;
case self.atype.IPv6: //ipv6: 16 bytes
return self._stream.recv(16).then(function(addr) {
//TODO: parse the octets into a string
self.remote.host = addr;
})
break;
default:
throw "Received unknown address type";
}
})
// finally, read the port
.then(function() { return self._stream.recv(2) })
.then(function(b) {
self.remote.port = b.charCodeAt(0) << 8 | b.charCodeAt(1)
})
}
SOCKS5WebSocket.prototype.send = function(d) {
// BEWARE: we send directly to self._ws here, though for cleanliness
// in the init phase we should be using this._stream.
// but, because I *know* private details of WebSocketStream,
// namely that .send() is a simple proxy,
// this will do the correct thing either way.
return this._ws.send(d)
}
SOCKS5WebSocket.prototype.close = function() {
return this._ws.close();
}
// default no-op event handlers so that we needn't worry
// about checking their existence before calling them.
SOCKS5WebSocket.prototype.onopen = function(evt) {}
SOCKS5WebSocket.prototype.onmessage = function(evt) {}
SOCKS5WebSocket.prototype.onclose = function(evt) {}
SOCKS5WebSocket.prototype.onerror = function(evt) {}
// These constants are hardcoded to correspond to their encoding within the protocol
// SOCKS is simple enough that the constants it uses are all single bytes.
SOCKS5WebSocket.prototype.VERSION = String.fromCharCode(5) // i.e. SOCKS version 5
SOCKS5WebSocket.prototype.RSV = String.fromCharCode(0) //RESERVED byte
SOCKS5WebSocket.prototype.auth = {
NONE: String.fromCharCode(0),
GSSAPI: String.fromCharCode(1),
LOGIN: String.fromCharCode(2),
UNACCEPTABLE: String.fromCharCode(0xFF),
// all others are reserved either by IANA or for custom use
// i.e. probably no one uses them(?)
}
SOCKS5WebSocket.prototype.commands = {CONNECT: String.fromCharCode(1),
BIND: String.fromCharCode(2),
UDP: String.fromCharCode(3)}
SOCKS5WebSocket.prototype.atype = {
IPv4: String.fromCharCode(1),
DOMAINNAME: String.fromCharCode(3),
IPv6: String.fromCharCode(4)
}
SOCKS5WebSocket.prototype.responses = {OK: String.fromCharCode(0),
//TODO: there's 8 possible errors
}
return SOCKS5;
}));