Merge pull request #1594 from Bilb/file-server-v2-receiving
File server v2 Attachments downloading, upload disabledpull/1597/head
						commit
						ecbd3b2c7c
					
				| @ -0,0 +1,131 @@ | ||||
| import { OpenGroupV2Request } from '../opengroup/opengroupV2/ApiUtil'; | ||||
| import { sendApiV2Request } from '../opengroup/opengroupV2/OpenGroupAPIV2'; | ||||
| import { parseStatusCodeFromOnionRequest } from '../opengroup/opengroupV2/OpenGroupAPIV2Parser'; | ||||
| import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String'; | ||||
| 
 | ||||
| // tslint:disable-next-line: no-http-string
 | ||||
| export const fileServerV2URL = 'http://88.99.175.227'; | ||||
| export const fileServerV2PubKey = | ||||
|   '7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69'; | ||||
| 
 | ||||
| export type FileServerV2Request = { | ||||
|   method: 'GET' | 'POST' | 'DELETE' | 'PUT'; | ||||
|   endpoint: string; | ||||
|   // queryParams are used for post or get, but not the same way
 | ||||
|   queryParams?: Record<string, any>; | ||||
|   headers?: Record<string, string>; | ||||
| }; | ||||
| 
 | ||||
| const FILES_ENDPOINT = 'files'; | ||||
| 
 | ||||
| // Disable this if you don't want to use the file server v2 for sending
 | ||||
| // Receiving is always enabled if the attachments url matches a fsv2 url
 | ||||
| export const useFileServerAPIV2Sending = false; | ||||
| 
 | ||||
| /** | ||||
|  * Upload a file to the file server v2 | ||||
|  * @param fileContent the data to send | ||||
|  * @returns null or the fileID and complete URL to share this file | ||||
|  */ | ||||
| export const uploadFileToFsV2 = async ( | ||||
|   fileContent: ArrayBuffer | ||||
| ): Promise<{ fileId: number; fileUrl: string } | null> => { | ||||
|   if (!fileContent || !fileContent.byteLength) { | ||||
|     return null; | ||||
|   } | ||||
|   const queryParams = { | ||||
|     file: fromArrayBufferToBase64(fileContent), | ||||
|   }; | ||||
| 
 | ||||
|   const request: FileServerV2Request = { | ||||
|     method: 'POST', | ||||
|     endpoint: FILES_ENDPOINT, | ||||
|     queryParams, | ||||
|   }; | ||||
| 
 | ||||
|   const result = await sendApiV2Request(request); | ||||
|   const statusCode = parseStatusCodeFromOnionRequest(result); | ||||
|   if (statusCode !== 200) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   // we should probably change the logic of sendOnionRequest to not have all those levels
 | ||||
|   const fileId = (result as any)?.result?.result as number | undefined; | ||||
|   if (!fileId) { | ||||
|     return null; | ||||
|   } | ||||
|   const fileUrl = `${fileServerV2URL}/${FILES_ENDPOINT}/${fileId}`; | ||||
|   return { | ||||
|     fileId: fileId, | ||||
|     fileUrl, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Download a file given the fileId from the fileserver v2 | ||||
|  * @param fileId the fileId to download | ||||
|  * @returns the data as an Uint8Array or null | ||||
|  */ | ||||
| export const downloadFileFromFSv2 = async (fileId: string): Promise<ArrayBuffer | null> => { | ||||
|   if (!fileId) { | ||||
|     window.log.warn(''); | ||||
|     return null; | ||||
|   } | ||||
|   const request: FileServerV2Request = { | ||||
|     method: 'GET', | ||||
|     endpoint: `${FILES_ENDPOINT}/${fileId}`, | ||||
|   }; | ||||
| 
 | ||||
|   const result = await sendApiV2Request(request); | ||||
|   const statusCode = parseStatusCodeFromOnionRequest(result); | ||||
|   if (statusCode !== 200) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   // we should probably change the logic of sendOnionRequest to not have all those levels
 | ||||
|   const base64Data = (result as any)?.result?.result as string | undefined; | ||||
| 
 | ||||
|   if (!base64Data) { | ||||
|     return null; | ||||
|   } | ||||
|   return fromBase64ToArrayBuffer(base64Data); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * This is a typescript type guard | ||||
|  * request.isAuthRequired Must be set for an OpenGroupV2Request | ||||
|  * @returns true if request.isAuthRequired is not undefined | ||||
|  */ | ||||
| export function isOpenGroupV2Request( | ||||
|   request: FileServerV2Request | OpenGroupV2Request | ||||
| ): request is OpenGroupV2Request { | ||||
|   return (request as OpenGroupV2Request).isAuthRequired !== undefined; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Try to build an full url and check it for validity. | ||||
|  * @returns null if the check failed. the built URL otherwise | ||||
|  */ | ||||
| export const buildUrl = (request: FileServerV2Request | OpenGroupV2Request): URL | null => { | ||||
|   let rawURL: string; | ||||
|   if (isOpenGroupV2Request(request)) { | ||||
|     rawURL = `${request.server}/${request.endpoint}`; | ||||
|   } else { | ||||
|     rawURL = `${fileServerV2URL}/${request.endpoint}`; | ||||
|   } | ||||
| 
 | ||||
|   if (request.method === 'GET') { | ||||
|     const entries = Object.entries(request.queryParams || {}); | ||||
| 
 | ||||
|     if (entries.length) { | ||||
|       const queryString = entries.map(([key, value]) => `${key}=${value}`).join('&'); | ||||
|       rawURL += `?${queryString}`; | ||||
|     } | ||||
|   } | ||||
|   // this just check that the URL is valid
 | ||||
|   try { | ||||
|     return new URL(`${rawURL}`); | ||||
|   } catch (error) { | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
| @ -0,0 +1,4 @@ | ||||
| import * as FSv2 from './FileServerApiV2'; | ||||
| 
 | ||||
| // fsv2 = File server V2
 | ||||
| export { FSv2 }; | ||||
| @ -0,0 +1,161 @@ | ||||
| import { getV2OpenGroupRoomByRoomId, saveV2OpenGroupRoom } from '../../data/opengroups'; | ||||
| import { allowOnlyOneAtATime } from '../../session/utils/Promise'; | ||||
| import { fromBase64ToArrayBuffer, toHex } from '../../session/utils/String'; | ||||
| import { getIdentityKeyPair, getOurPubKeyStrFromCache } from '../../session/utils/User'; | ||||
| import { OpenGroupRequestCommonType, OpenGroupV2Request } from './ApiUtil'; | ||||
| import { sendApiV2Request } from './OpenGroupAPIV2'; | ||||
| import { parseStatusCodeFromOnionRequest } from './OpenGroupAPIV2Parser'; | ||||
| 
 | ||||
| async function claimAuthToken( | ||||
|   authToken: string, | ||||
|   serverUrl: string, | ||||
|   roomId: string | ||||
| ): Promise<string | null> { | ||||
|   // Set explicitly here because is isn't in the database yet at this point
 | ||||
|   const headers = { Authorization: authToken }; | ||||
|   const request: OpenGroupV2Request = { | ||||
|     method: 'POST', | ||||
|     headers, | ||||
|     room: roomId, | ||||
|     server: serverUrl, | ||||
|     queryParams: { public_key: getOurPubKeyStrFromCache() }, | ||||
|     isAuthRequired: false, | ||||
|     endpoint: 'claim_auth_token', | ||||
|   }; | ||||
|   const result = await sendApiV2Request(request); | ||||
|   const statusCode = parseStatusCodeFromOnionRequest(result); | ||||
|   if (statusCode !== 200) { | ||||
|     window.log.warn(`Could not claim token, status code: ${statusCode}`); | ||||
|     return null; | ||||
|   } | ||||
|   return authToken; | ||||
| } | ||||
| 
 | ||||
| export async function getAuthToken({ | ||||
|   serverUrl, | ||||
|   roomId, | ||||
| }: OpenGroupRequestCommonType): Promise<string | null> { | ||||
|   // first try to fetch from db a saved token.
 | ||||
|   const roomDetails = await getV2OpenGroupRoomByRoomId({ serverUrl, roomId }); | ||||
|   if (!roomDetails) { | ||||
|     window.log.warn('getAuthToken Room does not exist.'); | ||||
|     return null; | ||||
|   } | ||||
|   if (roomDetails?.token) { | ||||
|     return roomDetails.token; | ||||
|   } | ||||
| 
 | ||||
|   await allowOnlyOneAtATime(`getAuthTokenV2${serverUrl}:${roomId}`, async () => { | ||||
|     try { | ||||
|       window.log.info('TRIGGERING NEW AUTH TOKEN WITH', { serverUrl, roomId }); | ||||
|       const token = await requestNewAuthToken({ serverUrl, roomId }); | ||||
|       if (!token) { | ||||
|         window.log.warn('invalid new auth token', token); | ||||
|         return; | ||||
|       } | ||||
|       const claimedToken = await claimAuthToken(token, serverUrl, roomId); | ||||
|       if (!claimedToken) { | ||||
|         window.log.warn('invalid claimed token', claimedToken); | ||||
|       } | ||||
|       // still save it to the db. just to mark it as to be refreshed later
 | ||||
|       roomDetails.token = claimedToken || ''; | ||||
|       await saveV2OpenGroupRoom(roomDetails); | ||||
|     } catch (e) { | ||||
|       window.log.error('Failed to getAuthToken', e); | ||||
|       throw e; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   const refreshedRoomDetails = await getV2OpenGroupRoomByRoomId({ | ||||
|     serverUrl, | ||||
|     roomId, | ||||
|   }); | ||||
|   if (!refreshedRoomDetails) { | ||||
|     window.log.warn('getAuthToken Room does not exist.'); | ||||
|     return null; | ||||
|   } | ||||
|   if (refreshedRoomDetails?.token) { | ||||
|     return refreshedRoomDetails?.token; | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
| 
 | ||||
| export const deleteAuthToken = async ({ | ||||
|   serverUrl, | ||||
|   roomId, | ||||
| }: OpenGroupRequestCommonType): Promise<boolean> => { | ||||
|   const request: OpenGroupV2Request = { | ||||
|     method: 'DELETE', | ||||
|     room: roomId, | ||||
|     server: serverUrl, | ||||
|     isAuthRequired: false, | ||||
|     endpoint: 'auth_token', | ||||
|   }; | ||||
|   try { | ||||
|     const result = await sendApiV2Request(request); | ||||
|     const statusCode = parseStatusCodeFromOnionRequest(result); | ||||
|     if (statusCode !== 200) { | ||||
|       window.log.warn(`Could not deleteAuthToken, status code: ${statusCode}`); | ||||
|       return false; | ||||
|     } | ||||
|     return true; | ||||
|   } catch (e) { | ||||
|     window.log.error('deleteAuthToken failed:', e); | ||||
|     return false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // tslint:disable: member-ordering
 | ||||
| export async function requestNewAuthToken({ | ||||
|   serverUrl, | ||||
|   roomId, | ||||
| }: OpenGroupRequestCommonType): Promise<string | null> { | ||||
|   const userKeyPair = await getIdentityKeyPair(); | ||||
|   if (!userKeyPair) { | ||||
|     throw new Error('Failed to fetch user keypair'); | ||||
|   } | ||||
| 
 | ||||
|   const ourPubkey = getOurPubKeyStrFromCache(); | ||||
|   const parameters = {} as Record<string, string>; | ||||
|   parameters.public_key = ourPubkey; | ||||
|   const request: OpenGroupV2Request = { | ||||
|     method: 'GET', | ||||
|     room: roomId, | ||||
|     server: serverUrl, | ||||
|     queryParams: parameters, | ||||
|     isAuthRequired: false, | ||||
|     endpoint: 'auth_token_challenge', | ||||
|   }; | ||||
|   const json = (await sendApiV2Request(request)) as any; | ||||
|   // parse the json
 | ||||
|   if (!json || !json?.result?.challenge) { | ||||
|     window.log.warn('Parsing failed'); | ||||
|     return null; | ||||
|   } | ||||
|   const { | ||||
|     ciphertext: base64EncodedCiphertext, | ||||
|     ephemeral_public_key: base64EncodedEphemeralPublicKey, | ||||
|   } = json?.result?.challenge; | ||||
| 
 | ||||
|   if (!base64EncodedCiphertext || !base64EncodedEphemeralPublicKey) { | ||||
|     window.log.warn('Parsing failed'); | ||||
|     return null; | ||||
|   } | ||||
|   const ciphertext = fromBase64ToArrayBuffer(base64EncodedCiphertext); | ||||
|   const ephemeralPublicKey = fromBase64ToArrayBuffer(base64EncodedEphemeralPublicKey); | ||||
|   try { | ||||
|     const symmetricKey = await window.libloki.crypto.deriveSymmetricKey( | ||||
|       ephemeralPublicKey, | ||||
|       userKeyPair.privKey | ||||
|     ); | ||||
| 
 | ||||
|     const plaintextBuffer = await window.libloki.crypto.DecryptAESGCM(symmetricKey, ciphertext); | ||||
| 
 | ||||
|     const token = toHex(plaintextBuffer); | ||||
| 
 | ||||
|     return token; | ||||
|   } catch (e) { | ||||
|     window.log.error('Failed to decrypt token open group v2'); | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
					Loading…
					
					
				
		Reference in New Issue