| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library shelf.request; | 5 library shelf.request; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 | 8 |
| 9 import 'package:http_parser/http_parser.dart'; | 9 import 'package:http_parser/http_parser.dart'; |
| 10 | 10 |
| 11 import 'hijack_exception.dart'; |
| 11 import 'message.dart'; | 12 import 'message.dart'; |
| 12 import 'util.dart'; | 13 import 'util.dart'; |
| 13 | 14 |
| 15 /// A callback provided by a Shelf handler that's passed to [Request.hijack]. |
| 16 typedef void HijackCallback( |
| 17 Stream<List<int>> stream, StreamSink<List<int>> sink); |
| 18 |
| 19 /// A callback provided by a Shelf adapter that's used by [Request.hijack] to |
| 20 /// provide a [HijackCallback] with a socket. |
| 21 typedef void OnHijackCallback(HijackCallback callback); |
| 22 |
| 14 /// Represents an HTTP request to be processed by a Shelf application. | 23 /// Represents an HTTP request to be processed by a Shelf application. |
| 15 class Request extends Message { | 24 class Request extends Message { |
| 16 /// The remainder of the [requestedUri] path and query designating the virtual | 25 /// The remainder of the [requestedUri] path and query designating the virtual |
| 17 /// "location" of the request's target within the handler. | 26 /// "location" of the request's target within the handler. |
| 18 /// | 27 /// |
| 19 /// [url] may be an empty, if [requestedUri]targets the handler | 28 /// [url] may be an empty, if [requestedUri]targets the handler |
| 20 /// root and does not have a trailing slash. | 29 /// root and does not have a trailing slash. |
| 21 /// | 30 /// |
| 22 /// [url] is never null. If it is not empty, it will start with `/`. | 31 /// [url] is never null. If it is not empty, it will start with `/`. |
| 23 /// | 32 /// |
| (...skipping 15 matching lines...) Expand all Loading... |
| 39 /// [scriptName] and [url] combine to create a valid path that should | 48 /// [scriptName] and [url] combine to create a valid path that should |
| 40 /// correspond to the [requestedUri] path. | 49 /// correspond to the [requestedUri] path. |
| 41 final String scriptName; | 50 final String scriptName; |
| 42 | 51 |
| 43 /// The HTTP protocol version used in the request, either "1.0" or "1.1". | 52 /// The HTTP protocol version used in the request, either "1.0" or "1.1". |
| 44 final String protocolVersion; | 53 final String protocolVersion; |
| 45 | 54 |
| 46 /// The original [Uri] for the request. | 55 /// The original [Uri] for the request. |
| 47 final Uri requestedUri; | 56 final Uri requestedUri; |
| 48 | 57 |
| 58 /// The callback wrapper for hijacking this request. |
| 59 /// |
| 60 /// This will be `null` if this request can't be hijacked. |
| 61 final _OnHijack _onHijack; |
| 62 |
| 63 /// Whether this request can be hijacked. |
| 64 /// |
| 65 /// This will be `false` either if the adapter doesn't support hijacking, or |
| 66 /// if the request has already been hijacked. |
| 67 bool get canHijack => _onHijack != null && !_onHijack.called; |
| 68 |
| 49 /// If this is non-`null` and the requested resource hasn't been modified | 69 /// If this is non-`null` and the requested resource hasn't been modified |
| 50 /// since this date and time, the server should return a 304 Not Modified | 70 /// since this date and time, the server should return a 304 Not Modified |
| 51 /// response. | 71 /// response. |
| 52 /// | 72 /// |
| 53 /// This is parsed from the If-Modified-Since header in [headers]. If | 73 /// This is parsed from the If-Modified-Since header in [headers]. If |
| 54 /// [headers] doesn't have an If-Modified-Since header, this will be `null`. | 74 /// [headers] doesn't have an If-Modified-Since header, this will be `null`. |
| 55 DateTime get ifModifiedSince { | 75 DateTime get ifModifiedSince { |
| 56 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; | 76 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; |
| 57 if (!headers.containsKey('if-modified-since')) return null; | 77 if (!headers.containsKey('if-modified-since')) return null; |
| 58 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); | 78 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); |
| 59 return _ifModifiedSinceCache; | 79 return _ifModifiedSinceCache; |
| 60 } | 80 } |
| 61 DateTime _ifModifiedSinceCache; | 81 DateTime _ifModifiedSinceCache; |
| 62 | 82 |
| 63 /// Creates a new [Request]. | 83 /// Creates a new [Request]. |
| 64 /// | 84 /// |
| 65 /// If [url] and [scriptName] are omitted, they are inferred from | 85 /// If [url] and [scriptName] are omitted, they are inferred from |
| 66 /// [requestedUri]. | 86 /// [requestedUri]. |
| 67 /// | 87 /// |
| 68 /// Setting one of [url] or [scriptName] and not the other will throw an | 88 /// Setting one of [url] or [scriptName] and not the other will throw an |
| 69 /// [ArgumentError]. | 89 /// [ArgumentError]. |
| 70 /// | 90 /// |
| 71 /// The default value for [protocolVersion] is '1.1'. | 91 /// The default value for [protocolVersion] is '1.1'. |
| 92 /// |
| 93 /// ## `onHijack` |
| 94 /// |
| 95 /// [onHijack] allows handlers to take control of the underlying socket for |
| 96 /// the request. It should be passed by adapters that can provide access to |
| 97 /// the bidirectional socket underlying the HTTP connection stream. |
| 98 /// |
| 99 /// The [onHijack] callback will only be called once per request. It will be |
| 100 /// passed another callback which takes a byte stream and a byte sink. |
| 101 /// [onHijack] must pass the stream and sink for the connection stream to this |
| 102 /// callback, although it may do so asynchronously. Both parameters may be the |
| 103 /// same object. If the user closes the sink, the adapter should ensure that |
| 104 /// the stream is closed as well. |
| 105 /// |
| 106 /// If a request is hijacked, the adapter should expect to receive a |
| 107 /// [HijackException] from the handler. This is a special exception used to |
| 108 /// indicate that hijacking has occurred. The adapter should avoid either |
| 109 /// sending a response or notifying the user of an error if a |
| 110 /// [HijackException] is caught. |
| 111 /// |
| 112 /// An adapter can check whether a request was hijacked using [canHijack], |
| 113 /// which will be `false` for a hijacked request. The adapter may throw an |
| 114 /// error if a [HijackException] is received for a non-hijacked request, or if |
| 115 /// no [HijackException] is received for a hijacked request. |
| 116 /// |
| 117 /// See also [hijack]. |
| 72 // TODO(kevmoo) finish documenting the rest of the arguments. | 118 // TODO(kevmoo) finish documenting the rest of the arguments. |
| 73 Request(this.method, Uri requestedUri, {String protocolVersion, | 119 Request(this.method, Uri requestedUri, {String protocolVersion, |
| 74 Map<String, String> headers, Uri url, String scriptName, | 120 Map<String, String> headers, Uri url, String scriptName, |
| 75 Stream<List<int>> body, Map<String, Object> context}) | 121 Stream<List<int>> body, Map<String, Object> context, |
| 122 OnHijackCallback onHijack}) |
| 76 : this.requestedUri = requestedUri, | 123 : this.requestedUri = requestedUri, |
| 77 this.protocolVersion = protocolVersion == null ? | 124 this.protocolVersion = protocolVersion == null ? |
| 78 '1.1' : protocolVersion, | 125 '1.1' : protocolVersion, |
| 79 this.url = _computeUrl(requestedUri, url, scriptName), | 126 this.url = _computeUrl(requestedUri, url, scriptName), |
| 80 this.scriptName = _computeScriptName(requestedUri, url, scriptName), | 127 this.scriptName = _computeScriptName(requestedUri, url, scriptName), |
| 128 this._onHijack = onHijack == null ? null : new _OnHijack(onHijack), |
| 81 super(body == null ? new Stream.fromIterable([]) : body, | 129 super(body == null ? new Stream.fromIterable([]) : body, |
| 82 headers: headers, context: context) { | 130 headers: headers, context: context) { |
| 83 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); | 131 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); |
| 84 | 132 |
| 85 if (!requestedUri.isAbsolute) { | 133 if (!requestedUri.isAbsolute) { |
| 86 throw new ArgumentError('requstedUri must be an absolute URI.'); | 134 throw new ArgumentError('requstedUri must be an absolute URI.'); |
| 87 } | 135 } |
| 88 | 136 |
| 89 if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { | 137 if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { |
| 90 throw new ArgumentError('scriptName must be empty or start with "/".'); | 138 throw new ArgumentError('scriptName must be empty or start with "/".'); |
| (...skipping 30 matching lines...) Expand all Loading... |
| 121 /// All other context and header values from the [Request] will be included | 169 /// All other context and header values from the [Request] will be included |
| 122 /// in the copied [Request] unchanged. | 170 /// in the copied [Request] unchanged. |
| 123 Request change({Map<String, String> headers, Map<String, Object> context}) { | 171 Request change({Map<String, String> headers, Map<String, Object> context}) { |
| 124 headers = updateMap(this.headers, headers); | 172 headers = updateMap(this.headers, headers); |
| 125 context = updateMap(this.context, context); | 173 context = updateMap(this.context, context); |
| 126 | 174 |
| 127 return new Request(this.method, this.requestedUri, | 175 return new Request(this.method, this.requestedUri, |
| 128 protocolVersion: this.protocolVersion, headers: headers, url: this.url, | 176 protocolVersion: this.protocolVersion, headers: headers, url: this.url, |
| 129 scriptName: this.scriptName, body: this.read(), context: context); | 177 scriptName: this.scriptName, body: this.read(), context: context); |
| 130 } | 178 } |
| 179 |
| 180 /// Takes control of the underlying request socket. |
| 181 /// |
| 182 /// Synchronously, this throws a [HijackException] that indicates to the |
| 183 /// adapter that it shouldn't emit a response itself. Asynchronously, |
| 184 /// [callback] is called with a [Stream<List<int>>] and |
| 185 /// [StreamSink<List<int>>], respectively, that provide access to the |
| 186 /// underlying request socket. |
| 187 /// |
| 188 /// If the sink is closed, the stream will be closed as well. The stream and |
| 189 /// sink may be the same object, as in the case of a `dart:io` `Socket` |
| 190 /// object. |
| 191 /// |
| 192 /// This may only be called when using a Shelf adapter that supports |
| 193 /// hijacking, such as the `dart:io` adapter. In addition, a given request may |
| 194 /// only be hijacked once. [canHijack] can be used to detect whether this |
| 195 /// request can be hijacked. |
| 196 void hijack(HijackCallback callback) { |
| 197 if (_onHijack == null) { |
| 198 throw new StateError("This request can't be hijacked."); |
| 199 } |
| 200 |
| 201 _onHijack.run(callback); |
| 202 throw const HijackException(); |
| 203 } |
| 204 } |
| 205 |
| 206 /// A class containing a callback for [Request.hijack] that also tracks whether |
| 207 /// the callback has been called. |
| 208 class _OnHijack { |
| 209 /// The callback. |
| 210 final OnHijackCallback _callback; |
| 211 |
| 212 /// Whether [this] has been called. |
| 213 bool called = false; |
| 214 |
| 215 _OnHijack(this._callback); |
| 216 |
| 217 /// Calls [this]. |
| 218 /// |
| 219 /// Throws a [StateError] if [this] has already been called. |
| 220 void run(HijackCallback callback) { |
| 221 if (called) throw new StateError("This request has already been hijacked."); |
| 222 called = true; |
| 223 newFuture(() => _callback(callback)); |
| 224 } |
| 131 } | 225 } |
| 132 | 226 |
| 133 /// Computes `url` from the provided [Request] constructor arguments. | 227 /// Computes `url` from the provided [Request] constructor arguments. |
| 134 /// | 228 /// |
| 135 /// If [url] and [scriptName] are `null`, infer value from [requestedUrl], | 229 /// If [url] and [scriptName] are `null`, infer value from [requestedUrl], |
| 136 /// otherwise return [url]. | 230 /// otherwise return [url]. |
| 137 /// | 231 /// |
| 138 /// If [url] is provided, but [scriptName] is omitted, throws an | 232 /// If [url] is provided, but [scriptName] is omitted, throws an |
| 139 /// [ArgumentError]. | 233 /// [ArgumentError]. |
| 140 Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { | 234 Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { |
| (...skipping 23 matching lines...) Expand all Loading... |
| 164 return ''; | 258 return ''; |
| 165 } | 259 } |
| 166 | 260 |
| 167 if (url != null && scriptName != null) { | 261 if (url != null && scriptName != null) { |
| 168 return scriptName; | 262 return scriptName; |
| 169 } | 263 } |
| 170 | 264 |
| 171 throw new ArgumentError( | 265 throw new ArgumentError( |
| 172 'url and scriptName must both be null or both be set.'); | 266 'url and scriptName must both be null or both be set.'); |
| 173 } | 267 } |
| OLD | NEW |