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.
		
		
		
		
		
			
		
			
				
	
	
		
			1177 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			1177 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			TypeScript
		
	
| import https from 'https';
 | |
| // eslint-disable-next-line import/no-named-default
 | |
| import { default as insecureNodeFetch, RequestInit, Response } from 'node-fetch';
 | |
| import ByteBuffer from 'bytebuffer';
 | |
| import pRetry from 'p-retry';
 | |
| import { cloneDeep, isEmpty, isString, omit } from 'lodash';
 | |
| import { AbortSignal } from 'abort-controller';
 | |
| import { to_string } from 'libsodium-wrappers-sumo';
 | |
| // eslint-disable-next-line import/no-unresolved
 | |
| import { AbortSignal as AbortSignalNode } from 'node-fetch/externals';
 | |
| 
 | |
| import { dropSnodeFromSnodePool, dropSnodeFromSwarmIfNeeded, updateSwarmFor } from './snodePool';
 | |
| 
 | |
| import { OnionPaths } from '../../onions';
 | |
| import { toHex } from '../../utils/String';
 | |
| import { ed25519Str, incrementBadPathCountOrDrop } from '../../onions/onionPath';
 | |
| 
 | |
| import { Snode } from '../../../data/data';
 | |
| import { ERROR_CODE_NO_CONNECT } from './SNodeAPI';
 | |
| import { hrefPnServerProd } from '../push_notification_api/PnServer';
 | |
| import { callUtilsWorker } from '../../../webworker/workers/browser/util_worker_interface';
 | |
| import { encodeV4Request } from '../../onions/onionv4';
 | |
| 
 | |
| // hold the ed25519 key of a snode against the time it fails. Used to remove a snode only after a few failures (snodeFailureThreshold failures)
 | |
| let snodeFailureCount: Record<string, number> = {};
 | |
| 
 | |
| export const resetSnodeFailureCount = () => {
 | |
|   snodeFailureCount = {};
 | |
| };
 | |
| 
 | |
| // The number of times a snode can fail before it's replaced.
 | |
| const snodeFailureThreshold = 3;
 | |
| 
 | |
| export const OXEN_SERVER_ERROR = 'Oxen Server error';
 | |
| 
 | |
| // Not ideal, but a pRetry.AbortError only lets us customize the message, and not the code
 | |
| const errorContent404 = ': 404 ';
 | |
| export const was404Error = (error: Error) => error.message.includes(errorContent404);
 | |
| 
 | |
| export const buildErrorMessageWithFailedCode = (prefix: string, code: number, suffix: string) =>
 | |
|   `${prefix}: ${code} ${suffix}`;
 | |
| 
 | |
| /**
 | |
|  * When sending a request over onion, we might get two status.
 | |
|  * The first one, on the request itself, the other one in the json returned.
 | |
|  *
 | |
|  * If the request failed to reach the one of the node of the onion path, the one on the request is set.
 | |
|  * But if the request reaches the destination node and it fails to process the request (bad node for this pubkey),
 | |
|  * you will get a 200 on the request itself, but the json you get will contain the real status.
 | |
|  */
 | |
| export interface SnodeResponse {
 | |
|   bodyBinary: Uint8Array | null;
 | |
|   body: string;
 | |
|   status?: number;
 | |
| }
 | |
| 
 | |
| export interface SnodeParsedResponse {
 | |
|   bodyBinary: Uint8Array | null;
 | |
|   body: string;
 | |
|   status?: number;
 | |
| }
 | |
| 
 | |
| // v4 onion request have a weird string and binary content, so better get it as binary to extract just the string part
 | |
| export interface SnodeResponseV4 {
 | |
|   bodyBinary: Uint8Array | null;
 | |
|   status?: number;
 | |
| }
 | |
| 
 | |
| export const NEXT_NODE_NOT_FOUND_PREFIX = 'Next node not found: ';
 | |
| export const ERROR_421_HANDLED_RETRY_REQUEST =
 | |
|   '421 handled. Retry this request with a new targetNode';
 | |
| 
 | |
| export const CLOCK_OUT_OF_SYNC_MESSAGE_ERROR =
 | |
|   'Your clock is out of sync with the network. Check your clock.';
 | |
| 
 | |
| export type EncodeV4OnionRequestInfos = {
 | |
|   headers: Record<string, any> | null | undefined;
 | |
|   body?: string | Uint8Array | null;
 | |
|   method: string;
 | |
|   endpoint: string;
 | |
| };
 | |
| 
 | |
| async function encryptOnionV4RequestForPubkey(
 | |
|   pubKeyX25519hex: string,
 | |
|   requestInfo: EncodeV4OnionRequestInfos
 | |
| ) {
 | |
|   const plaintext = encodeV4Request(requestInfo);
 | |
| 
 | |
|   return callUtilsWorker('encryptForPubkey', pubKeyX25519hex, plaintext) as Promise<
 | |
|     DestinationContext
 | |
|   >;
 | |
| }
 | |
| // Returns the actual ciphertext, symmetric key that will be used
 | |
| // for decryption, and an ephemeral_key to send to the next hop
 | |
| async function encryptForPubKey(
 | |
|   pubKeyX25519hex: string,
 | |
|   requestInfo: any
 | |
| ): Promise<DestinationContext> {
 | |
|   const plaintext = new TextEncoder().encode(JSON.stringify(requestInfo));
 | |
| 
 | |
|   return callUtilsWorker('encryptForPubkey', pubKeyX25519hex, plaintext) as Promise<
 | |
|     DestinationContext
 | |
|   >;
 | |
| }
 | |
| 
 | |
| export type DestinationRelayV2 = {
 | |
|   host?: string;
 | |
|   protocol?: string;
 | |
|   port?: number;
 | |
|   destination?: string;
 | |
|   method?: string;
 | |
|   target?: string;
 | |
| };
 | |
| 
 | |
| // `ctx` holds info used by `node` to relay further
 | |
| async function encryptForRelayV2(
 | |
|   relayX25519hex: string,
 | |
|   destination: DestinationRelayV2,
 | |
|   ctx: DestinationContext
 | |
| ) {
 | |
|   if (!destination.host && !destination.destination) {
 | |
|     window?.log?.warn('loki_rpc::encryptForRelayV2 - no destination', destination);
 | |
|   }
 | |
| 
 | |
|   const reqObj = {
 | |
|     ...destination,
 | |
|     ephemeral_key: toHex(ctx.ephemeralKey),
 | |
|   };
 | |
| 
 | |
|   const plaintext = encodeCiphertextPlusJson(ctx.ciphertext, reqObj);
 | |
|   return callUtilsWorker('encryptForPubkey', relayX25519hex, plaintext);
 | |
| }
 | |
| 
 | |
| /// Encode ciphertext as (len || binary) and append payloadJson as utf8
 | |
| function encodeCiphertextPlusJson(
 | |
|   ciphertext: Uint8Array,
 | |
|   payloadJson: Record<string, any>
 | |
| ): Uint8Array {
 | |
|   const payloadStr = JSON.stringify(payloadJson);
 | |
| 
 | |
|   const bufferJson = ByteBuffer.wrap(payloadStr, 'utf8');
 | |
| 
 | |
|   const len = ciphertext.length;
 | |
|   const arrayLen = bufferJson.buffer.length + 4 + len;
 | |
|   const littleEndian = true;
 | |
|   const buffer = new ByteBuffer(arrayLen, littleEndian);
 | |
| 
 | |
|   buffer.writeInt32(len);
 | |
|   buffer.append(ciphertext);
 | |
|   buffer.append(bufferJson);
 | |
| 
 | |
|   return new Uint8Array(buffer.buffer);
 | |
| }
 | |
| 
 | |
| async function buildOnionCtxs(
 | |
|   nodePath: Array<Snode>,
 | |
|   destCtx: DestinationContext,
 | |
|   useV4: boolean,
 | |
|   targetED25519Hex?: string,
 | |
|   finalRelayOptions?: FinalRelayOptions
 | |
| ) {
 | |
|   const ctxes = [destCtx];
 | |
|   if (!nodePath) {
 | |
|     throw new Error('buildOnionCtxs needs a valid path');
 | |
|   }
 | |
|   // from (3) 2 to 0
 | |
|   const firstPos = nodePath.length - 1;
 | |
| 
 | |
|   for (let i = firstPos; i > -1; i -= 1) {
 | |
|     let dest: DestinationRelayV2;
 | |
|     const relayingToFinalDestination = i === firstPos; // if last position
 | |
| 
 | |
|     if (relayingToFinalDestination && finalRelayOptions) {
 | |
|       const isCallToPn = finalRelayOptions?.host === hrefPnServerProd;
 | |
|       const target = !isCallToPn && !useV4 ? '/loki/v3/lsrpc' : '/oxen/v4/lsrpc';
 | |
| 
 | |
|       dest = {
 | |
|         host: finalRelayOptions.host,
 | |
|         target,
 | |
|         method: 'POST',
 | |
|       };
 | |
| 
 | |
|       if (finalRelayOptions?.protocol === 'http') {
 | |
|         dest.protocol = finalRelayOptions.protocol;
 | |
|         dest.port = finalRelayOptions.port || 80;
 | |
|       }
 | |
|     } else {
 | |
|       // set x25519 if destination snode
 | |
|       let pubkeyHex = targetED25519Hex; // relayingToFinalDestination
 | |
|       // or ed25519 snode destination
 | |
|       if (!relayingToFinalDestination) {
 | |
|         pubkeyHex = nodePath[i + 1].pubkey_ed25519;
 | |
|         if (!pubkeyHex) {
 | |
|           window?.log?.error(
 | |
|             'loki_rpc:::buildOnionGuardNodePayload - no ed25519 for',
 | |
|             nodePath[i + 1],
 | |
|             'path node',
 | |
|             i + 1
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|       // destination takes a hex key
 | |
|       dest = {
 | |
|         destination: pubkeyHex,
 | |
|       };
 | |
|     }
 | |
|     try {
 | |
|       // eslint-disable-next-line no-await-in-loop
 | |
|       const ctx = await encryptForRelayV2(nodePath[i].pubkey_x25519, dest, ctxes[ctxes.length - 1]);
 | |
|       ctxes.push(ctx);
 | |
|     } catch (e) {
 | |
|       window?.log?.error(
 | |
|         'loki_rpc:::buildOnionGuardNodePayload - encryptForRelayV2 failure',
 | |
|         e.code,
 | |
|         e.message
 | |
|       );
 | |
|       throw e;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return ctxes;
 | |
| }
 | |
| 
 | |
| // we just need the targetNode.pubkey_ed25519 for the encryption
 | |
| // targetPubKey is ed25519 if snode is the target
 | |
| async function buildOnionGuardNodePayload(
 | |
|   nodePath: Array<Snode>,
 | |
|   destCtx: DestinationContext,
 | |
|   useV4: boolean,
 | |
|   targetED25519Hex?: string,
 | |
|   finalRelayOptions?: FinalRelayOptions
 | |
| ) {
 | |
|   const ctxes = await buildOnionCtxs(nodePath, destCtx, useV4, targetED25519Hex, finalRelayOptions);
 | |
| 
 | |
|   // this is the OUTER side of the onion, the one encoded with multiple layer
 | |
|   // So the one we will send to the first guard node.
 | |
|   const guardCtx = ctxes[ctxes.length - 1]; // last ctx
 | |
| 
 | |
|   // New "semi-binary" encoding
 | |
| 
 | |
|   const guardPayloadObj = {
 | |
|     ephemeral_key: toHex(guardCtx.ephemeralKey),
 | |
|   };
 | |
| 
 | |
|   return encodeCiphertextPlusJson(guardCtx.ciphertext, guardPayloadObj);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 406 is a clock out of sync error
 | |
|  * 425 is the new 406 (too-early status code)
 | |
|  */
 | |
| function process406Or425Error(statusCode: number) {
 | |
|   if (statusCode === 406 || statusCode === 425) {
 | |
|     // clock out of sync
 | |
|     // this will make the pRetry stop
 | |
|     throw new pRetry.AbortError(CLOCK_OUT_OF_SYNC_MESSAGE_ERROR);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function processOxenServerError(_statusCode: number, body?: string) {
 | |
|   if (body === OXEN_SERVER_ERROR) {
 | |
|     window?.log?.warn('[path] Got Oxen server Error. Not much to do if the server has troubles.');
 | |
|     throw new pRetry.AbortError(OXEN_SERVER_ERROR);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 421 is a invalid swarm error
 | |
|  */
 | |
| async function process421Error(
 | |
|   statusCode: number,
 | |
|   body: string,
 | |
|   associatedWith?: string,
 | |
|   destinationSnodeEd25519?: string
 | |
| ) {
 | |
|   if (statusCode === 421) {
 | |
|     await handle421InvalidSwarm({
 | |
|       destinationSnodeEd25519,
 | |
|       body,
 | |
|       associatedWith,
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handle throwing errors for destination errors.
 | |
|  * A destination can either be a server (like an opengroup server) in this case destinationEd25519 is unset or be a snode (for snode rpc calls) and destinationEd25519 is set in this case.
 | |
|  *
 | |
|  * If destinationEd25519 is set, we will increment the failure count of the specified snode
 | |
|  */
 | |
| export async function processOnionRequestErrorAtDestination({
 | |
|   statusCode,
 | |
|   body,
 | |
|   destinationSnodeEd25519,
 | |
|   associatedWith,
 | |
| }: {
 | |
|   statusCode: number;
 | |
|   body: string;
 | |
|   destinationSnodeEd25519?: string;
 | |
|   associatedWith?: string;
 | |
| }) {
 | |
|   if (statusCode === 200) {
 | |
|     return;
 | |
|   }
 | |
|   window?.log?.info(
 | |
|     `processOnionRequestErrorAtDestination. statusCode nok: ${statusCode}: "${body}"`
 | |
|   );
 | |
|   process406Or425Error(statusCode);
 | |
|   processOxenServerError(statusCode, body);
 | |
|   await process421Error(statusCode, body, associatedWith, destinationSnodeEd25519);
 | |
|   if (destinationSnodeEd25519) {
 | |
|     await processAnyOtherErrorAtDestination(
 | |
|       statusCode,
 | |
|       body,
 | |
|       destinationSnodeEd25519,
 | |
|       associatedWith
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function handleNodeNotFound({
 | |
|   ed25519NotFound,
 | |
|   associatedWith,
 | |
| }: {
 | |
|   ed25519NotFound: string;
 | |
|   associatedWith?: string;
 | |
| }) {
 | |
|   const shortNodeNotFound = ed25519Str(ed25519NotFound);
 | |
|   window?.log?.warn('Handling NODE NOT FOUND with: ', shortNodeNotFound);
 | |
| 
 | |
|   if (associatedWith) {
 | |
|     await dropSnodeFromSwarmIfNeeded(associatedWith, ed25519NotFound);
 | |
|   }
 | |
| 
 | |
|   await dropSnodeFromSnodePool(ed25519NotFound);
 | |
|   snodeFailureCount[ed25519NotFound] = 0;
 | |
|   // try to remove the not found snode from any of the paths if it's there.
 | |
|   // it may not be here, as the snode note found might be the target snode of the request.
 | |
|   await OnionPaths.dropSnodeFromPath(ed25519NotFound);
 | |
| }
 | |
| 
 | |
| async function processAnyOtherErrorOnPath(
 | |
|   status: number,
 | |
|   guardNodeEd25519: string,
 | |
|   ciphertext?: string,
 | |
|   associatedWith?: string
 | |
| ) {
 | |
|   // this test checks for an error in your path.
 | |
|   if (status !== 200) {
 | |
|     window?.log?.warn(`[path] Got status: ${status}`);
 | |
| 
 | |
|     if (status === 404 || status === 400) {
 | |
|       window?.log?.warn(
 | |
|         'processAnyOtherErrorOnPathgot 404 or 400, probably a dead sogs. Skipping bad path update'
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // If we have a specific node in fault we can exclude just this node.
 | |
|     if (ciphertext?.startsWith(NEXT_NODE_NOT_FOUND_PREFIX)) {
 | |
|       const nodeNotFound = ciphertext.substr(NEXT_NODE_NOT_FOUND_PREFIX.length);
 | |
|       // we are checking errors on the path, a nodeNotFound on the path should trigger a rebuild
 | |
| 
 | |
|       await handleNodeNotFound({ ed25519NotFound: nodeNotFound, associatedWith });
 | |
|     } else {
 | |
|       // Otherwise we increment the whole path failure count
 | |
| 
 | |
|       await incrementBadPathCountOrDrop(guardNodeEd25519);
 | |
|     }
 | |
| 
 | |
|     processOxenServerError(status, ciphertext);
 | |
| 
 | |
|     throw new Error(`Bad Path handled. Retry this request. Status: ${status}`);
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function processAnyOtherErrorAtDestination(
 | |
|   status: number,
 | |
|   body: string,
 | |
|   destinationEd25519: string,
 | |
|   associatedWith?: string
 | |
| ) {
 | |
|   // this test checks for error at the destination.
 | |
|   if (
 | |
|     status !== 400 &&
 | |
|     status !== 406 && // handled in process406Error
 | |
|     status !== 421 // handled in process421Error
 | |
|   ) {
 | |
|     window?.log?.warn(`[path] Got status at destination: ${status}`);
 | |
| 
 | |
|     if (body?.startsWith(NEXT_NODE_NOT_FOUND_PREFIX)) {
 | |
|       const nodeNotFound = body.substr(NEXT_NODE_NOT_FOUND_PREFIX.length);
 | |
|       // if we get a nodeNotFound at the destination. it means the targetNode to which we made the request is not found.
 | |
|       await handleNodeNotFound({
 | |
|         ed25519NotFound: nodeNotFound,
 | |
|         associatedWith,
 | |
|       });
 | |
| 
 | |
|       // We have to retry with another targetNode so it's not just rebuilding the path. We have to go one lever higher (lokiOnionFetch).
 | |
|       // status is 502 for a node not found
 | |
|       throw new pRetry.AbortError(
 | |
|         `Bad Path handled. Retry this request with another targetNode. Status: ${status}`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     await Onions.incrementBadSnodeCountOrDrop({
 | |
|       snodeEd25519: destinationEd25519,
 | |
|       associatedWith,
 | |
|     });
 | |
| 
 | |
|     throw new Error(`Bad Path handled. Retry this request. Status: ${status}`);
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function processOnionRequestErrorOnPath(
 | |
|   httpStatusCode: number, // this is the one on the response object, not inside the json response
 | |
|   ciphertext: string | ArrayBuffer,
 | |
|   guardNodeEd25519: string,
 | |
|   destinationEd25519Key?: string,
 | |
|   associatedWith?: string
 | |
| ) {
 | |
|   let cipherAsString: string = '';
 | |
|   if (isString(ciphertext)) {
 | |
|     cipherAsString = ciphertext;
 | |
|   } else {
 | |
|     try {
 | |
|       cipherAsString = to_string(new Uint8Array(ciphertext));
 | |
|     } catch (e) {
 | |
|       // we might actually end up often in this case here often (for all calls to a non snode, so with onionv4 we
 | |
|       // will get binary data, and the to_string above won't work as it has a custom onion v4 encoding)
 | |
|       cipherAsString = '';
 | |
|     }
 | |
|   }
 | |
|   if (httpStatusCode !== 200) {
 | |
|     window?.log?.warn('processOnionRequestErrorOnPath:', ciphertext);
 | |
|   }
 | |
|   process406Or425Error(httpStatusCode);
 | |
|   await process421Error(httpStatusCode, cipherAsString, associatedWith, destinationEd25519Key);
 | |
|   await processAnyOtherErrorOnPath(
 | |
|     httpStatusCode,
 | |
|     guardNodeEd25519,
 | |
|     cipherAsString,
 | |
|     associatedWith
 | |
|   );
 | |
| }
 | |
| 
 | |
| function processAbortedRequest(abortSignal?: AbortSignal) {
 | |
|   if (abortSignal?.aborted) {
 | |
|     window?.log?.warn('[path] Call aborted');
 | |
|     // this will make the pRetry stop
 | |
|     throw new pRetry.AbortError('Request got aborted');
 | |
|   }
 | |
| }
 | |
| 
 | |
| const debug = false;
 | |
| 
 | |
| /**
 | |
|  * Only exported for testing purpose
 | |
|  */
 | |
| async function decodeOnionResult(
 | |
|   symmetricKey: ArrayBuffer,
 | |
|   ciphertext: string
 | |
| ): Promise<{
 | |
|   ciphertextBuffer: Uint8Array;
 | |
|   plaintext: string;
 | |
|   plaintextBuffer: ArrayBuffer;
 | |
| }> {
 | |
|   let parsedCiphertext = ciphertext;
 | |
|   try {
 | |
|     const jsonRes = JSON.parse(ciphertext);
 | |
|     parsedCiphertext = jsonRes.result;
 | |
|   } catch (e) {
 | |
|     // just try to get a json object from what is inside (for PN requests), if it fails, continue ()
 | |
|   }
 | |
|   const ciphertextBuffer = await callUtilsWorker('fromBase64ToArrayBuffer', parsedCiphertext);
 | |
| 
 | |
|   const plaintextBuffer = (await callUtilsWorker(
 | |
|     'DecryptAESGCM',
 | |
|     new Uint8Array(symmetricKey),
 | |
|     new Uint8Array(ciphertextBuffer)
 | |
|   )) as ArrayBuffer;
 | |
| 
 | |
|   return {
 | |
|     ciphertextBuffer,
 | |
|     plaintext: new TextDecoder().decode(plaintextBuffer),
 | |
|     plaintextBuffer,
 | |
|   };
 | |
| }
 | |
| 
 | |
| export const STATUS_NO_STATUS = 8888;
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * Process a non v4 onion request and throw the corresponding errors if needed, depending on the status code or the content of the body.
 | |
|  *
 | |
|  * This function will handle dropping a snode from the swarm, the snode list and the path if it believes it needs to be dropped, and just increment the failure, etc.
 | |
|  * Note: Only exported for testing purpose
 | |
|  */
 | |
| async function processOnionResponse({
 | |
|   response,
 | |
|   symmetricKey,
 | |
|   guardNode,
 | |
|   abortSignal,
 | |
|   associatedWith,
 | |
|   destinationSnodeEd25519,
 | |
| }: {
 | |
|   response?: { text: () => Promise<string>; status: number };
 | |
|   symmetricKey?: ArrayBuffer;
 | |
|   guardNode: Snode;
 | |
|   destinationSnodeEd25519?: string;
 | |
|   abortSignal?: AbortSignal;
 | |
|   associatedWith?: string;
 | |
| }): Promise<SnodeResponse> {
 | |
|   let ciphertext = '';
 | |
| 
 | |
|   processAbortedRequest(abortSignal);
 | |
| 
 | |
|   try {
 | |
|     ciphertext = (await response?.text()) || '';
 | |
|   } catch (e) {
 | |
|     window?.log?.warn(e);
 | |
|   }
 | |
| 
 | |
|   await processOnionRequestErrorOnPath(
 | |
|     response?.status || STATUS_NO_STATUS,
 | |
|     ciphertext,
 | |
|     guardNode.pubkey_ed25519,
 | |
|     destinationSnodeEd25519,
 | |
|     associatedWith
 | |
|   );
 | |
| 
 | |
|   if (!ciphertext) {
 | |
|     window?.log?.warn(
 | |
|       '[path] sessionRpc::processingOnionResponse - Target node return empty ciphertext'
 | |
|     );
 | |
|     throw new Error('Target node return empty ciphertext');
 | |
|   }
 | |
| 
 | |
|   let plaintext;
 | |
|   let ciphertextBuffer;
 | |
| 
 | |
|   try {
 | |
|     if (!symmetricKey) {
 | |
|       throw new Error('Decoding onion requests needs a symmetricKey');
 | |
|     }
 | |
|     const decoded = await Onions.decodeOnionResult(symmetricKey, ciphertext);
 | |
| 
 | |
|     plaintext = decoded.plaintext;
 | |
|     ciphertextBuffer = decoded.ciphertextBuffer;
 | |
|   } catch (e) {
 | |
|     window?.log?.error('[path] sessionRpc::processingOnionResponse - decode error', e);
 | |
|     if (symmetricKey) {
 | |
|       window?.log?.error(
 | |
|         '[path] sessionRpc::processingOnionResponse - symmetricKey',
 | |
|         toHex(symmetricKey)
 | |
|       );
 | |
|     }
 | |
|     if (ciphertextBuffer) {
 | |
|       window?.log?.error(
 | |
|         '[path] sessionRpc::processingOnionResponse - ciphertextBuffer',
 | |
|         toHex(ciphertextBuffer)
 | |
|       );
 | |
|     }
 | |
|     throw new Error('Ciphertext decode error');
 | |
|   }
 | |
| 
 | |
|   if (debug) {
 | |
|     window?.log?.debug('sessionRpc::processingOnionResponse - plaintext', plaintext);
 | |
|   }
 | |
| 
 | |
|   try {
 | |
|     const jsonRes = JSON.parse(plaintext, (_key, value) => {
 | |
|       if (typeof value === 'number' && value > Number.MAX_SAFE_INTEGER) {
 | |
|         window?.log?.warn('Received an out of bounds js number');
 | |
|       }
 | |
|       return value;
 | |
|     }) as Record<string, any>;
 | |
| 
 | |
|     const status = jsonRes.status_code || jsonRes.status;
 | |
| 
 | |
|     await processOnionRequestErrorAtDestination({
 | |
|       statusCode: status,
 | |
|       body: jsonRes?.body, // this is really important. the `.body`. the .body should be a string. for instance for nodeNotFound but is most likely a dict (Record<string,any>))
 | |
|       destinationSnodeEd25519,
 | |
|       associatedWith,
 | |
|     });
 | |
| 
 | |
|     return jsonRes as SnodeResponse;
 | |
|   } catch (e) {
 | |
|     window?.log?.error(
 | |
|       `[path] sessionRpc::processingOnionResponse - Rethrowing error ${e.message}'`
 | |
|     );
 | |
|     throw e;
 | |
|   }
 | |
| }
 | |
| 
 | |
| async function processNoSymmetricKeyError(
 | |
|   guardNode: Snode,
 | |
|   symmetricKey?: ArrayBuffer
 | |
| ): Promise<ArrayBuffer> {
 | |
|   if (!symmetricKey) {
 | |
|     const errorMsg =
 | |
|       'No symmetric key to decode response, probably a time out on the onion request itself';
 | |
| 
 | |
|     window?.log?.error(errorMsg);
 | |
| 
 | |
|     await incrementBadPathCountOrDrop(guardNode.pubkey_ed25519);
 | |
| 
 | |
|     throw new Error(errorMsg);
 | |
|   }
 | |
|   return symmetricKey;
 | |
| }
 | |
| 
 | |
| async function processOnionResponseV4({
 | |
|   response,
 | |
|   symmetricKey,
 | |
|   abortSignal,
 | |
|   guardNode,
 | |
|   destinationSnodeEd25519,
 | |
|   associatedWith,
 | |
| }: {
 | |
|   response?: Response;
 | |
|   symmetricKey?: ArrayBuffer;
 | |
|   guardNode: Snode;
 | |
|   destinationSnodeEd25519?: string;
 | |
|   abortSignal?: AbortSignal;
 | |
|   associatedWith?: string;
 | |
| }): Promise<SnodeResponseV4 | undefined> {
 | |
|   processAbortedRequest(abortSignal);
 | |
|   const validSymmetricKey = await processNoSymmetricKeyError(guardNode, symmetricKey);
 | |
| 
 | |
|   const cipherText = (await response?.arrayBuffer()) || new ArrayBuffer(0);
 | |
| 
 | |
|   if (!cipherText) {
 | |
|     window?.log?.warn(
 | |
|       '[path] sessionRpc::processOnionResponseV4 - Target node/path return empty ciphertext'
 | |
|     );
 | |
|     throw new Error('Target node return empty ciphertext');
 | |
|   }
 | |
| 
 | |
|   // before trying to decrypt the message with the symmetric key,
 | |
|   // we have to make sure the content is not a path error.
 | |
|   // This is because an error on path won't be encrypted with our symmetric key at all, but we still need to take care of it.
 | |
|   await processOnionRequestErrorOnPath(
 | |
|     response?.status || STATUS_NO_STATUS,
 | |
|     cipherText,
 | |
|     guardNode.pubkey_ed25519,
 | |
|     destinationSnodeEd25519,
 | |
|     associatedWith
 | |
|   );
 | |
| 
 | |
|   const plaintextBuffer = await callUtilsWorker(
 | |
|     'DecryptAESGCM',
 | |
|     new Uint8Array(validSymmetricKey),
 | |
|     new Uint8Array(cipherText)
 | |
|   );
 | |
| 
 | |
|   const bodyBinary: Uint8Array = new Uint8Array(plaintextBuffer);
 | |
| 
 | |
|   // Handling of the status code of the destination is done on the calling function, because the content of the onion response needs first to be decoded.
 | |
|   // Also, we actually do not care much about the status code of the destination here, because the destination
 | |
|   // cannot be a service node(we do not support onion v4 to a snode destination, only * through * them),
 | |
|   // so there is no rebuilding of the path for a destination case possible.
 | |
| 
 | |
|   return {
 | |
|     bodyBinary,
 | |
|   };
 | |
| }
 | |
| 
 | |
| export const snodeHttpsAgent = new https.Agent({
 | |
|   rejectUnauthorized: false,
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * As far as I know, FinalRelayOptions is only used for contacting non service node. So PN server, opengroups, fileserver, etc.
 | |
|  * It contains the details the last service node of the onion path needs to use to contact who we need to contact.
 | |
|  * So things, like the ip/port and protocol of the opengroup server/fileserver/PN server.
 | |
|  */
 | |
| export type FinalRelayOptions = {
 | |
|   host: string;
 | |
|   protocol?: 'http' | 'https'; // default to https
 | |
|   port?: number; // default to 443
 | |
| };
 | |
| 
 | |
| export type DestinationContext = {
 | |
|   ciphertext: Uint8Array;
 | |
|   symmetricKey: ArrayBuffer;
 | |
|   ephemeralKey: ArrayBuffer;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Handle a 421. The body is supposed to be the new swarm nodes for this publickey.
 | |
|  * @param destinationSnodeEd25519 the snode gaving the reply
 | |
|  * @param body the new swarm not parsed. If an error happens while parsing this we will drop the snode.
 | |
|  * @param associatedWith the specific publickey associated with this call
 | |
|  */
 | |
| async function handle421InvalidSwarm({
 | |
|   body,
 | |
|   destinationSnodeEd25519,
 | |
|   associatedWith,
 | |
| }: {
 | |
|   body: string;
 | |
|   destinationSnodeEd25519?: string;
 | |
|   associatedWith?: string;
 | |
| }) {
 | |
|   if (!destinationSnodeEd25519 || !associatedWith) {
 | |
|     // The snode isn't associated with the given public key anymore
 | |
|     // this does not make much sense to have a 421 without a publicKey set.
 | |
|     throw new Error('status 421 without a final destination or no associatedWith makes no sense');
 | |
|   }
 | |
|   window?.log?.info(`Invalidating swarm for ${ed25519Str(associatedWith)}`);
 | |
| 
 | |
|   try {
 | |
|     const parsedBody = JSON.parse(body);
 | |
| 
 | |
|     // The snode isn't associated with the given public key anymore
 | |
|     if (parsedBody?.snodes?.length) {
 | |
|       // the snode gave us the new swarm. Save it for the next retry
 | |
|       window?.log?.warn(
 | |
|         `Wrong swarm, now looking for pk ${ed25519Str(associatedWith)} at snodes: `,
 | |
|         parsedBody.snodes.map((s: any) => ed25519Str(s.pubkey_ed25519))
 | |
|       );
 | |
| 
 | |
|       await updateSwarmFor(associatedWith, parsedBody.snodes);
 | |
|       throw new pRetry.AbortError(ERROR_421_HANDLED_RETRY_REQUEST);
 | |
|     }
 | |
|     // remove this node from the swarm of this pubkey
 | |
|     await dropSnodeFromSwarmIfNeeded(associatedWith, destinationSnodeEd25519);
 | |
|   } catch (e) {
 | |
|     if (e.message !== ERROR_421_HANDLED_RETRY_REQUEST) {
 | |
|       window?.log?.warn(
 | |
|         'Got error while parsing 421 result. Dropping this snode from the swarm of this pubkey',
 | |
|         e
 | |
|       );
 | |
|       // could not parse result. Consider that this snode as invalid
 | |
|       await dropSnodeFromSwarmIfNeeded(associatedWith, destinationSnodeEd25519);
 | |
|     }
 | |
|   }
 | |
|   await Onions.incrementBadSnodeCountOrDrop({
 | |
|     snodeEd25519: destinationSnodeEd25519,
 | |
|     associatedWith,
 | |
|   });
 | |
| 
 | |
|   // this is important we throw so another retry is made and we exit the handling of that response
 | |
|   throw new pRetry.AbortError(ERROR_421_HANDLED_RETRY_REQUEST);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handle a bad snode result.
 | |
|  * The `snodeFailureCount` for that node is incremented. If it's more than `snodeFailureThreshold`,
 | |
|  * we drop this node from the snode pool and from the associatedWith publicKey swarm if this is set.
 | |
|  *
 | |
|  * So after this call, if the snode keeps getting errors, we won't contact it again
 | |
|  *
 | |
|  * @param snodeEd25519 the snode ed25519 which cause issues (this might be a nodeNotFound)
 | |
|  * @param guardNodeEd25519 the guard node ed25519 of the current path in use. a nodeNoteFound ed25519
 | |
|  *       is not part of any path, so we fallback to this one if we need to increment the bad path count of the current path in use
 | |
|  * @param associatedWith if set, we will drop this snode from the swarm of the pubkey too
 | |
|  * @param isNodeNotFound if set, we will drop this snode right now as this is an invalid node for the network.
 | |
|  */
 | |
| async function incrementBadSnodeCountOrDrop({
 | |
|   snodeEd25519,
 | |
|   associatedWith,
 | |
| }: {
 | |
|   snodeEd25519: string;
 | |
|   associatedWith?: string;
 | |
| }) {
 | |
|   const oldFailureCount = snodeFailureCount[snodeEd25519] || 0;
 | |
|   const newFailureCount = oldFailureCount + 1;
 | |
|   snodeFailureCount[snodeEd25519] = newFailureCount;
 | |
|   if (newFailureCount >= snodeFailureThreshold) {
 | |
|     window?.log?.warn(
 | |
|       `Failure threshold reached for snode: ${ed25519Str(snodeEd25519)}; dropping it.`
 | |
|     );
 | |
| 
 | |
|     if (associatedWith) {
 | |
|       await dropSnodeFromSwarmIfNeeded(associatedWith, snodeEd25519);
 | |
|     }
 | |
|     await dropSnodeFromSnodePool(snodeEd25519);
 | |
|     snodeFailureCount[snodeEd25519] = 0;
 | |
| 
 | |
|     await OnionPaths.dropSnodeFromPath(snodeEd25519);
 | |
|   } else {
 | |
|     window?.log?.warn(
 | |
|       `Couldn't reach snode at: ${ed25519Str(
 | |
|         snodeEd25519
 | |
|       )}; setting his failure count to ${newFailureCount}`
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This call tries to send the request via onion. If we get a bad path, it handles the snode removing of the swarm and snode pool.
 | |
|  * But the caller needs to handle the retry (and rebuild the path on his side if needed)
 | |
|  */
 | |
| async function sendOnionRequestHandlingSnodeEject({
 | |
|   destSnodeX25519,
 | |
|   finalDestOptions,
 | |
|   nodePath,
 | |
|   abortSignal,
 | |
|   associatedWith,
 | |
|   finalRelayOptions,
 | |
|   useV4,
 | |
|   throwErrors,
 | |
| }: {
 | |
|   nodePath: Array<Snode>;
 | |
|   destSnodeX25519: string;
 | |
|   finalDestOptions: FinalDestOptions;
 | |
|   finalRelayOptions?: FinalRelayOptions;
 | |
|   abortSignal?: AbortSignal;
 | |
|   associatedWith?: string;
 | |
|   useV4: boolean;
 | |
|   throwErrors: boolean;
 | |
| }): Promise<SnodeResponse | SnodeResponseV4 | undefined> {
 | |
|   // this sendOnionRequestNoRetries() call has to be the only one like this.
 | |
|   // If you need to call it, call it through sendOnionRequestHandlingSnodeEject because this is the one handling path rebuilding and known errors
 | |
|   let response;
 | |
|   let decodingSymmetricKey;
 | |
|   try {
 | |
|     // this might throw
 | |
|     const result = await sendOnionRequestNoRetries({
 | |
|       nodePath,
 | |
|       destSnodeX25519,
 | |
|       finalDestOptions,
 | |
|       finalRelayOptions,
 | |
|       abortSignal,
 | |
|       useV4,
 | |
|     });
 | |
| 
 | |
|     if (window.sessionFeatureFlags?.debug.debugOnionRequests) {
 | |
|       window.log.info(
 | |
|         `sendOnionRequestHandlingSnodeEject: sendOnionRequestNoRetries: useV4:${useV4} destSnodeX25519:${destSnodeX25519}; \nfinalDestOptions:${JSON.stringify(
 | |
|           finalDestOptions
 | |
|         )}; \nfinalRelayOptions:${JSON.stringify(finalRelayOptions)}\n\n result: ${JSON.stringify(
 | |
|           result
 | |
|         )}`
 | |
|       );
 | |
|     }
 | |
|     response = result.response;
 | |
|     if (
 | |
|       !isEmpty(finalRelayOptions) &&
 | |
|       response.status === 502 &&
 | |
|       response.statusText === 'Bad Gateway'
 | |
|     ) {
 | |
|       // it's an opengroup server and it is not responding. Consider this as a ENETUNREACH
 | |
|       throw new pRetry.AbortError('ENETUNREACH');
 | |
|     }
 | |
|     decodingSymmetricKey = result.decodingSymmetricKey;
 | |
|   } catch (e) {
 | |
|     window?.log?.warn('sendOnionRequestNoRetries error message: ', e.message);
 | |
|     if (e.code === 'ENETUNREACH' || e.message === 'ENETUNREACH' || throwErrors) {
 | |
|       throw e;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const destinationSnodeEd25519 =
 | |
|     (isFinalDestinationSnode(finalDestOptions) && finalDestOptions?.destination_ed25519_hex) ||
 | |
|     undefined;
 | |
| 
 | |
|   // those calls will handle the common onion failure logic.
 | |
|   // if an error is not retryable a AbortError is triggered, which is handled by pRetry and retries are stopped
 | |
|   if (useV4) {
 | |
|     return Onions.processOnionResponseV4({
 | |
|       response,
 | |
|       symmetricKey: decodingSymmetricKey,
 | |
|       guardNode: nodePath[0],
 | |
|       destinationSnodeEd25519,
 | |
|       abortSignal,
 | |
|       associatedWith,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   return Onions.processOnionResponse({
 | |
|     response,
 | |
|     symmetricKey: decodingSymmetricKey,
 | |
|     guardNode: nodePath[0],
 | |
|     destinationSnodeEd25519,
 | |
|     abortSignal,
 | |
|     associatedWith,
 | |
|   });
 | |
| }
 | |
| 
 | |
| function throwIfInvalidV4RequestInfos(request: FinalDestOptions): EncodeV4OnionRequestInfos {
 | |
|   if (isFinalDestinationSnode(request)) {
 | |
|     // a snode request cannot be v4 currently as they do not support it
 | |
|     throw new Error('v4onion request needs endpoint pubkey and method at least');
 | |
|   }
 | |
| 
 | |
|   const { body, endpoint, headers, method } = request;
 | |
| 
 | |
|   if (!endpoint || !method) {
 | |
|     throw new Error('v4onion request needs endpoint pubkey and method at least');
 | |
|   }
 | |
| 
 | |
|   const requestInfos: EncodeV4OnionRequestInfos = {
 | |
|     endpoint,
 | |
|     headers,
 | |
|     method,
 | |
|     body,
 | |
|   };
 | |
| 
 | |
|   return requestInfos;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * For a snode request, the body already contains the method and the args with our custom formatting in json
 | |
|  */
 | |
| export type FinalDestSnodeOptions = {
 | |
|   destination_ed25519_hex: string;
 | |
|   headers?: Record<string, string>;
 | |
|   body: string | null;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * For a non snode request (so fileserver or sogs), the body can be binary (for an upload of a file or a string) but we also need a method, endpoint and headers
 | |
|  */
 | |
| export type FinalDestNonSnodeOptions = {
 | |
|   headers: Record<string, string | number>;
 | |
|   body: string | null | Uint8Array;
 | |
|   method: string;
 | |
|   endpoint: string;
 | |
| };
 | |
| 
 | |
| export type FinalDestOptions = FinalDestSnodeOptions | FinalDestNonSnodeOptions;
 | |
| 
 | |
| /**
 | |
|  * Typescript guard to be used to separate between a snode destination options and a non snode one.
 | |
|  *
 | |
|  * A non snode destination needs a `.method` to be set
 | |
|  */
 | |
| function isFinalDestinationNonSnode(
 | |
|   options: FinalDestOptions
 | |
| ): options is FinalDestNonSnodeOptions {
 | |
|   return (options as any).method !== undefined;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Typescript guard to be used to separate between a snode destination options and a non snode one.
 | |
|  *
 | |
|  * A snode destination request needs a `.destination_ed25519_hex` to be set
 | |
|  */
 | |
| function isFinalDestinationSnode(options: FinalDestOptions): options is FinalDestSnodeOptions {
 | |
|   return (options as any).destination_ed25519_hex !== undefined;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * Onion requests looks like this
 | |
|  * Sender -> 1 -> 2 -> 3 -> Receiver
 | |
|  * 1, 2, 3 = onion Snodes
 | |
|  *
 | |
|  * This function does not retry, and is not meant to be used directly.
 | |
|  *
 | |
|  *
 | |
|  * @param nodePath the onion path to use to send the request
 | |
|  * @param finalDestOptions those are the options for the request from 3 to Receiver. It contains for instance the payload and headers.
 | |
|  * @param finalRelayOptions  those are the options 3 will use to make a request to R. It contains the host and port to make the request to, if the target is not a snode
 | |
|  */
 | |
| const sendOnionRequestNoRetries = async ({
 | |
|   nodePath,
 | |
|   destSnodeX25519: destX25519hex,
 | |
|   finalDestOptions: finalDestOptionsOri,
 | |
|   finalRelayOptions,
 | |
|   abortSignal,
 | |
|   useV4,
 | |
| }: {
 | |
|   nodePath: Array<Snode>;
 | |
|   destSnodeX25519: string;
 | |
|   finalDestOptions: FinalDestOptions;
 | |
|   finalRelayOptions?: FinalRelayOptions; // use only when the target is not a snode
 | |
|   abortSignal?: AbortSignal;
 | |
|   useV4: boolean;
 | |
| }) => {
 | |
|   // Warning: be sure to do a copy otherwise the delete below creates issue with retries
 | |
|   // we want to forward the destination_ed25519_hex explicitly so remove it from the copy directly
 | |
|   const finalDestOptions = cloneDeep(omit(finalDestOptionsOri, ['destination_ed25519_hex']));
 | |
|   if (typeof destX25519hex !== 'string') {
 | |
|     window?.log?.warn('destX25519hex was not a string');
 | |
|     throw new Error('sendOnionRequestNoRetries: destX25519hex was not a string');
 | |
|   }
 | |
| 
 | |
|   finalDestOptions.headers = finalDestOptions.headers || {};
 | |
| 
 | |
|   // finalRelayOptions is set only if we try to communicate with something else than a service node as end target of the request.
 | |
|   // so if that field is set, we are trying to communicate with a file server or an opengroup or whatever,
 | |
|   // and if that field is not set, we are trying to communicate with a service node (for a retrieve/send/whatever request)
 | |
|   const isRequestToSnode = !finalRelayOptions;
 | |
| 
 | |
|   let destCtx: DestinationContext;
 | |
|   try {
 | |
|     const bodyString = isString(finalDestOptions.body) ? finalDestOptions.body : null;
 | |
|     const bodyBinary =
 | |
|       !isString(finalDestOptions.body) && finalDestOptions.body ? finalDestOptions.body : null;
 | |
|     if (isRequestToSnode) {
 | |
|       if (useV4) {
 | |
|         throw new Error('sendOnionRequestNoRetries calls cannot be v4 for now.');
 | |
|       }
 | |
|       if (!isString(finalDestOptions.body)) {
 | |
|         window.log.warn(
 | |
|           'sendOnionRequestNoRetries calls should only take body as string: ',
 | |
|           typeof finalDestOptions.body
 | |
|         );
 | |
|         throw new Error('sendOnionRequestNoRetries calls should only take body as string.');
 | |
|       }
 | |
|       // delete finalDestOptions.body;
 | |
|       // not sure if that's strictly the same thing in this context
 | |
|       finalDestOptions.body = null;
 | |
| 
 | |
|       const textEncoder = new TextEncoder();
 | |
|       const bodyEncoded = bodyString ? textEncoder.encode(bodyString) : bodyBinary;
 | |
|       if (!bodyEncoded) {
 | |
|         throw new Error('bodyEncoded is empty after encoding');
 | |
|       }
 | |
| 
 | |
|       // snode requests do not support v4 onion requests, sadly
 | |
|       destCtx = (await callUtilsWorker(
 | |
|         'encryptForPubkey',
 | |
|         destX25519hex,
 | |
|         encodeCiphertextPlusJson(bodyEncoded, finalDestOptions)
 | |
|       )) as DestinationContext;
 | |
|     } else {
 | |
|       // request to something else than a snode, fileserver or a sogs, we do support v4 for those (and actually only for those for now)
 | |
|       destCtx = useV4
 | |
|         ? await encryptOnionV4RequestForPubkey(
 | |
|             destX25519hex,
 | |
|             throwIfInvalidV4RequestInfos(
 | |
|               finalDestOptions as FinalDestSnodeOptions | FinalDestNonSnodeOptions // FIXME fix this type
 | |
|             )
 | |
|           )
 | |
|         : await encryptForPubKey(destX25519hex, finalDestOptions);
 | |
|     }
 | |
|   } catch (e) {
 | |
|     window?.log?.error(
 | |
|       'sendOnionRequestNoRetries - encryptForPubKey failure [',
 | |
|       e.code,
 | |
|       e.message,
 | |
|       '] destination X25519',
 | |
|       destX25519hex.substring(0, 32),
 | |
|       '...',
 | |
|       destX25519hex.substring(32)
 | |
|     );
 | |
|     throw e;
 | |
|   }
 | |
| 
 | |
|   // if a snode destination is set, use it
 | |
|   const targetEd25519hex =
 | |
|     (isFinalDestinationSnode(finalDestOptionsOri) && finalDestOptionsOri.destination_ed25519_hex) ||
 | |
|     undefined;
 | |
| 
 | |
|   const payload = await buildOnionGuardNodePayload(
 | |
|     nodePath,
 | |
|     destCtx,
 | |
|     useV4,
 | |
|     targetEd25519hex,
 | |
|     finalRelayOptions
 | |
|   );
 | |
| 
 | |
|   const guardNode = nodePath[0];
 | |
| 
 | |
|   const guardFetchOptions: RequestInit = {
 | |
|     method: 'POST',
 | |
|     body: payload,
 | |
|     // we are talking to a snode...
 | |
|     agent: snodeHttpsAgent,
 | |
|     headers: {
 | |
|       'User-Agent': 'WhatsApp',
 | |
|       'Accept-Language': 'en-us',
 | |
|     },
 | |
|     timeout: 25000,
 | |
|   };
 | |
| 
 | |
|   if (abortSignal) {
 | |
|     guardFetchOptions.signal = abortSignal as AbortSignalNode;
 | |
|   }
 | |
| 
 | |
|   const guardUrl = `https://${guardNode.ip}:${guardNode.port}/onion_req/v2`;
 | |
|   // no logs for that one insecureNodeFetch as we do need to call insecureNodeFetch to our guardNodes
 | |
|   // window?.log?.info('insecureNodeFetch => plaintext for sendOnionRequestNoRetries');
 | |
| 
 | |
|   const response = await insecureNodeFetch(guardUrl, guardFetchOptions);
 | |
|   return { response, decodingSymmetricKey: destCtx.symmetricKey };
 | |
| };
 | |
| 
 | |
| async function sendOnionRequestSnodeDest(
 | |
|   onionPath: Array<Snode>,
 | |
|   targetNode: Snode,
 | |
|   headers: Record<string, any>,
 | |
| 
 | |
|   plaintext: string | null,
 | |
|   associatedWith?: string
 | |
| ) {
 | |
|   return Onions.sendOnionRequestHandlingSnodeEject({
 | |
|     nodePath: onionPath,
 | |
|     destSnodeX25519: targetNode.pubkey_x25519,
 | |
|     finalDestOptions: {
 | |
|       destination_ed25519_hex: targetNode.pubkey_ed25519,
 | |
|       body: plaintext,
 | |
|       headers,
 | |
|     },
 | |
|     associatedWith,
 | |
|     useV4: false, // sadly, request to snode do not support v4 yet
 | |
|     throwErrors: false,
 | |
|   });
 | |
| }
 | |
| 
 | |
| function getPathString(pathObjArr: Array<{ ip: string; port: number }>): string {
 | |
|   return pathObjArr.map(node => `${node.ip}:${node.port}`).join(', ');
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * If the fetch throws a retryable error we retry this call with a new path at most 3 times. If another error happens, we return it. If we have a result we just return it.
 | |
|  */
 | |
| async function lokiOnionFetch({
 | |
|   targetNode,
 | |
|   associatedWith,
 | |
|   body,
 | |
|   headers,
 | |
| }: {
 | |
|   targetNode: Snode;
 | |
|   headers: Record<string, any>;
 | |
|   body: string | null;
 | |
|   associatedWith?: string;
 | |
| }): Promise<SnodeResponse | undefined> {
 | |
|   try {
 | |
|     const retriedResult = await pRetry(
 | |
|       async () => {
 | |
|         // Get a path excluding `targetNode`:
 | |
|         const path = await OnionPaths.getOnionPath({ toExclude: targetNode });
 | |
|         const result = await sendOnionRequestSnodeDest(
 | |
|           path,
 | |
|           targetNode,
 | |
|           headers,
 | |
|           body,
 | |
|           associatedWith
 | |
|         );
 | |
|         return result;
 | |
|       },
 | |
|       {
 | |
|         retries: 3,
 | |
|         factor: 1,
 | |
|         minTimeout: 100,
 | |
|         onFailedAttempt: e => {
 | |
|           window?.log?.warn(
 | |
|             `onionFetchRetryable attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
 | |
|           );
 | |
|         },
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     return retriedResult as SnodeResponse | undefined;
 | |
|   } catch (e) {
 | |
|     window?.log?.warn('onionFetchRetryable failed ', e.message);
 | |
|     if (e?.errno === 'ENETUNREACH') {
 | |
|       // better handle the no connection state
 | |
|       throw new Error(ERROR_CODE_NO_CONNECT);
 | |
|     }
 | |
|     if (e?.message === CLOCK_OUT_OF_SYNC_MESSAGE_ERROR) {
 | |
|       window?.log?.warn('Its a clock out of sync error ');
 | |
|       throw new pRetry.AbortError(CLOCK_OUT_OF_SYNC_MESSAGE_ERROR);
 | |
|     }
 | |
|     throw e;
 | |
|   }
 | |
| }
 | |
| 
 | |
| export const Onions = {
 | |
|   sendOnionRequestHandlingSnodeEject,
 | |
|   incrementBadSnodeCountOrDrop,
 | |
|   decodeOnionResult,
 | |
|   lokiOnionFetch,
 | |
|   getPathString,
 | |
|   sendOnionRequestSnodeDest,
 | |
|   processOnionResponse,
 | |
|   processOnionResponseV4,
 | |
|   isFinalDestinationSnode,
 | |
|   isFinalDestinationNonSnode,
 | |
| };
 |