Refactor api.js into web_api, which encapsulates all web access
parent
37ebe9fcec
commit
f3bd0cf903
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,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;
|
|
||||||
}
|
|
||||||
};
|
|
@ -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,
|
||||||
|
};
|
Loading…
Reference in New Issue