diff --git a/net/js/package.json b/net/js/package.json index 460025828764649f516ec005fddae1c3c6445bea..52bbcc4bd473d4fd7913d43b3c2fa97ea76138e8 100644 --- a/net/js/package.json +++ b/net/js/package.json @@ -15,6 +15,7 @@ "ws": "^6.2.1" }, "devDependencies": { + "chai": "^4.2.0", "mocha": "^6.0.2" } } diff --git a/net/js/src/socket.js b/net/js/src/socket.js index d051f87637fc060fdbdbdfded37b73c1d89be2ad..22b70d44c9adfd6dff926286369ea1d751738a05 100644 --- a/net/js/src/socket.js +++ b/net/js/src/socket.js @@ -13,7 +13,10 @@ function Socket(uri) { 'close': [] }; + this.handshake_ = false; + this.lasterr_ = null; this.connected_ = false; + this.valid_ = false; this.buffer_ = new Buffer(0); if (t == "string") { @@ -23,24 +26,49 @@ function Socket(uri) { } } +Socket.ERROR_BADPROTOCOL = 0x01; +Socket.ERROR_BADHOST = 0x02; +Socket.ERROR_BADHANDSHAKE = 0x03; +Socket.ERROR_MALFORMEDURI = 0x04; + +Socket.prototype.isValid = function() { + return this.valid_; +} + +/** + * Construct the correct kind of socket connection from a URI. + */ Socket.prototype._fromURI = function(uri) { let uriobj = urijs.parse(uri); - this.uri_ = uri; this.scheme_ = uriobj.scheme; + // Could not parse uri so report error + if (uriobj.scheme === undefined || uriobj.host === undefined) { + this.lasterr_ = Socket.ERROR_MALFORMEDURI; + this.dispatch('error', [this.lasterr_]); + return; + } + + // Websocket protocol if (this.scheme_ == "ws") { + // Detect if in browser or not, choose correct websocket object if (typeof WebSocket == "undefined") { + // Nodejs this.socket_ = new ws(uri); } else { + // Browser this.socket_ = new WebSocket(uri); } this._initWebsocket(); + // TCP } else if (this.scheme_ == "tcp") { this.socket_ = net.connect(uriobj.port, uriobj.host); this._initTCPSocket(); + // Unrecognised protocol } else { - console.error("Invalid URI scheme for socket: ", this.scheme_); + this.lasterr_ = Socket.ERROR_BADPROTOCOL; + this.dispatch('error', [this.lasterr_]); } } @@ -60,7 +88,12 @@ Socket.prototype._fromObject = function(sock) { else if (this.scheme_ == "tcp") this._initTCPSocket(); } +/** + * Setup correct handlers for a websocket connection. + */ Socket.prototype._initWebsocket = function() { + this.valid_ = true; + let dataHandler = (data) => { let size = binary.readUInt32LE(data, 0); let service = binary.readUInt32LE(data, 4); @@ -80,9 +113,27 @@ Socket.prototype._initWebsocket = function() { } else { this.socket_.on('message', dataHandler); } + this.socket_.on('open', () => { + this.connected_ = true; + this.dispatch('open', []); + }); + this.socket_.on('error', (err) => { + this.connected_ = false; + this.valid_ = false; + switch (err.errno) { + case 'ENOTFOUND' : this.lasterr_ = Socket.ERROR_BADHOST; break; + default : this.lasterr_ = err.errno; + } + this.dispatch('error', [this.lasterr_]); + }); + this.socket_.on('close', () => { + this.dispatch('close', []); + }); } Socket.prototype._initTCPSocket = function() { + this.valid_ = true; + let dataHandler = (data) => { console.log('Received: ' + data); this.buffer_ = Buffer.concat([this.buffer_, data]); @@ -111,12 +162,28 @@ Socket.prototype._initTCPSocket = function() { this.connected_ = true; this.dispatch('open', []); }); + this.socket_.on('error', (err) => { + this.connected_ = false; + this.valid_ = false; + console.log("Sock error: ", err); + switch (err.errno) { + case 'ENOTFOUND' : this.lasterr_ = Socket.ERROR_BADHOST; break; + default : this.lasterr_ = err.errno; + } + this.dispatch('error', [this.lasterr_]); + }); + this.socket_.on('close', () => { + this.dispatch('close', []); + }); } Socket.prototype.isConnected = function() { return this.connected_; } +/** + * Register event handlers. + */ Socket.prototype.on = function(name, f) { if (typeof name == "string") { if (this.handlers_.hasOwnProperty(name)) { @@ -124,6 +191,10 @@ Socket.prototype.on = function(name, f) { } else { console.error("Unrecognised handler: ", name); } + + if (name == "error" && this.lasterr_ != null) { + f(this.lasterr_); + } } else if (typeof name == "number") { if (this.handlers_[name] === undefined) this.handlers_[name] = []; this.handlers_[name].push(f); @@ -145,7 +216,11 @@ Socket.prototype.dispatch = function(h, args) { } Socket.prototype.close = function() { - this.socket_.destroy(); + if (this.scheme_ == "ws") { + this.socket_.close(); + } else { + this.socket_.destroy(); + } this.socket_ = null; } diff --git a/net/js/test/socket_unit.js b/net/js/test/socket_unit.js index c7c104d198044e28fcce24600a544863a7d88ffd..a1cc4f78c8a8abe59feb73ee952db84a00842571 100644 --- a/net/js/test/socket_unit.js +++ b/net/js/test/socket_unit.js @@ -1,31 +1,110 @@ const Socket = require('../src/socket.js'); -const assert = require('assert'); +const assert = require('chai').assert; const net = require('net'); const binary = require('bops'); +const WebSocket = require('ws'); -describe("Constructing a socket", function() { +describe("Socket()", function() { let server; + let wss; + let dobadhandshake = false; beforeEach(() => { + dobadhandshake = false; server = net.createServer(socket => { - console.log("Client connected"); + socket.on('error', ()=> {}); + if (dobadhandshake) { + socket.write(Buffer.from([44,55,33,22,23,44,87])); + } else { + socket.write(Buffer.from([0x12,0x09,0x64,0x53,0x00,0x34,0x99,0x10,3,0,0,0,3,0,0,0,67,67,67,67,67,67])); + } }); server.listen(9000, 'localhost'); + + wss = new WebSocket.Server({ port: 9001 }); + wss.on('connection', (ws) => { + ws.on('error', ()=> {}); + if (dobadhandshake) { + ws.send(Buffer.from([44,55,33,22,23,44,87])); + } else { + ws.send(Buffer.from([0x12,0x09,0x64,0x53,0x00,0x34,0x99,0x10,3,0,0,0,3,0,0,0,67,67,67,67,67,67])); + } + }); }); - it("Connects to a valid tcp uri", function(done) { - let sock = new Socket("tcp://localhost:9000"); - sock.on('open', () => { - console.log("OPEN"); - assert.equal(sock.isConnected(),true); - sock.close(); - done(); + context("with a valid connection uri and handshake", () => { + it("make a tcp connection", function(done) { + let sock = new Socket("tcp://localhost:9000"); + sock.on('open', () => { + assert.isOk(sock.isConnected()); + sock.close(); + done(); + }); + }); + + it("make a websocket connection", function(done) { + let sock = new Socket("ws://localhost:9001"); + sock.on('open', () => { + assert.isOk(sock.isConnected()); + sock.close(); + done(); + }); + }); + }); + + context("with a valid uri but bad handshake", () => { + it("should reject the connection", () => { + let diderror = false; + dobadhandshake = true; + let sock = new Socket("tcp://localhost:9000"); + sock.on('error', (errno) => { + diderror = true; + assert.equal(errno, Socket.ERROR_BADHANDSHAKE); + assert.isOk(sock.isValid()); + assert.isNotOk(sock.isConnected()); + }); + assert.isOk(diderror); + }); + }); + + context("with an invalid connection uri", () => { + it("should give protocol error", () => { + let diderror = false; + let sock = new Socket("xyz://localhost:9000"); + sock.on('error', (errno) => { + diderror = true; + assert.equal(errno, Socket.ERROR_BADPROTOCOL); + assert.isNotOk(sock.isValid()); + }); + assert.isOk(diderror); + }); + + it("should give host error", (done) => { + let sock = new Socket("tcp://blah.blah:9000"); + sock.on('error', (errno) => { + assert.equal(errno, Socket.ERROR_BADHOST); + assert.isNotOk(sock.isValid()); + done(); + }); + }); + + it("should give a malformed uri error", () => { + let diderror = false; + let sock = new Socket("helloworld"); + sock.on('error', (errno) => { + diderror = true; + assert.equal(errno, Socket.ERROR_MALFORMEDURI); + assert.isNotOk(sock.isValid()); + }); + assert.isOk(diderror); }); }); afterEach(() => { server.close(() => { console.log("Closed"); }); server.unref(); + + wss.close(); }); }); @@ -34,7 +113,6 @@ describe("Receiving messages on a tcp socket", function() { beforeEach(() => { server = net.createServer(socket => { - console.log("Client connected"); socket.write(Buffer.from([8,0,0,0,44,0,0,0,23,0,0,0])); }); server.listen(9001, 'localhost'); @@ -57,3 +135,4 @@ describe("Receiving messages on a tcp socket", function() { }); }); +