You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			280 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			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
 | |
|   );
 | |
| }
 |