Add 'packages/cors/' from commit 'b5c0635952fe9d863f6190495ca99b3f5a8ce378'

git-subtree-dir: packages/cors
git-subtree-mainline: 3a14263a6f
git-subtree-split: b5c0635952
This commit is contained in:
Tobe O 2020-02-15 18:28:42 -05:00
commit 12aa791a8e
11 changed files with 522 additions and 0 deletions

74
packages/cors/.gitignore vendored Normal file
View file

@ -0,0 +1,74 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
.project
.pub/
.scripts-bin/
build/
**/packages/
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
*.dart.js
*.part.js
*.js.deps
*.js.map
*.info.json
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.dart_tool

View file

@ -0,0 +1 @@
language: dart

View file

@ -0,0 +1,2 @@
# 2.0.0
* Updates for Dart 2 and Angel 2.

21
packages/cors/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

8
packages/cors/README.md Normal file
View file

@ -0,0 +1,8 @@
# cors
[![Pub](https://img.shields.io/pub/v/angel_cors.svg)](https://pub.dartlang.org/packages/angel_cors)
[![build status](https://travis-ci.org/angel-dart/cors.svg)](https://travis-ci.org/angel-dart/cors)
Angel CORS middleware.
Port of [the original Express CORS middleware](https://github.com/expressjs/cors).
For complete example usage, see the [example file](example/example.dart).

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,51 @@
import 'dart:async';
import 'package:angel_cors/angel_cors.dart';
import 'package:angel_framework/angel_framework.dart';
Future configureServer(Angel app) async {
// The default options will allow CORS for any request.
// Combined with `fallback`, you can enable CORS application-wide.
app.fallback(cors());
// You can also enable CORS for a single route.
app.get(
'/my_api',
chain([
cors(),
(req, res) {
// Request handling logic here...
}
]),
);
// Likewise, you can apply CORS to a group.
app.chain([cors()]).group('/api', (router) {
router.get('/version', (req, res) => 'v0');
});
// Of course, you can configure CORS.
// The following is just a subset of the available options;
app.fallback(cors(
CorsOptions(
origin: 'https://pub.dartlang.org', successStatus: 200, // default 204
allowedHeaders: ['POST'],
preflightContinue: false, // default false
),
));
// You can specify the origin in different ways:
CorsOptions(origin: 'https://pub.dartlang.org');
CorsOptions(origin: ['https://example.org', 'http://foo.bar']);
CorsOptions(origin: RegExp(r'^foo\.[^$]+'));
CorsOptions(origin: (String s) => s.length == 4);
// Lastly, you can dynamically configure CORS:
app.fallback(dynamicCors((req, res) {
return CorsOptions(
origin: [
req.headers.value('origin') ?? 'https://pub.dartlang.org',
RegExp(r'\.com$'),
],
);
}));
}

View file

@ -0,0 +1,100 @@
/// Angel CORS middleware.
library angel_cors;
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'src/cors_options.dart';
export 'src/cors_options.dart';
/// Determines if a request origin is CORS-able.
typedef bool _CorsFilter(String origin);
bool _isOriginAllowed(String origin, [allowedOrigin]) {
allowedOrigin ??= [];
if (allowedOrigin is Iterable) {
return allowedOrigin.any((x) => _isOriginAllowed(origin, x));
} else if (allowedOrigin is String) {
return origin == allowedOrigin;
} else if (allowedOrigin is RegExp) {
return origin != null && allowedOrigin.hasMatch(origin);
} else if (origin != null && allowedOrigin is _CorsFilter) {
return allowedOrigin(origin);
} else {
return allowedOrigin != false;
}
}
/// On-the-fly configures the [cors] handler. Use this when the context of the surrounding request
/// is necessary to decide how to handle an incoming request.
Future<bool> Function(RequestContext, ResponseContext) dynamicCors(
FutureOr<CorsOptions> Function(RequestContext, ResponseContext) f) {
return (req, res) async {
var opts = await f(req, res);
var handler = cors(opts);
return await handler(req, res);
};
}
/// Applies the given [CorsOptions].
Future<bool> Function(RequestContext, ResponseContext) cors(
[CorsOptions options]) {
options ??= CorsOptions();
return (req, res) async {
// access-control-allow-credentials
if (options.credentials == true) {
res.headers['access-control-allow-credentials'] = 'true';
}
// access-control-allow-headers
if (req.method == 'OPTIONS' && options.allowedHeaders.isNotEmpty) {
res.headers['access-control-allow-headers'] =
options.allowedHeaders.join(',');
} else if (req.headers['access-control-request-headers'] != null) {
res.headers['access-control-allow-headers'] =
req.headers.value('access-control-request-headers');
}
// access-control-expose-headers
if (options.exposedHeaders.isNotEmpty) {
res.headers['access-control-expose-headers'] =
options.exposedHeaders.join(',');
}
// access-control-allow-methods
if (req.method == 'OPTIONS' && options.methods.isNotEmpty) {
res.headers['access-control-allow-methods'] = options.methods.join(',');
}
// access-control-max-age
if (req.method == 'OPTIONS' && options.maxAge != null) {
res.headers['access-control-max-age'] = options.maxAge.toString();
}
// access-control-allow-origin
if (options.origin == false || options.origin == '*') {
res.headers['access-control-allow-origin'] = '*';
} else if (options.origin is String) {
res
..headers['access-control-allow-origin'] = options.origin as String
..headers['vary'] = 'origin';
} else {
bool isAllowed =
_isOriginAllowed(req.headers.value('origin'), options.origin);
res.headers['access-control-allow-origin'] =
isAllowed ? req.headers.value('origin') : false.toString();
if (isAllowed) {
res.headers['vary'] = 'origin';
}
}
if (req.method != 'OPTIONS') return true;
res.statusCode = options.successStatus ?? 204;
res.contentLength = 0;
await res.close();
return options.preflightContinue;
};
}

View file

@ -0,0 +1,73 @@
/// CORS configuration options.
///
/// The default configuration is the equivalent of:
///
///```json
///{
/// "origin": "*",
/// "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
/// "preflightContinue": false
///}
/// ```
class CorsOptions {
/// Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header.
final List<String> allowedHeaders = [];
/// Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted.
final bool credentials;
/// Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed.
final List<String> exposedHeaders = [];
/// Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted.
///
/// Default: `null`
final int maxAge;
/// The status code to be sent on successful `OPTIONS` requests, if [preflightContinue] is `false`.
final int successStatus;
/// Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`).
///
/// Default: `['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE˝']`
final List<String> methods = [];
/// Configures the **Access-Control-Allow-Origin** CORS header.
/// Possible values:
/// - `Boolean` - set `origin` to `true` to reflect the [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), as defined by `req.header('Origin')`, or set it to `false` to disable CORS.
/// - `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed.
/// - `RegExp` - set `origin` to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com".
/// - `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com".
/// - `bool Function(String)` - set `origin` to a function implementing some custom logic. The function takes the request origin as the first parameter and returns a [bool].
///
/// Default: `'*'`
final origin;
/// If `false`, then the [cors] handler will terminate the response after performing its logic.
///
/// Default: `false`
final bool preflightContinue;
CorsOptions(
{Iterable<String> allowedHeaders = const [],
this.credentials,
this.maxAge,
Iterable<String> methods = const [
'GET',
'HEAD',
'PUT',
'PATCH',
'POST',
'DELETE'
],
this.origin = '*',
this.successStatus = 204,
this.preflightContinue = false,
Iterable<String> exposedHeaders = const []}) {
if (allowedHeaders != null) this.allowedHeaders.addAll(allowedHeaders);
if (methods != null) this.methods.addAll(methods);
if (exposedHeaders != null) this.exposedHeaders.addAll(exposedHeaders);
}
}

View file

@ -0,0 +1,14 @@
author: Tobe O <thosakwe@gmail.com>
description: Angel CORS middleware. Port of expressjs/cors to the Angel framework.
environment:
sdk: ">=2.0.0 <3.0.0"
homepage: https://github.com/angel-dart/cors.git
name: angel_cors
version: 2.0.0
dependencies:
angel_framework: ^2.0.0-alpha
dev_dependencies:
angel_test: ^2.0.0
http: ^0.12.0
pedantic: ^1.0.0
test: ^1.0.0

View file

@ -0,0 +1,174 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_cors/angel_cors.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
main() {
Angel app;
AngelHttp server;
http.Client client;
setUp(() async {
app = Angel()
..options('/credentials', cors(CorsOptions(credentials: true)))
..options('/credentials_d',
dynamicCors((req, res) => CorsOptions(credentials: true)))
..options(
'/headers', cors(CorsOptions(exposedHeaders: ['x-foo', 'x-bar'])))
..options('/max_age', cors(CorsOptions(maxAge: 250)))
..options('/methods', cors(CorsOptions(methods: ['GET', 'POST'])))
..get(
'/originl',
chain([
cors(CorsOptions(
origin: ['foo.bar', 'baz.quux'],
)),
(req, res) => req.headers['origin']
]))
..get(
'/origins',
chain([
cors(CorsOptions(
origin: 'foo.bar',
)),
(req, res) => req.headers['origin']
]))
..get(
'/originr',
chain([
cors(CorsOptions(
origin: RegExp(r'^foo\.[^x]+$'),
)),
(req, res) => req.headers['origin']
]))
..get(
'/originp',
chain([
cors(CorsOptions(
origin: (String s) => s.endsWith('.bar'),
)),
(req, res) => req.headers['origin']
]))
..options('/status', cors(CorsOptions(successStatus: 418)))
..fallback(cors(CorsOptions()))
..post('/', (req, res) async {
res.write('hello world');
})
..fallback((req, res) => throw AngelHttpException.notFound());
server = AngelHttp(app);
await server.startServer('127.0.0.1', 0);
client = http.Client();
});
tearDown(() async {
await server.close();
app = null;
client = null;
});
test('status 204 by default', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.statusCode, 204);
});
test('content length 0 by default', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.contentLength, 0);
});
test('custom successStatus', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/status'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.statusCode, 418);
});
test('max age', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/max_age'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-max-age'], '250');
});
test('methods', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/methods'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-methods'], 'GET,POST');
});
test('dynamicCors.credentials', () async {
var rq =
http.Request('OPTIONS', server.uri.replace(path: '/credentials_d'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-credentials'], 'true');
});
test('credentials', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/credentials'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-allow-credentials'], 'true');
});
test('exposed headers', () async {
var rq = http.Request('OPTIONS', server.uri.replace(path: '/headers'));
var response = await client.send(rq).then(http.Response.fromStream);
expect(response.headers['access-control-expose-headers'], 'x-foo,x-bar');
});
test('invalid origin', () async {
var response = await client.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'foreign'});
expect(response.headers['access-control-allow-origin'], 'false');
});
test('list origin', () async {
var response = await client.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin');
response = await client.get(server.uri.replace(path: '/originl'),
headers: {'origin': 'baz.quux'});
expect(response.headers['access-control-allow-origin'], 'baz.quux');
expect(response.headers['vary'], 'origin');
});
test('string origin', () async {
var response = await client.get(server.uri.replace(path: '/origins'),
headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin');
});
test('regex origin', () async {
var response = await client.get(server.uri.replace(path: '/originr'),
headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin');
});
test('predicate origin', () async {
var response = await client.get(server.uri.replace(path: '/originp'),
headers: {'origin': 'foo.bar'});
expect(response.headers['access-control-allow-origin'], 'foo.bar');
expect(response.headers['vary'], 'origin');
});
test('POST works', () async {
final response = await client.post(server.uri);
expect(response.statusCode, equals(200));
print('Response: ${response.body}');
print('Headers: ${response.headers}');
expect(response.headers['access-control-allow-origin'], equals('*'));
});
test('mirror headers', () async {
final response = await client
.post(server.uri, headers: {'access-control-request-headers': 'foo'});
expect(response.statusCode, equals(200));
print('Response: ${response.body}');
print('Headers: ${response.headers}');
expect(response.headers['access-control-allow-headers'], equals('foo'));
});
}