Merge branch 'master' of github.com:prawnsalad/KiwiIRC
[KiwiIRC.git] / server / socks.js
1 var stream = require('stream'),
2 util = require('util'),
3 net = require('net'),
4 tls = require('tls'),
5 _ = require('lodash'),
6 ipaddr = require('ipaddr.js');
7
8 var SocksConnection = function (remote_options, socks_options) {
9 var that = this;
10 stream.Duplex.call(this);
11
12 this.remote_options = _.defaults(remote_options, {
13 host: 'localhost',
14 ssl: false,
15 rejectUnauthorized: false
16 });
17 socks_options = _.defaults(socks_options, {
18 host: 'localhost',
19 port: 1080,
20 user: null,
21 pass: null
22 });
23
24 this.socksAddress = null;
25 this.socksPort = null;
26
27 this.socksSocket = net.connect({host: socks_options.host, port: socks_options.port}, socksConnected.bind(this, !(!socks_options.user)));
28 this.socksSocket.once('data', socksAuth.bind(this, {user: socks_options.user, pass: socks_options.pass}));
29 this.socksSocket.on('error', function (err) {
30 that.emit('error', err);
31 });
32
33 this.outSocket = this.socksSocket;
34 };
35
36 util.inherits(SocksConnection, stream.Duplex);
37
38 SocksConnection.connect = function (remote_options, socks_options, connection_listener) {
39 var socks_connection = new SocksConnection(remote_options, socks_options);
40 if (typeof connection_listener === 'Function') {
41 socks_connection.on('connect', connection_listener);
42 }
43 return socks_connection;
44 };
45
46 SocksConnection.prototype._read = function () {
47 this.outSocket.resume();
48 };
49
50 SocksConnection.prototype._write = function (chunk, encoding, callback) {
51 this.outSocket.write(chunk, 'utf8', callback);
52 };
53
54 SocksConnection.prototype.dispose = function () {
55 this.outSocket.destroy();
56 this.outSocket.removeAllListeners();
57 if (this.outSocket !== this.socksSocket) {
58 this.socksSocket.destroy();
59 this.socksSocket.removeAllListeners();
60 }
61 this.removeAllListeners();
62 };
63
64 var socksConnected = function (auth) {
65 if (auth) {
66 this.socksSocket.write('\x05\x02\x02\x00'); // SOCKS version 5, supporting two auth methods
67 // username/password and 'no authentication'
68 } else {
69 this.socksSocket.write('\x05\x01\x00'); // SOCKS version 5, only supporting 'no auth' scheme
70 }
71 };
72
73 var socksAuth = function (auth, data) {
74 var bufs = [];
75 switch (data.readUInt8(1)) {
76 case 255:
77 this.emit('error', 'SOCKS: No acceptable authentication methods');
78 this.socksSocket.destroy();
79 break;
80 case 2:
81 bufs[0] = new Buffer([1]);
82 bufs[1] = new Buffer([Buffer.byteLength(auth.user)]);
83 bufs[2] = new Buffer(auth.user);
84 bufs[3] = new Buffer([Buffer.byteLength(auth.pass)]);
85 bufs[4] = new Buffer(auth.pass);
86 this.socksSocket.write(Buffer.concat(bufs));
87 this.socksSocket.once('data', socksAuthStatus.bind(this));
88 break;
89 default:
90 socksRequest.call(this, this.remote_options.host, this.remote_options.port);
91 }
92 };
93
94 var socksAuthStatus = function (data) {
95 if (data.readUInt8(1) === 1) {
96 socksRequest.call(this, this.remote_options.host, this.remote_options.port);
97 } else {
98 this.emit('error', 'SOCKS: Authentication failed');
99 this.socksSocket.destroy();
100 }
101 };
102
103 var socksRequest = function (host, port) {
104 var header, type, hostBuf, portBuf;
105 if (net.isIP(host)) {
106 if (net.isIPv4(host)) {
107 type = new Buffer([1]);
108 } else if (net.isIPv6(host)) {
109 type = new Buffer([4]);
110 }
111 host = new Buffer(ipaddr.parse(host).toByteArray());
112 } else {
113 type = new Buffer([3]);
114 hostBuf = new Buffer(host);
115 hostBuf = Buffer.concat([new Buffer([Buffer.byteLength(host)]), hostBuf]);
116 }
117 header = new Buffer([5, 1, 0]);
118 portBuf = new Buffer(2);
119 portBuf.writeUInt16BE(port, 0);
120 this.socksSocket.write(Buffer.concat([header, type, hostBuf, portBuf]));
121 this.socksSocket.once('data', socksReply.bind(this));
122 };
123
124 var socksReply = function (data) {
125 var err, port, i, addr_len, addr = '';
126 var status = data.readUInt8(1);
127 if (status === 0) {
128 switch (data.readUInt8(3)) {
129 case 1:
130 for (i = 0; i < 4; i++) {
131 if (i !== 0) {
132 addr += '.';
133 }
134 addr += data.readUInt8(4 + i);
135 }
136 port = data.readUInt16BE(8);
137 break;
138 case 4:
139 for (i = 0; i < 16; i++) {
140 if (i !== 0) {
141 addr += ':';
142 }
143 addr += data.readUInt8(4 + i);
144 }
145 port = data.readUInt16BE(20);
146 break;
147 case 3:
148 addr_len = data.readUInt8(4);
149 addr = (data.slice(5, 5 + addr_len)).toString();
150 port = data.readUInt16BE(5 + addr_len);
151 }
152 this.socksAddress = addr;
153 this.socksPort = port;
154
155 if (this.remote_options.ssl) {
156 startTLS.call(this);
157 } else {
158 proxyData.call(this);
159 this.emit('connect');
160 }
161
162 } else {
163 switch (status) {
164 case 1:
165 err = 'SOCKS: general SOCKS server failure';
166 break;
167 case 2:
168 err = 'SOCKS: Connection not allowed by ruleset';
169 break;
170 case 3:
171 err = 'SOCKS: Network unreachable';
172 break;
173 case 4:
174 err = 'SOCKS: Host unreachable';
175 break;
176 case 5:
177 err = 'SOCKS: Connection refused';
178 break;
179 case 6:
180 err = 'SOCKS: TTL expired';
181 break;
182 case 7:
183 err = 'SOCKS: Command not supported';
184 break;
185 case 8:
186 err = 'SOCKS: Address type not supported';
187 break;
188 default:
189 err = 'SOCKS: Unknown error';
190 }
191 this.emit('error', err);
192 }
193 };
194
195 var startTLS = function () {
196 var that = this;
197 var plaintext = tls.connect({
198 socket: this.socksSocket,
199 rejectUnauthorized: this.remote_options.rejectUnauthorized
200 });
201
202 plaintext.on('error', function (err) {
203 that.emit('error', err);
204 });
205
206 plaintext.on('secureConnect', function () {
207 that.emit('connect');
208 });
209 this.outSocket = plaintext;
210 proxyData.call(this);
211 };
212
213 var proxyData = function () {
214 var that = this;
215
216 this.outSocket.on('data', function (data) {
217 var buffer_not_full = that.push(data);
218 if (!buffer_not_full) {
219 this.pause();
220 }
221 });
222
223 this.outSocket.on('end', function () {
224 that.push(null);
225 });
226 };
227
228 module.exports = SocksConnection;