-
Notifications
You must be signed in to change notification settings - Fork 148
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
Support for S3 CORS configuration policies #63
Changes from all commits
91c7956
f93a561
83835d6
f0ebed6
6c2edfe
7da2165
7464baa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,43 +34,45 @@ The goal of S3rver is to minimise runtime dependencies and be more of a developm | |
Install s3rver: | ||
|
||
```bash | ||
npm install s3rver -g | ||
$ npm install s3rver -g | ||
``` | ||
You will now have a command on your path called *s3rver* | ||
|
||
Executing this command for the various options: | ||
|
||
```bash | ||
s3rver --help | ||
$ s3rver --help | ||
``` | ||
|
||
## Supported clients | ||
|
||
Please see [Fake S3s wiki page](https://github.com/jubos/fake-s3/wiki/Supported-Clients) for a list of supported clients. | ||
When listening on HTTPS with a self-signed certificate, the AWS SDK in a Node.js environment will need ```httpOptions: { agent: new https.Agent({ rejectUnauthorized: false }) }``` in order to allow interaction. | ||
When listening on HTTPS with a self-signed certificate, the AWS SDK in a Node.js environment will need `httpOptions: { agent: new https.Agent({ rejectUnauthorized: false }) }` in order to allow interaction. | ||
|
||
Please test, if you encounter any problems please do not hesitate to open an issue :) | ||
|
||
## Static Website Hosting | ||
|
||
If you specify an *indexDocument* then ```get``` requests will serve the *indexDocument* if it is found, simulating the static website mode of AWS S3. An *errorDocument* can also be set, to serve a custom 404 page. | ||
If you specify an *indexDocument* then `GET` requests will serve the *indexDocument* if it is found, simulating the static website mode of AWS S3. An *errorDocument* can also be set, to serve a custom 404 page. | ||
|
||
### Hostname Resolution | ||
|
||
By default a bucket name needs to be given. So for a bucket called ```mysite.local```, with an indexDocument of ```index.html```. Visiting ```http://localhost:4568/mysite.local/``` in your browser will display the ```index.html``` file uploaded to the bucket. | ||
By default a bucket name needs to be given. So for a bucket called `mysite.local`, with an indexDocument of `index.html`. Visiting `http://localhost:4568/mysite.local/` in your browser will display the `index.html` file uploaded to the bucket. | ||
|
||
However you can also setup a local hostname in your /etc/hosts file pointing at 127.0.0.1 | ||
``` | ||
localhost 127.0.0.1 | ||
mysite.local 127.0.0.1 | ||
``` | ||
Now you can access the served content at ```http://mysite.local:4568/``` | ||
Now you can access the served content at `http://mysite.local:4568/` | ||
|
||
## Tests | ||
|
||
The tests should be run by one of the active LTS versions. The CI Server runs the tests on version `4.x`, `6.x` and `8.x`. | ||
I recommend using [NVM](https://github.com/creationix/nvm) to manage your node versions. | ||
|
||
The tests should be run by one of the active LTS versions. The CI Server runs the tests on the latest `6.x` and `8.x` releases. | ||
|
||
Be wary that Windows is not fully supported due to the way its filesystem works, so most tests that involve deleting objects will fail. | ||
Use Linux Subsystem for Windows to test if it's your primary environment. | ||
|
||
To run the test suite, first install the dependencies, then run `npm test`: | ||
|
||
```bash | ||
|
@@ -84,34 +86,60 @@ You can also run s3rver programmatically. | |
|
||
> This is particularly useful if you want to integrate s3rver into another projects tests that depends on access to an s3 environment | ||
|
||
### new S3rver([options]) | ||
|
||
Creates a S3rver instance | ||
|
||
| Option | Type | Default | Description | | ||
| ------ | ---- | ------- | ----------- | | ||
| port | `number` | `4578` | Port of the mock S3 server | | ||
| hostname | `string` | `localhost` | Host/IP to bind to | | ||
| key | `string` \| `Buffer` | | Private key for running with TLS | | ||
| cert | `string` \| `Buffer` | | Certificate for running with TLS | | ||
| silent | `boolean` | `false` | Suppress log messages | | ||
| directory | `string` | | Data directory | | ||
| cors | `string` \| `Buffer` | [S3 Sample policy](test/resources/cors_sample_policy.xml) | Raw XML string or Buffer of CORS policy | | ||
| indexDocument | `string` | | Index document for static web hosting | | ||
| errorDocument | `string` | | Error document for static web hosting | | ||
| removeBucketsOnClose | `boolean` | `false` | Remove all bucket data on server close | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎖 hero |
||
|
||
### s3rver.run(callback) | ||
Starts the server on the configured port and host | ||
|
||
Example in mocha: | ||
|
||
``` | ||
var S3rver = require('s3rver'); | ||
var client; | ||
```javascript | ||
const S3rver = require('s3rver'); | ||
let instance; | ||
|
||
before(function (done) { | ||
client = new S3rver({ | ||
instance = new S3rver({ | ||
port: 4569, | ||
hostname: 'localhost', | ||
silent: false, | ||
directory: '/tmp/s3rver_test_directory' | ||
}).run(function (err, host, port) { | ||
}).run((err, host, port) => { | ||
if(err) { | ||
return done(err); | ||
return done(err); | ||
} | ||
done(); | ||
}); | ||
}); | ||
|
||
after(function (done) { | ||
client.close(done); | ||
instance.close(done); | ||
}); | ||
``` | ||
|
||
### s3rver.callback() ⇒ `function (req, res)` | ||
*Also aliased as* **s3rver.getMiddleware()** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
Creates and returns a callback that can be passed into `http.createServer()` or mounted in an Express app. | ||
|
||
## Using [s3fs-fuse](https://github.com/s3fs-fuse/s3fs-fuse) with S3rver | ||
|
||
You can connect to s3rver and mount a bucket to your local file system by using the following command: | ||
|
||
``` | ||
s3fs bucket1 /tmp/3 -o url="http://localhost:4568" -o use_path_request_style -d -f -o f2 -o curldbg | ||
```bash | ||
$ s3fs bucket1 /tmp/3 -o url="http://localhost:4568" -o use_path_request_style -d -f -o f2 -o curldbg | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
'use strict'; | ||
|
||
const { escapeRegExp, includes } = require('lodash'); | ||
const xml2js = require('xml2js'); | ||
|
||
const templateBuilder = require('./xml-template-builder'); | ||
|
||
function createWildcardRegExp(str, flags = '') { | ||
const parts = str.split('*'); | ||
if (parts.length > 2) throw new Error(`"${str}" can not have more than one wildcard.`); | ||
return new RegExp(`^${parts.map(escapeRegExp).join('.*')}$`, flags); | ||
} | ||
|
||
// See https://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html | ||
module.exports = function cors(config) { | ||
// parse and validate config | ||
let CORSConfiguration; | ||
if (config) { | ||
xml2js.parseString(config, { async: false }, (err, parsed) => { | ||
if (!err) ({ CORSConfiguration } = parsed); | ||
}); | ||
if (!CORSConfiguration || !CORSConfiguration.CORSRule) { | ||
throw new Error('The CORS configuration XML you provided was not well-formed or did not validate'); | ||
} | ||
for (const rule of CORSConfiguration.CORSRule) { | ||
if (!rule.AllowedOrigin || !rule.AllowedMethod) { | ||
throw new Error('CORSRule must have at least one AllowedOrigin and AllowedMethod'); | ||
} | ||
|
||
// Keep track if the rule has the plain wildcard '*' origin since S3 responds with '*' | ||
// instead of echoing back the request origin in this case | ||
rule.hasWildcardOrigin = rule.AllowedOrigin.includes('*'); | ||
rule.AllowedOrigin = rule.AllowedOrigin.map(o => createWildcardRegExp(o)); | ||
rule.AllowedHeader = (rule.AllowedHeader || []).map(h => createWildcardRegExp(h, 'i')); | ||
} | ||
} | ||
|
||
return function (req, res, next) { | ||
// Prefer the Access-Control-Request-Method header if supplied | ||
const method = req.get('access-control-request-method') || req.method; | ||
const matchedRule = CORSConfiguration.CORSRule.find((rule) => { | ||
return rule.AllowedOrigin.some(pattern => pattern.test(req.get('origin'))) | ||
&& includes(rule.AllowedMethod, method); | ||
}); | ||
|
||
if (req.method === 'OPTIONS') { | ||
let template; | ||
|
||
if (!req.get('origin')) { | ||
template = templateBuilder.buildError('BadRequest', | ||
'Insufficient information. Origin request header needed.'); | ||
res.header('Content-Type', 'application/xml'); | ||
return res.status(403).send(template); | ||
} | ||
|
||
if (!req.get('access-control-request-method')) { | ||
template = templateBuilder.buildError('BadRequest', | ||
'Invalid Access-Control-Request-Method: null'); | ||
res.header('Content-Type', 'application/xml'); | ||
return res.status(403).send(template); | ||
} | ||
|
||
// S3 only checks if CORS is enabled *after* checking the existence of access control headers | ||
if (!CORSConfiguration) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the user does not specify a CORS configuration, but their client still sends a preflight If this is how S3 behaves when no CORS config is specified, or if we have a default applied when none is specified, then leave a note and keep this as-is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
template = templateBuilder.buildError('CORSResponse', | ||
'CORS is not enabled for this bucket.'); | ||
res.header('Content-Type', 'application/xml'); | ||
return res.status(403).send(template); | ||
} | ||
|
||
const requestHeaders = req.get('access-control-request-headers') || []; | ||
const allowedHeaders = matchedRule | ||
? requestHeaders | ||
.filter(header => matchedRule.AllowedHeader.some(pattern => pattern.test(header))) | ||
.map(header => header.toLowercase()) | ||
: []; | ||
|
||
if (!matchedRule || allowedHeaders.length < requestHeaders.length) { | ||
template = templateBuilder.buildError('CORSResponse', | ||
'This CORS request is not allowed. ' + | ||
'This is usually because the evalution of Origin, ' + | ||
'request method / Access-Control-Request-Method or Access-Control-Request-Headers ' + | ||
'are not whitelisted by the resource\'s CORS spec.') | ||
res.header('Content-Type', 'application/xml'); | ||
return res.status(403).send(template); | ||
} | ||
|
||
res.header('Access-Control-Allow-Origin', '*'); | ||
res.header('Access-Control-Allow-Methods', matchedRule.AllowedMethod.join(', ')); | ||
if (req.get('access-control-request-headers')) { | ||
res.header('Access-Control-Allow-Headers', allowedHeaders.join(', ')); | ||
} | ||
|
||
res.header('Vary', 'Origin, Access-Control-Request-Headers, Access-Control-Request-Method'); | ||
|
||
return res.status(200).send(); | ||
} else if (CORSConfiguration && req.get('origin')) { | ||
if (matchedRule) { | ||
res.header('Access-Control-Allow-Origin', matchedRule.hasWildcardOrigin ? '*' : req.get('origin')); | ||
if (matchedRule.ExposeHeader) { | ||
res.header('Access-Control-Expose-Headers', matchedRule.ExposeHeader.join(', ')); | ||
} | ||
if (matchedRule.MaxAgeSeconds) { | ||
res.header('Access-Control-Max-Age', matchedRule.MaxAgeSeconds[0]); | ||
} | ||
res.header('Access-Control-Allow-Credentials', true); | ||
res.header('Vary', 'Origin, Access-Control-Request-Headers, Access-Control-Request-Method'); | ||
} | ||
} | ||
next(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great work on this middleware, it looks fantastic and fairly easy to extend/maintain |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@specialkk Do you have any more info about this? You made a comment that you fixed all the tests for Windows?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not in this PR. If you try running tests on Windows without the changes here, you'd run into issues because of either a) permission issues trying to create a test data directory at
/tmp
or b)ENOENT
errors when resetting the data because Windows seems to have trouble recursively deleting all the folders created by the previous test. I partially fixed these by usingos.tmpdir()
and by not having the before hook fail if a bucket already exists.I have a commit that updates syntax to ES2015 across the project and another that has a pretty major refactor of the file system store back end. Incidentally I think I fixed whatever file lock trouble Windows had before in the process.