diff --git a/.babelrc b/.babelrc index 2aaf3cd1..3b4efa39 100644 --- a/.babelrc +++ b/.babelrc @@ -1,12 +1,10 @@ { - "presets": [ - [ - "@babel/preset-env", - { - "modules": false, - "corejs": false, - "forceAllTransforms": true - } - ] - ] + "presets": [ + [ "@babel/preset-env", { + "modules": false, + "corejs": false, + "forceAllTransforms": true + } ] + ], + "plugins": ["@babel/transform-runtime"] } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..74a53f59 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSaveMode": "modifications" +} \ No newline at end of file diff --git a/demos/browser/demo.js b/demos/browser/demo.js index 3d35c772..18c3a457 100644 --- a/demos/browser/demo.js +++ b/demos/browser/demo.js @@ -66,6 +66,7 @@ function startUpload() { endpoint, chunkSize, retryDelays: [0, 1000, 3000, 5000], + checksumAlgo: "SHA-256", parallelUploads, metadata: { filename: file.name, diff --git a/lib/browser/extensions/checksum.js b/lib/browser/extensions/checksum.js new file mode 100644 index 00000000..722cd480 --- /dev/null +++ b/lib/browser/extensions/checksum.js @@ -0,0 +1,37 @@ +class Checksum { + static supportedAlgorithms = ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']; + + constructor (algo = 'SHA-256') { + // Checking support for crypto on the browser + if (!crypto) { + throw new Error(`tus: this browser does not support checksum`) + } else if (!Checksum.supportedAlgorithms.includes(algo)) { + throw new Error( + `tus: unsupported checksumAlgo provided. Supported values are : ${Checksum.supportedAlgorithms.join( + ',', + )}`, + ) + } else { + this.algo = algo + } + } + + /** + * Gets Hexadecimal digest using the algorithm set in this.algo + * @param {ArrayBuffer} data contains the chunk of data to be hashed + */ + + getHexDigest = async (data) => { + try { + const hashBuffer = await crypto.subtle.digest(this.algo, data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join('') // convert bytes to hex string + return hashHex + } catch (err) { + throw new Error('tus: could not compute checksum for integrity check', err) + } + }; +} +export default Checksum diff --git a/lib/browser/index.js b/lib/browser/index.js index 6359507f..238e0d16 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -7,6 +7,7 @@ import { canStoreURLs, WebStorageUrlStorage } from './urlStorage.js' import DefaultHttpStack from './httpStack.js' import FileReader from './fileReader.js' import fingerprint from './fileSignature.js' +import Checksum from './extensions/checksum.js' const defaultOptions = { ...BaseUpload.defaultOptions, @@ -14,6 +15,7 @@ const defaultOptions = { fileReader: new FileReader(), urlStorage: canStoreURLs ? new WebStorageUrlStorage() : new NoopUrlStorage(), fingerprint, + Checksum, } class Upload extends BaseUpload { @@ -40,4 +42,5 @@ export { enableDebugLog, DefaultHttpStack, DetailedError, + Checksum, } diff --git a/lib/index.d.ts b/lib/index.d.ts index dfcbab68..7fd9b217 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -51,10 +51,12 @@ interface UploadOptions { removeFingerprintOnSuccess?: boolean uploadLengthDeferred?: boolean uploadDataDuringCreation?: boolean + checksumAlgo?: string urlStorage?: UrlStorage fileReader?: FileReader httpStack?: HttpStack + checksum?: Checksum; } interface UrlStorage { @@ -129,3 +131,7 @@ export class DetailedError extends Error { originalResponse: HttpResponse causingError: Error } + +export interface Checksum { + getHexDigest(): string; +} \ No newline at end of file diff --git a/lib/node/extensions/checksum.js b/lib/node/extensions/checksum.js new file mode 100644 index 00000000..2774a416 --- /dev/null +++ b/lib/node/extensions/checksum.js @@ -0,0 +1,36 @@ +import crypto from 'crypto' + +class Checksum { + static supportedAlgorithms = ['sha1', 'sha256', 'sha384', 'sha512', 'md5']; + + constructor (algo = 'sha256') { + if (!Checksum.supportedAlgorithms.includes(algo)) { + throw new Error( + `Checksum: unsupported checksumAlgo provided. Supported values are ${Checksum.supportedAlgorithms.join( + ',', + )}`, + ) + } else { + this.algo = algo + } + } + + /** + * Gets Hexadecimal digest using the algorithm set in this.algo + * @param {ArrayBuffer} data contains the chunk of data to be hashed + */ + getHexDigest = async (data) => { + try { + const hashHex = await crypto + .createHash(this.algo) + .update(data) + .digest('hex') + + return hashHex + } catch (err) { + throw new Error('tus: could not compute checksum for integrity check') + } + }; +} + +export default Checksum diff --git a/lib/node/index.js b/lib/node/index.js index 3486b2d4..c3c37678 100644 --- a/lib/node/index.js +++ b/lib/node/index.js @@ -8,6 +8,7 @@ import DefaultHttpStack from './httpStack.js' import FileReader from './fileReader.js' import fingerprint from './fileSignature.js' import StreamSource from './sources/StreamSource.js' +import Checksum from './extensions/checksum.js' const defaultOptions = { ...BaseUpload.defaultOptions, @@ -15,6 +16,7 @@ const defaultOptions = { fileReader: new FileReader(), urlStorage: new NoopUrlStorage(), fingerprint, + Checksum, } class Upload extends BaseUpload { @@ -47,4 +49,5 @@ export { DefaultHttpStack, DetailedError, StreamSource, + Checksum, } diff --git a/lib/upload.js b/lib/upload.js index 339cbb76..1cefe1f3 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -34,9 +34,10 @@ const defaultOptions = { uploadLengthDeferred: false, uploadDataDuringCreation: false, - urlStorage: null, - fileReader: null, - httpStack: null, + urlStorage : null, + fileReader : null, + httpStack : null, + checksumAlgo: null, } class BaseUpload { @@ -102,6 +103,9 @@ class BaseUpload { // An array of upload URLs which are used for uploading the different // parts, if the parallelUploads option is used. this._parallelUploadUrls = null + + // A platform dependent checksum generator, this is instantiated if options.checksumAlgo is provided + this._checksum = null } /** @@ -565,6 +569,9 @@ class BaseUpload { return } + if (this.options.checksumAlgo) { + this._checksum = new this.options.Checksum(this.options.checksumAlgo) + } const req = this._openRequest('POST', this.options.endpoint) if (this.options.uploadLengthDeferred) { @@ -793,8 +800,27 @@ class BaseUpload { if (value === null) { return this._sendRequest(req) } - this._emitProgress(this._offset, this._size) - return this._sendRequest(req, value) + + if (this.options.checksumAlgo) { + if (this._checksum) { + value.arrayBuffer().then((chunkBuffer) => { + this._checksum + .getHexDigest(chunkBuffer) + .then((hash) => { + req.setHeader('Upload-Checksum', `${this._checksum.algo} ${hash}`) + this._emitProgress(this._offset, this._size) + return this._sendRequest(req, value) + }) + .catch((err) => { + log(err) + throw err + }) + }) + } + } else { + this._emitProgress(this._offset, this._size) + return this._sendRequest(req, value) + } }) } diff --git a/package.json b/package.json index ef8e0f23..de009622 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ "@babel/helper-get-function-arity": "^7.16.7", "@babel/plugin-syntax-jsx": "^7.12.13", "@babel/plugin-transform-modules-commonjs": "^7.9.6", + "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.0.0", + "@babel/runtime": "^7.17.9", "axios": "^0.27.2", "babelify": "^10.0.0", "browserify": "^17.0.0", diff --git a/test/spec/test-browser-specific.js b/test/spec/test-browser-specific.js index 3ef7e933..2f4638a1 100644 --- a/test/spec/test-browser-specific.js +++ b/test/spec/test-browser-specific.js @@ -787,4 +787,24 @@ describe('tus', () => { await assertUrlStorage(tus.defaultOptions.urlStorage) }) }) + + describe('#Checksum', () => { + it('should generate hex digest for a given chunk of file', async () => { + const checksum = new tus.Checksum() + const hexDigest = await checksum.getHexDigest( + new TextEncoder().encode('hello'), + ) + expect(hexDigest).toBe( + '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', + ) + }) + + it('should throw an error when an unsupported algo is provided', () => { + try { + const checksum = new tus.Checksum('md5') + } catch (err) { + expect(err.message).toContain('unsupported checksumAlgo') + } + }) + }) }) diff --git a/test/spec/test-node-specific.js b/test/spec/test-node-specific.js index 64b28ccd..d8f45dd4 100644 --- a/test/spec/test-node-specific.js +++ b/test/spec/test-node-specific.js @@ -326,6 +326,22 @@ describe('tus', () => { }) }) + describe('#Checksum', () => { + it('should generate hex digest for a given chunk of file', async () => { + const checksum = new tus.Checksum() + const hexDigest = await checksum.getHexDigest(Buffer.from('hello', 'utf8')) + expect(hexDigest).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824') + }) + + it('should throw an error when an unsupported algo is provided', () => { + try { + const checksum = new tus.Checksum('new-algo') + } catch (err) { + expect(err.message).toContain('unsupported checksumAlgo') + } + }) + }) + describe('#StreamSource', () => { it('should slice at different ranges', async () => { const input = stream.Readable.from(Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), {