Compare commits

...

27 Commits

Author SHA1 Message Date
James Coglan 1120828003 Bump version to 0.8.1. 2014-11-12 19:35:37 +00:00
James Coglan 59911312ac Merge pull request #33 from meteor/specify-servername
Fix proxy connections to non-localhost SSL origins
2014-11-12 19:27:41 +00:00
David Glasser 98298acb36 Fix proxy connections to non-localhost SSL origins
The socket form of tls.connect needs to be told the server name it is
connecting to in order to validate the server certificate (and to
perform SNI). Without this change, just about any attempt to proxy to an
SSL origin will fail.

This name defaults to 'localhost':
  https://github.com/joyent/node/blob/v0.10/lib/tls.js#L1349
so an attempt to proxy to an SSL origin named 'localhost' would have
succeeded, which is why tests passed.
2014-11-10 16:47:56 -08:00
James Coglan 89a9b0d9b8 Update the release date for 0.8.0. 2014-11-08 19:47:25 +00:00
James Coglan 0e00ea5107 Fix the off-by-one error in the Autobahn client's progress bar. 2014-11-08 19:45:47 +00:00
James Coglan d471a6a707 Don't test on v0.6, its tls.connect({socket}) doesn't seem to work. 2014-11-08 15:40:12 +00:00
James Coglan 5dc1d7f682 Migrate back to the driver API from https://github.com/faye/websocket-driver-node/commit/0bb47c17b467980293a9f010de485722c4298b43, and perform all necessary TLS in this module. 2014-11-08 15:34:02 +00:00
James Coglan dfe361a18b Adjust formatting of example log messages. 2014-11-08 12:47:12 +00:00
James Coglan 140171307e Make ProxyServer work on Node 0.6. 2014-11-07 19:25:06 +00:00
James Coglan 6c5ef1c884 Load websocket-driver from git. 2014-11-07 08:57:51 +00:00
James Coglan 7ffe738208 Use an http: endpoint for the proxy example. 2014-11-06 23:35:45 +00:00
James Coglan 2db4ef20ff Pass in the client certificate for navigating https: proxies in the client example, and log the handshake headers. 2014-11-06 23:05:30 +00:00
James Coglan 54cd6f5831 Accientally left part of the proxy tests commented out. 2014-11-06 23:04:56 +00:00
James Coglan 35d9e6998e Log the request headers in the example servers, and log all I/O through the proxy server. 2014-11-06 09:01:39 +00:00
James Coglan e649b62396 Replace the old client example with echo_client.js. 2014-11-06 08:46:44 +00:00
James Coglan d0ec17d6a3 Migrate to the new simplified websocket-driver proxy API, and add integration tests for ws: and wss: sockets via an http: proxy. 2014-11-05 23:36:00 +00:00
James Coglan 1d3878e3ed Remove the stream.resume() call from Client, not needed now that Proxy emits 'close'. 2014-10-30 00:36:45 +00:00
James Coglan 97dfaf71cf Replace an empty on('data') hook with resume(). 2014-10-29 23:09:20 +00:00
James Coglan 723f4ce6a7 Migrate proxy support onto the API introduced in https://github.com/faye/websocket-driver-node/commit/76f41286342e517e7ff562495b249a7fa103bbe9. 2014-10-29 22:26:22 +00:00
James Coglan 97b7342538 [wip] Use the proxy-connect event from the driver to perform a TLS handshake when using wss: via a proxy. 2014-10-29 19:28:55 +00:00
James Coglan 9d8e5384b7 Use 'tls' not 'ssl' to indicate a secure server in the examples. 2014-10-29 19:28:18 +00:00
James Coglan 42ea7fb0a5 Adjust the UA string used for the Autobahn client. 2014-10-29 08:52:10 +00:00
James Coglan 03acb154e2 Set readyState to CLOSED as soon as finalize() is called to prevent reentrant calls from events this method emits. 2014-10-28 09:51:31 +00:00
James Coglan b40fc25444 Fix secure connection via proxies; the client was hard-coded to only treat wss:, not https:, as a secure protocol. 2014-10-27 22:43:38 +00:00
James Coglan 05cc735582 Add support for connecting via an HTTP proxy and bump version to 0.8.0. 2014-10-27 09:39:40 +00:00
James Coglan 29d07c5002 Close the socket if close() is called when we're still waiting for a handshake. 2014-10-26 13:04:06 +00:00
James Coglan 2118283941 Bump the copyright year. 2014-10-04 08:51:29 +01:00
14 changed files with 314 additions and 120 deletions
-4
View File
@@ -1,10 +1,6 @@
language: node_js
node_js:
- "0.6"
- "0.8"
- "0.10"
- "0.11"
before_install:
- '[ "${TRAVIS_NODE_VERSION}" = "0.6" ] && npm conf set strict-ssl false || true'
+11
View File
@@ -1,3 +1,14 @@
### 0.8.1 / 2014-11-12
* Send the correct hostname when upgrading a connection to TLS
### 0.8.0 / 2014-11-08
* Support connections via HTTP proxies
* Close the connection cleanly if we're still waiting for a handshake response
### 0.7.3 / 2014-10-04
* Allow sockets to be closed when they are in any state other than `CLOSED`
+26 -1
View File
@@ -117,6 +117,23 @@ ws.on('close', function(event) {
The WebSocket client also lets you inspect the status and headers of the
handshake response via its `statusCode` and `headers` properties.
To connect via a proxy, set the `proxy` option to the HTTP origin of the proxy,
including any authorization information, custom headers and TLS config you
require. Only the `origin` setting is required.
```js
var ws = new WebSocket.Client('ws://www.example.com/', null, {
proxy: {
origin: 'http://username:password@proxy.example.com',
headers: {'User-Agent': 'node'},
tls: {cert: fs.readFileSync('client.crt')}
}
});
```
The `tls` value is a Node 'TLS options' object that will be passed to
[`tls.connect()`](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
## Subprotocol negotiation
@@ -160,6 +177,14 @@ var ws = new WebSocket.Client(url, protocols, options);
* `ping` - an integer that sets how often the WebSocket should send ping
frames, measured in seconds
The client accepts some additional options:
* `proxy` - settings for a proxy as described above
* `tls` - a Node 'TLS options' object containing TLS settings for the origin
server, this will be passed to
[`tls.connect()`](http://nodejs.org/api/tls.html#tls_tls_connect_options_callback)
* `ca` - (legacy) a shorthand for passing `{tls: {ca: value}}`
## WebSocket API
@@ -273,7 +298,7 @@ some data over the wire to keep the connection alive.
(The MIT License)
Copyright (c) 2010-2013 James Coglan
Copyright (c) 2010-2014 James Coglan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the 'Software'), to deal in
+5 -4
View File
@@ -2,7 +2,7 @@ var WebSocket = require('../lib/faye/websocket'),
pace = require('pace');
var host = 'ws://localhost:9001',
agent = 'Node ' + process.version,
agent = 'node-' + process.version,
cases = 0,
skip = [];
@@ -17,13 +17,14 @@ socket.onmessage = function(event) {
socket.onclose = function() {
var runCase = function(n) {
progress.op();
if (n > cases) {
socket = new WebSocket.Client(host + '/updateReports?agent=' + encodeURIComponent(agent));
socket.onclose = process.exit;
return;
}
progress.op();
} else if (skip.indexOf(n) >= 0) {
if (skip.indexOf(n) >= 0) {
runCase(n + 1);
} else {
+17 -15
View File
@@ -1,23 +1,25 @@
var WebSocket = require('../lib/faye/websocket'),
port = process.argv[2] || 7000,
secure = process.argv[3] === 'ssl',
scheme = secure ? 'wss' : 'ws',
url = scheme + '://localhost:' + port + '/',
headers = {Origin: 'http://faye.jcoglan.com'},
ws = new WebSocket.Client(url, null, {headers: headers});
fs = require('fs');
console.log('Connecting to ' + ws.url);
var url = process.argv[2],
headers = {Origin: 'http://faye.jcoglan.com'},
ca = fs.readFileSync(__dirname + '/../spec/server.crt'),
proxy = {origin: process.argv[3], headers: {'User-Agent': 'Echo'}, tls: {ca: ca}},
ws = new WebSocket.Client(url, null, {headers: headers, proxy: proxy, tls: {ca: ca}});
ws.onopen = function(event) {
console.log('open');
ws.send('Hello, WebSocket!');
ws.onopen = function() {
console.log('[open]', ws.headers);
ws.send('mic check');
};
ws.onmessage = function(event) {
console.log('message', event.data);
// ws.close(1002, 'Going away');
ws.onclose = function(close) {
console.log('[close]', close.code, close.reason);
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
ws.onerror = function(error) {
console.log('[error]', error.message);
};
ws.onmessage = function(message) {
console.log('[message]', message.data);
};
+7
View File
@@ -0,0 +1,7 @@
var ProxyServer = require('../spec/proxy_server');
var port = process.argv[2],
secure = process.argv[3] === 'tls',
proxy = new ProxyServer({debug: true, tls: secure});
proxy.listen(port);
+5 -5
View File
@@ -4,16 +4,16 @@ var WebSocket = require('../lib/faye/websocket'),
https = require('https');
var port = process.argv[2] || 7000,
secure = process.argv[3] === 'ssl';
secure = process.argv[3] === 'tls';
var upgradeHandler = function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ['irc', 'xmpp'], {ping: 5});
console.log('open', ws.url, ws.version, ws.protocol);
console.log('[open]', ws.url, ws.version, ws.protocol, request.headers);
ws.pipe(ws);
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
console.log('[close]', event.code, event.reason);
ws = null;
};
};
@@ -25,7 +25,7 @@ var requestHandler = function(request, response) {
var es = new WebSocket.EventSource(request, response),
time = parseInt(es.lastEventId, 10) || 0;
console.log('open', es.url, es.lastEventId);
console.log('[open]', es.url, es.lastEventId);
var loop = setInterval(function() {
time += 1;
@@ -39,7 +39,7 @@ var requestHandler = function(request, response) {
es.onclose = function() {
clearInterval(loop);
console.log('close', es.url);
console.log('[close]', es.url);
es = null;
};
};
+30 -18
View File
@@ -23,20 +23,6 @@ var API = function(options) {
var self = this;
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
['close', 'end'].forEach(function(event) {
this._stream.on(event, function() { self._finalize('', 1006) });
}, this);
this._stream.on('error', function(error) {
var event = new Event('error', {message: 'Network error: ' + self.url + ': ' + error.message});
event.initEvent('error', false, false);
self.dispatchEvent(event);
self._finalize('', 1006);
});
this._driver.on('open', function(e) { self._open() });
this._driver.on('message', function(e) { self._receiveMessage(e.data) });
this._driver.on('close', function(e) { self._finalize(e.reason, e.code) });
@@ -45,6 +31,7 @@ var API = function(options) {
var event = new Event('error', {message: error.message});
event.initEvent('error', false, false);
self.dispatchEvent(event);
self._finalize('', 1006);
});
this.on('error', function() {});
@@ -58,8 +45,12 @@ var API = function(options) {
self.ping(self._pingId.toString());
}, this._ping * 1000);
this._stream.pipe(this._driver.io);
this._driver.io.pipe(this._stream);
this._configureStream();
if (!this._proxy) {
this._stream.pipe(this._driver.io);
this._driver.io.pipe(this._stream);
}
};
util.inherits(API, Stream);
@@ -102,6 +93,21 @@ var instance = {
this._driver.close();
},
_configureStream: function() {
var self = this;
this._stream.setTimeout(0);
this._stream.setNoDelay(true);
['close', 'end'].forEach(function(event) {
this._stream.on(event, function() { self._finalize('', 1006) });
}, this);
this._stream.on('error', function(error) {
self._driver.emit('error', new Error('Network error: ' + self.url + ': ' + error.message));
});
},
_open: function() {
if (this.readyState !== API.CONNECTING) return;
@@ -125,14 +131,20 @@ var instance = {
_finalize: function(reason, code) {
if (this.readyState === API.CLOSED) return;
this.readyState = API.CLOSED;
if (this._pingTimer) clearInterval(this._pingTimer);
if (this._stream) this._stream.end();
var stream = this._stream;
if (stream) {
if (stream.unpipe) stream.unpipe(this._driver.io);
stream.on('data', function() { stream.destroy() });
stream.end();
}
if (this.readable) this.emit('end');
this.readable = this.writable = false;
this.readyState = API.CLOSED;
var event = new Event('close', {code: code || 1000, reason: reason || ''});
event.initEvent('close', false, false);
this.dispatchEvent(event);
+58 -14
View File
@@ -1,16 +1,20 @@
var util = require('util'),
net = require('net'),
tls = require('tls'),
crypto = require('crypto'),
url = require('url'),
driver = require('websocket-driver'),
API = require('./api'),
Event = require('./api/event');
var Client = function(url, protocols, options) {
var DEFAULT_PORTS = {'http:': 80, 'https:': 443, 'ws:':80, 'wss:': 443},
SECURE_PROTOCOLS = ['https:', 'wss:'];
var Client = function(_url, protocols, options) {
options = options || {};
this.url = url;
this._uri = require('url').parse(url);
this._driver = driver.client(url, {maxLength: options.maxLength, protocols: protocols});
this.url = _url;
this._driver = driver.client(this.url, {maxLength: options.maxLength, protocols: protocols});
['open', 'error'].forEach(function(event) {
this._driver.on(event, function() {
@@ -19,22 +23,62 @@ var Client = function(url, protocols, options) {
});
}, this);
var secure = (this._uri.protocol === 'wss:'),
onConnect = function() { self._driver.start() },
tlsOptions = {},
self = this;
var proxy = options.proxy || {},
endpoint = url.parse(proxy.origin || this.url),
port = endpoint.port || DEFAULT_PORTS[endpoint.protocol],
secure = SECURE_PROTOCOLS.indexOf(endpoint.protocol) >= 0,
onConnect = function() { self._onConnect() },
originTLS = options.tls || {},
socketTLS = proxy.origin ? (proxy.tls || {}) : originTLS,
self = this;
if (options.ca) tlsOptions.ca = options.ca;
originTLS.ca = originTLS.ca || options.ca;
var connection = secure
? tls.connect(this._uri.port || 443, this._uri.hostname, tlsOptions, onConnect)
: net.createConnection(this._uri.port || 80, this._uri.hostname);
this._stream = secure
? tls.connect(port, endpoint.hostname, socketTLS, onConnect)
: net.connect(port, endpoint.hostname, onConnect);
this._stream = connection;
if (!secure) this._stream.on('connect', onConnect);
if (proxy.origin) this._configureProxy(proxy, originTLS);
API.call(this, options);
};
util.inherits(Client, API);
Client.prototype._onConnect = function() {
var worker = this._proxy || this._driver;
worker.start();
};
Client.prototype._configureProxy = function(proxy, originTLS) {
var uri = url.parse(this.url),
secure = SECURE_PROTOCOLS.indexOf(uri.protocol) >= 0,
self = this,
name;
this._proxy = this._driver.proxy(proxy.origin);
if (proxy.headers) {
for (name in proxy.headers) this._proxy.setHeader(name, proxy.headers[name]);
}
this._proxy.pipe(this._stream, {end: false});
this._stream.pipe(this._proxy);
this._proxy.on('connect', function() {
if (secure) {
var options = {socket: self._stream, servername: uri.hostname};
for (name in originTLS) options[name] = originTLS[name];
self._stream = tls.connect(options);
self._configureStream();
}
self._driver.io.pipe(self._stream);
self._stream.pipe(self._driver.io);
self._driver.start();
});
this._proxy.on('error', function(error) {
self._driver.emit('error', error);
});
};
module.exports = Client;
+2 -2
View File
@@ -5,10 +5,10 @@
, "keywords" : ["websocket", "eventsource"]
, "license" : "MIT"
, "version" : "0.7.3"
, "version" : "0.8.1"
, "engines" : {"node": ">=0.4.0"}
, "main" : "./lib/faye/websocket"
, "dependencies" : {"websocket-driver": ">=0.3.6"}
, "dependencies" : {"websocket-driver": ">=0.4.0"}
, "devDependencies" : {"jstest": "", "pace": ""}
, "scripts" : {"test": "jstest spec/runner.js"}
+31
View File
@@ -0,0 +1,31 @@
var fs = require('fs'),
http = require('http'),
https = require('https');
var WebSocket = require('../lib/faye/websocket');
var EchoServer = function(secure) {
var server = secure
? https.createServer({
key: fs.readFileSync(__dirname + '/server.key'),
cert: fs.readFileSync(__dirname + '/server.crt')
})
: http.createServer();
server.on('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ["echo"]);
ws.pipe(ws);
});
this._httpServer = server;
};
EchoServer.prototype.listen = function(port) {
this._httpServer.listen(port);
};
EchoServer.prototype.stop = function() {
this._httpServer.close();
};
module.exports = EchoServer;
+74 -26
View File
@@ -1,17 +1,29 @@
var Client = require('../../../lib/faye/websocket/client'),
test = require('jstest').Test,
fs = require('fs')
var Client = require('../../../lib/faye/websocket/client'),
EchoServer = require('../../echo_server'),
ProxyServer = require('../../proxy_server'),
test = require('jstest').Test,
fs = require('fs')
var WebSocketSteps = test.asyncSteps({
server: function(port, secure, callback) {
this._adapter = new EchoServer()
this._adapter.listen(port, secure)
this._port = port
this._echoServer = new EchoServer(secure)
this._echoServer.listen(port)
process.nextTick(callback)
},
stop: function(callback) {
this._adapter.stop()
this._echoServer.stop()
process.nextTick(callback)
},
proxy: function(port, secure, callback) {
this._proxyServer = new ProxyServer({tls: secure})
this._proxyServer.listen(port)
process.nextTick(callback)
},
stop_proxy: function(callback) {
this._proxyServer.stop()
process.nextTick(callback)
},
@@ -19,6 +31,8 @@ var WebSocketSteps = test.asyncSteps({
var done = false,
self = this,
tlsOptions = { ca: fs.readFileSync(__dirname + '/../../server.crt') },
resume = function(open) {
if (done) return
done = true
@@ -27,7 +41,8 @@ var WebSocketSteps = test.asyncSteps({
}
this._ws = new Client(url, protocols, {
ca: fs.readFileSync(__dirname + '/../../server.crt')
proxy: { origin: this.proxy_url, tls: tlsOptions },
tls: tlsOptions
})
this._ws.onopen = function() { resume(true) }
@@ -125,10 +140,15 @@ test.describe("Client", function() { with(this) {
include(WebSocketSteps)
before(function() {
this.protocols = ["foo", "echo"]
this.plain_text_url = "ws://localhost:4180/bayeux"
this.secure_url = "wss://localhost:4180/bayeux"
this.port = 4180
this.protocols = ["foo", "echo"]
this.plain_text_url = "ws://localhost:4180/bayeux"
this.secure_url = "wss://localhost:4180/bayeux"
this.port = 4180
this.plain_text_proxy_url = "http://localhost:4181"
this.secure_text_proxy_url = "https://localhost:4181"
this.proxy_port = 4181
})
sharedBehavior("socket client", function() { with(this) {
@@ -198,27 +218,55 @@ test.describe("Client", function() { with(this) {
}})
}})
describe("with a plain-text server", function() { with(this) {
before(function() {
this.socket_url = this.plain_text_url
this.blocked_url = this.secure_url
})
sharedBehavior("socket server", function() { with(this) {
describe("with a plain-text server", function() { with(this) {
before(function() {
this.socket_url = this.plain_text_url
this.blocked_url = this.secure_url
})
before(function() { this.server(4180, false) })
after (function() { this.stop() })
before(function() { this.server(this.port, false) })
after (function() { this.stop() })
behavesLike("socket client")
behavesLike("socket client")
}})
describe("with a secure server", function() { with(this) {
before(function() {
this.socket_url = this.secure_url
this.blocked_url = this.plain_text_url
})
before(function() { this.server(this.port, true) })
after (function() { this.stop() })
behavesLike("socket client")
}})
}})
describe("with a secure server", function() { with(this) {
describe("with no proxy", function() { with(this) {
behavesLike("socket server")
}})
describe("with a proxy", function() { with(this) {
before(function() {
this.socket_url = this.secure_url
this.blocked_url = this.plain_text_url
this.proxy_url = this.plain_text_proxy_url
})
before(function() { this.server(4180, true) })
after (function() { this.stop() })
before(function() { this.proxy(this.proxy_port, false) })
after (function() { this.stop_proxy() })
behavesLike("socket client")
behavesLike("socket server")
}})
describe("with a secure proxy", function() { with(this) {
before(function() {
this.proxy_url = this.secure_text_proxy_url
})
before(function() { this.proxy(this.proxy_port, true) })
after (function() { this.stop_proxy() })
behavesLike("socket server")
}})
}})
+48
View File
@@ -0,0 +1,48 @@
var fs = require('fs'),
http = require('http'),
https = require('https'),
net = require('net');
var ProxyServer = function(options) {
var proxy = options.tls
? https.createServer({
key: fs.readFileSync(__dirname + '/server.key'),
cert: fs.readFileSync(__dirname + '/server.crt')
})
: http.createServer();
options = options || {};
var onConnect = function(request, frontend, body) {
var parts = request.url.split(':'),
backend = net.connect(parts[1], parts[0]);
frontend.pipe(backend);
backend.pipe(frontend);
backend.on('connect', function() {
frontend.write('HTTP/1.1 200 OK\r\n\r\n');
});
if (!options.debug) return;
console.log(request.method, request.url, request.headers);
frontend.on('data', function(data) { console.log('I', data) });
backend.on( 'data', function(data) { console.log('O', data) });
};
proxy.on('connect', onConnect);
proxy.on('upgrade', onConnect);
this._proxy = proxy;
};
ProxyServer.prototype.listen = function(port) {
this._proxy.listen(port);
};
ProxyServer.prototype.stop = function() {
this._proxy.close();
};
module.exports = ProxyServer;
-31
View File
@@ -1,32 +1 @@
var WebSocket = require('../lib/faye/websocket'),
fs = require('fs'),
http = require('http'),
https = require('https'),
test = require('jstest').Test
EchoServer = function() {}
EchoServer.prototype.listen = function(port, ssl) {
var server = ssl
? https.createServer({
key: fs.readFileSync(__dirname + '/server.key'),
cert: fs.readFileSync(__dirname + '/server.crt')
})
: http.createServer()
server.on('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ["echo"])
ws.pipe(ws)
})
this._httpServer = server
server.listen(port)
}
EchoServer.prototype.stop = function(callback, scope) {
this._httpServer.on('close', function() {
if (callback) callback.call(scope);
});
this._httpServer.close();
}
require('./faye/websocket/client_spec')