|
|
|
import { default as insecureNodeFetch } from 'node-fetch';
|
|
|
|
import { OpenGroupV2Room } from '../../data/opengroups';
|
|
|
|
import { sendViaOnion } from '../../session/onions/onionSend';
|
|
|
|
import { OpenGroupRequestCommonType } from '../opengroupV2/ApiUtil';
|
|
|
|
|
|
|
|
const protocolRegex = new RegExp('(https?://)?');
|
|
|
|
|
|
|
|
const dot = '\\.';
|
|
|
|
const qMark = '\\?';
|
|
|
|
const hostnameRegex = new RegExp(
|
|
|
|
`(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])${dot})*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])`
|
|
|
|
);
|
|
|
|
const portRegex = '(:[0-9]+)?';
|
|
|
|
|
|
|
|
// roomIds allows between 2 and 64 of '0-9' or 'a-z' or '_' chars
|
|
|
|
export const roomIdV2Regex = '[0-9a-z_]{2,64}';
|
|
|
|
export const publicKeyRegex = '[0-9a-z]{64}';
|
|
|
|
export const publicKeyParam = 'public_key=';
|
|
|
|
export const openGroupV2ServerUrlRegex = new RegExp(
|
|
|
|
`${protocolRegex.source}${hostnameRegex.source}${portRegex}`
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Regex to use to check if a string is a v2open completeURL with pubkey.
|
|
|
|
* Be aware that the /g flag is not set as .test() will otherwise return alternating result
|
|
|
|
*
|
|
|
|
* @see https://stackoverflow.com/a/9275499/1680951
|
|
|
|
*/
|
|
|
|
export const openGroupV2CompleteURLRegex = new RegExp(
|
|
|
|
`${openGroupV2ServerUrlRegex.source}\/${roomIdV2Regex}${qMark}${publicKeyParam}${publicKeyRegex}`,
|
|
|
|
'm'
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Just a constant to have less `publicChat:` everywhere.
|
|
|
|
* This is the prefix used to identify our open groups in the conversation database (v1 or v2)
|
|
|
|
* Note: It does already have the ':' included
|
|
|
|
*/
|
|
|
|
export const openGroupPrefix = 'publicChat:';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Just a regex to match a public chat (i.e. a string starting with publicChat:)
|
|
|
|
*/
|
|
|
|
export const openGroupPrefixRegex = new RegExp(`^${openGroupPrefix}`);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An open group v1 conversation id can only have the char '1' as roomId
|
|
|
|
*/
|
|
|
|
export const openGroupV1ConversationIdRegex = new RegExp(
|
|
|
|
`${openGroupPrefix}1@${protocolRegex.source}${hostnameRegex.source}`
|
|
|
|
);
|
|
|
|
|
|
|
|
export const openGroupV2ConversationIdRegex = new RegExp(
|
|
|
|
`${openGroupPrefix}${roomIdV2Regex}@${protocolRegex.source}${hostnameRegex.source}${portRegex}`
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This function returns a full url on an open group v2 room used for sync messages for instance.
|
|
|
|
* This is basically what the QRcode encodes
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
export function getCompleteUrlFromRoom(roomInfos: OpenGroupV2Room) {
|
|
|
|
// serverUrl has the port and protocol already
|
|
|
|
return `${roomInfos.serverUrl}/${roomInfos.roomId}?${publicKeyParam}${roomInfos.serverPublicKey}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This function returns a base url to this room
|
|
|
|
* This is basically used for building url after posting an attachment
|
|
|
|
* hasRoomInEndpoint = true means the roomId is already in the endpoint.
|
|
|
|
* so we don't add the room after the serverUrl.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
export function getCompleteEndpointUrl(
|
|
|
|
roomInfos: OpenGroupRequestCommonType,
|
|
|
|
endpoint: string,
|
|
|
|
hasRoomInEndpoint: boolean
|
|
|
|
) {
|
|
|
|
// serverUrl has the port and protocol already
|
|
|
|
if (!hasRoomInEndpoint) {
|
|
|
|
return `${roomInfos.serverUrl}/${roomInfos.roomId}/${endpoint}`;
|
|
|
|
}
|
|
|
|
// not room based, the endpoint already has the room in it
|
|
|
|
return `${roomInfos.serverUrl}/${endpoint}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tries to establish a connection with the specified open group url.
|
|
|
|
*
|
|
|
|
* This will try to do an onion routing call if the `useFileOnionRequests` feature flag is set,
|
|
|
|
* or call directly insecureNodeFetch if it's not.
|
|
|
|
*
|
|
|
|
* Returns
|
|
|
|
* * true if useFileOnionRequests is false and no exception where thrown by insecureNodeFetch
|
|
|
|
* * true if useFileOnionRequests is true and we established a connection to the server with onion routing
|
|
|
|
* * false otherwise
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
export const validOpenGroupServer = async (serverUrl: string) => {
|
|
|
|
// test to make sure it's online (and maybe has a valid SSL cert)
|
|
|
|
try {
|
|
|
|
const url = new URL(serverUrl);
|
|
|
|
|
|
|
|
if (!window.lokiFeatureFlags.useFileOnionRequests) {
|
|
|
|
// we are not running with onion request
|
|
|
|
// this is an insecure insecureNodeFetch. It will expose the user ip to the serverUrl (not onion routed)
|
|
|
|
window.log.info(`insecureNodeFetch => plaintext for ${url.toString()}`);
|
|
|
|
|
|
|
|
// we probably have to check the response here
|
|
|
|
await insecureNodeFetch(serverUrl);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// This MUST be an onion routing call, no nodeFetch calls below here.
|
|
|
|
|
|
|
|
/**
|
|
|
|
* this is safe (as long as node's in your trust model)
|
|
|
|
*
|
|
|
|
* First, we need to fetch the open group public key of this open group.
|
|
|
|
* The fileserver have all the open groups public keys.
|
|
|
|
* We need the open group public key because for onion routing we will need to encode
|
|
|
|
* our request with it.
|
|
|
|
* We can just ask the file-server to get the one for the open group we are trying to add.
|
|
|
|
*/
|
|
|
|
|
|
|
|
const result = await window.tokenlessFileServerAdnAPI.serverRequest(
|
|
|
|
`loki/v1/getOpenGroupKey/${url.hostname}`
|
|
|
|
);
|
|
|
|
|
|
|
|
if (result.response.meta.code === 200) {
|
|
|
|
// we got the public key of the server we are trying to add.
|
|
|
|
// decode it.
|
|
|
|
const obj = JSON.parse(result.response.data);
|
|
|
|
const pubKey = window.dcodeIO.ByteBuffer.wrap(obj.data, 'base64').toArrayBuffer();
|
|
|
|
// verify we can make an onion routed call to that open group with the decoded public key
|
|
|
|
// get around the FILESERVER_HOSTS filter by not using serverRequest
|
|
|
|
const res = await sendViaOnion(pubKey, url, { method: 'GET' }, { noJson: true });
|
|
|
|
if (res && res.result && res.result.status === 200) {
|
|
|
|
window.log.info(
|
|
|
|
`loki_public_chat::validOpenGroupServer - onion routing enabled on ${url.toString()}`
|
|
|
|
);
|
|
|
|
// save pubkey for use...
|
|
|
|
window.lokiPublicChatAPI.openGroupPubKeys[serverUrl] = pubKey;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
// return here, just so we are sure adding some code below won't do a nodeFetch fallback
|
|
|
|
return false;
|
|
|
|
} else if (result.response.meta.code !== 404) {
|
|
|
|
// unknown error code
|
|
|
|
window.log.warn(
|
|
|
|
'loki_public_chat::validOpenGroupServer - unknown error code',
|
|
|
|
result.response.meta
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
} catch (e) {
|
|
|
|
window.log.warn(
|
|
|
|
`loki_public_chat::validOpenGroupServer - failing to create ${serverUrl}`,
|
|
|
|
e.code,
|
|
|
|
e.message
|
|
|
|
);
|
|
|
|
// bail out if not valid enough
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prefix server with https:// if it's not already prefixed with http or https.
|
|
|
|
*/
|
|
|
|
export function prefixify(server: string, hasSSL: boolean = true): string {
|
|
|
|
const hasPrefix = server.match('^https?://');
|
|
|
|
if (hasPrefix) {
|
|
|
|
return server;
|
|
|
|
}
|
|
|
|
|
|
|
|
return `http${hasSSL ? 's' : ''}://${server}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* No sql access. Just how our open groupv2 url looks like.
|
|
|
|
* ServerUrl can have the protocol and port included, or not
|
|
|
|
* @returns `${openGroupPrefix}${roomId}@${serverUrl}`
|
|
|
|
*/
|
|
|
|
export function getOpenGroupV2ConversationId(serverUrl: string, roomId: string) {
|
|
|
|
if (!roomId.match(roomIdV2Regex)) {
|
|
|
|
throw new Error('getOpenGroupV2ConversationId: Invalid roomId');
|
|
|
|
}
|
|
|
|
if (!serverUrl.match(openGroupV2ServerUrlRegex)) {
|
|
|
|
throw new Error('getOpenGroupV2ConversationId: Invalid serverUrl');
|
|
|
|
}
|
|
|
|
return `${openGroupPrefix}${roomId}@${serverUrl}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* No sql access. Just plain string logic
|
|
|
|
*/
|
|
|
|
export function getOpenGroupV2FromConversationId(
|
|
|
|
conversationId: string
|
|
|
|
): OpenGroupRequestCommonType {
|
|
|
|
if (isOpenGroupV2(conversationId)) {
|
|
|
|
const atIndex = conversationId.indexOf('@');
|
|
|
|
const roomId = conversationId.slice(openGroupPrefix.length, atIndex);
|
|
|
|
const serverUrl = conversationId.slice(atIndex + 1);
|
|
|
|
return {
|
|
|
|
serverUrl,
|
|
|
|
roomId,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
throw new Error('Not a v2 open group convo id');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if this conversation id corresponds to an OpenGroupV1 conversation.
|
|
|
|
* No access to database are made. Only regex matches
|
|
|
|
* @param conversationId the convo id to evaluate
|
|
|
|
* @returns true if this conversation id matches the Opengroupv1 conversation id regex
|
|
|
|
*/
|
|
|
|
export function isOpenGroupV1(conversationId: string) {
|
|
|
|
return openGroupV1ConversationIdRegex.test(conversationId);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if this conversation id corresponds to an OpenGroupV2 conversation.
|
|
|
|
* No access to database are made. Only regex matches
|
|
|
|
* @param conversationId the convo id to evaluate
|
|
|
|
* @returns true if this conversation id matches the Opengroupv2 conversation id regex
|
|
|
|
*/
|
|
|
|
export function isOpenGroupV2(conversationId: string) {
|
|
|
|
if (openGroupV1ConversationIdRegex.test(conversationId)) {
|
|
|
|
// this is an open group v1
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return openGroupV2ConversationIdRegex.test(conversationId);
|
|
|
|
}
|