Merge pull request #1100 from neuroscr/fileonion

File server onion routing support
This commit is contained in:
Ryan Tharp 2020-05-26 22:19:48 -07:00 committed by GitHub
commit e69828d490
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 616 additions and 137 deletions

View file

@ -1,4 +1,4 @@
/* global LokiAppDotNetServerAPI, LokiFileServerAPI, semver, log */
/* global LokiAppDotNetServerAPI, semver, log */
// eslint-disable-next-line func-names
(function() {
'use strict';
@ -12,9 +12,8 @@
);
// use the anonymous access token
window.tokenlessFileServerAdnAPI.token = 'loki';
window.tokenlessFileServerAdnAPI.pubKey = window.Signal.Crypto.base64ToArrayBuffer(
LokiFileServerAPI.secureRpcPubKey
);
// configure for file server comms
window.tokenlessFileServerAdnAPI.getPubKeyForUrl();
let nextWaitSeconds = 5;
const checkForUpgrades = async () => {

View file

@ -7,6 +7,8 @@ const FormData = require('form-data');
const https = require('https');
const path = require('path');
const lokiRpcUtils = require('./loki_rpc');
// Can't be less than 1200 if we have unauth'd requests
const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
const PUBLICCHAT_CHAN_POLL_EVERY = 20 * 1000; // 20s
@ -14,6 +16,7 @@ const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s
const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
// FIXME: replace with something on urlPubkeyMap...
const FILESERVER_HOSTS = [
'file-dev.lokinet.org',
'file.lokinet.org',
@ -21,6 +24,17 @@ const FILESERVER_HOSTS = [
'file.getsession.org',
];
const LOKIFOUNDATION_DEVFILESERVER_PUBKEY =
'BSZiMVxOco/b3sYfaeyiMWv/JnqokxGXkHoclEx8TmZ6';
const LOKIFOUNDATION_FILESERVER_PUBKEY =
'BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc';
const urlPubkeyMap = {
'https://file-dev.getsession.org': LOKIFOUNDATION_DEVFILESERVER_PUBKEY,
'https://file-dev.lokinet.org': LOKIFOUNDATION_DEVFILESERVER_PUBKEY,
'https://file.getsession.org': LOKIFOUNDATION_FILESERVER_PUBKEY,
'https://file.lokinet.org': LOKIFOUNDATION_FILESERVER_PUBKEY,
};
const HOMESERVER_USER_ANNOTATION_TYPE = 'network.loki.messenger.homeserver';
const AVATAR_USER_ANNOTATION_TYPE = 'network.loki.messenger.avatar';
const SETTINGS_CHANNEL_ANNOTATION_TYPE = 'net.patter-app.settings';
@ -34,12 +48,128 @@ const snodeHttpsAgent = new https.Agent({
const timeoutDelay = ms => new Promise(resolve => setTimeout(resolve, ms));
const sendToProxy = async (
srvPubKey,
endpoint,
pFetchOptions,
options = {}
) => {
const sendViaOnion = async (srvPubKey, url, fetchOptions, options = {}) => {
if (!srvPubKey) {
log.error(
'loki_app_dot_net:::sendViaOnion - called without a server public key'
);
return {};
}
// set retry count
if (options.retry === undefined) {
// eslint-disable-next-line no-param-reassign
options.retry = 0;
// eslint-disable-next-line no-param-reassign
options.requestNumber = window.lokiSnodeAPI.assignOnionRequestNumber();
}
const payloadObj = {
method: fetchOptions.method,
body: fetchOptions.body,
// safety issue with file server, just safer to have this
headers: fetchOptions.headers || {},
// no initial /
endpoint: url.pathname.replace(/^\//, ''),
};
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();
// update headers for boundary
payloadObj.headers = { ...payloadObj.headers, ...fHeaders };
// update body with base64 chunk
payloadObj.body = {
fileUpload: fData.toString('base64'),
};
}
let pathNodes = [];
try {
pathNodes = await lokiSnodeAPI.getOnionPath();
} catch (e) {
log.error(
`loki_app_dot_net:::sendViaOnion #${
options.requestNumber
} - getOnionPath Error ${e.code} ${e.message}`
);
}
if (!pathNodes || !pathNodes.length) {
log.warn(
`loki_app_dot_net:::sendViaOnion #${
options.requestNumber
} - failing, no path available`
);
// should we retry?
return {};
}
// do the request
let result;
try {
result = await lokiRpcUtils.sendOnionRequestLsrpcDest(
0,
pathNodes,
srvPubKey,
url.host,
payloadObj,
options.requestNumber
);
} catch (e) {
log.error(
'loki_app_dot_net:::sendViaOnion - lokiRpcUtils error',
e.code,
e.message
);
return {};
}
// handle error/retries
if (!result.status) {
log.error(
`loki_app_dot_net:::sendViaOnion #${options.requestNumber} - Retry #${
options.retry
} Couldnt handle onion request, retrying`,
payloadObj
);
return sendViaOnion(srvPubKey, url, fetchOptions, {
...options,
retry: options.retry + 1,
counter: options.requestNumber,
});
}
// get the return variables we need
let response = {};
let txtResponse = '';
let body = '';
try {
body = JSON.parse(result.body);
} catch (e) {
log.error(
`loki_app_dot_net:::sendViaOnion #${
options.requestNumber
} - Cant decode JSON body`,
result.body
);
}
// result.status has the http response code
txtResponse = JSON.stringify(body);
response = body;
response.headers = result.headers;
return { result, txtResponse, response };
};
const sendToProxy = async (srvPubKey, endpoint, fetchOptions, options = {}) => {
if (!srvPubKey) {
log.error(
'loki_app_dot_net:::sendToProxy - called without a server public key'
@ -47,17 +177,12 @@ const sendToProxy = async (
return {};
}
const fetchOptions = pFetchOptions; // make lint happy
// safety issue with file server, just safer to have this
if (fetchOptions.headers === undefined) {
fetchOptions.headers = {};
}
const payloadObj = {
body: fetchOptions.body, // might need to b64 if binary...
endpoint,
method: fetchOptions.method,
headers: fetchOptions.headers,
// safety issue with file server, just safer to have this
headers: fetchOptions.headers || {},
};
// from https://github.com/sindresorhus/is-stream/blob/master/index.js
@ -87,7 +212,7 @@ const sendToProxy = async (
log.warn('proxy random snode pool is not ready, retrying 10s', endpoint);
// no nodes in the pool yet, give it some time and retry
await timeoutDelay(1000);
return sendToProxy(srvPubKey, endpoint, pFetchOptions, options);
return sendToProxy(srvPubKey, endpoint, fetchOptions, options);
}
const url = `https://${randSnode.ip}:${randSnode.port}/file_proxy`;
@ -98,7 +223,10 @@ const sendToProxy = async (
payloadObj.body = false; // free memory
// make temporary key for this request/response
const ephemeralKey = await libsignal.Curve.async.generateKeyPair();
// async maybe preferable to avoid cpu spikes
// tho I think sync might be more apt in certain cases here...
// like sending
const ephemeralKey = await libloki.crypto.generateEphemeralKeyPair();
// mix server pub key with our priv key
const symKey = await libsignal.Curve.async.calculateAgreement(
@ -257,6 +385,21 @@ const serverRequest = async (endpoint, options = {}) => {
const host = url.host.toLowerCase();
// log.info('host', host, FILESERVER_HOSTS);
if (
window.lokiFeatureFlags.useFileOnionRequests &&
FILESERVER_HOSTS.includes(host)
) {
mode = 'sendViaOnion';
// url.search automatically includes the ? part
// const search = url.search || '';
// strip first slash
// const endpointWithQS = `${url.pathname}${search}`.replace(/^\//, '');
({ response, txtResponse, result } = await sendViaOnion(
srvPubKey,
url,
fetchOptions,
options
));
} else if (
window.lokiFeatureFlags.useSnodeProxy &&
FILESERVER_HOSTS.includes(host)
) {
@ -317,6 +460,14 @@ const serverRequest = async (endpoint, options = {}) => {
err: e,
};
}
if (!result) {
return {
err: 'noResult',
response,
};
}
// if it's a response style with a meta
if (result.status !== 200) {
if (!forceFreshToken && (!response.meta || response.meta.code === 401)) {
@ -418,6 +569,66 @@ class LokiAppDotNetServerAPI {
this.channels.splice(i, 1);
}
// set up pubKey & pubKeyHex properties
// optionally called for mainly file server comms
getPubKeyForUrl() {
// Hard coded
let pubKeyAB;
if (urlPubkeyMap) {
pubKeyAB = window.Signal.Crypto.base64ToArrayBuffer(
urlPubkeyMap[this.baseServerUrl]
);
}
// else will fail validation later
// if in proxy mode, don't allow "file-dev."...
// it only supports "file."... host.
if (
window.lokiFeatureFlags.useSnodeProxy &&
!window.lokiFeatureFlags.useOnionRequests
) {
pubKeyAB = window.Signal.Crypto.base64ToArrayBuffer(
LOKIFOUNDATION_FILESERVER_PUBKEY
);
}
// do we have their pubkey locally?
// FIXME: this._server won't be set yet...
// can't really do this for the file server because we'll need the key
// before we can communicate with lsrpc
/*
// get remote pubKey
this._server.serverRequest('loki/v1/public_key').then(keyRes => {
// we don't need to delay to protect identity because the token request
// should only be done over lokinet-lite
this.delayToken = true;
if (keyRes.err || !keyRes.response || !keyRes.response.data) {
if (keyRes.err) {
log.error(`Error ${keyRes.err}`);
}
} else {
// store it
this.pubKey = dcodeIO.ByteBuffer.wrap(
keyRes.response.data,
'base64'
).toArrayBuffer();
// write it to a file
}
});
*/
// now that key is loaded, lets verify
if (pubKeyAB && pubKeyAB.byteLength && pubKeyAB.byteLength !== 33) {
log.error('FILESERVER PUBKEY is invalid, length:', pubKeyAB.byteLength);
process.exit(1);
}
this.pubKey = pubKeyAB;
this.pubKeyHex = StringView.arrayBufferToHex(pubKeyAB);
return pubKeyAB;
}
async setProfileName(profileName) {
// when we add an annotation, may need this
/*

View file

@ -8,49 +8,11 @@ const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
const DEVICE_MAPPING_USER_ANNOTATION_TYPE =
'network.loki.messenger.devicemapping';
// const LOKIFOUNDATION_DEVFILESERVER_PUBKEY =
// 'BSZiMVxOco/b3sYfaeyiMWv/JnqokxGXkHoclEx8TmZ6';
const LOKIFOUNDATION_FILESERVER_PUBKEY =
'BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc';
// can have multiple of these instances as each user can have a
// different home server
class LokiFileServerInstance {
constructor(ourKey) {
this.ourKey = ourKey;
// do we have their pubkey locally?
/*
// get remote pubKey
this._server.serverRequest('loki/v1/public_key').then(keyRes => {
// we don't need to delay to protect identity because the token request
// should only be done over lokinet-lite
this.delayToken = true;
if (keyRes.err || !keyRes.response || !keyRes.response.data) {
if (keyRes.err) {
log.error(`Error ${keyRes.err}`);
}
} else {
// store it
this.pubKey = dcodeIO.ByteBuffer.wrap(
keyRes.response.data,
'base64'
).toArrayBuffer();
// write it to a file
}
});
*/
// Hard coded
this.pubKey = window.Signal.Crypto.base64ToArrayBuffer(
LOKIFOUNDATION_FILESERVER_PUBKEY
);
if (this.pubKey.byteLength && this.pubKey.byteLength !== 33) {
log.error(
'FILESERVER PUBKEY is invalid, length:',
this.pubKey.byteLength
);
process.exit(1);
}
}
// FIXME: this is not file-server specific
@ -65,9 +27,8 @@ class LokiFileServerInstance {
} else {
this._server = new LokiAppDotNetAPI(this.ourKey, serverUrl);
}
// configure proxy
this._server.pubKey = this.pubKey;
// make sure pubKey & pubKeyHex are set in _server
this.pubKey = this._server.getPubKeyForUrl();
if (options !== undefined && options.skipToken) {
return;
@ -80,6 +41,7 @@ class LokiFileServerInstance {
log.error('You are blacklisted form this home server');
}
}
async getUserDeviceMapping(pubKey) {
const annotations = await this._server.getUserAnnotations(pubKey);
const deviceMapping = annotations.find(
@ -333,7 +295,5 @@ class LokiFileServerFactoryAPI {
return thisServer;
}
}
// smuggle some data out of this joint (for expire.js/version upgrade check)
LokiFileServerFactoryAPI.secureRpcPubKey = LOKIFOUNDATION_FILESERVER_PUBKEY;
module.exports = LokiFileServerFactoryAPI;

View file

@ -11,88 +11,257 @@ const snodeHttpsAgent = new https.Agent({
const endpointBase = '/storage_rpc/v1';
// Request index for debugging
let onionReqIdx = 0;
const encryptForNode = async (node, payloadStr) => {
const textEncoder = new TextEncoder();
const plaintext = textEncoder.encode(payloadStr);
return libloki.crypto.encryptForPubkey(node.pubkey_x25519, plaintext);
};
// Returns the actual ciphertext, symmetric key that will be used
// for decryption, and an ephemeral_key to send to the next hop
const encryptForDestination = async (node, payload) => {
// Do we still need "headers"?
const reqStr = JSON.stringify({ body: payload, headers: '' });
const encryptForPubKey = async (pubKeyX25519hex, reqObj) => {
const reqStr = JSON.stringify(reqObj);
return encryptForNode(node, reqStr);
const textEncoder = new TextEncoder();
const plaintext = textEncoder.encode(reqStr);
return libloki.crypto.encryptForPubkey(pubKeyX25519hex, plaintext);
};
// `ctx` holds info used by `node` to relay further
const encryptForRelay = async (node, nextNode, ctx) => {
const encryptForRelay = async (relayX25519hex, destination, ctx) => {
// ctx contains: ciphertext, symmetricKey, ephemeralKey
const payload = ctx.ciphertext;
const reqJson = {
if (!destination.host && !destination.destination) {
log.warn(`loki_rpc::encryptForRelay - no destination`, destination);
}
const reqObj = {
...destination,
ciphertext: dcodeIO.ByteBuffer.wrap(payload).toString('base64'),
ephemeral_key: StringView.arrayBufferToHex(ctx.ephemeralKey),
destination: nextNode.pubkey_ed25519,
};
const reqStr = JSON.stringify(reqJson);
return encryptForNode(node, reqStr);
return encryptForPubKey(relayX25519hex, reqObj);
};
const BAD_PATH = 'bad_path';
// May return false BAD_PATH, indicating that we should try a new
const sendOnionRequest = async (reqIdx, nodePath, targetNode, plaintext) => {
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 makeGuardPayload = guardCtx => {
const ciphertextBase64 = dcodeIO.ByteBuffer.wrap(
guardCtx.ciphertext
).toString('base64');
const payload = {
const guardPayloadObj = {
ciphertext: ciphertextBase64,
ephemeral_key: StringView.arrayBufferToHex(guardCtx.ephemeralKey),
};
return guardPayloadObj;
};
const fetchOptions = {
// we just need the targetNode.pubkey_ed25519 for the encryption
// targetPubKey is ed25519 if snode is the target
const makeOnionRequest = async (
nodePath,
destCtx,
targetED25519Hex,
finalRelayOptions = false,
id = ''
) => {
const ctxes = [destCtx];
// from (3) 2 to 0
const firstPos = nodePath.length - 1;
for (let i = firstPos; i > -1; i -= 1) {
let dest;
const relayingToFinalDestination = i === 0; // if last position
if (relayingToFinalDestination && finalRelayOptions) {
dest = {
host: finalRelayOptions.host,
target: '/loki/v1/lsrpc',
method: 'POST',
};
} else {
// set x25519 if destination snode
let pubkeyHex = targetED25519Hex; // relayingToFinalDestination
// or ed25519 snode destination
if (!relayingToFinalDestination) {
pubkeyHex = nodePath[i + 1].pubkey_ed25519;
if (!pubkeyHex) {
log.error(
`loki_rpc:::makeOnionRequest ${id} - no ed25519 for`,
nodePath[i + 1],
'path node',
i + 1
);
}
}
// destination takes a hex key
dest = {
destination: pubkeyHex,
};
}
try {
// eslint-disable-next-line no-await-in-loop
const ctx = await encryptForRelay(
nodePath[i].pubkey_x25519,
dest,
ctxes[ctxes.length - 1]
);
ctxes.push(ctx);
} catch (e) {
log.error(
`loki_rpc:::makeOnionRequest ${id} - encryptForRelay failure`,
e.code,
e.message
);
throw e;
}
}
const guardCtx = ctxes[ctxes.length - 1]; // last ctx
const payloadObj = makeGuardPayload(guardCtx);
// all these requests should use AesGcm
return payloadObj;
};
// finalDestOptions is an object
// FIXME: internally track reqIdx, not externally
const sendOnionRequest = async (
reqIdx,
nodePath,
destX25519Any,
finalDestOptions,
finalRelayOptions = false,
lsrpcIdx
) => {
if (!destX25519Any) {
log.error('loki_rpc::sendOnionRequest - no destX25519Any given');
return {};
}
// loki-storage may need this to function correctly
// but ADN calls will not always have a body
/*
if (!finalDestOptions.body) {
finalDestOptions.body = '';
}
*/
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') {
// convert AB to hex
destX25519hex = StringView.arrayBufferToHex(destX25519Any);
}
// safely build destination
let targetEd25519hex;
if (finalDestOptions) {
if (finalDestOptions.destination_ed25519_hex) {
// snode destination
targetEd25519hex = finalDestOptions.destination_ed25519_hex;
// eslint-disable-next-line no-param-reassign
delete finalDestOptions.destination_ed25519_hex;
}
// else it's lsrpc...
} else {
// eslint-disable-next-line no-param-reassign
finalDestOptions = {};
log.warn(`loki_rpc::sendOnionRequest ${id} - no finalDestOptions`);
return {};
}
const options = finalDestOptions; // lint
// do we need this?
if (options.headers === undefined) {
options.headers = '';
}
let destCtx;
try {
destCtx = await encryptForPubKey(destX25519hex, options);
} catch (e) {
log.error(
`loki_rpc::sendOnionRequest ${id} - encryptForPubKey failure [`,
e.code,
e.message,
'] destination X25519',
destX25519hex.substr(0, 32),
'...',
destX25519hex.substr(32),
'options',
options
);
throw e;
}
const payloadObj = await makeOnionRequest(
nodePath,
destCtx,
targetEd25519hex,
finalRelayOptions,
id
);
const guardFetchOptions = {
method: 'POST',
body: JSON.stringify(payload),
body: JSON.stringify(payloadObj),
// we are talking to a snode...
agent: snodeHttpsAgent,
};
const url = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
const guardUrl = `https://${nodePath[0].ip}:${nodePath[0].port}/onion_req`;
const response = await nodeFetch(guardUrl, guardFetchOptions);
// we only proxy to snodes...
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const response = await nodeFetch(url, fetchOptions);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
return processOnionResponse(reqIdx, response, ctxes[0].symmetricKey, true);
return processOnionResponse(reqIdx, response, destCtx.symmetricKey, true);
};
const sendOnionRequestSnodeDest = async (
reqIdx,
nodePath,
targetNode,
plaintext
) =>
sendOnionRequest(reqIdx, nodePath, targetNode.pubkey_x25519, {
destination_ed25519_hex: targetNode.pubkey_ed25519,
body: plaintext,
});
// need relay node's pubkey_x25519_hex
// always the same target: /loki/v1/lsrpc
const sendOnionRequestLsrpcDest = async (
reqIdx,
nodePath,
destX25519Any,
host,
payloadObj,
lsrpcIdx = 0
) =>
sendOnionRequest(
reqIdx,
nodePath,
destX25519Any,
payloadObj,
{ host },
lsrpcIdx
);
const BAD_PATH = 'bad_path';
// 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) => {
// May return false BAD_PATH, indicating that we should try a new path.
const processOnionResponse = async (
reqIdx,
response,
sharedKey,
useAesGcm,
debug
) => {
// FIXME: 401/500 handling?
// detect SNode is not ready (not in swarm; not done syncing)
@ -115,16 +284,26 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
if (response.status !== 200) {
log.warn(
`(${reqIdx}) [path] fetch unhandled error code: ${response.status}`
`(${reqIdx}) [path] lokiRpc::processOnionResponse - fetch unhandled error code: ${
response.status
}`
);
return false;
}
const ciphertext = await response.text();
if (!ciphertext) {
log.warn(`(${reqIdx}) [path]: Target node return empty ciphertext`);
log.warn(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - Target node return empty ciphertext`
);
return false;
}
if (debug) {
log.debug(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertext`,
ciphertext
);
}
let plaintext;
let ciphertextBuffer;
@ -134,22 +313,52 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
'base64'
).toArrayBuffer();
const decryptFn = useAesGcm
? window.libloki.crypto.DecryptGCM
: window.libloki.crypto.DHDecrypt;
if (debug) {
log.debug(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
StringView.arrayBufferToHex(ciphertextBuffer),
'useAesGcm',
useAesGcm
);
}
const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer);
const decryptFn = useAesGcm
? libloki.crypto.DecryptGCM
: libloki.crypto.DHDecrypt;
const plaintextBuffer = await decryptFn(sharedKey, ciphertextBuffer, debug);
if (debug) {
log.debug(
'lokiRpc::processOnionResponse - plaintextBuffer',
plaintextBuffer.toString()
);
}
const textDecoder = new TextDecoder();
plaintext = textDecoder.decode(plaintextBuffer);
} catch (e) {
log.error(`(${reqIdx}) [path] decode error`);
log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - decode error`,
e.code,
e.message
);
log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - symKey`,
StringView.arrayBufferToHex(sharedKey)
);
if (ciphertextBuffer) {
log.error(`(${reqIdx}) [path] ciphertextBuffer`, ciphertextBuffer);
log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - ciphertextBuffer`,
StringView.arrayBufferToHex(ciphertextBuffer)
);
}
return false;
}
if (debug) {
log.debug('lokiRpc::processOnionResponse - plaintext', plaintext);
}
try {
const jsonRes = JSON.parse(plaintext);
// emulate nodeFetch response...
@ -158,13 +367,22 @@ const processOnionResponse = async (reqIdx, response, sharedKey, useAesGcm) => {
const res = JSON.parse(jsonRes.body);
return res;
} catch (e) {
log.error(`(${reqIdx}) [path] parse error json: `, jsonRes.body);
log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error inner json: `,
jsonRes.body
);
}
return false;
};
return jsonRes;
} catch (e) {
log.error('[path] parse error', e.code, e.message, `json:`, plaintext);
log.error(
`(${reqIdx}) [path] lokiRpc::processOnionResponse - parse error outer json`,
e.code,
e.message,
`json:`,
plaintext
);
return false;
}
};
@ -206,7 +424,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519);
const myKeys = await window.libloki.crypto.generateEphemeralKeyPair();
const myKeys = await libloki.crypto.generateEphemeralKeyPair();
const symmetricKey = await libsignal.Curve.async.calculateAgreement(
snPubkeyHex,
@ -217,7 +435,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
const body = JSON.stringify(options);
const plainText = textEncoder.encode(body);
const ivAndCiphertext = await window.libloki.crypto.DHEncrypt(
const ivAndCiphertext = await libloki.crypto.DHEncrypt(
symmetricKey,
plainText
);
@ -279,6 +497,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
// grab a fresh random one
return sendToProxy(options, targetNode, pRetryNumber);
}
// 502 is "Next node not found"
// detect SNode is not ready (not in swarm; not done syncing)
// 503 can be proxy target or destination in pre 2.0.3
@ -364,7 +583,7 @@ const sendToProxy = async (options = {}, targetNode, retryNumber = 0) => {
'base64'
).toArrayBuffer();
const plaintextBuffer = await window.libloki.crypto.DHDecrypt(
const plaintextBuffer = await libloki.crypto.DHDecrypt(
symmetricKey,
ciphertextBuffer
);
@ -460,6 +679,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
// Wrong PoW difficulty
if (response.status === 432) {
const result = await response.json();
log.error(`lokirpc:::lokiFetch ${type} - WRONG POW`, result);
throw new textsecure.WrongDifficultyError(result.difficulty);
}
@ -480,11 +700,10 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
// Get a path excluding `targetNode`:
// eslint-disable-next-line no-await-in-loop
const path = await lokiSnodeAPI.getOnionPath(targetNode);
const thisIdx = onionReqIdx;
onionReqIdx += 1;
const thisIdx = window.lokiSnodeAPI.assignOnionRequestNumber();
// eslint-disable-next-line no-await-in-loop
const result = await sendOnionRequest(
const result = await sendOnionRequestSnodeDest(
thisIdx,
path,
targetNode,
@ -640,4 +859,5 @@ const lokiRpc = (
module.exports = {
lokiRpc,
sendOnionRequestLsrpcDest,
};

View file

@ -23,8 +23,17 @@ const compareSnodes = (current, search) =>
// just get the filtered list
async function tryGetSnodeListFromLokidSeednode(
seedNodes = [...window.seedNodeList]
seedNodes = window.seedNodeList
) {
if (!seedNodes.length) {
log.error(
`loki_snodes:::tryGetSnodeListFromLokidSeednode - no seedNodes given`,
seedNodes,
'window',
window.seedNodeList
);
return [];
}
// Removed limit until there is a way to get snode info
// for individual nodes (needed for guard nodes); this way
// we get all active nodes
@ -42,6 +51,13 @@ async function tryGetSnodeListFromLokidSeednode(
Math.floor(Math.random() * seedNodes.length),
1
)[0];
if (!seedNode) {
log.error(
`loki_snodes:::tryGetSnodeListFromLokidSeednode - seedNode selection failure - seedNodes`,
seedNodes
);
return [];
}
let snodes = [];
try {
const getSnodesFromSeedUrl = async urlObj => {
@ -53,6 +69,30 @@ async function tryGetSnodeListFromLokidSeednode(
{}, // Options
'/json_rpc' // Seed request endpoint
);
if (!response) {
log.error(
`loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid response from seed ${urlObj.toString()}:`,
response
);
return [];
}
// should we try to JSON.parse this?
if (typeof response === 'string') {
log.error(
`loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid string response from seed ${urlObj.toString()}:`,
response
);
return [];
}
if (!response.result) {
log.error(
`loki_snodes:::tryGetSnodeListFromLokidSeednode - invalid result from seed ${urlObj.toString()}:`,
response
);
return [];
}
// Filter 0.0.0.0 nodes which haven't submitted uptime proofs
return response.result.service_node_states.filter(
snode => snode.public_ip !== '0.0.0.0'
@ -72,6 +112,13 @@ async function tryGetSnodeListFromLokidSeednode(
);
}
}
if (snodes.length) {
log.info(
`loki_snodes:::tryGetSnodeListFromLokidSeednode - got ${
snodes.length
} service nodes from seed`
);
}
return snodes;
} catch (e) {
log.warn(
@ -87,9 +134,18 @@ async function tryGetSnodeListFromLokidSeednode(
}
async function getSnodeListFromLokidSeednode(
seedNodes = [...window.seedNodeList],
seedNodes = window.seedNodeList,
retries = 0
) {
if (!seedNodes.length) {
log.error(
`loki_snodes:::getSnodeListFromLokidSeednode - no seedNodes given`,
seedNodes,
'window',
window.seedNodeList
);
return [];
}
let snodes = [];
try {
snodes = await tryGetSnodeListFromLokidSeednode(seedNodes);
@ -129,6 +185,12 @@ class LokiSnodeAPI {
this.onionPaths = [];
this.guardNodes = [];
this.onionRequestCounter = 0; // Request index for debugging
}
assignOnionRequestNumber() {
this.onionRequestCounter += 1;
return this.onionRequestCounter;
}
async getRandomSnodePool() {
@ -202,7 +264,7 @@ class LokiSnodeAPI {
// FIXME: handle rejections
let nodePool = await this.getRandomSnodePool();
if (nodePool.length === 0) {
log.error(`Could not select guarn nodes: node pool is empty`);
log.error(`Could not select guard nodes: node pool is empty`);
return [];
}
@ -213,7 +275,7 @@ class LokiSnodeAPI {
const DESIRED_GUARD_COUNT = 3;
if (shuffled.length < DESIRED_GUARD_COUNT) {
log.error(
`Could not select guarn nodes: node pool is not big enough, pool size ${
`Could not select guard nodes: node pool is not big enough, pool size ${
shuffled.length
}, need ${DESIRED_GUARD_COUNT}, attempting to refresh randomPool`
);
@ -222,7 +284,7 @@ class LokiSnodeAPI {
shuffled = _.shuffle(nodePool);
if (shuffled.length < DESIRED_GUARD_COUNT) {
log.error(
`Could not select guarn nodes: node pool is not big enough, pool size ${
`Could not select guard nodes: node pool is not big enough, pool size ${
shuffled.length
}, need ${DESIRED_GUARD_COUNT}, failing...`
);
@ -278,12 +340,15 @@ class LokiSnodeAPI {
`Must have at least 2 good onion paths, actual: ${goodPaths.length}`
);
await this.buildNewOnionPaths();
// should we add a delay? buildNewOnionPaths should act as one
// reload goodPaths now
return this.getOnionPath(toExclude);
}
const paths = _.shuffle(goodPaths);
if (!toExclude) {
return paths[0];
return paths[0].path;
}
// Select a path that doesn't contain `toExclude`
@ -294,6 +359,19 @@ class LokiSnodeAPI {
if (otherPaths.length === 0) {
// This should never happen!
// well it did happen, should we
// await this.buildNewOnionPaths();
// and restart call?
log.error(
`loki_snode_api::getOnionPath - no paths without`,
toExclude.pubkey_ed25519,
'path count',
paths.length,
'goodPath count',
goodPaths.length,
'paths',
paths
);
throw new Error('No onion paths available after filtering');
}
@ -569,7 +647,17 @@ class LokiSnodeAPI {
);
}
async refreshRandomPool(seedNodes = [...window.seedNodeList]) {
async refreshRandomPool(seedNodes = window.seedNodeList) {
if (!seedNodes.length) {
if (!window.seedNodeList || !window.seedNodeList.length) {
log.error(
`loki_snodes:::refreshRandomPool - seedNodeList has not been loaded yet`
);
return [];
}
// eslint-disable-next-line no-param-reassign
seedNodes = window.seedNodeList;
}
return primitives.allowOnlyOneAtATime('refreshRandomPool', async () => {
// are we running any _getAllVerionsForRandomSnodePool
if (this.stopGetAllVersionPromiseControl !== false) {

View file

@ -416,6 +416,7 @@ window.lokiFeatureFlags = {
privateGroupChats: true,
useSnodeProxy: !process.env.USE_STUBBED_NETWORK,
useOnionRequests: true,
useFileOnionRequests: false,
onionRequestHops: 1,
};