This commit is contained in:
Audric Ackermann 2021-05-18 13:51:01 +10:00
parent c599d0b629
commit ed53ab43e6
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
7 changed files with 301 additions and 260 deletions

View File

@ -155,7 +155,7 @@ const triggerSyncIfNeeded = async () => {
const doAppStartUp = (dispatch: Dispatch<any>) => {
if (window.lokiFeatureFlags.useOnionRequests || window.lokiFeatureFlags.useFileOnionRequests) {
// Initialize paths for onion requests
void OnionPaths.buildNewOnionPaths();
void OnionPaths.buildNewOnionPathsOneAtATime();
}
// init the messageQueue. In the constructor, we add all not send messages

View File

@ -24,9 +24,8 @@ const pathFailureThreshold = 3;
// so using GuardNode would not be correct (there is
// some naming issue here it seems)
let guardNodes: Array<SnodePool.Snode> = [];
let onionRequestCounter = 0; // Request index for debugging
export async function buildNewOnionPaths() {
export async function buildNewOnionPathsOneAtATime() {
// this function may be called concurrently make sure we only have one inflight
return allowOnlyOneAtATime('buildNewOnionPaths', async () => {
await buildNewOnionPathsWorker();
@ -53,9 +52,7 @@ export async function dropSnodeFromPath(snodeEd25519: string) {
if (pathWithSnodeIndex === -1) {
return;
}
window.log.info(
`dropping snode (...${snodeEd25519.substr(58)}) from path index: ${pathWithSnodeIndex}`
);
window.log.info(`dropping snode ${snodeEd25519} from path index: ${pathWithSnodeIndex}`);
// make a copy now so we don't alter the real one while doing stuff here
const oldPaths = _.cloneDeep(onionPaths);
@ -84,11 +81,11 @@ export async function getOnionPath(toExclude?: SnodePool.Snode): Promise<Array<S
let attemptNumber = 0;
while (onionPaths.length < minimumGuardCount) {
log.error(
`Must have at least 2 good onion paths, actual: ${onionPaths.length}, attempt #${attemptNumber} fetching more...`
`Must have at least ${minimumGuardCount} good onion paths, actual: ${onionPaths.length}, attempt #${attemptNumber} fetching more...`
);
// eslint-disable-next-line no-await-in-loop
await buildNewOnionPaths();
// should we add a delay? buildNewOnionPaths should act as one
await buildNewOnionPathsOneAtATime();
// should we add a delay? buildNewOnionPathsOneAtATime should act as one
// reload goodPaths now
attemptNumber += 1;
@ -130,9 +127,13 @@ export async function incrementBadPathCountOrDrop(guardNodeEd25519: string) {
window.log.info('handling bad path for path index', pathIndex);
const oldPathFailureCount = pathFailureCount[guardNodeEd25519] || 0;
// tslint:disable: prefer-for-of
const newPathFailureCount = oldPathFailureCount + 1;
// tslint:disable-next-line: prefer-for-of
for (let index = 0; index < pathWithIssues.length; index++) {
// skip the first one as the first one is the guard node.
// a guard node is dropped when the path is dropped completely (in dropPathStartingWithGuardNode)
for (let index = 1; index < pathWithIssues.length; index++) {
const snode = pathWithIssues[index];
await incrementBadSnodeCountOrDrop(snode.pubkey_ed25519);
}
@ -149,30 +150,28 @@ export async function incrementBadPathCountOrDrop(guardNodeEd25519: string) {
* It writes to the db the updated list of guardNodes.
* @param ed25519Key the guard node ed25519 pubkey
*/
async function dropPathStartingWithGuardNode(ed25519Key: string) {
async function dropPathStartingWithGuardNode(guardNodeEd25519: string) {
// we are dropping it. Reset the counter in case this same guard gets choosen later
const failingPathIndex = onionPaths.findIndex(p => p[0].pubkey_ed25519 === ed25519Key);
const failingPathIndex = onionPaths.findIndex(p => p[0].pubkey_ed25519 === guardNodeEd25519);
if (failingPathIndex === -1) {
console.warn('No such path starts with this guard node ');
debugger;
window.log.warn('No such path starts with this guard node ');
return;
}
onionPaths = onionPaths.filter(p => p[0].pubkey_ed25519 !== ed25519Key);
onionPaths = onionPaths.filter(p => p[0].pubkey_ed25519 !== guardNodeEd25519);
const edKeys = guardNodes.filter(g => g.pubkey_ed25519 !== ed25519Key).map(n => n.pubkey_ed25519);
const edKeys = guardNodes
.filter(g => g.pubkey_ed25519 !== guardNodeEd25519)
.map(n => n.pubkey_ed25519);
guardNodes = guardNodes.filter(g => g.pubkey_ed25519 !== ed25519Key);
pathFailureCount[ed25519Key] = 0;
guardNodes = guardNodes.filter(g => g.pubkey_ed25519 !== guardNodeEd25519);
pathFailureCount[guardNodeEd25519] = 0;
// write the updates guard nodes to the db.
// the next call to getOnionPath will trigger a rebuild of the path
await updateGuardNodes(edKeys);
}
export function assignOnionRequestNumber() {
onionRequestCounter += 1;
return onionRequestCounter;
}
async function testGuardNode(snode: SnodePool.Snode) {
const { log } = window;
@ -317,7 +316,8 @@ async function buildNewOnionPathsWorker() {
'LokiSnodeAPI::buildNewOnionPaths - Too few nodes to build an onion path! Refreshing pool and retrying'
);
await SnodePool.refreshRandomPool();
await buildNewOnionPaths();
debugger;
await buildNewOnionPathsOneAtATime();
return;
}
@ -345,6 +345,8 @@ async function buildNewOnionPathsWorker() {
}
onionPaths.push(path);
}
console.warn('guards', guards);
console.warn('onionPaths', onionPaths);
log.info(`Built ${onionPaths.length} onion paths`);
}

View File

@ -13,6 +13,7 @@ import _, { toNumber } from 'lodash';
import { default as insecureNodeFetch } from 'node-fetch';
import { PROTOCOLS } from '../constants';
import { toHex } from '../utils/String';
import pRetry from 'p-retry';
// FIXME: replace with something on urlPubkeyMap...
const FILESERVER_HOSTS = [
@ -22,8 +23,6 @@ const FILESERVER_HOSTS = [
'file.getsession.org',
];
const MAX_SEND_ONION_RETRIES = 3;
type OnionFetchOptions = {
method: string;
body?: string;
@ -32,45 +31,17 @@ type OnionFetchOptions = {
type OnionFetchBasicOptions = {
retry?: number;
requestNumber?: number;
noJson?: boolean;
counter?: number;
};
const handleSendViaOnionRetry = async (
result: RequestError,
options: OnionFetchBasicOptions,
srvPubKey: string,
url: URL,
fetchOptions: OnionFetchOptions,
abortSignal?: AbortSignal
) => {
window.log.error('sendOnionRequestLsrpcDest() returned a RequestError: ', result);
if (options.retry && options.retry >= MAX_SEND_ONION_RETRIES) {
window.log.error(`sendViaOnion too many retries: ${options.retry}. Stopping retries.`);
return null;
} else {
// handle error/retries, this is a RequestError
window.log.error(
`sendViaOnion #${options.requestNumber} - Retry #${options.retry} Couldnt handle onion request, retrying`
);
}
// retry the same request, and increment the counter
return sendViaOnion(
srvPubKey,
url,
fetchOptions,
{
...options,
retry: (options.retry as number) + 1,
counter: options.requestNumber,
},
abortSignal
);
type OnionPayloadObj = {
method: string;
endpoint: string;
body: any;
headers: Record<string, any>;
};
const buildSendViaOnionPayload = (url: URL, fetchOptions: OnionFetchOptions) => {
const buildSendViaOnionPayload = (url: URL, fetchOptions: OnionFetchOptions): OnionPayloadObj => {
let tempHeaders = fetchOptions.headers || {};
const payloadObj = {
method: fetchOptions.method || 'GET',
@ -103,15 +74,15 @@ const buildSendViaOnionPayload = (url: URL, fetchOptions: OnionFetchOptions) =>
return payloadObj;
};
export const getOnionPathForSending = async (requestNumber: number) => {
export const getOnionPathForSending = async () => {
let pathNodes: Array<Snode> = [];
try {
pathNodes = await OnionPaths.getOnionPath();
} catch (e) {
window.log.error(`sendViaOnion #${requestNumber} - getOnionPath Error ${e.code} ${e.message}`);
window.log.error(`sendViaOnion - getOnionPath Error ${e.code} ${e.message}`);
}
if (!pathNodes?.length) {
window.log.warn(`sendViaOnion #${requestNumber} - failing, no path available`);
window.log.warn('sendViaOnion - failing, no path available');
// should we retry?
return null;
}
@ -121,11 +92,40 @@ export const getOnionPathForSending = async (requestNumber: number) => {
const initOptionsWithDefaults = (options: OnionFetchBasicOptions) => {
const defaultFetchBasicOptions = {
retry: 0,
requestNumber: OnionPaths.assignOnionRequestNumber(),
};
return _.defaults(options, defaultFetchBasicOptions);
};
const sendViaOnionRetryable = async ({
castedDestinationX25519Key,
finalRelayOptions,
payloadObj,
abortSignal,
}: {
castedDestinationX25519Key: string;
finalRelayOptions: FinalRelayOptions;
payloadObj: OnionPayloadObj;
abortSignal?: AbortSignal;
}) => {
const pathNodes = await getOnionPathForSending();
if (!pathNodes) {
throw new Error('getOnionPathForSending is emtpy');
}
// this call throws a normal error (which will trigger a retry) if a retryable is got (bad path or whatever)
// it throws an AbortError in case the retry should not be retried again (aborted, or )
const result = await sendOnionRequestLsrpcDest(
pathNodes,
castedDestinationX25519Key,
finalRelayOptions,
payloadObj,
abortSignal
);
return result;
};
/**
* FIXME the type for this is not correct for open group api v2 returned values
* result is status_code and whatever the body should be
@ -151,61 +151,48 @@ export const sendViaOnion = async (
const defaultedOptions = initOptionsWithDefaults(options);
const payloadObj = buildSendViaOnionPayload(url, fetchOptions);
const pathNodes = await getOnionPathForSending(defaultedOptions.requestNumber);
if (!pathNodes) {
return null;
// if protocol is forced to 'http:' => just use http (without the ':').
// otherwise use https as protocol (this is the default)
const forcedHttp = url.protocol === PROTOCOLS.HTTP;
const finalRelayOptions: FinalRelayOptions = {
host: url.hostname,
};
if (forcedHttp) {
finalRelayOptions.protocol = 'http';
}
if (forcedHttp) {
finalRelayOptions.port = url.port ? toNumber(url.port) : 80;
}
// do the request
let result: SnodeResponse | RequestError;
let result: SnodeResponse;
try {
// if protocol is forced to 'http:' => just use http (without the ':').
// otherwise use https as protocol (this is the default)
const forcedHttp = url.protocol === PROTOCOLS.HTTP;
const finalRelayOptions: FinalRelayOptions = {
host: url.hostname,
};
if (forcedHttp) {
finalRelayOptions.protocol = 'http';
}
if (forcedHttp) {
finalRelayOptions.port = url.port ? toNumber(url.port) : 80;
}
result = await sendOnionRequestLsrpcDest(
0,
pathNodes,
castedDestinationX25519Key,
finalRelayOptions,
payloadObj,
defaultedOptions.requestNumber,
abortSignal
result = await pRetry(
async () => {
return sendViaOnionRetryable({
castedDestinationX25519Key,
finalRelayOptions,
payloadObj,
abortSignal,
});
},
{
retries: 5,
factor: 1,
minTimeout: 1000,
onFailedAttempt: e => {
window.log.warn(
`sendViaOnionRetryable attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
);
},
}
);
} catch (e) {
window.log.error('sendViaOnion - lokiRpcUtils error', e.code, e.message);
window.log.warn('sendViaOnionRetryable failed ', e);
console.warn('error to show to user', e);
return null;
}
// RequestError return type is seen as number (as it is an enum)
if (typeof result === 'string') {
console.warn('above string type is not correct');
if (result === RequestError.ABORTED) {
window.log.info('sendViaOnion aborted. not retrying');
return null;
}
const retriedResult = await handleSendViaOnionRetry(
result,
defaultedOptions,
castedDestinationX25519Key,
url,
fetchOptions,
abortSignal
);
// keep the await separate so we can log it easily
return retriedResult;
}
// If we expect something which is not json, just return the body we got.
if (defaultedOptions.noJson) {
return {
@ -226,11 +213,7 @@ export const sendViaOnion = async (
try {
body = JSON.parse(result.body);
} catch (e) {
window.log.error(
`sendViaOnion #${defaultedOptions.requestNumber} - Can't decode JSON body`,
typeof result.body,
result.body
);
window.log.error("sendViaOnion Can't decode JSON body", typeof result.body, result.body);
}
}
// result.status has the http response code
@ -251,9 +234,7 @@ type ServerRequestOptionsType = {
forceFreshToken?: boolean;
retry?: number;
requestNumber?: number;
noJson?: boolean;
counter?: number;
};
// tslint:disable-next-line: max-func-body-length

View File

@ -50,8 +50,8 @@ export async function sendMessage(
// send parameters
const params = {
pubKey,
ttl: ttl.toString(),
timestamp: messageTimeStamp.toString(),
ttl: `${ttl}`,
timestamp: `${messageTimeStamp}`,
data: data64,
};

View File

@ -1,7 +1,6 @@
// we don't throw or catch here
import { default as insecureNodeFetch } from 'node-fetch';
import https from 'https';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
@ -11,26 +10,13 @@ import Electron from 'electron';
const { remote } = Electron;
import { snodeRpc } from './lokiRpc';
import { sendOnionRequestLsrpcDest, SnodeResponse } from './onions';
export { sendOnionRequestLsrpcDest };
import {
getRandomSnode,
getRandomSnodePool,
getSwarmFor,
requiredSnodesForAgreement,
Snode,
updateSwarmFor,
} from './snodePool';
import { getRandomSnode, getRandomSnodePool, requiredSnodesForAgreement, Snode } from './snodePool';
import { Constants } from '..';
import { sleepFor } from '../utils/Promise';
import { sha256 } from '../crypto';
import pRetry from 'p-retry';
import _ from 'lodash';
const maxAcceptableFailuresStoreOnNode = 10;
const getSslAgentForSeedNode = (seedNodeHost: string, isSsl = false) => {
let filePrefix = '';
let pubkey256 = '';
@ -290,6 +276,11 @@ export async function getSnodePoolFromSnodes() {
retries: 3,
factor: 1,
minTimeout: 1000,
onFailedAttempt: e => {
window.log.warn(
`getSnodePoolFromSnode attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
);
},
}
);
})

View File

@ -11,6 +11,7 @@ import ByteBuffer from 'bytebuffer';
import { OnionPaths } from '../onions';
import { fromBase64ToArrayBuffer, toHex } from '../utils/String';
import pRetry from 'p-retry';
import { incrementBadPathCountOrDrop } from '../onions/onionPath';
export enum RequestError {
BAD_PATH = 'BAD_PATH',
@ -102,8 +103,7 @@ async function buildOnionCtxs(
nodePath: Array<Snode>,
destCtx: DestinationContext,
targetED25519Hex?: string,
finalRelayOptions?: FinalRelayOptions,
id = ''
finalRelayOptions?: FinalRelayOptions
) {
const { log } = window;
@ -142,7 +142,7 @@ async function buildOnionCtxs(
pubkeyHex = nodePath[i + 1].pubkey_ed25519;
if (!pubkeyHex) {
log.error(
`loki_rpc:::buildOnionGuardNodePayload ${id} - no ed25519 for`,
'loki_rpc:::buildOnionGuardNodePayload - no ed25519 for',
nodePath[i + 1],
'path node',
i + 1
@ -160,7 +160,7 @@ async function buildOnionCtxs(
ctxes.push(ctx);
} catch (e) {
log.error(
`loki_rpc:::buildOnionGuardNodePayload ${id} - encryptForRelayV2 failure`,
'loki_rpc:::buildOnionGuardNodePayload - encryptForRelayV2 failure',
e.code,
e.message
);
@ -177,10 +177,9 @@ async function buildOnionGuardNodePayload(
nodePath: Array<Snode>,
destCtx: DestinationContext,
targetED25519Hex?: string,
finalRelayOptions?: FinalRelayOptions,
id = ''
finalRelayOptions?: FinalRelayOptions
) {
const ctxes = await buildOnionCtxs(nodePath, destCtx, targetED25519Hex, finalRelayOptions, id);
const ctxes = await buildOnionCtxs(nodePath, destCtx, 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.
@ -195,53 +194,78 @@ async function buildOnionGuardNodePayload(
return encodeCiphertextPlusJson(guardCtx.ciphertext, guardPayloadObj);
}
// Process a response as it arrives from `fetch`, handling
// http errors and attempting to decrypt the body with `sharedKey`
// May return false BAD_PATH, indicating that we should try a new path.
// tslint:disable-next-line: cyclomatic-complexity
async function processOnionResponse(
reqIdx: number,
response: Response,
symmetricKey: ArrayBuffer,
debug: boolean,
abortSignal?: AbortSignal,
associatedWith?: string
): Promise<SnodeResponse> {
let ciphertext = '';
try {
ciphertext = await response.text();
} catch (e) {
window.log.warn(e);
}
if (abortSignal?.aborted) {
window.log.warn(`(${reqIdx}) [path] Call aborted`);
// this will make the pRetry stop
throw new pRetry.AbortError('Request got aborted');
}
if (response.status === 406) {
function process406Error(statusCode: number) {
if (statusCode === 406) {
// clock out of sync
console.warn('clocko ut of sync todo');
console.warn('clock out of sync todo');
// this will make the pRetry stop
throw new pRetry.AbortError('You clock is out of sync with the network. Check your clock.');
}
}
if (response.status === 421) {
// clock out of sync
async function process421Error(
statusCode: number,
body: string,
associatedWith?: string,
lsrpcEd25519Key?: string
) {
if (statusCode === 421) {
if (!lsrpcEd25519Key || !associatedWith) {
throw new Error('status 421 without a final destination or no associatedWith makes no sense');
}
window.log.info('Invalidating swarm');
await handle421InvalidSwarm(lsrpcEd25519Key, 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
*/
async function processOnionRequestErrorAtDestination({
statusCode,
body,
destinationEd25519,
associatedWith,
}: {
statusCode: number;
body: string;
destinationEd25519?: string;
associatedWith?: string;
}) {
if (statusCode === 200) {
window.log.info('processOnionRequestErrorAtDestination. statusCode ok:', statusCode);
return;
}
process406Error(statusCode);
await process421Error(statusCode, body, associatedWith, destinationEd25519);
if (destinationEd25519) {
await processAnyOtherErrorAtDestination(statusCode, destinationEd25519, associatedWith);
} else {
console.warn(
'processOnionRequestErrorAtDestination: destinationEd25519 unset. was it a group call?',
statusCode
);
}
}
async function processAnyOtherErrorOnPath(
status: number,
guardNodeEd25519: string,
ciphertext?: string,
associatedWith?: string
) {
// this test checks for on error in your path.
if (
// response.status === 502 ||
// response.status === 503 ||
// response.status === 504 ||
// response.status === 404 ||
response.status !== 200 // this is pretty strong. a 400 (Oxen server error) will be handled as a bad path.
status !== 200 // this is pretty strong. a 400 (Oxen server error) will be handled as a bad path.
) {
window.log.warn(`(${reqIdx}) [path] Got status: ${response.status}`);
window.log.warn(`[path] Got status: ${status}`);
//
const prefix = 'Next node not found: ';
let nodeNotFound;
@ -251,12 +275,94 @@ async function processOnionResponse(
// If we have a specific node in fault we can exclude just this node.
// Otherwise we increment the whole path failure count
await handleOnionRequestErrors(response.status, nodeNotFound, body || '', associatedWith);
if (nodeNotFound) {
await incrementBadSnodeCountOrDrop(nodeNotFound, associatedWith);
} else {
await incrementBadPathCountOrDrop(guardNodeEd25519);
}
throw new Error(`Bad Path handled. Retry this request. Status: ${status}`);
}
}
async function processAnyOtherErrorAtDestination(
status: number,
destinationEd25519: string,
associatedWith?: string
) {
// this test checks for on error in your path.
if (
// response.status === 502 ||
// response.status === 503 ||
// response.status === 504 ||
// response.status === 404 ||
status !== 200 // this is pretty strong. a 400 (Oxen server error) will be handled as a bad path.
) {
window.log.warn(`[path] Got status at destination: ${status}`);
await incrementBadSnodeCountOrDrop(destinationEd25519, associatedWith);
throw new Error(`Bad Path handled. Retry this request. Status: ${status}`);
}
}
async function processOnionRequestErrorOnPath(
response: Response,
ciphertext: string,
guardNodeEd25519: string,
lsrpcEd25519Key?: string,
associatedWith?: string
) {
if (response.status !== 200) {
console.warn('errorONpath:', ciphertext);
}
process406Error(response.status);
await process421Error(response.status, ciphertext, associatedWith, lsrpcEd25519Key);
await processAnyOtherErrorOnPath(response.status, guardNodeEd25519, ciphertext, 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;
// Process a response as it arrives from `fetch`, handling
// http errors and attempting to decrypt the body with `sharedKey`
// May return false BAD_PATH, indicating that we should try a new path.
// tslint:disable-next-line: cyclomatic-complexity
async function processOnionResponse(
response: Response,
symmetricKey: ArrayBuffer,
guardNode: Snode,
lsrpcEd25519Key?: 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,
ciphertext,
guardNode.pubkey_ed25519,
lsrpcEd25519Key,
associatedWith
);
if (!ciphertext) {
window.log.warn(
`(${reqIdx}) [path] lokiRpc::processingOnionResponse - Target node return empty ciphertext`
'[path] lokiRpc::processingOnionResponse - Target node return empty ciphertext'
);
throw new Error('Target node return empty ciphertext');
}
@ -278,14 +384,11 @@ async function processOnionResponse(
);
plaintext = new TextDecoder().decode(plaintextBuffer);
} catch (e) {
window.log.error(`(${reqIdx}) [path] lokiRpc::processingOnionResponse - decode error`, e);
window.log.error(
`(${reqIdx}) [path] lokiRpc::processingOnionResponse - symmetricKey`,
toHex(symmetricKey)
);
window.log.error('[path] lokiRpc::processingOnionResponse - decode error', e);
window.log.error('[path] lokiRpc::processingOnionResponse - symmetricKey', toHex(symmetricKey));
if (ciphertextBuffer) {
window.log.error(
`(${reqIdx}) [path] lokiRpc::processingOnionResponse - ciphertextBuffer`,
'[path] lokiRpc::processingOnionResponse - ciphertextBuffer',
toHex(ciphertextBuffer)
);
}
@ -302,18 +405,22 @@ async function processOnionResponse(
window.log.warn('Received an out of bounds js number');
}
return value;
});
}) as Record<string, any>;
if (jsonRes.status_code) {
jsonRes.status = jsonRes.status_code;
}
const status = jsonRes.status_code || jsonRes.status;
await processOnionRequestErrorAtDestination({
statusCode: status,
body: ciphertext,
destinationEd25519: lsrpcEd25519Key,
associatedWith,
});
return jsonRes as SnodeResponse;
} catch (e) {
window.log.error(
`(${reqIdx}) [path] lokiRpc::processingOnionResponse - parse error outer json ${e.code} ${e.message} json: '${plaintext}'`
`[path] lokiRpc::processingOnionResponse - parse error outer json ${e.code} ${e.message} json: '${plaintext}'`
);
throw new Error('Parsing error on outer json');
throw e;
}
}
@ -376,31 +483,6 @@ async function handle421InvalidSwarm(snodeEd25519: string, body: string, associa
window.log.warn('Got a 421 without an associatedWith publickey');
}
/**
* 406 => clock out of sync
* 421 => swarm changed for this associatedWith publicKey
* 500, 502, 503, AND default => bad snode.
*/
async function handleOnionRequestErrors(
statusCode: number,
snodeEd25519: string,
body: string,
associatedWith?: string
) {
switch (statusCode) {
case 406:
// FIXME audric
console.warn('Clockoutofsync TODO');
window.log.warn('The users clock is out of sync with the service node network.');
throw new Error('ClockOutOfSync TODO');
// return ClockOutOfSync;
case 421:
return handle421InvalidSwarm(snodeEd25519, body, associatedWith);
default:
return incrementBadSnodeCountOrDrop(snodeEd25519, associatedWith);
}
}
/**
* Handle a bad snode result.
* The `snodeFailureCount` for that node is incremented. If it's more than `snodeFailureThreshold`,
@ -422,10 +504,10 @@ export async function incrementBadSnodeCountOrDrop(snodeEd25519: string, associa
if (newFailureCount >= snodeFailureThreshold) {
window.log.warn(`Failure threshold reached for: ${snodeEd25519}; dropping it.`);
if (associatedWith) {
console.warn(`Dropping ${snodeEd25519} from swarm of ${associatedWith}`);
window.log.info(`Dropping ${snodeEd25519} from swarm of ${associatedWith}`);
await dropSnodeFromSwarmIfNeeded(associatedWith, snodeEd25519);
}
console.warn(`Dropping ${snodeEd25519} from snodepool`);
window.log.info(`Dropping ${snodeEd25519} from snodepool`);
dropSnodeFromSnodePool(snodeEd25519);
// the snode was ejected from the pool so it won't be used again.
@ -435,7 +517,10 @@ export async function incrementBadSnodeCountOrDrop(snodeEd25519: string, associa
try {
await OnionPaths.dropSnodeFromPath(snodeEd25519);
} catch (e) {
console.warn('dropSnodeFromPath, patchingup', e);
window.log.warn(
'dropSnodeFromPath, got error while patchingup... incrementing the whole path as bad',
e
);
// if dropSnodeFromPath throws, it means there is an issue patching up the path, increment the whole path issues count
await OnionPaths.incrementBadPathCountOrDrop(snodeEd25519);
}
@ -447,16 +532,13 @@ export async function incrementBadSnodeCountOrDrop(snodeEd25519: string, associa
* But the caller needs to handle the retry (and rebuild the path on his side if needed)
*/
const sendOnionRequestHandlingSnodeEject = async ({
reqIdx,
destX25519Any,
finalDestOptions,
nodePath,
abortSignal,
associatedWith,
finalRelayOptions,
lsrpcIdx,
}: {
reqIdx: number;
nodePath: Array<Snode>;
destX25519Any: string;
finalDestOptions: {
@ -465,27 +547,24 @@ const sendOnionRequestHandlingSnodeEject = async ({
body?: string;
};
finalRelayOptions?: FinalRelayOptions;
lsrpcIdx?: number;
abortSignal?: AbortSignal;
associatedWith?: string;
}): Promise<SnodeResponse> => {
const { response, decodingSymmetricKey } = await sendOnionRequest({
reqIdx,
nodePath,
destX25519Any,
finalDestOptions,
finalRelayOptions,
lsrpcIdx,
abortSignal,
});
// this call 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
const processed = await processOnionResponse(
reqIdx,
response,
decodingSymmetricKey,
false,
nodePath[0],
finalDestOptions?.destination_ed25519_hex,
abortSignal,
associatedWith
);
@ -505,15 +584,12 @@ const sendOnionRequestHandlingSnodeEject = async ({
* @param finalRelayOptions those are the options 3 will use to make a request to R. It contains for instance the host to make the request to
*/
const sendOnionRequest = async ({
reqIdx,
nodePath,
destX25519Any,
finalDestOptions,
finalRelayOptions,
lsrpcIdx,
abortSignal,
}: {
reqIdx: number;
nodePath: Array<Snode>;
destX25519Any: string;
finalDestOptions: {
@ -522,19 +598,10 @@ const sendOnionRequest = async ({
body?: string;
};
finalRelayOptions?: FinalRelayOptions;
lsrpcIdx?: number;
abortSignal?: AbortSignal;
}) => {
const { log } = window;
let id = '';
if (lsrpcIdx !== undefined) {
id += `${lsrpcIdx}=>`;
}
if (reqIdx !== undefined) {
id += `${reqIdx}`;
}
// get destination pubkey in array buffer format
let destX25519hex = destX25519Any;
if (typeof destX25519hex !== 'string') {
@ -575,7 +642,7 @@ const sendOnionRequest = async ({
}
} catch (e) {
log.error(
`loki_rpc::sendOnionRequest ${id} - encryptForPubKey failure [`,
'loki_rpc::sendOnionRequest - encryptForPubKey failure [',
e.code,
e.message,
'] destination X25519',
@ -592,8 +659,7 @@ const sendOnionRequest = async ({
nodePath,
destCtx,
targetEd25519hex,
finalRelayOptions,
id
finalRelayOptions
);
const guardNode = nodePath[0];
@ -615,14 +681,12 @@ const sendOnionRequest = async ({
};
async function sendOnionRequestSnodeDest(
reqIdx: any,
onionPath: Array<Snode>,
targetNode: Snode,
plaintext?: string,
associatedWith?: string
) {
return sendOnionRequestHandlingSnodeEject({
reqIdx,
nodePath: onionPath,
destX25519Any: targetNode.pubkey_x25519,
finalDestOptions: {
@ -638,21 +702,17 @@ async function sendOnionRequestSnodeDest(
* But the caller needs to handle the retry (and rebuild the path on his side if needed)
*/
export async function sendOnionRequestLsrpcDest(
reqIdx: number,
onionPath: Array<Snode>,
destX25519Any: string,
finalRelayOptions: FinalRelayOptions,
payloadObj: FinalDestinationOptions,
lsrpcIdx: number,
abortSignal?: AbortSignal
): Promise<SnodeResponse | RequestError> {
): Promise<SnodeResponse> {
return sendOnionRequestHandlingSnodeEject({
reqIdx,
nodePath: onionPath,
destX25519Any,
finalDestOptions: payloadObj,
finalRelayOptions,
lsrpcIdx,
abortSignal,
});
}
@ -663,16 +723,12 @@ export function getPathString(pathObjArr: Array<{ ip: string; port: number }>):
async function onionFetchRetryable(
targetNode: Snode,
requestId: number,
body?: string,
associatedWith?: string
): Promise<SnodeResponse> {
const { log } = window;
// Get a path excluding `targetNode`:
// eslint-disable no-await-in-loop
const path = await OnionPaths.getOnionPath(targetNode);
const result = await sendOnionRequestSnodeDest(requestId, path, targetNode, body, associatedWith);
const result = await sendOnionRequestSnodeDest(path, targetNode, body, associatedWith);
return result;
}
@ -684,24 +740,27 @@ export async function lokiOnionFetch(
body?: string,
associatedWith?: string
): Promise<SnodeResponse | undefined> {
// Get a path excluding `targetNode`:
const thisIdx = OnionPaths.assignOnionRequestNumber();
try {
const retriedResult = await pRetry(
async () => {
return onionFetchRetryable(targetNode, thisIdx, body, associatedWith);
return onionFetchRetryable(targetNode, body, associatedWith);
},
{
retries: 5,
factor: 1,
minTimeout: 1000,
onFailedAttempt: e => {
window.log.warn(
`onionFetchRetryable attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
);
},
}
);
return retriedResult;
} catch (e) {
window.log.warn('onionFetchRetryable failed ');
window.log.warn('onionFetchRetryable failed ', e);
console.warn('error to show to user');
return undefined;
}
}

View File

@ -112,11 +112,14 @@ async function tryGetSnodeListFromLokidSeednode(seedNodes: Array<SeedNode>): Pro
* @param snodeEd25519 the snode ed25519 to drop from the snode pool
*/
export function dropSnodeFromSnodePool(snodeEd25519: string) {
_.remove(randomSnodePool, x => x.pubkey_ed25519 === snodeEd25519);
const exists = _.some(randomSnodePool, x => x.pubkey_ed25519 === snodeEd25519);
if (exists) {
_.remove(randomSnodePool, x => x.pubkey_ed25519 === snodeEd25519);
window.log.warn(
`Marking ${snodeEd25519} as unreachable, ${randomSnodePool.length} snodes remaining in randomPool`
);
window.log.warn(
`Marking ${snodeEd25519} as unreachable, ${randomSnodePool.length} snodes remaining in randomPool`
);
}
}
/**
@ -284,6 +287,11 @@ export async function refreshRandomPool(): Promise<void> {
retries: 2,
factor: 1,
minTimeout: 1000,
onFailedAttempt: e => {
window.log.warn(
`getSnodePoolFromSnodes attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
);
},
}
);
} catch (e) {