diff --git a/lib/actions/upload-actions.js b/lib/actions/upload-actions.js --- a/lib/actions/upload-actions.js +++ b/lib/actions/upload-actions.js @@ -1,10 +1,14 @@ // @flow +import invariant from 'invariant'; +import * as React from 'react'; import uuid from 'uuid'; import blobService from '../facts/blob-service.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.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 { toBase64URL } from '../utils/base64.js'; import { @@ -15,7 +19,10 @@ import type { CallSingleKeyserverEndpoint } from '../utils/call-single-keyserver-endpoint.js'; import { getMessageForException } from '../utils/errors.js'; import { useKeyserverCall } from '../utils/keyserver-call.js'; -import { handleHTTPResponseError } from '../utils/services-utils.js'; +import { + handleHTTPResponseError, + createDefaultHTTPRequestHeaders, +} from '../utils/services-utils.js'; import { type UploadBlob } from '../utils/upload-blob.js'; export type MultimediaUploadCallbacks = Partial<{ @@ -144,13 +151,17 @@ }) => Promise; const blobServiceUpload = - (callKeyserverEndpoint: CallKeyserverEndpoint): BlobServiceUploadAction => + ( + callKeyserverEndpoint: CallKeyserverEndpoint, + authMetadata: AuthMetadata, + ): BlobServiceUploadAction => async input => { const { uploadInput, callbacks, keyserverOrThreadID } = input; const { encryptionKey, loop, dimensions, thumbHash, blobInput } = uploadInput; const blobHolder = uuid.v4(); const blobHash = toBase64URL(uploadInput.blobHash); + const defaultHeaders = createDefaultHTTPRequestHeaders(authMetadata); // 1. Assign new holder for blob with given blobHash let blobAlreadyExists: boolean; @@ -165,6 +176,7 @@ blob_hash: blobHash, }), headers: { + ...defaultHeaders, 'content-type': 'application/json', }, }, @@ -196,6 +208,7 @@ blobHash, blobInput, }, + authMetadata, { ...callbacks }, ); } catch (e) { @@ -240,7 +253,25 @@ }; function useBlobServiceUpload(): BlobServiceUploadAction { - return useKeyserverCall(blobServiceUpload); + const identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'Identity context should be set'); + const { getAuthMetadata } = identityContext; + + const blobUploadAction = React.useCallback( + ( + callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, + ): BlobServiceUploadAction => + async input => { + const authMetadata = await getAuthMetadata(); + const authenticatedUploadAction = blobServiceUpload( + callSingleKeyserverEndpoint, + authMetadata, + ); + return authenticatedUploadAction(input); + }, + [getAuthMetadata], + ); + return useKeyserverCall(blobUploadAction); } export { diff --git a/lib/utils/blob-service-upload.js b/lib/utils/blob-service-upload.js --- a/lib/utils/blob-service-upload.js +++ b/lib/utils/blob-service-upload.js @@ -3,10 +3,12 @@ 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, @@ -15,6 +17,7 @@ blobHash: string, blobInput: BlobServiceUploadFile, }, + authMetadata: AuthMetadata, options?: ?MultimediaUploadCallbacks, ): Promise { if (input.blobInput.type !== 'file') { @@ -28,6 +31,9 @@ const xhr = new XMLHttpRequest(); xhr.open(method, url); + const authHeader = createHTTPAuthorizationHeader(authMetadata); + xhr.setRequestHeader('Authorization', authHeader); + const { timeout, onProgress, abortHandler } = options ?? {}; if (timeout) { diff --git a/native/utils/blob-service-upload.js b/native/utils/blob-service-upload.js --- a/native/utils/blob-service-upload.js +++ b/native/utils/blob-service-upload.js @@ -6,11 +6,13 @@ import { pathFromURI } from 'lib/media/file-utils.js'; import type { BlobServiceUploadHandler } from 'lib/utils/blob-service-upload.js'; import { getMessageForException } from 'lib/utils/errors.js'; +import { createDefaultHTTPRequestHeaders } from 'lib/utils/services-utils.js'; const blobServiceUploadHandler: BlobServiceUploadHandler = async ( url, method, input, + authMetadata, options, ) => { if (input.blobInput.type !== 'uri') { @@ -23,6 +25,7 @@ path = resolvedPath; } } + const headers = authMetadata && createDefaultHTTPRequestHeaders(authMetadata); const uploadTask = FileSystem.createUploadTask( url, path, @@ -31,6 +34,7 @@ fieldName: 'blob_data', httpMethod: method, parameters: { blob_hash: input.blobHash }, + headers, }, uploadProgress => { if (options?.onProgress) { diff --git a/web/input/input-state-container.react.js b/web/input/input-state-container.react.js --- a/web/input/input-state-container.react.js +++ b/web/input/input-state-container.react.js @@ -41,6 +41,8 @@ import commStaffCommunity from 'lib/facts/comm-staff-community.js'; import { getNextLocalUploadID } from 'lib/media/media-utils.js'; import { pendingToRealizedThreadIDsSelector } from 'lib/selectors/thread-selectors.js'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; +import type { IdentityClientContextType } from 'lib/shared/identity-client-context.js'; import { createMediaMessageInfo, localIDPrefix, @@ -95,6 +97,7 @@ } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { generateReportID } from 'lib/utils/report-utils.js'; +import { createDefaultHTTPRequestHeaders } from 'lib/utils/services-utils.js'; import { type BaseInputState, @@ -151,6 +154,7 @@ +registerSendCallback: (() => mixed) => void, +unregisterSendCallback: (() => mixed) => void, +textMessageCreationSideEffectsFunc: CreationSideEffectsFunc, + +identityContext: ?IdentityClientContextType, }; type WritableState = { pendingUploads: { @@ -867,6 +871,9 @@ const steps = [...upload.steps]; let userTime; + const { identityContext } = this.props; + invariant(identityContext, 'Identity context should be set'); + const sendReport = (missionResult: MediaMissionResult) => { const newThreadID = this.getRealizedOrPendingThreadID(threadID); const latestUpload = this.state.pendingUploads[newThreadID][localID]; @@ -907,6 +914,7 @@ encryptionKey && blobHash && dimensions, 'incomplete encrypted upload', ); + uploadResult = await this.props.blobServiceUpload({ uploadInput: { blobInput: { @@ -1011,7 +1019,11 @@ }); if (encryptionKey) { - const { steps: preloadSteps } = await preloadMediaResource(result.uri); + const authMetadata = await identityContext.getAuthMetadata(); + const { steps: preloadSteps } = await preloadMediaResource( + result.uri, + authMetadata, + ); steps.push(...preloadSteps); } else { const { steps: preloadSteps } = await preloadImage(result.uri); @@ -1202,6 +1214,8 @@ keyserverOrThreadID: threadID, }); if (isBlobServiceURI(pendingUpload.uri)) { + const identityContext = this.props.identityContext; + invariant(identityContext, 'Identity context should be set'); invariant( pendingUpload.blobHolder, 'blob service upload has no holder', @@ -1209,16 +1223,22 @@ const endpoint = blobService.httpEndpoints.DELETE_BLOB; const holder = pendingUpload.blobHolder; const blobHash = blobHashFromBlobServiceURI(pendingUpload.uri); - void fetch(makeBlobServiceEndpointURL(endpoint), { - method: endpoint.method, - body: JSON.stringify({ - holder, - blob_hash: blobHash, - }), - headers: { - 'content-type': 'application/json', - }, - }); + void (async () => { + const authMetadata = await identityContext.getAuthMetadata(); + const defaultHeaders = + createDefaultHTTPRequestHeaders(authMetadata); + await fetch(makeBlobServiceEndpointURL(endpoint), { + method: endpoint.method, + body: JSON.stringify({ + holder, + blob_hash: blobHash, + }), + headers: { + ...defaultHeaders, + 'content-type': 'application/json', + }, + }); + })(); } } const newPendingUploads = _omit([localUploadID])(currentPendingUploads); @@ -1656,6 +1676,7 @@ const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); + const identityContext = React.useContext(IdentityClientContext); const [sendCallbacks, setSendCallbacks] = React.useState< $ReadOnlyArray<() => mixed>, @@ -1696,6 +1717,7 @@ registerSendCallback={registerSendCallback} unregisterSendCallback={unregisterSendCallback} textMessageCreationSideEffectsFunc={textMessageCreationSideEffectsFunc} + identityContext={identityContext} /> ); }); diff --git a/web/media/media-utils.js b/web/media/media-utils.js --- a/web/media/media-utils.js +++ b/web/media/media-utils.js @@ -5,13 +5,16 @@ import { thumbHashToDataURL } from 'thumbhash'; import { fetchableMediaURI } from 'lib/media/media-utils.js'; +import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; import type { MediaType, Dimensions, MediaMissionStep, MediaMissionFailure, } from 'lib/types/media-types.js'; +import { isBlobServiceURI } from 'lib/utils/blob-service.js'; import { getMessageForException } from 'lib/utils/errors.js'; +import { createDefaultHTTPRequestHeaders } from 'lib/utils/services-utils.js'; import { probeFile } from './blob-utils.js'; import { decryptThumbhashToDataURL } from './encryption-utils.js'; @@ -64,15 +67,23 @@ * @returns Steps and the result of the preload. The preload is successful * if the HTTP response is OK (20x). */ -async function preloadMediaResource(uri: string): Promise<{ +async function preloadMediaResource( + uri: string, + authMetadata: AuthMetadata, +): Promise<{ steps: $ReadOnlyArray, result: { +success: boolean }, }> { + let headers; + if (isBlobServiceURI(uri)) { + headers = createDefaultHTTPRequestHeaders(authMetadata); + } + const start = Date.now(); const mediaURI = fetchableMediaURI(uri); let success, exceptionMessage; try { - const response = await fetch(mediaURI); + const response = await fetch(mediaURI, { headers }); // we need to read the blob to make sure the browser caches it await response.blob(); success = response.ok;