diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index c0bf8744c1..fe8054fcf7 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -13,6 +13,11 @@ const port = process.env.PORT ?? 8080 const accessControlAllowOrigin = '*' // You should define the actual domain(s) that are allowed to make requests. const bodyParser = require('body-parser') +const unhoistableHeaders = new Set([ + 'x-amz-sdk-checksum-algorithm', + 'x-amz-checksum-sha256', +]) + const { S3Client, AbortMultipartUploadCommand, @@ -109,7 +114,7 @@ const signOnServer = (req, res, next) => { // For the sake of simplification, we skip that check in this example. const Key = `${crypto.randomUUID()}-${req.body.filename}` - const { contentType } = req.body + const { contentType, ChecksumSHA256 } = req.body getSignedUrl( getS3Client(), @@ -117,8 +122,16 @@ const signOnServer = (req, res, next) => { Bucket: process.env.COMPANION_AWS_BUCKET, Key, ContentType: contentType, + ChecksumAlgorithm: 'SHA256', + ChecksumSHA256, }), - { expiresIn }, + { + expiresIn, + // If not supplied, the presigner moves all the AWS-specific headers + // (starting with `x-amz-`) to the request query string. + // If supplied, these headers remain in the presigned request's header. + unhoistableHeaders, + }, ).then((url) => { res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) res.json({ @@ -152,6 +165,7 @@ app.post('/s3/multipart', (req, res, next) => { Key, ContentType: type, Metadata: metadata, + ChecksumAlgorithm: 'SHA256', } const command = new CreateMultipartUploadCommand(params) @@ -176,7 +190,7 @@ function validatePartNumber(partNumber) { } app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => { const { uploadId, partNumber } = req.params - const { key } = req.query + const { key, sha256 } = req.query if (!validatePartNumber(partNumber)) { return res @@ -201,12 +215,20 @@ app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => { Key: key, UploadId: uploadId, PartNumber: partNumber, - Body: '', + ChecksumSHA256: sha256, }), - { expiresIn }, + { + expiresIn, + // If not supplied, the presigner moves all the AWS-specific headers + // (starting with `x-amz-`) to the request query string. + // If supplied, these headers remain in the presigned request's header. + unhoistableHeaders, + }, ).then((url) => { res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) - res.json({ url, expires: expiresIn }) + res.json({ url, expires: expiresIn, headers: { + 'x-amz-checksum-sha256': sha256, + } }) }, next) }) @@ -264,7 +286,12 @@ app.post('/s3/multipart/:uploadId/complete', (req, res, next) => { const client = getS3Client() const { uploadId } = req.params const { key } = req.query - const { parts } = req.body + + const parts = Array.from(req.body.parts[0].ETag, (ETag, i) => ({ + ETag, + PartNumber: req.body.parts[0].PartNumber[i], + ChecksumSHA256: req.body.parts[0].ChecksumSHA256[i], + })) if (typeof key !== 'string') { return res diff --git a/examples/aws-nodejs/public/withCustomEndpoints.html b/examples/aws-nodejs/public/withCustomEndpoints.html index 2265a7bb70..f2f1f8c8fb 100644 --- a/examples/aws-nodejs/public/withCustomEndpoints.html +++ b/examples/aws-nodejs/public/withCustomEndpoints.html @@ -43,7 +43,18 @@

AWS upload example

// return JSON.stringify(data) // You'd also have to add `Content-Type` header with value `application/json`. } + async function computeHash(blob) { + const hashBuffer = await crypto.subtle.digest( + 'SHA-256', + await blob.arrayBuffer(), + ) + const stringifiedHash = String.fromCharCode( + ...new Uint8Array(hashBuffer), + ) + return btoa(stringifiedHash) + } { + const cache = Object.create(null) const MiB = 0x10_00_00 const uppy = new Uppy() @@ -57,17 +68,6 @@

AWS upload example

// Files that are more than 100MiB should be uploaded in multiple parts. shouldUseMultipart: (file) => file.size > 100 * MiB, - /** - * This method tells Uppy how to retrieve a temporary token for signing on the client. - * Signing on the client is optional, you can also do the signing from the server. - */ - async getTemporarySecurityCredentials({ signal }) { - const response = await fetch('/s3/sts', { signal }) - if (!response.ok) - throw new Error('Unsuccessful request', { cause: response }) - return response.json() - }, - // ========== Non-Multipart Uploads ========== /** @@ -76,13 +76,7 @@

AWS upload example

* you don't need to implement it. */ async getUploadParameters(file, options) { - if (typeof crypto?.subtle === 'object') { - // If WebCrypto is available, let's do signing from the client. - return uppy - .getPlugin('myAWSPlugin') - .createSignedURL(file, options) - } - + const ChecksumSHA256 = await computeHash(file.data) // Send a request to our Express.js signing endpoint. const response = await fetch('/s3/sign', { method: 'POST', @@ -92,6 +86,7 @@

AWS upload example

body: serialize({ filename: file.name, contentType: file.type, + ChecksumSHA256, }), signal: options.signal, }) @@ -110,6 +105,8 @@

AWS upload example

// Provide content type header required by S3 headers: { 'Content-Type': file.type, + 'x-amz-checksum-sha256': ChecksumSHA256, + 'x-amz-sdk-checksum-algorithm': 'SHA256', }, } }, @@ -170,14 +167,7 @@

AWS upload example

}, async signPart(file, options) { - if (typeof crypto?.subtle === 'object') { - // If WebCrypto, let's do signing from the client. - return uppy - .getPlugin('myAWSPlugin') - .createSignedURL(file, options) - } - - const { uploadId, key, partNumber, signal } = options + const { uploadId, key, partNumber, signal, body } = options signal?.throwIfAborted() @@ -186,10 +176,12 @@

AWS upload example

'Cannot sign without a key, an uploadId, and a partNumber', ) } + const sha256 = await computeHash(body) + cache[uploadId + partNumber] = sha256 const filename = encodeURIComponent(key) const response = await fetch( - `/s3/multipart/${uploadId}/${partNumber}?key=${filename}`, + `/s3/multipart/${uploadId}/${partNumber}?key=${filename}&sha256=${encodeURIComponent(sha256)}`, { signal }, ) @@ -234,7 +226,13 @@

AWS upload example

headers: { accept: 'application/json', }, - body: serialize({ parts }), + body: serialize({ + parts: parts.map(({ ETag, PartNumber }) => { + const ChecksumSHA256 = cache[uploadId + PartNumber] + delete cache[uploadId + PartNumber] + return { ETag, PartNumber, ChecksumSHA256 } + }), + }), signal, }, )