var TextSecureServer = (function() { 'use strict'; function validateResponse(response, schema) { try { for (var i in schema) { switch (schema[i]) { case 'object': case 'string': case 'number': if (typeof response[i] !== schema[i]) { return false; } break; } } } catch (ex) { return false; } return true; } function createSocket(url) { var proxyUrl = window.config.proxyUrl; var requestOptions; if (proxyUrl) { requestOptions = { ca: window.config.certificateAuthorities, agent: new ProxyAgent(proxyUrl), }; } else { requestOptions = { ca: window.config.certificateAuthorities, }; } return new nodeWebSocket(url, null, null, null, requestOptions); } window.setImmediate = nodeSetImmediate; function promise_ajax(url, options) { return new Promise(function(resolve, reject) { if (!url) { url = options.host + '/' + options.path; } console.log(options.type, url); var timeout = typeof options.timeout !== 'undefined' ? options.timeout : 10000; var proxyUrl = window.config.proxyUrl; var agent; if (proxyUrl) { agent = new ProxyAgent(proxyUrl); } var fetchOptions = { method: options.type, body: options.data || null, headers: { 'X-Signal-Agent': 'OWD' }, agent: agent, ca: options.certificateAuthorities, timeout: timeout, }; if (fetchOptions.body instanceof ArrayBuffer) { // node-fetch doesn't support ArrayBuffer, only node Buffer var 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) { fetchOptions.headers['Authorization'] = 'Basic ' + btoa(getString(options.user) + ':' + getString(options.password)); } if (options.contentType) { fetchOptions.headers['Content-Type'] = options.contentType; } window .nodeFetch(url, fetchOptions) .then(function(response) { var 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(function(result) { if (options.responseType === 'arraybuffer') { 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( 'promise_ajax: invalid response', response.status, result, options.stack ) ); } } } if (0 <= response.status && 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', response.status, result, options.stack ) ); } }); }) .catch(function(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)); }); }); } function retry_ajax(url, options, limit, count) { count = count || 0; limit = limit || 3; count++; return promise_ajax(url, options).catch(function(e) { if (e.name === 'HTTPError' && e.code === -1 && count < limit) { return new Promise(function(resolve) { setTimeout(function() { resolve(retry_ajax(url, options, limit, count)); }, 1000); }); } else { throw e; } }); } function ajax(url, options) { options.stack = new Error().stack; // just in case, save stack here. return retry_ajax(url, options); } function HTTPError(message, code, response, stack) { if (code > 999 || code < 100) { code = -1; } var 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; } var URL_CALLS = { accounts: 'v1/accounts', devices: 'v1/devices', keys: 'v2/keys', signed: 'v2/keys/signed', messages: 'v1/messages', attachment: 'v1/attachments', profile: 'v1/profile', }; function TextSecureServer(url, username, password, cdn_url) { if (typeof url !== 'string') { throw new Error('Invalid server url'); } this.url = url; this.cdn_url = cdn_url; this.username = username; this.password = password; } TextSecureServer.prototype = { constructor: TextSecureServer, ajax: function(param) { if (!param.urlParameters) { 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(function(e) { var code = e.code; if (code === 200) { // happens sometimes when we get no response // (TODO: Fix server to return 204? instead) return null; } var 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: function(number) { return this.ajax({ call: 'profile', httpType: 'GET', urlParameters: '/' + number, responseType: 'json', }); }, getAvatar: function(path) { return ajax(this.cdn_url + '/' + path, { type: 'GET', responseType: 'arraybuffer', contentType: 'application/octet-stream', certificateAuthorities: window.config.certificateAuthorities, timeout: 0, }); }, requestVerificationSMS: function(number) { return this.ajax({ call: 'accounts', httpType: 'GET', urlParameters: '/sms/code/' + number, }); }, requestVerificationVoice: function(number) { return this.ajax({ call: 'accounts', httpType: 'GET', urlParameters: '/voice/code/' + number, }); }, confirmCode: function( number, code, password, signaling_key, registrationId, deviceName ) { var jsonData = { signalingKey: btoa(getString(signaling_key)), supportsSms: false, fetchesMessages: true, registrationId: registrationId, }; var call, urlPrefix, schema, 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: call, httpType: 'PUT', urlParameters: urlPrefix + code, jsonData: jsonData, responseType: responseType, validateResponse: schema, }); }, getDevices: function(number) { return this.ajax({ call: 'devices', httpType: 'GET', }); }, registerKeys: function(genKeys) { var 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 = []; var j = 0; for (var i in genKeys.preKeys) { keys.preKeys[j++] = { keyId: genKeys.preKeys[i].keyId, publicKey: btoa(getString(genKeys.preKeys[i].publicKey)), }; } // 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: function(signedPreKey) { return this.ajax({ call: 'signed', httpType: 'PUT', jsonData: { keyId: signedPreKey.keyId, publicKey: btoa(getString(signedPreKey.publicKey)), signature: btoa(getString(signedPreKey.signature)), }, }); }, getMyKeys: function(number, deviceId) { return this.ajax({ call: 'keys', httpType: 'GET', responseType: 'json', validateResponse: { count: 'number' }, }).then(function(res) { return res.count; }); }, getKeysForNumber: function(number, deviceId) { if (deviceId === undefined) deviceId = '*'; return this.ajax({ call: 'keys', httpType: 'GET', urlParameters: '/' + number + '/' + deviceId, responseType: 'json', validateResponse: { identityKey: 'string', devices: 'object' }, }).then(function(res) { if (res.devices.constructor !== Array) { throw new Error('Invalid response'); } res.identityKey = StringView.base64ToBytes(res.identityKey); res.devices.forEach(function(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'); } device.preKey.publicKey = StringView.base64ToBytes( device.preKey.publicKey ); } device.signedPreKey.publicKey = StringView.base64ToBytes( device.signedPreKey.publicKey ); device.signedPreKey.signature = StringView.base64ToBytes( device.signedPreKey.signature ); }); return res; }); }, sendMessages: function(destination, messageArray, timestamp, silent) { var jsonData = { messages: messageArray, timestamp: timestamp }; if (silent) { jsonData.silent = true; } return this.ajax({ call: 'messages', httpType: 'PUT', urlParameters: '/' + destination, jsonData: jsonData, responseType: 'json', }); }, getAttachment: function(id) { return this.ajax({ call: 'attachment', httpType: 'GET', 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) ); }, putAttachment: function(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) ); }, getMessageSocket: function() { console.log('opening message socket', this.url); return createSocket( this.url.replace('https://', 'wss://').replace('http://', 'ws://') + '/v1/websocket/?login=' + encodeURIComponent(this.username) + '&password=' + encodeURIComponent(this.password) + '&agent=OWD' ); }, getProvisioningSocket: function() { console.log('opening provisioning socket', this.url); return createSocket( this.url.replace('https://', 'wss://').replace('http://', 'ws://') + '/v1/websocket/provisioning/?agent=OWD' ); }, }; return TextSecureServer; })();