diff --git a/Gruntfile.js b/Gruntfile.js index 4867705056..d25be15878 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 244243b9cd..92964b2f1b 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 b52ba72b50..4152a7bddf 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 0000000000..2afd5d2bec --- /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 cd5e4028d5..2bb8dd58b3 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 6f127c5d8f..fe3ff99b3f 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 f0993dc04d..dbec1a33d2 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 1803bb30ab..e4fac16671 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 d000fb88b0..b0d3430bbf 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 d83d0ad0e2..ae729a6046 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 9436fd4b42..652f525524 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 19e87f354f..19e5edfa97 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 4823af85d1..5556b5b54f 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 4d59405796..82be760f0c 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 555c59a1a4..bf239bb4ac 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"