session-desktop/ts/session/snode_api/proxy.ts
2020-07-06 15:41:52 +10:00

280 lines
7.9 KiB
TypeScript

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
);
}