- Add VariableSpec class with magnitude field validation - Add Formula class supporting multiple input/output variables - Support d4rt_code as string or object with code field - Add comprehensive tests for parsing and serialization - Fix broken test import in pruebas_d4rt_test.dart Follows README.md format requirements exactly
387 lines
15 KiB
Dart
387 lines
15 KiB
Dart
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:http_parser/http_parser.dart';
|
|
import 'package:stream_channel/stream_channel.dart';
|
|
|
|
import 'hijack_exception.dart';
|
|
import 'message.dart';
|
|
import 'util.dart';
|
|
|
|
/// An HTTP request to be processed by a Shelf application.
|
|
class Request extends Message {
|
|
/// The URL path from the current handler to the requested resource, relative
|
|
/// to [handlerPath], plus any query parameters.
|
|
///
|
|
/// This should be used by handlers for determining which resource to serve,
|
|
/// in preference to [requestedUri]. This allows handlers to do the right
|
|
/// thing when they're mounted anywhere in the application. Routers should be
|
|
/// sure to update this when dispatching to a nested handler, using the
|
|
/// `path` parameter to [change].
|
|
///
|
|
/// [url]'s path is always relative. It may be empty, if [requestedUri] ends
|
|
/// at this handler. [url] will always have the same query parameters as
|
|
/// [requestedUri].
|
|
///
|
|
/// [handlerPath] and [url]'s path combine to create [requestedUri]'s path.
|
|
final Uri url;
|
|
|
|
/// The HTTP request method, such as "GET" or "POST".
|
|
final String method;
|
|
|
|
/// The URL path to the current handler.
|
|
///
|
|
/// This allows a handler to know its location within the URL-space of an
|
|
/// application. Routers should be sure to update this when dispatching to a
|
|
/// nested handler, using the `path` parameter to [change].
|
|
///
|
|
/// [handlerPath] is always a root-relative URL path; that is, it always
|
|
/// starts with `/`. It will also end with `/` whenever [url]'s path is
|
|
/// non-empty, or if [requestedUri]'s path ends with `/`.
|
|
///
|
|
/// [handlerPath] and [url]'s path combine to create [requestedUri]'s path.
|
|
final String handlerPath;
|
|
|
|
/// The HTTP protocol version used in the request, either "1.0" or "1.1".
|
|
final String protocolVersion;
|
|
|
|
/// The original [Uri] for the request.
|
|
final Uri requestedUri;
|
|
|
|
/// The callback wrapper for hijacking this request.
|
|
///
|
|
/// This will be `null` if this request can't be hijacked.
|
|
final _OnHijack? _onHijack;
|
|
|
|
/// Whether this request can be hijacked.
|
|
///
|
|
/// This will be `false` either if the adapter doesn't support hijacking, or
|
|
/// if the request has already been hijacked.
|
|
bool get canHijack => _onHijack != null && !_onHijack.called;
|
|
|
|
/// If this is non-`null` and the requested resource hasn't been modified
|
|
/// since this date and time, the server should return a 304 Not Modified
|
|
/// response.
|
|
///
|
|
/// This is parsed from the If-Modified-Since header in [headers]. If
|
|
/// [headers] doesn't have an If-Modified-Since header, this will be `null`.
|
|
///
|
|
/// Throws [FormatException], if incoming HTTP request has an invalid
|
|
/// If-Modified-Since header.
|
|
DateTime? get ifModifiedSince {
|
|
if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache;
|
|
if (!headers.containsKey('if-modified-since')) return null;
|
|
_ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']!);
|
|
return _ifModifiedSinceCache;
|
|
}
|
|
|
|
DateTime? _ifModifiedSinceCache;
|
|
|
|
/// Creates a new [Request].
|
|
///
|
|
/// [handlerPath] must be root-relative. [url]'s path must be fully relative,
|
|
/// and it must have the same query parameters as [requestedUri].
|
|
/// [handlerPath] and [url]'s path must combine to be the path component of
|
|
/// [requestedUri]. If they're not passed, [handlerPath] will default to `/`
|
|
/// and [url] to `requestedUri.path` without the initial `/`. If only one is
|
|
/// passed, the other will be inferred.
|
|
///
|
|
/// [body] is the request body. It may be either a [String], a [List<int>], a
|
|
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
|
|
/// [encoding] is used to encode it to a [Stream<List<int>>]. The default
|
|
/// encoding is UTF-8.
|
|
///
|
|
/// If [encoding] is passed, the "encoding" field of the Content-Type header
|
|
/// in [headers] will be set appropriately. If there is no existing
|
|
/// Content-Type header, it will be set to "application/octet-stream".
|
|
/// [headers] must contain values that are either `String` or `List<String>`.
|
|
/// An empty list will cause the header to be omitted.
|
|
///
|
|
/// The default value for [protocolVersion] is '1.1'.
|
|
///
|
|
/// ## `onHijack`
|
|
///
|
|
/// [onHijack] allows handlers to take control of the underlying socket for
|
|
/// the request. It should be passed by adapters that can provide access to
|
|
/// the bidirectional socket underlying the HTTP connection stream.
|
|
///
|
|
/// The [onHijack] callback will only be called once per request. It will be
|
|
/// passed another callback which takes a byte StreamChannel. [onHijack] must
|
|
/// pass the channel for the connection stream to this callback, although it
|
|
/// may do so asynchronously.
|
|
///
|
|
/// If a request is hijacked, the adapter should expect to receive a
|
|
/// [HijackException] from the handler. This is a special exception used to
|
|
/// indicate that hijacking has occurred. The adapter should avoid either
|
|
/// sending a response or notifying the user of an error if a
|
|
/// [HijackException] is caught.
|
|
///
|
|
/// An adapter can check whether a request was hijacked using [canHijack],
|
|
/// which will be `false` for a hijacked request. The adapter may throw an
|
|
/// error if a [HijackException] is received for a non-hijacked request, or if
|
|
/// no [HijackException] is received for a hijacked request.
|
|
///
|
|
/// See also [hijack].
|
|
// TODO(kevmoo) finish documenting the rest of the arguments.
|
|
Request(
|
|
String method,
|
|
Uri requestedUri, {
|
|
String? protocolVersion,
|
|
Map<String, /* String | List<String> */ Object>? headers,
|
|
String? handlerPath,
|
|
Uri? url,
|
|
Object? body,
|
|
Encoding? encoding,
|
|
Map<String, Object>? context,
|
|
void Function(void Function(StreamChannel<List<int>>))? onHijack,
|
|
}) : this._(method, requestedUri,
|
|
protocolVersion: protocolVersion,
|
|
headers: headers,
|
|
url: url,
|
|
handlerPath: handlerPath,
|
|
body: body,
|
|
encoding: encoding,
|
|
context: context,
|
|
onHijack: onHijack == null ? null : _OnHijack(onHijack));
|
|
|
|
/// This constructor has the same signature as [Request.new] except that
|
|
/// accepts [onHijack] as [_OnHijack].
|
|
///
|
|
/// Any [Request] created by calling [change] will pass [_onHijack] from the
|
|
/// source [Request] to ensure that [hijack] can only be called once, even
|
|
/// from a changed [Request].
|
|
Request._(
|
|
this.method,
|
|
this.requestedUri, {
|
|
String? protocolVersion,
|
|
Map<String, /* String | List<String> */ Object>? headers,
|
|
String? handlerPath,
|
|
Uri? url,
|
|
Object? body,
|
|
Encoding? encoding,
|
|
Map<String, Object>? context,
|
|
_OnHijack? onHijack,
|
|
}) : protocolVersion = protocolVersion ?? '1.1',
|
|
url = _computeUrl(requestedUri, handlerPath, url),
|
|
handlerPath = _computeHandlerPath(requestedUri, handlerPath, url),
|
|
_onHijack = onHijack,
|
|
super(body, encoding: encoding, headers: headers, context: context) {
|
|
if (method.isEmpty) {
|
|
throw ArgumentError.value(method, 'method', 'cannot be empty.');
|
|
}
|
|
|
|
try {
|
|
// Trigger URI parsing methods that may throw format exception (in Request
|
|
// constructor or in handlers / routing).
|
|
requestedUri.pathSegments;
|
|
requestedUri.queryParametersAll;
|
|
} on FormatException catch (e) {
|
|
throw ArgumentError.value(
|
|
requestedUri, 'requestedUri', 'URI parsing failed: $e');
|
|
}
|
|
|
|
if (!requestedUri.isAbsolute) {
|
|
throw ArgumentError.value(
|
|
requestedUri, 'requestedUri', 'must be an absolute URL.');
|
|
}
|
|
|
|
if (requestedUri.fragment.isNotEmpty) {
|
|
throw ArgumentError.value(
|
|
requestedUri, 'requestedUri', 'may not have a fragment.');
|
|
}
|
|
|
|
// Notice that because relative paths must encode colon (':') as %3A we
|
|
// cannot actually combine this.handlerPath and this.url.path, but we can
|
|
// compare the pathSegments. In practice exposing this.url.path as a Uri
|
|
// and not a String is probably the underlying flaw here.
|
|
final handlerPart = Uri(path: this.handlerPath).pathSegments.join('/');
|
|
final rest = this.url.pathSegments.join('/');
|
|
final join = this.url.path.startsWith('/') ? '/' : '';
|
|
final pathSegments = '$handlerPart$join$rest';
|
|
if (pathSegments != requestedUri.pathSegments.join('/')) {
|
|
throw ArgumentError.value(
|
|
requestedUri,
|
|
'requestedUri',
|
|
'handlerPath "${this.handlerPath}" and url "${this.url}" must '
|
|
'combine to equal requestedUri path "${requestedUri.path}".');
|
|
}
|
|
}
|
|
|
|
/// Creates a new [Request] by copying existing values and applying specified
|
|
/// changes.
|
|
///
|
|
/// New key-value pairs in [context] and [headers] will be added to the copied
|
|
/// [Request]. If [context] or [headers] includes a key that already exists,
|
|
/// the key-value pair will replace the corresponding entry in the copied
|
|
/// [Request]. If [context] or [headers] contains a `null` value the
|
|
/// corresponding `key` will be removed if it exists, otherwise the `null`
|
|
/// value will be ignored.
|
|
/// For [headers] a value which is an empty list will also cause the
|
|
/// corresponding key to be removed.
|
|
///
|
|
/// All other context and header values from the [Request] will be
|
|
/// included in the copied [Request] unchanged.
|
|
///
|
|
/// [body] is the request body. It may be either a [String], a [List<int>], a
|
|
/// [Stream<List<int>>], or `null` to indicate no body.
|
|
///
|
|
/// [path] is used to update both [handlerPath] and [url]. It's designed for
|
|
/// routing middleware, and represents the path from the current handler to
|
|
/// the next handler. It must be a prefix of [url]; [handlerPath] becomes
|
|
/// `handlerPath + "/" + path`, and [url] becomes relative to that. For
|
|
/// example:
|
|
///
|
|
/// print(request.handlerPath); // => /static/
|
|
/// print(request.url); // => dir/file.html
|
|
///
|
|
/// request = request.change(path: "dir");
|
|
/// print(request.handlerPath); // => /static/dir/
|
|
/// print(request.url); // => file.html
|
|
@override
|
|
Request change({
|
|
Map<String, /* String | List<String> */ Object?>? headers,
|
|
Map<String, Object?>? context,
|
|
String? path,
|
|
Object? body,
|
|
}) {
|
|
final headersAll = updateHeaders(this.headersAll, headers);
|
|
final newContext = updateMap<String, Object>(this.context, context);
|
|
|
|
body ??= extractBody(this);
|
|
|
|
var handlerPath = this.handlerPath;
|
|
if (path != null) handlerPath += path;
|
|
|
|
return Request._(method, requestedUri,
|
|
protocolVersion: protocolVersion,
|
|
headers: headersAll,
|
|
handlerPath: handlerPath,
|
|
body: body,
|
|
context: newContext,
|
|
onHijack: _onHijack);
|
|
}
|
|
|
|
/// Takes control of the underlying request socket.
|
|
///
|
|
/// Synchronously, this throws a [HijackException] that indicates to the
|
|
/// adapter that it shouldn't emit a response itself. Asynchronously,
|
|
/// [callback] is called with a [StreamChannel<List<int>>] that provides
|
|
/// access to the underlying request socket.
|
|
///
|
|
/// This may only be called when using a Shelf adapter that supports
|
|
/// hijacking, such as the `dart:io` adapter. In addition, a given request may
|
|
/// only be hijacked once. [canHijack] can be used to detect whether this
|
|
/// request can be hijacked.
|
|
Never hijack(void Function(StreamChannel<List<int>>) callback) {
|
|
if (_onHijack == null) {
|
|
throw StateError("This request can't be hijacked.");
|
|
}
|
|
|
|
_onHijack.run(callback);
|
|
|
|
throw const HijackException();
|
|
}
|
|
}
|
|
|
|
/// A callback for [Request.hijack] and tracking of whether it has been called.
|
|
class _OnHijack {
|
|
final void Function(void Function(StreamChannel<List<int>>)) _callback;
|
|
|
|
bool called = false;
|
|
|
|
_OnHijack(this._callback);
|
|
|
|
/// Calls `this`.
|
|
///
|
|
/// Throws a [StateError] if `this` has already been called.
|
|
void run(void Function(StreamChannel<List<int>>) callback) {
|
|
if (called) throw StateError('This request has already been hijacked.');
|
|
called = true;
|
|
Future.microtask(() => _callback(callback));
|
|
}
|
|
}
|
|
|
|
/// Computes `url` from the provided [Request] constructor arguments.
|
|
///
|
|
/// If [url] is `null`, the value is inferred from [requestedUri] and
|
|
/// [handlerPath] if available. Otherwise [url] is returned.
|
|
Uri _computeUrl(Uri requestedUri, String? handlerPath, Uri? url) {
|
|
if (handlerPath != null &&
|
|
handlerPath != requestedUri.path &&
|
|
!handlerPath.endsWith('/')) {
|
|
handlerPath += '/';
|
|
}
|
|
|
|
if (url != null) {
|
|
if (url.scheme.isNotEmpty || url.hasAuthority || url.fragment.isNotEmpty) {
|
|
throw ArgumentError('url "$url" may contain only a path and query '
|
|
'parameters.');
|
|
}
|
|
|
|
if (!requestedUri.path.endsWith(url.path)) {
|
|
throw ArgumentError('url "$url" must be a suffix of requestedUri '
|
|
'"$requestedUri".');
|
|
}
|
|
|
|
if (requestedUri.query != url.query) {
|
|
throw ArgumentError('url "$url" must have the same query parameters '
|
|
'as requestedUri "$requestedUri".');
|
|
}
|
|
|
|
if (url.path.startsWith('/')) {
|
|
throw ArgumentError('url "$url" must be relative.');
|
|
}
|
|
|
|
var startOfUrl = requestedUri.path.length - url.path.length;
|
|
if (url.path.isNotEmpty &&
|
|
requestedUri.path.substring(startOfUrl - 1, startOfUrl) != '/') {
|
|
throw ArgumentError('url "$url" must be on a path boundary in '
|
|
'requestedUri "$requestedUri".');
|
|
}
|
|
|
|
return url;
|
|
} else if (handlerPath != null) {
|
|
return Uri(
|
|
path: requestedUri.path.substring(handlerPath.length),
|
|
query: requestedUri.query);
|
|
} else {
|
|
// Skip the initial "/".
|
|
var path = requestedUri.path.substring(1);
|
|
return Uri(path: path, query: requestedUri.query);
|
|
}
|
|
}
|
|
|
|
/// Computes `handlerPath` from the provided [Request] constructor arguments.
|
|
///
|
|
/// If [handlerPath] is `null`, the value is inferred from [requestedUri] and
|
|
/// [url] if available. Otherwise [handlerPath] is returned.
|
|
String _computeHandlerPath(Uri requestedUri, String? handlerPath, Uri? url) {
|
|
if (handlerPath != null &&
|
|
handlerPath != requestedUri.path &&
|
|
!handlerPath.endsWith('/')) {
|
|
handlerPath += '/';
|
|
}
|
|
|
|
if (handlerPath != null) {
|
|
if (!requestedUri.path.startsWith(handlerPath)) {
|
|
throw ArgumentError('handlerPath "$handlerPath" must be a prefix of '
|
|
'requestedUri path "${requestedUri.path}"');
|
|
}
|
|
|
|
if (!handlerPath.startsWith('/')) {
|
|
throw ArgumentError('handlerPath "$handlerPath" must be root-relative.');
|
|
}
|
|
|
|
return handlerPath;
|
|
} else if (url != null) {
|
|
if (url.path.isEmpty) return requestedUri.path;
|
|
|
|
var index = requestedUri.path.indexOf(url.path);
|
|
return requestedUri.path.substring(0, index);
|
|
} else {
|
|
return '/';
|
|
}
|
|
}
|