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
This commit is contained in:
Lilia 2017-09-01 17:58:58 +02:00 committed by Scott Nonnenberg
parent 7a2c8e815c
commit 50c470e53d
No known key found for this signature in database
GPG Key ID: A4931C09644C654B
15 changed files with 774 additions and 159 deletions

View File

@ -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',

View File

@ -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"]
}

View File

@ -1,3 +1,3 @@
{
"serverUrl": "https://textsecure-service-ca.whispersystems.org"
"serverUrl": "https://textsecure-service.whispersystems.org"
}

637
js/XMLHttpRequest.js Normal file
View File

@ -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 <dan@driverdan.com>
* @contributor David Ellis <d.f.ellis@ieee.org>
* @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");
}
}
};
};

View File

@ -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

View File

@ -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);

View File

@ -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();
}

View File

@ -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');
}
};

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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;
})();

View File

@ -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"