Refactor api.js into web_api, which encapsulates all web access
This commit is contained in:
parent
37ebe9fcec
commit
f3bd0cf903
16 changed files with 763 additions and 577 deletions
|
@ -57,7 +57,6 @@ module.exports = function(grunt) {
|
|||
'libtextsecure/helpers.js',
|
||||
'libtextsecure/stringview.js',
|
||||
'libtextsecure/event_target.js',
|
||||
'libtextsecure/api.js',
|
||||
'libtextsecure/account_manager.js',
|
||||
'libtextsecure/websocket-resources.js',
|
||||
'libtextsecure/message_receiver.js',
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
"disableAutoUpdate": false,
|
||||
"openDevTools": false,
|
||||
"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"
|
||||
],
|
||||
"certificateAuthority":
|
||||
"-----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",
|
||||
"import": false
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
(async function() {
|
||||
'use strict';
|
||||
|
||||
// We add this to window here because the default Node context is erased at the end
|
||||
// of preload.js processing
|
||||
window.setImmediate = window.nodeSetImmediate;
|
||||
|
||||
const { IdleDetector, MessageDataMigrator } = Signal.Workflow;
|
||||
const { Errors, Message } = window.Signal.Types;
|
||||
const { upgradeMessageSchema } = window.Signal.Migrations;
|
||||
|
|
678
js/modules/web_api.js
Normal file
678
js/modules/web_api.js
Normal file
|
@ -0,0 +1,678 @@
|
|||
const WebSocket = require('websocket').w3cwebsocket;
|
||||
const fetch = require('node-fetch');
|
||||
const ProxyAgent = require('proxy-agent');
|
||||
|
||||
const is = require('@sindresorhus/is');
|
||||
|
||||
/* global Buffer: false */
|
||||
/* global setTimeout: false */
|
||||
|
||||
/* eslint-disable more/no-then, no-bitwise, no-nested-ternary */
|
||||
|
||||
function _btoa(str) {
|
||||
let buffer;
|
||||
|
||||
if (str instanceof Buffer) {
|
||||
buffer = str;
|
||||
} else {
|
||||
buffer = Buffer.from(str.toString(), 'binary');
|
||||
}
|
||||
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
|
||||
const _call = object => Object.prototype.toString.call(object);
|
||||
|
||||
const ArrayBufferToString = _call(new ArrayBuffer());
|
||||
const Uint8ArrayToString = _call(new Uint8Array());
|
||||
|
||||
function _getString(thing) {
|
||||
if (typeof thing !== 'string') {
|
||||
if (_call(thing) === Uint8ArrayToString)
|
||||
return String.fromCharCode.apply(null, thing);
|
||||
if (_call(thing) === ArrayBufferToString)
|
||||
return _getString(new Uint8Array(thing));
|
||||
}
|
||||
return thing;
|
||||
}
|
||||
|
||||
function _b64ToUint6(nChr) {
|
||||
return nChr > 64 && nChr < 91
|
||||
? nChr - 65
|
||||
: nChr > 96 && nChr < 123
|
||||
? nChr - 71
|
||||
: nChr > 47 && nChr < 58
|
||||
? nChr + 4
|
||||
: nChr === 43
|
||||
? 62
|
||||
: nChr === 47
|
||||
? 63
|
||||
: 0;
|
||||
}
|
||||
|
||||
function _getStringable(thing) {
|
||||
return (
|
||||
typeof thing === 'string' ||
|
||||
typeof thing === 'number' ||
|
||||
typeof thing === 'boolean' ||
|
||||
(thing === Object(thing) &&
|
||||
(_call(thing) === ArrayBufferToString ||
|
||||
_call(thing) === Uint8ArrayToString))
|
||||
);
|
||||
}
|
||||
|
||||
function _ensureStringed(thing) {
|
||||
if (_getStringable(thing)) {
|
||||
return _getString(thing);
|
||||
} else if (thing instanceof Array) {
|
||||
const res = [];
|
||||
for (let i = 0; i < thing.length; i += 1) {
|
||||
res[i] = _ensureStringed(thing[i]);
|
||||
}
|
||||
return res;
|
||||
} else if (thing === Object(thing)) {
|
||||
const res = {};
|
||||
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
||||
for (const key in thing) {
|
||||
res[key] = _ensureStringed(thing[key]);
|
||||
}
|
||||
return res;
|
||||
} else if (thing === null) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`unsure of how to jsonify object of type ${typeof thing}`);
|
||||
}
|
||||
|
||||
function _jsonThing(thing) {
|
||||
return JSON.stringify(_ensureStringed(thing));
|
||||
}
|
||||
|
||||
function _base64ToBytes(sBase64, nBlocksSize) {
|
||||
const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, '');
|
||||
const nInLen = sB64Enc.length;
|
||||
const nOutLen = nBlocksSize
|
||||
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
|
||||
: (nInLen * 3 + 1) >> 2;
|
||||
const aBBytes = new ArrayBuffer(nOutLen);
|
||||
const taBytes = new Uint8Array(aBBytes);
|
||||
|
||||
for (
|
||||
let nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0;
|
||||
nInIdx < nInLen;
|
||||
nInIdx += 1
|
||||
) {
|
||||
nMod4 = nInIdx & 3;
|
||||
nUint24 |= _b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4);
|
||||
if (nMod4 === 3 || nInLen - nInIdx === 1) {
|
||||
for (
|
||||
nMod3 = 0;
|
||||
nMod3 < 3 && nOutIdx < nOutLen;
|
||||
nMod3 += 1, nOutIdx += 1
|
||||
) {
|
||||
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
|
||||
}
|
||||
nUint24 = 0;
|
||||
}
|
||||
}
|
||||
return aBBytes;
|
||||
}
|
||||
|
||||
function _validateResponse(response, schema) {
|
||||
try {
|
||||
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
||||
for (const i in schema) {
|
||||
switch (schema[i]) {
|
||||
case 'object':
|
||||
case 'string':
|
||||
case 'number':
|
||||
// eslint-disable-next-line valid-typeof
|
||||
if (typeof response[i] !== schema[i]) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _createSocket(url, { certificateAuthority, proxyUrl }) {
|
||||
let requestOptions;
|
||||
if (proxyUrl) {
|
||||
requestOptions = {
|
||||
ca: certificateAuthority,
|
||||
agent: new ProxyAgent(proxyUrl),
|
||||
};
|
||||
} else {
|
||||
requestOptions = {
|
||||
ca: certificateAuthority,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
return new WebSocket(url, null, null, null, requestOptions);
|
||||
}
|
||||
|
||||
function _promiseAjax(providedUrl, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = providedUrl || `${options.host}/${options.path}`;
|
||||
console.log(options.type, url);
|
||||
const timeout =
|
||||
typeof options.timeout !== 'undefined' ? options.timeout : 10000;
|
||||
|
||||
const { proxyUrl } = options;
|
||||
let agent;
|
||||
if (proxyUrl) {
|
||||
agent = new ProxyAgent(proxyUrl);
|
||||
}
|
||||
|
||||
const fetchOptions = {
|
||||
method: options.type,
|
||||
body: options.data || null,
|
||||
headers: { 'X-Signal-Agent': 'OWD' },
|
||||
agent,
|
||||
ca: options.certificateAuthority,
|
||||
timeout,
|
||||
};
|
||||
|
||||
if (fetchOptions.body instanceof ArrayBuffer) {
|
||||
// node-fetch doesn't support ArrayBuffer, only node Buffer
|
||||
const contentLength = fetchOptions.body.byteLength;
|
||||
fetchOptions.body = Buffer.from(fetchOptions.body);
|
||||
|
||||
// node-fetch doesn't set content-length like S3 requires
|
||||
fetchOptions.headers['Content-Length'] = contentLength;
|
||||
}
|
||||
|
||||
if (options.user && options.password) {
|
||||
const user = _getString(options.user);
|
||||
const password = _getString(options.password);
|
||||
const auth = _btoa(`${user}:${password}`);
|
||||
fetchOptions.headers.Authorization = `Basic ${auth}`;
|
||||
}
|
||||
if (options.contentType) {
|
||||
fetchOptions.headers['Content-Type'] = options.contentType;
|
||||
}
|
||||
fetch(url, fetchOptions)
|
||||
.then(response => {
|
||||
let resultPromise;
|
||||
if (
|
||||
options.responseType === 'json' &&
|
||||
response.headers.get('Content-Type') === 'application/json'
|
||||
) {
|
||||
resultPromise = response.json();
|
||||
} else if (options.responseType === 'arraybuffer') {
|
||||
resultPromise = response.buffer();
|
||||
} else {
|
||||
resultPromise = response.text();
|
||||
}
|
||||
return resultPromise.then(result => {
|
||||
if (options.responseType === 'arraybuffer') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result = result.buffer.slice(
|
||||
result.byteOffset,
|
||||
result.byteOffset + result.byteLength
|
||||
);
|
||||
}
|
||||
if (options.responseType === 'json') {
|
||||
if (options.validateResponse) {
|
||||
if (!_validateResponse(result, options.validateResponse)) {
|
||||
console.log(options.type, url, response.status, 'Error');
|
||||
reject(
|
||||
HTTPError(
|
||||
'promiseAjax: invalid response',
|
||||
response.status,
|
||||
result,
|
||||
options.stack
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.status >= 0 && response.status < 400) {
|
||||
console.log(options.type, url, response.status, 'Success');
|
||||
resolve(result, response.status);
|
||||
} else {
|
||||
console.log(options.type, url, response.status, 'Error');
|
||||
reject(
|
||||
HTTPError(
|
||||
'promiseAjax: error response',
|
||||
response.status,
|
||||
result,
|
||||
options.stack
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(options.type, url, 0, 'Error');
|
||||
const stack = `${e.stack}\nInitial stack:\n${options.stack}`;
|
||||
reject(HTTPError('promiseAjax catch', 0, e.toString(), stack));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _retryAjax(url, options, providedLimit, providedCount) {
|
||||
const count = (providedCount || 0) + 1;
|
||||
const limit = providedLimit || 3;
|
||||
return _promiseAjax(url, options).catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code === -1 && count < limit) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(_retryAjax(url, options, limit, count));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function _outerAjax(url, options) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.stack = new Error().stack; // just in case, save stack here.
|
||||
return _retryAjax(url, options);
|
||||
}
|
||||
|
||||
function HTTPError(message, providedCode, response, stack) {
|
||||
const code = providedCode > 999 || providedCode < 100 ? -1 : providedCode;
|
||||
const e = new Error(`${message}; code: ${code}`);
|
||||
e.name = 'HTTPError';
|
||||
e.code = code;
|
||||
e.stack += `\nOriginal stack:\n${stack}`;
|
||||
if (response) {
|
||||
e.response = response;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
const URL_CALLS = {
|
||||
accounts: 'v1/accounts',
|
||||
devices: 'v1/devices',
|
||||
keys: 'v2/keys',
|
||||
signed: 'v2/keys/signed',
|
||||
messages: 'v1/messages',
|
||||
attachment: 'v1/attachments',
|
||||
profile: 'v1/profile',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
};
|
||||
|
||||
// We first set up the data that won't change during this session of the app
|
||||
function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
||||
if (!is.string(url)) {
|
||||
throw new Error('WebAPI.initialize: Invalid server url');
|
||||
}
|
||||
if (!is.string(cdnUrl)) {
|
||||
throw new Error('WebAPI.initialize: Invalid cdnUrl');
|
||||
}
|
||||
if (!is.string(certificateAuthority)) {
|
||||
throw new Error('WebAPI.initialize: Invalid certificateAuthority');
|
||||
}
|
||||
|
||||
// Thanks to function-hoisting, we can put this return statement before all of the
|
||||
// below function definitions.
|
||||
return {
|
||||
connect,
|
||||
};
|
||||
|
||||
// Then we connect to the server with user-specific information. This is the only API
|
||||
// exposed to the browser context, ensuring that it can't connect to arbitrary
|
||||
// locations.
|
||||
function connect({ username: initialUsername, password: initialPassword }) {
|
||||
let username = initialUsername;
|
||||
let password = initialPassword;
|
||||
|
||||
// Thanks, function hoisting!
|
||||
return {
|
||||
confirmCode,
|
||||
getAttachment,
|
||||
getAvatar,
|
||||
getDevices,
|
||||
getKeysForNumber,
|
||||
getMessageSocket,
|
||||
getMyKeys,
|
||||
getProfile,
|
||||
getProvisioningSocket,
|
||||
putAttachment,
|
||||
registerKeys,
|
||||
requestVerificationSMS,
|
||||
requestVerificationVoice,
|
||||
sendMessages,
|
||||
setSignedPreKey,
|
||||
};
|
||||
|
||||
function _ajax(param) {
|
||||
if (!param.urlParameters) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
param.urlParameters = '';
|
||||
}
|
||||
return _outerAjax(null, {
|
||||
certificateAuthority,
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
data: param.jsonData && _jsonThing(param.jsonData),
|
||||
host: url,
|
||||
password,
|
||||
path: URL_CALLS[param.call] + param.urlParameters,
|
||||
proxyUrl,
|
||||
responseType: param.responseType,
|
||||
timeout: param.timeout,
|
||||
type: param.httpType,
|
||||
user: username,
|
||||
validateResponse: param.validateResponse,
|
||||
}).catch(e => {
|
||||
const { code } = e;
|
||||
if (code === 200) {
|
||||
// happens sometimes when we get no response
|
||||
// (TODO: Fix server to return 204? instead)
|
||||
return null;
|
||||
}
|
||||
let message;
|
||||
switch (code) {
|
||||
case -1:
|
||||
message =
|
||||
'Failed to connect to the server, please check your network connection.';
|
||||
break;
|
||||
case 413:
|
||||
message = 'Rate limit exceeded, please try again later.';
|
||||
break;
|
||||
case 403:
|
||||
message = 'Invalid code, please try again.';
|
||||
break;
|
||||
case 417:
|
||||
// TODO: This shouldn't be a thing?, but its in the API doc?
|
||||
message = 'Number already registered.';
|
||||
break;
|
||||
case 401:
|
||||
message =
|
||||
'Invalid authentication, most likely someone re-registered and invalidated our registration.';
|
||||
break;
|
||||
case 404:
|
||||
message = 'Number is not registered.';
|
||||
break;
|
||||
default:
|
||||
message =
|
||||
'The server rejected our query, please file a bug report.';
|
||||
}
|
||||
e.message = message;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function getProfile(number) {
|
||||
return _ajax({
|
||||
call: 'profile',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}`,
|
||||
responseType: 'json',
|
||||
});
|
||||
}
|
||||
|
||||
function getAvatar(path) {
|
||||
// Using _outerAJAX, since it's not hardcoded to the Signal Server. Unlike our
|
||||
// attachment CDN, it uses our self-signed certificate, so we pass it in.
|
||||
return _outerAjax(`${cdnUrl}/${path}`, {
|
||||
certificateAuthority,
|
||||
contentType: 'application/octet-stream',
|
||||
proxyUrl,
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 0,
|
||||
type: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
function requestVerificationSMS(number) {
|
||||
return _ajax({
|
||||
call: 'accounts',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/sms/code/${number}`,
|
||||
});
|
||||
}
|
||||
|
||||
function requestVerificationVoice(number) {
|
||||
return _ajax({
|
||||
call: 'accounts',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/voice/code/${number}`,
|
||||
});
|
||||
}
|
||||
|
||||
async function confirmCode(
|
||||
number,
|
||||
code,
|
||||
newPassword,
|
||||
signalingKey,
|
||||
registrationId,
|
||||
deviceName
|
||||
) {
|
||||
const jsonData = {
|
||||
signalingKey: _btoa(_getString(signalingKey)),
|
||||
supportsSms: false,
|
||||
fetchesMessages: true,
|
||||
registrationId,
|
||||
};
|
||||
|
||||
let call;
|
||||
let urlPrefix;
|
||||
let schema;
|
||||
let responseType;
|
||||
|
||||
if (deviceName) {
|
||||
jsonData.name = deviceName;
|
||||
call = 'devices';
|
||||
urlPrefix = '/';
|
||||
schema = { deviceId: 'number' };
|
||||
responseType = 'json';
|
||||
} else {
|
||||
call = 'accounts';
|
||||
urlPrefix = '/code/';
|
||||
}
|
||||
|
||||
// We update our saved username and password, since we're creating a new account
|
||||
username = number;
|
||||
password = newPassword;
|
||||
|
||||
const response = await _ajax({
|
||||
call,
|
||||
httpType: 'PUT',
|
||||
urlParameters: urlPrefix + code,
|
||||
jsonData,
|
||||
responseType,
|
||||
validateResponse: schema,
|
||||
});
|
||||
|
||||
// From here on out, our username will be our phone number combined with device
|
||||
username = `${number}.${response.deviceId || 1}`;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function getDevices() {
|
||||
return _ajax({
|
||||
call: 'devices',
|
||||
httpType: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
function registerKeys(genKeys) {
|
||||
const keys = {};
|
||||
keys.identityKey = _btoa(_getString(genKeys.identityKey));
|
||||
keys.signedPreKey = {
|
||||
keyId: genKeys.signedPreKey.keyId,
|
||||
publicKey: _btoa(_getString(genKeys.signedPreKey.publicKey)),
|
||||
signature: _btoa(_getString(genKeys.signedPreKey.signature)),
|
||||
};
|
||||
|
||||
keys.preKeys = [];
|
||||
let j = 0;
|
||||
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
||||
for (const i in genKeys.preKeys) {
|
||||
keys.preKeys[j] = {
|
||||
keyId: genKeys.preKeys[i].keyId,
|
||||
publicKey: _btoa(_getString(genKeys.preKeys[i].publicKey)),
|
||||
};
|
||||
j += 1;
|
||||
}
|
||||
|
||||
// This is just to make the server happy
|
||||
// (v2 clients should choke on publicKey)
|
||||
keys.lastResortKey = { keyId: 0x7fffffff, publicKey: _btoa('42') };
|
||||
|
||||
return _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'PUT',
|
||||
jsonData: keys,
|
||||
});
|
||||
}
|
||||
|
||||
function setSignedPreKey(signedPreKey) {
|
||||
return _ajax({
|
||||
call: 'signed',
|
||||
httpType: 'PUT',
|
||||
jsonData: {
|
||||
keyId: signedPreKey.keyId,
|
||||
publicKey: _btoa(_getString(signedPreKey.publicKey)),
|
||||
signature: _btoa(_getString(signedPreKey.signature)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getMyKeys() {
|
||||
return _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
validateResponse: { count: 'number' },
|
||||
}).then(res => res.count);
|
||||
}
|
||||
|
||||
function getKeysForNumber(number, deviceId = '*') {
|
||||
return _ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}/${deviceId}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
}).then(res => {
|
||||
if (res.devices.constructor !== Array) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
res.identityKey = _base64ToBytes(res.identityKey);
|
||||
res.devices.forEach(device => {
|
||||
if (
|
||||
!_validateResponse(device, { signedPreKey: 'object' }) ||
|
||||
!_validateResponse(device.signedPreKey, {
|
||||
publicKey: 'string',
|
||||
signature: 'string',
|
||||
})
|
||||
) {
|
||||
throw new Error('Invalid signedPreKey');
|
||||
}
|
||||
if (device.preKey) {
|
||||
if (
|
||||
!_validateResponse(device, { preKey: 'object' }) ||
|
||||
!_validateResponse(device.preKey, { publicKey: 'string' })
|
||||
) {
|
||||
throw new Error('Invalid preKey');
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.preKey.publicKey = _base64ToBytes(device.preKey.publicKey);
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.signedPreKey.publicKey = _base64ToBytes(
|
||||
device.signedPreKey.publicKey
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.signedPreKey.signature = _base64ToBytes(
|
||||
device.signedPreKey.signature
|
||||
);
|
||||
});
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function sendMessages(destination, messageArray, timestamp, silent) {
|
||||
const jsonData = { messages: messageArray, timestamp };
|
||||
|
||||
if (silent) {
|
||||
jsonData.silent = true;
|
||||
}
|
||||
|
||||
return _ajax({
|
||||
call: 'messages',
|
||||
httpType: 'PUT',
|
||||
urlParameters: `/${destination}`,
|
||||
jsonData,
|
||||
responseType: 'json',
|
||||
});
|
||||
}
|
||||
|
||||
function getAttachment(id) {
|
||||
return _ajax({
|
||||
call: 'attachment',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${id}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { location: 'string' },
|
||||
}).then(response =>
|
||||
// Using _outerAJAX, since it's not hardcoded to the Signal Server
|
||||
_outerAjax(response.location, {
|
||||
contentType: 'application/octet-stream',
|
||||
proxyUrl,
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 0,
|
||||
type: 'GET',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function putAttachment(encryptedBin) {
|
||||
return _ajax({
|
||||
call: 'attachment',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
}).then(response =>
|
||||
// Using _outerAJAX, since it's not hardcoded to the Signal Server
|
||||
_outerAjax(response.location, {
|
||||
contentType: 'application/octet-stream',
|
||||
data: encryptedBin,
|
||||
processData: false,
|
||||
proxyUrl,
|
||||
timeout: 0,
|
||||
type: 'PUT',
|
||||
}).then(() => response.idString)
|
||||
);
|
||||
}
|
||||
|
||||
function getMessageSocket() {
|
||||
console.log('opening message socket', url);
|
||||
const fixedScheme = url
|
||||
.replace('https://', 'wss://')
|
||||
.replace('http://', 'ws://');
|
||||
const login = encodeURIComponent(username);
|
||||
const pass = encodeURIComponent(password);
|
||||
|
||||
return _createSocket(
|
||||
`${fixedScheme}/v1/websocket/?login=${login}&password=${pass}&agent=OWD`,
|
||||
{ certificateAuthority, proxyUrl }
|
||||
);
|
||||
}
|
||||
|
||||
function getProvisioningSocket() {
|
||||
console.log('opening provisioning socket', url);
|
||||
const fixedScheme = url
|
||||
.replace('https://', 'wss://')
|
||||
.replace('http://', 'ws://');
|
||||
|
||||
return _createSocket(
|
||||
`${fixedScheme}/v1/websocket/provisioning/?agent=OWD`,
|
||||
{ certificateAuthority, proxyUrl }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
var ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function AccountManager(url, username, password) {
|
||||
this.server = new TextSecureServer(url, username, password);
|
||||
this.server = window.WebAPI.connect({ username, password });
|
||||
this.pending = Promise.resolve();
|
||||
}
|
||||
|
||||
|
@ -426,7 +426,6 @@
|
|||
'regionCode',
|
||||
libphonenumber.util.getRegionCodeForNumber(number)
|
||||
);
|
||||
this.server.username = textsecure.storage.get('number_id');
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,517 +0,0 @@
|
|||
/* global nodeBuffer: false */
|
||||
/* global nodeWebSocket: false */
|
||||
/* global nodeFetch: false */
|
||||
/* global nodeSetImmediate: false */
|
||||
/* global ProxyAgent: false */
|
||||
|
||||
/* global window: false */
|
||||
/* global getString: false */
|
||||
/* global btoa: false */
|
||||
/* global StringView: false */
|
||||
/* global textsecure: false */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars, func-names
|
||||
const TextSecureServer = (function() {
|
||||
function validateResponse(response, schema) {
|
||||
try {
|
||||
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
||||
for (const i in schema) {
|
||||
switch (schema[i]) {
|
||||
case 'object':
|
||||
case 'string':
|
||||
case 'number':
|
||||
// eslint-disable-next-line valid-typeof
|
||||
if (typeof response[i] !== schema[i]) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function createSocket(url) {
|
||||
const { proxyUrl } = window.config;
|
||||
let requestOptions;
|
||||
if (proxyUrl) {
|
||||
requestOptions = {
|
||||
ca: window.config.certificateAuthorities,
|
||||
agent: new ProxyAgent(proxyUrl),
|
||||
};
|
||||
} else {
|
||||
requestOptions = {
|
||||
ca: window.config.certificateAuthorities,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
return new nodeWebSocket(url, null, null, null, requestOptions);
|
||||
}
|
||||
|
||||
// We add this to window here because the default Node context is erased at the end
|
||||
// of preload.js processing
|
||||
window.setImmediate = nodeSetImmediate;
|
||||
|
||||
function promiseAjax(providedUrl, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = providedUrl || `${options.host}/${options.path}`;
|
||||
console.log(options.type, url);
|
||||
const timeout =
|
||||
typeof options.timeout !== 'undefined' ? options.timeout : 10000;
|
||||
|
||||
const { proxyUrl } = window.config;
|
||||
let agent;
|
||||
if (proxyUrl) {
|
||||
agent = new ProxyAgent(proxyUrl);
|
||||
}
|
||||
|
||||
const fetchOptions = {
|
||||
method: options.type,
|
||||
body: options.data || null,
|
||||
headers: { 'X-Signal-Agent': 'OWD' },
|
||||
agent,
|
||||
ca: options.certificateAuthorities,
|
||||
timeout,
|
||||
};
|
||||
|
||||
if (fetchOptions.body instanceof ArrayBuffer) {
|
||||
// node-fetch doesn't support ArrayBuffer, only node Buffer
|
||||
const contentLength = fetchOptions.body.byteLength;
|
||||
fetchOptions.body = nodeBuffer.from(fetchOptions.body);
|
||||
|
||||
// node-fetch doesn't set content-length like S3 requires
|
||||
fetchOptions.headers['Content-Length'] = contentLength;
|
||||
}
|
||||
|
||||
if (options.user && options.password) {
|
||||
const user = getString(options.user);
|
||||
const password = getString(options.password);
|
||||
const auth = btoa(`${user}:${password}`);
|
||||
fetchOptions.headers.Authorization = `Basic ${auth}`;
|
||||
}
|
||||
if (options.contentType) {
|
||||
fetchOptions.headers['Content-Type'] = options.contentType;
|
||||
}
|
||||
nodeFetch(url, fetchOptions)
|
||||
.then(response => {
|
||||
let resultPromise;
|
||||
if (
|
||||
options.responseType === 'json' &&
|
||||
response.headers.get('Content-Type') === 'application/json'
|
||||
) {
|
||||
resultPromise = response.json();
|
||||
} else if (options.responseType === 'arraybuffer') {
|
||||
resultPromise = response.buffer();
|
||||
} else {
|
||||
resultPromise = response.text();
|
||||
}
|
||||
return resultPromise.then(result => {
|
||||
if (options.responseType === 'arraybuffer') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
result = result.buffer.slice(
|
||||
result.byteOffset,
|
||||
result.byteOffset + result.byteLength
|
||||
);
|
||||
}
|
||||
if (options.responseType === 'json') {
|
||||
if (options.validateResponse) {
|
||||
if (!validateResponse(result, options.validateResponse)) {
|
||||
console.log(options.type, url, response.status, 'Error');
|
||||
reject(
|
||||
HTTPError(
|
||||
'promiseAjax: invalid response',
|
||||
response.status,
|
||||
result,
|
||||
options.stack
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.status >= 0 && response.status < 400) {
|
||||
console.log(options.type, url, response.status, 'Success');
|
||||
resolve(result, response.status);
|
||||
} else {
|
||||
console.log(options.type, url, response.status, 'Error');
|
||||
reject(
|
||||
HTTPError(
|
||||
'promiseAjax: error response',
|
||||
response.status,
|
||||
result,
|
||||
options.stack
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(options.type, url, 0, 'Error');
|
||||
const stack = `${e.stack}\nInitial stack:\n${options.stack}`;
|
||||
reject(HTTPError('promiseAjax catch', 0, e.toString(), stack));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function retryAjax(url, options, providedLimit, providedCount) {
|
||||
const count = (providedCount || 0) + 1;
|
||||
const limit = providedLimit || 3;
|
||||
return promiseAjax(url, options).catch(e => {
|
||||
if (e.name === 'HTTPError' && e.code === -1 && count < limit) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(retryAjax(url, options, limit, count));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
function ajax(url, options) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
options.stack = new Error().stack; // just in case, save stack here.
|
||||
return retryAjax(url, options);
|
||||
}
|
||||
|
||||
function HTTPError(message, providedCode, response, stack) {
|
||||
const code = providedCode > 999 || providedCode < 100 ? -1 : providedCode;
|
||||
const e = new Error(`${message}; code: ${code}`);
|
||||
e.name = 'HTTPError';
|
||||
e.code = code;
|
||||
e.stack += `\nOriginal stack:\n${stack}`;
|
||||
if (response) {
|
||||
e.response = response;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
const URL_CALLS = {
|
||||
accounts: 'v1/accounts',
|
||||
devices: 'v1/devices',
|
||||
keys: 'v2/keys',
|
||||
signed: 'v2/keys/signed',
|
||||
messages: 'v1/messages',
|
||||
attachment: 'v1/attachments',
|
||||
profile: 'v1/profile',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
function TextSecureServer(url, username, password, cdnUrl) {
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('Invalid server url');
|
||||
}
|
||||
this.url = url;
|
||||
this.cdnUrl = cdnUrl;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
TextSecureServer.prototype = {
|
||||
constructor: TextSecureServer,
|
||||
ajax(param) {
|
||||
if (!param.urlParameters) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
param.urlParameters = '';
|
||||
}
|
||||
return ajax(null, {
|
||||
host: this.url,
|
||||
path: URL_CALLS[param.call] + param.urlParameters,
|
||||
type: param.httpType,
|
||||
data: param.jsonData && textsecure.utils.jsonThing(param.jsonData),
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
responseType: param.responseType,
|
||||
user: this.username,
|
||||
password: this.password,
|
||||
validateResponse: param.validateResponse,
|
||||
certificateAuthorities: window.config.certificateAuthorities,
|
||||
timeout: param.timeout,
|
||||
}).catch(e => {
|
||||
const { code } = e;
|
||||
if (code === 200) {
|
||||
// happens sometimes when we get no response
|
||||
// (TODO: Fix server to return 204? instead)
|
||||
return null;
|
||||
}
|
||||
let message;
|
||||
switch (code) {
|
||||
case -1:
|
||||
message =
|
||||
'Failed to connect to the server, please check your network connection.';
|
||||
break;
|
||||
case 413:
|
||||
message = 'Rate limit exceeded, please try again later.';
|
||||
break;
|
||||
case 403:
|
||||
message = 'Invalid code, please try again.';
|
||||
break;
|
||||
case 417:
|
||||
// TODO: This shouldn't be a thing?, but its in the API doc?
|
||||
message = 'Number already registered.';
|
||||
break;
|
||||
case 401:
|
||||
message =
|
||||
'Invalid authentication, most likely someone re-registered and invalidated our registration.';
|
||||
break;
|
||||
case 404:
|
||||
message = 'Number is not registered.';
|
||||
break;
|
||||
default:
|
||||
message =
|
||||
'The server rejected our query, please file a bug report.';
|
||||
}
|
||||
e.message = message;
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
getProfile(number) {
|
||||
return this.ajax({
|
||||
call: 'profile',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}`,
|
||||
responseType: 'json',
|
||||
});
|
||||
},
|
||||
getAvatar(path) {
|
||||
return ajax(`${this.cdnUrl}/${path}`, {
|
||||
type: 'GET',
|
||||
responseType: 'arraybuffer',
|
||||
contentType: 'application/octet-stream',
|
||||
certificateAuthorities: window.config.certificateAuthorities,
|
||||
timeout: 0,
|
||||
});
|
||||
},
|
||||
requestVerificationSMS(number) {
|
||||
return this.ajax({
|
||||
call: 'accounts',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/sms/code/${number}`,
|
||||
});
|
||||
},
|
||||
requestVerificationVoice(number) {
|
||||
return this.ajax({
|
||||
call: 'accounts',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/voice/code/${number}`,
|
||||
});
|
||||
},
|
||||
confirmCode(
|
||||
number,
|
||||
code,
|
||||
password,
|
||||
signalingKey,
|
||||
registrationId,
|
||||
deviceName
|
||||
) {
|
||||
const jsonData = {
|
||||
signalingKey: btoa(getString(signalingKey)),
|
||||
supportsSms: false,
|
||||
fetchesMessages: true,
|
||||
registrationId,
|
||||
};
|
||||
|
||||
let call;
|
||||
let urlPrefix;
|
||||
let schema;
|
||||
let responseType;
|
||||
|
||||
if (deviceName) {
|
||||
jsonData.name = deviceName;
|
||||
call = 'devices';
|
||||
urlPrefix = '/';
|
||||
schema = { deviceId: 'number' };
|
||||
responseType = 'json';
|
||||
} else {
|
||||
call = 'accounts';
|
||||
urlPrefix = '/code/';
|
||||
}
|
||||
|
||||
this.username = number;
|
||||
this.password = password;
|
||||
return this.ajax({
|
||||
call,
|
||||
httpType: 'PUT',
|
||||
urlParameters: urlPrefix + code,
|
||||
jsonData,
|
||||
responseType,
|
||||
validateResponse: schema,
|
||||
});
|
||||
},
|
||||
getDevices() {
|
||||
return this.ajax({
|
||||
call: 'devices',
|
||||
httpType: 'GET',
|
||||
});
|
||||
},
|
||||
registerKeys(genKeys) {
|
||||
const keys = {};
|
||||
keys.identityKey = btoa(getString(genKeys.identityKey));
|
||||
keys.signedPreKey = {
|
||||
keyId: genKeys.signedPreKey.keyId,
|
||||
publicKey: btoa(getString(genKeys.signedPreKey.publicKey)),
|
||||
signature: btoa(getString(genKeys.signedPreKey.signature)),
|
||||
};
|
||||
|
||||
keys.preKeys = [];
|
||||
let j = 0;
|
||||
// eslint-disable-next-line guard-for-in, no-restricted-syntax
|
||||
for (const i in genKeys.preKeys) {
|
||||
keys.preKeys[j] = {
|
||||
keyId: genKeys.preKeys[i].keyId,
|
||||
publicKey: btoa(getString(genKeys.preKeys[i].publicKey)),
|
||||
};
|
||||
j += 1;
|
||||
}
|
||||
|
||||
// This is just to make the server happy
|
||||
// (v2 clients should choke on publicKey)
|
||||
keys.lastResortKey = { keyId: 0x7fffffff, publicKey: btoa('42') };
|
||||
|
||||
return this.ajax({
|
||||
call: 'keys',
|
||||
httpType: 'PUT',
|
||||
jsonData: keys,
|
||||
});
|
||||
},
|
||||
setSignedPreKey(signedPreKey) {
|
||||
return this.ajax({
|
||||
call: 'signed',
|
||||
httpType: 'PUT',
|
||||
jsonData: {
|
||||
keyId: signedPreKey.keyId,
|
||||
publicKey: btoa(getString(signedPreKey.publicKey)),
|
||||
signature: btoa(getString(signedPreKey.signature)),
|
||||
},
|
||||
});
|
||||
},
|
||||
getMyKeys() {
|
||||
return this.ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
validateResponse: { count: 'number' },
|
||||
}).then(res => res.count);
|
||||
},
|
||||
getKeysForNumber(number, deviceId = '*') {
|
||||
return this.ajax({
|
||||
call: 'keys',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${number}/${deviceId}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { identityKey: 'string', devices: 'object' },
|
||||
}).then(res => {
|
||||
if (res.devices.constructor !== Array) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
res.identityKey = StringView.base64ToBytes(res.identityKey);
|
||||
res.devices.forEach(device => {
|
||||
if (
|
||||
!validateResponse(device, { signedPreKey: 'object' }) ||
|
||||
!validateResponse(device.signedPreKey, {
|
||||
publicKey: 'string',
|
||||
signature: 'string',
|
||||
})
|
||||
) {
|
||||
throw new Error('Invalid signedPreKey');
|
||||
}
|
||||
if (device.preKey) {
|
||||
if (
|
||||
!validateResponse(device, { preKey: 'object' }) ||
|
||||
!validateResponse(device.preKey, { publicKey: 'string' })
|
||||
) {
|
||||
throw new Error('Invalid preKey');
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.preKey.publicKey = StringView.base64ToBytes(
|
||||
device.preKey.publicKey
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.signedPreKey.publicKey = StringView.base64ToBytes(
|
||||
device.signedPreKey.publicKey
|
||||
);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
device.signedPreKey.signature = StringView.base64ToBytes(
|
||||
device.signedPreKey.signature
|
||||
);
|
||||
});
|
||||
return res;
|
||||
});
|
||||
},
|
||||
sendMessages(destination, messageArray, timestamp, silent) {
|
||||
const jsonData = { messages: messageArray, timestamp };
|
||||
|
||||
if (silent) {
|
||||
jsonData.silent = true;
|
||||
}
|
||||
|
||||
return this.ajax({
|
||||
call: 'messages',
|
||||
httpType: 'PUT',
|
||||
urlParameters: `/${destination}`,
|
||||
jsonData,
|
||||
responseType: 'json',
|
||||
});
|
||||
},
|
||||
getAttachment(id) {
|
||||
return this.ajax({
|
||||
call: 'attachment',
|
||||
httpType: 'GET',
|
||||
urlParameters: `/${id}`,
|
||||
responseType: 'json',
|
||||
validateResponse: { location: 'string' },
|
||||
}).then(response =>
|
||||
ajax(response.location, {
|
||||
timeout: 0,
|
||||
type: 'GET',
|
||||
responseType: 'arraybuffer',
|
||||
contentType: 'application/octet-stream',
|
||||
})
|
||||
);
|
||||
},
|
||||
putAttachment(encryptedBin) {
|
||||
return this.ajax({
|
||||
call: 'attachment',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
}).then(response =>
|
||||
ajax(response.location, {
|
||||
timeout: 0,
|
||||
type: 'PUT',
|
||||
contentType: 'application/octet-stream',
|
||||
data: encryptedBin,
|
||||
processData: false,
|
||||
}).then(() => response.idString)
|
||||
);
|
||||
},
|
||||
getMessageSocket() {
|
||||
console.log('opening message socket', this.url);
|
||||
const fixedScheme = this.url
|
||||
.replace('https://', 'wss://')
|
||||
.replace('http://', 'ws://');
|
||||
const login = encodeURIComponent(this.username);
|
||||
const password = encodeURIComponent(this.password);
|
||||
|
||||
return createSocket(
|
||||
`${fixedScheme}/v1/websocket/?login=${login}&password=${password}&agent=OWD`
|
||||
);
|
||||
},
|
||||
getProvisioningSocket() {
|
||||
console.log('opening provisioning socket', this.url);
|
||||
const fixedScheme = this.url
|
||||
.replace('https://', 'wss://')
|
||||
.replace('http://', 'ws://');
|
||||
|
||||
return createSocket(
|
||||
`${fixedScheme}/v1/websocket/provisioning/?agent=OWD`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return TextSecureServer;
|
||||
})();
|
|
@ -1,6 +1,6 @@
|
|||
/* global window: false */
|
||||
/* global textsecure: false */
|
||||
/* global TextSecureServer: false */
|
||||
/* global WebAPI: false */
|
||||
/* global libsignal: false */
|
||||
/* global WebSocketResource: false */
|
||||
/* global WebSocket: false */
|
||||
|
@ -19,7 +19,7 @@ function MessageReceiver(url, username, password, signalingKey, options = {}) {
|
|||
this.signalingKey = signalingKey;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.server = new TextSecureServer(url, username, password);
|
||||
this.server = WebAPI.connect({ username, password });
|
||||
|
||||
const address = libsignal.SignalProtocolAddress.fromString(username);
|
||||
this.number = address.getName();
|
||||
|
|
|
@ -140,7 +140,7 @@ Message.prototype = {
|
|||
};
|
||||
|
||||
function MessageSender(url, username, password, cdn_url) {
|
||||
this.server = new TextSecureServer(url, username, password, cdn_url);
|
||||
this.server = WebAPI.connect({ username, password });
|
||||
this.pendingMessages = {};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,15 +2,6 @@
|
|||
|
||||
describe('AccountManager', function() {
|
||||
let accountManager;
|
||||
let originalServer;
|
||||
|
||||
before(function() {
|
||||
originalServer = window.TextSecureServer;
|
||||
window.TextSecureServer = function() {};
|
||||
});
|
||||
after(function() {
|
||||
window.TextSecureServer = originalServer;
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
accountManager = new window.textsecure.AccountManager();
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
var getKeysForNumberMap = {};
|
||||
TextSecureServer.getKeysForNumber = function(number, deviceId) {
|
||||
var res = getKeysForNumberMap[number];
|
||||
if (res !== undefined) {
|
||||
delete getKeysForNumberMap[number];
|
||||
return Promise.resolve(res);
|
||||
} else throw new Error('getKeysForNumber of unknown/used number');
|
||||
};
|
||||
|
||||
var messagesSentMap = {};
|
||||
TextSecureServer.sendMessages = function(destination, messageArray) {
|
||||
for (i in messageArray) {
|
||||
var msg = messageArray[i];
|
||||
if (
|
||||
(msg.type != 1 && msg.type != 3) ||
|
||||
msg.destinationDeviceId === undefined ||
|
||||
msg.destinationRegistrationId === undefined ||
|
||||
msg.body === undefined ||
|
||||
msg.timestamp == undefined ||
|
||||
msg.relay !== undefined ||
|
||||
msg.destination !== undefined
|
||||
)
|
||||
throw new Error('Invalid message');
|
||||
|
||||
messagesSentMap[
|
||||
destination + '.' + messageArray[i].destinationDeviceId
|
||||
] = msg;
|
||||
}
|
||||
};
|
56
libtextsecure/test/fake_web_api.js
Normal file
56
libtextsecure/test/fake_web_api.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
window.setImmediate = window.nodeSetImmediate;
|
||||
|
||||
const getKeysForNumberMap = {};
|
||||
const messagesSentMap = {};
|
||||
|
||||
const fakeCall = () => Promise.resolve();
|
||||
|
||||
const fakeAPI = {
|
||||
confirmCode: fakeCall,
|
||||
getAttachment: fakeCall,
|
||||
getAvatar: fakeCall,
|
||||
getDevices: fakeCall,
|
||||
// getKeysForNumber: fakeCall,
|
||||
getMessageSocket: fakeCall,
|
||||
getMyKeys: fakeCall,
|
||||
getProfile: fakeCall,
|
||||
getProvisioningSocket: fakeCall,
|
||||
putAttachment: fakeCall,
|
||||
registerKeys: fakeCall,
|
||||
requestVerificationSMS: fakeCall,
|
||||
requestVerificationVoice: fakeCall,
|
||||
// sendMessages: fakeCall,
|
||||
setSignedPreKey: fakeCall,
|
||||
|
||||
getKeysForNumber: function(number, deviceId) {
|
||||
var res = getKeysForNumberMap[number];
|
||||
if (res !== undefined) {
|
||||
delete getKeysForNumberMap[number];
|
||||
return Promise.resolve(res);
|
||||
} else throw new Error('getKeysForNumber of unknown/used number');
|
||||
},
|
||||
|
||||
sendMessages: function(destination, messageArray) {
|
||||
for (i in messageArray) {
|
||||
var msg = messageArray[i];
|
||||
if (
|
||||
(msg.type != 1 && msg.type != 3) ||
|
||||
msg.destinationDeviceId === undefined ||
|
||||
msg.destinationRegistrationId === undefined ||
|
||||
msg.body === undefined ||
|
||||
msg.timestamp == undefined ||
|
||||
msg.relay !== undefined ||
|
||||
msg.destination !== undefined
|
||||
)
|
||||
throw new Error('Invalid message');
|
||||
|
||||
messagesSentMap[
|
||||
destination + '.' + messageArray[i].destinationDeviceId
|
||||
] = msg;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.WebAPI = {
|
||||
connect: () => fakeAPI,
|
||||
};
|
|
@ -12,6 +12,8 @@
|
|||
<div id="tests">
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="fake_web_api.js"></script>
|
||||
|
||||
<script type="text/javascript" src="test.js"></script>
|
||||
<script type="text/javascript" src="in_memory_signal_protocol_store.js"></script>
|
||||
|
||||
|
@ -33,8 +35,6 @@
|
|||
<script type="text/javascript" src="../contacts_parser.js" data-cover></script>
|
||||
<script type="text/javascript" src="../task_with_timeout.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="fake_api.js"></script>
|
||||
|
||||
<script type="text/javascript" src="errors_test.js"></script>
|
||||
<script type="text/javascript" src="helpers_test.js"></script>
|
||||
<script type="text/javascript" src="storage_test.js"></script>
|
||||
|
@ -47,11 +47,12 @@
|
|||
<script type="text/javascript" src="account_manager_test.js"></script>
|
||||
|
||||
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
||||
<script type="text/javascript" src="blanket_mocha.js"></script>
|
||||
<!-- NOTE: blanket doesn't support modern syntax and will choke until we find a replacement. :0( -->
|
||||
<!-- <script type="text/javascript" src="blanket_mocha.js"></script> -->
|
||||
|
||||
<!-- Uncomment to start tests without code coverage enabled -->
|
||||
<!-- <script type="text/javascript">
|
||||
<script type="text/javascript">
|
||||
mocha.run();
|
||||
</script> -->
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -33,6 +33,4 @@ describe('Protocol', function() {
|
|||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Use fake_api's hiding of api.sendMessage to test sendmessage.js' maze
|
||||
});
|
||||
|
|
2
main.js
2
main.js
|
@ -123,7 +123,7 @@ function prepareURL(pathSegments) {
|
|||
buildExpiration: config.get('buildExpiration'),
|
||||
serverUrl: config.get('serverUrl'),
|
||||
cdnUrl: config.get('cdnUrl'),
|
||||
certificateAuthorities: config.get('certificateAuthorities'),
|
||||
certificateAuthority: config.get('certificateAuthority'),
|
||||
environment: config.environment,
|
||||
node_version: process.versions.node,
|
||||
hostname: os.hostname(),
|
||||
|
|
14
preload.js
14
preload.js
|
@ -82,7 +82,15 @@ if (window.config.proxyUrl) {
|
|||
}
|
||||
|
||||
window.nodeSetImmediate = setImmediate;
|
||||
window.nodeWebSocket = require('websocket').w3cwebsocket;
|
||||
|
||||
const { initialize: initializeWebAPI } = require('./js/modules/web_api');
|
||||
|
||||
window.WebAPI = initializeWebAPI({
|
||||
url: window.config.serverUrl,
|
||||
cdnUrl: window.config.cdnUrl,
|
||||
certificateAuthority: window.config.certificateAuthority,
|
||||
proxyUrl: window.config.proxyUrl,
|
||||
});
|
||||
|
||||
// Linux seems to periodically let the event loop stop, so this is a global workaround
|
||||
setInterval(() => {
|
||||
|
@ -100,10 +108,6 @@ window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInst
|
|||
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
||||
window.loadImage = require('blueimp-load-image');
|
||||
|
||||
window.nodeBuffer = Buffer;
|
||||
window.nodeFetch = require('node-fetch');
|
||||
window.ProxyAgent = require('proxy-agent');
|
||||
|
||||
// Note: when modifying this file, consider whether our React Components or Backbone Views
|
||||
// will need these things to render in the Style Guide. If so, go update one of these
|
||||
// two locations:
|
||||
|
|
|
@ -564,6 +564,8 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="../libtextsecure/test/fake_web_api.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../js/components.js"></script>
|
||||
<script type="text/javascript" src="../js/reliable_trigger.js" data-cover></script>
|
||||
<script type="text/javascript" src="test.js"></script>
|
||||
|
@ -653,6 +655,7 @@
|
|||
<script type="text/javascript" src="fixtures_test.js"></script>
|
||||
|
||||
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
||||
<!-- NOTE: blanket doesn't support modern syntax and will choke until we find a replacement. :0( -->
|
||||
<!-- <script type="text/javascript" src="blanket_mocha.js"></script> -->
|
||||
|
||||
<!-- Uncomment to start tests without code coverage enabled -->
|
||||
|
|
Loading…
Reference in a new issue