pull/1624/head
Audric Ackermann 4 years ago
parent c599d0b629
commit ed53ab43e6
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

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

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

@ -13,6 +13,7 @@ import _, { toNumber } from 'lodash';
import { default as insecureNodeFetch } from 'node-fetch'; import { default as insecureNodeFetch } from 'node-fetch';
import { PROTOCOLS } from '../constants'; import { PROTOCOLS } from '../constants';
import { toHex } from '../utils/String'; import { toHex } from '../utils/String';
import pRetry from 'p-retry';
// FIXME: replace with something on urlPubkeyMap... // FIXME: replace with something on urlPubkeyMap...
const FILESERVER_HOSTS = [ const FILESERVER_HOSTS = [
@ -22,8 +23,6 @@ const FILESERVER_HOSTS = [
'file.getsession.org', 'file.getsession.org',
]; ];
const MAX_SEND_ONION_RETRIES = 3;
type OnionFetchOptions = { type OnionFetchOptions = {
method: string; method: string;
body?: string; body?: string;
@ -32,45 +31,17 @@ type OnionFetchOptions = {
type OnionFetchBasicOptions = { type OnionFetchBasicOptions = {
retry?: number; retry?: number;
requestNumber?: number;
noJson?: boolean; noJson?: boolean;
counter?: number;
}; };
const handleSendViaOnionRetry = async ( type OnionPayloadObj = {
result: RequestError, method: string;
options: OnionFetchBasicOptions, endpoint: string;
srvPubKey: string, body: any;
url: URL, headers: Record<string, any>;
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
);
}; };
const buildSendViaOnionPayload = (url: URL, fetchOptions: OnionFetchOptions) => { const buildSendViaOnionPayload = (url: URL, fetchOptions: OnionFetchOptions): OnionPayloadObj => {
let tempHeaders = fetchOptions.headers || {}; let tempHeaders = fetchOptions.headers || {};
const payloadObj = { const payloadObj = {
method: fetchOptions.method || 'GET', method: fetchOptions.method || 'GET',
@ -103,15 +74,15 @@ const buildSendViaOnionPayload = (url: URL, fetchOptions: OnionFetchOptions) =>
return payloadObj; return payloadObj;
}; };
export const getOnionPathForSending = async (requestNumber: number) => { export const getOnionPathForSending = async () => {
let pathNodes: Array<Snode> = []; let pathNodes: Array<Snode> = [];
try { try {
pathNodes = await OnionPaths.getOnionPath(); pathNodes = await OnionPaths.getOnionPath();
} catch (e) { } 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) { if (!pathNodes?.length) {
window.log.warn(`sendViaOnion #${requestNumber} - failing, no path available`); window.log.warn('sendViaOnion - failing, no path available');
// should we retry? // should we retry?
return null; return null;
} }
@ -121,11 +92,40 @@ export const getOnionPathForSending = async (requestNumber: number) => {
const initOptionsWithDefaults = (options: OnionFetchBasicOptions) => { const initOptionsWithDefaults = (options: OnionFetchBasicOptions) => {
const defaultFetchBasicOptions = { const defaultFetchBasicOptions = {
retry: 0, retry: 0,
requestNumber: OnionPaths.assignOnionRequestNumber(),
}; };
return _.defaults(options, defaultFetchBasicOptions); 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 * 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 * result is status_code and whatever the body should be
@ -151,61 +151,48 @@ export const sendViaOnion = async (
const defaultedOptions = initOptionsWithDefaults(options); const defaultedOptions = initOptionsWithDefaults(options);
const payloadObj = buildSendViaOnionPayload(url, fetchOptions); const payloadObj = buildSendViaOnionPayload(url, fetchOptions);
const pathNodes = await getOnionPathForSending(defaultedOptions.requestNumber); // if protocol is forced to 'http:' => just use http (without the ':').
if (!pathNodes) { // otherwise use https as protocol (this is the default)
return null; 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;
let result: SnodeResponse | RequestError;
try { try {
// if protocol is forced to 'http:' => just use http (without the ':'). result = await pRetry(
// otherwise use https as protocol (this is the default) async () => {
const forcedHttp = url.protocol === PROTOCOLS.HTTP; return sendViaOnionRetryable({
const finalRelayOptions: FinalRelayOptions = { castedDestinationX25519Key,
host: url.hostname, finalRelayOptions,
}; payloadObj,
abortSignal,
if (forcedHttp) { });
finalRelayOptions.protocol = 'http'; },
} {
if (forcedHttp) { retries: 5,
finalRelayOptions.port = url.port ? toNumber(url.port) : 80; factor: 1,
} minTimeout: 1000,
onFailedAttempt: e => {
result = await sendOnionRequestLsrpcDest( window.log.warn(
0, `sendViaOnionRetryable attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
pathNodes, );
castedDestinationX25519Key, },
finalRelayOptions, }
payloadObj,
defaultedOptions.requestNumber,
abortSignal
); );
} catch (e) { } 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; 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 we expect something which is not json, just return the body we got.
if (defaultedOptions.noJson) { if (defaultedOptions.noJson) {
return { return {
@ -226,11 +213,7 @@ export const sendViaOnion = async (
try { try {
body = JSON.parse(result.body); body = JSON.parse(result.body);
} catch (e) { } catch (e) {
window.log.error( window.log.error("sendViaOnion Can't decode JSON body", typeof result.body, result.body);
`sendViaOnion #${defaultedOptions.requestNumber} - Can't decode JSON body`,
typeof result.body,
result.body
);
} }
} }
// result.status has the http response code // result.status has the http response code
@ -251,9 +234,7 @@ type ServerRequestOptionsType = {
forceFreshToken?: boolean; forceFreshToken?: boolean;
retry?: number; retry?: number;
requestNumber?: number;
noJson?: boolean; noJson?: boolean;
counter?: number;
}; };
// tslint:disable-next-line: max-func-body-length // tslint:disable-next-line: max-func-body-length

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

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

@ -11,6 +11,7 @@ import ByteBuffer from 'bytebuffer';
import { OnionPaths } from '../onions'; import { OnionPaths } from '../onions';
import { fromBase64ToArrayBuffer, toHex } from '../utils/String'; import { fromBase64ToArrayBuffer, toHex } from '../utils/String';
import pRetry from 'p-retry'; import pRetry from 'p-retry';
import { incrementBadPathCountOrDrop } from '../onions/onionPath';
export enum RequestError { export enum RequestError {
BAD_PATH = 'BAD_PATH', BAD_PATH = 'BAD_PATH',
@ -102,8 +103,7 @@ async function buildOnionCtxs(
nodePath: Array<Snode>, nodePath: Array<Snode>,
destCtx: DestinationContext, destCtx: DestinationContext,
targetED25519Hex?: string, targetED25519Hex?: string,
finalRelayOptions?: FinalRelayOptions, finalRelayOptions?: FinalRelayOptions
id = ''
) { ) {
const { log } = window; const { log } = window;
@ -142,7 +142,7 @@ async function buildOnionCtxs(
pubkeyHex = nodePath[i + 1].pubkey_ed25519; pubkeyHex = nodePath[i + 1].pubkey_ed25519;
if (!pubkeyHex) { if (!pubkeyHex) {
log.error( log.error(
`loki_rpc:::buildOnionGuardNodePayload ${id} - no ed25519 for`, 'loki_rpc:::buildOnionGuardNodePayload - no ed25519 for',
nodePath[i + 1], nodePath[i + 1],
'path node', 'path node',
i + 1 i + 1
@ -160,7 +160,7 @@ async function buildOnionCtxs(
ctxes.push(ctx); ctxes.push(ctx);
} catch (e) { } catch (e) {
log.error( log.error(
`loki_rpc:::buildOnionGuardNodePayload ${id} - encryptForRelayV2 failure`, 'loki_rpc:::buildOnionGuardNodePayload - encryptForRelayV2 failure',
e.code, e.code,
e.message e.message
); );
@ -177,10 +177,9 @@ async function buildOnionGuardNodePayload(
nodePath: Array<Snode>, nodePath: Array<Snode>,
destCtx: DestinationContext, destCtx: DestinationContext,
targetED25519Hex?: string, targetED25519Hex?: string,
finalRelayOptions?: FinalRelayOptions, finalRelayOptions?: FinalRelayOptions
id = ''
) { ) {
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 // 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. // So the one we will send to the first guard node.
@ -195,53 +194,78 @@ async function buildOnionGuardNodePayload(
return encodeCiphertextPlusJson(guardCtx.ciphertext, guardPayloadObj); return encodeCiphertextPlusJson(guardCtx.ciphertext, guardPayloadObj);
} }
// Process a response as it arrives from `fetch`, handling function process406Error(statusCode: number) {
// http errors and attempting to decrypt the body with `sharedKey` if (statusCode === 406) {
// 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) {
// clock out of sync // clock out of sync
console.warn('clocko ut of sync todo'); console.warn('clock out of sync todo');
// this will make the pRetry stop // this will make the pRetry stop
throw new pRetry.AbortError('You clock is out of sync with the network. Check your clock.'); throw new pRetry.AbortError('You clock is out of sync with the network. Check your clock.');
} }
}
if (response.status === 421) { async function process421Error(
// clock out of sync 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'); 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. // this test checks for on error in your path.
if ( if (
// response.status === 502 || // response.status === 502 ||
// response.status === 503 || // response.status === 503 ||
// response.status === 504 || // response.status === 504 ||
// response.status === 404 || // 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: '; const prefix = 'Next node not found: ';
let nodeNotFound; let nodeNotFound;
@ -251,12 +275,94 @@ async function processOnionResponse(
// If we have a specific node in fault we can exclude just this node. // If we have a specific node in fault we can exclude just this node.
// Otherwise we increment the whole path failure count // 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) { if (!ciphertext) {
window.log.warn( 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'); throw new Error('Target node return empty ciphertext');
} }
@ -278,14 +384,11 @@ async function processOnionResponse(
); );
plaintext = new TextDecoder().decode(plaintextBuffer); plaintext = new TextDecoder().decode(plaintextBuffer);
} catch (e) { } catch (e) {
window.log.error(`(${reqIdx}) [path] lokiRpc::processingOnionResponse - decode error`, e); window.log.error('[path] lokiRpc::processingOnionResponse - decode error', e);
window.log.error( window.log.error('[path] lokiRpc::processingOnionResponse - symmetricKey', toHex(symmetricKey));
`(${reqIdx}) [path] lokiRpc::processingOnionResponse - symmetricKey`,
toHex(symmetricKey)
);
if (ciphertextBuffer) { if (ciphertextBuffer) {
window.log.error( window.log.error(
`(${reqIdx}) [path] lokiRpc::processingOnionResponse - ciphertextBuffer`, '[path] lokiRpc::processingOnionResponse - ciphertextBuffer',
toHex(ciphertextBuffer) toHex(ciphertextBuffer)
); );
} }
@ -302,18 +405,22 @@ async function processOnionResponse(
window.log.warn('Received an out of bounds js number'); window.log.warn('Received an out of bounds js number');
} }
return value; return value;
}) as Record<string, any>;
const status = jsonRes.status_code || jsonRes.status;
await processOnionRequestErrorAtDestination({
statusCode: status,
body: ciphertext,
destinationEd25519: lsrpcEd25519Key,
associatedWith,
}); });
if (jsonRes.status_code) {
jsonRes.status = jsonRes.status_code;
}
return jsonRes as SnodeResponse; return jsonRes as SnodeResponse;
} catch (e) { } catch (e) {
window.log.error( 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'); 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. * Handle a bad snode result.
* The `snodeFailureCount` for that node is incremented. If it's more than `snodeFailureThreshold`, * 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) { if (newFailureCount >= snodeFailureThreshold) {
window.log.warn(`Failure threshold reached for: ${snodeEd25519}; dropping it.`); window.log.warn(`Failure threshold reached for: ${snodeEd25519}; dropping it.`);
if (associatedWith) { if (associatedWith) {
console.warn(`Dropping ${snodeEd25519} from swarm of ${associatedWith}`); window.log.info(`Dropping ${snodeEd25519} from swarm of ${associatedWith}`);
await dropSnodeFromSwarmIfNeeded(associatedWith, snodeEd25519); await dropSnodeFromSwarmIfNeeded(associatedWith, snodeEd25519);
} }
console.warn(`Dropping ${snodeEd25519} from snodepool`); window.log.info(`Dropping ${snodeEd25519} from snodepool`);
dropSnodeFromSnodePool(snodeEd25519); dropSnodeFromSnodePool(snodeEd25519);
// the snode was ejected from the pool so it won't be used again. // 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 { try {
await OnionPaths.dropSnodeFromPath(snodeEd25519); await OnionPaths.dropSnodeFromPath(snodeEd25519);
} catch (e) { } 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 // if dropSnodeFromPath throws, it means there is an issue patching up the path, increment the whole path issues count
await OnionPaths.incrementBadPathCountOrDrop(snodeEd25519); 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) * But the caller needs to handle the retry (and rebuild the path on his side if needed)
*/ */
const sendOnionRequestHandlingSnodeEject = async ({ const sendOnionRequestHandlingSnodeEject = async ({
reqIdx,
destX25519Any, destX25519Any,
finalDestOptions, finalDestOptions,
nodePath, nodePath,
abortSignal, abortSignal,
associatedWith, associatedWith,
finalRelayOptions, finalRelayOptions,
lsrpcIdx,
}: { }: {
reqIdx: number;
nodePath: Array<Snode>; nodePath: Array<Snode>;
destX25519Any: string; destX25519Any: string;
finalDestOptions: { finalDestOptions: {
@ -465,27 +547,24 @@ const sendOnionRequestHandlingSnodeEject = async ({
body?: string; body?: string;
}; };
finalRelayOptions?: FinalRelayOptions; finalRelayOptions?: FinalRelayOptions;
lsrpcIdx?: number;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
associatedWith?: string; associatedWith?: string;
}): Promise<SnodeResponse> => { }): Promise<SnodeResponse> => {
const { response, decodingSymmetricKey } = await sendOnionRequest({ const { response, decodingSymmetricKey } = await sendOnionRequest({
reqIdx,
nodePath, nodePath,
destX25519Any, destX25519Any,
finalDestOptions, finalDestOptions,
finalRelayOptions, finalRelayOptions,
lsrpcIdx,
abortSignal, abortSignal,
}); });
// this call will handle the common onion failure logic. // 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 // if an error is not retryable a AbortError is triggered, which is handled by pRetry and retries are stopped
const processed = await processOnionResponse( const processed = await processOnionResponse(
reqIdx,
response, response,
decodingSymmetricKey, decodingSymmetricKey,
false, nodePath[0],
finalDestOptions?.destination_ed25519_hex,
abortSignal, abortSignal,
associatedWith 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 * @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 ({ const sendOnionRequest = async ({
reqIdx,
nodePath, nodePath,
destX25519Any, destX25519Any,
finalDestOptions, finalDestOptions,
finalRelayOptions, finalRelayOptions,
lsrpcIdx,
abortSignal, abortSignal,
}: { }: {
reqIdx: number;
nodePath: Array<Snode>; nodePath: Array<Snode>;
destX25519Any: string; destX25519Any: string;
finalDestOptions: { finalDestOptions: {
@ -522,19 +598,10 @@ const sendOnionRequest = async ({
body?: string; body?: string;
}; };
finalRelayOptions?: FinalRelayOptions; finalRelayOptions?: FinalRelayOptions;
lsrpcIdx?: number;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
}) => { }) => {
const { log } = window; const { log } = window;
let id = '';
if (lsrpcIdx !== undefined) {
id += `${lsrpcIdx}=>`;
}
if (reqIdx !== undefined) {
id += `${reqIdx}`;
}
// get destination pubkey in array buffer format // get destination pubkey in array buffer format
let destX25519hex = destX25519Any; let destX25519hex = destX25519Any;
if (typeof destX25519hex !== 'string') { if (typeof destX25519hex !== 'string') {
@ -575,7 +642,7 @@ const sendOnionRequest = async ({
} }
} catch (e) { } catch (e) {
log.error( log.error(
`loki_rpc::sendOnionRequest ${id} - encryptForPubKey failure [`, 'loki_rpc::sendOnionRequest - encryptForPubKey failure [',
e.code, e.code,
e.message, e.message,
'] destination X25519', '] destination X25519',
@ -592,8 +659,7 @@ const sendOnionRequest = async ({
nodePath, nodePath,
destCtx, destCtx,
targetEd25519hex, targetEd25519hex,
finalRelayOptions, finalRelayOptions
id
); );
const guardNode = nodePath[0]; const guardNode = nodePath[0];
@ -615,14 +681,12 @@ const sendOnionRequest = async ({
}; };
async function sendOnionRequestSnodeDest( async function sendOnionRequestSnodeDest(
reqIdx: any,
onionPath: Array<Snode>, onionPath: Array<Snode>,
targetNode: Snode, targetNode: Snode,
plaintext?: string, plaintext?: string,
associatedWith?: string associatedWith?: string
) { ) {
return sendOnionRequestHandlingSnodeEject({ return sendOnionRequestHandlingSnodeEject({
reqIdx,
nodePath: onionPath, nodePath: onionPath,
destX25519Any: targetNode.pubkey_x25519, destX25519Any: targetNode.pubkey_x25519,
finalDestOptions: { 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) * But the caller needs to handle the retry (and rebuild the path on his side if needed)
*/ */
export async function sendOnionRequestLsrpcDest( export async function sendOnionRequestLsrpcDest(
reqIdx: number,
onionPath: Array<Snode>, onionPath: Array<Snode>,
destX25519Any: string, destX25519Any: string,
finalRelayOptions: FinalRelayOptions, finalRelayOptions: FinalRelayOptions,
payloadObj: FinalDestinationOptions, payloadObj: FinalDestinationOptions,
lsrpcIdx: number,
abortSignal?: AbortSignal abortSignal?: AbortSignal
): Promise<SnodeResponse | RequestError> { ): Promise<SnodeResponse> {
return sendOnionRequestHandlingSnodeEject({ return sendOnionRequestHandlingSnodeEject({
reqIdx,
nodePath: onionPath, nodePath: onionPath,
destX25519Any, destX25519Any,
finalDestOptions: payloadObj, finalDestOptions: payloadObj,
finalRelayOptions, finalRelayOptions,
lsrpcIdx,
abortSignal, abortSignal,
}); });
} }
@ -663,16 +723,12 @@ export function getPathString(pathObjArr: Array<{ ip: string; port: number }>):
async function onionFetchRetryable( async function onionFetchRetryable(
targetNode: Snode, targetNode: Snode,
requestId: number,
body?: string, body?: string,
associatedWith?: string associatedWith?: string
): Promise<SnodeResponse> { ): Promise<SnodeResponse> {
const { log } = window;
// Get a path excluding `targetNode`: // Get a path excluding `targetNode`:
// eslint-disable no-await-in-loop
const path = await OnionPaths.getOnionPath(targetNode); 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; return result;
} }
@ -684,24 +740,27 @@ export async function lokiOnionFetch(
body?: string, body?: string,
associatedWith?: string associatedWith?: string
): Promise<SnodeResponse | undefined> { ): Promise<SnodeResponse | undefined> {
// Get a path excluding `targetNode`:
const thisIdx = OnionPaths.assignOnionRequestNumber();
try { try {
const retriedResult = await pRetry( const retriedResult = await pRetry(
async () => { async () => {
return onionFetchRetryable(targetNode, thisIdx, body, associatedWith); return onionFetchRetryable(targetNode, body, associatedWith);
}, },
{ {
retries: 5, retries: 5,
factor: 1, factor: 1,
minTimeout: 1000, minTimeout: 1000,
onFailedAttempt: e => {
window.log.warn(
`onionFetchRetryable attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
);
},
} }
); );
return retriedResult; return retriedResult;
} catch (e) { } catch (e) {
window.log.warn('onionFetchRetryable failed '); window.log.warn('onionFetchRetryable failed ', e);
console.warn('error to show to user');
return undefined; return undefined;
} }
} }

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

Loading…
Cancel
Save