Merge pull request #1085 from neuroscr/patchopensnapps

Configurable onion request hops and wrong PoW/timestamp/swarm fixes for proxy/ORs
pull/1084/head
Ryan Tharp 5 years ago committed by GitHub
commit 335244c6cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -113,6 +113,11 @@ class LokiMessageAPI {
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
snode = await primitives.firstTrue(promises); snode = await primitives.firstTrue(promises);
} catch (e) { } catch (e) {
log.warn(
`loki_message:::sendMessage - ${e.code} ${e.message} to ${pubKey} via ${
snode.ip
}:${snode.port}`
);
if (e instanceof textsecure.WrongDifficultyError) { if (e instanceof textsecure.WrongDifficultyError) {
// Force nonce recalculation // Force nonce recalculation
// NOTE: Currently if there are snodes with conflicting difficulties we // NOTE: Currently if there are snodes with conflicting difficulties we
@ -194,6 +199,9 @@ class LokiMessageAPI {
'/storage_rpc/v1', '/storage_rpc/v1',
targetNode targetNode
); );
// succcessful messages should look like
// `{\"difficulty\":1}`
// but so does invalid pow, so be careful!
// do not return true if we get false here... // do not return true if we get false here...
if (result === false) { if (result === false) {

@ -109,7 +109,6 @@ function abortableIterator(array, iterator) {
start: async serially => { start: async serially => {
let item = destructableList.pop(); let item = destructableList.pop();
while (item && !abortIteration) { while (item && !abortIteration) {
// console.log('iterating on item', item);
if (serially) { if (serially) {
try { try {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop

@ -78,20 +78,30 @@ const BAD_PATH = 'bad_path';
// May return false BAD_PATH, indicating that we should try a new // May return false BAD_PATH, indicating that we should try a new
const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => { const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => {
log.debug('Sending an onion request'); const ctxes = [await encryptForDestination(targetNode, plaintext)];
// from (3) 2 to 0
const ctx1 = await encryptForDestination(targetNode, plaintext); const firstPos = nodePath.length - 1;
const ctx2 = await encryptForRelay(nodePath[2], targetNode, ctx1);
const ctx3 = await encryptForRelay(nodePath[1], nodePath[2], ctx2); for (let i = firstPos; i > -1; i -= 1) {
const ctx4 = await encryptForRelay(nodePath[0], nodePath[1], ctx3); // 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( const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(
'base64' guardCtx.ciphertext
); ).toString('base64');
const payload = { const payload = {
ciphertext: ciphertextBase64, ciphertext: ciphertextBase64,
ephemeral_key: StringView.arrayBufferToHex(ctx4.ephemeral_key), ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeral_key),
}; };
const fetchOptions = { const fetchOptions = {
@ -106,13 +116,13 @@ const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => {
const response = await nodeFetch(url, fetchOptions); const response = await nodeFetch(url, fetchOptions);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'; 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 // Process a response as it arrives from `nodeFetch`, handling
// http errors and attempting to decrypt the body with `sharedKey` // http errors and attempting to decrypt the body with `sharedKey`
const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => { 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) // detect SNode is not ready (not in swarm; not done syncing)
if (response.status === 503) { if (response.status === 503) {
@ -464,6 +474,31 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
method, 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 { try {
// Absence of targetNode indicates that we want a direct connection // Absence of targetNode indicates that we want a direct connection
// (e.g. to connect to a seed node for the first time) // (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; const thisIdx = onionReqIdx;
onionReqIdx += 1; 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 // eslint-disable-next-line no-await-in-loop
const result = await sendOnionRequest( const result = await sendOnionRequest(
thisIdx, thisIdx,
@ -493,12 +520,36 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
fetchOptions.body fetchOptions.body
); );
const getPathString = pathObjArr =>
pathObjArr.map(node => `${node.ip}:${node.port}`).join(', ');
if (result === BAD_PATH) { 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); 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 { } 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 // pass the false value up
return false; 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?? // 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:\/\//)) { if (url.match(/https:\/\//)) {
@ -542,31 +599,14 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
// restore TLS checking // restore TLS checking
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
let result; // will throw if there's a problem
// Wrong swarm await checkResponse(response, 'direct');
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)'
);
}
if (!response.ok) { if (!response.ok) {
throw new textsecure.HTTPError('Loki_rpc error', response); throw new textsecure.HTTPError('Loki_rpc error', response);
} }
let result;
if (response.headers.get('Content-Type') === 'application/json') { if (response.headers.get('Content-Type') === 'application/json') {
result = await response.json(); result = await response.json();
} else if (options.responseType === 'arraybuffer') { } else if (options.responseType === 'arraybuffer') {

@ -348,19 +348,30 @@ class LokiSnodeAPI {
const guards = _.shuffle(this.guardNodes); const guards = _.shuffle(this.guardNodes);
// Create path for every guard node: // Create path for every guard node:
const nodesNeededPerPaths = window.lokiFeatureFlags.onionRequestHops - 1;
// Each path needs 2 nodes in addition to the guard node:
const maxPath = Math.floor(Math.min(guards.length, otherNodes.length / 2)); // Each path needs X (nodesNeededPerPaths) nodes in addition to the guard node:
const maxPath = Math.floor(
Math.min(
guards.length,
nodesNeededPerPaths
? otherNodes.length / nodesNeededPerPaths
: otherNodes.length
)
);
// TODO: might want to keep some of the existing paths // TODO: might want to keep some of the existing paths
this.onionPaths = []; this.onionPaths = [];
for (let i = 0; i < maxPath; i += 1) { for (let i = 0; i < maxPath; i += 1) {
const path = [guards[i], otherNodes[i * 2], otherNodes[i * 2 + 1]]; const path = [guards[i]];
for (let j = 0; j < nodesNeededPerPaths; j += 1) {
path.push(otherNodes[i * nodesNeededPerPaths + j]);
}
this.onionPaths.push({ path, bad: false }); this.onionPaths.push({ path, bad: false });
} }
log.info('Built onion paths: ', this.onionPaths); log.info(`Built ${this.onionPaths.length} onion paths`, this.onionPaths);
} }
async getRandomSnodeAddress() { async getRandomSnodeAddress() {

@ -414,7 +414,8 @@ window.lokiFeatureFlags = {
privateGroupChats: true, privateGroupChats: true,
useSnodeProxy: !process.env.USE_STUBBED_NETWORK, useSnodeProxy: !process.env.USE_STUBBED_NETWORK,
useSealedSender: true, useSealedSender: true,
useOnionRequests: false, useOnionRequests: true,
onionRequestHops: 1,
}; };
// eslint-disable-next-line no-extend-native,func-names // eslint-disable-next-line no-extend-native,func-names
@ -440,14 +441,7 @@ if (
}; };
/* eslint-enable global-require, import/no-extraneous-dependencies */ /* eslint-enable global-require, import/no-extraneous-dependencies */
window.lokiFeatureFlags = {}; window.lokiFeatureFlags = {};
window.lokiSnodeAPI = { window.lokiSnodeAPI = {}; // no need stub out each function here
refreshSwarmNodesForPubKey: () => [],
getFreshSwarmNodes: () => [],
updateSwarmNodes: () => {},
updateLastHash: () => {},
getSwarmNodesForPubKey: () => [],
buildNewOnionPaths: () => [],
};
} }
if (config.environment.includes('test-integration')) { if (config.environment.includes('test-integration')) {
window.lokiFeatureFlags = { window.lokiFeatureFlags = {

Loading…
Cancel
Save