Skip to content

Commit

Permalink
add WebSocket over TLS/SSL support
Browse files Browse the repository at this point in the history
  • Loading branch information
abbshr committed Sep 4, 2014
1 parent d06cfbc commit b39fe99
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 106 deletions.
2 changes: 1 addition & 1 deletion lib/browser/wsf.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
throw new Error('URL is needed');
} else if (typeOf(ws_url) != 'string') {
throw new Error('Unknow ws URL pattern');
} else if (!ws_url.match(/^(ws:\/\/)|(wss:\/\/)/)) {
} else if (!ws_url.match(/^(ws|wss):\/\//)) {
throw new Error('Unknow ws URL pattern');
} else {
socket.sysEmit = socket.emit;
Expand Down
113 changes: 9 additions & 104 deletions lib/fslider.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

var http = require('http');
var net = require('net');
var crypto = require('crypto');
var EventEmitter = require('events').EventEmitter;

Expand All @@ -9,8 +7,6 @@ var utils = require('./utils'),
upgrade_router = require('./handlers'),
datarecv_handler = require('./handlers/datarecv_handler.js');

var MAGIC_STRING = utils.MAGIC_STRING;

// inherint from EventEmitter
var wsf = module.exports = Object.create(EventEmitter.prototype);

Expand Down Expand Up @@ -49,8 +45,8 @@ wsf.listen = function (httpServer, callback) {
utils.coolLogo();
callback(httpServer);
});
if (!httpServer || !(httpServer instanceof http.Server))
return new Error('no http server init');
if (!httpServer || !utils.isHttpServer(httpServer))
return new Error('no http(s) server init');

// add this http server to stack
wsf._httpd.push(httpServer);
Expand All @@ -64,7 +60,7 @@ wsf.listen = function (httpServer, callback) {
/* close listening 'upgrade' on httpServer */
wsf.close = function (httpServer, callback) {
var stack = this._httpd;
if (!(httpServer instanceof http.Server))
if (!utils.isHttpServer(httpServer))
return false;
this.on('close', function (httpServer) {
typeof callback == 'function' && callback(httpServer);
Expand Down Expand Up @@ -99,7 +95,7 @@ wsf.close = function (httpServer, callback) {
// as a Server

/*
* @server: http.Server instance
* @server: http(s).Server instance
* @options: {
* max: max counts of clients can connect to wsf server
* namespace: URL that could request handshake
Expand All @@ -108,7 +104,7 @@ wsf.close = function (httpServer, callback) {
wsf.Server = function (server, options) {
if (!(this instanceof wsf.Server))
return new wsf.Server(server, options);
if (typeof server == 'object' && !(server instanceof http.Server))
if (typeof server == 'object' && !utils.isHttpServer(server))
options = server;
options = options || {};
options.max = options.max || 60;
Expand Down Expand Up @@ -165,7 +161,7 @@ wsf.Server.prototype.emit = function (client, event, data) {
*/
wsf.Server.prototype.bind = function (server, options) {
if (server) {
if (!(server instanceof http.Server))
if (!utils.isHttpServer(server))
return new Error('argument "server" must be a http.Server instance!');
if (server === this.httpServer)
return;
Expand All @@ -182,7 +178,7 @@ wsf.Server.prototype.bind = function (server, options) {
* des: oppsite to #bind() method
*/
wsf.Server.prototype.unbind = function () {
if (this.httpServer && this.httpServer instanceof http.Server)
if (this.httpServer && utils.isHttpServer(this.httpServer))
this.httpServer = null, this.namespace = '/';
};

Expand All @@ -199,96 +195,5 @@ wsf.Server.prototype.broadcast = function (e, data) {
});
};

// as a Client
wsf.connect = function (url, callback) {

wsf.on('connected', callback);

var urlpattern = /^ws:\/\/([^\s\/:]+)(:(\d{2,5}))?(\/\S*)?$/;
var result = urlpattern.exec(url);
if (!result)
throw new Error('unexpected url pattern');
var dsthost = result[1],
dstport = result[3] || 80,
dstpath = result[4] || '/';

// the key is a 16 bytes' random str
var key = crypto.randomBytes(16).toString('base64');

var socket = net.connect({
host: dsthost,
port: dstport
}, function () {
/* ws protocol handshake request head */
var reqHeaders = ([
'GET ' + dstpath + ' HTTP/1.1',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Key: ' + key
]).concat('', '').join('\r\n');

socket.write(reqHeaders) || socket.pause();
});

var client = new Client(socket);

// cache recive-buffer, as ref
var cache = {
frame: new Buffer(0),
fragmentCache: new Buffer(0)
};

socket
.once('data', function (data) {
var resHeaders = data.toString();
var secKey = crypto.createHash('sha1')
.update(key + utils.MAGIC_STRING)
.digest('base64');
var acKey = resHeaders.match(/Sec-WebSocket-Accept:\s*(\S+)(\r\n)+/)[1];

// first verify the accept key
if (secKey !== acKey)
return utils.error('connection closed \
because an unexpected handshake frame');

// TODO: other values' verify

wsf.emit('connected', client);
// if everything is ok, successfully end handshake progress
socket.on('data', datarecv_handler.bind(global, client, cache));
})
.on('drain', function () {
client.sysEmit('drained', socket.bufferSize);
utils.log('user space buffer-queue has been drained');
socket.resume();
})
// server TCP socket ready to close
.on('end', function () {
client.sysEmit('serverclose');
utils.log('client TCP socket ready to close');
// on reciving a FIN Packet from server, response a FIN
socket.end();
})
// client TCP socket ready to close
.on('finish', function () {
client.sysEmit('clientclose');
utils.log('server TCP socket ready to close');
})
// underlying TCP socket has been closed
.on('close', function (has_error) {
client.sysEmit('disconnected');
utils.log('TCP connection closed');
if (has_error)
utils.error(new Error('some problems happened during TCP connection closing'));
})
.on('error', function (err) {
client.sysEmit('exception', err);
socket.destory();
utils.error(err);
})

// there's no timeout on client
.setTimeout(0);

return client;
};
// as a non-browser client
wsf.connect = require('./non-browser.js');
4 changes: 3 additions & 1 deletion lib/handlers/datarecv_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ var utils = require('../utils');
/* this has been binded to Server instance */
/* arguments @client and @cache has been pre-setted */
module.exports = function (client, cache, data) {
var server = (this === global) ? null : this;
// if this module is used by non-browser client,
// server is a null object
var server = this || null;

var readable_data, payload_data, event, rawdata, head_len;
var FIN, Opcode, MASK, Payload_len;
Expand Down
110 changes: 110 additions & 0 deletions lib/non-browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
var crypto = require('crypto');
var utils = require('./utils');
var Client = require('./client.js');
var datarecv_handler = require('./handlers/datarecv_handler.js');

module.exports = function connect(url, options, callback) {
var wsf = this;
wsf.on('connected', callback);

var urlpattern = /^(ws|wss):\/\/([^\s\/:]+)(:(\d{2,5}))?(\/\S*)?$/;
var result = urlpattern.exec(url);
if (!result)
throw new Error('Bad url pattern');

var protocol = result[1];
var wsclient = null;
if (protocol == 'ws'){
wsclient = require('net');
callback = options instanceof Function ? options : callback;
options = {};
} else {
wsclient = require('tls');
if (options instanceof Function)
throw new Error('CA Cert are required');
}

var dsthost = result[2],
dstport = result[4] || 80,
dstpath = result[5] || '/';

options.host = dsthost;
options.port = dstport;

// the key is a 16 bytes' random str
var key = crypto.randomBytes(16).toString('base64');

var socket = wsclient.connect(options, function () {
/* ws protocol handshake request head */
var reqHeaders = ([
'GET ' + dstpath + ' HTTP/1.1',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Key: ' + key
]).concat('', '').join('\r\n');

socket.write(reqHeaders) || socket.pause();
});

var client = new Client(socket);

// cache recive-buffer, as ref
var cache = {
frame: new Buffer(0),
fragmentCache: new Buffer(0)
};

socket
.once('data', function (data) {
var resHeaders = data.toString();
var secKey = crypto.createHash('sha1')
.update(key + utils.MAGIC_STRING)
.digest('base64');
var acKey = resHeaders.match(/Sec-WebSocket-Accept:\s*(\S+)(\r\n)+/)[1];

// first verify the accept key
if (secKey !== acKey)
return utils.error('connection closed \
because an unexpected handshake frame');

// TODO: other values' verify

wsf.emit('connected', client);
// if everything is ok, successfully end handshake progress
socket.on('data', datarecv_handler.bind(null, client, cache));
})
.on('drain', function () {
client.sysEmit('drained', socket.bufferSize);
utils.log('user space buffer-queue has been drained');
socket.resume();
})
// server TCP socket ready to close
.on('end', function () {
client.sysEmit('serverclose');
utils.log('client TCP socket ready to close');
// on reciving a FIN Packet from server, response a FIN
socket.end();
})
// client TCP socket ready to close
.on('finish', function () {
client.sysEmit('clientclose');
utils.log('server TCP socket ready to close');
})
// underlying TCP socket has been closed
.on('close', function (has_error) {
client.sysEmit('disconnected');
utils.log('TCP connection closed');
if (has_error)
utils.error(new Error('some problems happened during TCP connection closing'));
})
.on('error', function (err) {
client.sysEmit('exception', err);
socket.destory();
utils.error(err);
})

// there's no timeout on client
.setTimeout(0);

return client;
};
2 changes: 2 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ utils.decodeFrame = require('./decode.js');
utils.genMasking_key = require('./genmask.js');
utils.typeOf = require('./typeOf.js');
utils.coolLogo = require('./coolLogo.js');
utils.isHttpServer = require('./isHttpServer.js');

// make magic str readonly
Object.defineProperty(utils, 'MAGIC_STRING', {
value: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
Expand Down
10 changes: 10 additions & 0 deletions lib/utils/isHttpServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var http = require('http').Server;
var https = require('https').Server;

module.exports = function (s) {
if (s instanceof http)
return true;
if (s instanceof https)
return true;
return false;
};

0 comments on commit b39fe99

Please sign in to comment.