diff --git a/lib/sdk/io/net.js b/lib/sdk/io/net.js new file mode 100644 index 000000000..600adcaf7 --- /dev/null +++ b/lib/sdk/io/net.js @@ -0,0 +1,322 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + + +const { Cc, Ci, CC } = require("chrome"); + +const { DuplexStream, InputStream, OutputStream } = require("./stream"); +const { emit, off } = require("../event/core"); +const { EventTarget } = require("../event/target"); +const { Buffer } = require("./buffer"); +const { Class } = require("../core/heritage"); + +const threadManager = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager); +const { createTransport } = Cc["@mozilla.org/network/socket-transport-service;1"]. + getService(Ci.nsISocketTransportService); +const makeServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket"); +const StreamPump = CC("@mozilla.org/network/input-stream-pump;1", + "nsIInputStreamPump", "init"); +const StreamCopier = CC("@mozilla.org/network/async-stream-copier;1", + "nsIAsyncStreamCopier", "init"); +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); +const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", "setOutputStream"); + +const { + STATUS_RESOLVING, STATUS_CONNECTING_TO, + STATUS_CONNECTED_TO, STATUS_SENDING_TO, + STATUS_WAITING_FOR, STATUS_RECEIVING_FROM, + + TIMEOUT_CONNECT, TIMEOUT_READ_WRITE +} = Ci.nsISocketTransport; + +const BACKLOG = -1; +const CONNECTING = "opening"; +const OPEN = "open"; +const CLOSED = "closed"; +const READ = "readOnly"; +const WRITE = "writeOnly"; +const ENCODING_UTF8 = "utf-8"; +const ENCODING_BINARY = "binary"; + +const STATE_EVENTS = { + open: "connect", + writeOnly: "end", + closed: "close" +}; + +let isPort = (x) => parseInt(x) >= 0 + +let accessor = () => { + let map = new WeakMap(); + return (fd, value) => { + if (value === null) map.delete(fd); + if (value !== undefined) map.set(fd, value); + return map.get(fd); + } +} + +let onStatus = (socket) => { + let state = socket.readyState; + //if (previous !== state) { + switch (state) { + case CONNECTING: + break; + case OPEN: + emit(socket, "connect"); + break; + case WRITE: + emit(socket, "end"); + break; + case READ: + break; + case CLOSED: + emit(socket, "close"); + break; + } + //} +} + +let nsITransport = accessor(); +let isConnecting = accessor(); + +const Socket = Class({ + extends: DuplexStream, + initialize: function(options) { + options = options || {}; + + if ("server" in options) + this.server = options.server; + + // This is client connected to your server. + if ("transport" in options) { + let transport = nsITransport(this, options.transport); + + let asyncInputStream = transport.openInputStream(null, 0, 0); + let asyncOutputStream = transport.openOutputStream(null, 0, 0); + + let binaryInputStream = BinaryInputStream(asyncInputStream); + let binaryOutputStream = BinaryOutputStream(asyncOutputStream); + + let pump = StreamPump(asyncInputStream, -1, -1, 0, 0, false); + + + transport.setEventSink({ + onTransportStatus: (transport, status, progress, total) => { + let state = this.readyState; + switch (status) { + case STATUS_RESOLVING: + isConnecting(this, true); + break; + case STATUS_CONNECTING_TO: + isConnecting(this, true); + break; + case STATUS_CONNECTED_TO: + isConnecting(this, false); + this.readable = true; + this.writable = true; + break; + case STATUS_SENDING_TO: + return; + case STATUS_WAITING_FOR: + return; + case STATUS_RECEIVING_FROM: + return; + } + + emit(this, "readyState", state); + onStatus(this); + } + }, threadManager.currentThread); + + OutputStream.prototype.initialize.call(this, { + asyncOutputStream: asyncOutputStream, + output: binaryOutputStream + }); + + + InputStream.prototype.initialize.call(this, { + input: binaryInputStream, + pump: pump + }); + + this.read(); + } + }, + bufferSize: 0, + fd: null, + type: null, + resolving: false, + get readyState() { + if (isConnecting(this)) return CONNECTING; + else if (this.readable && this.writable) return OPEN; + else if (this.readable && !this.writable) return READ; + else if (!this.readable && this.writable) return WRITE; + else return CLOSED; + }, + get remoteAddress() isConnecting(this) ? null : nsITransport(this).host, + get remotePort() isConnecting(this) ? null : nsITransport(this).port, + address: function address() { + }, + setNoDelay: function setNoDelay() { + }, + setKeepAlive: function setKeepAlive() { + }, + setSecure: function setSecure() { + }, + setTimeout: function setTimeout(time, callback) { + if (callback) this.once("timeout", callback); + + nsITransport(this).setTimeout(time, TIMEOUT_READ_WRITE); + }, + open: function open(fd, type) { + throw Error("Not implemented"); + }, + connect: function connect(port, host) { + try { + this.initialize({ + transport: createTransport(null, 0, host, port, null) + }); + } catch(error) { + emit(this, "error", error); + } + }, + setEncoding: function setEncoding(encoding) { + }, + end: function end(data, encoding) { + try { + this.readable = false; + this.writable = false; + + nsITransport(this).close(0); + onStatus(this); + } catch(error) { + emit(this, "error", error); + } + } +}); +exports.Socket = Socket; + +let nsIServerSocket = accessor(); + +const Server = Class({ + extends: EventTarget, + initialize: function(options, listener) { + options = options || {}; + if ("loopbackOnly" in options) + this.loopbackOnly = !!options.loopbackOnly; + if ("maxConnections" in options) + this.maxConnections = options.maxConnections; + if ("connections" in options) + this.connections = options.connections; + + nsIServerSocket(this, makeServerSocket()); + + if (listener) this.on("connection", listener); + + }, + type: null, + get port() (nsIServerSocket(this) || 0).port, + host: null, + /** + * The number of concurrent connections on the server. + */ + connections: 0, + /** + * Set this property to reject connections when the server's connection + * count gets high. + */ + maxConnections: -1, + /** + * Returns the bound address of the server as seen by the operating system. + * Useful to find which port was assigned when giving getting an OS-assigned + * address. + */ + address: function() this.host + ':' + this.port, + listenFD: function listenFD(fd, type) { + throw Error('Not implemented'); + }, + listen: function listen(port, host, callback) { + let server = this; + let connections = 0; + + if (this.fd) throw Error("Server already opened"); + + if (!callback) { + callback = host + host = "localhost" + } + + if (callback) this.on("listening", callback) + + if (isPort(port)) { + this.type = "tcp" + this.host = host; + + let rawServer = nsIServerSocket(this); + rawServer.init(port, this.loopbackOnly, this.maxConnections); + rawServer.asyncListen({ + onSocketAccepted: function onConnect(rawServer, transport) { + try { + let socket = new Socket({ + transport: transport, + server: server + }); + server.connections = server.connections + 1; + emit(server, "connection", socket); + } catch (error) { + emit(server, "error", error); + } + }, + onStopListening: function onDisconnect(rawServer, status) { + try { + emit(server, "close"); + } catch (error) { + emit(server, "error", error); + } + } + }); + + emit(this, "listening"); + } + }, + pause: function pause(time) { + throw Error("Not implemented"); + }, + /** + * Stops the server from accepting new connections. This function is + * asynchronous, the server is finally closed when the server emits a + * `'close'` event. + */ + close: function close() { + off(this); + nsIServerSocket(this).close(); + }, + destroy: function destroy(error) { + this.close(); + if (error) emit(this, "error", error); + nsIServerSocket(this, null); + } +}); +exports.Server = Server; + +let createServer = (options, listener) => Server(options, listener) +exports.createServer = createServer; + +let createConnection = (port, host) => { + let socket = Socket(); + socket.connect(port, host); + + return socket; +}; +exports.createConnection = createConnection; \ No newline at end of file diff --git a/lib/sdk/web-socket.js b/lib/sdk/web-socket.js new file mode 100644 index 000000000..2bbe14459 --- /dev/null +++ b/lib/sdk/web-socket.js @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { WebSocket } = require("./addon/window").window; +exports.WebSocket = WebSocket; diff --git a/test/addons/websocket/main.js b/test/addons/websocket/main.js new file mode 100644 index 000000000..fdfe540ea --- /dev/null +++ b/test/addons/websocket/main.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +let { Cc, Ci } = require("chrome"); +let { WebSocket } = require("sdk/web-socket"); +let { createServer } = require("sdk/io/net"); + +exports.testWebSocket = function(assert, done) { + let server = createServer({ allowHalfOpen: true }, function(socket) { + assert.equal(server.connections, 1, "got one connection"); + socket.on("data", function(data) { + assert.ok(data.toString().indexOf("Upgrade: websocket") >= 0, + "got websocket client handshake"); + socket.end(); + server.close(); + }); + }); + + server.listen(8099, "localhost", function() { + let socket = new WebSocket("ws://localhost:8099/"); + + socket.onopen = function(event) { + assert.equal(event.type, "open", "open event recieved"); + socket.send("hello socket"); + } + socket.onmessage = function(event) { + assert.equal(event.type, "message", "recieved message"); + assert.equal(event.data, "hello socket", "message recieved back"); + socket.close(); + } + socket.onclose = function(event) { + assert.equal(event.type, "close", "socket was closed"); + done(); + } + socket.onerror = function(event) { + assert.pass("connection will fail because of no handshake"); + } + }); +} + +require("sdk/test/runner").runTestsFromModule(module); diff --git a/test/addons/websocket/package.json b/test/addons/websocket/package.json new file mode 100644 index 000000000..33439062d --- /dev/null +++ b/test/addons/websocket/package.json @@ -0,0 +1,3 @@ +{ + "id": "test-web-socket" +} diff --git a/test/test-net-ping-pong.js b/test/test-net-ping-pong.js new file mode 100644 index 000000000..5f2b86904 --- /dev/null +++ b/test/test-net-ping-pong.js @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +"use strict"; + +let net = require("sdk/io/net"); + +function pingPongTest(port, host) { + return function(assert, end) { + let n = 1000; + let count = 0; + let sentPongs = 0; + let last = false; + + let server = net.createServer({ allowHalfOpen: true }, function(socket) { + + assert.equal(server, socket.server, "socket.server is server"); + assert.equal(server.connections, 1, "has one connection"); + assert.equal(true, socket.remoteAddress !== null); + assert.equal(true, socket.remoteAddress !== undefined); + + if (host === "127.0.0.1" || host === "localhost" || !host) { + assert.equal(socket.remoteAddress, "127.0.0.1", "remote address"); + } else { + assert.equal(socket.remoteAddress, "::1"); + } + + socket.setEncoding("utf8"); + socket.setNoDelay(); + socket.timeout = 0; + + socket.on("data", function(data) { + assert.equal("open", socket.readyState, "is open state"); + assert.equal(true, socket.writable, "socket is writable"); + assert.equal(true, socket.readable, "socket is readable"); + assert.equal(true, count <= n); + if (/PING/.exec(data)) { + socket.write("PONG") + sentPongs++; + } + }); + + socket.on("end", function() { + assert.equal("writeOnly", socket.readyState, "once ended socket is write only"); + assert.equal(true, socket.writable); + assert.equal(false, socket.readable); + socket.end(); + }); + + socket.on("close", function(error) { + assert.ok(!error, "closed without error"); + assert.equal("closed", socket.readyState, "state is closed"); + socket.server.close(); + }); + + socket.on("error", function(error) { + assert.fail(error); + }); + }); + + server.listen(port, host, function() { + let client = net.createConnection(port, host); + + client.setEncoding("utf8"); + client.on("connect", function() { + assert.equal("open", client.readyState, "client is open"); + assert.equal(true, client.readable, "client readable"); + assert.equal(true, client.writable, "client writable"); + client.write("PING"); + }); + + client.on("data", function(data) { + assert.equal("PONG", data, "got pong"); + count += 1; + + if (last) { + assert.equal("readOnly", client.readyState); + assert.equal(false, client.writable, "no longer writable"); + assert.equal(true, client.readable, "still readable though"); + return; + } else { + assert.equal("open", client.readyState); + assert.equal(true, client.writable, "writable"); + assert.equal(true, client.readable, "readable"); + } + + if (count < n) client.write("PING"); + else { + last = true; + client.write("PING"); + client.end(); + } + }); + + client.on("close", function() { + assert.equal(n, count); + assert.equal(n, sentPongs); + assert.equal(true, last); + end(); + }); + + client.on("error", function(error) { + assert.fail(error) + }); + }); + } +} + +exports["test ping pong"] = pingPongTest(8099, "localhost"); + +require("test").run(exports); \ No newline at end of file