Merge remote-tracking branch 'upstream/clearnet' into react-refactor

pull/1387/head
Audric Ackermann 5 years ago
commit 79eae4838d
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -101,7 +101,7 @@
const res = await window.tokenlessFileServerAdnAPI.serverRequest(
'loki/v1/time'
);
if (res.statusCode === 200) {
if (res.ok) {
timestamp = res.response;
}
} catch (e) {

@ -1,6 +1,6 @@
/* global log, textsecure, libloki, Signal, Whisper, ConversationController,
clearTimeout, MessageController, libsignal, StringView, window, _,
dcodeIO, Buffer, TextDecoder, process */
dcodeIO, Buffer, process */
const nodeFetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');
const FormData = require('form-data');
@ -49,8 +49,6 @@ const snodeHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
const timeoutDelay = ms => new Promise(resolve => setTimeout(resolve, ms));
const MAX_SEND_ONION_RETRIES = 3;
const sendViaOnion = async (srvPubKey, url, fetchOptions, options = {}) => {
@ -201,156 +199,6 @@ const sendViaOnion = async (srvPubKey, url, fetchOptions, options = {}) => {
return { result, txtResponse, response };
};
const sendToProxy = async (srvPubKey, endpoint, fetchOptions, options = {}) => {
if (!srvPubKey) {
log.error(
'loki_app_dot_net:::sendToProxy - called without a server public key'
);
return {};
}
const payloadObj = {
body: fetchOptions.body, // might need to b64 if binary...
endpoint,
method: fetchOptions.method,
// safety issue with file server, just safer to have this
headers: fetchOptions.headers || {},
};
// from https://github.com/sindresorhus/is-stream/blob/master/index.js
if (
payloadObj.body &&
typeof payloadObj.body === 'object' &&
typeof payloadObj.body.pipe === 'function'
) {
const fData = payloadObj.body.getBuffer();
const fHeaders = payloadObj.body.getHeaders();
// update headers for boundary
payloadObj.headers = { ...payloadObj.headers, ...fHeaders };
// update body with base64 chunk
payloadObj.body = {
fileUpload: fData.toString('base64'),
};
}
const randSnode = await window.SnodePool.getRandomSnodeAddress();
if (randSnode === false) {
log.warn('proxy random snode pool is not ready, retrying 10s', endpoint);
// no nodes in the pool yet, give it some time and retry
await timeoutDelay(1000);
return sendToProxy(srvPubKey, endpoint, fetchOptions, options);
}
const url = `https://${randSnode.ip}:${randSnode.port}/file_proxy`;
// convert our payload to binary buffer
const payloadData = Buffer.from(
dcodeIO.ByteBuffer.wrap(JSON.stringify(payloadObj)).toArrayBuffer()
);
payloadObj.body = false; // free memory
// make temporary key for this request/response
// async maybe preferable to avoid cpu spikes
// but I think sync might be more apt in cases like sending...
const ephemeralKey = await libloki.crypto.generateEphemeralKeyPair();
// mix server pub key with our priv key
const symKey = await libsignal.Curve.async.calculateAgreement(
srvPubKey, // server's pubkey
ephemeralKey.privKey // our privkey
);
const ivAndCiphertext = await libloki.crypto.DHEncrypt(symKey, payloadData);
// convert final buffer to base64
const cipherText64 = dcodeIO.ByteBuffer.wrap(ivAndCiphertext).toString(
'base64'
);
const ephemeralPubKey64 = dcodeIO.ByteBuffer.wrap(
ephemeralKey.pubKey
).toString('base64');
const finalRequestHeader = {
'X-Loki-File-Server-Ephemeral-Key': ephemeralPubKey64,
};
const firstHopOptions = {
method: 'POST',
// not sure why I can't use anything but json...
// text/plain would be preferred...
body: JSON.stringify({ cipherText64 }),
headers: {
'Content-Type': 'application/json',
'X-Loki-File-Server-Target': '/loki/v1/secure_rpc',
'X-Loki-File-Server-Verb': 'POST',
'X-Loki-File-Server-Headers': JSON.stringify(finalRequestHeader),
},
// we are talking to a snode...
agent: snodeHttpsAgent,
};
// weird this doesn't need NODE_TLS_REJECT_UNAUTHORIZED = '0'
const result = await nodeFetch(url, firstHopOptions);
const txtResponse = await result.text();
if (txtResponse.match(/^Service node is not ready: not in any swarm/i)) {
// mark snode bad
const randomPoolRemainingCount = window.SnodePool.markNodeUnreachable(
randSnode
);
log.warn(
`loki_app_dot_net:::sendToProxy - Marking random snode bad, internet address ${randSnode.ip}:${randSnode.port}. ${randomPoolRemainingCount} snodes remaining in randomPool`
);
// retry (hopefully with new snode)
// FIXME: max number of retries...
return sendToProxy(srvPubKey, endpoint, fetchOptions, options);
}
let response = {};
try {
response = JSON.parse(txtResponse);
} catch (e) {
log.warn(
`loki_app_dot_net:::sendToProxy - Could not parse outer JSON [${txtResponse}]`,
endpoint,
'on',
url
);
}
if (response.meta && response.meta.code === 200) {
// convert base64 in response to binary
const ivAndCiphertextResponse = dcodeIO.ByteBuffer.wrap(
response.data,
'base64'
).toArrayBuffer();
const decrypted = await libloki.crypto.DHDecrypt(
symKey,
ivAndCiphertextResponse
);
const textDecoder = new TextDecoder();
const respStr = textDecoder.decode(decrypted);
// replace response
try {
response = options.textResponse ? respStr : JSON.parse(respStr);
} catch (e) {
log.warn(
`loki_app_dot_net:::sendToProxy - Could not parse inner JSON [${respStr}]`,
endpoint,
'on',
url
);
}
} else {
log.warn(
'loki_app_dot_net:::sendToProxy - file server secure_rpc gave an non-200 response: ',
response,
` txtResponse[${txtResponse}]`,
endpoint
);
}
return { result, txtResponse, response };
};
const serverRequest = async (endpoint, options = {}) => {
const {
params = {},
@ -363,7 +211,7 @@ const serverRequest = async (endpoint, options = {}) => {
} = options;
const url = new URL(endpoint);
if (params) {
if (!_.isEmpty(params)) {
url.search = new URLSearchParams(params);
}
const fetchOptions = {};
@ -395,6 +243,7 @@ const serverRequest = async (endpoint, options = {}) => {
);
return {
err: e,
ok: false,
};
}
@ -424,21 +273,6 @@ const serverRequest = async (endpoint, options = {}) => {
fetchOptions,
options
));
} else if (
window.lokiFeatureFlags.useSnodeProxy &&
FILESERVER_HOSTS.includes(host)
) {
mode = 'sendToProxy';
// url.search automatically includes the ? part
const search = url.search || '';
// strip first slash
const endpointWithQS = `${url.pathname}${search}`.replace(/^\//, '');
({ response, txtResponse, result } = await sendToProxy(
srvPubKey,
endpointWithQS,
fetchOptions,
options
));
} else {
// disable check for .loki
process.env.NODE_TLS_REJECT_UNAUTHORIZED = host.match(/\.loki$/i)
@ -477,14 +311,10 @@ const serverRequest = async (endpoint, options = {}) => {
url
);
}
if (mode === 'sendToProxy') {
// if we can detect, certain types of failures, we can retry...
if (e.code === 'ECONNRESET') {
// retry with counter?
}
}
return {
err: e,
ok: false,
};
}
@ -492,6 +322,7 @@ const serverRequest = async (endpoint, options = {}) => {
return {
err: 'noResult',
response,
ok: false,
};
}
@ -508,11 +339,13 @@ const serverRequest = async (endpoint, options = {}) => {
err: 'statusCode',
statusCode: result.status,
response,
ok: false,
};
}
return {
statusCode: result.status,
response,
ok: result.status >= 200 && result.status <= 299,
};
};
@ -599,10 +432,7 @@ class LokiAppDotNetServerAPI {
// set up pubKey & pubKeyHex properties
// optionally called for mainly file server comms
getPubKeyForUrl() {
if (
!window.lokiFeatureFlags.useSnodeProxy &&
!window.lokiFeatureFlags.useOnionRequests
) {
if (!window.lokiFeatureFlags.useOnionRequests) {
// pubkeys don't matter
return '';
}
@ -628,12 +458,6 @@ class LokiAppDotNetServerAPI {
pubKeyAB =
window.lokiPublicChatAPI.openGroupPubKeys[this.baseServerUrl];
}
} else if (window.lokiFeatureFlags.useSnodeProxy) {
// if in proxy mode, replace with "file."...
// it only supports this host...
pubKeyAB = window.Signal.Crypto.base64ToArrayBuffer(
LOKIFOUNDATION_FILESERVER_PUBKEY
);
}
// else will fail validation later
@ -838,13 +662,13 @@ class LokiAppDotNetServerAPI {
async requestToken() {
let res;
try {
const url = new URL(`${this.baseServerUrl}/loki/v1/get_challenge`);
const params = {
pubKey: this.ourKey,
};
url.search = new URLSearchParams(params);
res = await this.proxyFetch(url);
res = await this.serverRequest('loki/v1/get_challenge', {
method: 'GET',
params,
});
} catch (e) {
// should we retry here?
// no, this is the low level function
@ -877,72 +701,29 @@ class LokiAppDotNetServerAPI {
log.error('requestToken request failed');
return null;
}
const body = await res.json();
const body = res.response;
const token = await libloki.crypto.decryptToken(body);
return token;
}
// activate token
async submitToken(token) {
const fetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pubKey: this.ourKey,
token,
}),
};
try {
const res = await this.proxyFetch(
new URL(`${this.baseServerUrl}/loki/v1/submit_challenge`),
fetchOptions,
{ textResponse: true }
);
const res = await this.serverRequest('loki/v1/submit_challenge', {
method: 'POST',
objBody: {
pubKey: this.ourKey,
token,
},
noJson: true,
});
return res.ok;
} catch (e) {
log.error('submitToken proxyFetch failure', e.code, e.message);
log.error('submitToken serverRequest failure', e.code, e.message);
return false;
}
}
async proxyFetch(urlObj, fetchOptions = { method: 'GET' }, options = {}) {
if (
window.lokiFeatureFlags.useSnodeProxy &&
(this.baseServerUrl === 'https://file-dev.lokinet.org' ||
this.baseServerUrl === 'https://file.lokinet.org' ||
this.baseServerUrl === 'https://file-dev.getsession.org' ||
this.baseServerUrl === 'https://file.getsession.org')
) {
const finalOptions = { ...fetchOptions };
if (!fetchOptions.method) {
finalOptions.method = 'GET';
}
const urlStr = urlObj.toString();
const endpoint = urlStr.replace(`${this.baseServerUrl}/`, '');
const { response, result } = await sendToProxy(
this.pubKey,
endpoint,
finalOptions,
options
);
// emulate nodeFetch response...
return {
ok: result.status === 200,
json: () => response,
};
}
const host = urlObj.host.toLowerCase();
if (host.match(/\.loki$/)) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
const result = nodeFetch(urlObj, fetchOptions, options);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
return result;
}
// make a request to the server
async serverRequest(endpoint, options = {}) {
if (options.forceFreshToken) {
@ -1161,12 +942,9 @@ class LokiAppDotNetServerAPI {
rawBody: data,
};
const { statusCode, response } = await this.serverRequest(
endpoint,
options
);
const { response, ok } = await this.serverRequest(endpoint, options);
if (statusCode !== 200) {
if (!ok) {
throw new Error(`Failed to upload avatar to ${this.baseServerUrl}`);
}
@ -1194,11 +972,8 @@ class LokiAppDotNetServerAPI {
rawBody: data,
};
const { statusCode, response } = await this.serverRequest(
endpoint,
options
);
if (statusCode !== 200) {
const { ok, response } = await this.serverRequest(endpoint, options);
if (!ok) {
throw new Error(`Failed to upload data to server: ${this.baseServerUrl}`);
}

@ -32,7 +32,7 @@ const validOpenGroupServer = async serverUrl => {
{ method: 'GET' },
{ noJson: true }
);
if (res.result.status === 200) {
if (res.result && res.result.status === 200) {
log.info(
`loki_public_chat::validOpenGroupServer - onion routing enabled on ${url.toString()}`
);

@ -451,9 +451,8 @@ window.pubkeyPattern = /@[a-fA-F0-9]{64,66}\b/g;
window.lokiFeatureFlags = {
multiDeviceUnpairing: true,
privateGroupChats: true,
useSnodeProxy: !process.env.USE_STUBBED_NETWORK,
useOnionRequests: true,
useOnionRequestsV2: false,
useOnionRequestsV2: true,
useFileOnionRequests: true,
enableSenderKeys: true,
onionRequestHops: 3,
@ -490,9 +489,9 @@ if (config.environment.includes('test-integration')) {
window.lokiFeatureFlags = {
multiDeviceUnpairing: true,
privateGroupChats: true,
useSnodeProxy: !process.env.USE_STUBBED_NETWORK,
useOnionRequests: false,
useFileOnionRequests: false,
useOnionRequestsV2: false,
debugMessageLogs: true,
enableSenderKeys: true,
useMultiDevice: false,

@ -4,7 +4,6 @@ import https from 'https';
import { Snode } from './snodePool';
import { lokiOnionFetch, SnodeResponse } from './onions';
import { sendToProxy } from './proxy';
const snodeHttpsAgent = new https.Agent({
rejectUnauthorized: false,
@ -17,7 +16,7 @@ async function lokiPlainFetch(
const { log } = window;
if (url.match(/https:\/\//)) {
// import that this does not get set in sendToProxy fetchOptions
// import that this does not get set in lokiFetch fetchOptions
fetchOptions.agent = snodeHttpsAgent;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
} else {
@ -65,10 +64,6 @@ async function lokiFetch(
return await lokiOnionFetch(fetchOptions.body, targetNode);
}
if (window.lokiFeatureFlags.useSnodeProxy && targetNode) {
return await sendToProxy(fetchOptions, targetNode);
}
return await lokiPlainFetch(url, fetchOptions);
} catch (e) {
if (e.code === 'ENOTFOUND') {

@ -6,6 +6,8 @@ import ByteBuffer from 'bytebuffer';
import { StringUtils } from '../utils';
import { OnionAPI } from '../onions';
let onionPayload = 0;
enum RequestError {
BAD_PATH,
OTHER,
@ -435,8 +437,8 @@ const sendOnionRequest = async (
finalRelayOptions,
id
);
log.debug('Onion payload size: ', payload.length);
onionPayload += payload.length;
log.debug('Onion payload size: ', payload.length, ' total:', onionPayload);
const guardFetchOptions = {
method: 'POST',

@ -1,279 +0,0 @@
import fetch from 'node-fetch';
import https from 'https';
import * as SnodePool from './snodePool';
import { sleepFor } from '../../../js/modules/loki_primitives';
import { SnodeResponse } from './onions';
import _ from 'lodash';
const snodeHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
type Snode = SnodePool.Snode;
// (Disable max body rule for code that we will be removing)
// tslint:disable: max-func-body-length
async function processProxyResponse(
response: any,
randSnode: any,
targetNode: Snode,
symmetricKey: any,
options: any,
retryNumber: number
) {
const { log, dcodeIO } = window;
if (response.status === 401) {
// decom or dereg
// remove
// but which the proxy or the target...
// we got a ton of randomPool nodes, let's just not worry about this one
SnodePool.markNodeUnreachable(randSnode);
const text = await response.text();
log.warn(
'lokiRpc:::sendToProxy -',
`snode ${randSnode.ip}:${randSnode.port} to ${targetNode.ip}:${targetNode.port}`,
'snode is decom or dereg: ',
text,
`Try #${retryNumber}`
);
// retry, just count it happening 5 times to be the target for now
return sendToProxy(options, targetNode, retryNumber + 1);
}
// 504 is only present in 2.0.3 and after
// relay is fine but destination is not good
if (response.status === 504) {
const pRetryNumber = retryNumber + 1;
if (pRetryNumber > 3) {
log.warn(
`lokiRpc:::sendToProxy - snode ${randSnode.ip}:${randSnode.port}`,
`can not relay to target node ${targetNode.ip}:${targetNode.port}`,
'after 3 retries'
);
if (options.ourPubKey) {
SnodePool.markNodeUnreachable(targetNode);
}
return false;
}
// we don't have to wait here
// because we're not marking the random snode bad
// grab a fresh random one
return sendToProxy(options, targetNode, pRetryNumber);
}
// 502 is "Next node not found"
// detect SNode is not ready (not in swarm; not done syncing)
// 503 can be proxy target or destination in pre 2.0.3
// 2.0.3 and after means target
if (response.status === 503 || response.status === 500) {
// this doesn't mean the random node is bad, it could be the target node
// but we got a ton of randomPool nodes, let's just not worry about this one
SnodePool.markNodeUnreachable(randSnode);
const text = await response.text();
log.warn(
'lokiRpc:::sendToProxy -',
`snode ${randSnode.ip}:${randSnode.port} to ${targetNode.ip}:${targetNode.port}`,
`code ${response.status} error`,
text,
// `marking random snode bad ${randomPoolRemainingCount} remaining`
`Try #${retryNumber}`
);
// mark as bad for this round (should give it some time and improve success rates)
// retry for a new working snode
const pRetryNumber = retryNumber + 1;
if (pRetryNumber > 5) {
// it's likely a net problem or an actual problem on the target node
// lets mark the target node bad for now
// we'll just rotate it back in if it's a net problem
log.warn(
`lokiRpc:::sendToProxy - Failing ${targetNode.ip}:${targetNode.port} after 5 retries`
);
if (options.ourPubKey) {
SnodePool.markNodeUnreachable(targetNode);
}
return false;
}
// 500 burns through a node too fast,
// let's slow the retry to give it more time to recover
if (response.status === 500) {
await sleepFor(5000);
}
return sendToProxy(options, targetNode, pRetryNumber);
}
/*
if (response.status === 500) {
// usually when the server returns nothing...
}
*/
// FIXME: handle fetch errors/exceptions...
if (response.status !== 200) {
// let us know we need to create handlers for new unhandled codes
log.warn(
'lokiRpc:::sendToProxy - fetch non-200 statusCode',
response.status,
`from snode ${randSnode.ip}:${randSnode.port} to ${targetNode.ip}:${targetNode.port}`
);
return false;
}
const ciphertext = await response.text();
if (!ciphertext) {
// avoid base64 decode failure
// usually a 500 but not always
// could it be a timeout?
log.warn(
'lokiRpc:::sendToProxy - Server did not return any data for',
options,
targetNode
);
return false;
}
let plaintext;
let ciphertextBuffer;
try {
ciphertextBuffer = dcodeIO.ByteBuffer.wrap(
ciphertext,
'base64'
).toArrayBuffer();
const plaintextBuffer = await window.libloki.crypto.DHDecrypt(
symmetricKey,
ciphertextBuffer
);
const textDecoder = new TextDecoder();
plaintext = textDecoder.decode(plaintextBuffer);
} catch (e) {
log.error(
'lokiRpc:::sendToProxy - decode error',
e.code,
e.message,
`from ${randSnode.ip}:${randSnode.port} to ${targetNode.ip}:${targetNode.port} ciphertext:`,
ciphertext
);
if (ciphertextBuffer) {
log.error('ciphertextBuffer', ciphertextBuffer);
}
return false;
}
if (retryNumber) {
log.debug(
'lokiRpc:::sendToProxy - request succeeded,',
`snode ${randSnode.ip}:${randSnode.port} to ${targetNode.ip}:${targetNode.port}`,
`on retry #${retryNumber}`
);
}
try {
const jsonRes = JSON.parse(plaintext);
if (jsonRes.body === 'Timestamp error: check your clock') {
log.error(
'lokiRpc:::sendToProxy - Timestamp error: check your clock',
Date.now()
);
}
return jsonRes;
} catch (e) {
log.error(
'lokiRpc:::sendToProxy - (outer) parse error',
e.code,
e.message,
`from ${randSnode.ip}:${randSnode.port} json:`,
plaintext
);
}
return false;
}
// tslint:enable: max-func-body-length
export async function sendToProxy(
options: any = {},
targetNode: Snode,
retryNumber: any = 0
): Promise<boolean | SnodeResponse> {
const { log, StringView, libloki, libsignal } = window;
let snodePool = await SnodePool.getRandomSnodePool();
if (snodePool.length < 2) {
// this is semi-normal to happen
log.info(
'lokiRpc::sendToProxy - Not enough service nodes for a proxy request, only have:',
snodePool.length,
'snode, attempting refresh'
);
await SnodePool.refreshRandomPool([]);
snodePool = await SnodePool.getRandomSnodePool();
if (snodePool.length < 2) {
log.error(
'lokiRpc::sendToProxy - Not enough service nodes for a proxy request, only have:',
snodePool.length,
'failing'
);
return false;
}
}
// Making sure the proxy node is not the same as the target node:
const snodePoolSafe = _.without(
snodePool,
_.find(snodePool, { pubkey_ed25519: targetNode.pubkey_ed25519 })
);
const randSnode = _.sample(snodePoolSafe);
if (!randSnode) {
log.error('No snodes left for a proxy request');
return false;
}
// Don't allow arbitrary URLs, only snodes and loki servers
const url = `https://${randSnode.ip}:${randSnode.port}/proxy`;
const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519);
const myKeys = await libloki.crypto.generateEphemeralKeyPair();
const symmetricKey = await libsignal.Curve.async.calculateAgreement(
snPubkeyHex,
myKeys.privKey
);
const textEncoder = new TextEncoder();
const body = JSON.stringify(options);
const plainText = textEncoder.encode(body);
const ivAndCiphertext = await libloki.crypto.DHEncrypt(
symmetricKey,
plainText
);
const firstHopOptions = {
method: 'POST',
body: ivAndCiphertext,
headers: {
'X-Sender-Public-Key': StringView.arrayBufferToHex(myKeys.pubKey),
'X-Target-Snode-Key': targetNode.pubkey_ed25519,
},
agent: snodeHttpsAgent,
};
// we only proxy to snodes...
const response = await fetch(url, firstHopOptions);
return processProxyResponse(
response,
randSnode,
targetNode,
symmetricKey,
options,
retryNumber
);
}

1
ts/window.d.ts vendored

@ -59,7 +59,6 @@ declare global {
lokiFeatureFlags: {
multiDeviceUnpairing: boolean;
privateGroupChats: boolean;
useSnodeProxy: boolean;
useOnionRequests: boolean;
useOnionRequestsV2: boolean;
useFileOnionRequests: boolean;

Loading…
Cancel
Save