Skip to content

Commit

Permalink
Add CORS middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
deriegle committed Aug 23, 2020
1 parent 4a190b3 commit f0e1643
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 12 deletions.
2 changes: 2 additions & 0 deletions example/dart_express_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ void main() {
var app = express();

app.use(BodyParser.json());
app.use(CorsMiddleware.use());

app.engine(MarkdownEngine.use());
app.engine(MustacheEngine.use());
app.engine(JaelEngine.use());
Expand Down
15 changes: 11 additions & 4 deletions lib/dart_express.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,32 @@ import 'package:mustache4dart/mustache4dart.dart' deferred as mustache;
import 'package:path_to_regexp/path_to_regexp.dart';

export 'dart:io' show HttpStatus;

/// Top level classes
part 'src/dart_express_base.dart';
part 'src/route.dart';
part 'src/repositories/file_repository.dart';
part 'src/app.dart';
part 'src/view.dart';
part 'src/layer.dart';
part 'src/router.dart';
part 'src/request.dart';
part 'src/response.dart';
part 'src/http_methods.dart';

/// Repositories
part 'src/repositories/file_repository.dart';

/// Middleware
part 'src/middleware/body_parser.dart';
part 'src/middleware/init.dart';
part 'src/http_methods.dart';
part 'src/middleware/cors.dart';

/// Exceptions
part 'src/exceptions/view_exception.dart';

/// View Engines
part 'src/engines/engine.dart';
part 'src/engines/mustache.dart';
part 'src/engines/html.dart';
part 'src/engines/markdown.dart';
part 'src/engines/jael.dart';

// TODO: Export any libraries intended for clients of this package.
2 changes: 1 addition & 1 deletion lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class App {
}

Router _lazyRouter() {
return _router ??= Router().use(_Middleware.init);
return _router ??= Router().use(_InitMiddleware.init);
}

View _getViewFromFileName(String fileName) {
Expand Down
11 changes: 10 additions & 1 deletion lib/src/http_methods.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ class _HTTPMethods {
static const HEAD = 'HEAD';
static const PATCH = 'PATCH';
static const PUT = 'PUT';
static const OPTIONS = 'OPTIONS';

static const ALL = [GET, POST, DELETE, HEAD, PATCH, PUT];
static const ALL = [
GET,
POST,
DELETE,
HEAD,
PATCH,
PUT,
OPTIONS,
];
}
2 changes: 1 addition & 1 deletion lib/src/layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Layer {
}

return true;
} else if (name == _Middleware.name) {
} else if (name == _InitMiddleware.name) {
return true;
}

Expand Down
4 changes: 2 additions & 2 deletions lib/src/middleware/body_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ class BodyParser {
convertBodyToJson(req).then((Map<String, dynamic> json) {
req.body = json;

req?.next();
req.next();
});
} else {
req?.next();
req.next();
}
};
}
Expand Down
216 changes: 216 additions & 0 deletions lib/src/middleware/cors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
part of dart_express;

class CorsOptions {
final dynamic origin;
final List<String> methods;
final bool preflightContinue;
final int optionsSuccessStatus;
final bool credentials;
final List<String> allowedHeaders;
final List<String> exposedHeaders;
final int maxAge;

const CorsOptions({
this.origin = '*',
this.methods = _HTTPMethods.ALL,
this.preflightContinue = false,
this.optionsSuccessStatus = 204,
this.credentials = false,
this.allowedHeaders = const <String>[],
this.exposedHeaders = const <String>[],
this.maxAge,
});

CorsOptions copyWith({
dynamic origin,
List<String> methods,
bool preflightContinue,
int optionsSuccessStatus,
bool credentials,
List<String> allowedHeaders,
List<String> exposedHeaders,
int maxAge,
}) {
return CorsOptions(
origin: origin ?? this.origin,
methods: methods ?? this.methods,
preflightContinue: preflightContinue ?? this.preflightContinue,
optionsSuccessStatus: optionsSuccessStatus ?? this.optionsSuccessStatus,
credentials: credentials ?? this.credentials,
allowedHeaders: allowedHeaders ?? this.allowedHeaders,
exposedHeaders: exposedHeaders ?? this.exposedHeaders,
maxAge: maxAge ?? this.maxAge,
);
}
}

class CorsMiddleware {
static RouteMethod use({
dynamic origin,
List<String> methods,
bool preflightContinue,
int optionsSuccessStatus,
bool credentials,
List<String> allowedHeaders,
List<String> exposedHeaders,
int maxAge,
}) {
final options = CorsOptions(
origin: origin,
methods: methods,
preflightContinue: preflightContinue,
optionsSuccessStatus: optionsSuccessStatus,
credentials: credentials,
allowedHeaders: allowedHeaders,
exposedHeaders: exposedHeaders,
maxAge: maxAge,
);

return (Request req, Response res) {
final headers = <MapEntry>[];

if (req.method == _HTTPMethods.OPTIONS) {
headers.addAll(configureOrigin(options, req));
headers.add(configureCredentials(options));
headers.add(configureMethods(options));
headers.addAll(configureAllowedHeaders(options, req));
headers.add(configureMaxAge(options));
headers.add(configureExposedHeaders(options));
_applyHeaders(req, headers);

if (options.preflightContinue) {
req.next();
} else {
res.statusCode = options.optionsSuccessStatus;
res.headers.contentLength = 0;
res.end();
}
} else {
headers.addAll(configureOrigin(options, req));
headers.add(configureCredentials(options));
headers.add(configureExposedHeaders(options));
_applyHeaders(req, headers);
req.next();
}
};
}

static void _applyHeaders(Request req, List<MapEntry> headers) {
headers
.forEach((mapEntry) => req.headers.add(mapEntry.key, mapEntry.value));
}

static bool isOriginAllowed(String origin, dynamic allowedOrigin) {
if (allowedOrigin is List) {
for (var i = 0; i < allowedOrigin.length; ++i) {
if (isOriginAllowed(origin, allowedOrigin[i])) {
return true;
}
}
return false;
} else if (allowedOrigin is String) {
return origin == allowedOrigin;
} else if (allowedOrigin is RegExp) {
return allowedOrigin.hasMatch(origin);
} else {
return !!allowedOrigin;
}
}

static List<MapEntry> configureOrigin(CorsOptions options, Request req) {
final requestOrigin = req.headers.value('origin');
final headers = <MapEntry>[];
bool isAllowed;

if (options.origin != null && options.origin == '*') {
// allow any origin
headers.add(
MapEntry('Access-Control-Allow-Origin', '*'),
);
} else if (options.origin is String) {
// fixed origin
headers.add(
MapEntry('Access-Control-Allow-Origin', options.origin),
);
headers.add(
MapEntry('Vary', 'Origin'),
);
} else {
isAllowed = isOriginAllowed(requestOrigin, options.origin);
// reflect origin
headers.add(
MapEntry(
'Access-Control-Allow-Origin',
isAllowed ? requestOrigin : false,
),
);
headers.add(MapEntry('Vary', 'Origin'));
}

return headers;
}

static MapEntry configureMethods(CorsOptions options) {
return MapEntry(
'Access-Control-Allow-Methods',
options.methods.join(','),
);
}

static MapEntry configureCredentials(CorsOptions options) {
if (options.credentials) {
return MapEntry('Access-Control-Allow-Credentials', 'true');
}

return null;
}

static List<MapEntry> configureAllowedHeaders(
CorsOptions options, Request req) {
String allowedHeaders;
final headers = <MapEntry>[];

if (options.allowedHeaders == null) {
allowedHeaders = req.headers.value('access-control-request-headers');

headers.add(
MapEntry('Vary', 'Access-Control-Request-Headers'),
);
} else {
allowedHeaders = options.allowedHeaders.join(',');
}

if (allowedHeaders != null && allowedHeaders.isNotEmpty) {
headers.add(MapEntry('Access-Control-Allow-Headers', allowedHeaders));
}

return headers;
}

static MapEntry configureMaxAge(CorsOptions options) {
if (options.maxAge != null) {
return MapEntry(
'Access-Control-Max-Age',
options.maxAge.toString(),
);
}

return null;
}

static MapEntry configureExposedHeaders(CorsOptions options) {
String headers;

if (headers == null) {
return null;
} else if (headers is List) {
headers = options.exposedHeaders.join(',');
}

if (headers != null && headers.isNotEmpty) {
return MapEntry('Access-Control-Expose-Headers', headers);
}

return null;
}
}
2 changes: 1 addition & 1 deletion lib/src/middleware/init.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
part of dart_express;

class _Middleware {
class _InitMiddleware {
static final String name = 'EXPRESS_INIT';

static void init(Request req, Response res) {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ part of dart_express;

class Request {
final HttpRequest _request;
Next next;
Next next = () {};
Map<String, dynamic> body;
Map<String, dynamic> params;

Expand Down
2 changes: 1 addition & 1 deletion lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Router {
}

Router use(RouteMethod cb) {
var layer = Layer('/', handle: cb, name: _Middleware.name);
var layer = Layer('/', handle: cb, name: _InitMiddleware.name);

stack.add(layer);

Expand Down

0 comments on commit f0e1643

Please sign in to comment.