Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default allow headers #3167

Merged
merged 6 commits into from Sep 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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