Get snode from snode (#1614)
* force deleteAccount after 10sec timeout waiting for configMessage * move some constants to file where they are used * add a way to fetch snodes from snodes * remove a snode from a pubkey's swarm if we get 421 without valid content * remove getVersion from snodes * hide groupMembers in right panel for non-group convo
This commit is contained in:
parent
442b881438
commit
58abd08e6d
|
@ -59,6 +59,17 @@
|
|||
}
|
||||
inherit(ReplayableError, EmptySwarmError);
|
||||
|
||||
function InvalidateSwarm(number, message) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
this.number = number.split('.')[0];
|
||||
|
||||
ReplayableError.call(this, {
|
||||
name: 'InvalidateSwarm',
|
||||
message,
|
||||
});
|
||||
}
|
||||
inherit(ReplayableError, InvalidateSwarm);
|
||||
|
||||
function NotFoundError(message, error) {
|
||||
this.name = 'NotFoundError';
|
||||
this.message = message;
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface LibTextsecure {
|
|||
SendMessageNetworkError: any;
|
||||
ReplayableError: any;
|
||||
EmptySwarmError: any;
|
||||
InvalidateSwarm: any;
|
||||
SeedNodeError: any;
|
||||
HTTPError: any;
|
||||
NotFoundError: any;
|
||||
|
|
|
@ -58,7 +58,6 @@ window.lokiFeatureFlags = {
|
|||
useOnionRequests: true,
|
||||
useFileOnionRequests: true,
|
||||
useFileOnionRequestsV2: true, // more compact encoding of files in response
|
||||
onionRequestHops: 3,
|
||||
useRequestEncryptionKeyPair: false,
|
||||
padOutgoingAttachments: true,
|
||||
};
|
||||
|
@ -83,8 +82,6 @@ window.isBeforeVersion = (toCheck, baseVersion) => {
|
|||
|
||||
// eslint-disable-next-line func-names
|
||||
window.CONSTANTS = new (function() {
|
||||
this.MAX_GROUP_NAME_LENGTH = 64;
|
||||
this.CLOSED_GROUP_SIZE_LIMIT = 100;
|
||||
// Number of seconds to turn on notifications after reconnect/start of app
|
||||
this.NOTIFICATION_ENABLE_TIMEOUT_SECONDS = 10;
|
||||
|
||||
|
@ -94,8 +91,6 @@ window.CONSTANTS = new (function() {
|
|||
// Conforms to naming rules here
|
||||
// https://loki.network/2020/03/25/loki-name-system-the-facts/
|
||||
this.LNS_REGEX = `^[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,${this.LNS_MAX_LENGTH - 2}}[a-zA-Z0-9_]){0,1}$`;
|
||||
this.MIN_GUARD_COUNT = 2;
|
||||
this.DESIRED_GUARD_COUNT = 3;
|
||||
})();
|
||||
|
||||
window.versionInfo = {
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import { ContactType } from './session/SessionMemberListItem';
|
||||
import { ToastUtils } from '../session/utils';
|
||||
import { createClosedGroup as createClosedGroupV2 } from '../receiver/closedGroups';
|
||||
import { VALIDATION } from '../session/constants';
|
||||
|
||||
export class MessageView extends React.Component {
|
||||
public render() {
|
||||
|
@ -44,7 +45,7 @@ async function createClosedGroup(
|
|||
ToastUtils.pushToastError('invalidGroupName', window.i18n('invalidGroupNameTooShort'));
|
||||
|
||||
return false;
|
||||
} else if (groupName.length > window.CONSTANTS.MAX_GROUP_NAME_LENGTH) {
|
||||
} else if (groupName.length > VALIDATION.MAX_GROUP_NAME_LENGTH) {
|
||||
ToastUtils.pushToastError('invalidGroupName', window.i18n('invalidGroupNameTooLong'));
|
||||
return false;
|
||||
}
|
||||
|
@ -55,7 +56,7 @@ async function createClosedGroup(
|
|||
if (groupMembers.length < 1) {
|
||||
ToastUtils.pushToastError('pickClosedGroupMember', window.i18n('pickClosedGroupMember'));
|
||||
return false;
|
||||
} else if (groupMembers.length >= window.CONSTANTS.CLOSED_GROUP_SIZE_LIMIT) {
|
||||
} else if (groupMembers.length >= VALIDATION.CLOSED_GROUP_SIZE_LIMIT) {
|
||||
ToastUtils.pushToastError('closedGroupMaxSize', window.i18n('closedGroupMaxSize'));
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ConversationModel, ConversationTypeEnum } from '../../models/conversati
|
|||
import { getCompleteUrlForV2ConvoId } from '../../interactions/conversation';
|
||||
import _ from 'lodash';
|
||||
import autoBind from 'auto-bind';
|
||||
import { VALIDATION } from '../../session/constants';
|
||||
interface Props {
|
||||
contactList: Array<any>;
|
||||
chatName: string;
|
||||
|
@ -151,7 +152,7 @@ class InviteContactsDialogInner extends React.Component<Props, State> {
|
|||
// be sure to include current zombies in this count
|
||||
if (
|
||||
newMembers.length + existingMembers.length + existingZombies.length >
|
||||
window.CONSTANTS.CLOSED_GROUP_SIZE_LIMIT
|
||||
VALIDATION.CLOSED_GROUP_SIZE_LIMIT
|
||||
) {
|
||||
ToastUtils.pushTooManyMembers();
|
||||
return;
|
||||
|
|
|
@ -252,7 +252,7 @@ class SessionRightPanel extends React.Component<Props, State> {
|
|||
const showUpdateGroupNameButton = isAdmin && !commonNoShow;
|
||||
const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic;
|
||||
|
||||
const showUpdateGroupMembersButton = !isPublic && !commonNoShow;
|
||||
const showUpdateGroupMembersButton = !isPublic && isGroup && !commonNoShow;
|
||||
|
||||
return (
|
||||
<div className="group-settings">
|
||||
|
|
|
@ -204,7 +204,8 @@ export class OpenGroupManagerV2 {
|
|||
} catch (e) {
|
||||
window.log.warn('Failed to join open group v2', e);
|
||||
await removeV2OpenGroupRoom(conversationId);
|
||||
throw new Error(window.i18n('connectToServerFail'));
|
||||
// throw new Error(window.i18n('connectToServerFail'));
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,11 @@ export const CONVERSATION = {
|
|||
MAX_ATTACHMENT_FILESIZE_BYTES: 6 * 1000 * 1000,
|
||||
};
|
||||
|
||||
export const VALIDATION = {
|
||||
MAX_GROUP_NAME_LENGTH: 64,
|
||||
CLOSED_GROUP_SIZE_LIMIT: 100,
|
||||
};
|
||||
|
||||
export const UI = {
|
||||
// Pixels (scroll) from the top of the top of message container
|
||||
// at which more messages should be loaded
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
ConversationTypeEnum,
|
||||
} from '../../models/conversation';
|
||||
import { BlockedNumberController } from '../../util';
|
||||
import { getSnodesFor } from '../snode_api/snodePool';
|
||||
import { getSwarm } from '../snode_api/snodePool';
|
||||
import { PubKey } from '../types';
|
||||
import { actions as conversationActions } from '../../state/ducks/conversations';
|
||||
import { getV2OpenGroupRoom, removeV2OpenGroupRoom } from '../../data/opengroups';
|
||||
|
@ -120,7 +120,7 @@ export class ConversationController {
|
|||
await Promise.all([
|
||||
conversation.updateProfileAvatar(),
|
||||
// NOTE: we request snodes updating the cache, but ignore the result
|
||||
void getSnodesFor(id),
|
||||
void getSwarm(id),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -8,6 +8,8 @@ import { allowOnlyOneAtATime } from '../utils/Promise';
|
|||
|
||||
export type Snode = SnodePool.Snode;
|
||||
|
||||
const desiredGuardCount = 3;
|
||||
const minimumGuardCount = 2;
|
||||
interface SnodePath {
|
||||
path: Array<Snode>;
|
||||
bad: boolean;
|
||||
|
@ -15,7 +17,7 @@ interface SnodePath {
|
|||
|
||||
export class OnionPaths {
|
||||
private static instance: OnionPaths | null;
|
||||
|
||||
private static readonly onionRequestHops = 3;
|
||||
private onionPaths: Array<SnodePath> = [];
|
||||
|
||||
// This array is meant to store nodes will full info,
|
||||
|
@ -46,7 +48,7 @@ export class OnionPaths {
|
|||
let goodPaths = this.onionPaths.filter(x => !x.bad);
|
||||
|
||||
let attemptNumber = 0;
|
||||
while (goodPaths.length < CONSTANTS.MIN_GUARD_COUNT) {
|
||||
while (goodPaths.length < minimumGuardCount) {
|
||||
log.error(
|
||||
`Must have at least 2 good onion paths, actual: ${goodPaths.length}, attempt #${attemptNumber} fetching more...`
|
||||
);
|
||||
|
@ -174,11 +176,11 @@ export class OnionPaths {
|
|||
}
|
||||
|
||||
private async selectGuardNodes(): Promise<Array<Snode>> {
|
||||
const { CONSTANTS, log } = window;
|
||||
const { log } = window;
|
||||
|
||||
// `getRandomSnodePool` is expected to refresh itself on low nodes
|
||||
const nodePool = await SnodePool.getRandomSnodePool();
|
||||
if (nodePool.length < CONSTANTS.DESIRED_GUARD_COUNT) {
|
||||
if (nodePool.length < desiredGuardCount) {
|
||||
log.error('Could not select guard nodes. Not enough nodes in the pool: ', nodePool.length);
|
||||
return [];
|
||||
}
|
||||
|
@ -191,12 +193,12 @@ export class OnionPaths {
|
|||
// we only want to repeat if the await fails
|
||||
// eslint-disable-next-line-no-await-in-loop
|
||||
while (guardNodes.length < 3) {
|
||||
if (shuffled.length < CONSTANTS.DESIRED_GUARD_COUNT) {
|
||||
if (shuffled.length < desiredGuardCount) {
|
||||
log.error('Not enought nodes in the pool');
|
||||
break;
|
||||
}
|
||||
|
||||
const candidateNodes = shuffled.splice(0, CONSTANTS.DESIRED_GUARD_COUNT);
|
||||
const candidateNodes = shuffled.splice(0, desiredGuardCount);
|
||||
|
||||
// Test all three nodes at once
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
@ -209,7 +211,7 @@ export class OnionPaths {
|
|||
guardNodes = _.concat(guardNodes, goodNodes);
|
||||
}
|
||||
|
||||
if (guardNodes.length < CONSTANTS.DESIRED_GUARD_COUNT) {
|
||||
if (guardNodes.length < desiredGuardCount) {
|
||||
log.error(`COULD NOT get enough guard nodes, only have: ${guardNodes.length}`);
|
||||
}
|
||||
|
||||
|
@ -223,7 +225,7 @@ export class OnionPaths {
|
|||
}
|
||||
|
||||
private async buildNewOnionPathsWorker() {
|
||||
const { CONSTANTS, log } = window;
|
||||
const { log } = window;
|
||||
|
||||
log.info('LokiSnodeAPI::buildNewOnionPaths - building new onion paths');
|
||||
|
||||
|
@ -250,7 +252,7 @@ export class OnionPaths {
|
|||
}
|
||||
|
||||
// If guard nodes is still empty (the old nodes are now invalid), select new ones:
|
||||
if (this.guardNodes.length < CONSTANTS.MIN_GUARD_COUNT) {
|
||||
if (this.guardNodes.length < minimumGuardCount) {
|
||||
// TODO: don't throw away potentially good guard nodes
|
||||
this.guardNodes = await this.selectGuardNodes();
|
||||
}
|
||||
|
@ -272,7 +274,7 @@ export class OnionPaths {
|
|||
const guards = _.shuffle(this.guardNodes);
|
||||
|
||||
// Create path for every guard node:
|
||||
const nodesNeededPerPaths = window.lokiFeatureFlags.onionRequestHops - 1;
|
||||
const nodesNeededPerPaths = OnionPaths.onionRequestHops - 1;
|
||||
|
||||
// Each path needs X (nodesNeededPerPaths) nodes in addition to the guard node:
|
||||
const maxPath = Math.floor(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import { Snode } from '../onions';
|
||||
import { SendParams, storeOnNode } from '../snode_api/serviceNodeAPI';
|
||||
import { getSnodesFor } from '../snode_api/snodePool';
|
||||
import { getSwarm } from '../snode_api/snodePool';
|
||||
import { firstTrue } from '../utils/Promise';
|
||||
|
||||
const DEFAULT_CONNECTIONS = 3;
|
||||
|
@ -46,7 +46,7 @@ export async function sendMessage(
|
|||
const data64 = window.dcodeIO.ByteBuffer.wrap(data).toString('base64');
|
||||
|
||||
// Using timestamp as a unique identifier
|
||||
const swarm = await getSnodesFor(pubKey);
|
||||
const swarm = await getSwarm(pubKey);
|
||||
|
||||
// send parameters
|
||||
const params = {
|
||||
|
@ -62,7 +62,6 @@ export async function sendMessage(
|
|||
|
||||
let snode;
|
||||
try {
|
||||
// eslint-disable-next-line more/no-then
|
||||
snode = await firstTrue(promises);
|
||||
} catch (e) {
|
||||
const snodeStr = snode ? `${snode.ip}:${snode.port}` : 'null';
|
||||
|
|
|
@ -14,7 +14,7 @@ async function lokiFetch(
|
|||
url: string,
|
||||
options: FetchOptions,
|
||||
targetNode?: Snode
|
||||
): Promise<boolean | SnodeResponse> {
|
||||
): Promise<undefined | SnodeResponse> {
|
||||
const timeout = 10000;
|
||||
const method = options.method || 'GET';
|
||||
|
||||
|
@ -28,7 +28,11 @@ async function lokiFetch(
|
|||
// Absence of targetNode indicates that we want a direct connection
|
||||
// (e.g. to connect to a seed node for the first time)
|
||||
if (window.lokiFeatureFlags.useOnionRequests && targetNode) {
|
||||
return await lokiOnionFetch(fetchOptions.body, targetNode);
|
||||
const fetchResult = await lokiOnionFetch(fetchOptions.body, targetNode);
|
||||
if (!fetchResult) {
|
||||
return undefined;
|
||||
}
|
||||
return fetchResult;
|
||||
}
|
||||
|
||||
if (url.match(/https:\/\//)) {
|
||||
|
@ -62,7 +66,7 @@ export async function snodeRpc(
|
|||
method: string,
|
||||
params: any,
|
||||
targetNode: Snode
|
||||
): Promise<boolean | SnodeResponse> {
|
||||
): Promise<undefined | SnodeResponse> {
|
||||
const url = `https://${targetNode.ip}:${targetNode.port}/storage_rpc/v1`;
|
||||
|
||||
// TODO: The jsonrpc and body field will be ignored on storage server
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { default as insecureNodeFetch } from 'node-fetch';
|
||||
import { default as insecureNodeFetch, Response } from 'node-fetch';
|
||||
import https from 'https';
|
||||
|
||||
import { Snode } from './snodePool';
|
||||
import ByteBuffer from 'bytebuffer';
|
||||
import { StringUtils } from '../utils';
|
||||
import { OnionPaths } from '../onions';
|
||||
import { fromBase64ToArrayBuffer, toHex } from '../utils/String';
|
||||
|
||||
|
@ -190,7 +189,7 @@ async function buildOnionGuardNodePayload(
|
|||
// May return false BAD_PATH, indicating that we should try a new path.
|
||||
const processOnionResponse = async (
|
||||
reqIdx: number,
|
||||
response: any,
|
||||
response: Response,
|
||||
symmetricKey: ArrayBuffer,
|
||||
debug: boolean,
|
||||
abortSignal?: AbortSignal
|
||||
|
@ -231,6 +230,7 @@ const processOnionResponse = async (
|
|||
|
||||
if (response.status !== 200) {
|
||||
const rsp = await response.text();
|
||||
|
||||
log.warn(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - fetch unhandled error code: ${response.status}: ${rsp}`
|
||||
);
|
||||
|
@ -241,7 +241,7 @@ const processOnionResponse = async (
|
|||
return RequestError.BAD_PATH;
|
||||
}
|
||||
|
||||
let ciphertext = (await response.text()) as string;
|
||||
let ciphertext = await response.text();
|
||||
if (!ciphertext) {
|
||||
log.warn(
|
||||
`(${reqIdx}) [path] lokiRpc::processOnionResponse - Target node return empty ciphertext`
|
||||
|
@ -492,10 +492,7 @@ function getPathString(pathObjArr: Array<any>): string {
|
|||
return pathObjArr.map(node => `${node.ip}:${node.port}`).join(', ');
|
||||
}
|
||||
|
||||
export async function lokiOnionFetch(
|
||||
body: any,
|
||||
targetNode: Snode
|
||||
): Promise<SnodeResponse | boolean> {
|
||||
export async function lokiOnionFetch(body: any, targetNode: Snode): Promise<SnodeResponse | false> {
|
||||
const { log } = window;
|
||||
|
||||
// Loop until the result is not BAD_PATH
|
||||
|
|
|
@ -15,59 +15,22 @@ import { sendOnionRequestLsrpcDest, snodeHttpsAgent, SnodeResponse } from './oni
|
|||
|
||||
export { sendOnionRequestLsrpcDest };
|
||||
|
||||
import { getRandomSnodeAddress, markNodeUnreachable, Snode, updateSnodesFor } from './snodePool';
|
||||
import {
|
||||
getRandomSnodeAddress,
|
||||
getRandomSnodePool,
|
||||
getSwarm,
|
||||
markNodeUnreachable,
|
||||
requiredSnodesForAgreement,
|
||||
Snode,
|
||||
updateSnodesFor,
|
||||
} from './snodePool';
|
||||
import { Constants } from '..';
|
||||
import { sleepFor } from '../utils/Promise';
|
||||
import { sha256 } from '../crypto';
|
||||
import pRetry from 'p-retry';
|
||||
import _ from 'lodash';
|
||||
|
||||
/**
|
||||
* Currently unused. If we need it again, be sure to update it to onion routing rather
|
||||
* than using a plain nodeFetch
|
||||
*/
|
||||
export async function getVersion(node: Snode, retries: number = 0): Promise<string | boolean> {
|
||||
const SNODE_VERSION_RETRIES = 3;
|
||||
|
||||
const { log } = window;
|
||||
|
||||
try {
|
||||
window.log.warn('insecureNodeFetch => plaintext for getVersion');
|
||||
const result = await insecureNodeFetch(`https://${node.ip}:${node.port}/get_stats/v1`, {
|
||||
agent: snodeHttpsAgent,
|
||||
});
|
||||
const data = await result.json();
|
||||
if (data.version) {
|
||||
return data.version;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
// ECONNREFUSED likely means it's just offline...
|
||||
// ECONNRESET seems to retry and fail as ECONNREFUSED (so likely a node going offline)
|
||||
// ETIMEDOUT not sure what to do about these
|
||||
// retry for now but maybe we should be marking bad...
|
||||
if (e.code === 'ECONNREFUSED') {
|
||||
markNodeUnreachable(node);
|
||||
// clean up these error messages to be a little neater
|
||||
log.warn(`LokiSnodeAPI::_getVersion - ${node.ip}:${node.port} is offline, removing`);
|
||||
// if not ECONNREFUSED, it's mostly ECONNRESETs
|
||||
// ENOTFOUND could mean no internet or hiccup
|
||||
} else if (retries < SNODE_VERSION_RETRIES) {
|
||||
log.warn(
|
||||
'LokiSnodeAPI::_getVersion - Error',
|
||||
e.code,
|
||||
e.message,
|
||||
`on ${node.ip}:${node.port} retrying in 1s`
|
||||
);
|
||||
await sleepFor(1000);
|
||||
return getVersion(node, retries + 1);
|
||||
} else {
|
||||
markNodeUnreachable(node);
|
||||
log.warn(`LokiSnodeAPI::_getVersion - failing to get version for ${node.ip}:${node.port}`);
|
||||
}
|
||||
// maybe throw?
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const maxAcceptableFailuresStoreOnNode = 10;
|
||||
|
||||
const getSslAgentForSeedNode = (seedNodeHost: string, isSsl = false) => {
|
||||
let filePrefix = '';
|
||||
|
@ -235,39 +198,37 @@ export type SendParams = {
|
|||
export async function requestSnodesForPubkey(pubKey: string): Promise<Array<Snode>> {
|
||||
const { log } = window;
|
||||
|
||||
let snode;
|
||||
let targetNode;
|
||||
try {
|
||||
snode = await getRandomSnodeAddress();
|
||||
targetNode = await getRandomSnodeAddress();
|
||||
const result = await snodeRpc(
|
||||
'get_snodes_for_pubkey',
|
||||
{
|
||||
pubKey,
|
||||
},
|
||||
snode
|
||||
targetNode
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
log.warn(
|
||||
`LokiSnodeAPI::requestSnodesForPubkey - lokiRpc on ${snode.ip}:${snode.port} returned falsish value`,
|
||||
`LokiSnodeAPI::requestSnodesForPubkey - lokiRpc on ${targetNode.ip}:${targetNode.port} returned falsish value`,
|
||||
result
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const res = result as SnodeResponse;
|
||||
|
||||
if (res.status !== 200) {
|
||||
if (result.status !== 200) {
|
||||
log.warn('Status is not 200 for get_snodes_for_pubkey');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(res.body);
|
||||
const json = JSON.parse(result.body);
|
||||
|
||||
if (!json.snodes) {
|
||||
// we hit this when snode gives 500s
|
||||
log.warn(
|
||||
`LokiSnodeAPI::requestSnodesForPubkey - lokiRpc on ${snode.ip}:${snode.port} returned falsish value for snodes`,
|
||||
`LokiSnodeAPI::requestSnodesForPubkey - lokiRpc on ${targetNode.ip}:${targetNode.port} returned falsish value for snodes`,
|
||||
result
|
||||
);
|
||||
return [];
|
||||
|
@ -282,19 +243,18 @@ export async function requestSnodesForPubkey(pubKey: string): Promise<Array<Snod
|
|||
} catch (e) {
|
||||
log.error('LokiSnodeAPI::requestSnodesForPubkey - error', e.code, e.message);
|
||||
|
||||
if (snode) {
|
||||
markNodeUnreachable(snode);
|
||||
if (targetNode) {
|
||||
markNodeUnreachable(targetNode);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestLnsMapping(node: Snode, nameHash: any) {
|
||||
export async function requestLnsMapping(targetNode: Snode, nameHash: any) {
|
||||
const { log } = window;
|
||||
|
||||
log.debug('[lns] lns requests to {}:{}', node.ip, node.port);
|
||||
|
||||
log.debug('[lns] lns requests to {}:{}', targetNode.ip, targetNode);
|
||||
try {
|
||||
// TODO: Check response status
|
||||
return snodeRpc(
|
||||
|
@ -302,28 +262,134 @@ export async function requestLnsMapping(node: Snode, nameHash: any) {
|
|||
{
|
||||
name_hash: nameHash,
|
||||
},
|
||||
node
|
||||
targetNode
|
||||
);
|
||||
} catch (e) {
|
||||
log.warn('exception caught making lns requests to a node', node, e);
|
||||
log.warn('exception caught making lns requests to a node', targetNode, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function checkResponse(response: SnodeResponse): void {
|
||||
const { log, textsecure } = window;
|
||||
|
||||
if (response.status === 406) {
|
||||
throw new textsecure.TimestampError('Invalid Timestamp (check your clock)');
|
||||
/**
|
||||
* Try to fetch from 3 different snodes an updated list of snodes.
|
||||
* If we get less than 24 common snodes in those result, we consider the request to failed and an exception is thrown.
|
||||
* Return the list of nodes all snodes agreed on.
|
||||
*/
|
||||
export async function getSnodePoolFromSnodes() {
|
||||
const existingSnodePool = await getRandomSnodePool();
|
||||
if (existingSnodePool.length < 3) {
|
||||
window.log.warn('cannot get snodes from snodes; not enough snodes', existingSnodePool.length);
|
||||
return;
|
||||
}
|
||||
|
||||
const json = JSON.parse(response.body);
|
||||
// Note intersectionWith only works with 3 at most array to find the common snodes.
|
||||
const nodesToRequest = _.sampleSize(existingSnodePool, 3);
|
||||
const results = await Promise.all(
|
||||
nodesToRequest.map(async node => {
|
||||
return pRetry(
|
||||
async () => {
|
||||
return getSnodePoolFromSnode(node);
|
||||
},
|
||||
{
|
||||
retries: 3,
|
||||
factor: 1,
|
||||
minTimeout: 1000,
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// Wrong swarm
|
||||
// we want those at least `requiredSnodesForAgreement` snodes common between all the result
|
||||
const commonSnodes = _.intersectionWith(
|
||||
results[0],
|
||||
results[1],
|
||||
results[2],
|
||||
(s1: Snode, s2: Snode) => {
|
||||
return s1.ip === s2.ip && s1.port === s2.port;
|
||||
}
|
||||
);
|
||||
// We want the snodes to agree on at least this many snodes
|
||||
if (commonSnodes.length < requiredSnodesForAgreement) {
|
||||
throw new Error('inconsistentSnodePools');
|
||||
}
|
||||
return commonSnodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of uniq snodes got from the specified targetNode
|
||||
*/
|
||||
async function getSnodePoolFromSnode(targetNode: Snode): Promise<Array<Snode>> {
|
||||
const params = {
|
||||
endpoint: 'get_service_nodes',
|
||||
params: {
|
||||
active_only: true,
|
||||
// limit: 256,
|
||||
fields: {
|
||||
public_ip: true,
|
||||
storage_port: true,
|
||||
pubkey_x25519: true,
|
||||
pubkey_ed25519: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const method = 'oxend_request';
|
||||
const result = await snodeRpc(method, params, targetNode);
|
||||
if (!result || result.status !== 200) {
|
||||
throw new Error('Invalid result');
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(result.body);
|
||||
|
||||
if (!json || !json.result || !json.result.service_node_states?.length) {
|
||||
window.log.error(
|
||||
'loki_snode_api:::getSnodePoolFromSnode - invalid result from seed',
|
||||
result.body
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter 0.0.0.0 nodes which haven't submitted uptime proofs
|
||||
const snodes = json.result.service_node_states
|
||||
.filter((snode: any) => snode.public_ip !== '0.0.0.0')
|
||||
.map((snode: any) => ({
|
||||
ip: snode.public_ip,
|
||||
port: snode.storage_port,
|
||||
pubkey_x25519: snode.pubkey_x25519,
|
||||
pubkey_ed25519: snode.pubkey_ed25519,
|
||||
version: '',
|
||||
})) as Array<Snode>;
|
||||
|
||||
// we the return list by the snode is already made of uniq snodes
|
||||
return _.compact(snodes);
|
||||
} catch (e) {
|
||||
window.log.error('Invalid json response');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function checkResponse(response: SnodeResponse): void {
|
||||
if (response.status === 406) {
|
||||
throw new window.textsecure.TimestampError('Invalid Timestamp (check your clock)');
|
||||
}
|
||||
|
||||
// Wrong/invalid swarm
|
||||
if (response.status === 421) {
|
||||
log.warn('Wrong swarm, now looking at snodes', json.snodes);
|
||||
const newSwarm = json.snodes ? json.snodes : [];
|
||||
throw new textsecure.WrongSwarmError(newSwarm);
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(response.body);
|
||||
} catch (e) {
|
||||
// could not parse result. Consider that snode as invalid
|
||||
throw new window.textsecure.InvalidateSwarm();
|
||||
}
|
||||
|
||||
// The snode isn't associated with the given public key anymore
|
||||
window.log.warn('Wrong swarm, now looking at snodes', json.snodes);
|
||||
if (json.snodes?.length) {
|
||||
throw new window.textsecure.WrongSwarmError(json.snodes);
|
||||
}
|
||||
// remove this node from the swarm of this pubkey
|
||||
throw new window.textsecure.InvalidateSwarm();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -331,7 +397,8 @@ export async function storeOnNode(targetNode: Snode, params: SendParams): Promis
|
|||
const { log, textsecure } = window;
|
||||
|
||||
let successiveFailures = 0;
|
||||
while (successiveFailures < MAX_ACCEPTABLE_FAILURES) {
|
||||
|
||||
while (successiveFailures < maxAcceptableFailuresStoreOnNode) {
|
||||
// the higher this is, the longer the user delay is
|
||||
// we don't want to burn through all our retries quickly
|
||||
// we need to give the node a chance to heal
|
||||
|
@ -343,17 +410,17 @@ export async function storeOnNode(targetNode: Snode, params: SendParams): Promis
|
|||
const result = await snodeRpc('store', params, targetNode);
|
||||
|
||||
// do not return true if we get false here...
|
||||
if (result === false) {
|
||||
if (!result) {
|
||||
// this means the node we asked for is likely down
|
||||
log.warn(
|
||||
`loki_message:::storeOnNode - Try #${successiveFailures}/${MAX_ACCEPTABLE_FAILURES} ${targetNode.ip}:${targetNode.port} failed`
|
||||
`loki_message:::storeOnNode - Try #${successiveFailures}/${maxAcceptableFailuresStoreOnNode} ${targetNode.ip}:${targetNode.port} failed`
|
||||
);
|
||||
successiveFailures += 1;
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const snodeRes = result as SnodeResponse;
|
||||
const snodeRes = result;
|
||||
|
||||
checkResponse(snodeRes);
|
||||
|
||||
|
@ -382,6 +449,16 @@ export async function storeOnNode(targetNode: Snode, params: SendParams): Promis
|
|||
// TODO: Handle working connection but error response
|
||||
const body = await e.response.text();
|
||||
log.warn('loki_message:::storeOnNode - HTTPError body:', body);
|
||||
} else if (e instanceof window.textsecure.InvalidateSwarm) {
|
||||
window.log.warn(
|
||||
'Got an `InvalidateSwarm` error, removing this node from this swarm of this pubkey'
|
||||
);
|
||||
const existingSwarm = await getSwarm(params.pubKey);
|
||||
const updatedSwarm = existingSwarm.filter(
|
||||
node => node.pubkey_ed25519 !== targetNode.pubkey_ed25519
|
||||
);
|
||||
|
||||
await updateSnodesFor(params.pubKey, updatedSwarm);
|
||||
}
|
||||
successiveFailures += 1;
|
||||
}
|
||||
|
@ -394,7 +471,7 @@ export async function storeOnNode(targetNode: Snode, params: SendParams): Promis
|
|||
}
|
||||
|
||||
export async function retrieveNextMessages(
|
||||
nodeData: Snode,
|
||||
targetNode: Snode,
|
||||
lastHash: string,
|
||||
pubkey: string
|
||||
): Promise<Array<any>> {
|
||||
|
@ -404,36 +481,42 @@ export async function retrieveNextMessages(
|
|||
};
|
||||
|
||||
// let exceptions bubble up
|
||||
const result = await snodeRpc('retrieve', params, nodeData);
|
||||
const result = await snodeRpc('retrieve', params, targetNode);
|
||||
|
||||
if (!result) {
|
||||
window.log.warn(
|
||||
`loki_message:::_retrieveNextMessages - lokiRpc could not talk to ${nodeData.ip}:${nodeData.port}`
|
||||
`loki_message:::_retrieveNextMessages - lokiRpc could not talk to ${targetNode.ip}:${targetNode.port}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const res = result as SnodeResponse;
|
||||
|
||||
// NOTE: we call `checkResponse` to check for "wrong swarm"
|
||||
try {
|
||||
checkResponse(res);
|
||||
checkResponse(result);
|
||||
} catch (e) {
|
||||
window.log.warn('loki_message:::retrieveNextMessages - send error:', e.code, e.message);
|
||||
if (e instanceof window.textsecure.WrongSwarmError) {
|
||||
const { newSwarm } = e;
|
||||
await updateSnodesFor(params.pubKey, newSwarm);
|
||||
return [];
|
||||
} else if (e instanceof window.textsecure.InvalidateSwarm) {
|
||||
const existingSwarm = await getSwarm(params.pubKey);
|
||||
const updatedSwarm = existingSwarm.filter(
|
||||
node => node.pubkey_ed25519 !== targetNode.pubkey_ed25519
|
||||
);
|
||||
|
||||
await updateSnodesFor(params.pubKey, updatedSwarm);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (res.status !== 200) {
|
||||
if (result.status !== 200) {
|
||||
window.log('retrieve result is not 200');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(res.body);
|
||||
const json = JSON.parse(result.body);
|
||||
return json.messages || [];
|
||||
} catch (e) {
|
||||
window.log.warn('exception while parsing json of nextMessage:', e);
|
||||
|
@ -441,5 +524,3 @@ export async function retrieveNextMessages(
|
|||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_ACCEPTABLE_FAILURES = 10;
|
||||
|
|
|
@ -1,14 +1,36 @@
|
|||
import semver from 'semver';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { getSnodesFromSeedUrl, requestSnodesForPubkey } from './serviceNodeAPI';
|
||||
import {
|
||||
getSnodePoolFromSnodes,
|
||||
getSnodesFromSeedUrl,
|
||||
requestSnodesForPubkey,
|
||||
} from './serviceNodeAPI';
|
||||
|
||||
import { getSwarmNodesForPubkey, updateSwarmNodesForPubkey } from '../../../ts/data/data';
|
||||
import * as Data from '../../../ts/data/data';
|
||||
|
||||
export type SnodeEdKey = string;
|
||||
import { allowOnlyOneAtATime } from '../utils/Promise';
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const MIN_NODES = 3;
|
||||
/**
|
||||
* If we get less than this snode in a swarm, we fetch new snodes for this pubkey
|
||||
*/
|
||||
const minSwarmSnodeCount = 3;
|
||||
|
||||
/**
|
||||
* If we get less than minSnodePoolCount we consider that we need to fetch the new snode pool from a seed node
|
||||
* and not from those snodes.
|
||||
*/
|
||||
const minSnodePoolCount = 12;
|
||||
|
||||
/**
|
||||
* If we do a request to fetch nodes from snodes and they don't return at least
|
||||
* the same `requiredSnodesForAgreement` snodes we consider that this is not a valid return.
|
||||
*
|
||||
* Too many nodes are not shared for this call to be trustworthy
|
||||
*/
|
||||
export const requiredSnodesForAgreement = 24;
|
||||
|
||||
export interface Snode {
|
||||
ip: string;
|
||||
|
@ -24,10 +46,13 @@ let randomSnodePool: Array<Snode> = [];
|
|||
// We only store nodes' identifiers here,
|
||||
const nodesForPubkey: Map<string, Array<SnodeEdKey>> = new Map();
|
||||
|
||||
export type SeedNode = {
|
||||
url: string;
|
||||
ip_url: string;
|
||||
};
|
||||
|
||||
// just get the filtered list
|
||||
async function tryGetSnodeListFromLokidSeednode(
|
||||
seedNodes = window.seedNodeList
|
||||
): Promise<Array<Snode>> {
|
||||
async function tryGetSnodeListFromLokidSeednode(seedNodes: Array<SeedNode>): Promise<Array<Snode>> {
|
||||
const { log } = window;
|
||||
|
||||
if (!seedNodes.length) {
|
||||
|
@ -106,7 +131,7 @@ export async function getRandomSnodeAddress(): Promise<Snode> {
|
|||
if (randomSnodePool.length === 0) {
|
||||
// TODO: ensure that we only call this once at a time
|
||||
// Should not this be saved to the database?
|
||||
await refreshRandomPool([]);
|
||||
await refreshRandomPool();
|
||||
|
||||
if (randomSnodePool.length === 0) {
|
||||
throw new window.textsecure.SeedNodeError('Invalid seed node response');
|
||||
|
@ -117,51 +142,19 @@ export async function getRandomSnodeAddress(): Promise<Snode> {
|
|||
return _.sample(randomSnodePool) as Snode;
|
||||
}
|
||||
|
||||
function compareSnodes(lhs: any, rhs: any): boolean {
|
||||
return lhs.pubkey_ed25519 === rhs.pubkey_ed25519;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the version of the snode.
|
||||
* THIS IS AN INSECURE NODE FETCH and leaks our IP to all snodes but with no other identifying information
|
||||
* except "that a client started up" or "ran out of random pool snodes"
|
||||
* and the order of the list is randomized, so a snode can't tell if it just started or not
|
||||
*/
|
||||
async function requestVersion(node: any): Promise<void> {
|
||||
const { log } = window;
|
||||
|
||||
// WARNING: getVersion is doing an insecure node fetch.
|
||||
// be sure to update getVersion to onion routing if we need this call again.
|
||||
const result = false; // await getVersion(node);
|
||||
|
||||
if (result === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const version = result as string;
|
||||
|
||||
const foundNodeIdx = randomSnodePool.findIndex((n: any) => compareSnodes(n, node));
|
||||
if (foundNodeIdx !== -1) {
|
||||
randomSnodePool[foundNodeIdx].version = version;
|
||||
} else {
|
||||
// maybe already marked bad...
|
||||
log.debug(`LokiSnodeAPI::_getVersion - can't find ${node.ip}:${node.port} in randomSnodePool`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function force the snode poll to be refreshed from a random seed node again.
|
||||
* This should be called once in a day or so for when the app it kept on.
|
||||
*/
|
||||
export async function forceRefreshRandomSnodePool(): Promise<Array<Snode>> {
|
||||
await refreshRandomPool([]);
|
||||
await refreshRandomPool();
|
||||
|
||||
return randomSnodePool;
|
||||
}
|
||||
|
||||
export async function getRandomSnodePool(): Promise<Array<Snode>> {
|
||||
if (randomSnodePool.length === 0) {
|
||||
await refreshRandomPool([]);
|
||||
await refreshRandomPool();
|
||||
}
|
||||
return randomSnodePool;
|
||||
}
|
||||
|
@ -172,7 +165,7 @@ export function getNodesMinVersion(minVersion: string): Array<Snode> {
|
|||
}
|
||||
|
||||
async function getSnodeListFromLokidSeednode(
|
||||
seedNodes = window.seedNodeList,
|
||||
seedNodes: Array<SeedNode>,
|
||||
retries = 0
|
||||
): Promise<Array<Snode>> {
|
||||
const SEED_NODE_RETRIES = 3;
|
||||
|
@ -207,7 +200,11 @@ async function getSnodeListFromLokidSeednode(
|
|||
return snodes;
|
||||
}
|
||||
|
||||
async function refreshRandomPoolDetail(seedNodes: Array<any>): Promise<void> {
|
||||
/**
|
||||
* Fetch all snodes from a seed nodes if we don't have enough snodes to make the request ourself
|
||||
* @param seedNodes the seednodes to use to fetch snodes details
|
||||
*/
|
||||
async function refreshRandomPoolDetail(seedNodes: Array<SeedNode>): Promise<void> {
|
||||
const { log } = window;
|
||||
|
||||
let snodes = [];
|
||||
|
@ -241,21 +238,51 @@ async function refreshRandomPoolDetail(seedNodes: Array<any>): Promise<void> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshRandomPool(seedNodes?: Array<any>): Promise<void> {
|
||||
/**
|
||||
* This function runs only once at a time, and fetches the snode pool from a random seed node,
|
||||
* or if we have enough snodes, fetches the snode pool from one of the snode.
|
||||
*/
|
||||
export async function refreshRandomPool(): Promise<void> {
|
||||
const { log } = window;
|
||||
|
||||
if (!seedNodes || !seedNodes.length) {
|
||||
if (!window.seedNodeList || !window.seedNodeList.length) {
|
||||
log.error('LokiSnodeAPI:::refreshRandomPool - seedNodeList has not been loaded yet');
|
||||
return;
|
||||
}
|
||||
// tslint:disable-next-line:no-parameter-reassignment
|
||||
seedNodes = window.seedNodeList;
|
||||
if (!window.seedNodeList || !window.seedNodeList.length) {
|
||||
log.error('LokiSnodeAPI:::refreshRandomPool - seedNodeList has not been loaded yet');
|
||||
return;
|
||||
}
|
||||
// tslint:disable-next-line:no-parameter-reassignment
|
||||
const seedNodes = window.seedNodeList;
|
||||
|
||||
return allowOnlyOneAtATime('refreshRandomPool', async () => {
|
||||
if (seedNodes) {
|
||||
// we don't have nodes to fetch the pool from them, so call the seed node instead.
|
||||
if (randomSnodePool.length < minSnodePoolCount) {
|
||||
await refreshRandomPoolDetail(seedNodes);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// let this request try 3 (2+1) times. If all those requests end up without having a consensus,
|
||||
// fetch the snode pool from one of the seed nodes (see the catch).
|
||||
await pRetry(
|
||||
async () => {
|
||||
const commonNodes = await getSnodePoolFromSnodes();
|
||||
if (!commonNodes || commonNodes.length < requiredSnodesForAgreement) {
|
||||
// throwing makes trigger a retry if we have some left.
|
||||
throw new Error('Not enough common nodes.');
|
||||
}
|
||||
window.log.info('updating snode list with snode pool length:', commonNodes.length);
|
||||
randomSnodePool = commonNodes;
|
||||
},
|
||||
{
|
||||
retries: 2,
|
||||
factor: 1,
|
||||
minTimeout: 1000,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
window.log.warn(
|
||||
'Failed to fetch snode pool from snodes. Fetching from seed node instead:',
|
||||
e
|
||||
);
|
||||
// fallback to a seed node fetch of the snode pool
|
||||
await refreshRandomPoolDetail(seedNodes);
|
||||
}
|
||||
});
|
||||
|
@ -267,18 +294,20 @@ export async function updateSnodesFor(pubkey: string, snodes: Array<Snode>): Pro
|
|||
}
|
||||
|
||||
async function internalUpdateSnodesFor(pubkey: string, edkeys: Array<string>) {
|
||||
// update our in-memory cache
|
||||
nodesForPubkey.set(pubkey, edkeys);
|
||||
await updateSwarmNodesForPubkey(pubkey, edkeys);
|
||||
// write this change to the db
|
||||
await Data.updateSwarmNodesForPubkey(pubkey, edkeys);
|
||||
}
|
||||
|
||||
export async function getSnodesFor(pubkey: string): Promise<Array<Snode>> {
|
||||
export async function getSwarm(pubkey: string): Promise<Array<Snode>> {
|
||||
const maybeNodes = nodesForPubkey.get(pubkey);
|
||||
let nodes: Array<string>;
|
||||
|
||||
// NOTE: important that maybeNodes is not [] here
|
||||
if (maybeNodes === undefined) {
|
||||
// First time access, try the database:
|
||||
nodes = await getSwarmNodesForPubkey(pubkey);
|
||||
// First time access, no cache yet, let's try the database.
|
||||
nodes = await Data.getSwarmNodesForPubkey(pubkey);
|
||||
nodesForPubkey.set(pubkey, nodes);
|
||||
} else {
|
||||
nodes = maybeNodes;
|
||||
|
@ -287,13 +316,12 @@ export async function getSnodesFor(pubkey: string): Promise<Array<Snode>> {
|
|||
// See how many are actually still reachable
|
||||
const goodNodes = randomSnodePool.filter((n: Snode) => nodes.indexOf(n.pubkey_ed25519) !== -1);
|
||||
|
||||
if (goodNodes.length < MIN_NODES) {
|
||||
if (goodNodes.length < minSwarmSnodeCount) {
|
||||
// Request new node list from the network
|
||||
const freshNodes = _.shuffle(await requestSnodesForPubkey(pubkey));
|
||||
|
||||
const edkeys = freshNodes.map((n: Snode) => n.pubkey_ed25519);
|
||||
void internalUpdateSnodesFor(pubkey, edkeys);
|
||||
// TODO: We could probably check that the retuned sndoes are not "unreachable"
|
||||
await internalUpdateSnodesFor(pubkey, edkeys);
|
||||
|
||||
return freshNodes;
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PubKey } from '../types';
|
||||
import { getSnodesFor, Snode } from './snodePool';
|
||||
import { getSwarm, Snode } from './snodePool';
|
||||
import { retrieveNextMessages } from './serviceNodeAPI';
|
||||
import { SignalService } from '../../protobuf';
|
||||
import * as Receiver from '../../receiver/receiver';
|
||||
|
@ -91,7 +91,7 @@ export class SwarmPolling {
|
|||
// accept both until this is fixed:
|
||||
const pkStr = pubkey.key;
|
||||
|
||||
const snodes = await getSnodesFor(pkStr);
|
||||
const snodes = await getSwarm(pkStr);
|
||||
|
||||
// Select nodes for which we already have lastHashes
|
||||
const alreadyPolled = snodes.filter((n: Snode) => this.lastHashes[n.pubkey_ed25519]);
|
||||
|
|
|
@ -65,6 +65,11 @@ export const forceSyncConfigurationNowIfNeeded = async (waitForMessageSent = fal
|
|||
new Promise(resolve => {
|
||||
const allConvos = ConversationController.getInstance().getConversations();
|
||||
|
||||
// if we hang for more than 10sec, force resolve this promise.
|
||||
setTimeout(() => {
|
||||
resolve(false);
|
||||
}, 10000);
|
||||
|
||||
void getCurrentConfigurationMessage(allConvos)
|
||||
.then(configMessage => {
|
||||
// this just adds the message to the sending queue.
|
||||
|
|
|
@ -55,7 +55,6 @@ declare global {
|
|||
useOnionRequests: boolean;
|
||||
useFileOnionRequests: boolean;
|
||||
useFileOnionRequestsV2: boolean;
|
||||
onionRequestHops: number;
|
||||
useRequestEncryptionKeyPair: boolean;
|
||||
padOutgoingAttachments: boolean;
|
||||
};
|
||||
|
@ -82,9 +81,6 @@ declare global {
|
|||
versionInfo: any;
|
||||
getStoragePubKey: (key: string) => string;
|
||||
getConversations: () => ConversationCollection;
|
||||
SnodePool: {
|
||||
getSnodesFor: (string) => any;
|
||||
};
|
||||
profileImages: any;
|
||||
MediaRecorder: any;
|
||||
dataURLToBlobSync: any;
|
||||
|
|
Loading…
Reference in New Issue