2016-09-15 19:53:01 +00:00
|
|
|
library angel_framework.http.request_context;
|
2016-10-22 20:41:36 +00:00
|
|
|
|
2016-09-15 19:53:01 +00:00
|
|
|
import 'dart:async';
|
2018-12-09 04:18:31 +00:00
|
|
|
import 'dart:convert';
|
2018-08-20 03:27:34 +00:00
|
|
|
import 'dart:io' show Cookie, HttpHeaders, HttpSession, InternetAddress;
|
|
|
|
|
2018-08-20 19:42:05 +00:00
|
|
|
import 'package:angel_container/angel_container.dart';
|
2018-08-20 03:27:34 +00:00
|
|
|
import 'package:http_parser/http_parser.dart';
|
2018-12-09 04:18:31 +00:00
|
|
|
import 'package:http_server/http_server.dart';
|
2018-02-07 04:59:05 +00:00
|
|
|
import 'package:meta/meta.dart';
|
2018-12-09 04:18:31 +00:00
|
|
|
import 'package:mime/mime.dart';
|
2017-11-28 18:14:50 +00:00
|
|
|
import 'package:path/path.dart' as p;
|
2018-08-20 03:27:34 +00:00
|
|
|
|
2017-10-10 16:55:42 +00:00
|
|
|
import 'metadata.dart';
|
2017-09-24 19:43:14 +00:00
|
|
|
import 'response_context.dart';
|
|
|
|
import 'routable.dart';
|
2017-03-02 04:04:37 +00:00
|
|
|
import 'server.dart' show Angel;
|
2018-06-08 07:06:26 +00:00
|
|
|
|
2017-09-24 19:43:14 +00:00
|
|
|
part 'injection.dart';
|
2016-04-18 03:27:23 +00:00
|
|
|
|
2018-08-20 03:27:34 +00:00
|
|
|
/// A convenience wrapper around an incoming [RawRequest].
|
|
|
|
abstract class RequestContext<RawRequest> {
|
2017-11-28 18:14:50 +00:00
|
|
|
String _acceptHeaderCache, _extensionCache;
|
2018-12-09 04:18:31 +00:00
|
|
|
bool _acceptsAllCache, _hasParsedBody;
|
|
|
|
Map<String, dynamic> _bodyFields, _queryParameters;
|
|
|
|
List _bodyList;
|
|
|
|
Object _bodyObject;
|
|
|
|
List<HttpMultipartFormData> _bodyFiles;
|
|
|
|
MediaType _contentType;
|
2017-08-03 16:40:21 +00:00
|
|
|
|
2018-08-20 03:27:34 +00:00
|
|
|
/// The underlying [RawRequest] provided by the driver.
|
|
|
|
RawRequest get rawRequest;
|
|
|
|
|
2017-02-23 00:37:15 +00:00
|
|
|
/// Additional params to be passed to services.
|
2018-09-11 20:25:07 +00:00
|
|
|
final Map<String, dynamic> serviceParams = {};
|
2017-02-23 00:37:15 +00:00
|
|
|
|
2016-04-18 03:27:23 +00:00
|
|
|
/// The [Angel] instance that is responding to this request.
|
2017-03-02 04:04:37 +00:00
|
|
|
Angel app;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// Any cookies sent with this request.
|
2018-02-07 04:59:05 +00:00
|
|
|
List<Cookie> get cookies;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// All HTTP headers sent with this request.
|
2018-02-07 04:59:05 +00:00
|
|
|
HttpHeaders get headers;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The requested hostname.
|
2018-02-07 04:59:05 +00:00
|
|
|
String get hostname;
|
2016-11-23 19:50:17 +00:00
|
|
|
|
2018-08-20 19:42:05 +00:00
|
|
|
/// The IoC container that can be used to provide functionality to produce
|
|
|
|
/// objects of a given type.
|
|
|
|
///
|
|
|
|
/// This is a *child* of the container found in `app`.
|
|
|
|
Container get container;
|
2016-11-23 19:50:17 +00:00
|
|
|
|
2016-04-18 03:27:23 +00:00
|
|
|
/// The user's IP.
|
|
|
|
String get ip => remoteAddress.address;
|
|
|
|
|
|
|
|
/// This request's HTTP method.
|
2017-03-02 22:06:02 +00:00
|
|
|
///
|
|
|
|
/// This may have been processed by an override. See [originalMethod] to get the real method.
|
2018-02-07 04:59:05 +00:00
|
|
|
String get method;
|
2017-03-02 22:06:02 +00:00
|
|
|
|
|
|
|
/// The original HTTP verb sent to the server.
|
2018-02-07 04:59:05 +00:00
|
|
|
String get originalMethod;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The content type of an incoming request.
|
2018-11-11 01:07:09 +00:00
|
|
|
MediaType get contentType =>
|
2018-12-09 04:18:31 +00:00
|
|
|
_contentType ??= new MediaType.parse(headers.contentType.toString());
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The URL parameters extracted from the request URI.
|
2018-08-20 20:43:38 +00:00
|
|
|
Map<String, dynamic> params = <String, dynamic>{};
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The requested path.
|
2018-02-07 04:59:05 +00:00
|
|
|
String get path;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
2018-11-11 01:07:09 +00:00
|
|
|
/// Is this an **XMLHttpRequest**?
|
|
|
|
bool get isXhr {
|
|
|
|
return headers.value("X-Requested-With")?.trim()?.toLowerCase() ==
|
|
|
|
'xmlhttprequest';
|
|
|
|
}
|
|
|
|
|
2016-04-18 03:27:23 +00:00
|
|
|
/// The remote address requesting this resource.
|
2018-02-07 04:59:05 +00:00
|
|
|
InternetAddress get remoteAddress;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The user's HTTP session.
|
2018-02-07 04:59:05 +00:00
|
|
|
HttpSession get session;
|
2016-10-22 20:41:36 +00:00
|
|
|
|
|
|
|
/// The [Uri] instance representing the path this request is responding to.
|
2018-02-07 04:59:05 +00:00
|
|
|
Uri get uri;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
2018-12-09 04:18:31 +00:00
|
|
|
/// The [Stream] of incoming binary data sent from the client.
|
|
|
|
Stream<List<int>> get body;
|
|
|
|
|
|
|
|
/// Returns `true` if [parseBody] has been called so far.
|
|
|
|
bool get hasParsedBody => _hasParsedBody;
|
|
|
|
|
|
|
|
/// Returns a *mutable* [Map] of the fields parsed from the request [body].
|
|
|
|
///
|
|
|
|
/// Note that [parseBody] must be called first.
|
|
|
|
Map<String, dynamic> get bodyFields {
|
|
|
|
if (!hasParsedBody) {
|
|
|
|
throw new StateError('The request body has not been parsed yet.');
|
|
|
|
} else if (_bodyFields == null) {
|
|
|
|
throw new StateError('The request body, $_bodyObject, is not a Map.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return _bodyFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a *mutable* [List] parsed from the request [body].
|
|
|
|
///
|
|
|
|
/// Note that [parseBody] must be called first.
|
|
|
|
List get bodyList {
|
|
|
|
if (!hasParsedBody) {
|
|
|
|
throw new StateError('The request body has not been parsed yet.');
|
|
|
|
} else if (_bodyList == null) {
|
|
|
|
throw new StateError('The request body, $_bodyObject, is not a List.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return _bodyList;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the parsed request body, whatever it may be (typically a [Map] or [List]).
|
|
|
|
///
|
|
|
|
/// Note that [parseBody] must be called first.
|
|
|
|
Object get bodyObject {
|
|
|
|
if (!hasParsedBody) {
|
|
|
|
throw new StateError('The request body has not been parsed yet.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return _bodyObject;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a *mutable* map of the files parsed from the request [body].
|
|
|
|
///
|
|
|
|
/// Note that [parseBody] must be called first.
|
|
|
|
List<HttpMultipartFormData> get bodyFiles {
|
|
|
|
if (!hasParsedBody) {
|
|
|
|
throw new StateError('The request body has not been parsed yet.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return _bodyFiles;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a *mutable* map of the fields contained in the query.
|
|
|
|
Map<String, dynamic> get queryParameters =>
|
|
|
|
_queryParameters ??= new Map<String, dynamic>.from(uri.queryParameters);
|
|
|
|
|
2017-11-28 18:14:50 +00:00
|
|
|
/// Returns the file extension of the requested path, if any.
|
|
|
|
///
|
|
|
|
/// Includes the leading `.`, if there is one.
|
|
|
|
String get extension => _extensionCache ??= p.extension(uri.path);
|
|
|
|
|
2017-07-10 23:08:05 +00:00
|
|
|
/// Returns `true` if the client's `Accept` header indicates that the given [contentType] is considered a valid response.
|
|
|
|
///
|
|
|
|
/// You cannot provide a `null` [contentType].
|
|
|
|
/// If the `Accept` header's value is `*/*`, this method will always return `true`.
|
2017-11-28 18:14:50 +00:00
|
|
|
/// To ignore the wildcard (`*/*`), pass [strict] as `true`.
|
2017-07-10 23:08:05 +00:00
|
|
|
///
|
|
|
|
/// [contentType] can be either of the following:
|
|
|
|
/// * A [ContentType], in which case the `Accept` header will be compared against its `mimeType` property.
|
|
|
|
/// * Any other Dart value, in which case the `Accept` header will be compared against the result of a `toString()` call.
|
2017-11-28 18:14:50 +00:00
|
|
|
bool accepts(contentType, {bool strict: false}) {
|
2018-08-20 03:27:34 +00:00
|
|
|
var contentTypeString = contentType is MediaType
|
2017-07-10 23:08:05 +00:00
|
|
|
? contentType.mimeType
|
|
|
|
: contentType?.toString();
|
|
|
|
|
2018-02-07 04:59:05 +00:00
|
|
|
// Change to assert
|
2017-07-10 23:08:05 +00:00
|
|
|
if (contentTypeString == null)
|
|
|
|
throw new ArgumentError(
|
|
|
|
'RequestContext.accepts expects the `contentType` parameter to NOT be null.');
|
|
|
|
|
2018-06-23 03:29:38 +00:00
|
|
|
_acceptHeaderCache ??= headers.value('accept');
|
2017-07-10 23:08:05 +00:00
|
|
|
|
|
|
|
if (_acceptHeaderCache == null)
|
|
|
|
return false;
|
2017-11-28 18:14:50 +00:00
|
|
|
else if (strict != true && _acceptHeaderCache.contains('*/*'))
|
2017-07-10 23:08:05 +00:00
|
|
|
return true;
|
|
|
|
else
|
|
|
|
return _acceptHeaderCache.contains(contentTypeString);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns as `true` if the client's `Accept` header indicates that it will accept any response content type.
|
|
|
|
bool get acceptsAll => _acceptsAllCache ??= accepts('*/*');
|
|
|
|
|
2017-03-28 23:29:22 +00:00
|
|
|
/// Manually parses the request body, if it has not already been parsed.
|
2018-12-09 04:18:31 +00:00
|
|
|
Future<void> parseBody({Encoding encoding: utf8}) async {
|
|
|
|
if (!_hasParsedBody) {
|
|
|
|
_hasParsedBody = true;
|
|
|
|
|
|
|
|
if (contentType.type == 'application' && contentType.subtype == 'json') {
|
|
|
|
_bodyFiles = [];
|
|
|
|
|
|
|
|
var parsed = _bodyObject =
|
|
|
|
await body.transform(encoding.decoder).join().then(json.decode);
|
|
|
|
|
|
|
|
if (parsed is Map) {
|
|
|
|
_bodyFields = new Map<String, dynamic>.from(parsed);
|
|
|
|
} else if (parsed is List) {
|
|
|
|
_bodyList = parsed;
|
|
|
|
}
|
|
|
|
} else if (contentType.type == 'multipart' &&
|
|
|
|
contentType.subtype == 'form-data' &&
|
|
|
|
contentType.parameters.containsKey('boundary')) {
|
|
|
|
var boundary = contentType.parameters['boundary'];
|
|
|
|
var transformer = new MimeMultipartTransformer(boundary);
|
|
|
|
var parts = body.transform(transformer).map((part) =>
|
|
|
|
HttpMultipartFormData.parse(part, defaultEncoding: encoding));
|
|
|
|
_bodyFields = {};
|
|
|
|
_bodyFiles = [];
|
|
|
|
|
|
|
|
await for (var part in parts) {
|
|
|
|
if (part.isBinary) {
|
|
|
|
_bodyFiles.add(part);
|
|
|
|
} else if (part.isText &&
|
|
|
|
part.contentDisposition.parameters.containsKey('name')) {
|
|
|
|
// If there is no name, then don't parse it.
|
|
|
|
var key = part.contentDisposition.parameters['name'];
|
|
|
|
var value = await part.join();
|
|
|
|
_bodyFields[key] = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_bodyFields = {};
|
|
|
|
_bodyFiles = [];
|
|
|
|
}
|
|
|
|
}
|
2017-03-28 23:29:22 +00:00
|
|
|
}
|
2017-10-28 08:50:16 +00:00
|
|
|
|
|
|
|
/// Disposes of all resources.
|
2018-06-08 07:06:26 +00:00
|
|
|
Future close() {
|
2017-10-28 08:50:16 +00:00
|
|
|
_acceptsAllCache = null;
|
|
|
|
_acceptHeaderCache = null;
|
|
|
|
serviceParams.clear();
|
|
|
|
params.clear();
|
2018-06-08 07:06:26 +00:00
|
|
|
return new Future.value();
|
2017-10-28 08:50:16 +00:00
|
|
|
}
|
2016-10-22 20:41:36 +00:00
|
|
|
}
|