remove completely send to proxy

pull/1383/head
Audric Ackermann 5 years ago
parent 4c91d977ca
commit 2b13321c23
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -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 = {},
@ -424,21 +272,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,12 +310,7 @@ 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,
};
@ -599,10 +427,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 +453,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
@ -900,41 +719,6 @@ class LokiAppDotNetServerAPI {
}
}
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) {

@ -448,7 +448,6 @@ 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: true,
useFileOnionRequests: true,
@ -489,6 +488,7 @@ if (config.environment.includes('test-integration')) {
privateGroupChats: true,
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,
@ -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,9 @@ const sendOnionRequest = async (
finalRelayOptions,
id
);
onionPayload += payload.length;
log.debug('Onion payload size: ', payload.length);
log.debug('Onion 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

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

Loading…
Cancel
Save