You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			217 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			217 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			TypeScript
		
	
| import { default as insecureNodeFetch } from 'node-fetch';
 | |
| import { OpenGroupV2Room } from '../../data/opengroups';
 | |
| import { sendViaOnion } from '../../session/onions/onionSend';
 | |
| import { toHex } from '../../session/utils/String';
 | |
| 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}`;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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();
 | |
|       const pubKeyHex = toHex(pubKey);
 | |
|       // 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(pubKeyHex, 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);
 | |
| }
 |