From 50c470e53d91e4e0ecb075a0086b409d9acbce95 Mon Sep 17 00:00:00 2001 From: Lilia Date: Fri, 1 Sep 2017 17:58:58 +0200 Subject: [PATCH] Certificate pinning via node XMLHttpRequest implementation (#1394) * Add certificate pinning on https service requests Make https requests to the server using node apis instead of browser apis, so we can specify our own CA list, which contains only our own CA. This protects us from MITM by a rogue CA. As a bonus, this let's us drop the use of non-standard ports and just use good ol' default 443 all the time, at least for http requests. // FREEBIE * Make certificateAuthorities an option on requests Modify node-based xhr implementation based on driverdan/node-XMLHttpRequest, adding support for setting certificate authorities on each request. This allows us to pin our master CA for requests to the server and cdn but not to the s3 attachment server, for instance. Also fix an exception when sending binary data in a request: it is submitted as an array buffer, and must be converted to a node Buffer since we are now using a node based request api. // FREEBIE * Import node-based xhr implementation Add a copy of https://github.com/driverdan/node-XMLHttpRequest@86ff70e, and expose it to the renderer in the preload script. In later commits this module will be extended to support custom certificate authorities. // FREEBIE * Support "arraybuffer" responseType on requests When fetching attachments, we want the result as binary data rather than a utf8 string. This lets our node-based XMLHttpRequest honor the responseType property if it is set on the xhr. Note that naively using the raw `.buffer` from a node Buffer won't work, since it is a reuseable backing buffer that is often much larger than the actual content defined by the Buffer's offset and length. Instead, we'll prepare a return buffer based on the response's content length header, and incrementally write chunks of data into it as they arrive. // FREEBIE * Switch to self-signed server endpoint * Log more error info on failed requests With the node-based xhr, relevant error info are stored in statusText and responseText when a request fails. // FREEBIE * Add node-based websocket w/ support for custom CA // FREEBIE * Support handling array buffers instead of blobs Our node-based websocket calls onmessage with an arraybuffer instead of a blob. For robustness (on the off chance we switch or update the socket implementation agian) I've kept the machinery for converting blobs to array buffers. // FREEBIE * Destroy all wacky server ports // FREEBIE --- Gruntfile.js | 2 + config/default.json | 3 +- config/production.json | 2 +- js/XMLHttpRequest.js | 637 +++++++++++++++++++++++++++ js/background.js | 7 +- js/libtextsecure.js | 97 ++-- libtextsecure/account_manager.js | 4 +- libtextsecure/api.js | 61 +-- libtextsecure/message_receiver.js | 8 +- libtextsecure/sendmessage.js | 8 +- libtextsecure/websocket-resources.js | 16 +- main.js | 1 + package.json | 3 +- preload.js | 3 + yarn.lock | 81 ++-- 15 files changed, 774 insertions(+), 159 deletions(-) create mode 100644 js/XMLHttpRequest.js diff --git a/Gruntfile.js b/Gruntfile.js index 486770505..d25be1587 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -101,6 +101,7 @@ module.exports = function(grunt) { '!js/Mp3LameEncoder.min.js', '!js/libsignal-protocol-worker.js', '!js/components.js', + '!js/XMLHttpRequest.js', '!js/signal_protocol_store.js', '_locales/**/*' ], @@ -161,6 +162,7 @@ module.exports = function(grunt) { '!js/Mp3LameEncoder.min.js', '!js/libsignal-protocol-worker.js', '!js/components.js', + '!js/XMLHttpRequest.js', 'test/**/*.js', '!test/blanket_mocha.js', '!test/test.js', diff --git a/config/default.json b/config/default.json index 244243b9c..92964b2f1 100644 --- a/config/default.json +++ b/config/default.json @@ -2,5 +2,6 @@ "serverUrl": "https://textsecure-service-staging.whispersystems.org", "disableAutoUpdate": false, "openDevTools": false, - "buildExpiration": 0 + "buildExpiration": 0, + "certificateAuthorities": ["-----BEGIN CERTIFICATE-----\nMIID7zCCAtegAwIBAgIJAIm6LatK5PNiMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD\nVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5j\naXNjbzEdMBsGA1UECgwUT3BlbiBXaGlzcGVyIFN5c3RlbXMxHTAbBgNVBAsMFE9w\nZW4gV2hpc3BlciBTeXN0ZW1zMRMwEQYDVQQDDApUZXh0U2VjdXJlMB4XDTEzMDMy\nNTIyMTgzNVoXDTIzMDMyMzIyMTgzNVowgY0xCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMR0wGwYDVQQKDBRP\ncGVuIFdoaXNwZXIgU3lzdGVtczEdMBsGA1UECwwUT3BlbiBXaGlzcGVyIFN5c3Rl\nbXMxEzARBgNVBAMMClRleHRTZWN1cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQDBSWBpOCBDF0i4q2d4jAXkSXUGpbeWugVPQCjaL6qD9QDOxeW1afvf\nPo863i6Crq1KDxHpB36EwzVcjwLkFTIMeo7t9s1FQolAt3mErV2U0vie6Ves+yj6\ngrSfxwIDAcdsKmI0a1SQCZlr3Q1tcHAkAKFRxYNawADyps5B+Zmqcgf653TXS5/0\nIPPQLocLn8GWLwOYNnYfBvILKDMItmZTtEbucdigxEA9mfIvvHADEbteLtVgwBm9\nR5vVvtwrD6CCxI3pgH7EH7kMP0Od93wLisvn1yhHY7FuYlrkYqdkMvWUrKoASVw4\njb69vaeJCUdU+HCoXOSP1PQcL6WenNCHAgMBAAGjUDBOMB0GA1UdDgQWBBQBixjx\nP/s5GURuhYa+lGUypzI8kDAfBgNVHSMEGDAWgBQBixjxP/s5GURuhYa+lGUypzI8\nkDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB+Hr4hC56m0LvJAu1R\nK6NuPDbTMEN7/jMojFHxH4P3XPFfupjR+bkDq0pPOU6JjIxnrD1XD/EVmTTaTVY5\niOheyv7UzJOefb2pLOc9qsuvI4fnaESh9bhzln+LXxtCrRPGhkxA1IMIo3J/s2WF\n/KVYZyciu6b4ubJ91XPAuBNZwImug7/srWvbpk0hq6A6z140WTVSKtJG7EP41kJe\n/oF4usY5J7LPkxK3LWzMJnb5EIJDmRvyH8pyRwWg6Qm6qiGFaI4nL8QU4La1x2en\n4DGXRaLMPRwjELNgQPodR38zoCMuA8gHZfZYYoZ7D7Q1wNUiVHcxuFrEeBaYJbLE\nrwLV\n-----END CERTIFICATE-----\n"] } diff --git a/config/production.json b/config/production.json index b52ba72b5..4152a7bdd 100644 --- a/config/production.json +++ b/config/production.json @@ -1,3 +1,3 @@ { - "serverUrl": "https://textsecure-service-ca.whispersystems.org" + "serverUrl": "https://textsecure-service.whispersystems.org" } diff --git a/js/XMLHttpRequest.js b/js/XMLHttpRequest.js new file mode 100644 index 000000000..2afd5d2be --- /dev/null +++ b/js/XMLHttpRequest.js @@ -0,0 +1,637 @@ +/** + * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object. + * + * This can be used with JS designed for browsers to improve reuse of code and + * allow the use of existing libraries. + * + * Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs. + * + * @author Dan DeFelippi + * @contributor David Ellis + * @license MIT + */ + +var Url = require("url"); +var spawn = require("child_process").spawn; +var fs = require("fs"); + +exports.XMLHttpRequest = function() { + "use strict"; + + /** + * Private variables + */ + var self = this; + var http = require("http"); + var https = require("https"); + + // Holds http.js objects + var request; + var response; + + // Request settings + var settings = {}; + + // Disable header blacklist. + // Not part of XHR specs. + var disableHeaderCheck = false; + + // Set some default headers + var defaultHeaders = { + "User-Agent": "node-XMLHttpRequest", + "Accept": "*/*", + }; + + var headers = {}; + var headersCase = {}; + var certificateAuthorities; + var responseOffset; + + // These headers are not user setable. + // The following are allowed but banned in the spec: + // * user-agent + var forbiddenRequestHeaders = [ + "accept-charset", + "accept-encoding", + "access-control-request-headers", + "access-control-request-method", + "connection", + "content-length", + "content-transfer-encoding", + "cookie", + "cookie2", + "date", + "expect", + "host", + "keep-alive", + "origin", + "referer", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "via" + ]; + + // These request methods are not allowed + var forbiddenRequestMethods = [ + "TRACE", + "TRACK", + "CONNECT" + ]; + + // Send flag + var sendFlag = false; + // Error flag, used when errors occur or abort is called + var errorFlag = false; + + // Event listeners + var listeners = {}; + + /** + * Constants + */ + + this.UNSENT = 0; + this.OPENED = 1; + this.HEADERS_RECEIVED = 2; + this.LOADING = 3; + this.DONE = 4; + + /** + * Public vars + */ + + // Current state + this.readyState = this.UNSENT; + + // default ready state change handler in case one is not set or is set late + this.onreadystatechange = null; + + // Result & response + this.responseText = ""; + this.responseXML = ""; + this.status = null; + this.statusText = null; + + // Whether cross-site Access-Control requests should be made using + // credentials such as cookies or authorization headers + this.withCredentials = false; + + /** + * Private methods + */ + + /** + * Check if the specified header is allowed. + * + * @param string header Header to validate + * @return boolean False if not allowed, otherwise true + */ + var isAllowedHttpHeader = function(header) { + return disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1); + }; + + /** + * Check if the specified method is allowed. + * + * @param string method Request method to validate + * @return boolean False if not allowed, otherwise true + */ + var isAllowedHttpMethod = function(method) { + return (method && forbiddenRequestMethods.indexOf(method) === -1); + }; + + /** + * Public methods + */ + + /** + * Open the connection. Currently supports local server requests. + * + * @param string method Connection method (eg GET, POST) + * @param string url URL for the connection. + * @param boolean async Asynchronous connection. Default is true. + * @param string user Username for basic authentication (optional) + * @param string password Password for basic authentication (optional) + */ + this.open = function(method, url, async, user, password) { + this.abort(); + errorFlag = false; + + // Check for valid request method + if (!isAllowedHttpMethod(method)) { + throw new Error("SecurityError: Request method not allowed"); + } + + settings = { + "method": method, + "url": url.toString(), + "async": (typeof async !== "boolean" ? true : async), + "user": user || null, + "password": password || null + }; + + setState(this.OPENED); + }; + + /** + * Disables or enables isAllowedHttpHeader() check the request. Enabled by default. + * This does not conform to the W3C spec. + * + * @param boolean state Enable or disable header checking. + */ + this.setDisableHeaderCheck = function(state) { + disableHeaderCheck = state; + }; + + /** + * Sets a header for the request or appends the value if one is already set. + * + * @param string header Header name + * @param string value Header value + */ + this.setRequestHeader = function(header, value) { + if (this.readyState !== this.OPENED) { + throw new Error("INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN"); + } + if (!isAllowedHttpHeader(header)) { + console.warn("Refused to set unsafe header \"" + header + "\""); + return; + } + if (sendFlag) { + throw new Error("INVALID_STATE_ERR: send flag is true"); + } + header = headersCase[header.toLowerCase()] || header; + headersCase[header.toLowerCase()] = header; + headers[header] = headers[header] ? headers[header] + ', ' + value : value; + }; + + this.setCertificateAuthorities = function(list) { + certificateAuthorities = list; + }; + + /** + * Gets a header from the server response. + * + * @param string header Name of header to get. + * @return string Text of the header or null if it doesn't exist. + */ + this.getResponseHeader = function(header) { + if (typeof header === "string" + && this.readyState > this.OPENED + && response + && response.headers + && response.headers[header.toLowerCase()] + && !errorFlag + ) { + return response.headers[header.toLowerCase()]; + } + + return null; + }; + + /** + * Gets all the response headers. + * + * @return string A string with all response headers separated by CR+LF + */ + this.getAllResponseHeaders = function() { + if (this.readyState < this.HEADERS_RECEIVED || errorFlag) { + return ""; + } + var result = ""; + + for (var i in response.headers) { + // Cookie headers are excluded + if (i !== "set-cookie" && i !== "set-cookie2") { + result += i + ": " + response.headers[i] + "\r\n"; + } + } + return result.substr(0, result.length - 2); + }; + + /** + * Gets a request header + * + * @param string name Name of header to get + * @return string Returns the request header or empty string if not set + */ + this.getRequestHeader = function(name) { + if (typeof name === "string" && headersCase[name.toLowerCase()]) { + return headers[headersCase[name.toLowerCase()]]; + } + + return ""; + }; + + /** + * Sends the request to the server. + * + * @param string data Optional data to send as request body. + */ + this.send = function(data) { + if (this.readyState !== this.OPENED) { + throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called"); + } + + if (sendFlag) { + throw new Error("INVALID_STATE_ERR: send has already been called"); + } + + var ssl = false, local = false; + var url = Url.parse(settings.url); + var host; + // Determine the server + switch (url.protocol) { + case "https:": + ssl = true; + // SSL & non-SSL both need host, no break here. + case "http:": + host = url.hostname; + break; + + case "file:": + local = true; + break; + + case undefined: + case null: + case "": + host = "localhost"; + break; + + default: + throw new Error("Protocol not supported."); + } + + // Load files off the local filesystem (file://) + if (local) { + if (settings.method !== "GET") { + throw new Error("XMLHttpRequest: Only GET method is supported"); + } + + if (settings.async) { + fs.readFile(url.pathname, "utf8", function(error, data) { + if (error) { + self.handleError(error); + } else { + self.status = 200; + self.responseText = data; + setState(self.DONE); + } + }); + } else { + try { + this.responseText = fs.readFileSync(url.pathname, "utf8"); + this.status = 200; + setState(self.DONE); + } catch(e) { + this.handleError(e); + } + } + + return; + } + + // Default to port 80. If accessing localhost on another port be sure + // to use http://localhost:port/path + var port = url.port || (ssl ? 443 : 80); + // Add query string if one is used + var uri = url.pathname + (url.search ? url.search : ""); + + // Set the defaults if they haven't been set + for (var name in defaultHeaders) { + if (!headersCase[name.toLowerCase()]) { + headers[name] = defaultHeaders[name]; + } + } + + // Set the Host header or the server may reject the request + headers.Host = host; + if (!((ssl && port === 443) || port === 80)) { + headers.Host += ":" + url.port; + } + + // Set Basic Auth if necessary + if (settings.user) { + if (typeof settings.password === "undefined") { + settings.password = ""; + } + var authBuf = new Buffer(settings.user + ":" + settings.password); + headers.Authorization = "Basic " + authBuf.toString("base64"); + } + + // Set content length header + if (settings.method === "GET" || settings.method === "HEAD") { + data = null; + } else if (data) { + headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data); + + if (!headers["Content-Type"]) { + headers["Content-Type"] = "text/plain;charset=UTF-8"; + } + } else if (settings.method === "POST") { + // For a post with no data set Content-Length: 0. + // This is required by buggy servers that don't meet the specs. + headers["Content-Length"] = 0; + } + + var options = { + host: host, + port: port, + path: uri, + method: settings.method, + headers: headers, + agent: new https.Agent({ ca: certificateAuthorities }), + withCredentials: self.withCredentials + }; + + // Reset error flag + errorFlag = false; + + // Handle async requests + if (settings.async) { + // Use the proper protocol + var doRequest = ssl ? https.request : http.request; + + // Request is being sent, set send flag + sendFlag = true; + + // As per spec, this is called here for historical reasons. + self.dispatchEvent("readystatechange"); + + // Handler for the response + var responseHandler = function responseHandler(resp) { + // Set response var to the response we got back + // This is so it remains accessable outside this scope + response = resp; + // Check for redirect + // @TODO Prevent looped redirects + if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { + // Change URL to the redirect location + settings.url = response.headers.location; + var url = Url.parse(settings.url); + // Set host var in case it's used later + host = url.hostname; + // Options for the new request + var newOptions = { + hostname: url.hostname, + port: url.port, + path: url.path, + method: response.statusCode === 303 ? "GET" : settings.method, + headers: headers, + withCredentials: self.withCredentials + }; + + // Issue the new request + request = doRequest(newOptions, responseHandler).on("error", errorHandler); + request.end(); + // @TODO Check if an XHR event needs to be fired here + return; + } + + if (self.responseType === "arraybuffer") { + self.response = new ArrayBuffer(response.headers['content-length']); + responseOffset = 0; + } else { + response.setEncoding("utf8"); + } + + setState(self.HEADERS_RECEIVED); + self.status = response.statusCode; + + response.on("data", function(chunk) { + // Make sure there's some data + if (chunk) { + if (self.responseType === "arraybuffer") { + chunk.copy(new Uint8Array(self.response), responseOffset); + responseOffset += chunk.length; + } else { + self.responseText += chunk; // chunk is a string + } + } + // Don't emit state changes if the connection has been aborted. + if (sendFlag) { + setState(self.LOADING); + } + }); + + response.on("end", function() { + if (sendFlag) { + // Discard the end event if the connection has been aborted + + setState(self.DONE); + sendFlag = false; + } + }); + + response.on("error", function(error) { + self.handleError(error); + }); + }; + + // Error handler for the request + var errorHandler = function errorHandler(error) { + self.handleError(error); + }; + + // Create the request + request = doRequest(options, responseHandler).on("error", errorHandler); + + // Node 0.4 and later won't accept empty data. Make sure it's needed. + if (data) { + request.write(Buffer.from(data)); + } + + request.end(); + + self.dispatchEvent("loadstart"); + } else { // Synchronous + // Create a temporary file for communication with the other Node process + var contentFile = ".node-xmlhttprequest-content-" + process.pid; + var syncFile = ".node-xmlhttprequest-sync-" + process.pid; + fs.writeFileSync(syncFile, "", "utf8"); + // The async request the other Node process executes + var execString = "var http = require('http'), https = require('https'), fs = require('fs');" + + "var doRequest = http" + (ssl ? "s" : "") + ".request;" + + "var options = " + JSON.stringify(options) + ";" + + "var responseText = '';" + + "var req = doRequest(options, function(response) {" + + "response.setEncoding('utf8');" + + "response.on('data', function(chunk) {" + + " responseText += chunk;" + + "});" + + "response.on('end', function() {" + + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText}}), 'utf8');" + + "fs.unlinkSync('" + syncFile + "');" + + "});" + + "response.on('error', function(error) {" + + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');" + + "fs.unlinkSync('" + syncFile + "');" + + "});" + + "}).on('error', function(error) {" + + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');" + + "fs.unlinkSync('" + syncFile + "');" + + "});" + + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"") + + "req.end();"; + // Start the other Node Process, executing this string + var syncProc = spawn(process.argv[0], ["-e", execString]); + while(fs.existsSync(syncFile)) { + // Wait while the sync file is empty + } + var resp = JSON.parse(fs.readFileSync(contentFile, 'utf8')); + // Kill the child process once the file has data + syncProc.stdin.end(); + // Remove the temporary file + fs.unlinkSync(contentFile); + + if (resp.err) { + self.handleError(resp.err); + } else { + response = resp.data; + self.status = resp.data.statusCode; + self.responseText = resp.data.text; + setState(self.DONE); + } + } + }; + + /** + * Called when an error is encountered to deal with it. + */ + this.handleError = function(error) { + this.status = 0; + this.statusText = error; + this.responseText = error.stack; + errorFlag = true; + setState(this.DONE); + this.dispatchEvent('error'); + }; + + /** + * Aborts a request. + */ + this.abort = function() { + if (request) { + request.abort(); + request = null; + } + + headers = defaultHeaders; + this.status = 0; + this.responseText = ""; + this.responseXML = ""; + + errorFlag = true; + + if (this.readyState !== this.UNSENT + && (this.readyState !== this.OPENED || sendFlag) + && this.readyState !== this.DONE) { + sendFlag = false; + setState(this.DONE); + } + this.readyState = this.UNSENT; + this.dispatchEvent('abort'); + }; + + /** + * Adds an event listener. Preferred method of binding to events. + */ + this.addEventListener = function(event, callback) { + if (!(event in listeners)) { + listeners[event] = []; + } + // Currently allows duplicate callbacks. Should it? + listeners[event].push(callback); + }; + + /** + * Remove an event callback that has already been bound. + * Only works on the matching funciton, cannot be a copy. + */ + this.removeEventListener = function(event, callback) { + if (event in listeners) { + // Filter will return a new array with the callback removed + listeners[event] = listeners[event].filter(function(ev) { + return ev !== callback; + }); + } + }; + + /** + * Dispatch any events, including both "on" methods and events attached using addEventListener. + */ + this.dispatchEvent = function(event) { + if (typeof self["on" + event] === "function") { + self["on" + event](); + } + if (event in listeners) { + for (var i = 0, len = listeners[event].length; i < len; i++) { + listeners[event][i].call(self); + } + } + }; + + /** + * Changes readyState and calls onreadystatechange. + * + * @param int state New state + */ + var setState = function(state) { + if (state == self.LOADING || self.readyState !== state) { + self.readyState = state; + + if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) { + self.dispatchEvent("readystatechange"); + } + + if (self.readyState === self.DONE && !errorFlag) { + self.dispatchEvent("load"); + // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie) + self.dispatchEvent("loadend"); + } + } + }; +}; diff --git a/js/background.js b/js/background.js index cd5e4028d..2bb8dd58b 100644 --- a/js/background.js +++ b/js/background.js @@ -22,7 +22,6 @@ }); var SERVER_URL = window.config.serverUrl; - var SERVER_PORTS = [80, 4433, 8443]; var messageReceiver; window.getSocketStatus = function() { if (messageReceiver) { @@ -38,7 +37,7 @@ var USERNAME = storage.get('number_id'); var PASSWORD = storage.get('password'); accountManager = new textsecure.AccountManager( - SERVER_URL, SERVER_PORTS, USERNAME, PASSWORD + SERVER_URL, USERNAME, PASSWORD ); accountManager.addEventListener('registration', function() { if (!Whisper.Registration.everDone()) { @@ -171,7 +170,7 @@ // initialize the socket and start listening for messages messageReceiver = new textsecure.MessageReceiver( - SERVER_URL, SERVER_PORTS, USERNAME, PASSWORD, mySignalingKey + SERVER_URL, USERNAME, PASSWORD, mySignalingKey ); messageReceiver.addEventListener('message', onMessageReceived); messageReceiver.addEventListener('receipt', onDeliveryReceipt); @@ -185,7 +184,7 @@ messageReceiver.addEventListener('progress', onProgress); window.textsecure.messaging = new textsecure.MessageSender( - SERVER_URL, SERVER_PORTS, USERNAME, PASSWORD + SERVER_URL, USERNAME, PASSWORD ); // Because v0.43.2 introduced a bug that lost contact details, v0.43.4 introduces diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 6f127c5d8..fe3ff99b3 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -37215,9 +37215,8 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ socket.onmessage = function(socketMessage) { var blob = socketMessage.data; - var reader = new FileReader(); - reader.onload = function() { - var message = textsecure.protobuf.WebSocketMessage.decode(reader.result); + var handleArrayBuffer = function(buffer) { + var message = textsecure.protobuf.WebSocketMessage.decode(buffer); if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) { handleRequest( new IncomingWebSocketRequest({ @@ -37247,7 +37246,16 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ } } }; - reader.readAsArrayBuffer(blob); + + if (blob instanceof ArrayBuffer) { + handleArrayBuffer(blob); + } else { + var reader = new FileReader(); + reader.onload = function() { + handleArrayBuffer(reader.result); + }; + reader.readAsArrayBuffer(blob); + } }; if (opts.keepalive) { @@ -37568,20 +37576,6 @@ window.textsecure.utils = function() { * vim: ts=4:sw=4:expandtab */ -function PortManager(ports) { - this.ports = ports; - this.idx = 0; -} - -PortManager.prototype = { - constructor: PortManager, - getPort: function() { - var port = this.ports[this.idx]; - this.idx = (this.idx + 1) % this.ports.length; - return port; - } -}; - var TextSecureServer = (function() { 'use strict'; @@ -37604,11 +37598,19 @@ var TextSecureServer = (function() { return true; } + function createSocket(url) { + var requestOptions = { ca: window.config.certificateAuthorities }; + return new nodeWebSocket(url, null, null, null, requestOptions); + } + + var XMLHttpRequest = nodeXMLHttpRequest; + window.setImmediate = nodeSetImmediate; + // Promise-based async xhr routine function promise_ajax(url, options) { return new Promise(function (resolve, reject) { if (!url) { - url = options.host + ':' + options.port + '/' + options.path; + url = options.host + '/' + options.path; } console.log(options.type, url); var xhr = new XMLHttpRequest(); @@ -37625,6 +37627,9 @@ var TextSecureServer = (function() { } xhr.setRequestHeader( 'X-Signal-Agent', 'OWD' ); + if (options.certificateAuthorities) { + xhr.setCertificateAuthorities(options.certificateAuthorities); + } xhr.onload = function() { var result = xhr.response; @@ -37651,7 +37656,8 @@ var TextSecureServer = (function() { }; xhr.onerror = function() { console.log(options.type, url, xhr.status, 'Error'); - reject(HTTPError(xhr.status, null, options.stack)); + console.log(xhr.statusText); + reject(HTTPError(xhr.status, xhr.statusText, options.stack)); }; xhr.send( options.data || null ); }); @@ -37660,9 +37666,6 @@ var TextSecureServer = (function() { function retry_ajax(url, options, limit, count) { count = count || 0; limit = limit || 3; - if (options.ports) { - options.port = options.ports[count % options.ports.length]; - } count++; return promise_ajax(url, options).catch(function(e) { if (e.name === 'HTTPError' && e.code === -1 && count < limit) { @@ -37706,11 +37709,10 @@ var TextSecureServer = (function() { profile : "v1/profile" }; - function TextSecureServer(url, ports, username, password) { + function TextSecureServer(url, username, password) { if (typeof url !== 'string') { throw new Error('Invalid server url'); } - this.portManager = new PortManager(ports); this.url = url; this.username = username; this.password = password; @@ -37718,16 +37720,12 @@ var TextSecureServer = (function() { TextSecureServer.prototype = { constructor: TextSecureServer, - getUrl: function() { - return this.url + ':' + this.portManager.getPort(); - }, ajax: function(param) { if (!param.urlParameters) { param.urlParameters = ''; } return ajax(null, { host : this.url, - ports : this.portManager.ports, path : URL_CALLS[param.call] + param.urlParameters, type : param.httpType, data : param.jsonData && textsecure.utils.jsonThing(param.jsonData), @@ -37735,7 +37733,8 @@ var TextSecureServer = (function() { dataType : 'json', user : this.username, password : this.password, - validateResponse: param.validateResponse + validateResponse: param.validateResponse, + certificateAuthorities: window.config.certificateAuthorities }).catch(function(e) { var code = e.code; if (code === 200) { @@ -37947,22 +37946,16 @@ var TextSecureServer = (function() { }.bind(this)); }, getMessageSocket: function() { - var url = this.getUrl(); - console.log('opening message socket', url); - return new WebSocket( - url.replace('https://', 'wss://').replace('http://', 'ws://') + console.log('opening message socket', this.url); + return createSocket(this.url.replace('https://', 'wss://').replace('http://', 'ws://') + '/v1/websocket/?login=' + encodeURIComponent(this.username) + '&password=' + encodeURIComponent(this.password) - + '&agent=OWD' - ); + + '&agent=OWD'); }, getProvisioningSocket: function () { - var url = this.getUrl(); - console.log('opening provisioning socket', url); - return new WebSocket( - url.replace('https://', 'wss://').replace('http://', 'ws://') - + '/v1/websocket/provisioning/?agent=OWD' - ); + console.log('opening provisioning socket', this.url); + return createSocket(this.url.replace('https://', 'wss://').replace('http://', 'ws://') + + '/v1/websocket/provisioning/?agent=OWD'); } }; @@ -37980,8 +37973,8 @@ var TextSecureServer = (function() { var ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000; - function AccountManager(url, ports, username, password) { - this.server = new TextSecureServer(url, ports, username, password); + function AccountManager(url, username, password) { + this.server = new TextSecureServer(url, username, password); this.pending = Promise.resolve(); } @@ -38264,14 +38257,14 @@ var TextSecureServer = (function() { * vim: ts=4:sw=4:expandtab */ -function MessageReceiver(url, ports, username, password, signalingKey) { +function MessageReceiver(url, username, password, signalingKey) { this.count = 0; this.url = url; this.signalingKey = signalingKey; this.username = username; this.password = password; - this.server = new TextSecureServer(url, ports, username, password); + this.server = new TextSecureServer(url, username, password); var address = libsignal.SignalProtocolAddress.fromString(username); this.number = address.getName(); @@ -39096,8 +39089,8 @@ MessageReceiver.prototype.extend({ window.textsecure = window.textsecure || {}; -textsecure.MessageReceiver = function(url, ports, username, password, signalingKey) { - var messageReceiver = new MessageReceiver(url, ports, username, password, signalingKey); +textsecure.MessageReceiver = function(url, username, password, signalingKey) { + var messageReceiver = new MessageReceiver(url, username, password, signalingKey); this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); this.getStatus = messageReceiver.getStatus.bind(messageReceiver); @@ -39462,8 +39455,8 @@ Message.prototype = { } }; -function MessageSender(url, ports, username, password) { - this.server = new TextSecureServer(url, ports, username, password); +function MessageSender(url, username, password) { + this.server = new TextSecureServer(url, username, password); this.pendingMessages = {}; } @@ -39989,8 +39982,8 @@ MessageSender.prototype = { window.textsecure = window.textsecure || {}; -textsecure.MessageSender = function(url, ports, username, password) { - var sender = new MessageSender(url, ports, username, password); +textsecure.MessageSender = function(url, username, password) { + var sender = new MessageSender(url, username, password); textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.ENCRYPT_MESSAGE); textsecure.replay.registerFunction(sender.retransmitMessage.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE); textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE); diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index f0993dc04..dbec1a33d 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -9,8 +9,8 @@ var ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000; - function AccountManager(url, ports, username, password) { - this.server = new TextSecureServer(url, ports, username, password); + function AccountManager(url, username, password) { + this.server = new TextSecureServer(url, username, password); this.pending = Promise.resolve(); } diff --git a/libtextsecure/api.js b/libtextsecure/api.js index 1803bb30a..e4fac1667 100644 --- a/libtextsecure/api.js +++ b/libtextsecure/api.js @@ -2,20 +2,6 @@ * vim: ts=4:sw=4:expandtab */ -function PortManager(ports) { - this.ports = ports; - this.idx = 0; -} - -PortManager.prototype = { - constructor: PortManager, - getPort: function() { - var port = this.ports[this.idx]; - this.idx = (this.idx + 1) % this.ports.length; - return port; - } -}; - var TextSecureServer = (function() { 'use strict'; @@ -38,11 +24,19 @@ var TextSecureServer = (function() { return true; } + function createSocket(url) { + var requestOptions = { ca: window.config.certificateAuthorities }; + return new nodeWebSocket(url, null, null, null, requestOptions); + } + + var XMLHttpRequest = nodeXMLHttpRequest; + window.setImmediate = nodeSetImmediate; + // Promise-based async xhr routine function promise_ajax(url, options) { return new Promise(function (resolve, reject) { if (!url) { - url = options.host + ':' + options.port + '/' + options.path; + url = options.host + '/' + options.path; } console.log(options.type, url); var xhr = new XMLHttpRequest(); @@ -59,6 +53,9 @@ var TextSecureServer = (function() { } xhr.setRequestHeader( 'X-Signal-Agent', 'OWD' ); + if (options.certificateAuthorities) { + xhr.setCertificateAuthorities(options.certificateAuthorities); + } xhr.onload = function() { var result = xhr.response; @@ -85,7 +82,8 @@ var TextSecureServer = (function() { }; xhr.onerror = function() { console.log(options.type, url, xhr.status, 'Error'); - reject(HTTPError(xhr.status, null, options.stack)); + console.log(xhr.statusText); + reject(HTTPError(xhr.status, xhr.statusText, options.stack)); }; xhr.send( options.data || null ); }); @@ -94,9 +92,6 @@ var TextSecureServer = (function() { function retry_ajax(url, options, limit, count) { count = count || 0; limit = limit || 3; - if (options.ports) { - options.port = options.ports[count % options.ports.length]; - } count++; return promise_ajax(url, options).catch(function(e) { if (e.name === 'HTTPError' && e.code === -1 && count < limit) { @@ -140,11 +135,10 @@ var TextSecureServer = (function() { profile : "v1/profile" }; - function TextSecureServer(url, ports, username, password) { + function TextSecureServer(url, username, password) { if (typeof url !== 'string') { throw new Error('Invalid server url'); } - this.portManager = new PortManager(ports); this.url = url; this.username = username; this.password = password; @@ -152,16 +146,12 @@ var TextSecureServer = (function() { TextSecureServer.prototype = { constructor: TextSecureServer, - getUrl: function() { - return this.url + ':' + this.portManager.getPort(); - }, ajax: function(param) { if (!param.urlParameters) { param.urlParameters = ''; } return ajax(null, { host : this.url, - ports : this.portManager.ports, path : URL_CALLS[param.call] + param.urlParameters, type : param.httpType, data : param.jsonData && textsecure.utils.jsonThing(param.jsonData), @@ -169,7 +159,8 @@ var TextSecureServer = (function() { dataType : 'json', user : this.username, password : this.password, - validateResponse: param.validateResponse + validateResponse: param.validateResponse, + certificateAuthorities: window.config.certificateAuthorities }).catch(function(e) { var code = e.code; if (code === 200) { @@ -381,22 +372,16 @@ var TextSecureServer = (function() { }.bind(this)); }, getMessageSocket: function() { - var url = this.getUrl(); - console.log('opening message socket', url); - return new WebSocket( - url.replace('https://', 'wss://').replace('http://', 'ws://') + console.log('opening message socket', this.url); + return createSocket(this.url.replace('https://', 'wss://').replace('http://', 'ws://') + '/v1/websocket/?login=' + encodeURIComponent(this.username) + '&password=' + encodeURIComponent(this.password) - + '&agent=OWD' - ); + + '&agent=OWD'); }, getProvisioningSocket: function () { - var url = this.getUrl(); - console.log('opening provisioning socket', url); - return new WebSocket( - url.replace('https://', 'wss://').replace('http://', 'ws://') - + '/v1/websocket/provisioning/?agent=OWD' - ); + console.log('opening provisioning socket', this.url); + return createSocket(this.url.replace('https://', 'wss://').replace('http://', 'ws://') + + '/v1/websocket/provisioning/?agent=OWD'); } }; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index d000fb88b..b0d3430bb 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -2,14 +2,14 @@ * vim: ts=4:sw=4:expandtab */ -function MessageReceiver(url, ports, username, password, signalingKey) { +function MessageReceiver(url, username, password, signalingKey) { this.count = 0; this.url = url; this.signalingKey = signalingKey; this.username = username; this.password = password; - this.server = new TextSecureServer(url, ports, username, password); + this.server = new TextSecureServer(url, username, password); var address = libsignal.SignalProtocolAddress.fromString(username); this.number = address.getName(); @@ -834,8 +834,8 @@ MessageReceiver.prototype.extend({ window.textsecure = window.textsecure || {}; -textsecure.MessageReceiver = function(url, ports, username, password, signalingKey) { - var messageReceiver = new MessageReceiver(url, ports, username, password, signalingKey); +textsecure.MessageReceiver = function(url, username, password, signalingKey) { + var messageReceiver = new MessageReceiver(url, username, password, signalingKey); this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); this.getStatus = messageReceiver.getStatus.bind(messageReceiver); diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index d83d0ad0e..ae729a604 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -104,8 +104,8 @@ Message.prototype = { } }; -function MessageSender(url, ports, username, password) { - this.server = new TextSecureServer(url, ports, username, password); +function MessageSender(url, username, password) { + this.server = new TextSecureServer(url, username, password); this.pendingMessages = {}; } @@ -631,8 +631,8 @@ MessageSender.prototype = { window.textsecure = window.textsecure || {}; -textsecure.MessageSender = function(url, ports, username, password) { - var sender = new MessageSender(url, ports, username, password); +textsecure.MessageSender = function(url, username, password) { + var sender = new MessageSender(url, username, password); textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.ENCRYPT_MESSAGE); textsecure.replay.registerFunction(sender.retransmitMessage.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE); textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE); diff --git a/libtextsecure/websocket-resources.js b/libtextsecure/websocket-resources.js index 9436fd4b4..652f52552 100644 --- a/libtextsecure/websocket-resources.js +++ b/libtextsecure/websocket-resources.js @@ -94,9 +94,8 @@ socket.onmessage = function(socketMessage) { var blob = socketMessage.data; - var reader = new FileReader(); - reader.onload = function() { - var message = textsecure.protobuf.WebSocketMessage.decode(reader.result); + var handleArrayBuffer = function(buffer) { + var message = textsecure.protobuf.WebSocketMessage.decode(buffer); if (message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST ) { handleRequest( new IncomingWebSocketRequest({ @@ -126,7 +125,16 @@ } } }; - reader.readAsArrayBuffer(blob); + + if (blob instanceof ArrayBuffer) { + handleArrayBuffer(blob); + } else { + var reader = new FileReader(); + reader.onload = function() { + handleArrayBuffer(reader.result); + }; + reader.readAsArrayBuffer(blob); + } }; if (opts.keepalive) { diff --git a/main.js b/main.js index 19e87f354..19e5edfa9 100644 --- a/main.js +++ b/main.js @@ -98,6 +98,7 @@ function createWindow () { version: app.getVersion(), buildExpiration: config.get('buildExpiration'), serverUrl: config.get('serverUrl'), + certificateAuthorities: config.get('certificateAuthorities'), environment: config.environment, node_version: process.versions.node } diff --git a/package.json b/package.json index 4823af85d..5556b5b54 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "lodash": "^4.17.4", "os-locale": "^2.1.0", "semver": "^5.4.1", - "spellchecker": "^3.4.1" + "spellchecker": "^3.4.1", + "websocket": "^1.0.24" } } diff --git a/preload.js b/preload.js index 4d5940579..82be760f0 100644 --- a/preload.js +++ b/preload.js @@ -34,4 +34,7 @@ require('./js/spell_check'); require('./js/backup'); + window.nodeSetImmediate = setImmediate; + window.nodeXMLHttpRequest = require("./js/XMLHttpRequest").XMLHttpRequest; + window.nodeWebSocket = require("websocket").w3cwebsocket; })(); diff --git a/yarn.lock b/yarn.lock index 555c59a1a..bf239bb4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -652,13 +652,6 @@ cross-spawn@^3.0.0: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1194,18 +1187,6 @@ execa@^0.4.0: path-key "^1.0.0" strip-eof "^1.0.0" -execa@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.5.1.tgz#de3fb85cb8d6e91c85bcbceb164581785cb57b36" - dependencies: - cross-spawn "^4.0.0" - get-stream "^2.2.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -1449,13 +1430,6 @@ get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" -get-stream@^2.2.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" - dependencies: - object-assign "^4.0.1" - pinkie-promise "^2.0.0" - get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -2008,7 +1982,7 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -2329,18 +2303,18 @@ lodash@^3.10.1, lodash@^3.5.0, lodash@^3.7.0, lodash@~3.10.0, lodash@~3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.13.1, lodash@~4.13.1: +lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.8.0: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +lodash@~4.13.1: version "4.13.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.13.1.tgz#83e4b10913f48496d4d16fec4a560af2ee744b68" -lodash@^4.14.0, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.16.4: +lodash@~4.16.4: version "4.16.6" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777" -lodash@^4.17.4: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - lodash@~4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.3.0.tgz#efd9c4a6ec53f3b05412429915c3e4824e4d25a4" @@ -2504,7 +2478,7 @@ mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.0.0, nan@^2.3.2: +nan@^2.0.0, nan@^2.3.2, nan@^2.3.3: version "2.6.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" @@ -2714,15 +2688,7 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-locale@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.0.0.tgz#15918ded510522b81ee7ae5a309d54f639fc39a4" - dependencies: - execa "^0.5.0" - lcid "^1.0.0" - mem "^1.1.0" - -os-locale@^2.1.0: +os-locale@^2.0.0, os-locale@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" dependencies: @@ -3284,11 +3250,7 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - -semver@^5.4.1: +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" @@ -3296,6 +3258,10 @@ semver@~5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + send@0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" @@ -3750,6 +3716,12 @@ type-is@~1.6.10: media-typer "0.3.0" mime-types "~2.1.15" +typedarray-to-buffer@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.2.tgz#1017b32d984ff556eba100f501589aba1ace2e04" + dependencies: + is-typedarray "^1.0.0" + typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -3945,6 +3917,15 @@ websocket-extensions@>=0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" +websocket@^1.0.24: + version "1.0.24" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.24.tgz#74903e75f2545b6b2e1de1425bc1c905917a1890" + dependencies: + debug "^2.2.0" + nan "^2.3.3" + typedarray-to-buffer "^3.1.2" + yaeti "^0.0.6" + wgxpath@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wgxpath/-/wgxpath-1.0.0.tgz#eef8a4b9d558cc495ad3a9a2b751597ecd9af690" @@ -4078,6 +4059,10 @@ y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + yallist@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"