Skip to content

Commit

Permalink
Default allow headers (#3167)
Browse files Browse the repository at this point in the history
* refactor cors middleware to avoid duplicates

* Make more ACAH headers default

Add the following headers to access-control-allow-headers by default:
Authorization, Origin, Content-Type, Accept
They are needed for basic operation. See #3021
therefore also remove custom middlewares in examples and standalone

* Update outdated readme for S3

AWS now requires JSON instead of XML format

* Update packages/@uppy/companion/src/server/middlewares.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>

* Update packages/@uppy/companion/src/server/middlewares.js

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>

* fix review comments

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
  • Loading branch information
mifi and aduh95 committed Sep 30, 2021
1 parent 1ae19a2 commit 12705e7
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 110 deletions.
12 changes: 0 additions & 12 deletions examples/custom-provider/server/index.js
Expand Up @@ -14,18 +14,6 @@ app.use(session({
saveUninitialized: true,
}))

app.use((req, res, next) => {
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
next()
})

// Routes
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/plain')
Expand Down
8 changes: 0 additions & 8 deletions examples/uppy-with-companion/server/index.js
Expand Up @@ -14,14 +14,6 @@ app.use(session({

app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE'
)
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
next()
})

Expand Down
46 changes: 30 additions & 16 deletions packages/@uppy/companion/src/server/middlewares.js
Expand Up @@ -79,25 +79,39 @@ exports.loadSearchProviderToken = (req, res, next) => {
}

exports.cors = (options = {}) => (req, res, next) => {
const exposedHeaders = [
// exposed so it can be accessed for our custom uppy client preflight
'Access-Control-Allow-Headers',
]
if (options.sendSelfEndpoint) exposedHeaders.push('i-am')
if (res.get('Access-Control-Expose-Headers')) exposedHeaders.push(res.get('Access-Control-Expose-Headers'))
// HTTP headers are not case sensitive, and express always handles them in lower case, so that's why we lower case them.
// I believe that HTTP verbs are case sensitive, and should be uppercase.

// TODO: Move to optional chaining when we drop Node.js v12.x support
const existingExposeHeaders = res.get('Access-Control-Expose-Headers')
const exposeHeadersSet = new Set(existingExposeHeaders && existingExposeHeaders.split(',').map(method => method.trim().toLowerCase()))

// exposed so it can be accessed for our custom uppy client preflight
exposeHeadersSet.add('access-control-allow-headers')
if (options.sendSelfEndpoint) exposeHeadersSet.add('i-am')

// Needed for basic operation: https://github.com/transloadit/uppy/issues/3021
const allowedHeaders = [
'uppy-auth-token',
'uppy-versions',
'uppy-credentials-params',
'authorization',
'origin',
'content-type',
'accept',
]
if (res.get('Access-Control-Allow-Headers')) allowedHeaders.push(res.get('Access-Control-Allow-Headers'))

// TODO: Move to optional chaining when we drop Node.js v12.x support
const ACAMHeader = res.get('Access-Control-Allow-Methods')
const existingAllowMethodsHeader = new Set(ACAMHeader && ACAMHeader.split(',').map(method => method.trim().toUpperCase()))
existingAllowMethodsHeader.add('GET').add('POST').add('OPTIONS').add('DELETE')
const methods = Array.from(existingAllowMethodsHeader)
const existingAllowHeaders = res.get('Access-Control-Allow-Headers')
const allowHeadersSet = new Set(existingAllowHeaders
? existingAllowHeaders
.split(',')
.map((method) => method.trim().toLowerCase())
.concat(allowedHeaders)
: allowedHeaders)

const existingAllowMethods = res.get('Access-Control-Allow-Methods')
const allowMethodsSet = new Set(existingAllowMethods && existingAllowMethods.split(',').map(method => method.trim().toUpperCase()))
// Needed for basic operation:
allowMethodsSet.add('GET').add('POST').add('OPTIONS').add('DELETE')

// If endpoint urls are specified, then we only allow those endpoints.
// Otherwise, we allow any client url to access companion.
Expand All @@ -110,9 +124,9 @@ exports.cors = (options = {}) => (req, res, next) => {
return cors({
credentials: true,
origin,
methods,
allowedHeaders: allowedHeaders.join(','),
exposedHeaders: exposedHeaders.join(','),
methods: Array.from(allowMethodsSet),
allowedHeaders: Array.from(allowHeadersSet).join(','),
exposedHeaders: Array.from(exposeHeadersSet).join(','),
})(req, res, next)
}

Expand Down
8 changes: 0 additions & 8 deletions packages/@uppy/companion/src/standalone/index.js
Expand Up @@ -133,14 +133,6 @@ module.exports = function server (inputCompanionOptions = {}) {

app.use(session(sessionOptions))

app.use((req, res, next) => {
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization, Origin, Content-Type, Accept'
)
next()
})

// Routes
if (process.env.COMPANION_HIDE_WELCOME !== 'true') {
app.get('/', (req, res) => {
Expand Down
16 changes: 8 additions & 8 deletions packages/@uppy/companion/test/__tests__/cors.js
Expand Up @@ -41,8 +41,8 @@ describe('cors', () => {
['Vary', 'Origin'],
['Access-Control-Allow-Credentials', 'true'],
['Access-Control-Allow-Methods', 'PATCH,OPTIONS,POST,GET,DELETE'],
['Access-Control-Allow-Headers', 'uppy-auth-token,uppy-versions,uppy-credentials-params,test-allow-header'],
['Access-Control-Expose-Headers', 'Access-Control-Allow-Headers,i-am,test'],
['Access-Control-Allow-Headers', 'test-allow-header,uppy-auth-token,uppy-versions,uppy-credentials-params,authorization,origin,content-type,accept'],
['Access-Control-Expose-Headers', 'test,access-control-allow-headers,i-am'],
['Content-Length', '0'],
])
// expect(next).toHaveBeenCalled()
Expand All @@ -55,8 +55,8 @@ describe('cors', () => {
['Vary', 'Origin'],
['Access-Control-Allow-Credentials', 'true'],
['Access-Control-Allow-Methods', 'GET,POST,OPTIONS,DELETE'],
['Access-Control-Allow-Headers', 'uppy-auth-token,uppy-versions,uppy-credentials-params'],
['Access-Control-Expose-Headers', 'Access-Control-Allow-Headers'],
['Access-Control-Allow-Headers', 'uppy-auth-token,uppy-versions,uppy-credentials-params,authorization,origin,content-type,accept'],
['Access-Control-Expose-Headers', 'access-control-allow-headers'],
['Content-Length', '0'],
])
})
Expand All @@ -72,8 +72,8 @@ describe('cors', () => {
['Vary', 'Origin'],
['Access-Control-Allow-Credentials', 'true'],
['Access-Control-Allow-Methods', 'GET,POST,OPTIONS,DELETE'],
['Access-Control-Allow-Headers', 'uppy-auth-token,uppy-versions,uppy-credentials-params'],
['Access-Control-Expose-Headers', 'Access-Control-Allow-Headers'],
['Access-Control-Allow-Headers', 'uppy-auth-token,uppy-versions,uppy-credentials-params,authorization,origin,content-type,accept'],
['Access-Control-Expose-Headers', 'access-control-allow-headers'],
['Content-Length', '0'],
])
})
Expand All @@ -85,8 +85,8 @@ describe('cors', () => {
['Vary', 'Origin'],
['Access-Control-Allow-Credentials', 'true'],
['Access-Control-Allow-Methods', 'GET,POST,OPTIONS,DELETE'],
['Access-Control-Allow-Headers', 'uppy-auth-token,uppy-versions,uppy-credentials-params'],
['Access-Control-Expose-Headers', 'Access-Control-Allow-Headers'],
['Access-Control-Allow-Headers', 'uppy-auth-token,uppy-versions,uppy-credentials-params,authorization,origin,content-type,accept'],
['Access-Control-Expose-Headers', 'access-control-allow-headers'],
['Content-Length', '0'],
])
})
Expand Down
40 changes: 24 additions & 16 deletions website/src/docs/aws-s3-multipart.md
Expand Up @@ -150,20 +150,28 @@ The default implementation calls out to Companion's S3 signing endpoints.

## S3 Bucket Configuration

S3 buckets do not allow public uploads by default. In order to allow Uppy to upload to a bucket directly, its CORS permissions need to be configured.

This process is described in the [AwsS3 documentation](/docs/aws-s3/#S3-Bucket-configuration).

While the Uppy AWS S3 plugin uses `POST` requests while uploading files to an S3 bucket, the AWS S3 Multipart plugin uses `PUT` requests when uploading file parts. Additionally, the `ETag` header must also be whitelisted:

```xml
<CORSRule>
<!-- Change from POST to PUT if you followed the docs for the AWS S3 plugin ... -->
<AllowedMethod>PUT</AllowedMethod>

<!-- ... keep the existing MaxAgeSeconds and AllowedHeader lines and your other stuff ... -->

<!-- ... and don't forget to add this tag. -->
<ExposeHeader>ETag</ExposeHeader>
</CORSRule>
S3 buckets do not allow public uploads by default. In order to allow Uppy to upload to a bucket directly, its CORS permissions need to be configured. This process is described in the [AwsS3 documentation](/docs/aws-s3/#S3-Bucket-configuration).

While the Uppy AWS S3 plugin uses `POST` requests when uploading files to an S3 bucket, the AWS S3 Multipart plugin uses `PUT` requests when uploading file parts. Additionally, the `ETag` header must also be exposed (in the response):

```json
[
{
"AllowedOrigins": ["https://my-app.com"],
"AllowedMethods": ["GET", "PUT"],
"MaxAgeSeconds": 3000,
"AllowedHeaders": [
"Authorization",
"x-amz-date",
"x-amz-content-sha256",
"content-type"
]
"ExposedHeaders": ["ETag"]
},
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET"],
"MaxAgeSeconds": 3000
}
]
```
92 changes: 50 additions & 42 deletions website/src/docs/aws-s3.md
Expand Up @@ -135,71 +135,79 @@ In order to allow Uppy to upload directly to a bucket, at least its CORS permiss

CORS permissions can be found in the [S3 Management Console](https://console.aws.amazon.com/s3/home).
Click the bucket that will receive the uploads, then go into the "Permissions" tab and select the "CORS configuration" button.
An XML document will be shown that contains the CORS configuration.
A JSON document will be shown that contains the CORS configuration. (AWS used to use XML but now only allow JSON). More information about the [S3 CORS format here](https://docs.amazonaws.cn/en_us/AmazonS3/latest/userguide/ManageCorsUsing.html).

It is good practice to use two CORS rules: one for viewing the uploaded files, and one for uploading files.

Depending on which settings were enabled during bucket creation, AWS S3 may have defined a CORS rule that allows public reading already.
This rule looks like:

```xml
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
</CORSRule>
```json
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET"],
"MaxAgeSeconds": 3000
}
```

If uploaded files should be publically viewable, but a rule like this is not present, add it.

A different `<CORSRule>` is necessary to allow uploading.
A different rule is necessary to allow uploading.
This rule should come _before_ the existing rule, because S3 only uses the first rule that matches the origin of the request.

At minimum, the domain from which the uploads will happen must be whitelisted, and the definitions from the previous rule must be added:

```xml
<AllowedOrigin>https://my-app.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
```json
{
"AllowedOrigins": ["https://my-app.com"],
"AllowedMethods": ["GET"],
"MaxAgeSeconds": 3000
}
```

When using Companion, which generates a POST policy document, the following permissions must be granted:

```xml
<AllowedMethod>POST</AllowedMethod>
<AllowedHeader>Authorization</AllowedHeader>
<AllowedHeader>x-amz-date</AllowedHeader>
<AllowedHeader>x-amz-content-sha256</AllowedHeader>
<AllowedHeader>content-type</AllowedHeader>
```json
{
"AllowedMethods": ["POST"],
"AllowedHeaders": [
"Authorization",
"x-amz-date",
"x-amz-content-sha256",
"content-type"
]
}
```

When using a presigned upload URL, the following permissions must be granted:

```xml
<AllowedMethod>PUT</AllowedMethod>
```json
{
"AllowedMethods": ["PUT"],
}
```

The final configuration should look something like this:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>https://my-app.com</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>Authorization</AllowedHeader>
<AllowedHeader>x-amz-date</AllowedHeader>
<AllowedHeader>x-amz-content-sha256</AllowedHeader>
<AllowedHeader>content-type</AllowedHeader>
</CORSRule>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>
The final configuration should look something like this (note that it contains two rules in an array `[]`):

```json
[
{
"AllowedOrigins": ["https://my-app.com"],
"AllowedMethods": ["GET", "POST"],
"MaxAgeSeconds": 3000,
"AllowedHeaders": [
"Authorization",
"x-amz-date",
"x-amz-content-sha256",
"content-type"
]
},
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET"],
"MaxAgeSeconds": 3000
}
]
```

Even with these CORS rules in place, you browser might still encounter HTTP status 403 responses with `AccessDenied` in the response body when it tries to `POST` to your bucket. In this case, within the "Permissions" tab of the [S3 Management Console](https://console.aws.amazon.com/s3/home), choose "Public access settings".
Expand Down Expand Up @@ -258,7 +266,7 @@ You do not need to configure the region with GCS.

You also need to configure CORS differently. Unlike Amazon, Google does not offer a UI for CORS configurations. Instead, an HTTP API must be used. If you haven't done this already, see [Configuring CORS on a Bucket](https://cloud.google.com/storage/docs/configuring-cors#Configuring-CORS-on-a-Bucket) in the GCS documentation, or follow the steps below to do it using Google's API playground.

GCS has multiple CORS formats, both XML and JSON. Unfortunately, their XML format is different from Amazon's, so we can't simply use the one from the [S3 Bucket configuration](#S3-Bucket-configuration) section. Google appears to favour the JSON format, so we will use that.
GCS has multiple CORS formats, both XML and JSON. Unfortunately, their formats are different from Amazon's, so we can't simply use the one from the [S3 Bucket configuration](#S3-Bucket-configuration) section. Google appears to favour the JSON format, so we will use that.

#### JSON CORS configuration

Expand Down

0 comments on commit 12705e7

Please sign in to comment.