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';
|
|
|
|
import 'dart:io';
|
2017-09-24 19:43:14 +00:00
|
|
|
import 'dart:mirrors';
|
2016-09-15 19:53:01 +00:00
|
|
|
import 'package:body_parser/body_parser.dart';
|
2017-08-03 16:40:21 +00:00
|
|
|
import 'package:charcode/charcode.dart';
|
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;
|
2017-09-24 19:43:14 +00:00
|
|
|
part 'injection.dart';
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// A convenience wrapper around an incoming HTTP request.
|
2017-08-03 16:40:21 +00:00
|
|
|
class RequestContext {
|
2017-07-10 23:08:05 +00:00
|
|
|
String _acceptHeaderCache;
|
|
|
|
bool _acceptsAllCache;
|
2016-10-22 20:41:36 +00:00
|
|
|
BodyParseResult _body;
|
|
|
|
ContentType _contentType;
|
2016-11-23 19:50:17 +00:00
|
|
|
HttpRequest _io;
|
2017-03-02 22:06:02 +00:00
|
|
|
String _override, _path;
|
2017-08-03 16:40:21 +00:00
|
|
|
Map _provisionalQuery;
|
|
|
|
|
|
|
|
final Map properties = {};
|
2016-10-22 20:41:36 +00:00
|
|
|
|
2017-02-23 00:37:15 +00:00
|
|
|
/// Additional params to be passed to services.
|
|
|
|
final Map serviceParams = {};
|
|
|
|
|
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.
|
2016-11-23 19:50:17 +00:00
|
|
|
List<Cookie> get cookies => io.cookies;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// All HTTP headers sent with this request.
|
2016-11-23 19:50:17 +00:00
|
|
|
HttpHeaders get headers => io.headers;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The requested hostname.
|
2016-11-23 19:50:17 +00:00
|
|
|
String get hostname => io.headers.value(HttpHeaders.HOST);
|
|
|
|
|
2017-09-24 19:43:14 +00:00
|
|
|
final Map _injections = {};
|
|
|
|
|
|
|
|
/// A [Map] of singletons injected via [inject]. *Read-only*.
|
|
|
|
Map get injections => new Map.unmodifiable(_injections);
|
2016-11-23 19:50:17 +00:00
|
|
|
|
|
|
|
/// The underlying [HttpRequest] instance underneath this context.
|
|
|
|
HttpRequest get io => _io;
|
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.
|
|
|
|
String get method => _override ?? originalMethod;
|
|
|
|
|
|
|
|
/// The original HTTP verb sent to the server.
|
|
|
|
String get originalMethod => io.method;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
2017-03-28 23:29:22 +00:00
|
|
|
StateError _unparsed(String type, String caps) => new StateError(
|
|
|
|
'Cannot get the $type of an unparsed request. Use lazy${caps}() instead.');
|
|
|
|
|
2016-04-18 03:27:23 +00:00
|
|
|
/// All post data submitted to the server.
|
2017-03-28 23:29:22 +00:00
|
|
|
///
|
|
|
|
/// If you are lazy-parsing request bodies, but have not manually [parse]d this one,
|
|
|
|
/// then an error will be thrown.
|
2017-07-09 16:50:46 +00:00
|
|
|
///
|
2017-03-28 23:29:22 +00:00
|
|
|
/// **If you are writing a plug-in, use [lazyBody] instead.**
|
|
|
|
Map get body {
|
|
|
|
if (_body == null)
|
|
|
|
throw _unparsed('body', 'Body');
|
|
|
|
else
|
|
|
|
return _body.body;
|
|
|
|
}
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The content type of an incoming request.
|
2016-10-22 20:41:36 +00:00
|
|
|
ContentType get contentType => _contentType;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// Any and all files sent to the server with this request.
|
2017-03-28 23:29:22 +00:00
|
|
|
///
|
|
|
|
/// If you are lazy-parsing request bodies, but have not manually [parse]d this one,
|
|
|
|
/// then an error will be thrown.
|
2017-07-09 16:50:46 +00:00
|
|
|
///
|
2017-03-28 23:29:22 +00:00
|
|
|
/// **If you are writing a plug-in, use [lazyFiles] instead.**
|
|
|
|
List<FileUploadInfo> get files {
|
|
|
|
if (_body == null)
|
|
|
|
throw _unparsed('query', 'Files');
|
|
|
|
else
|
|
|
|
return _body.files;
|
|
|
|
}
|
2016-04-18 03:27:23 +00:00
|
|
|
|
2017-01-14 13:56:14 +00:00
|
|
|
/// The original body bytes sent with this request. May be empty.
|
2017-03-28 23:29:22 +00:00
|
|
|
///
|
|
|
|
/// If you are lazy-parsing request bodies, but have not manually [parse]d this one,
|
|
|
|
/// then an error will be thrown.
|
2017-07-09 16:50:46 +00:00
|
|
|
///
|
2017-03-28 23:29:22 +00:00
|
|
|
/// **If you are writing a plug-in, use [lazyOriginalBuffer] instead.**
|
|
|
|
List<int> get originalBuffer {
|
|
|
|
if (_body == null)
|
|
|
|
throw _unparsed('original buffer', 'OriginalBuffer');
|
|
|
|
else
|
|
|
|
return _body.originalBuffer ?? [];
|
|
|
|
}
|
2017-01-14 13:56:14 +00:00
|
|
|
|
2016-04-18 03:27:23 +00:00
|
|
|
/// The URL parameters extracted from the request URI.
|
|
|
|
Map params = {};
|
|
|
|
|
|
|
|
/// The requested path.
|
2016-10-22 20:41:36 +00:00
|
|
|
String get path => _path;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The parsed request query string.
|
2017-03-28 23:29:22 +00:00
|
|
|
///
|
|
|
|
/// If you are lazy-parsing request bodies, but have not manually [parse]d this one,
|
|
|
|
/// then [uri].query will be returned.
|
2017-07-09 16:50:46 +00:00
|
|
|
///
|
|
|
|
/// **If you are writing a plug-in, consider using [lazyQuery] instead.**
|
2017-03-28 23:29:22 +00:00
|
|
|
Map get query {
|
|
|
|
if (_body == null)
|
2017-08-03 16:40:21 +00:00
|
|
|
return _provisionalQuery ??= new Map.from(uri.queryParameters);
|
2017-03-28 23:29:22 +00:00
|
|
|
else
|
|
|
|
return _body.query;
|
|
|
|
}
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The remote address requesting this resource.
|
2016-12-21 03:10:03 +00:00
|
|
|
InternetAddress get remoteAddress => io.connectionInfo.remoteAddress;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// The user's HTTP session.
|
2016-11-23 19:50:17 +00:00
|
|
|
HttpSession get session => io.session;
|
2016-10-22 20:41:36 +00:00
|
|
|
|
|
|
|
/// The [Uri] instance representing the path this request is responding to.
|
2016-11-23 19:50:17 +00:00
|
|
|
Uri get uri => io.uri;
|
2016-04-18 03:27:23 +00:00
|
|
|
|
|
|
|
/// Is this an **XMLHttpRequest**?
|
2016-10-22 20:41:36 +00:00
|
|
|
bool get xhr =>
|
2016-12-21 03:10:03 +00:00
|
|
|
io.headers.value("X-Requested-With")?.trim()?.toLowerCase() ==
|
2016-10-22 20:41:36 +00:00
|
|
|
'xmlhttprequest';
|
2016-04-18 03:27:23 +00:00
|
|
|
|
2016-10-22 20:41:36 +00:00
|
|
|
/// Magically transforms an [HttpRequest] into a [RequestContext].
|
2017-03-02 04:04:37 +00:00
|
|
|
static Future<RequestContext> from(HttpRequest request, Angel app) async {
|
2016-10-22 20:41:36 +00:00
|
|
|
RequestContext ctx = new RequestContext();
|
|
|
|
|
2017-03-02 22:06:02 +00:00
|
|
|
String override = request.method;
|
|
|
|
|
|
|
|
if (app.allowMethodOverrides == true)
|
|
|
|
override =
|
|
|
|
request.headers.value('x-http-method-override')?.toUpperCase() ??
|
|
|
|
request.method;
|
|
|
|
|
2016-10-22 20:41:36 +00:00
|
|
|
ctx.app = app;
|
|
|
|
ctx._contentType = request.headers.contentType;
|
2017-03-02 22:06:02 +00:00
|
|
|
ctx._override = override;
|
2017-08-03 16:40:21 +00:00
|
|
|
|
|
|
|
// Faster way to get path
|
|
|
|
List<int> _path = [];
|
|
|
|
|
|
|
|
// Go up until we reach a ?
|
|
|
|
for (int ch in request.uri.toString().codeUnits) {
|
|
|
|
if (ch != $question)
|
|
|
|
_path.add(ch);
|
2017-09-22 14:03:23 +00:00
|
|
|
else
|
|
|
|
break;
|
2017-08-03 16:40:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove trailing slashes
|
|
|
|
int lastSlash = -1;
|
|
|
|
|
|
|
|
for (int i = _path.length - 1; i >= 0; i--) {
|
|
|
|
if (_path[i] == $slash)
|
|
|
|
lastSlash = i;
|
2017-09-22 14:03:23 +00:00
|
|
|
else
|
|
|
|
break;
|
2017-08-03 16:40:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (lastSlash > -1)
|
|
|
|
ctx._path = new String.fromCharCodes(_path.take(lastSlash));
|
2017-09-22 14:03:23 +00:00
|
|
|
else
|
|
|
|
ctx._path = new String.fromCharCodes(_path);
|
2016-11-23 19:50:17 +00:00
|
|
|
ctx._io = request;
|
2016-10-22 20:41:36 +00:00
|
|
|
|
2017-08-03 16:40:21 +00:00
|
|
|
if (app.lazyParseBodies != true) {
|
2017-03-28 23:29:22 +00:00
|
|
|
ctx._body = (await parseBody(request,
|
2017-09-22 14:03:23 +00:00
|
|
|
storeOriginalBuffer: app.storeOriginalBuffer == true)) ??
|
2017-03-28 23:29:22 +00:00
|
|
|
{};
|
2017-08-03 16:40:21 +00:00
|
|
|
}
|
2016-10-22 20:41:36 +00:00
|
|
|
|
|
|
|
return ctx;
|
2016-04-18 03:27:23 +00:00
|
|
|
}
|
2016-11-23 19:50:17 +00:00
|
|
|
|
2017-09-24 19:43:14 +00:00
|
|
|
/// Grabs an object by key or type from [params], [_injections], or
|
2017-01-20 22:11:20 +00:00
|
|
|
/// [app].container. Use this to perform dependency injection
|
|
|
|
/// within a service hook.
|
2017-02-25 20:57:28 +00:00
|
|
|
T grab<T>(key) {
|
2017-01-20 22:11:20 +00:00
|
|
|
if (params.containsKey(key))
|
|
|
|
return params[key];
|
2017-09-24 19:43:14 +00:00
|
|
|
else if (_injections.containsKey(key))
|
|
|
|
return _injections[key];
|
2017-02-26 21:31:09 +00:00
|
|
|
else if (properties.containsKey(key))
|
|
|
|
return properties[key];
|
2017-07-09 16:50:46 +00:00
|
|
|
else {
|
|
|
|
var prop = app?.findProperty(key);
|
|
|
|
if (prop != null)
|
|
|
|
return prop;
|
|
|
|
else if (key is Type) {
|
|
|
|
try {
|
|
|
|
return app.container.make(key);
|
|
|
|
} catch (e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
} else
|
2017-01-20 22:11:20 +00:00
|
|
|
return null;
|
2017-07-09 16:50:46 +00:00
|
|
|
}
|
2017-01-20 22:11:20 +00:00
|
|
|
}
|
|
|
|
|
2017-09-24 19:43:14 +00:00
|
|
|
/// Shorthand to add to [_injections].
|
2016-12-23 20:53:39 +00:00
|
|
|
void inject(type, value) {
|
2017-10-28 08:50:16 +00:00
|
|
|
if (!app.isProduction && type is Type) {
|
|
|
|
if (!reflect(value).type.isAssignableTo(reflectType(type)))
|
|
|
|
throw new ArgumentError('Cannot inject $value (${value.runtimeType}) as an instance of $type.');
|
|
|
|
}
|
|
|
|
|
2017-09-24 19:43:14 +00:00
|
|
|
_injections[type] = value;
|
2016-11-23 19:50:17 +00:00
|
|
|
}
|
2017-03-28 23:29:22 +00:00
|
|
|
|
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`.
|
|
|
|
///
|
|
|
|
/// [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.
|
|
|
|
bool accepts(contentType) {
|
|
|
|
var contentTypeString = contentType is ContentType
|
|
|
|
? contentType.mimeType
|
|
|
|
: contentType?.toString();
|
|
|
|
|
|
|
|
if (contentTypeString == null)
|
|
|
|
throw new ArgumentError(
|
|
|
|
'RequestContext.accepts expects the `contentType` parameter to NOT be null.');
|
|
|
|
|
|
|
|
_acceptHeaderCache ??= headers.value(HttpHeaders.ACCEPT);
|
|
|
|
|
|
|
|
if (_acceptHeaderCache == null)
|
|
|
|
return false;
|
|
|
|
else if (_acceptHeaderCache.contains('*/*'))
|
|
|
|
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
|
|
|
/// Retrieves the request body if it has already been parsed, or lazy-parses it before returning the body.
|
|
|
|
Future<Map> lazyBody() => parse().then((b) => b.body);
|
|
|
|
|
|
|
|
/// Retrieves the request files if it has already been parsed, or lazy-parses it before returning the files.
|
|
|
|
Future<List<FileUploadInfo>> lazyFiles() => parse().then((b) => b.files);
|
|
|
|
|
2017-03-29 00:36:54 +00:00
|
|
|
/// Retrieves the original request buffer if it has already been parsed, or lazy-parses it before returning the files.
|
2017-03-28 23:29:22 +00:00
|
|
|
///
|
|
|
|
/// This will return an empty `List` if you have not enabled `storeOriginalBuffer` on your [app] instance.
|
|
|
|
Future<List<int>> lazyOriginalBuffer() =>
|
|
|
|
parse().then((b) => b.originalBuffer);
|
|
|
|
|
|
|
|
/// Retrieves the request body if it has already been parsed, or lazy-parses it before returning the query.
|
|
|
|
///
|
|
|
|
/// If [forceParse] is not `true`, then [uri].query will be returned, and no parsing will be performed.
|
|
|
|
Future<Map<String, dynamic>> lazyQuery({bool forceParse: false}) {
|
|
|
|
if (_body == null && forceParse != true)
|
2017-03-29 00:36:54 +00:00
|
|
|
return new Future.value(uri.queryParameters);
|
2017-03-28 23:29:22 +00:00
|
|
|
else
|
|
|
|
return parse().then((b) => b.query);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Manually parses the request body, if it has not already been parsed.
|
|
|
|
Future<BodyParseResult> parse() async {
|
|
|
|
if (_body != null)
|
|
|
|
return _body;
|
|
|
|
else
|
2017-08-03 16:40:21 +00:00
|
|
|
_provisionalQuery = null;
|
2017-09-22 14:03:23 +00:00
|
|
|
return _body = await parseBody(io,
|
|
|
|
storeOriginalBuffer: app.storeOriginalBuffer == true);
|
2017-03-28 23:29:22 +00:00
|
|
|
}
|
2017-10-28 08:50:16 +00:00
|
|
|
|
|
|
|
/// Disposes of all resources.
|
|
|
|
Future close() async {
|
|
|
|
_body = null;
|
|
|
|
_acceptsAllCache = null;
|
|
|
|
_acceptHeaderCache = null;
|
|
|
|
_io = null;
|
|
|
|
_override = _path = null;
|
|
|
|
_contentType = null;
|
|
|
|
_provisionalQuery?.clear();
|
|
|
|
properties.clear();
|
|
|
|
_injections.clear();
|
|
|
|
serviceParams.clear();
|
|
|
|
params.clear();
|
|
|
|
}
|
2016-10-22 20:41:36 +00:00
|
|
|
}
|