diff --git a/.env.example b/.env.example index c00e1b2a9..364b7e3e0 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ COMPANION_PORT=3020 COMPANION_CLIENT_ORIGINS= COMPANION_SECRET=development COMPANION_PREAUTH_SECRET=development2 +COMPANION_OAUTH_ORIGIN=* # NOTE: Only enable this in development. Enabling it in production is a security risk COMPANION_ALLOW_LOCAL_URLS=true @@ -25,6 +26,8 @@ COMPANION_AWS_REGION="AWS REGION" COMPANION_AWS_PREFIX="OPTIONAL PREFIX" # to enable S3 Transfer Acceleration (default: false) # COMPANION_AWS_USE_ACCELERATE_ENDPOINT="false" +# to enable S3 path style uploads (default: false), this is useful for localstack support +# COMPANION_AWS_FORCE_PATH_STYLE="true" # to set X-Amz-Expires query param in presigned urls (in seconds, default: 800) # COMPANION_AWS_EXPIRES="800" # to set a canned ACL for uploaded objects: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl diff --git a/.github/workflows/companion-deploy.yml b/.github/workflows/companion-deploy.yml index 6b476f127..ddf98e2cb 100644 --- a/.github/workflows/companion-deploy.yml +++ b/.github/workflows/companion-deploy.yml @@ -63,7 +63,7 @@ jobs: username: ${{secrets.DOCKER_USERNAME}} password: ${{secrets.DOCKER_PASSWORD}} - name: Build and push - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6.1.0 + uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6.2.0 with: push: true context: . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82a25e934..a82cb4103 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,7 +161,7 @@ jobs: username: ${{secrets.DOCKER_USERNAME}} password: ${{secrets.DOCKER_PASSWORD}} - name: Build and push - uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6.1.0 + uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6.2.0 with: push: true context: . diff --git a/README.md b/README.md index 097e68652..fe9c81df9 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,22 @@ about more important problems than building a file uploader. **[Read the docs](https://uppy.io/docs)** | **[Try Uppy](https://uppy.io/examples/dashboard/)** - +

+ + + + + Developed by Transloadit + + +

Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile API to handle any file in your app. - - + +
TestsCI status for Uppy testsCI status for Companion testsCI status for browser tests
DeploysCI status for CDN deploymentCI status for Companion deploymentCI status for website deployment
TestsCI status for Uppy testsCI status for Companion testsCI status for browser tests
DeploysCI status for CDN deploymentCI status for Companion deploymentCI status for website deployment
## Example diff --git a/docs/companion.md b/docs/companion.md index 98344876b..22b112079 100644 --- a/docs/companion.md +++ b/docs/companion.md @@ -343,6 +343,21 @@ which has only the secret, nothing else. ::: +### `oauthOrigin` `COMPANION_OAUTH_ORIGIN` + +:::caution + +Setting this option is strongly recommended. If left unset (or set to `'*'`), +your app could be impersonated. + +::: + +An [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) specifying +allowed origins, or an array of origins (comma-separated origins in +`COMPANION_OAUTH_ORIGIN`). Any browser request from an origin that is not listed +will not receive OAuth2 tokens, and the OAuth request won’t complete. Set it to +`'*'` to allow all origins (not recommended). Default: `'*'`. + #### `uploadUrls` `COMPANION_UPLOAD_URLS` An allowlist (array) of strings (exact URLs) or regular expressions. Companion @@ -484,6 +499,12 @@ takes one argument which is an object with the following properties: - `req`, Express.js `Request` object. Do not use any Companion internals from the req object, as these might change in any minor version of Companion. +#### `s3.forcePathStyle` `COMPANION_AWS_FORCE_PATH_STYLE` + +This adds support for setting the S3 client’s `forcePathStyle` option. That is +necessary to use Uppy/Companion alongside localstack in development +environments. **Default**: `false`. + ##### `s3.region` `COMPANION_AWS_REGION` The datacenter region where the target bucket is located. diff --git a/docs/framework-integrations/angular.mdx b/docs/framework-integrations/angular.mdx index e2f9ec3fb..6c6b74765 100644 --- a/docs/framework-integrations/angular.mdx +++ b/docs/framework-integrations/angular.mdx @@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem'; ## When should I use it? -When you are using the Angular framework and you wouldl like to use on of the UI +When you are using the Angular framework and you would like to use one of the UI components. ## Install diff --git a/e2e/start-companion-with-load-balancer.mjs b/e2e/start-companion-with-load-balancer.mjs index d228df880..697c2865c 100755 --- a/e2e/start-companion-with-load-balancer.mjs +++ b/e2e/start-companion-with-load-balancer.mjs @@ -67,6 +67,7 @@ const startCompanion = ({ name, port }) => { COMPANION_ALLOW_LOCAL_URLS: 'true', COMPANION_ENABLE_URL_ENDPOINT: 'true', COMPANION_LOGGER_PROCESS_NAME: name, + COMPANION_OAUTH_ORIGIN: '*', }, }) // Adding a `then` property so the return value is awaitable: diff --git a/examples/aws-companion/server.cjs b/examples/aws-companion/server.cjs index c64f9830f..d1a92dfeb 100644 --- a/examples/aws-companion/server.cjs +++ b/examples/aws-companion/server.cjs @@ -35,6 +35,7 @@ const options = { bucket: process.env.COMPANION_AWS_BUCKET, region: process.env.COMPANION_AWS_REGION, endpoint: process.env.COMPANION_AWS_ENDPOINT, + forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true', }, server: { host: 'localhost:3020' }, filePath: DATA_DIR, diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index c0bf8744c..97accb384 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -58,6 +58,7 @@ function getS3Client() { accessKeyId: process.env.COMPANION_AWS_KEY, secretAccessKey: process.env.COMPANION_AWS_SECRET, }, + forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true', }) return s3Client } diff --git a/examples/digitalocean-spaces/server.cjs b/examples/digitalocean-spaces/server.cjs index f98219648..5cbeefc25 100644 --- a/examples/digitalocean-spaces/server.cjs +++ b/examples/digitalocean-spaces/server.cjs @@ -14,6 +14,7 @@ const companion = require('../../packages/@uppy/companion') * - COMPANION_AWS_KEY - Your access key ID * - COMPANION_AWS_SECRET - Your secret access key * - COMPANION_AWS_BUCKET - Your space's name. + * - COMPANION_AWS_FORCE_PATH_STYLE - Indicates if s3ForcePathStyle should be used rather than subdomain for S3 buckets. */ if (!process.env.COMPANION_AWS_REGION) throw new Error('Missing Space region, please set the COMPANION_AWS_REGION environment variable (eg. "COMPANION_AWS_REGION=ams3")') @@ -43,6 +44,7 @@ const { app: companionApp } = companion.app({ secret: process.env.COMPANION_AWS_SECRET, bucket: process.env.COMPANION_AWS_BUCKET, region: process.env.COMPANION_AWS_REGION, + forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true', }, server: { host }, filePath: DATA_DIR, diff --git a/packages/@uppy/companion-client/src/Provider.ts b/packages/@uppy/companion-client/src/Provider.ts index b8e12dc27..2c9bc7b93 100644 --- a/packages/@uppy/companion-client/src/Provider.ts +++ b/packages/@uppy/companion-client/src/Provider.ts @@ -166,6 +166,7 @@ export default class Provider }): string { const params = new URLSearchParams({ ...query, + // This is only used for Companion instances configured to accept multiple origins. state: btoa(JSON.stringify({ origin: getOrigin() })), ...this.authQuery({ authFormData }), }) diff --git a/packages/@uppy/companion/env_example b/packages/@uppy/companion/env_example index 89e7b2298..08b3e0acf 100644 --- a/packages/@uppy/companion/env_example +++ b/packages/@uppy/companion/env_example @@ -39,6 +39,7 @@ COMPANION_AWS_BUCKET= COMPANION_AWS_ENDPOINT= COMPANION_AWS_REGION= COMPANION_AWS_PREFIX= +COMPANION_AWS_FORCE_PATH_STYLE="false" COMPANION_ZOOM_KEY= COMPANION_ZOOM_SECRET= diff --git a/packages/@uppy/companion/src/server/controllers/connect.js b/packages/@uppy/companion/src/server/controllers/connect.js index b5d3b9c79..772f80fec 100644 --- a/packages/@uppy/companion/src/server/controllers/connect.js +++ b/packages/@uppy/companion/src/server/controllers/connect.js @@ -12,12 +12,25 @@ const queryString = (params, prefix = '?') => { * @param {object} res */ module.exports = function connect(req, res) { - const { secret } = req.companion.options + const { secret, oauthOrigin } = req.companion.options const stateObj = oAuthState.generateState() - if (req.query.state) { + // not sure if we need to store origin in the session state (e.g. we could've just gotten it directly inside send-token) + // but we're afraid to change the logic there + if (oauthOrigin && !Array.isArray(oauthOrigin)) { + // If the server only allows a single origin, we ignore the client-supplied + // origin from query because we don't need it. + stateObj.origin = oauthOrigin + } else if (oauthOrigin && oauthOrigin.length < 2) { + // eslint-disable-next-line prefer-destructuring + stateObj.origin = oauthOrigin[0] + } else { + // If we have multiple allowed origins, we need to check the client-supplied origin from query. + // If the client provides an untrusted origin, + // we want to send `undefined`. `undefined` means `/`, which is the same origin when passed to `postMessage`. + // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-window-postmessage-options-dev const { origin } = JSON.parse(atob(req.query.state)) - stateObj.origin = origin + stateObj.origin = oauthOrigin ? oauthOrigin.find(o => o === origin) : origin } if (req.companion.options.server.oauthDomain) { diff --git a/packages/@uppy/companion/src/server/s3-client.js b/packages/@uppy/companion/src/server/s3-client.js index d5b5c48be..74717d238 100644 --- a/packages/@uppy/companion/src/server/s3-client.js +++ b/packages/@uppy/companion/src/server/s3-client.js @@ -29,6 +29,7 @@ module.exports = (companionOptions, createPresignedPostMode = false) => { /** @type {import('@aws-sdk/client-s3').S3ClientConfig} */ let s3ClientOptions = { region: s3.region, + forcePathStyle: Boolean(s3.forcePathStyle) } if (s3.useAccelerateEndpoint) { diff --git a/packages/@uppy/companion/src/standalone/helper.js b/packages/@uppy/companion/src/standalone/helper.js index 76662d1fc..9f85d51be 100644 --- a/packages/@uppy/companion/src/standalone/helper.js +++ b/packages/@uppy/companion/src/standalone/helper.js @@ -133,6 +133,7 @@ const getConfigFromEnv = () => { process.env.COMPANION_AWS_USE_ACCELERATE_ENDPOINT === 'true', expires: parseInt(process.env.COMPANION_AWS_EXPIRES || '800', 10), acl: process.env.COMPANION_AWS_ACL, + forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true', }, server: { host: process.env.COMPANION_DOMAIN, @@ -182,6 +183,9 @@ const getConfigFromEnv = () => { corsOrigins: getCorsOrigins(), testDynamicOauthCredentials: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS === 'true', testDynamicOauthCredentialsSecret: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS_SECRET, + oauthOrigin: process.env.COMPANION_OAUTH_ORIGIN?.includes(',') ? + process.env.COMPANION_OAUTH_ORIGIN.split(',') : + process.env.COMPANION_OAUTH_ORIGIN, } } diff --git a/packages/@uppy/companion/test/__tests__/uploader.js b/packages/@uppy/companion/test/__tests__/uploader.js index 3e6d1398a..8cf3c2c93 100644 --- a/packages/@uppy/companion/test/__tests__/uploader.js +++ b/packages/@uppy/companion/test/__tests__/uploader.js @@ -20,6 +20,7 @@ afterAll(() => { process.env.COMPANION_DATADIR = './test/output' process.env.COMPANION_DOMAIN = 'localhost:3020' +process.env.COMPANION_OAUTH_ORIGIN = '*' const { companionOptions } = standalone() const mockReq = {} diff --git a/packages/@uppy/companion/test/mockserver.js b/packages/@uppy/companion/test/mockserver.js index b236e6505..6afe9cd11 100644 --- a/packages/@uppy/companion/test/mockserver.js +++ b/packages/@uppy/companion/test/mockserver.js @@ -45,6 +45,8 @@ const defaultEnv = { COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '', COMPANION_ENABLE_URL_ENDPOINT: 'true', + + COMPANION_OAUTH_ORIGIN: '*', } function updateEnv (env) { diff --git a/packages/@uppy/core/src/Uppy.test.ts b/packages/@uppy/core/src/Uppy.test.ts index 25f373611..c812cb450 100644 --- a/packages/@uppy/core/src/Uppy.test.ts +++ b/packages/@uppy/core/src/Uppy.test.ts @@ -1947,6 +1947,57 @@ describe('src/Core', () => { }) }) + describe('clearUploadedFiles', () => { + it('should reset state to default', () => { + const core = new Core() + core.addFile({ + source: 'vi', + name: 'foo.jpg', + type: 'image/jpeg', + data: testImage, + }) + core.addFile({ + source: 'vi', + name: 'foo2.jpg', + type: 'image/jpeg', + data: testImage, + }) + core.clearUploadedFiles() + expect(core.getState()).toMatchObject({ + totalProgress: 0, + allowNewUpload: true, + error: null, + recoveredState: null, + files: {}, + }) + }) + + it('should throw error if plugin does not allow removing files during an upload', () => { + const core = new Core() + const newState = { + capabilities: { + individualCancellation: false, + uploadProgress: true, + resumableUploads: false, + }, + currentUploads: { + upload1: { + fileIDs: [ + 'uppy-file1/jpg-1e-image/jpeg', + 'uppy-file2/jpg-1e-image/jpeg', + 'uppy-file3/jpg-1e-image/jpeg', + ], + }, + }, + } + // @ts-ignore + core.setState(newState) + expect(() => { + core.clearUploadedFiles() + }).toThrowError() + }) + }) + describe('checkRestrictions', () => { it('should enforce the maxNumberOfFiles rule', () => { const core = new Core({ diff --git a/packages/@uppy/drag-drop/README.md b/packages/@uppy/drag-drop/README.md index fa6619552..7566041fc 100644 --- a/packages/@uppy/drag-drop/README.md +++ b/packages/@uppy/drag-drop/README.md @@ -9,8 +9,7 @@ Droppable zone UI for Uppy. Drag and drop files into it to upload. -**[Read the docs](https://uppy.io/docs/dragdrop)** | -**[Try it](https://uppy.io/examples/dragdrop/)** +**[Read the docs](https://uppy.io/docs/drag-drop/)** Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service. @@ -41,7 +40,7 @@ Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global ## Documentation Documentation for this plugin can be found on the -[Uppy website](https://uppy.io/docs/dragdrop). +[Uppy website](https://uppy.io/docs/drag-drop/). ## License