diff --git a/example/main.dart b/example/main.dart deleted file mode 100644 index 8b137891..00000000 --- a/example/main.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/angel_security.dart b/lib/angel_security.dart deleted file mode 100644 index 771373cd..00000000 --- a/lib/angel_security.dart +++ /dev/null @@ -1,9 +0,0 @@ -/// Angel middleware designed to enhance application security. -library angel_security; - -export 'src/ban.dart'; -export 'src/csrf.dart'; -export 'src/permissions.dart'; -export 'src/sanitize.dart'; -export 'src/throttle.dart'; -export 'src/trust_proxy.dart'; diff --git a/lib/hooks.dart b/lib/hooks.dart deleted file mode 100644 index 9b35ff21..00000000 --- a/lib/hooks.dart +++ /dev/null @@ -1,11 +0,0 @@ -/// Service hooks to lock down user data. -library angel_security.hooks; - -export 'src/hooks/add_user_to_params.dart'; -export 'src/hooks/associate_current_user.dart'; -export 'src/hooks/hash_password.dart'; -export 'src/hooks/is_server_side.dart'; -export 'src/hooks/query_with_current_user.dart'; -export 'src/hooks/resrict_to_authenticated.dart'; -export 'src/hooks/restrict_to_owner.dart'; -export 'src/hooks/variant_permission.dart'; diff --git a/lib/src/ban.dart b/lib/src/ban.dart deleted file mode 100644 index 6bb6577e..00000000 --- a/lib/src/ban.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; - -/// Throws a 403 Forbidden if the user's IP is banned. -/// -/// [filter] can be: -/// A `String`, `RegExp`, `InternetAddress`, or an `Iterable`. -/// -/// String can take the following formats: -/// 1. 1.2.3.4 -/// 2. 1.2.3.*, 1.2.*.*, etc. -RequestHandler banIp(filter, - {String message = - 'Your IP address is forbidden from accessing this server.'}) { - var filters = []; - Iterable inputs = filter is Iterable ? filter : [filter]; - - for (var input in inputs) { - if (input is RegExp || input is InternetAddress) - filters.add(input); - else if (input is String) { - if (!input.contains('*')) - filters.add(input); - else { - filters.add(RegExp(input.replaceAll('*', '[0-9]+'))); - } - } else - throw ArgumentError('Cannot use $input as an IP filter.'); - } - - return (RequestContext req, ResponseContext res) async { - var ip = req.ip; - - bool check() { - for (var input in filters) { - if (input is RegExp && input.hasMatch(ip)) - return false; - else if (input is InternetAddress && input.address == ip) - return false; - else if (input is String && input == ip) return false; - } - - return true; - } - - if (!check()) throw AngelHttpException.forbidden(message: message); - return true; - }; -} - -/// Throws a 403 Forbidden if the user's Origin header is banned. -/// -/// [filter] can be: -/// A `String`, `RegExp`, or an `Iterable`. -/// -/// String can take the following formats: -/// 1. example.com -/// 2. *.example.com, a.b.*.d.e.f, etc. -RequestHandler banOrigin(filter, - {String message = 'You are forbidden from accessing this server.', - bool allowEmptyOrigin = false}) { - var filters = []; - Iterable inputs = filter is Iterable ? filter : [filter]; - - for (var input in inputs) { - if (input is RegExp) - filters.add(input); - else if (input is String) { - if (!input.contains('*')) - filters.add(input); - else { - filters.add(RegExp(input.replaceAll('*', '[^\.]+'))); - } - } else - 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 AngelHttpException.badRequest( - message: "'Origin' header is required."); - - bool check() { - for (var input in filters) { - if (input is RegExp && input.hasMatch(origin)) - return false; - else if (input is String && input == origin) return false; - } - - return true; - } - - if (!check()) throw AngelHttpException.forbidden(message: message); - return true; - }; -} diff --git a/lib/src/csrf.dart b/lib/src/csrf.dart deleted file mode 100644 index fd33b2f8..00000000 --- a/lib/src/csrf.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:uuid/uuid.dart'; - -final Uuid _uuid = Uuid(); - -/// Ensures that the request contains a correct CSRF token. -/// -/// For `POST` methods, or anything requiring, provide [parseBody] as `true`. -/// -/// Note that this is useless without [setCsrfToken]. If no [name] is present in the -/// session, clients will pass through. -RequestHandler verifyCsrfToken( - {bool allowCookie = false, - bool allowQuery = true, - bool parseBody = false, - String name = 'csrf_token'}) { - return (RequestContext req, res) async { - if (!req.session.containsKey(name)) { - throw AngelHttpException.forbidden( - message: 'You cannot access this resource without a CSRF token.'); - } - - String csrfToken; - if (parseBody) { - await req.parseBody(); - } - - if (allowQuery && req.queryParameters.containsKey(name)) - csrfToken = req.queryParameters[name]; - else if (req.hasParsedBody && req.bodyAsMap.containsKey(name)) - csrfToken = req.bodyAsMap[name]; - else if (allowCookie) { - var cookie = - req.cookies.firstWhere((c) => c.name == name, orElse: () => null); - if (cookie != null) csrfToken = cookie.value; - } - - if (csrfToken == null) - throw AngelHttpException.badRequest(message: 'Missing CSRF token.'); - - String correctToken = req.session[name]; - - if (csrfToken != correctToken) - 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}) { - return (RequestContext req, res) async { - if (!req.session.containsKey(name)) req.session[name] = _uuid.v4(); - if (cookie) res.cookies.add(Cookie(name, req.session[name])); - return true; - }; -} diff --git a/lib/src/hooks/add_user_to_params.dart b/lib/src/hooks/add_user_to_params.dart deleted file mode 100644 index 974bb0ab..00000000 --- a/lib/src/hooks/add_user_to_params.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; - -/// Adds the authed user to `e.params`, only if present in `req.container`. -HookedServiceEventListener addUserToParams({String as}) { - return (HookedServiceEvent e) async { - var user = await e.request?.container?.makeAsync(); - if (user != null) e.params[as ?? 'user'] = user; - }; -} diff --git a/lib/src/hooks/associate_current_user.dart b/lib/src/hooks/associate_current_user.dart deleted file mode 100644 index 0b1d8c9b..00000000 --- a/lib/src/hooks/associate_current_user.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:async'; -import 'dart:mirrors'; -import 'package:angel_framework/angel_framework.dart'; -import 'errors.dart'; -import 'is_server_side.dart'; - -/// Adds the authed user's id to `data`. -/// -///Default [idField] is `'id'`. -/// Default [ownerField] is `'userId'`. -HookedServiceEventListener associateCurrentUser( - {String idField, - String ownerField, - String errorMessage, - bool allowNullUserId = false, - FutureOr Function(User) getId, - FutureOr Function(Id, Data) assignUserId}) { - return (HookedServiceEvent e) async { - var fieldName = ownerField?.isNotEmpty == true ? ownerField : 'userId'; - var user = await e.request?.container?.makeAsync(); - - if (user == null) { - if (!isServerSide(e)) - throw AngelHttpException.forbidden( - message: errorMessage ?? Errors.NOT_LOGGED_IN); - else - return; - } - - Future _getId(User user) async { - if (getId != null) - return await getId(user); - else if (user is Map) - return user[idField ?? 'id']; - else - return reflect(user).getField(Symbol(idField ?? 'id')).reflectee; - } - - var id = await _getId(user); - - if (id == null && allowNullUserId != true) - throw AngelHttpException.notProcessable( - message: 'Current user is missing a $fieldName field.'); - - Future _assignUserId(Id id, Data obj) async { - if (assignUserId != null) - return assignUserId(id, obj); - else if (obj is Map) - return obj..[fieldName] = id; - else { - reflect(obj).setField(Symbol(fieldName), id); - return obj; - } - } - - e.data = await _assignUserId(id, e.data); - }; -} diff --git a/lib/src/hooks/errors.dart b/lib/src/hooks/errors.dart deleted file mode 100644 index 10e83ecd..00000000 --- a/lib/src/hooks/errors.dart +++ /dev/null @@ -1,6 +0,0 @@ -abstract class Errors { - static const String NOT_LOGGED_IN = - 'You must be logged in to perform this action.'; - static const String INSUFFICIENT_PERMISSIONS = - 'You have insufficient permissions to access this resource.'; -} diff --git a/lib/src/hooks/hash_password.dart b/lib/src/hooks/hash_password.dart deleted file mode 100644 index a9621f7c..00000000 --- a/lib/src/hooks/hash_password.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:async'; -import 'dart:mirrors'; -import 'package:crypto/crypto.dart'; -import 'package:angel_framework/angel_framework.dart'; - -/// Hashes a user's password using a [Hash] algorithm (Default: [sha256]). -/// -/// You may provide your own functions to obtain or set a user's password, -/// or just provide a [passwordField] if you are only ever going to deal with Maps. -@deprecated -HookedServiceEventListener hashPassword( - {Hash hasher, - String passwordField, - FutureOr Function(User) getPassword, - setPassword(password, user)}) { - Hash h = hasher ?? sha256; - - return (HookedServiceEvent e) async { - _getPassword(user) { - if (getPassword != null) - return getPassword(user); - else if (user is Map) - return user[passwordField ?? 'password']; - else if (passwordField == 'password') - return user?.password; - else - return reflect(user) - .getField(Symbol(passwordField ?? 'password')) - .reflectee; - } - - _setPassword(password, user) { - if (setPassword != null) - return setPassword(password, user); - else if (user is Map) - user[passwordField ?? 'password'] = password; - else - reflect(user).setField(Symbol(passwordField ?? 'password'), password); - } - - if (e.data != null) { - applyHash(user) async { - var password = (await _getPassword(user))?.toString(); - - if (password != null) { - var digest = h.convert(password.codeUnits); - return _setPassword(String.fromCharCodes(digest.bytes), user); - } - } - - if (e.data is Iterable) { - var futures = await Future.wait(e.data.map((data) async { - await applyHash(data); - return data; - })); - - e.data = futures.toList(); - } else - await applyHash(e.data); - } - }; -} diff --git a/lib/src/hooks/is_server_side.dart b/lib/src/hooks/is_server_side.dart deleted file mode 100644 index 9d18bf85..00000000 --- a/lib/src/hooks/is_server_side.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; - -/// Returns `true` if the event was triggered server-side. -bool isServerSide(HookedServiceEvent e) => !e.params.containsKey('provider'); diff --git a/lib/src/hooks/query_with_current_user.dart b/lib/src/hooks/query_with_current_user.dart deleted file mode 100644 index 3010f161..00000000 --- a/lib/src/hooks/query_with_current_user.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:async'; -import 'dart:mirrors'; -import 'package:angel_framework/angel_framework.dart'; -import 'errors.dart'; -import 'is_server_side.dart'; - -/// Adds the authed user's id to `params['query']`. -/// -/// Default [as] is `'user_id'`. -/// Default [userKey] is `'user'`. -HookedServiceEventListener queryWithCurrentUser( - {String as, - String idField, - String errorMessage, - bool allowNullUserId = false, - FutureOr Function(User) getId}) { - return (HookedServiceEvent e) async { - var fieldName = idField?.isNotEmpty == true ? idField : 'id'; - var user = await e.request?.container?.makeAsync(); - - if (user == null) { - if (!isServerSide(e)) - throw AngelHttpException.forbidden( - message: errorMessage ?? Errors.NOT_LOGGED_IN); - else - return; - } - - Future _getId(User user) async { - if (getId != null) - return getId(user); - else if (user is Map) - return user[fieldName] as Id; - else - return reflect(user).getField(Symbol(fieldName)).reflectee as Id; - } - - var id = await _getId(user); - - if (id == null && allowNullUserId != true) - throw AngelHttpException.notProcessable( - message: 'Current user is missing a \'$fieldName\' field.'); - - var data = {as?.isNotEmpty == true ? as : 'user_id': id}; - - e.params['query'] = e.params.containsKey('query') - ? (e.params['query']..addAll(data)) - : data; - }; -} diff --git a/lib/src/hooks/resrict_to_authenticated.dart b/lib/src/hooks/resrict_to_authenticated.dart deleted file mode 100644 index 337240da..00000000 --- a/lib/src/hooks/resrict_to_authenticated.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; -import 'errors.dart'; -import 'is_server_side.dart'; - -/// Restricts the service method to authed users only. -HookedServiceEventListener restrictToAuthenticated( - {String errorMessage}) { - return (HookedServiceEvent e) async { - var user = await e.request?.container?.makeAsync(); - - if (user == null) { - if (!isServerSide(e)) - throw AngelHttpException.forbidden( - message: errorMessage ?? Errors.NOT_LOGGED_IN); - else - return; - } - }; -} diff --git a/lib/src/hooks/restrict_to_owner.dart b/lib/src/hooks/restrict_to_owner.dart deleted file mode 100644 index 38cc50e2..00000000 --- a/lib/src/hooks/restrict_to_owner.dart +++ /dev/null @@ -1,62 +0,0 @@ -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( - {String idField, - String ownerField, - String errorMessage, - FutureOr Function(User) getId, - FutureOr Function(Data) getOwnerId}) { - return (HookedServiceEvent e) async { - if (!isServerSide(e)) { - var user = await e.request?.container?.makeAsync(); - - if (user == null) - throw AngelHttpException.notAuthenticated( - message: - 'The current user is missing. You must not be authenticated.'); - - Future _getId(User user) async { - if (getId != null) - return getId(user); - else if (user is Map) - return user[idField ?? 'id']; - else - return reflect(user).getField(Symbol(idField ?? 'id')).reflectee; - } - - var id = await _getId(user); - - if (id == null) throw Exception('The current user has no ID.'); - - var resource = await e.service.read( - e.id, - {} - ..addAll(e.params ?? {}) - ..remove('provider')) as Data; - - if (resource != null) { - Future _getOwner(Data obj) async { - if (getOwnerId != null) - return await getOwnerId(obj); - else if (obj is Map) - return obj[ownerField ?? 'user_id']; - else - return reflect(obj) - .getField(Symbol(ownerField ?? 'userId')) - .reflectee; - } - - var ownerId = await _getOwner(resource); - - if ((ownerId is Iterable && !ownerId.contains(id)) || ownerId != id) - throw AngelHttpException.forbidden( - message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS); - } - } - }; -} diff --git a/lib/src/hooks/variant_permission.dart b/lib/src/hooks/variant_permission.dart deleted file mode 100644 index 28d4e3c0..00000000 --- a/lib/src/hooks/variant_permission.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; -import '../permissions.dart'; - -/// Generates a [Permission] based on the situation, and runs it as a hook. -/// -/// This is ideal for cases when you want to limit permissions to a dynamic -/// resource. -HookedServiceEventListener variantPermission( - createPermission(HookedServiceEvent e), - {String errorMessage, - userKey, - bool owner = false, - getRoles(user), - getId(user), - getOwner(obj)}) { - return (HookedServiceEvent e) async { - var permission = await createPermission(e); - - if (permission is PermissionBuilder) permission = permission.toPermission(); - - if (permission is! Permission) - throw ArgumentError( - 'createPermission must generate a Permission, whether synchronously or asynchronously.'); - await permission.toHook( - errorMessage: errorMessage, - userKey: userKey, - owner: owner, - getRoles: getRoles, - getId: getId, - getOwner: getOwner)(e); - }; -} diff --git a/lib/src/permissions.dart b/lib/src/permissions.dart deleted file mode 100644 index 8d8020a3..00000000 --- a/lib/src/permissions.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'dart:async'; - -import 'package:angel_framework/angel_framework.dart'; -import 'hooks/errors.dart'; -import 'hooks/restrict_to_owner.dart'; - -/// Easy mechanism to restrict access to services or routes. -class Permission { - /// A string representation of the minimum required privilege required - /// to access a resource. - final String minimum; - - Permission(this.minimum); - - /// 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( - {String errorMessage, - String idField, - String ownerField, - bool owner = false, - FutureOr> Function(User) getRoles, - FutureOr Function(User) getId, - FutureOr Function(Data) getOwnerId}) { - return (HookedServiceEvent e) async { - if (e.params.containsKey('provider')) { - var user = await e.request?.container?.makeAsync(); - - if (user == null) - throw AngelHttpException.forbidden( - message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS); - - var roleFinder = getRoles ?? (user) => []; - 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( - idField: idField, - ownerField: ownerField, - errorMessage: errorMessage, - getId: getId, - getOwnerId: getOwnerId); - await listener(e); - } else - throw AngelHttpException.forbidden( - message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS); - } - } - }; - } - - /// Restricts a route to users who have sufficient permissions. - RequestHandler toMiddleware({String message, getRoles(user)}) { - return (RequestContext req, ResponseContext res) async { - var user = await req.container.makeAsync(); - - if (user == null) - throw AngelHttpException.forbidden( - message: message ?? - 'You have insufficient permissions to perform this action.'); - - var roleFinder = getRoles ?? (user) async => user.roles ?? []; - List roles = (await roleFinder(user)).toList(); - - if (!roles.any(verify)) - throw AngelHttpException.forbidden( - message: message ?? - 'You have insufficient permissions to perform this action.'); - - return true; - }; - } - - /// Returns `true` if the [given] permission string - /// represents a sufficient permission, matching the [minimum]. - bool verify(String given) { - bool verifyOne(String minimum) { - if (minimum == '*') return true; - - var minSplit = minimum.split(':'); - var split = given.split(':'); - - for (int i = 0; i < minSplit.length; i++) { - if (i >= split.length) return false; - var min = minSplit[i], giv = split[i]; - - if (min == '*' || min == giv) { - if (i >= minSplit.length - 1) - return true; - else - continue; - } else - return false; - } - - return false; - } - - var minima = minimum - .split('|') - .map((str) => str.trim()) - .where((str) => str.isNotEmpty); - return minima.any(verifyOne); - } - - @override - String toString() => 'Permission: $minimum'; -} - -/// Builds [Permission]s. -class PermissionBuilder { - String _min; - - /// A minimum - PermissionBuilder(this._min); - - factory PermissionBuilder.wildcard() => PermissionBuilder('*'); - - PermissionBuilder operator +(other) { - if (other is String) - return add(other); - else if (other is PermissionBuilder) - return add(other._min); - else if (other is Permission) - return add(other.minimum); - else - throw ArgumentError( - 'Cannot add a ${other.runtimeType} to a PermissionBuilder.'); - } - - PermissionBuilder operator |(other) { - if (other is String) - return or(PermissionBuilder(other)); - else if (other is PermissionBuilder) - return or(other); - else if (other is Permission) - return or(PermissionBuilder(other.minimum)); - else - throw ArgumentError( - 'Cannot or a ${other.runtimeType} and a PermissionBuilder.'); - } - - /// Adds another level of [constraint]. - PermissionBuilder add(String constraint) => - PermissionBuilder('$_min:$constraint'); - - /// Adds a wildcard permission. - PermissionBuilder allowAll() => add('*'); - - /// Duplicates this builder. - PermissionBuilder clone() => PermissionBuilder(_min); - - /// Allows an alternative permission. - PermissionBuilder or(PermissionBuilder other) => - PermissionBuilder('$_min | ${other._min}'); - - /// Builds a [Permission]. - Permission toPermission() => Permission(_min); -} diff --git a/lib/src/sanitize.dart b/lib/src/sanitize.dart deleted file mode 100644 index 8a9df338..00000000 --- a/lib/src/sanitize.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:convert'; -import 'package:angel_framework/angel_framework.dart'; - -final Map defaultSanitizers = { - 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): '' -}; - -/// Mitigates XSS risk by sanitizing user HTML input. -/// -/// You can also provide a Map of patterns to [replace]. -/// -/// You can sanitize the [body] or [query] (both `true` by default). -RequestHandler sanitizeHtmlInput( - {bool body = true, - bool query = true, - Map replace = const {}}) { - var sanitizers = Map.from(defaultSanitizers) - ..addAll(replace ?? {}); - - return (req, res) async { - if (body) { - await req.parseBody(); - _sanitizeMap(req.bodyAsMap, sanitizers); - } - - if (query) _sanitizeMap(req.queryParameters, sanitizers); - return true; - }; -} - -_sanitize(v, Map sanitizers) { - if (v is String) { - var str = v; - - sanitizers.forEach((needle, replace) { - str = str.replaceAll(needle, replace); - }); - - return htmlEscape.convert(str); - } else if (v is Map) { - _sanitizeMap(v, sanitizers); - return v; - } else if (v is Iterable) { - bool isList = v is List; - var mapped = v.map((x) => _sanitize(x, sanitizers)); - return isList ? mapped.toList() : mapped; - } else - return v; -} - -void _sanitizeMap(Map data, Map sanitizers) { - data.forEach((k, v) { - data[k] = _sanitize(v, sanitizers); - }); -} diff --git a/lib/src/throttle.dart b/lib/src/throttle.dart deleted file mode 100644 index cf234a41..00000000 --- a/lib/src/throttle.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; - -/// Prevents users from sending more than a given [max] number of -/// requests within a given [duration]. -/// -/// Use [identify] to create a unique identifier for each request. -/// The default is to identify requests by their source IP. -/// -/// This works well attached to a `multiserver` instance. -RequestHandler throttleRequests(int max, Duration duration, - {String message = '429 Too Many Requests', identify(RequestContext req)}) { - var identifyRequest = identify ?? (RequestContext req) async => req.ip; - Map table = {}; - Map> times = {}; - - return (RequestContext req, ResponseContext res) async { - var id = (await identifyRequest(req)).toString(); - int currentCount; - - var now = DateTime.now().millisecondsSinceEpoch; - int firstVisit; - - // If the user has visited within the given duration... - if (times.containsKey(id)) { - firstVisit = times[id].first; - } - - // If difference in times is greater than duration, reset counter ;) - if (firstVisit != null) { - if (now - firstVisit > duration.inMilliseconds) { - table.remove(id); - times.remove(id); - } - } - - // Save to time table - if (times.containsKey(id)) - times[id].add(now); - else - times[id] = [now]; - - if (table.containsKey(id)) - currentCount = table[id] = table[id] + 1; - else - currentCount = table[id] = 1; - - if (currentCount > max) { - throw AngelHttpException(null, - statusCode: 429, message: message ?? '429 Too Many Requests'); - } - - return true; - }; -} diff --git a/lib/src/trust_proxy.dart b/lib/src/trust_proxy.dart deleted file mode 100644 index 11122aaa..00000000 --- a/lib/src/trust_proxy.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; - -/// Injects a [ForwardedClient] if the user comes from a -/// trusted proxy. -/// -/// [filter] can be: -/// A `String`, `RegExp`, `InternetAddress`, or an `Iterable`. -/// -/// String can take the following formats: -/// 1. 1.2.3.4 -/// 2. 1.2.3.*, 1.2.*.*, etc. -RequestHandler trustProxy(filter) { - var filters = []; - Iterable inputs = filter is Iterable ? filter : [filter]; - - for (var input in inputs) { - if (input is RegExp || input is InternetAddress) - filters.add(input); - else if (input is String) { - if (!input.contains('*')) - filters.add(input); - else { - filters.add(RegExp(input.replaceAll('*', '[0-9]+'))); - } - } else - throw ArgumentError('Cannot use $input as a trusted proxy filter.'); - } - - return (RequestContext req, ResponseContext res) async { - var ip = req.ip; - - bool check() { - for (var input in filters) { - if (input is RegExp && input.hasMatch(ip)) - return true; - else if (input is InternetAddress && input.address == ip) - return true; - else if (input is String && input == ip) return true; - } - - return false; - } - - if (check()) { - Map> headers = {}; - - req.headers.forEach((k, v) { - if (k.trim().toLowerCase().startsWith('x-forwarded')) headers[k] = v; - }); - - req.container - .registerSingleton(_ForwardedClientImpl(headers)); - } - - return true; - }; -} - -/// Presents information about the client forwarded by a trusted -/// reverse proxy. -abstract class ForwardedClient { - Map> get headers; - - String get ip => headers['x-forwarded-for']?.join(','); - String get host => headers['x-forwarded-host']?.join(','); - String get protocol => headers['x-forwarded-proto']?.join(','); - - int get port { - var portString = headers['x-forwarded-proto']?.join(','); - return portString != null ? int.parse(portString) : null; - } -} - -class _ForwardedClientImpl extends ForwardedClient { - final Map> _headers; - - _ForwardedClientImpl(this._headers); - - @override - Map> get headers => - Map>.unmodifiable(_headers); -} diff --git a/pubspec.yaml b/pubspec.yaml index 87b973c7..094b57a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ homepage: https://github.com/angel-dart/security environment: sdk: ">=2.0.0-dev <3.0.0" dependencies: - angel_framework: ^2.0.0-alpha + angel_framework: ^2.0.0 dev_dependencies: angel_auth: ^2.0.0 angel_test: ^2.0.0 @@ -14,6 +14,3 @@ dev_dependencies: console: ^3.0.0 pedantic: ^1.0.0 test: ^1.0.0 -dependency_overrides: - angel_framework: - path: ../framework diff --git a/test/ban_test.dart b/test/ban_test.dart deleted file mode 100644 index 70c64c54..00000000 --- a/test/ban_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_security/angel_security.dart'; -import 'package:angel_test/angel_test.dart'; -import 'package:test/test.dart'; - -main() { - Angel app; - TestClient client; - - setUp(() async { - 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); - }); - - tearDown(() => client.close()); - - test('ban everyone', () async { - var response = await client.get('/ban'); - print(response.body); - expect(response, hasStatus(403)); - expect(response.body.contains('WTF'), isFalse); - }); - - group('origin', () { - test('ban everyone', () async { - var response = await client - .get('/ban-origin', headers: {'Origin': 'www.example.com'}); - print(response.body); - expect(response, hasStatus(403)); - expect(response.body.contains('WTF'), isFalse); - }); - - test('ban specific', () async { - var response = - await client.get('/allow-origin', headers: {'Origin': 'www.foo.bar'}); - print(response.body); - expect(response, hasStatus(403)); - expect(response.body.contains('YAY'), isFalse); - - response = await client - .get('/allow-origin', headers: {'Origin': 'www.example.com'}); - print(response.body); - expect(response, hasStatus(200)); - expect(response.body, contains('YAY')); - }); - }); -} diff --git a/test/csrf_token_test.dart b/test/csrf_token_test.dart deleted file mode 100644 index 33bfe4d2..00000000 --- a/test/csrf_token_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_security/angel_security.dart'; -import 'package:angel_test/angel_test.dart'; -import 'package:http/http.dart' as http; -import 'package:test/test.dart'; - -final RegExp _sessId = RegExp(r'DARTSESSID=([^;]+);'); - -main() async { - Angel app; - TestClient client; - - setUp(() async { - app = Angel(); - - app - ..chain([verifyCsrfToken()]).get('/valid', (req, res) => 'Valid!') - ..fallback(setCsrfToken()); - - client = await connectTo(app); - }); - - tearDown(() => client.close()); - - test('need pre-existing token', () async { - var response = await client.get('/valid?csrf_token=evil'); - print(response.body); - expect(response, hasStatus(403)); - }); - - test('fake token', () async { - // Get a valid CSRF, but ignore it. - var response = await client.get('/'); - var sessionId = getCookie(response); - response = await client.get( - Uri(path: '/valid', queryParameters: {'csrf_token': 'evil'}), - headers: {'cookie': 'DARTSESSID=$sessionId'}); - print(response.body); - expect(response, hasStatus(400)); - expect(response.body.contains('Valid'), isFalse); - expect(response.body, contains('Invalid CSRF token')); - }); -} - -String getCookie(http.Response response) { - if (response.headers.containsKey('set-cookie')) { - var header = response.headers['set-cookie']; - var match = _sessId.firstMatch(header); - return match?.group(1); - } else - return null; -} diff --git a/test/hooks_test.dart b/test/hooks_test.dart deleted file mode 100644 index 22f1a8b8..00000000 --- a/test/hooks_test.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'dart:async'; - -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_security/angel_security.dart'; -import 'package:angel_security/hooks.dart' as hooks; -import 'package:angel_test/angel_test.dart'; -import 'package:crypto/crypto.dart'; -import 'package:logging/logging.dart'; -import 'package:pedantic/pedantic.dart'; -import 'package:test/test.dart'; -import 'pretty_logging.dart'; - -void main() { - Angel app; - TestClient client; - - setUp(() async { - var logger = Logger.detached('hooks_test')..onRecord.listen(prettyLog); - app = Angel(logger: logger); - client = await connectTo(app); - - HookedService> serve(String path, - T Function(Map) encoder, Map Function(T) decoder) { - var inner = MapService(); - var mapped = inner.map(encoder, decoder); - return app.use>(path, mapped); - } - - var userService = - serve('/api/users', User.fromMap, (u) => u.toJson()); - var houseService = - serve('/api/houses', House.fromMap, (h) => h.toJson()); - - // Seed things up. - var pSherman = await userService.create(User('0', 'P Sherman')); - await houseService.create(House('0', pSherman.id, '42 Wallaby Way')); - await houseService.create(House('1', pSherman.id, 'Finding Nemo')); - await houseService - .create(House('1', '4', 'Should Not Appear for P. Sherman')); - - // Inject a user depending on the authorization header. - app.container.registerFactory>((container) async { - var req = container.make(); - var authValue = - req.headers.value('authorization')?.replaceAll('Bearer', '')?.trim(); - if (authValue == null) - throw AngelHttpException.badRequest( - message: 'Missing "authorization".'); - var user = await userService.read(authValue).catchError((_) => null); - if (user == null) - throw AngelHttpException.notAuthenticated( - message: 'Invalid "authorization" ($authValue).'); - return user; - }); - - // ACCESS CONTROL: - - // A user can only see their own houses. - houseService.beforeIndexed.listen(hooks.queryWithCurrentUser( - as: 'owner_id', - getId: (user) => user.id, - )); - - // A house is associated with the current user. - houseService.beforeCreated - .listen(hooks.associateCurrentUser( - getId: (user) => user.id, - assignUserId: (id, house) => house.withOwner(id), - )); - }); - - tearDown(() async { - app.logger.clearListeners(); - await app.close(); - unawaited(client.close()); - }); - - test('query with current user', () async { - client.authToken = '0'; - var houseService = client.service('/api/houses'); - expect(await houseService.index(), [ - {'id': '0', 'owner_id': '0', 'address': '42 Wallaby Way'}, - {'id': '1', 'owner_id': '0', 'address': 'Finding Nemo'} - ]); - }); - - test('associate current user', () async { - client.authToken = '0'; - var houseService = client.service('/api/houses'); - expect( - await houseService.create({'address': 'Hello'}), - allOf( - containsPair('address', 'Hello'), - containsPair('owner_id', '0'), - )); - }); -} - -class User { - final String id; - final String name; - - User(this.id, this.name); - - static User fromMap(Map map) => - User(map['id'] as String, map['name'] as String); - - Map toJson() { - return {'id': id, 'name': name}; - } -} - -class House { - final String id; - final String ownerId; - final String address; - - House(this.id, this.ownerId, this.address); - - static House fromMap(Map map) { - print('In $map'); - return House(map['id'] as String, map['owner_id'] as String, - map['address'] as String); - } - - House withOwner(String newOwnerId) { - return House(id, newOwnerId, address); - } - - Map toJson() { - print('Out ${{'id': id, 'owner_id': ownerId, 'address': address}}'); - return {'id': id, 'owner_id': ownerId, 'address': address}; - } -} diff --git a/test/hooks_test.dart.old b/test/hooks_test.dart.old deleted file mode 100644 index 02f95b32..00000000 --- a/test/hooks_test.dart.old +++ /dev/null @@ -1,286 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_security/angel_security.dart'; -import 'package:angel_security/hooks.dart' as hooks; -import 'package:angel_test/angel_test.dart'; -import 'package:crypto/crypto.dart'; -import 'package:test/test.dart'; - -main() { - Angel app; - TestClient client; - - setUp(() async { - app = Angel() - ..fallback((req, res) async { - var xUser = req.headers.value('X-User'); - if (xUser != null) { - req.container.registerSingleton( - User(id: xUser, roles: xUser == 'John' ? ['foo:bar'] : [])); - } - return true; - }); - - app - ..use('/user_data', UserDataService()) - ..use('/artists', ArtistService()) - ..use('/roled', RoledService()); - - (app.findService('user_data') as HookedService) - ..beforeIndexed.listen(hooks.queryWithCurrentUser()) - ..beforeCreated.listen(hooks.hashPassword()); - - app.findService('artists') as HookedService - ..beforeIndexed.listen(hooks.restrictToAuthenticated()) - ..beforeRead.listen(hooks.restrictToOwner()) - ..beforeCreated - .listen(hooks.associateCurrentUser()); - - (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) { - print(e.toJson()); - print(e.stackTrace); - return errorHandler(e, req, res); - }; - - client = await connectTo(app); - }); - - tearDown(() => client.close()); - - group('associateCurrentUser', () { - test('fail', () async { - try { - var response = await client.service('artists').create({'foo': 'bar'}); - print(response); - throw StateError('Creating without userId bad request'); - } catch (e) { - print(e); - expect(e, const TypeMatcher()); - var err = e as AngelHttpException; - expect(err.statusCode, equals(403)); - } - }); - - test('succeed', () async { - var response = await client - .post('/artists', headers: {'X-User': 'John'}, body: {'foo': 'bar'}); - print('Response: ${response.body}'); - print('Status: ${response.statusCode}'); - expect(response, allOf(hasStatus(201), isJson({'foo': 'bar'}))); - }); - }); - - group('queryWithCurrentUser', () { - test('fail', () async { - try { - var response = await client.service('user_data').index(); - print(response); - throw StateError('Indexing without user forbidden'); - } catch (e) { - print(e); - expect(e, const TypeMatcher()); - var err = e as AngelHttpException; - expect(err.statusCode, equals(403)); - } - }); - - test('succeed', () async { - var response = await client.get('user_data', headers: {'X-User': 'John'}); - print('Response: ${response.body}'); - expect(response, allOf(hasStatus(200), isJson(['foo', 'bar']))); - }); - }); - - test('hashPassword', () async { - var response = await client - .service('user_data') - .create({'username': 'foo', 'password': 'jdoe1'}); - print('Response: ${response}'); - expect(response, equals({'foo': 'bar'})); - }); - - group('restrictToAuthenticated', () { - test('fail', () async { - try { - var response = await client.service('artists').index(); - print(response); - throw StateError('Indexing without user forbidden'); - } catch (e) { - print(e); - expect(e, const TypeMatcher()); - var err = e as AngelHttpException; - expect(err.statusCode, equals(403)); - } - }); - - test('succeed', () async { - var response = await client.get('/artists', headers: {'X-User': 'John'}); - print('Response: ${response.body}'); - expect( - response, - allOf( - hasStatus(200), - isJson([ - { - "id": "king_of_pop", - "userId": "John", - "name": "Michael Jackson" - }, - {"id": "raymond", "userId": "Bob", "name": "Usher"} - ]))); - }); - }); - - group('restrictToOwner', () { - test('fail', () async { - try { - var response = await client.service('artists').read('king_of_pop'); - print(response); - throw StateError('Reading without owner forbidden'); - } catch (e) { - print(e); - expect(e, const TypeMatcher()); - var err = e as AngelHttpException; - expect(err.statusCode, equals(401)); - } - }); - - test('succeed', () async { - var response = - await client.get('/artists/king_of_pop', headers: {'X-User': 'John'}); - print('Response: ${response.body}'); - expect( - response, - allOf( - hasStatus(200), - isJson({ - "id": "king_of_pop", - "userId": "John", - "name": "Michael Jackson" - }))); - }); - }); - - group('permission restrict', () { - test('fail', () async { - try { - var response = await client.service('roled').index(); - print(response); - throw StateError('Reading without roles forbidden'); - } catch (e) { - print(e); - expect(e, const TypeMatcher()); - var err = e as AngelHttpException; - expect(err.statusCode, equals(403)); - } - }); - - test('succeed', () async { - var response = - await client.get('/roled/king_of_pop', headers: {'X-User': 'John'}); - print('Response: ${response.body}'); - expect( - response, - allOf( - hasStatus(200), - isJson({ - "id": "king_of_pop", - "userId": "John", - "name": "Michael Jackson" - }))); - }); - - test('owner', () async { - var response = - await client.get('/roled/raymond', headers: {'X-User': 'Bob'}); - print('Response: ${response.body}'); - expect( - response, - allOf(hasStatus(200), - isJson({"id": "raymond", "userId": "Bob", "name": "Usher"}))); - }); - }); -} - -class User { - String id; - List roles; - User({this.id, this.roles = const []}); -} - -class UserDataService extends Service { - static const Map _data = const { - 'John': const ['foo', 'bar'] - }; - - @override - index([Map params]) async { - print('Params: $params'); - if (params?.containsKey('query') != true) - throw AngelHttpException.badRequest(message: 'query required'); - - String name = params['query']['userId']?.toString(); - - if (!_data.containsKey(name)) - throw AngelHttpException.notFound( - message: "No data found for user '$name'."); - - return _data[name]; - } - - @override - create(data, [Map params]) async { - if (data is! Map || !data.containsKey('password')) - throw AngelHttpException.badRequest(message: 'Required password!'); - - var expected = - String.fromCharCodes(sha256.convert('jdoe1'.codeUnits).bytes); - - if (data['password'] != (expected)) - throw AngelHttpException.conflict(message: 'Passwords do not match.'); - return {'foo': 'bar'}; - } -} - -class ArtistService extends Service { - static const List _ARTISTS = const [_MICHAEL_JACKSON, _USHER]; - - @override - index([params]) async => _ARTISTS; - - @override - read(id, [params]) async => _ARTISTS.firstWhere((a) => a.id == id); - - @override - create(data, [params]) async { - return data; - // if (data is! Map || !data.containsKey('userId')) - // throw AngelHttpException.badRequest(message: 'Required userId'); - // return {'foo': 'bar'}; - } -} - -class Artist { - final String id, userId, name; - const Artist({this.id, this.userId, this.name}); -} - -const Artist _USHER = const Artist(id: 'raymond', userId: 'Bob', name: 'Usher'); -const Artist _MICHAEL_JACKSON = - const Artist(id: 'king_of_pop', userId: 'John', name: 'Michael Jackson'); - -class RoledService extends Service { - @override - index([params]) async { - return [Artist(name: 'foo')]; - // return ['foo']; - } - - @override - read(id, [params]) async => - ArtistService._ARTISTS.firstWhere((a) => a.id == id); -} diff --git a/test/permission_test.dart b/test/permission_test.dart deleted file mode 100644 index 212ced08..00000000 --- a/test/permission_test.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_security/angel_security.dart'; -import 'package:angel_test/angel_test.dart'; -import 'package:test/test.dart'; - -class User { - final List roles; - - User(this.roles); -} - -main() { - Angel app; - TestClient client; - - setUp(() async { - app = Angel(); - - 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['X-Roles']; - - if (xRoles?.isNotEmpty == true) { - req.container.registerSingleton(User(xRoles)); - } - - return true; - }); - - 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([ - PermissionBuilder('super') - .add('specific') - .add('permission') - .allowAll() - .or(PermissionBuilder('admin')) - .toPermission() - .toMiddleware() - ]).get('/or', (req, res) => 'Hello, world!'); - - client = await connectTo(app); - }); - - tearDown(() => client.close()); - - test('open permission', () async { - var response = await client.get('/', headers: {'X-Roles': 'foo'}); - print('Response: ${response.body}'); - expect(response, hasStatus(200)); - expect(response, isJson('Hello, world!')); - }); - - group('restrict', () { - test('one', () async { - var response = await client.get('/one', headers: {'X-Roles': 'foo'}); - print('Response: ${response.body}'); - expect(response, hasStatus(200)); - expect(response, isJson('Hello, world!')); - - response = await client.get('/one', headers: {'X-Roles': 'bar'}); - print('Response: ${response.body}'); - expect(response, hasStatus(403)); - }); - - test('two', () async { - var response = await client.get('/two', headers: {'X-Roles': 'two:foo'}); - print('Response: ${response.body}'); - expect(response, hasStatus(200)); - expect(response, isJson('Hello, world!')); - - response = await client.get('/two', headers: {'X-Roles': 'two:bar'}); - print('Response: ${response.body}'); - expect(response, hasStatus(403)); - }); - - test('two with star', () async { - var response = - await client.get('/two-star', headers: {'X-Roles': 'two:foo'}); - print('Response: ${response.body}'); - expect(response, hasStatus(200)); - expect(response, isJson('Hello, world!')); - - response = - await client.get('/two-star', headers: {'X-Roles': 'three:foo'}); - print('Response: ${response.body}'); - expect(response, hasStatus(403)); - }); - - test('three', () async { - var response = - await client.get('/three', headers: {'X-Roles': 'three:foo:bar'}); - print('Response: ${response.body}'); - expect(response, hasStatus(200)); - expect(response, isJson('Hello, world!')); - - response = - await client.get('/three', headers: {'X-Roles': 'three:foo:baz'}); - print('Response: ${response.body}'); - expect(response, hasStatus(403)); - - response = - await client.get('/three', headers: {'X-Roles': 'three:foz:bar'}); - print('Response: ${response.body}'); - expect(response, hasStatus(403)); - }); - - test('three with star', () async { - var response = await client - .get('/three-star', headers: {'X-Roles': 'three:foo:bar'}); - print('Response: ${response.body}'); - expect(response, hasStatus(200)); - expect(response, isJson('Hello, world!')); - - response = await client - .get('/three-star', headers: {'X-Roles': 'three:foz:bar'}); - print('Response: ${response.body}'); - expect(response, hasStatus(200)); - expect(response, isJson('Hello, world!')); - - response = await client - .get('/three-star', headers: {'X-Roles': 'three:foo:baz'}); - print('Response: ${response.body}'); - expect(response, hasStatus(403)); - }); - }); - - test('or', () async { - var response = await client.get('/or', headers: {'X-Roles': 'admin'}); - print('Response: ${response.body}'); - expect(response, hasStatus(200)); - expect(response, isJson('Hello, world!')); - - response = await client - .get('/or', headers: {'X-Roles': 'not:specific:enough:i:guess'}); - print('Response: ${response.body}'); - expect(response, hasStatus(403)); - }); -} diff --git a/test/pretty_logging.dart b/test/pretty_logging.dart deleted file mode 100644 index 6552719e..00000000 --- a/test/pretty_logging.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:console/console.dart'; -import 'package:logging/logging.dart'; - -/// Prints the contents of a [LogRecord] with pretty colors. -prettyLog(LogRecord record) async { - 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()); - - pen(); -} - -/// Chooses a color based on the logger [level]. -void chooseLogColor(TextPen pen, Level level) { - if (level == Level.SHOUT) - pen.darkRed(); - else if (level == Level.SEVERE) - pen.red(); - else if (level == Level.WARNING) - pen.yellow(); - else if (level == Level.INFO) - pen.magenta(); - else if (level == Level.FINER) - pen.blue(); - else if (level == Level.FINEST) pen.darkBlue(); -} diff --git a/test/sanitize_test.dart b/test/sanitize_test.dart deleted file mode 100644 index e0fff4e7..00000000 --- a/test/sanitize_test.dart +++ /dev/null @@ -1,126 +0,0 @@ -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 = Validator({'html*': isString}); - -main() async { - Angel app; - TestClient client; - - setUp(() async { - var logger = Logger.detached('angel_security')..onRecord.listen(prettyLog); - app = Angel(logger: logger); - app.chain([validate(untrustedSchema), sanitizeHtmlInput()]) - ..post('/untrusted', (RequestContext req, ResponseContext res) async { - String untrusted = req.bodyAsMap['html']; - res - ..contentType = MediaType('text', 'html') - ..write(''' - - - - Potential Security Hole - - $untrusted - '''); - }) - ..post('/attribute', (RequestContext req, ResponseContext res) async { - String untrusted = req.bodyAsMap['html']; - res - ..contentType = MediaType('text', 'html') - ..write(''' - - - - Potential Security Hole - - - - - '''); - }); - - var oldHandler = app.errorHandler; - app.errorHandler = (e, req, res) { - app.logger.severe(e, e.error, e.stackTrace); - return oldHandler(e, req, res); - }; - - client = await connectTo(app); - }); - - tearDown(() => client.close()); - - group('script tag', () { - test('normal', () async { - var xss = ""; - var response = await client.post('/untrusted', body: {'html': xss}); - print(response.body); - expect(response.body.contains(xss), isFalse); - expect(response.body.toLowerCase().contains('"; - var response = await client.post('/untrusted', body: {'html': xss}); - print(response.body); - expect(response.body.contains(xss), isFalse); - expect(response.body.toLowerCase().contains('"'; - var response = await client.post('/attribute', body: {'html': xss}); - print(response.body); - expect(response.body.contains(xss), isFalse); - expect(response.body.toLowerCase().contains('