| Index: mojo/public/dart/third_party/shelf/lib/src/request.dart
|
| diff --git a/mojo/public/dart/third_party/shelf/lib/src/request.dart b/mojo/public/dart/third_party/shelf/lib/src/request.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..5c5f44cf3a3aa0f483cf3ea648ecbe139750d8fd
|
| --- /dev/null
|
| +++ b/mojo/public/dart/third_party/shelf/lib/src/request.dart
|
| @@ -0,0 +1,355 @@
|
| +// 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.
|
| +
|
| +library shelf.request;
|
| +
|
| +import 'dart:async';
|
| +import 'dart:convert';
|
| +
|
| +import 'package:http_parser/http_parser.dart';
|
| +
|
| +import 'hijack_exception.dart';
|
| +import 'message.dart';
|
| +import 'util.dart';
|
| +
|
| +/// A callback provided by a Shelf handler that's passed to [Request.hijack].
|
| +typedef void HijackCallback(
|
| + Stream<List<int>> stream, StreamSink<List<int>> sink);
|
| +
|
| +/// A callback provided by a Shelf adapter that's used by [Request.hijack] to
|
| +/// provide a [HijackCallback] with a socket.
|
| +typedef void OnHijackCallback(HijackCallback callback);
|
| +
|
| +/// Represents 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 [requestUri]'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`.
|
| + 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
|
| + /// [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".
|
| + ///
|
| + /// 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 stream and a byte sink.
|
| + /// [onHijack] must pass the stream and sink for the connection stream to this
|
| + /// callback, although it may do so asynchronously. Both parameters may be the
|
| + /// same object. If the user closes the sink, the adapter should ensure that
|
| + /// the stream is closed as well.
|
| + ///
|
| + /// 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> headers, String handlerPath, Uri url, body,
|
| + Encoding encoding, Map<String, Object> context,
|
| + OnHijackCallback onHijack})
|
| + : this._(method, requestedUri,
|
| + protocolVersion: protocolVersion,
|
| + headers: headers,
|
| + url: url,
|
| + handlerPath: handlerPath,
|
| + body: body,
|
| + encoding: encoding,
|
| + context: context,
|
| + onHijack: onHijack == null ? null : new _OnHijack(onHijack));
|
| +
|
| + /// This constructor has the same signature as [new Request] 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, Uri requestedUri, {String protocolVersion,
|
| + Map<String, String> headers, String handlerPath, Uri url, body,
|
| + Encoding encoding, Map<String, Object> context, _OnHijack onHijack})
|
| + : this.requestedUri = requestedUri,
|
| + this.protocolVersion = protocolVersion == null
|
| + ? '1.1'
|
| + : protocolVersion,
|
| + this.url = _computeUrl(requestedUri, handlerPath, url),
|
| + this.handlerPath = _computeHandlerPath(requestedUri, handlerPath, url),
|
| + this._onHijack = onHijack,
|
| + super(body, encoding: encoding, headers: headers, context: context) {
|
| + if (method.isEmpty) throw new ArgumentError('method cannot be empty.');
|
| +
|
| + if (!requestedUri.isAbsolute) {
|
| + throw new ArgumentError(
|
| + 'requestedUri "$requestedUri" must be an absolute URL.');
|
| + }
|
| +
|
| + if (requestedUri.fragment.isNotEmpty) {
|
| + throw new ArgumentError(
|
| + 'requestedUri "$requestedUri" may not have a fragment.');
|
| + }
|
| +
|
| + if (this.handlerPath + this.url.path != this.requestedUri.path) {
|
| + throw new ArgumentError('handlerPath "$handlerPath" and url "$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]. 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] or a
|
| + /// [Stream<List<int>>].
|
| + ///
|
| + /// [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
|
| + Request change({Map<String, String> headers, Map<String, Object> context,
|
| + String path, body}) {
|
| + headers = updateMap(this.headers, headers);
|
| + context = updateMap(this.context, context);
|
| +
|
| + if (body == null) body = getBody(this);
|
| +
|
| + var handlerPath = this.handlerPath;
|
| + if (path != null) handlerPath += path;
|
| +
|
| + return new Request._(this.method, this.requestedUri,
|
| + protocolVersion: this.protocolVersion,
|
| + headers: headers,
|
| + handlerPath: handlerPath,
|
| + body: body,
|
| + context: context,
|
| + 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 [Stream<List<int>>] and
|
| + /// [StreamSink<List<int>>], respectively, that provide access to the
|
| + /// underlying request socket.
|
| + ///
|
| + /// If the sink is closed, the stream will be closed as well. The stream and
|
| + /// sink may be the same object, as in the case of a `dart:io` `Socket`
|
| + /// object.
|
| + ///
|
| + /// 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.
|
| + void hijack(HijackCallback callback) {
|
| + if (_onHijack == null) {
|
| + throw new StateError("This request can't be hijacked.");
|
| + }
|
| +
|
| + _onHijack.run(callback);
|
| + throw const HijackException();
|
| + }
|
| +}
|
| +
|
| +/// A class containing a callback for [Request.hijack] that also tracks whether
|
| +/// the callback has been called.
|
| +class _OnHijack {
|
| + /// The callback.
|
| + final OnHijackCallback _callback;
|
| +
|
| + /// Whether [this] has been called.
|
| + bool called = false;
|
| +
|
| + _OnHijack(this._callback);
|
| +
|
| + /// Calls [this].
|
| + ///
|
| + /// Throws a [StateError] if [this] has already been called.
|
| + void run(HijackCallback callback) {
|
| + if (called) throw new StateError("This request has already been hijacked.");
|
| + called = true;
|
| + newFuture(() => _callback(callback));
|
| + }
|
| +}
|
| +
|
| +/// Computes `url` from the provided [Request] constructor arguments.
|
| +///
|
| +/// If [url] is `null`, the value is inferred from [requestedUrl] 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 new ArgumentError('url "$url" may contain only a path and query '
|
| + 'parameters.');
|
| + }
|
| +
|
| + if (!requestedUri.path.endsWith(url.path)) {
|
| + throw new ArgumentError('url "$url" must be a suffix of requestedUri '
|
| + '"$requestedUri".');
|
| + }
|
| +
|
| + if (requestedUri.query != url.query) {
|
| + throw new ArgumentError('url "$url" must have the same query parameters '
|
| + 'as requestedUri "$requestedUri".');
|
| + }
|
| +
|
| + if (url.path.startsWith('/')) {
|
| + throw new 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 new ArgumentError('url "$url" must be on a path boundary in '
|
| + 'requestedUri "$requestedUri".');
|
| + }
|
| +
|
| + return url;
|
| + } else if (handlerPath != null) {
|
| + return new Uri(
|
| + path: requestedUri.path.substring(handlerPath.length),
|
| + query: requestedUri.query);
|
| + } else {
|
| + // Skip the initial "/".
|
| + var path = requestedUri.path.substring(1);
|
| + return new Uri(path: path, query: requestedUri.query);
|
| + }
|
| +}
|
| +
|
| +/// Computes `handlerPath` from the provided [Request] constructor arguments.
|
| +///
|
| +/// If [handlerPath] is `null`, the value is inferred from [requestedUrl] 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 new ArgumentError('handlerPath "$handlerPath" must be a prefix of '
|
| + 'requestedUri path "${requestedUri.path}"');
|
| + }
|
| +
|
| + if (!handlerPath.startsWith('/')) {
|
| + throw new 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 '/';
|
| + }
|
| +}
|
|
|