eslintify libtextsecure/api.js

This commit is contained in:
Scott Nonnenberg 2018-05-25 16:32:43 -07:00
parent beb65b14c0
commit 37ebe9fcec
2 changed files with 148 additions and 129 deletions

View file

@ -29,6 +29,7 @@ js/WebAudioRecorderMp3.js
ts/**/*.js ts/**/*.js
# ES2015+ files # ES2015+ files
!libtextsecure/api.js
!js/background.js !js/background.js
!js/backup.js !js/backup.js
!js/database.js !js/database.js

View file

@ -1,17 +1,33 @@
var TextSecureServer = (function() { /* global nodeBuffer: false */
'use strict'; /* 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) { function validateResponse(response, schema) {
try { try {
for (var i in schema) { // eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const i in schema) {
switch (schema[i]) { switch (schema[i]) {
case 'object': case 'object':
case 'string': case 'string':
case 'number': case 'number':
// eslint-disable-next-line valid-typeof
if (typeof response[i] !== schema[i]) { if (typeof response[i] !== schema[i]) {
return false; return false;
} }
break; break;
default:
} }
} }
} catch (ex) { } catch (ex) {
@ -21,8 +37,8 @@ var TextSecureServer = (function() {
} }
function createSocket(url) { function createSocket(url) {
var proxyUrl = window.config.proxyUrl; const { proxyUrl } = window.config;
var requestOptions; let requestOptions;
if (proxyUrl) { if (proxyUrl) {
requestOptions = { requestOptions = {
ca: window.config.certificateAuthorities, ca: window.config.certificateAuthorities,
@ -34,38 +50,39 @@ var TextSecureServer = (function() {
}; };
} }
// eslint-disable-next-line new-cap
return new nodeWebSocket(url, null, null, null, requestOptions); 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; window.setImmediate = nodeSetImmediate;
function promise_ajax(url, options) { function promiseAjax(providedUrl, options) {
return new Promise(function(resolve, reject) { return new Promise((resolve, reject) => {
if (!url) { const url = providedUrl || `${options.host}/${options.path}`;
url = options.host + '/' + options.path;
}
console.log(options.type, url); console.log(options.type, url);
var timeout = const timeout =
typeof options.timeout !== 'undefined' ? options.timeout : 10000; typeof options.timeout !== 'undefined' ? options.timeout : 10000;
var proxyUrl = window.config.proxyUrl; const { proxyUrl } = window.config;
var agent; let agent;
if (proxyUrl) { if (proxyUrl) {
agent = new ProxyAgent(proxyUrl); agent = new ProxyAgent(proxyUrl);
} }
var fetchOptions = { const fetchOptions = {
method: options.type, method: options.type,
body: options.data || null, body: options.data || null,
headers: { 'X-Signal-Agent': 'OWD' }, headers: { 'X-Signal-Agent': 'OWD' },
agent: agent, agent,
ca: options.certificateAuthorities, ca: options.certificateAuthorities,
timeout: timeout, timeout,
}; };
if (fetchOptions.body instanceof ArrayBuffer) { if (fetchOptions.body instanceof ArrayBuffer) {
// node-fetch doesn't support ArrayBuffer, only node Buffer // node-fetch doesn't support ArrayBuffer, only node Buffer
var contentLength = fetchOptions.body.byteLength; const contentLength = fetchOptions.body.byteLength;
fetchOptions.body = nodeBuffer.from(fetchOptions.body); fetchOptions.body = nodeBuffer.from(fetchOptions.body);
// node-fetch doesn't set content-length like S3 requires // node-fetch doesn't set content-length like S3 requires
@ -73,17 +90,17 @@ var TextSecureServer = (function() {
} }
if (options.user && options.password) { if (options.user && options.password) {
fetchOptions.headers['Authorization'] = const user = getString(options.user);
'Basic ' + const password = getString(options.password);
btoa(getString(options.user) + ':' + getString(options.password)); const auth = btoa(`${user}:${password}`);
fetchOptions.headers.Authorization = `Basic ${auth}`;
} }
if (options.contentType) { if (options.contentType) {
fetchOptions.headers['Content-Type'] = options.contentType; fetchOptions.headers['Content-Type'] = options.contentType;
} }
window nodeFetch(url, fetchOptions)
.nodeFetch(url, fetchOptions) .then(response => {
.then(function(response) { let resultPromise;
var resultPromise;
if ( if (
options.responseType === 'json' && options.responseType === 'json' &&
response.headers.get('Content-Type') === 'application/json' response.headers.get('Content-Type') === 'application/json'
@ -94,8 +111,9 @@ var TextSecureServer = (function() {
} else { } else {
resultPromise = response.text(); resultPromise = response.text();
} }
return resultPromise.then(function(result) { return resultPromise.then(result => {
if (options.responseType === 'arraybuffer') { if (options.responseType === 'arraybuffer') {
// eslint-disable-next-line no-param-reassign
result = result.buffer.slice( result = result.buffer.slice(
result.byteOffset, result.byteOffset,
result.byteOffset + result.byteLength result.byteOffset + result.byteLength
@ -107,7 +125,7 @@ var TextSecureServer = (function() {
console.log(options.type, url, response.status, 'Error'); console.log(options.type, url, response.status, 'Error');
reject( reject(
HTTPError( HTTPError(
'promise_ajax: invalid response', 'promiseAjax: invalid response',
response.status, response.status,
result, result,
options.stack options.stack
@ -116,14 +134,14 @@ var TextSecureServer = (function() {
} }
} }
} }
if (0 <= response.status && response.status < 400) { if (response.status >= 0 && response.status < 400) {
console.log(options.type, url, response.status, 'Success'); console.log(options.type, url, response.status, 'Success');
resolve(result, response.status); resolve(result, response.status);
} else { } else {
console.log(options.type, url, response.status, 'Error'); console.log(options.type, url, response.status, 'Error');
reject( reject(
HTTPError( HTTPError(
'promise_ajax: error response', 'promiseAjax: error response',
response.status, response.status,
result, result,
options.stack options.stack
@ -132,51 +150,48 @@ var TextSecureServer = (function() {
} }
}); });
}) })
.catch(function(e) { .catch(e => {
console.log(options.type, url, 0, 'Error'); console.log(options.type, url, 0, 'Error');
var stack = e.stack + '\nInitial stack:\n' + options.stack; const stack = `${e.stack}\nInitial stack:\n${options.stack}`;
reject(HTTPError('promise_ajax catch', 0, e.toString(), stack)); reject(HTTPError('promiseAjax catch', 0, e.toString(), stack));
}); });
}); });
} }
function retry_ajax(url, options, limit, count) { function retryAjax(url, options, providedLimit, providedCount) {
count = count || 0; const count = (providedCount || 0) + 1;
limit = limit || 3; const limit = providedLimit || 3;
count++; return promiseAjax(url, options).catch(e => {
return promise_ajax(url, options).catch(function(e) {
if (e.name === 'HTTPError' && e.code === -1 && count < limit) { if (e.name === 'HTTPError' && e.code === -1 && count < limit) {
return new Promise(function(resolve) { return new Promise(resolve => {
setTimeout(function() { setTimeout(() => {
resolve(retry_ajax(url, options, limit, count)); resolve(retryAjax(url, options, limit, count));
}, 1000); }, 1000);
}); });
} else {
throw e;
} }
throw e;
}); });
} }
function ajax(url, options) { function ajax(url, options) {
// eslint-disable-next-line no-param-reassign
options.stack = new Error().stack; // just in case, save stack here. options.stack = new Error().stack; // just in case, save stack here.
return retry_ajax(url, options); return retryAjax(url, options);
} }
function HTTPError(message, code, response, stack) { function HTTPError(message, providedCode, response, stack) {
if (code > 999 || code < 100) { const code = providedCode > 999 || providedCode < 100 ? -1 : providedCode;
code = -1; const e = new Error(`${message}; code: ${code}`);
}
var e = new Error(message + '; code: ' + code);
e.name = 'HTTPError'; e.name = 'HTTPError';
e.code = code; e.code = code;
e.stack += '\nOriginal stack:\n' + stack; e.stack += `\nOriginal stack:\n${stack}`;
if (response) { if (response) {
e.response = response; e.response = response;
} }
return e; return e;
} }
var URL_CALLS = { const URL_CALLS = {
accounts: 'v1/accounts', accounts: 'v1/accounts',
devices: 'v1/devices', devices: 'v1/devices',
keys: 'v2/keys', keys: 'v2/keys',
@ -186,20 +201,22 @@ var TextSecureServer = (function() {
profile: 'v1/profile', profile: 'v1/profile',
}; };
function TextSecureServer(url, username, password, cdn_url) { // eslint-disable-next-line no-shadow
function TextSecureServer(url, username, password, cdnUrl) {
if (typeof url !== 'string') { if (typeof url !== 'string') {
throw new Error('Invalid server url'); throw new Error('Invalid server url');
} }
this.url = url; this.url = url;
this.cdn_url = cdn_url; this.cdnUrl = cdnUrl;
this.username = username; this.username = username;
this.password = password; this.password = password;
} }
TextSecureServer.prototype = { TextSecureServer.prototype = {
constructor: TextSecureServer, constructor: TextSecureServer,
ajax: function(param) { ajax(param) {
if (!param.urlParameters) { if (!param.urlParameters) {
// eslint-disable-next-line no-param-reassign
param.urlParameters = ''; param.urlParameters = '';
} }
return ajax(null, { return ajax(null, {
@ -214,14 +231,14 @@ var TextSecureServer = (function() {
validateResponse: param.validateResponse, validateResponse: param.validateResponse,
certificateAuthorities: window.config.certificateAuthorities, certificateAuthorities: window.config.certificateAuthorities,
timeout: param.timeout, timeout: param.timeout,
}).catch(function(e) { }).catch(e => {
var code = e.code; const { code } = e;
if (code === 200) { if (code === 200) {
// happens sometimes when we get no response // happens sometimes when we get no response
// (TODO: Fix server to return 204? instead) // (TODO: Fix server to return 204? instead)
return null; return null;
} }
var message; let message;
switch (code) { switch (code) {
case -1: case -1:
message = message =
@ -252,16 +269,16 @@ var TextSecureServer = (function() {
throw e; throw e;
}); });
}, },
getProfile: function(number) { getProfile(number) {
return this.ajax({ return this.ajax({
call: 'profile', call: 'profile',
httpType: 'GET', httpType: 'GET',
urlParameters: '/' + number, urlParameters: `/${number}`,
responseType: 'json', responseType: 'json',
}); });
}, },
getAvatar: function(path) { getAvatar(path) {
return ajax(this.cdn_url + '/' + path, { return ajax(`${this.cdnUrl}/${path}`, {
type: 'GET', type: 'GET',
responseType: 'arraybuffer', responseType: 'arraybuffer',
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
@ -269,36 +286,40 @@ var TextSecureServer = (function() {
timeout: 0, timeout: 0,
}); });
}, },
requestVerificationSMS: function(number) { requestVerificationSMS(number) {
return this.ajax({ return this.ajax({
call: 'accounts', call: 'accounts',
httpType: 'GET', httpType: 'GET',
urlParameters: '/sms/code/' + number, urlParameters: `/sms/code/${number}`,
}); });
}, },
requestVerificationVoice: function(number) { requestVerificationVoice(number) {
return this.ajax({ return this.ajax({
call: 'accounts', call: 'accounts',
httpType: 'GET', httpType: 'GET',
urlParameters: '/voice/code/' + number, urlParameters: `/voice/code/${number}`,
}); });
}, },
confirmCode: function( confirmCode(
number, number,
code, code,
password, password,
signaling_key, signalingKey,
registrationId, registrationId,
deviceName deviceName
) { ) {
var jsonData = { const jsonData = {
signalingKey: btoa(getString(signaling_key)), signalingKey: btoa(getString(signalingKey)),
supportsSms: false, supportsSms: false,
fetchesMessages: true, fetchesMessages: true,
registrationId: registrationId, registrationId,
}; };
var call, urlPrefix, schema, responseType; let call;
let urlPrefix;
let schema;
let responseType;
if (deviceName) { if (deviceName) {
jsonData.name = deviceName; jsonData.name = deviceName;
call = 'devices'; call = 'devices';
@ -313,22 +334,22 @@ var TextSecureServer = (function() {
this.username = number; this.username = number;
this.password = password; this.password = password;
return this.ajax({ return this.ajax({
call: call, call,
httpType: 'PUT', httpType: 'PUT',
urlParameters: urlPrefix + code, urlParameters: urlPrefix + code,
jsonData: jsonData, jsonData,
responseType: responseType, responseType,
validateResponse: schema, validateResponse: schema,
}); });
}, },
getDevices: function(number) { getDevices() {
return this.ajax({ return this.ajax({
call: 'devices', call: 'devices',
httpType: 'GET', httpType: 'GET',
}); });
}, },
registerKeys: function(genKeys) { registerKeys(genKeys) {
var keys = {}; const keys = {};
keys.identityKey = btoa(getString(genKeys.identityKey)); keys.identityKey = btoa(getString(genKeys.identityKey));
keys.signedPreKey = { keys.signedPreKey = {
keyId: genKeys.signedPreKey.keyId, keyId: genKeys.signedPreKey.keyId,
@ -337,12 +358,14 @@ var TextSecureServer = (function() {
}; };
keys.preKeys = []; keys.preKeys = [];
var j = 0; let j = 0;
for (var i in genKeys.preKeys) { // eslint-disable-next-line guard-for-in, no-restricted-syntax
keys.preKeys[j++] = { for (const i in genKeys.preKeys) {
keys.preKeys[j] = {
keyId: genKeys.preKeys[i].keyId, keyId: genKeys.preKeys[i].keyId,
publicKey: btoa(getString(genKeys.preKeys[i].publicKey)), publicKey: btoa(getString(genKeys.preKeys[i].publicKey)),
}; };
j += 1;
} }
// This is just to make the server happy // This is just to make the server happy
@ -355,7 +378,7 @@ var TextSecureServer = (function() {
jsonData: keys, jsonData: keys,
}); });
}, },
setSignedPreKey: function(signedPreKey) { setSignedPreKey(signedPreKey) {
return this.ajax({ return this.ajax({
call: 'signed', call: 'signed',
httpType: 'PUT', httpType: 'PUT',
@ -366,31 +389,27 @@ var TextSecureServer = (function() {
}, },
}); });
}, },
getMyKeys: function(number, deviceId) { getMyKeys() {
return this.ajax({ return this.ajax({
call: 'keys', call: 'keys',
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
validateResponse: { count: 'number' }, validateResponse: { count: 'number' },
}).then(function(res) { }).then(res => res.count);
return res.count;
});
}, },
getKeysForNumber: function(number, deviceId) { getKeysForNumber(number, deviceId = '*') {
if (deviceId === undefined) deviceId = '*';
return this.ajax({ return this.ajax({
call: 'keys', call: 'keys',
httpType: 'GET', httpType: 'GET',
urlParameters: '/' + number + '/' + deviceId, urlParameters: `/${number}/${deviceId}`,
responseType: 'json', responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' }, validateResponse: { identityKey: 'string', devices: 'object' },
}).then(function(res) { }).then(res => {
if (res.devices.constructor !== Array) { if (res.devices.constructor !== Array) {
throw new Error('Invalid response'); throw new Error('Invalid response');
} }
res.identityKey = StringView.base64ToBytes(res.identityKey); res.identityKey = StringView.base64ToBytes(res.identityKey);
res.devices.forEach(function(device) { res.devices.forEach(device => {
if ( if (
!validateResponse(device, { signedPreKey: 'object' }) || !validateResponse(device, { signedPreKey: 'object' }) ||
!validateResponse(device.signedPreKey, { !validateResponse(device.signedPreKey, {
@ -407,13 +426,16 @@ var TextSecureServer = (function() {
) { ) {
throw new Error('Invalid preKey'); throw new Error('Invalid preKey');
} }
// eslint-disable-next-line no-param-reassign
device.preKey.publicKey = StringView.base64ToBytes( device.preKey.publicKey = StringView.base64ToBytes(
device.preKey.publicKey device.preKey.publicKey
); );
} }
// eslint-disable-next-line no-param-reassign
device.signedPreKey.publicKey = StringView.base64ToBytes( device.signedPreKey.publicKey = StringView.base64ToBytes(
device.signedPreKey.publicKey device.signedPreKey.publicKey
); );
// eslint-disable-next-line no-param-reassign
device.signedPreKey.signature = StringView.base64ToBytes( device.signedPreKey.signature = StringView.base64ToBytes(
device.signedPreKey.signature device.signedPreKey.signature
); );
@ -421,8 +443,8 @@ var TextSecureServer = (function() {
return res; return res;
}); });
}, },
sendMessages: function(destination, messageArray, timestamp, silent) { sendMessages(destination, messageArray, timestamp, silent) {
var jsonData = { messages: messageArray, timestamp: timestamp }; const jsonData = { messages: messageArray, timestamp };
if (silent) { if (silent) {
jsonData.silent = true; jsonData.silent = true;
@ -431,66 +453,62 @@ var TextSecureServer = (function() {
return this.ajax({ return this.ajax({
call: 'messages', call: 'messages',
httpType: 'PUT', httpType: 'PUT',
urlParameters: '/' + destination, urlParameters: `/${destination}`,
jsonData: jsonData, jsonData,
responseType: 'json', responseType: 'json',
}); });
}, },
getAttachment: function(id) { getAttachment(id) {
return this.ajax({ return this.ajax({
call: 'attachment', call: 'attachment',
httpType: 'GET', httpType: 'GET',
urlParameters: '/' + id, urlParameters: `/${id}`,
responseType: 'json', responseType: 'json',
validateResponse: { location: 'string' }, validateResponse: { location: 'string' },
}).then( }).then(response =>
function(response) { ajax(response.location, {
return ajax(response.location, { timeout: 0,
timeout: 0, type: 'GET',
type: 'GET', responseType: 'arraybuffer',
responseType: 'arraybuffer', contentType: 'application/octet-stream',
contentType: 'application/octet-stream', })
});
}.bind(this)
); );
}, },
putAttachment: function(encryptedBin) { putAttachment(encryptedBin) {
return this.ajax({ return this.ajax({
call: 'attachment', call: 'attachment',
httpType: 'GET', httpType: 'GET',
responseType: 'json', responseType: 'json',
}).then( }).then(response =>
function(response) { ajax(response.location, {
return ajax(response.location, { timeout: 0,
timeout: 0, type: 'PUT',
type: 'PUT', contentType: 'application/octet-stream',
contentType: 'application/octet-stream', data: encryptedBin,
data: encryptedBin, processData: false,
processData: false, }).then(() => response.idString)
}).then(
function() {
return response.idString;
}.bind(this)
);
}.bind(this)
); );
}, },
getMessageSocket: function() { getMessageSocket() {
console.log('opening message socket', this.url); 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( return createSocket(
this.url.replace('https://', 'wss://').replace('http://', 'ws://') + `${fixedScheme}/v1/websocket/?login=${login}&password=${password}&agent=OWD`
'/v1/websocket/?login=' +
encodeURIComponent(this.username) +
'&password=' +
encodeURIComponent(this.password) +
'&agent=OWD'
); );
}, },
getProvisioningSocket: function() { getProvisioningSocket() {
console.log('opening provisioning socket', this.url); console.log('opening provisioning socket', this.url);
const fixedScheme = this.url
.replace('https://', 'wss://')
.replace('http://', 'ws://');
return createSocket( return createSocket(
this.url.replace('https://', 'wss://').replace('http://', 'ws://') + `${fixedScheme}/v1/websocket/provisioning/?agent=OWD`
'/v1/websocket/provisioning/?agent=OWD'
); );
}, },
}; };