diff --git a/.eslintignore b/.eslintignore index 9de130ebe002..b2b9eb5cb92d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -29,6 +29,7 @@ js/WebAudioRecorderMp3.js ts/**/*.js # ES2015+ files +!libtextsecure/api.js !js/background.js !js/backup.js !js/database.js diff --git a/libtextsecure/api.js b/libtextsecure/api.js index 71d6f4f137b2..3cc45c47b66e 100644 --- a/libtextsecure/api.js +++ b/libtextsecure/api.js @@ -1,17 +1,33 @@ -var TextSecureServer = (function() { - 'use strict'; +/* 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 { - for (var i in schema) { + // 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) { @@ -21,8 +37,8 @@ var TextSecureServer = (function() { } function createSocket(url) { - var proxyUrl = window.config.proxyUrl; - var requestOptions; + const { proxyUrl } = window.config; + let requestOptions; if (proxyUrl) { requestOptions = { 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); } + // We add this to window here because the default Node context is erased at the end + // of preload.js processing window.setImmediate = nodeSetImmediate; - function promise_ajax(url, options) { - return new Promise(function(resolve, reject) { - if (!url) { - url = options.host + '/' + options.path; - } + function promiseAjax(providedUrl, options) { + return new Promise((resolve, reject) => { + const url = providedUrl || `${options.host}/${options.path}`; console.log(options.type, url); - var timeout = + const timeout = typeof options.timeout !== 'undefined' ? options.timeout : 10000; - var proxyUrl = window.config.proxyUrl; - var agent; + const { proxyUrl } = window.config; + let agent; if (proxyUrl) { agent = new ProxyAgent(proxyUrl); } - var fetchOptions = { + const fetchOptions = { method: options.type, body: options.data || null, headers: { 'X-Signal-Agent': 'OWD' }, - agent: agent, + agent, ca: options.certificateAuthorities, - timeout: timeout, + timeout, }; if (fetchOptions.body instanceof ArrayBuffer) { // 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); // node-fetch doesn't set content-length like S3 requires @@ -73,17 +90,17 @@ var TextSecureServer = (function() { } if (options.user && options.password) { - fetchOptions.headers['Authorization'] = - 'Basic ' + - btoa(getString(options.user) + ':' + getString(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; } - window - .nodeFetch(url, fetchOptions) - .then(function(response) { - var resultPromise; + nodeFetch(url, fetchOptions) + .then(response => { + let resultPromise; if ( options.responseType === 'json' && response.headers.get('Content-Type') === 'application/json' @@ -94,8 +111,9 @@ var TextSecureServer = (function() { } else { resultPromise = response.text(); } - return resultPromise.then(function(result) { + 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 @@ -107,7 +125,7 @@ var TextSecureServer = (function() { console.log(options.type, url, response.status, 'Error'); reject( HTTPError( - 'promise_ajax: invalid response', + 'promiseAjax: invalid response', response.status, result, 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'); resolve(result, response.status); } else { console.log(options.type, url, response.status, 'Error'); reject( HTTPError( - 'promise_ajax: error response', + 'promiseAjax: error response', response.status, result, options.stack @@ -132,51 +150,48 @@ var TextSecureServer = (function() { } }); }) - .catch(function(e) { + .catch(e => { console.log(options.type, url, 0, 'Error'); - var stack = e.stack + '\nInitial stack:\n' + options.stack; - reject(HTTPError('promise_ajax catch', 0, e.toString(), stack)); + const stack = `${e.stack}\nInitial stack:\n${options.stack}`; + reject(HTTPError('promiseAjax catch', 0, e.toString(), stack)); }); }); } - function retry_ajax(url, options, limit, count) { - count = count || 0; - limit = limit || 3; - count++; - return promise_ajax(url, options).catch(function(e) { + 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(function(resolve) { - setTimeout(function() { - resolve(retry_ajax(url, options, limit, count)); + return new Promise(resolve => { + setTimeout(() => { + resolve(retryAjax(url, options, limit, count)); }, 1000); }); - } else { - throw e; } + 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 retry_ajax(url, options); + return retryAjax(url, options); } - function HTTPError(message, code, response, stack) { - if (code > 999 || code < 100) { - code = -1; - } - var e = new Error(message + '; code: ' + code); + 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; + e.stack += `\nOriginal stack:\n${stack}`; if (response) { e.response = response; } return e; } - var URL_CALLS = { + const URL_CALLS = { accounts: 'v1/accounts', devices: 'v1/devices', keys: 'v2/keys', @@ -186,20 +201,22 @@ var TextSecureServer = (function() { 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') { throw new Error('Invalid server url'); } this.url = url; - this.cdn_url = cdn_url; + this.cdnUrl = cdnUrl; this.username = username; this.password = password; } TextSecureServer.prototype = { constructor: TextSecureServer, - ajax: function(param) { + ajax(param) { if (!param.urlParameters) { + // eslint-disable-next-line no-param-reassign param.urlParameters = ''; } return ajax(null, { @@ -214,14 +231,14 @@ var TextSecureServer = (function() { validateResponse: param.validateResponse, certificateAuthorities: window.config.certificateAuthorities, timeout: param.timeout, - }).catch(function(e) { - var code = e.code; + }).catch(e => { + const { code } = e; if (code === 200) { // happens sometimes when we get no response // (TODO: Fix server to return 204? instead) return null; } - var message; + let message; switch (code) { case -1: message = @@ -252,16 +269,16 @@ var TextSecureServer = (function() { throw e; }); }, - getProfile: function(number) { + getProfile(number) { return this.ajax({ call: 'profile', httpType: 'GET', - urlParameters: '/' + number, + urlParameters: `/${number}`, responseType: 'json', }); }, - getAvatar: function(path) { - return ajax(this.cdn_url + '/' + path, { + getAvatar(path) { + return ajax(`${this.cdnUrl}/${path}`, { type: 'GET', responseType: 'arraybuffer', contentType: 'application/octet-stream', @@ -269,36 +286,40 @@ var TextSecureServer = (function() { timeout: 0, }); }, - requestVerificationSMS: function(number) { + requestVerificationSMS(number) { return this.ajax({ call: 'accounts', httpType: 'GET', - urlParameters: '/sms/code/' + number, + urlParameters: `/sms/code/${number}`, }); }, - requestVerificationVoice: function(number) { + requestVerificationVoice(number) { return this.ajax({ call: 'accounts', httpType: 'GET', - urlParameters: '/voice/code/' + number, + urlParameters: `/voice/code/${number}`, }); }, - confirmCode: function( + confirmCode( number, code, password, - signaling_key, + signalingKey, registrationId, deviceName ) { - var jsonData = { - signalingKey: btoa(getString(signaling_key)), + const jsonData = { + signalingKey: btoa(getString(signalingKey)), supportsSms: false, fetchesMessages: true, - registrationId: registrationId, + registrationId, }; - var call, urlPrefix, schema, responseType; + let call; + let urlPrefix; + let schema; + let responseType; + if (deviceName) { jsonData.name = deviceName; call = 'devices'; @@ -313,22 +334,22 @@ var TextSecureServer = (function() { this.username = number; this.password = password; return this.ajax({ - call: call, + call, httpType: 'PUT', urlParameters: urlPrefix + code, - jsonData: jsonData, - responseType: responseType, + jsonData, + responseType, validateResponse: schema, }); }, - getDevices: function(number) { + getDevices() { return this.ajax({ call: 'devices', httpType: 'GET', }); }, - registerKeys: function(genKeys) { - var keys = {}; + registerKeys(genKeys) { + const keys = {}; keys.identityKey = btoa(getString(genKeys.identityKey)); keys.signedPreKey = { keyId: genKeys.signedPreKey.keyId, @@ -337,12 +358,14 @@ var TextSecureServer = (function() { }; keys.preKeys = []; - var j = 0; - for (var i in genKeys.preKeys) { - keys.preKeys[j++] = { + 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 @@ -355,7 +378,7 @@ var TextSecureServer = (function() { jsonData: keys, }); }, - setSignedPreKey: function(signedPreKey) { + setSignedPreKey(signedPreKey) { return this.ajax({ call: 'signed', httpType: 'PUT', @@ -366,31 +389,27 @@ var TextSecureServer = (function() { }, }); }, - getMyKeys: function(number, deviceId) { + getMyKeys() { return this.ajax({ call: 'keys', httpType: 'GET', responseType: 'json', validateResponse: { count: 'number' }, - }).then(function(res) { - return res.count; - }); + }).then(res => res.count); }, - getKeysForNumber: function(number, deviceId) { - if (deviceId === undefined) deviceId = '*'; - + getKeysForNumber(number, deviceId = '*') { return this.ajax({ call: 'keys', httpType: 'GET', - urlParameters: '/' + number + '/' + deviceId, + urlParameters: `/${number}/${deviceId}`, responseType: 'json', validateResponse: { identityKey: 'string', devices: 'object' }, - }).then(function(res) { + }).then(res => { if (res.devices.constructor !== Array) { throw new Error('Invalid response'); } res.identityKey = StringView.base64ToBytes(res.identityKey); - res.devices.forEach(function(device) { + res.devices.forEach(device => { if ( !validateResponse(device, { signedPreKey: 'object' }) || !validateResponse(device.signedPreKey, { @@ -407,13 +426,16 @@ var TextSecureServer = (function() { ) { 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 ); @@ -421,8 +443,8 @@ var TextSecureServer = (function() { return res; }); }, - sendMessages: function(destination, messageArray, timestamp, silent) { - var jsonData = { messages: messageArray, timestamp: timestamp }; + sendMessages(destination, messageArray, timestamp, silent) { + const jsonData = { messages: messageArray, timestamp }; if (silent) { jsonData.silent = true; @@ -431,66 +453,62 @@ var TextSecureServer = (function() { return this.ajax({ call: 'messages', httpType: 'PUT', - urlParameters: '/' + destination, - jsonData: jsonData, + urlParameters: `/${destination}`, + jsonData, responseType: 'json', }); }, - getAttachment: function(id) { + getAttachment(id) { return this.ajax({ call: 'attachment', httpType: 'GET', - urlParameters: '/' + id, + urlParameters: `/${id}`, responseType: 'json', validateResponse: { location: 'string' }, - }).then( - function(response) { - return ajax(response.location, { - timeout: 0, - type: 'GET', - responseType: 'arraybuffer', - contentType: 'application/octet-stream', - }); - }.bind(this) + }).then(response => + ajax(response.location, { + timeout: 0, + type: 'GET', + responseType: 'arraybuffer', + contentType: 'application/octet-stream', + }) ); }, - putAttachment: function(encryptedBin) { + putAttachment(encryptedBin) { return this.ajax({ call: 'attachment', httpType: 'GET', responseType: 'json', - }).then( - function(response) { - return ajax(response.location, { - timeout: 0, - type: 'PUT', - contentType: 'application/octet-stream', - data: encryptedBin, - processData: false, - }).then( - function() { - return response.idString; - }.bind(this) - ); - }.bind(this) + }).then(response => + ajax(response.location, { + timeout: 0, + type: 'PUT', + contentType: 'application/octet-stream', + data: encryptedBin, + processData: false, + }).then(() => response.idString) ); }, - getMessageSocket: function() { + 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( - this.url.replace('https://', 'wss://').replace('http://', 'ws://') + - '/v1/websocket/?login=' + - encodeURIComponent(this.username) + - '&password=' + - encodeURIComponent(this.password) + - '&agent=OWD' + `${fixedScheme}/v1/websocket/?login=${login}&password=${password}&agent=OWD` ); }, - getProvisioningSocket: function() { + getProvisioningSocket() { console.log('opening provisioning socket', this.url); + const fixedScheme = this.url + .replace('https://', 'wss://') + .replace('http://', 'ws://'); + return createSocket( - this.url.replace('https://', 'wss://').replace('http://', 'ws://') + - '/v1/websocket/provisioning/?agent=OWD' + `${fixedScheme}/v1/websocket/provisioning/?agent=OWD` ); }, };