diff --git a/lib/WebSocketClient.js b/lib/WebSocketClient.js index 4b4abb25..054a5315 100644 --- a/lib/WebSocketClient.js +++ b/lib/WebSocketClient.js @@ -23,6 +23,8 @@ var https = require('https'); var url = require('url'); var crypto = require('crypto'); var WebSocketConnection = require('./WebSocketConnection'); +var path = require('path'); +var fs = require('fs'); var protocolSeparators = [ '(', ')', '<', '>', '@', @@ -100,7 +102,7 @@ function WebSocketClient(config) { } this._req = null; - + switch (this.config.webSocketVersion) { case 8: case 13: @@ -112,6 +114,28 @@ function WebSocketClient(config) { util.inherits(WebSocketClient, EventEmitter); +function handleWsfProtocol(requestUrl, parsedUrl) { + if (typeof(requestUrl) !== 'string') { + return; + } + var prefix = parsedUrl.protocol+"//["; + if (requestUrl.indexOf(prefix) != 0 || + (parsedUrl.hostname && parsedUrl.hostname.length > 0)) { + // parse found a regular ipv6 literal this is not for us + return; + } + var closing = requestUrl.indexOf(']', prefix.length); + if (closing <= 0) { + // the url string is not valid + return; + } + var sockName = requestUrl.substring(prefix.length, closing); + var reparseUrl = requestUrl.substring(0, prefix.length) + "dead::beef" + requestUrl.substring(closing); + var reparsedUrl = url.parse(reparseUrl); + Object.assign(parsedUrl, reparsedUrl); + parsedUrl.socketPath = sockName; +} + WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, headers, extraRequestOptions) { var self = this; if (typeof(protocols) === 'string') { @@ -137,6 +161,7 @@ WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, head if (!this.url.protocol) { throw new Error('You must specify a full WebSocket URL, including protocol.'); } + handleWsfProtocol(requestUrl, this.url); if (!this.url.host) { throw new Error('You must specify a full WebSocket URL, including hostname. Relative URLs are not supported.'); } @@ -169,19 +194,13 @@ WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, head } this.base64nonce = nonce.toString('base64'); - var hostHeaderValue = this.url.hostname; - if ((this.url.protocol === 'ws:' && this.url.port !== '80') || - (this.url.protocol === 'wss:' && this.url.port !== '443')) { - hostHeaderValue += (':' + this.url.port); - } - var reqHeaders = headers || {}; extend(reqHeaders, { 'Upgrade': 'websocket', 'Connection': 'Upgrade', 'Sec-WebSocket-Version': this.config.webSocketVersion.toString(10), 'Sec-WebSocket-Key': this.base64nonce, - 'Host': hostHeaderValue + 'Host': this.url.host }); if (this.protocols.length > 0) { @@ -224,6 +243,7 @@ WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, head // These options are always overridden by the library. The user is not // allowed to specify these directly. extend(requestOptions, { + socketPath: this.url.socketPath, hostname: this.url.hostname, port: this.url.port, method: 'GET', diff --git a/lib/WebSocketRequest.js b/lib/WebSocketRequest.js index f4d96555..62a3b1ef 100644 --- a/lib/WebSocketRequest.js +++ b/lib/WebSocketRequest.js @@ -95,13 +95,13 @@ function WebSocketRequest(socket, httpRequest, serverConfig) { this.remoteAddress = socket.remoteAddress; this.remoteAddresses = [this.remoteAddress]; this.serverConfig = serverConfig; - + // Watch for the underlying TCP socket closing before we call accept this._socketIsClosing = false; this._socketCloseHandler = this._handleSocketCloseBeforeAccept.bind(this); this.socket.on('end', this._socketCloseHandler); this.socket.on('close', this._socketCloseHandler); - + this._resolved = false; } @@ -248,7 +248,7 @@ WebSocketRequest.prototype.parseCookies = function(str) { WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) { this._verifyResolution(); - + // TODO: Handle extensions var protocolFullCase; @@ -286,7 +286,7 @@ WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, co } if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) { this.reject(500); - throw new Error('Specified protocol was not requested by the client.'); + throw new Error('Specified protocol was not requested by the client:'+acceptedProtocol+":"+this.requestedProtocols); } protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, ''); @@ -426,21 +426,21 @@ WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, co // if (negotiatedExtensions) { // response += 'Sec-WebSocket-Extensions: ' + negotiatedExtensions.join(', ') + '\r\n'; // } - + // Mark the request resolved now so that the user can't call accept or // reject a second time. this._resolved = true; this.emit('requestResolved', this); - + response += '\r\n'; var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig); connection.webSocketVersion = this.webSocketVersion; connection.remoteAddress = this.remoteAddress; connection.remoteAddresses = this.remoteAddresses; - + var self = this; - + if (this._socketIsClosing) { // Handle case when the client hangs up before we get a chance to // accept the connection and send our side of the opening handshake. @@ -452,7 +452,7 @@ WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, co cleanupFailedConnection(connection); return; } - + self._removeSocketCloseListeners(); connection._addSocketEventListeners(); }); @@ -464,12 +464,12 @@ WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, co WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) { this._verifyResolution(); - + // Mark the request resolved now so that the user can't call accept or // reject a second time. this._resolved = true; this.emit('requestResolved', this); - + if (typeof(status) !== 'number') { status = 403; } diff --git a/test/unit/fileSockets.js b/test/unit/fileSockets.js new file mode 100644 index 00000000..6db1966f --- /dev/null +++ b/test/unit/fileSockets.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +var test = require('tape'); +var http = require('http'); +var path = require('path'); +var os = require('os'); +var WebSocketServer = require('../../lib/WebSocketServer'); +var WebSocketClient = require('../../lib/WebSocketClient'); + + +function serverSide(t, sockFname, addr) { + var server = http.createServer((request, response) => { + response.writeHead(404); + response.end(); + }); + if (addr) { + server.listen(sockFname, addr, () => { }); + } else { + server.listen(sockFname, () => { }); + } + server.on('error', (e) => { + t.assert(true, false, "errors should not happen"); + }); + + var wsServer = new WebSocketServer({ httpServer: server, autoAcceptConnections: false }); + wsServer.on('request', (request) => { + var connection = request.accept('sockfname-protocol', request.origin); + connection.on('message', function(message) { + switch (message.utf8Data) { + case "ping" : + connection.send("pong"); + break; + case "bye" : + connection.close(); + break; + } + }); + connection.on('close', function(reasonCode, description) { + }); + }); + return server; +} + +function clientSide(t, sockFname, addr, cb) { + let client = new WebSocketClient(); + client.on('connectFailed', (error) => { + console.error(error); + t.assert(true, false, "errors should not happen"); + }); + + client.on('connect', (connection) => { + connection.on('error', (error) => { + t.assert(true, false, "errors should not happen"); + }); + connection.on('close', function() { + t.assert(true, true); + cb(); + }); + connection.on('message', function(message) { + switch (message.utf8Data) { + case "pong" : + connection.send("bye"); + } + }); + connection.send("ping"); + }); + if (typeof(sockFname) == "number") { + if (addr.includes(':')) { + addr = "["+addr+"]"; + } + var url = 'ws://'+addr+':'+sockFname+'/'; + client.connect(url, 'sockfname-protocol'); + //client.connect('ws://[::1]:'+sockFname+'/', 'sockfname-protocol'); + } else { + client.connect('ws://['+sockFname+']/', 'sockfname-protocol'); + } +} + +function run(t, socket, adr, finCb) { + var server = serverSide(t, socket, adr); + var i = 0; + var cb = function() { + if (i < 10) { + ++i; + clientSide(t, socket, adr, cb); + } else { + server.close(); + finCb && finCb(); + } + } + clientSide(t, socket, adr, cb); +} + +test('use ws over file sockets', function(t) { + t.plan(10 + 1); + var sock = './S.test.' + process.pid; + if (os.platform() == 'win32') { + var sock = path.join("\\\\?\\pipe", process.cwd(), "S.test." + process.pid); + } + run(t, sock); +}); + +test('use ws over ipv4 tcp sockets', function(t) { + t.plan(10 + 1); + run(t, 4711, "127.0.0.1"); +}); + +test('use ws over ipv6 tcp sockets', function(t) { + t.plan(10 + 1); + run(t, 4711, "::1"); +});