Upgrades for 2.0, fix static errors

This commit is contained in:
Tobe O 2019-04-20 10:53:52 -04:00
parent ec7bff58c1
commit fcbcc6963d
27 changed files with 244 additions and 253 deletions

3
.gitignore vendored
View file

@ -26,4 +26,5 @@ doc/api/
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
log.txt
log.txt
.dart_tool

2
CHANGELOG.md Normal file
View file

@ -0,0 +1,2 @@
# 2.0.0
* Angel 2 updates.

View file

@ -41,7 +41,7 @@ app.before.add(banIp('1.2.3.*'));
app.before.add(banIp('1.2.*.4'));
// Or multiple filters:
app.before.add(banIp(['1.2.3.4', '192.*.*.*', new RegExp(r'1\.2.\3.\4')]));
app.before.add(banIp(['1.2.3.4', '192.*.*.*', RegExp(r'1\.2.\3.\4')]));
// Also can ban origins
app.before.add(banOrigin('*.known-attacker.com'));
@ -64,7 +64,7 @@ Throws a `429` error if the given rate limit is exceeded.
```dart
// Example: 5 requests per minute
app.before.add(throttleRequests(5, new Duration(minutes: 1)));
app.before.add(throttleRequests(5, Duration(minutes: 1)));
```
# Helmet
@ -101,11 +101,11 @@ They take the form of:
The specifics are up to you.
```dart
var permission = new Permission('admin | users:find');
var permission = Permission('admin | users:find');
// Or:
// PermissionBuilders support + and | operators. Operands can be Strings, Permissions or PermissionBuilders.
var permission = (new PermissionBuilder('admin') | (new PermissionBuilder('users') + 'find')).toPermission();
var permission = (PermissionBuilder('admin') | (PermissionBuilder('users') + 'find')).toPermission();
// Transform into middleware
app.chain(permission.toMiddleware()).get('/protected', ...);
@ -118,6 +118,6 @@ app.service('protected').beforeModify(permission.toHook());
//
// `variantPermission` is included in the `package:angel_security/hooks.dart` library.
app.service('posts').beforeModify(variantPermission((e) {
return new PermissionBuilder('posts:modify:${e.id}');
return PermissionBuilder('posts:modify:${e.id}');
}));
```

View file

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

1
example/main.dart Normal file
View file

@ -0,0 +1 @@

View file

@ -9,8 +9,8 @@ import 'package:angel_framework/angel_framework.dart';
/// String can take the following formats:
/// 1. 1.2.3.4
/// 2. 1.2.3.*, 1.2.*.*, etc.
RequestMiddleware banIp(filter,
{String message:
RequestHandler banIp(filter,
{String message =
'Your IP address is forbidden from accessing this server.'}) {
var filters = [];
Iterable inputs = filter is Iterable ? filter : [filter];
@ -22,10 +22,10 @@ RequestMiddleware banIp(filter,
if (!input.contains('*'))
filters.add(input);
else {
filters.add(new RegExp(input.replaceAll('*', '[0-9]+')));
filters.add(RegExp(input.replaceAll('*', '[0-9]+')));
}
} else
throw new ArgumentError('Cannot use $input as an IP filter.');
throw ArgumentError('Cannot use $input as an IP filter.');
}
return (RequestContext req, ResponseContext res) async {
@ -43,7 +43,7 @@ RequestMiddleware banIp(filter,
return true;
}
if (!check()) throw new AngelHttpException.forbidden(message: message);
if (!check()) throw AngelHttpException.forbidden(message: message);
return true;
};
}
@ -56,9 +56,9 @@ RequestMiddleware banIp(filter,
/// String can take the following formats:
/// 1. example.com
/// 2. *.example.com, a.b.*.d.e.f, etc.
RequestMiddleware banOrigin(filter,
{String message: 'You are forbidden from accessing this server.',
bool allowEmptyOrigin: false}) {
RequestHandler banOrigin(filter,
{String message = 'You are forbidden from accessing this server.',
bool allowEmptyOrigin = false}) {
var filters = [];
Iterable inputs = filter is Iterable ? filter : [filter];
@ -69,17 +69,17 @@ RequestMiddleware banOrigin(filter,
if (!input.contains('*'))
filters.add(input);
else {
filters.add(new RegExp(input.replaceAll('*', '[^\.]+')));
filters.add(RegExp(input.replaceAll('*', '[^\.]+')));
}
} else
throw new ArgumentError('Cannot use $input as an origin filter.');
throw ArgumentError('Cannot use $input as an origin filter.');
}
return (RequestContext req, ResponseContext res) async {
var origin = req.headers.value('origin');
if ((origin == null || origin.isEmpty) && !allowEmptyOrigin)
throw new AngelHttpException.badRequest(
throw AngelHttpException.badRequest(
message: "'Origin' header is required.");
bool check() {
@ -92,7 +92,7 @@ RequestMiddleware banOrigin(filter,
return true;
}
if (!check()) throw new AngelHttpException.forbidden(message: message);
if (!check()) throw AngelHttpException.forbidden(message: message);
return true;
};
}

View file

@ -2,20 +2,21 @@ import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:uuid/uuid.dart';
final Uuid _uuid = new Uuid();
final Uuid _uuid = Uuid();
/// Ensures that the request contains a correct CSRF token.
RequestMiddleware verifyCsrfToken(
{bool allowCookie: false,
bool allowQuery: true,
String name: 'csrf_token'}) {
RequestHandler verifyCsrfToken(
{bool allowCookie = false,
bool allowQuery = true,
String name = 'csrf_token'}) {
return (RequestContext req, res) async {
String csrfToken;
if (allowQuery && (await req.lazyQuery()).containsKey(name))
csrfToken = req.query[name];
else if ((await req.lazyBody()).containsKey(name))
csrfToken = req.body[name];
if (allowQuery && req.queryParameters.containsKey(name))
csrfToken = req.queryParameters[name];
else if ((await req.parseBody().then((_) => req.bodyAsMap))
.containsKey(name))
csrfToken = req.bodyAsMap[name];
else if (allowCookie) {
var cookie =
req.cookies.firstWhere((c) => c.name == name, orElse: () => null);
@ -23,22 +24,22 @@ RequestMiddleware verifyCsrfToken(
}
if (csrfToken == null || !req.session.containsKey(name))
throw new AngelHttpException.badRequest(message: 'Missing CSRF token.');
throw AngelHttpException.badRequest(message: 'Missing CSRF token.');
String correctToken = req.session[name];
if (csrfToken != correctToken)
throw new AngelHttpException.badRequest(message: 'Invalid CSRF token.');
throw AngelHttpException.badRequest(message: 'Invalid CSRF token.');
return true;
};
}
/// Adds a CSRF token to the session, if none is present.
RequestHandler setCsrfToken({String name: 'csrf_token', bool cookie: false}) {
RequestHandler setCsrfToken({String name = 'csrf_token', bool cookie = false}) {
return (RequestContext req, res) async {
if (!req.session.containsKey(name)) req.session[name] = _uuid.v4();
if (cookie) res.cookies.add(new Cookie(name, req.session[name]));
if (cookie) res.cookies.add(Cookie(name, req.session[name]));
return true;
};
}

View file

@ -1,10 +1,9 @@
import 'package:angel_framework/angel_framework.dart';
/// Adds the authed user to `e.params`, only if present in `req.injections`.
HookedServiceEventListener addUserToParams({String as, userKey}) {
return (HookedServiceEvent e) {
var user = e.request?.grab(userKey ?? 'user');
/// Adds the authed user to `e.params`, only if present in `req.container`.
HookedServiceEventListener addUserToParams<User>({String as}) {
return (HookedServiceEvent e) async {
var user = await e.request?.container?.makeAsync<User>();
if (user != null) e.params[as ?? 'user'] = user;
};
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:mirrors';
import 'package:angel_framework/angel_framework.dart';
import 'errors.dart';
@ -7,55 +8,51 @@ import 'is_server_side.dart';
///
///Default [idField] is `'id'`.
/// Default [ownerField] is `'userId'`.
/// Default [userKey] is `'user'`.
HookedServiceEventListener associateCurrentUser(
HookedServiceEventListener associateCurrentUser<Id, Data, User>(
{String idField,
String ownerField,
userKey,
String errorMessage,
bool allowNullUserId: false,
getId(user),
assignUserId(id, obj)}) {
bool allowNullUserId = false,
FutureOr<Id> Function(User) getId,
FutureOr<Data> Function(Id, Data) assignUserId}) {
return (HookedServiceEvent e) async {
var fieldName = ownerField?.isNotEmpty == true ? ownerField : 'userId';
var user = e.request?.grab(userKey ?? 'user');
var user = await e.request?.container?.makeAsync<User>();
if (user == null) {
if (!isServerSide(e))
throw new AngelHttpException.forbidden(
throw AngelHttpException.forbidden(
message: errorMessage ?? Errors.NOT_LOGGED_IN);
else
return;
}
_getId(user) {
Future<Id> _getId(User user) async {
if (getId != null)
return getId(user);
return await getId(user);
else if (user is Map)
return user[idField ?? 'id'];
else if (idField == null || idField == 'id')
return user.id;
else
return reflect(user).getField(new Symbol(idField ?? 'id')).reflectee;
return reflect(user).getField(Symbol(idField ?? 'id')).reflectee;
}
var id = await _getId(user);
if (id == null && allowNullUserId != true)
throw new AngelHttpException.notProcessable(
throw AngelHttpException.notProcessable(
message: 'Current user is missing a $fieldName field.');
_assignUserId(id, obj) {
Future<Data> _assignUserId(Id id, Data obj) async {
if (assignUserId != null)
return assignUserId(id, obj);
else if (obj is Map)
obj[fieldName] = id;
else if (fieldName == 'userId')
obj.userId = id;
else
reflect(obj).setField(new Symbol(fieldName), id);
return obj..[fieldName] = id;
else {
reflect(obj).setField(Symbol(fieldName), id);
return obj;
}
}
await _assignUserId(id, e.data);
e.data = await _assignUserId(id, e.data);
};
}

View file

@ -24,7 +24,7 @@ HookedServiceEventListener hashPassword(
return user?.password;
else
return reflect(user)
.getField(new Symbol(passwordField ?? 'password'))
.getField(Symbol(passwordField ?? 'password'))
.reflectee;
}
@ -34,8 +34,7 @@ HookedServiceEventListener hashPassword(
else if (user is Map)
user[passwordField ?? 'password'] = password;
else
reflect(user)
.setField(new Symbol(passwordField ?? 'password'), password);
reflect(user).setField(Symbol(passwordField ?? 'password'), password);
}
if (e.data != null) {
@ -44,7 +43,7 @@ HookedServiceEventListener hashPassword(
if (password != null) {
var digest = h.convert(password.codeUnits);
return _setPassword(new String.fromCharCodes(digest.bytes), user);
return _setPassword(String.fromCharCodes(digest.bytes), user);
}
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:mirrors';
import 'package:angel_framework/angel_framework.dart';
import 'errors.dart';
@ -7,20 +8,19 @@ import 'is_server_side.dart';
///
/// Default [as] is `'userId'`.
/// Default [userKey] is `'user'`.
HookedServiceEventListener queryWithCurrentUser(
HookedServiceEventListener queryWithCurrentUser<Id, User>(
{String as,
String idField,
userKey,
String errorMessage,
bool allowNullUserId: false,
getId(user)}) {
bool allowNullUserId = false,
FutureOr<Id> Function(User) getId}) {
return (HookedServiceEvent e) async {
var fieldName = idField?.isNotEmpty == true ? idField : 'id';
var user = e.request?.grab(userKey ?? 'user');
var user = await e.request?.container?.makeAsync<User>();
if (user == null) {
if (!isServerSide(e))
throw new AngelHttpException.forbidden(
throw AngelHttpException.forbidden(
message: errorMessage ?? Errors.NOT_LOGGED_IN);
else
return;
@ -34,13 +34,13 @@ HookedServiceEventListener queryWithCurrentUser(
else if (fieldName == 'id')
return user.id;
else
return reflect(user).getField(new Symbol(fieldName)).reflectee;
return reflect(user).getField(Symbol(fieldName)).reflectee;
}
var id = await _getId(user);
if (id == null && allowNullUserId != true)
throw new AngelHttpException.notProcessable(
throw AngelHttpException.notProcessable(
message: 'Current user is missing a \'$fieldName\' field.');
var data = {as?.isNotEmpty == true ? as : 'userId': id};

View file

@ -3,14 +3,14 @@ import 'errors.dart';
import 'is_server_side.dart';
/// Restricts the service method to authed users only.
HookedServiceEventListener restrictToAuthenticated(
{userKey, String errorMessage}) {
HookedServiceEventListener restrictToAuthenticated<User>(
{String errorMessage}) {
return (HookedServiceEvent e) async {
var user = e.request?.grab(userKey ?? 'user');
var user = await e.request?.container?.makeAsync<User>();
if (user == null) {
if (!isServerSide(e))
throw new AngelHttpException.forbidden(
throw AngelHttpException.forbidden(
message: errorMessage ?? Errors.NOT_LOGGED_IN);
else
return;

View file

@ -1,64 +1,60 @@
import 'dart:async';
import 'dart:mirrors';
import 'package:angel_framework/angel_framework.dart';
import 'errors.dart';
import 'is_server_side.dart';
/// Restricts users to accessing only their own resources.
HookedServiceEventListener restrictToOwner(
HookedServiceEventListener restrictToOwner<Id, Data, User>(
{String idField,
String ownerField,
userKey,
String errorMessage,
getId(user),
getOwner(obj)}) {
FutureOr<Id> Function(User) getId,
FutureOr<Id> Function(Data) getOwnerId}) {
return (HookedServiceEvent e) async {
if (!isServerSide(e)) {
var user = e.request?.grab(userKey ?? 'user');
var user = await e.request?.container?.makeAsync<User>();
if (user == null)
throw new AngelHttpException.notAuthenticated(
throw AngelHttpException.notAuthenticated(
message:
'The current user is missing. You must not be authenticated.');
_getId(user) {
Future<Id> _getId(User user) async {
if (getId != null)
return getId(user);
else if (user is Map)
return user[idField ?? 'id'];
else if (idField == null || idField == 'id')
return user.id;
else
return reflect(user).getField(new Symbol(idField ?? 'id')).reflectee;
return reflect(user).getField(Symbol(idField ?? 'id')).reflectee;
}
var id = await _getId(user);
if (id == null) throw new Exception('The current user has no ID.');
if (id == null) throw Exception('The current user has no ID.');
var resource = await e.service.read(
e.id,
{}
..addAll(e.params ?? {})
..remove('provider'));
..remove('provider')) as Data;
if (resource != null) {
_getOwner(obj) {
if (getOwner != null)
return getOwner(obj);
Future<Id> _getOwner(Data obj) async {
if (getOwnerId != null)
return await getOwnerId(obj);
else if (obj is Map)
return obj[ownerField ?? 'userId'];
else if (ownerField == null || ownerField == 'userId')
return obj.userId;
return obj[ownerField ?? 'user_id'];
else
return reflect(obj)
.getField(new Symbol(ownerField ?? 'userId'))
.getField(Symbol(ownerField ?? 'userId'))
.reflectee;
}
var ownerId = await _getOwner(resource);
if ((ownerId is Iterable && !ownerId.contains(id)) || ownerId != id)
throw new AngelHttpException.forbidden(
throw AngelHttpException.forbidden(
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
}
}

View file

@ -9,7 +9,7 @@ HookedServiceEventListener variantPermission(
createPermission(HookedServiceEvent e),
{String errorMessage,
userKey,
bool owner: false,
bool owner = false,
getRoles(user),
getId(user),
getOwner(obj)}) {
@ -19,7 +19,7 @@ HookedServiceEventListener variantPermission(
if (permission is PermissionBuilder) permission = permission.toPermission();
if (permission is! Permission)
throw new ArgumentError(
throw ArgumentError(
'createPermission must generate a Permission, whether synchronously or asynchronously.');
await permission.toHook(
errorMessage: errorMessage,

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'hooks/errors.dart';
import 'hooks/restrict_to_owner.dart';
@ -10,48 +12,42 @@ class Permission {
Permission(this.minimum);
call(RequestContext req, ResponseContext res) {
return toMiddleware()(req, res);
}
/// Creates a hook that restricts a service method to users with this
/// permission, or if they are the resource [owner].
///
/// [getId] and [getOwner] are passed to [restrictToOwner], along with
/// [idField], [ownerField], [userKey] and [errorMessage].
HookedServiceEventListener toHook(
HookedServiceEventListener toHook<Id, Data, User>(
{String errorMessage,
String idField,
String ownerField,
String userKey,
bool owner: false,
getRoles(user),
getId(user),
getOwner(obj)}) {
bool owner = false,
FutureOr<Iterable<String>> Function(User) getRoles,
FutureOr<Id> Function(User) getId,
FutureOr<Id> Function(Data) getOwnerId}) {
return (HookedServiceEvent e) async {
if (e.params.containsKey('provider')) {
var user = e.request?.grab(userKey ?? 'user');
var user = await e.request?.container?.makeAsync<User>();
if (user == null)
throw new AngelHttpException.forbidden(
throw AngelHttpException.forbidden(
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
var roleFinder = getRoles ?? (user) async => user.roles ?? [];
List<String> roles = (await roleFinder(user)).toList();
var roleFinder = getRoles ?? (user) => <String>[];
var roles = (await roleFinder(user)).toList();
if (!roles.any(verify)) {
// Try owner if the roles are not in-place
if (owner == true) {
var listener = restrictToOwner(
var listener = restrictToOwner<Id, Data, User>(
idField: idField,
ownerField: ownerField,
userKey: userKey,
errorMessage: errorMessage,
getId: getId,
getOwner: getOwner);
getOwnerId: getOwnerId);
await listener(e);
} else
throw new AngelHttpException.forbidden(
throw AngelHttpException.forbidden(
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
}
}
@ -59,13 +55,12 @@ class Permission {
}
/// Restricts a route to users who have sufficient permissions.
RequestMiddleware toMiddleware(
{String message, String userKey, getRoles(user)}) {
RequestHandler toMiddleware<User>({String message, getRoles(user)}) {
return (RequestContext req, ResponseContext res) async {
var user = req.grab(userKey ?? 'user');
var user = await req.container.makeAsync<User>();
if (user == null)
throw new AngelHttpException.forbidden(
throw AngelHttpException.forbidden(
message: message ??
'You have insufficient permissions to perform this action.');
@ -73,7 +68,7 @@ class Permission {
List<String> roles = (await roleFinder(user)).toList();
if (!roles.any(verify))
throw new AngelHttpException.forbidden(
throw AngelHttpException.forbidden(
message: message ??
'You have insufficient permissions to perform this action.');
@ -124,7 +119,7 @@ class PermissionBuilder {
/// A minimum
PermissionBuilder(this._min);
factory PermissionBuilder.wildcard() => new PermissionBuilder('*');
factory PermissionBuilder.wildcard() => PermissionBuilder('*');
PermissionBuilder operator +(other) {
if (other is String)
@ -134,38 +129,36 @@ class PermissionBuilder {
else if (other is Permission)
return add(other.minimum);
else
throw new ArgumentError(
throw ArgumentError(
'Cannot add a ${other.runtimeType} to a PermissionBuilder.');
}
PermissionBuilder operator |(other) {
if (other is String)
return or(new PermissionBuilder(other));
return or(PermissionBuilder(other));
else if (other is PermissionBuilder)
return or(other);
else if (other is Permission)
return or(new PermissionBuilder(other.minimum));
return or(PermissionBuilder(other.minimum));
else
throw new ArgumentError(
throw ArgumentError(
'Cannot or a ${other.runtimeType} and a PermissionBuilder.');
}
call(RequestContext req, ResponseContext res) => toPermission()(req, res);
/// Adds another level of [constraint].
PermissionBuilder add(String constraint) =>
new PermissionBuilder('$_min:$constraint');
PermissionBuilder('$_min:$constraint');
/// Adds a wildcard permission.
PermissionBuilder allowAll() => add('*');
/// Duplicates this builder.
PermissionBuilder clone() => new PermissionBuilder(_min);
PermissionBuilder clone() => PermissionBuilder(_min);
/// Allows an alternative permission.
PermissionBuilder or(PermissionBuilder other) =>
new PermissionBuilder('$_min | ${other._min}');
PermissionBuilder('$_min | ${other._min}');
/// Builds a [Permission].
Permission toPermission() => new Permission(_min);
Permission toPermission() => Permission(_min);
}

View file

@ -2,8 +2,7 @@ import 'dart:convert';
import 'package:angel_framework/angel_framework.dart';
final Map<Pattern, String> DEFAULT_SANITIZERS = {
new RegExp(
r'<\s*s\s*c\s*r\s*i\s*p\s*t\s*>.*<\s*\/\s*s\s*c\s*r\s*i\s*p\s*t\s*>',
RegExp(r'<\s*s\s*c\s*r\s*i\s*p\s*t\s*>.*<\s*\/\s*s\s*c\s*r\s*i\s*p\s*t\s*>',
caseSensitive: false): ''
};
@ -12,19 +11,19 @@ final Map<Pattern, String> DEFAULT_SANITIZERS = {
/// You can also provide a Map of patterns to [replace].
///
/// You can sanitize the [body] or [query] (both `true` by default).
RequestMiddleware sanitizeHtmlInput(
{bool body: true,
bool query: true,
Map<Pattern, String> replace: const {}}) {
RequestHandler sanitizeHtmlInput(
{bool body = true,
bool query = true,
Map<Pattern, String> replace = const {}}) {
var sanitizers = {}..addAll(DEFAULT_SANITIZERS)..addAll(replace ?? {});
return (RequestContext req, res) async {
return (req, res) async {
if (body) {
await req.parse();
_sanitizeMap(req.body, sanitizers);
await req.parseBody();
_sanitizeMap(req.bodyAsMap, sanitizers);
}
if (query) _sanitizeMap(req.query, sanitizers);
if (query) _sanitizeMap(req.queryParameters, sanitizers);
return true;
};
}
@ -37,7 +36,7 @@ _sanitize(v, Map<Pattern, String> sanitizers) {
str = str.replaceAll(needle, replace);
});
return HTML_ESCAPE.convert(str);
return htmlEscape.convert(str);
} else if (v is Map) {
_sanitizeMap(v, sanitizers);
return v;

View file

@ -7,8 +7,8 @@ import 'package:angel_framework/angel_framework.dart';
/// The default is to identify requests by their source IP.
///
/// This works well attached to a `multiserver` instance.
RequestMiddleware throttleRequests(int max, Duration duration,
{String message: '429 Too Many Requests', identify(RequestContext req)}) {
RequestHandler throttleRequests(int max, Duration duration,
{String message = '429 Too Many Requests', identify(RequestContext req)}) {
var identifyRequest = identify ?? (RequestContext req) async => req.ip;
Map<String, int> table = {};
Map<String, List<int>> times = {};
@ -17,7 +17,7 @@ RequestMiddleware throttleRequests(int max, Duration duration,
var id = (await identifyRequest(req)).toString();
int currentCount;
var now = new DateTime.now().millisecondsSinceEpoch;
var now = DateTime.now().millisecondsSinceEpoch;
int firstVisit;
// If the user has visited within the given duration...
@ -45,7 +45,7 @@ RequestMiddleware throttleRequests(int max, Duration duration,
currentCount = table[id] = 1;
if (currentCount > max) {
throw new AngelHttpException(null,
throw AngelHttpException(null,
statusCode: 429, message: message ?? '429 Too Many Requests');
}

View file

@ -10,7 +10,7 @@ import 'package:angel_framework/angel_framework.dart';
/// String can take the following formats:
/// 1. 1.2.3.4
/// 2. 1.2.3.*, 1.2.*.*, etc.
RequestMiddleware trustProxy(filter) {
RequestHandler trustProxy(filter) {
var filters = [];
Iterable inputs = filter is Iterable ? filter : [filter];
@ -21,10 +21,10 @@ RequestMiddleware trustProxy(filter) {
if (!input.contains('*'))
filters.add(input);
else {
filters.add(new RegExp(input.replaceAll('*', '[0-9]+')));
filters.add(RegExp(input.replaceAll('*', '[0-9]+')));
}
} else
throw new ArgumentError('Cannot use $input as a trusted proxy filter.');
throw ArgumentError('Cannot use $input as a trusted proxy filter.');
}
return (RequestContext req, ResponseContext res) async {
@ -49,7 +49,8 @@ RequestMiddleware trustProxy(filter) {
if (k.trim().toLowerCase().startsWith('x-forwarded')) headers[k] = v;
});
req.inject(ForwardedClient, new _ForwardedClientImpl(headers));
req.container
.registerSingleton<ForwardedClient>(_ForwardedClientImpl(headers));
}
return true;
@ -78,5 +79,5 @@ class _ForwardedClientImpl extends ForwardedClient {
@override
Map<String, List<String>> get headers =>
new Map<String, List<String>>.unmodifiable(_headers);
Map<String, List<String>>.unmodifiable(_headers);
}

View file

@ -1,15 +1,16 @@
author: "Tobe O <thosakwe@gmail.com>"
description: "Angel middleware designed to enhance application security by patching common Web security holes."
homepage: "https://github.com/angel-dart/security"
name: "angel_security"
version: "1.1.0"
dependencies:
angel_framework: "^1.1.0"
dev_dependencies:
angel_auth: "^1.1.0"
angel_test: "^1.1.0"
angel_validate: "^1.0.0"
console: "^2.2.4"
test: "^0.12.0"
name: angel_security
version: 2.0.0
description: Angel infrastructure for improving security by patching common holes.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/security
environment:
sdk: ">=1.19.0"
sdk: ">=2.0.0-dev <3.0.0"
dependencies:
angel_framework: ^2.0.0-alpha
dev_dependencies:
angel_auth: ^2.0.0
angel_test: ^2.0.0
angel_validate: ^2.0.0
console: ^3.0.0
pedantic: ^1.0.0
test: ^1.0.0

View file

@ -8,10 +8,11 @@ main() {
TestClient client;
setUp(() async {
app = new Angel()
..chain(banIp('*.*.*.*')).get('/ban', 'WTF')
..chain(banOrigin('*')).get('/ban-origin', 'WTF')
..chain(banOrigin('*.foo.bar')).get('/allow-origin', 'YAY');
app = Angel()
..chain([banIp('*.*.*.*')]).get('/ban', (req, res) => 'WTF')
..chain([banOrigin('*')]).get('/ban-origin', (req, res) => 'WTF')
..chain([banOrigin('*.foo.bar')])
.get('/allow-origin', (req, res) => 'YAY');
client = await connectTo(app);
});

View file

@ -4,16 +4,16 @@ import 'package:angel_test/angel_test.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
final RegExp _sessId = new RegExp(r'DARTSESSID=([^;]+);');
final RegExp _sessId = RegExp(r'DARTSESSID=([^;]+);');
main() async {
Angel app;
TestClient client;
setUp(() async {
app = new Angel()..responseFinalizers.add(setCsrfToken());
app = Angel()..responseFinalizers.add(setCsrfToken());
app.chain(verifyCsrfToken()).get('/valid', 'Valid!');
app.chain([verifyCsrfToken()]).get('/valid', (req, res) => 'Valid!');
client = await connectTo(app);
});

View file

@ -10,33 +10,33 @@ main() {
TestClient client;
setUp(() async {
app = new Angel()
..lazyParseBodies = true
..use((RequestContext req, res) async {
app = Angel()
..fallback((req, res) async {
var xUser = req.headers.value('X-User');
if (xUser != null)
req.inject('user',
new User(id: xUser, roles: xUser == 'John' ? ['foo:bar'] : []));
if (xUser != null) {
req.container.registerSingleton(
User(id: xUser, roles: xUser == 'John' ? ['foo:bar'] : []));
}
return true;
});
app
..use('/user_data', new UserDataService())
..use('/artists', new ArtistService())
..use('/roled', new RoledService());
..use('/user_data', UserDataService())
..use('/artists', ArtistService())
..use('/roled', RoledService());
(app.service('user_data') as HookedService)
(app.findService('user_data') as HookedService)
..beforeIndexed.listen(hooks.queryWithCurrentUser())
..beforeCreated.listen(hooks.hashPassword());
app.service('artists') as HookedService
app.findService('artists') as HookedService
..beforeIndexed.listen(hooks.restrictToAuthenticated())
..beforeRead.listen(hooks.restrictToOwner())
..beforeCreated.listen(hooks.associateCurrentUser());
(app.service('roled') as HookedService)
..beforeIndexed.listen(new Permission('foo:*').toHook())
..beforeRead.listen(new Permission('foo:*').toHook(owner: true));
(app.findService('roled') as HookedService)
..beforeIndexed.listen(Permission('foo:*').toHook())
..beforeRead.listen(Permission('foo:*').toHook(owner: true));
var errorHandler = app.errorHandler;
app.errorHandler = (e, req, res) {
@ -55,10 +55,10 @@ main() {
try {
var response = await client.service('artists').create({'foo': 'bar'});
print(response);
throw new StateError('Creating without userId bad request');
throw StateError('Creating without userId bad request');
} catch (e) {
print(e);
expect(e, new isInstanceOf<AngelHttpException>());
expect(e, const TypeMatcher<AngelHttpException>());
var err = e as AngelHttpException;
expect(err.statusCode, equals(403));
}
@ -78,10 +78,10 @@ main() {
try {
var response = await client.service('user_data').index();
print(response);
throw new StateError('Indexing without user forbidden');
throw StateError('Indexing without user forbidden');
} catch (e) {
print(e);
expect(e, new isInstanceOf<AngelHttpException>());
expect(e, const TypeMatcher<AngelHttpException>());
var err = e as AngelHttpException;
expect(err.statusCode, equals(403));
}
@ -107,10 +107,10 @@ main() {
try {
var response = await client.service('artists').index();
print(response);
throw new StateError('Indexing without user forbidden');
throw StateError('Indexing without user forbidden');
} catch (e) {
print(e);
expect(e, new isInstanceOf<AngelHttpException>());
expect(e, const TypeMatcher<AngelHttpException>());
var err = e as AngelHttpException;
expect(err.statusCode, equals(403));
}
@ -139,10 +139,10 @@ main() {
try {
var response = await client.service('artists').read('king_of_pop');
print(response);
throw new StateError('Reading without owner forbidden');
throw StateError('Reading without owner forbidden');
} catch (e) {
print(e);
expect(e, new isInstanceOf<AngelHttpException>());
expect(e, const TypeMatcher<AngelHttpException>());
var err = e as AngelHttpException;
expect(err.statusCode, equals(401));
}
@ -169,10 +169,10 @@ main() {
try {
var response = await client.service('roled').index();
print(response);
throw new StateError('Reading without roles forbidden');
throw StateError('Reading without roles forbidden');
} catch (e) {
print(e);
expect(e, new isInstanceOf<AngelHttpException>());
expect(e, const TypeMatcher<AngelHttpException>());
var err = e as AngelHttpException;
expect(err.statusCode, equals(403));
}
@ -208,7 +208,7 @@ main() {
class User {
String id;
List<String> roles;
User({this.id, this.roles: const []});
User({this.id, this.roles = const []});
}
class UserDataService extends Service {
@ -220,12 +220,12 @@ class UserDataService extends Service {
index([Map params]) async {
print('Params: $params');
if (params?.containsKey('query') != true)
throw new AngelHttpException.badRequest(message: 'query required');
throw AngelHttpException.badRequest(message: 'query required');
String name = params['query']['userId']?.toString();
if (!_data.containsKey(name))
throw new AngelHttpException.notFound(
throw AngelHttpException.notFound(
message: "No data found for user '$name'.");
return _data[name];
@ -234,13 +234,13 @@ class UserDataService extends Service {
@override
create(data, [Map params]) async {
if (data is! Map || !data.containsKey('password'))
throw new AngelHttpException.badRequest(message: 'Required password!');
throw AngelHttpException.badRequest(message: 'Required password!');
var expected =
new String.fromCharCodes(sha256.convert('jdoe1'.codeUnits).bytes);
String.fromCharCodes(sha256.convert('jdoe1'.codeUnits).bytes);
if (data['password'] != (expected))
throw new AngelHttpException.conflict(message: 'Passwords do not match.');
throw AngelHttpException.conflict(message: 'Passwords do not match.');
return {'foo': 'bar'};
}
}
@ -257,7 +257,7 @@ class ArtistService extends Service {
@override
create(data, [params]) async {
if (data is! Map || !data.containsKey('userId'))
throw new AngelHttpException.badRequest(message: 'Required userId');
throw AngelHttpException.badRequest(message: 'Required userId');
return {'foo': 'bar'};
}

View file

@ -14,36 +14,42 @@ main() {
TestClient client;
setUp(() async {
app = new Angel();
app = Angel();
app.use((RequestContext req, res) async {
app.fallback((req, res) async {
// In real life, you'd use auth to check user roles,
// but in this case, let's just set the user manually
var xRoles = req.headers.value('X-Roles');
var xRoles = req.headers['X-Roles'];
if (xRoles?.isNotEmpty == true) {
req.inject('user', new User(req.headers['X-Roles']));
req.container.registerSingleton(User(xRoles));
}
return true;
});
app.chain(new PermissionBuilder.wildcard()).get('/', 'Hello, world!');
app.chain(new Permission('foo')).get('/one', 'Hello, world!');
app.chain(new Permission('two:foo')).get('/two', 'Hello, world!');
app.chain(new Permission('two:*')).get('/two-star', 'Hello, world!');
app.chain(new Permission('three:foo:bar')).get('/three', 'Hello, world!');
app
.chain(new Permission('three:*:bar'))
.get('/three-star', 'Hello, world!');
app.chain([PermissionBuilder.wildcard().toPermission().toMiddleware()]).get(
'/', (req, res) => 'Hello, world!');
app.chain([Permission('foo').toMiddleware()]).get(
'/one', (req, res) => 'Hello, world!');
app.chain([Permission('two:foo').toMiddleware()]).get(
'/two', (req, res) => 'Hello, world!');
app.chain([Permission('two:*').toMiddleware()]).get(
'/two-star', (req, res) => 'Hello, world!');
app.chain([Permission('three:foo:bar').toMiddleware()]).get(
'/three', (req, res) => 'Hello, world!');
app.chain([Permission('three:*:bar').toMiddleware()]).get(
'/three-star', (req, res) => 'Hello, world!');
app
.chain(new PermissionBuilder('super')
.add('specific')
.add('permission')
.allowAll()
.or(new PermissionBuilder('admin')))
.get('/or', 'Hello, world!');
app.chain([
PermissionBuilder('super')
.add('specific')
.add('permission')
.allowAll()
.or(PermissionBuilder('admin'))
.toPermission()
.toMiddleware()
]).get('/or', (req, res) => 'Hello, world!');
client = await connectTo(app);
});

View file

@ -3,15 +3,13 @@ import 'package:logging/logging.dart';
/// Prints the contents of a [LogRecord] with pretty colors.
prettyLog(LogRecord record) async {
var pen = new TextPen();
var pen = TextPen();
chooseLogColor(pen.reset(), record.level);
pen(record.toString());
if (record.error != null)
pen(record.error.toString());
if (record.stackTrace != null)
pen(record.stackTrace.toString());
if (record.error != null) pen(record.error.toString());
if (record.stackTrace != null) pen(record.stackTrace.toString());
pen();
}
@ -27,6 +25,5 @@ void chooseLogColor(TextPen pen, Level level) {
pen.magenta();
else if (level == Level.FINER)
pen.blue();
else if (level == Level.FINEST)
pen.darkBlue();
else if (level == Level.FINEST) pen.darkBlue();
}

View file

@ -1,26 +1,26 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_security/angel_security.dart';
import 'package:angel_test/angel_test.dart';
import 'package:angel_validate/server.dart';
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:matcher/matcher.dart';
import 'package:test/test.dart';
import 'pretty_logging.dart';
final Validator untrustedSchema = new Validator({'html*': isString});
final Validator untrustedSchema = Validator({'html*': isString});
main() async {
Angel app;
TestClient client;
setUp(() async {
app = new Angel();
app = Angel();
app.chain([validate(untrustedSchema), sanitizeHtmlInput()])
..post('/untrusted', (RequestContext req, ResponseContext res) async {
String untrusted = req.body['html'];
String untrusted = req.bodyAsMap['html'];
res
..contentType = ContentType.HTML
..contentType = MediaType('text', 'html')
..write('''
<!DOCTYPE html>
<html>
@ -31,9 +31,9 @@ main() async {
</html>''');
})
..post('/attribute', (RequestContext req, ResponseContext res) async {
String untrusted = req.body['html'];
String untrusted = req.bodyAsMap['html'];
res
..contentType = ContentType.HTML
..contentType = MediaType('text', 'html')
..write('''
<!DOCTYPE html>
<html>
@ -46,8 +46,7 @@ main() async {
</html>''');
});
app.logger = new Logger.detached('angel_security')
..onRecord.listen(prettyLog);
app.logger = Logger.detached('angel_security')..onRecord.listen(prettyLog);
client = await connectTo(app);
});

View file

@ -8,15 +8,13 @@ main() {
TestClient client;
setUp(() async {
app = new Angel();
app = Angel();
app
.chain(throttleRequests(1, new Duration(hours: 1)))
.get('/once-per-hour', 'OK');
app.chain([throttleRequests(1, Duration(hours: 1))]).get(
'/once-per-hour', (req, res) => 'OK');
app
.chain(throttleRequests(3, new Duration(minutes: 1)))
.get('/thrice-per-minute', 'OK');
app.chain([throttleRequests(3, Duration(minutes: 1))]).get(
'/thrice-per-minute', (req, res) => 'OK');
client = await connectTo(app);
});
@ -45,13 +43,11 @@ main() {
print(response.body);
expect(response, hasBody('"OK"'));
// Second request within the minute is fine
response = await client.get('/thrice-per-minute');
print(response.body);
expect(response, hasBody('"OK"'));
// Third request within the minute is fine
response = await client.get('/thrice-per-minute');
print(response.body);

View file

@ -3,17 +3,17 @@ import 'package:angel_security/angel_security.dart';
import 'package:angel_test/angel_test.dart';
import 'package:test/test.dart';
verifyProxy(RequestContext req) =>
req.injections.containsKey(ForwardedClient) ? 'Yep' : 'Nope';
verifyProxy(RequestContext req, ResponseContext res) =>
req.container.has<ForwardedClient>() ? 'Yep' : 'Nope';
main() {
Angel app;
TestClient client;
setUp(() async {
app = new Angel()
..chain(trustProxy('127.*.*.*')).get('/hello', verifyProxy)
..chain(trustProxy('1.2.3.4')).get('/foo', verifyProxy);
app = Angel()
..chain([trustProxy('127.*.*.*')]).get('/hello', verifyProxy)
..chain([trustProxy('1.2.3.4')]).get('/foo', verifyProxy);
client = await connectTo(app);
});