|
|
|
@ -78,20 +78,30 @@ const BAD_PATH = 'bad_path';
|
|
|
|
|
|
|
|
|
|
// May return false BAD_PATH, indicating that we should try a new
|
|
|
|
|
const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => {
|
|
|
|
|
log.debug('Sending an onion request');
|
|
|
|
|
|
|
|
|
|
const ctx1 = await encryptForDestination(targetNode, plaintext);
|
|
|
|
|
const ctx2 = await encryptForRelay(nodePath[2], targetNode, ctx1);
|
|
|
|
|
const ctx3 = await encryptForRelay(nodePath[1], nodePath[2], ctx2);
|
|
|
|
|
const ctx4 = await encryptForRelay(nodePath[0], nodePath[1], ctx3);
|
|
|
|
|
const ctxes = [await encryptForDestination(targetNode, plaintext)];
|
|
|
|
|
// from (3) 2 to 0
|
|
|
|
|
const firstPos = nodePath.length - 1;
|
|
|
|
|
|
|
|
|
|
for (let i = firstPos; i > -1; i -= 1) {
|
|
|
|
|
// this nodePath points to the previous (i + 1) context
|
|
|
|
|
ctxes.push(
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
await encryptForRelay(
|
|
|
|
|
nodePath[i],
|
|
|
|
|
i === firstPos ? targetNode : nodePath[i + 1],
|
|
|
|
|
ctxes[ctxes.length - 1]
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const guardCtx = ctxes[ctxes.length - 1]; // last ctx
|
|
|
|
|
|
|
|
|
|
const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(ctx4.ciphertext).toString(
|
|
|
|
|
'base64'
|
|
|
|
|
);
|
|
|
|
|
const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(
|
|
|
|
|
guardCtx.ciphertext
|
|
|
|
|
).toString('base64');
|
|
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
ciphertext: ciphertextBase64,
|
|
|
|
|
ephemeral_key: StringView.arrayBufferToHex(ctx4.ephemeral_key),
|
|
|
|
|
ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeral_key),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchOptions = {
|
|
|
|
@ -106,13 +116,13 @@ const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => {
|
|
|
|
|
const response = await nodeFetch(url, fetchOptions);
|
|
|
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
|
|
|
|
|
|
|
|
|
|
return processOnionResponse(reqIdx, response, ctx1.symmetricKey, true);
|
|
|
|
|
return processOnionResponse(reqIdx, response, ctxes[0].symmetricKey, true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Process a response as it arrives from `nodeFetch`, handling
|
|
|
|
|
// http errors and attempting to decrypt the body with `sharedKey`
|
|
|
|
|
const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
|
|
|
|
|
log.debug(`(${reqIdx}) [path] processing onion response`);
|
|
|
|
|
// FIXME: 401/500 handling?
|
|
|
|
|
|
|
|
|
|
// detect SNode is not ready (not in swarm; not done syncing)
|
|
|
|
|
if (response.status === 503) {
|
|
|
|
@ -464,6 +474,31 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
|
|
|
|
|
method,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function checkResponse(response, type) {
|
|
|
|
|
// Wrong swarm
|
|
|
|
|
if (response.status === 421) {
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
log.warn(
|
|
|
|
|
`lokirpc:::lokiFetch ${type} - wrong swarm, now looking at snodes`,
|
|
|
|
|
result.snode
|
|
|
|
|
);
|
|
|
|
|
const newSwarm = result.snodes ? result.snodes : [];
|
|
|
|
|
throw new textsecure.WrongSwarmError(newSwarm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wrong PoW difficulty
|
|
|
|
|
if (response.status === 432) {
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
throw new textsecure.WrongDifficultyError(result.difficulty);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response.status === 406) {
|
|
|
|
|
throw new textsecure.TimestampError(
|
|
|
|
|
'Invalid Timestamp (check your clock)'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Absence of targetNode indicates that we want a direct connection
|
|
|
|
|
// (e.g. to connect to a seed node for the first time)
|
|
|
|
@ -477,14 +512,6 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
|
|
|
|
|
const thisIdx = onionReqIdx;
|
|
|
|
|
onionReqIdx += 1;
|
|
|
|
|
|
|
|
|
|
log.debug(
|
|
|
|
|
`(${thisIdx}) using path ${path[0].ip}:${path[0].port} -> ${
|
|
|
|
|
path[1].ip
|
|
|
|
|
}:${path[1].port} -> ${path[2].ip}:${path[2].port} => ${
|
|
|
|
|
targetNode.ip
|
|
|
|
|
}:${targetNode.port}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
const result = await sendOnionRequest(
|
|
|
|
|
thisIdx,
|
|
|
|
@ -493,12 +520,36 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
|
|
|
|
|
fetchOptions.body
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getPathString = pathObjArr =>
|
|
|
|
|
pathObjArr.map(node => `${node.ip}:${node.port}`).join(', ');
|
|
|
|
|
|
|
|
|
|
if (result === BAD_PATH) {
|
|
|
|
|
log.error('[path] Error on the path');
|
|
|
|
|
log.error(
|
|
|
|
|
`[path] Error on the path: ${getPathString(path)} to ${
|
|
|
|
|
targetNode.ip
|
|
|
|
|
}:${targetNode.port}`
|
|
|
|
|
);
|
|
|
|
|
lokiSnodeAPI.markPathAsBad(path);
|
|
|
|
|
return false;
|
|
|
|
|
} else if (result) {
|
|
|
|
|
// not bad_path
|
|
|
|
|
// will throw if there's a problem
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
await checkResponse(result, 'onion');
|
|
|
|
|
} else {
|
|
|
|
|
return result ? result.json() : false;
|
|
|
|
|
// not truish and not bad_path
|
|
|
|
|
// false could mean, fail to parse results
|
|
|
|
|
// or status code wasn't 200
|
|
|
|
|
// or can't decrypt
|
|
|
|
|
// it's not a bad_path, so we don't need to mark the path as bad
|
|
|
|
|
log.error(
|
|
|
|
|
`[path] sendOnionRequest gave false for path: ${getPathString(
|
|
|
|
|
path
|
|
|
|
|
)} to ${targetNode.ip}:${targetNode.port}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result ? result.json() : false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -526,9 +577,15 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
|
|
|
|
|
*/
|
|
|
|
|
// pass the false value up
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} else if (result) {
|
|
|
|
|
// will throw if there's a problem
|
|
|
|
|
await checkResponse(result, 'proxy');
|
|
|
|
|
} // result is not truish and not explicitly false
|
|
|
|
|
|
|
|
|
|
// if not result, maybe we should throw??
|
|
|
|
|
return result ? result.json() : {};
|
|
|
|
|
// [] would make _retrieveNextMessages return undefined
|
|
|
|
|
// which would break messages.length
|
|
|
|
|
return result ? result.json() : false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.match(/https:\/\//)) {
|
|
|
|
@ -542,31 +599,14 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
|
|
|
|
|
// restore TLS checking
|
|
|
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
|
|
|
|
|
|
|
|
|
|
let result;
|
|
|
|
|
// Wrong swarm
|
|
|
|
|
if (response.status === 421) {
|
|
|
|
|
result = await response.json();
|
|
|
|
|
const newSwarm = result.snodes ? result.snodes : [];
|
|
|
|
|
throw new textsecure.WrongSwarmError(newSwarm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wrong PoW difficulty
|
|
|
|
|
if (response.status === 432) {
|
|
|
|
|
result = await response.json();
|
|
|
|
|
const { difficulty } = result;
|
|
|
|
|
throw new textsecure.WrongDifficultyError(difficulty);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response.status === 406) {
|
|
|
|
|
throw new textsecure.TimestampError(
|
|
|
|
|
'Invalid Timestamp (check your clock)'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
// will throw if there's a problem
|
|
|
|
|
await checkResponse(response, 'direct');
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new textsecure.HTTPError('Loki_rpc error', response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let result;
|
|
|
|
|
if (response.headers.get('Content-Type') === 'application/json') {
|
|
|
|
|
result = await response.json();
|
|
|
|
|
} else if (options.responseType === 'arraybuffer') {
|
|
|
|
|