diff --git a/keyserver/src/services/blob.js b/keyserver/src/services/blob.js index 585246c45..d88b17ed8 100644 --- a/keyserver/src/services/blob.js +++ b/keyserver/src/services/blob.js @@ -1,127 +1,143 @@ // @flow import type { BlobHashAndHolder } from 'lib/types/holder-types.js'; import { downloadBlob, removeBlobHolder, assignBlobHolder, } from 'lib/utils/blob-service.js'; import { uploadBlob, removeMultipleHolders, type BlobOperationResult, } from 'lib/utils/blob-service.js'; import { createHTTPAuthorizationHeader } from 'lib/utils/services-utils.js'; +import { clearIdentityInfo } from '../user/identity.js'; import { verifyUserLoggedIn } from '../user/login.js'; import { getContentSigningKey } from '../utils/olm-utils.js'; async function createRequestHeaders( includeContentType: boolean = true, ): Promise<{ [string]: string }> { const [{ userId: userID, accessToken }, deviceID] = await Promise.all([ verifyUserLoggedIn(), getContentSigningKey(), ]); const authorization = createHTTPAuthorizationHeader({ userID, deviceID, accessToken, }); return { Authorization: authorization, ...(includeContentType && { 'Content-Type': 'application/json' }), }; } type BlobDescriptor = { +hash: string, +holder: string, }; async function assignHolder( params: BlobDescriptor, ): Promise { const { hash: blobHash, holder } = params; const headers = await createRequestHeaders(); - return assignBlobHolder({ blobHash, holder }, headers); + const assignResult = await assignBlobHolder({ blobHash, holder }, headers); + if (!assignResult.success && assignResult.reason === 'INVALID_CSAT') { + await clearIdentityInfo(); + } + return assignResult; } async function uploadBlobKeyserverWrapper( blob: Blob, hash: string, ): Promise { const authHeaders = await createRequestHeaders(false); - return uploadBlob(blob, hash, authHeaders); + const uploadResult = await uploadBlob(blob, hash, authHeaders); + if (!uploadResult.success && uploadResult.reason === 'INVALID_CSAT') { + await clearIdentityInfo(); + } + return uploadResult; } async function upload( blob: Blob, params: BlobDescriptor, ): Promise< | { +success: true, } | { +success: false, +assignHolderResult: BlobOperationResult, +uploadBlobResult: BlobOperationResult, }, > { const { hash, holder } = params; const [holderResult, uploadResult] = await Promise.all([ assignHolder({ hash, holder }), uploadBlobKeyserverWrapper(blob, hash), ]); if (holderResult.success && uploadResult.success) { return { success: true }; } return { success: false, assignHolderResult: holderResult, uploadBlobResult: uploadResult, }; } export type BlobDownloadResult = | { +found: false, +status: number, } | { +found: true, +blob: Blob, }; async function download(hash: string): Promise { const headers = await createRequestHeaders(); const blobResult = await downloadBlob(hash, headers); if (blobResult.result !== 'success') { return { found: false, status: blobResult.status }; } const blob = await blobResult.response.blob(); return { found: true, blob }; } async function deleteBlob(params: BlobDescriptor, instant?: boolean) { const { hash: blobHash, holder } = params; const headers = await createRequestHeaders(); - await removeBlobHolder({ blobHash, holder }, headers, instant); + const removeResult = await removeBlobHolder( + { blobHash, holder }, + headers, + instant, + ); + if (!removeResult.success && removeResult.reason === 'INVALID_CSAT') { + await clearIdentityInfo(); + } } async function removeBlobHolders(holders: $ReadOnlyArray) { const headers = await createRequestHeaders(false); await removeMultipleHolders(holders, headers); } export { upload, uploadBlob, assignHolder, download, deleteBlob, uploadBlobKeyserverWrapper, removeBlobHolders, }; diff --git a/lib/actions/upload-actions.js b/lib/actions/upload-actions.js index a1dce644b..156bf6601 100644 --- a/lib/actions/upload-actions.js +++ b/lib/actions/upload-actions.js @@ -1,368 +1,386 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import uuid from 'uuid'; import { storeEstablishedHolderActionType } from './holder-actions.js'; import blobService from '../facts/blob-service.js'; import type { CallSingleKeyserverEndpoint } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import { extractKeyserverIDFromID, extractKeyserverIDFromIDOptional, } from '../keyserver-conn/keyserver-call-utils.js'; import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { type PerformHTTPMultipartUpload } from '../keyserver-conn/multipart-upload.js'; import { mediaConfig } from '../media/file-utils.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import type { AuthMetadata } from '../shared/identity-client-context.js'; import type { UploadMultimediaResult, Dimensions } from '../types/media-types'; import type { Dispatch } from '../types/redux-types.js'; import { toBase64URL } from '../utils/base64.js'; import { blobServiceUploadHandler, type BlobServiceUploadHandler, } from '../utils/blob-service-upload.js'; import { makeBlobServiceEndpointURL, makeBlobServiceURI, generateBlobHolder, assignBlobHolder, } from '../utils/blob-service.js'; import { getMessageForException } from '../utils/errors.js'; import { useDispatch } from '../utils/redux-utils.js'; -import { createDefaultHTTPRequestHeaders } from '../utils/services-utils.js'; +import { + createDefaultHTTPRequestHeaders, + errorMessageIsInvalidCSAT, +} from '../utils/services-utils.js'; export type MultimediaUploadCallbacks = Partial<{ +onProgress: (percent: number) => void, +abortHandler: (abort: () => void) => void, +performHTTPMultipartUpload: PerformHTTPMultipartUpload, +blobServiceUploadHandler: BlobServiceUploadHandler, +timeout: ?number, }>; export type DeleteUploadInput = { +id: string, +keyserverOrThreadID: string, }; const updateMultimediaMessageMediaActionType = 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA'; const deleteUpload = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DeleteUploadInput) => Promise) => async input => { const { id, keyserverOrThreadID } = input; const keyserverID: string = extractKeyserverIDFromIDOptional(keyserverOrThreadID) ?? keyserverOrThreadID; const requests = { [keyserverID]: { id } }; await callKeyserverEndpoint('delete_upload', requests); }; function useDeleteUpload(): (input: DeleteUploadInput) => Promise { return useKeyserverCall(deleteUpload); } export type BlobServiceUploadFile = | { +type: 'file', +file: File } | { +type: 'uri', +uri: string, +filename: string, +mimeType: string, }; export type BlobServiceUploadInput = { +blobInput: BlobServiceUploadFile, +blobHash: string, +encryptionKey: string, +dimensions: ?Dimensions, +thumbHash?: ?string, +loop?: boolean, }; export type BlobServiceUploadResult = $ReadOnly<{ ...UploadMultimediaResult, +blobHolder: string, }>; export type BlobServiceUploadAction = (input: { +uploadInput: BlobServiceUploadInput, // use `null` to skip metadata upload to keyserver +keyserverOrThreadID: ?string, +callbacks?: MultimediaUploadCallbacks, }) => Promise; const blobServiceUpload = ( callKeyserverEndpoint: CallKeyserverEndpoint, dispatch: Dispatch, authMetadata: AuthMetadata, ): BlobServiceUploadAction => async input => { const { uploadInput, callbacks, keyserverOrThreadID } = input; const { encryptionKey, loop, dimensions, thumbHash, blobInput } = uploadInput; const blobHash = toBase64URL(uploadInput.blobHash); const defaultHeaders = createDefaultHTTPRequestHeaders(authMetadata); let maybeKeyserverID; if (keyserverOrThreadID) { maybeKeyserverID = extractKeyserverIDFromIDOptional(keyserverOrThreadID) ?? keyserverOrThreadID; } // don't prefix keyserver-owned holders with deviceID const holderPrefix = maybeKeyserverID ? null : authMetadata.deviceID; const blobHolder = generateBlobHolder(holderPrefix); // 1. Assign new holder for blob with given blobHash let blobAlreadyExists: boolean; try { const assignHolderResult = await assignBlobHolder( { blobHash, holder: blobHolder }, defaultHeaders, ); if (!assignHolderResult.success) { + if (assignHolderResult.reason === 'INVALID_CSAT') { + throw new Error('invalid_csat'); + } const { status, statusText } = assignHolderResult; throw new Error(`Server responded with HTTP ${status}: ${statusText}`); } const { data_exists: dataExistsResponse } = await assignHolderResult.response.json(); blobAlreadyExists = dataExistsResponse; } catch (e) { + if (errorMessageIsInvalidCSAT(e)) { + throw e; + } throw new Error( `Failed to assign holder: ${ getMessageForException(e) ?? 'unknown error' }`, ); } // 2. Upload blob contents if blob doesn't exist if (!blobAlreadyExists) { const uploadEndpoint = blobService.httpEndpoints.UPLOAD_BLOB; let blobServiceUploadCallback = blobServiceUploadHandler; if (callbacks && callbacks.blobServiceUploadHandler) { blobServiceUploadCallback = callbacks.blobServiceUploadHandler; } try { await blobServiceUploadCallback( makeBlobServiceEndpointURL(uploadEndpoint), uploadEndpoint.method, { blobHash, blobInput, }, authMetadata, { ...callbacks }, ); } catch (e) { + if (errorMessageIsInvalidCSAT(e)) { + throw e; + } throw new Error( `Failed to upload blob: ${ getMessageForException(e) ?? 'unknown error' }`, ); } } // 3. Optionally upload metadata to keyserver const mimeType = blobInput.type === 'file' ? blobInput.file.type : blobInput.mimeType; if (!maybeKeyserverID) { dispatch({ type: storeEstablishedHolderActionType, payload: { blobHash, holder: blobHolder, }, }); if (!dimensions) { throw new Error('dimensions are required for non-keyserver uploads'); } const mediaType = mediaConfig[mimeType]?.mediaType; if (mediaType !== 'photo' && mediaType !== 'video') { throw new Error(`mediaType for ${mimeType} should be photo or video`); } return { id: uuid.v4(), uri: makeBlobServiceURI(blobHash), mediaType, dimensions, loop: loop ?? false, blobHolder, }; } // for Flow const keyserverID: string = maybeKeyserverID; const requests = { [keyserverID]: { blobHash, blobHolder, encryptionKey, filename: blobInput.type === 'file' ? blobInput.file.name : blobInput.filename, mimeType, loop, thumbHash, ...dimensions, }, }; const responses = await callKeyserverEndpoint( 'upload_media_metadata', requests, ); const response = responses[keyserverID]; return { id: response.id, uri: response.uri, mediaType: response.mediaType, dimensions: response.dimensions, loop: response.loop, blobHolder, }; }; function useBlobServiceUpload(): BlobServiceUploadAction { const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { getAuthMetadata } = identityContext; const dispatch = useDispatch(); const blobUploadAction = React.useCallback( ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): BlobServiceUploadAction => async input => { const authMetadata = await getAuthMetadata(); const authenticatedUploadAction = blobServiceUpload( callSingleKeyserverEndpoint, dispatch, authMetadata, ); return authenticatedUploadAction(input); }, [dispatch, getAuthMetadata], ); return useKeyserverCall(blobUploadAction); } export type ThickThreadMediaMetadataInput = { +blobHash: string, +encryptionKey: string, +mimeType: string, +dimensions: ?Dimensions, +filename?: ?string, +thumbHash?: ?string, +loop?: boolean, }; export type MediaMetadataReassignmentAction = (input: { +mediaMetadataInput: ThickThreadMediaMetadataInput, +keyserverOrThreadID: string, }) => Promise; const reassignThickThreadMediaForThinThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, authMetadata: AuthMetadata, ): MediaMetadataReassignmentAction => async input => { const { mediaMetadataInput, keyserverOrThreadID } = input; const { encryptionKey, loop, dimensions, thumbHash, mimeType } = mediaMetadataInput; const blobHolder = generateBlobHolder(); const blobHash = toBase64URL(mediaMetadataInput.blobHash); const defaultHeaders = createDefaultHTTPRequestHeaders(authMetadata); let filename = mediaMetadataInput.filename; if (!filename) { const basename = Math.random().toString(36).slice(-10); const extension = mediaConfig[mimeType]?.extension; filename = extension ? `${basename}.${extension}` : basename; } // 1. Assign new holder for blob with given blobHash try { const assignHolderResult = await assignBlobHolder( { blobHash, holder: blobHolder }, defaultHeaders, ); if (!assignHolderResult.success) { + if (assignHolderResult.reason === 'INVALID_CSAT') { + throw new Error('invalid_csat'); + } const { status, statusText } = assignHolderResult; throw new Error(`Server responded with HTTP ${status}: ${statusText}`); } } catch (e) { + if (errorMessageIsInvalidCSAT(e)) { + throw e; + } throw new Error( `Failed to assign holder: ${ getMessageForException(e) ?? 'unknown error' }`, ); } // 2. Upload media metadata to keyserver const keyserverID = extractKeyserverIDFromID(keyserverOrThreadID); const requests = { [keyserverID]: { blobHash, blobHolder, encryptionKey, filename, mimeType, loop, thumbHash, ...dimensions, }, }; const responses = await callKeyserverEndpoint( 'upload_media_metadata', requests, ); const response = responses[keyserverID]; return { id: response.id, uri: response.uri, mediaType: response.mediaType, dimensions: response.dimensions, loop: response.loop, }; }; function useMediaMetadataReassignment(): MediaMetadataReassignmentAction { const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { getAuthMetadata } = identityContext; const thickThreadMediaReassignmentAction = React.useCallback( ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): MediaMetadataReassignmentAction => async input => { const authMetadata = await getAuthMetadata(); const authenticatedAction = reassignThickThreadMediaForThinThread( callSingleKeyserverEndpoint, authMetadata, ); return authenticatedAction(input); }, [getAuthMetadata], ); return useKeyserverCall(thickThreadMediaReassignmentAction); } export { useBlobServiceUpload, useMediaMetadataReassignment, updateMultimediaMessageMediaActionType, useDeleteUpload, }; diff --git a/lib/utils/blob-service-upload.js b/lib/utils/blob-service-upload.js index f9f3050fe..4e9429e02 100644 --- a/lib/utils/blob-service-upload.js +++ b/lib/utils/blob-service-upload.js @@ -1,90 +1,95 @@ // @flow import invariant from 'invariant'; import _throttle from 'lodash/throttle.js'; import { createHTTPAuthorizationHeader } from './services-utils.js'; import type { MultimediaUploadCallbacks, BlobServiceUploadFile, } from '../actions/upload-actions.js'; import type { AuthMetadata } from '../shared/identity-client-context.js'; function blobServiceUploadHandler( url: string, method: string, input: { blobHash: string, blobInput: BlobServiceUploadFile, }, authMetadata: AuthMetadata, options?: ?MultimediaUploadCallbacks, ): Promise { if (input.blobInput.type !== 'file') { throw new Error('Use file to upload blob to blob service!'); } const formData = new FormData(); formData.append('blob_hash', input.blobHash); invariant(input.blobInput.file, 'file should be defined'); formData.append('blob_data', input.blobInput.file); const xhr = new XMLHttpRequest(); xhr.open(method, url); const authHeader = createHTTPAuthorizationHeader(authMetadata); xhr.setRequestHeader('Authorization', authHeader); const { timeout, onProgress, abortHandler } = options ?? {}; if (timeout) { xhr.timeout = timeout; } if (onProgress) { xhr.upload.onprogress = _throttle( ({ loaded, total }) => onProgress(loaded / total), 50, ); } let failed = false; const responsePromise = new Promise((resolve, reject) => { xhr.onload = () => { if (failed) { return; } + if (xhr.status === 401 || xhr.status === 403) { + failed = true; + reject(new Error('invalid_csat')); + return; + } resolve(); }; xhr.onabort = () => { failed = true; reject(new Error('request aborted')); }; xhr.onerror = event => { failed = true; reject(event); }; if (timeout) { xhr.ontimeout = event => { failed = true; reject(event); }; } if (abortHandler) { abortHandler(() => { failed = true; reject(new Error('request aborted')); xhr.abort(); }); } }); if (!failed) { xhr.send(formData); } return responsePromise; } export type BlobServiceUploadHandler = typeof blobServiceUploadHandler; export { blobServiceUploadHandler }; diff --git a/lib/utils/blob-service.js b/lib/utils/blob-service.js index b9fcc9b8a..6347e0fd8 100644 --- a/lib/utils/blob-service.js +++ b/lib/utils/blob-service.js @@ -1,307 +1,331 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import { toBase64URL } from './base64.js'; +import { httpResponseIsInvalidCSAT } from './services-utils.js'; import { replacePathParams, type URLPathParams } from './url-utils.js'; import { assertWithValidator } from './validation-utils.js'; import type { BlobServiceHTTPEndpoint } from '../facts/blob-service.js'; import blobServiceConfig from '../facts/blob-service.js'; import { type BlobInfo, type AssignHoldersRequest, type RemoveHoldersRequest, assignHoldersResponseValidator, removeHoldersResponseValidator, } from '../types/blob-service-types.js'; const BLOB_SERVICE_URI_PREFIX = 'comm-blob-service://'; function makeBlobServiceURI(blobHash: string): string { return `${BLOB_SERVICE_URI_PREFIX}${blobHash}`; } function isBlobServiceURI(uri: string): boolean { return uri.startsWith(BLOB_SERVICE_URI_PREFIX); } /** * Returns the base64url-encoded blob hash from a blob service URI. * Throws an error if the URI is not a blob service URI. */ function blobHashFromBlobServiceURI(uri: string): string { invariant(isBlobServiceURI(uri), 'Not a blob service URI'); return uri.slice(BLOB_SERVICE_URI_PREFIX.length); } /** * Returns the base64url-encoded blob hash from a blob service URI. * Returns null if the URI is not a blob service URI. */ function blobHashFromURI(uri: string): ?string { if (!isBlobServiceURI(uri)) { return null; } return blobHashFromBlobServiceURI(uri); } function makeBlobServiceEndpointURL( endpoint: BlobServiceHTTPEndpoint, params: URLPathParams = {}, ): string { const path = replacePathParams(endpoint.path, params); return `${blobServiceConfig.url}${path}`; } function getBlobFetchableURL(blobHash: string): string { return makeBlobServiceEndpointURL(blobServiceConfig.httpEndpoints.GET_BLOB, { blobHash, }); } /** * Generates random blob holder prefixed by current device ID if present */ function generateBlobHolder(deviceID?: ?string): string { const randomID = uuid.v4(); if (!deviceID) { return randomID; } const urlSafeDeviceID = toBase64URL(deviceID); return `${urlSafeDeviceID}:${uuid.v4()}`; } export type BlobOperationResult = | { +success: true, +response: Response, } | { +success: false, - +reason: 'HASH_IN_USE' | 'OTHER', + +reason: 'HASH_IN_USE' | 'INVALID_CSAT' | 'OTHER', +status: number, +statusText: string, }; export type BlobDownloadResult = | { +result: 'success', response: Response } | { +result: 'error', +status: number, +statusText: string }; async function downloadBlob( blobHash: string, headers: { [string]: string }, ): Promise { const blobURL = getBlobFetchableURL(blobHash); const response = await fetch(blobURL, { method: blobServiceConfig.httpEndpoints.GET_BLOB.method, headers, }); if (response.status !== 200) { const { status, statusText } = response; return { result: 'error', status, statusText }; } return { result: 'success', response }; } async function uploadBlob( blob: Blob | string, hash: string, headers: { [string]: string }, ): Promise { const formData = new FormData(); formData.append('blob_hash', hash); if (typeof blob === 'string') { formData.append('base64_data', blob); } else { formData.append('blob_data', blob); } const uploadBlobResponse = await fetch( makeBlobServiceEndpointURL(blobServiceConfig.httpEndpoints.UPLOAD_BLOB), { method: blobServiceConfig.httpEndpoints.UPLOAD_BLOB.method, body: formData, headers, }, ); if (!uploadBlobResponse.ok) { const { status, statusText } = uploadBlobResponse; - const reason = status === 409 ? 'HASH_IN_USE' : 'OTHER'; + + let reason = 'OTHER'; + if (status === 409) { + reason = 'HASH_IN_USE'; + } else if (httpResponseIsInvalidCSAT(uploadBlobResponse)) { + reason = 'INVALID_CSAT'; + } + return { success: false, reason, status, statusText, }; } return { success: true, response: uploadBlobResponse }; } async function assignBlobHolder( blobInfo: BlobInfo, headers: { [string]: string }, ): Promise { const { blobHash, holder } = blobInfo; const response = await fetch( makeBlobServiceEndpointURL(blobServiceConfig.httpEndpoints.ASSIGN_HOLDER), { method: blobServiceConfig.httpEndpoints.ASSIGN_HOLDER.method, body: JSON.stringify({ holder, blob_hash: blobHash, }), headers: { ...headers, 'content-type': 'application/json', }, }, ); if (!response.ok) { const { status, statusText } = response; - return { success: false, reason: 'OTHER', status, statusText }; + const reason = httpResponseIsInvalidCSAT(response) + ? 'INVALID_CSAT' + : 'OTHER'; + return { + success: false, + reason, + status, + statusText, + }; } return { success: true, response }; } async function removeBlobHolder( blobInfo: BlobInfo, headers: { [string]: string }, instantDelete?: boolean, ): Promise { const { blobHash, holder } = blobInfo; const endpoint = blobServiceConfig.httpEndpoints.DELETE_BLOB; const response = await fetch(makeBlobServiceEndpointURL(endpoint), { method: endpoint.method, body: JSON.stringify({ holder, blob_hash: blobHash, instant_delete: !!instantDelete, }), headers: { ...headers, 'content-type': 'application/json', }, }); if (!response.ok) { const { status, statusText } = response; - return { success: false, reason: 'OTHER', status, statusText }; + const reason = httpResponseIsInvalidCSAT(response) + ? 'INVALID_CSAT' + : 'OTHER'; + return { + success: false, + reason, + status, + statusText, + }; } return { success: true, response }; } async function assignMultipleHolders( holders: $ReadOnlyArray, headers: { [string]: string }, ): Promise< | { +result: 'success' } | { +result: 'error', +status: number, +statusText: string } | { +failedRequests: $ReadOnlyArray, +result: 'failed_requests', }, > { const requestBody: AssignHoldersRequest = { requests: holders, }; const assignMultipleHoldersResponse = await fetch( makeBlobServiceEndpointURL( blobServiceConfig.httpEndpoints.ASSIGN_MULTIPLE_HOLDERS, ), { method: blobServiceConfig.httpEndpoints.ASSIGN_MULTIPLE_HOLDERS.method, headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }, ); if (!assignMultipleHoldersResponse.ok) { const { status, statusText } = assignMultipleHoldersResponse; return { result: 'error', status, statusText }; } const responseJson = await assignMultipleHoldersResponse.json(); const { results } = assertWithValidator( responseJson, assignHoldersResponseValidator, ); const failedRequests = results .filter(result => !result.success) .map(({ blobHash, holder }) => ({ blobHash, holder })); if (failedRequests.length !== 0) { return { result: 'failed_requests', failedRequests }; } return { result: 'success' }; } async function removeMultipleHolders( holders: $ReadOnlyArray, headers: { [string]: string }, instantDelete?: boolean, ): Promise< | { +result: 'success' } | { +result: 'error', +status: number, +statusText: string } | { +result: 'failed_requests', +failedRequests: $ReadOnlyArray, }, > { const requestBody: RemoveHoldersRequest = { requests: holders, instantDelete: !!instantDelete, }; const response = await fetch( makeBlobServiceEndpointURL( blobServiceConfig.httpEndpoints.REMOVE_MULTIPLE_HOLDERS, ), { method: blobServiceConfig.httpEndpoints.REMOVE_MULTIPLE_HOLDERS.method, headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }, ); if (!response.ok) { const { status, statusText } = response; return { result: 'error', status, statusText }; } const responseJson = await response.json(); const { failedRequests } = assertWithValidator( responseJson, removeHoldersResponseValidator, ); if (failedRequests.length !== 0) { return { result: 'failed_requests', failedRequests }; } return { result: 'success' }; } export { makeBlobServiceURI, isBlobServiceURI, blobHashFromURI, blobHashFromBlobServiceURI, generateBlobHolder, getBlobFetchableURL, makeBlobServiceEndpointURL, downloadBlob, uploadBlob, assignBlobHolder, removeBlobHolder, assignMultipleHolders, removeMultipleHolders, }; diff --git a/lib/utils/services-utils.js b/lib/utils/services-utils.js index 6b083d11f..0ee0c293a 100644 --- a/lib/utils/services-utils.js +++ b/lib/utils/services-utils.js @@ -1,50 +1,63 @@ // @flow import base64 from 'base-64'; +import { getMessageForException } from './errors.js'; import type { AuthMetadata } from '../shared/identity-client-context.js'; // If this is true then we're using the identity service for auth. After we // auth, the identity service gives us a CSAT, which we can use to auth with // other Comm services. const usingCommServicesAccessToken = true; // If this is true, then the app is able to support multiple keyservers. This // requires the use of Tunnelbroker and the backup service to persist and sync // the KeyserverStore. const supportingMultipleKeyservers = false; // If this is false, then the app no longer needs to rely on being connected to // an authoritative keyserver for things like DMs. const relyingOnAuthoritativeKeyserver = true; // If this is true, then we're using the login 2.0, which means that a user // can either restore an account (primary login) or log in using the QR code // (secondary login). const usingRestoreFlow = false; function createHTTPAuthorizationHeader(authMetadata: AuthMetadata): string { // explicit destructure to make it future-proof const { userID, deviceID, accessToken } = authMetadata; const payload = JSON.stringify({ userID, deviceID, accessToken }); const base64EncodedPayload = base64.encode(payload); return `Bearer ${base64EncodedPayload}`; } function createDefaultHTTPRequestHeaders(authMetadata: AuthMetadata): { [string]: string, } { const authorization = createHTTPAuthorizationHeader(authMetadata); return { Authorization: authorization, }; } +function httpResponseIsInvalidCSAT(response: Response): boolean { + const { status } = response; + return status === 401 || status === 403; +} + +function errorMessageIsInvalidCSAT(exception: mixed): boolean { + const errorMessage = getMessageForException(exception); + return errorMessage === 'invalid_csat'; +} + export { usingCommServicesAccessToken, supportingMultipleKeyservers, relyingOnAuthoritativeKeyserver, usingRestoreFlow, createHTTPAuthorizationHeader, createDefaultHTTPRequestHeaders, + httpResponseIsInvalidCSAT, + errorMessageIsInvalidCSAT, };