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.
206 lines
5.9 KiB
TypeScript
206 lines
5.9 KiB
TypeScript
// tslint:disable: cyclomatic-complexity
|
|
|
|
import { OnionPaths } from '.';
|
|
import {
|
|
FinalRelayOptions,
|
|
sendOnionRequestHandlingSnodeEject,
|
|
SnodeResponse,
|
|
} from '../apis/snode_api/onions';
|
|
import _, { toNumber } from 'lodash';
|
|
import { PROTOCOLS } from '../constants';
|
|
import { toHex } from '../utils/String';
|
|
import pRetry from 'p-retry';
|
|
import { Snode } from '../../data/data';
|
|
|
|
type OnionFetchOptions = {
|
|
method: string;
|
|
body?: string;
|
|
headers?: Record<string, string>;
|
|
};
|
|
|
|
type OnionFetchBasicOptions = {
|
|
retry?: number;
|
|
noJson?: boolean;
|
|
};
|
|
|
|
type OnionPayloadObj = {
|
|
method: string;
|
|
endpoint: string;
|
|
body: any;
|
|
headers: Record<string, any>;
|
|
};
|
|
|
|
export type FinalDestinationOptions = {
|
|
destination_ed25519_hex?: string;
|
|
headers?: Record<string, string>;
|
|
body?: string;
|
|
};
|
|
|
|
const buildSendViaOnionPayload = (url: URL, fetchOptions: OnionFetchOptions): OnionPayloadObj => {
|
|
let tempHeaders = fetchOptions.headers || {};
|
|
const payloadObj = {
|
|
method: fetchOptions.method || 'GET',
|
|
body: fetchOptions.body || ('' as any),
|
|
// safety issue with file server, just safer to have this
|
|
// no initial /
|
|
endpoint: url.pathname.replace(/^\//, ''),
|
|
headers: {},
|
|
};
|
|
if (url.search) {
|
|
payloadObj.endpoint += url.search;
|
|
}
|
|
|
|
// from https://github.com/sindresorhus/is-stream/blob/master/index.js
|
|
if (
|
|
payloadObj.body &&
|
|
typeof payloadObj.body === 'object' &&
|
|
typeof payloadObj.body.pipe === 'function'
|
|
) {
|
|
const fData = payloadObj.body.getBuffer();
|
|
const fHeaders = payloadObj.body.getHeaders();
|
|
tempHeaders = { ...tempHeaders, ...fHeaders };
|
|
// update headers for boundary
|
|
// update body with base64 chunk
|
|
payloadObj.body = {
|
|
fileUpload: fData.toString('base64'),
|
|
};
|
|
}
|
|
payloadObj.headers = tempHeaders;
|
|
return payloadObj;
|
|
};
|
|
|
|
export const getOnionPathForSending = async () => {
|
|
let pathNodes: Array<Snode> = [];
|
|
try {
|
|
pathNodes = await OnionPaths.getOnionPath({});
|
|
} catch (e) {
|
|
window?.log?.error(`sendViaOnion - getOnionPath Error ${e.code} ${e.message}`);
|
|
}
|
|
if (!pathNodes?.length) {
|
|
window?.log?.warn('sendViaOnion - failing, no path available');
|
|
// should we retry?
|
|
return null;
|
|
}
|
|
return pathNodes;
|
|
};
|
|
|
|
const initOptionsWithDefaults = (options: OnionFetchBasicOptions) => {
|
|
const defaultFetchBasicOptions = {
|
|
retry: 0,
|
|
noJson: false,
|
|
};
|
|
return _.defaults(options, defaultFetchBasicOptions);
|
|
};
|
|
|
|
/**
|
|
*
|
|
* This function can be used to make a request via onion to a non snode server.
|
|
*
|
|
* A non Snode server is for instance the Push Notification server or an OpengroupV2 server.
|
|
*
|
|
* 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
|
|
*/
|
|
export const sendViaOnionToNonSnode = async (
|
|
destinationX25519Key: string,
|
|
url: URL,
|
|
fetchOptions: OnionFetchOptions,
|
|
options: OnionFetchBasicOptions = {},
|
|
abortSignal?: AbortSignal
|
|
): Promise<{
|
|
result: SnodeResponse;
|
|
txtResponse: string;
|
|
response: string;
|
|
} | null> => {
|
|
const castedDestinationX25519Key =
|
|
typeof destinationX25519Key !== 'string' ? toHex(destinationX25519Key) : destinationX25519Key;
|
|
// FIXME audric looks like this might happen for opengroupv1
|
|
if (!destinationX25519Key || typeof destinationX25519Key !== 'string') {
|
|
window?.log?.error('sendViaOnion - called without a server public key or not a string key');
|
|
}
|
|
|
|
const defaultedOptions = initOptionsWithDefaults(options);
|
|
|
|
const payloadObj = buildSendViaOnionPayload(url, fetchOptions);
|
|
// 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;
|
|
}
|
|
|
|
let result: SnodeResponse;
|
|
try {
|
|
result = await pRetry(
|
|
async () => {
|
|
const pathNodes = await getOnionPathForSending();
|
|
|
|
if (!pathNodes) {
|
|
throw new Error('getOnionPathForSending is emtpy');
|
|
}
|
|
|
|
/**
|
|
* This call handles ejecting a snode or a path if needed. If that happens, it throws a retryable error and the pRetry
|
|
* call above will call us again with the same params but a different path.
|
|
* If the error is not recoverable, it throws a pRetry.AbortError.
|
|
*/
|
|
return sendOnionRequestHandlingSnodeEject({
|
|
nodePath: pathNodes,
|
|
destX25519Any: castedDestinationX25519Key,
|
|
finalDestOptions: payloadObj,
|
|
finalRelayOptions,
|
|
abortSignal,
|
|
});
|
|
},
|
|
{
|
|
retries: 2, // retry 3 (2+1) times at most
|
|
minTimeout: 500,
|
|
onFailedAttempt: e => {
|
|
window?.log?.warn(
|
|
`sendViaOnionToNonSnodeRetryable attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
|
|
);
|
|
},
|
|
}
|
|
);
|
|
} catch (e) {
|
|
window?.log?.warn('sendViaOnionToNonSnodeRetryable failed ', e.message);
|
|
return null;
|
|
}
|
|
|
|
// If we expect something which is not json, just return the body we got.
|
|
if (defaultedOptions.noJson) {
|
|
return {
|
|
result,
|
|
txtResponse: result.body,
|
|
response: result.body,
|
|
};
|
|
}
|
|
|
|
// get the return variables we need
|
|
let txtResponse = '';
|
|
|
|
let { body } = result;
|
|
if (typeof body === 'string') {
|
|
// adn does uses this path
|
|
// log.info(`sendViaOnion - got text response ${url.toString()}`);
|
|
txtResponse = result.body;
|
|
try {
|
|
body = JSON.parse(result.body);
|
|
} catch (e) {
|
|
window?.log?.error("sendViaOnion Can't decode JSON body", typeof result.body, result.body);
|
|
}
|
|
}
|
|
// result.status has the http response code
|
|
if (!txtResponse) {
|
|
txtResponse = JSON.stringify(body);
|
|
}
|
|
return { result, txtResponse, response: body };
|
|
};
|