|  |  |  | 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); | 
					
						
							|  |  |  | } |