| // 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 remainder of the [requestedUri] path and query designating the virtual |
| /// "location" of the request's target within the handler. |
| /// |
| /// [url] may be an empty, if [requestedUri]targets the handler |
| /// root and does not have a trailing slash. |
| /// |
| /// [url] is never null. If it is not empty, it will start with `/`. |
| /// |
| /// [scriptName] and [url] combine to create a valid path that should |
| /// correspond to the [requestedUri] path. |
| final Uri url; |
| |
| /// The HTTP request method, such as "GET" or "POST". |
| final String method; |
| |
| /// The initial portion of the [requestedUri] path that corresponds to the |
| /// handler. |
| /// |
| /// [scriptName] allows a handler to know its virtual "location". |
| /// |
| /// If the handler corresponds to the "root" of a server, it will be an |
| /// empty string, otherwise it will start with a `/` |
| /// |
| /// [scriptName] and [url] combine to create a valid path that should |
| /// correspond to the [requestedUri] path. |
| final String scriptName; |
| |
| /// 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]. |
| /// |
| /// If [url] and [scriptName] are omitted, they are inferred from |
| /// [requestedUri]. |
| /// |
| /// Setting one of [url] or [scriptName] and not the other will throw an |
| /// [ArgumentError]. |
| /// |
| /// [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, Uri url, String scriptName, body, |
| Encoding encoding, Map<String, Object> context, |
| OnHijackCallback onHijack}) |
| : this._(method, requestedUri, |
| protocolVersion: protocolVersion, |
| headers: headers, |
| url: url, |
| scriptName: scriptName, |
| 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, Uri url, String scriptName, body, |
| Encoding encoding, Map<String, Object> context, _OnHijack onHijack}) |
| : this.requestedUri = requestedUri, |
| this.protocolVersion = protocolVersion == null |
| ? '1.1' |
| : protocolVersion, |
| this.url = _computeUrl(requestedUri, url, scriptName), |
| this.scriptName = _computeScriptName(requestedUri, url, scriptName), |
| 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('requstedUri must be an absolute URI.'); |
| } |
| |
| // TODO(kevmoo) if defined, check that scriptName is a fully-encoded, valid |
| // path component |
| if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { |
| throw new ArgumentError('scriptName must be empty or start with "/".'); |
| } |
| |
| if (this.scriptName == '/') { |
| throw new ArgumentError( |
| 'scriptName can never be "/". It should be empty instead.'); |
| } |
| |
| if (this.scriptName.endsWith('/')) { |
| throw new ArgumentError('scriptName must not end with "/".'); |
| } |
| |
| if (this.url.path.isNotEmpty && !this.url.path.startsWith('/')) { |
| throw new ArgumentError('url must be empty or start with "/".'); |
| } |
| |
| if (this.scriptName.isEmpty && this.url.path.isEmpty) { |
| throw new ArgumentError('scriptName and url cannot both be empty.'); |
| } |
| } |
| |
| /// 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. |
| /// |
| /// If [scriptName] is provided and [url] is not, [scriptName] must be a |
| /// prefix of [this.url]. [url] will default to [this.url] with this prefix |
| /// removed. Useful for routing middleware that sends requests to an inner |
| /// [Handler]. |
| Request change({Map<String, String> headers, Map<String, Object> context, |
| String scriptName, Uri url}) { |
| headers = updateMap(this.headers, headers); |
| context = updateMap(this.context, context); |
| |
| if (scriptName != null && url == null) { |
| var path = this.url.path; |
| if (path.startsWith(scriptName)) { |
| path = path.substring(scriptName.length); |
| url = new Uri(path: path, query: this.url.query); |
| } else { |
| throw new ArgumentError('If scriptName is provided without url, it must' |
| ' be a prefix of the existing url path.'); |
| } |
| } |
| |
| if (url == null) url = this.url; |
| if (scriptName == null) scriptName = this.scriptName; |
| |
| return new Request._(this.method, this.requestedUri, |
| protocolVersion: this.protocolVersion, |
| headers: headers, |
| url: url, |
| scriptName: scriptName, |
| body: this.read(), |
| 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] and [scriptName] are `null`, infer value from [requestedUrl], |
| /// otherwise return [url]. |
| /// |
| /// If [url] is provided, but [scriptName] is omitted, throws an |
| /// [ArgumentError]. |
| Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { |
| if (url == null && scriptName == null) { |
| return new Uri(path: requestedUri.path, query: requestedUri.query); |
| } |
| |
| if (url != null && scriptName != null) { |
| if (url.scheme.isNotEmpty) throw new ArgumentError('url must be relative.'); |
| return url; |
| } |
| |
| throw new ArgumentError( |
| 'url and scriptName must both be null or both be set.'); |
| } |
| |
| /// Computes `scriptName` from the provided [Request] constructor arguments. |
| /// |
| /// If [url] and [scriptName] are `null` it returns an empty string, otherwise |
| /// [scriptName] is returned. |
| /// |
| /// If [script] is provided, but [url] is omitted, throws an |
| /// [ArgumentError]. |
| String _computeScriptName(Uri requstedUri, Uri url, String scriptName) { |
| if (url == null && scriptName == null) { |
| return ''; |
| } |
| |
| if (url != null && scriptName != null) { |
| return scriptName; |
| } |
| |
| throw new ArgumentError( |
| 'url and scriptName must both be null or both be set.'); |
| } |