OLD | NEW |
(Empty) | |
| 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 |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library shelf.request; |
| 6 |
| 7 import 'dart:async'; |
| 8 import 'dart:convert'; |
| 9 |
| 10 import 'package:http_parser/http_parser.dart'; |
| 11 |
| 12 import 'hijack_exception.dart'; |
| 13 import 'message.dart'; |
| 14 import 'util.dart'; |
| 15 |
| 16 /// A callback provided by a Shelf handler that's passed to [Request.hijack]. |
| 17 typedef void HijackCallback( |
| 18 Stream<List<int>> stream, StreamSink<List<int>> sink); |
| 19 |
| 20 /// A callback provided by a Shelf adapter that's used by [Request.hijack] to |
| 21 /// provide a [HijackCallback] with a socket. |
| 22 typedef void OnHijackCallback(HijackCallback callback); |
| 23 |
| 24 /// Represents an HTTP request to be processed by a Shelf application. |
| 25 class Request extends Message { |
| 26 /// The URL path from the current handler to the requested resource, relative |
| 27 /// to [handlerPath], plus any query parameters. |
| 28 /// |
| 29 /// This should be used by handlers for determining which resource to serve, |
| 30 /// in preference to [requestedUri]. This allows handlers to do the right |
| 31 /// thing when they're mounted anywhere in the application. Routers should be |
| 32 /// sure to update this when dispatching to a nested handler, using the |
| 33 /// `path` parameter to [change]. |
| 34 /// |
| 35 /// [url]'s path is always relative. It may be empty, if [requestedUri] ends |
| 36 /// at this handler. [url] will always have the same query parameters as |
| 37 /// [requestedUri]. |
| 38 /// |
| 39 /// [handlerPath] and [url]'s path combine to create [requestedUri]'s path. |
| 40 final Uri url; |
| 41 |
| 42 /// The HTTP request method, such as "GET" or "POST". |
| 43 final String method; |
| 44 |
| 45 /// The URL path to the current handler. |
| 46 /// |
| 47 /// This allows a handler to know its location within the URL-space of an |
| 48 /// application. Routers should be sure to update this when dispatching to a |
| 49 /// nested handler, using the `path` parameter to [change]. |
| 50 /// |
| 51 /// [handlerPath] is always a root-relative URL path; that is, it always |
| 52 /// starts with `/`. It will also end with `/` whenever [url]'s path is |
| 53 /// non-empty, or if [requestUri]'s path ends with `/`. |
| 54 /// |
| 55 /// [handlerPath] and [url]'s path combine to create [requestedUri]'s path. |
| 56 final String handlerPath; |
| 57 |
| 58 /// The HTTP protocol version used in the request, either "1.0" or "1.1". |
| 59 final String protocolVersion; |
| 60 |
| 61 /// The original [Uri] for the request. |
| 62 final Uri requestedUri; |
| 63 |
| 64 /// The callback wrapper for hijacking this request. |
| 65 /// |
| 66 /// This will be `null` if this request can't be hijacked. |
| 67 final _OnHijack _onHijack; |
| 68 |
| 69 /// Whether this request can be hijacked. |
| 70 /// |
| 71 /// This will be `false` either if the adapter doesn't support hijacking, or |
| 72 /// if the request has already been hijacked. |
| 73 bool get canHijack => _onHijack != null && !_onHijack.called; |
| 74 |
| 75 /// If this is non-`null` and the requested resource hasn't been modified |
| 76 /// since this date and time, the server should return a 304 Not Modified |
| 77 /// response. |
| 78 /// |
| 79 /// This is parsed from the If-Modified-Since header in [headers]. If |
| 80 /// [headers] doesn't have an If-Modified-Since header, this will be `null`. |
| 81 DateTime get ifModifiedSince { |
| 82 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; |
| 83 if (!headers.containsKey('if-modified-since')) return null; |
| 84 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); |
| 85 return _ifModifiedSinceCache; |
| 86 } |
| 87 DateTime _ifModifiedSinceCache; |
| 88 |
| 89 /// Creates a new [Request]. |
| 90 /// |
| 91 /// [handlerPath] must be root-relative. [url]'s path must be fully relative, |
| 92 /// and it must have the same query parameters as [requestedUri]. |
| 93 /// [handlerPath] and [url]'s path must combine to be the path component of |
| 94 /// [requestedUri]. If they're not passed, [handlerPath] will default to `/` |
| 95 /// and [url] to `requestedUri.path` without the initial `/`. If only one is |
| 96 /// passed, the other will be inferred. |
| 97 /// |
| 98 /// [body] is the request body. It may be either a [String], a |
| 99 /// [Stream<List<int>>], or `null` to indicate no body. |
| 100 /// If it's a [String], [encoding] is used to encode it to a |
| 101 /// [Stream<List<int>>]. The default encoding is UTF-8. |
| 102 /// |
| 103 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
| 104 /// in [headers] will be set appropriately. If there is no existing |
| 105 /// Content-Type header, it will be set to "application/octet-stream". |
| 106 /// |
| 107 /// The default value for [protocolVersion] is '1.1'. |
| 108 /// |
| 109 /// ## `onHijack` |
| 110 /// |
| 111 /// [onHijack] allows handlers to take control of the underlying socket for |
| 112 /// the request. It should be passed by adapters that can provide access to |
| 113 /// the bidirectional socket underlying the HTTP connection stream. |
| 114 /// |
| 115 /// The [onHijack] callback will only be called once per request. It will be |
| 116 /// passed another callback which takes a byte stream and a byte sink. |
| 117 /// [onHijack] must pass the stream and sink for the connection stream to this |
| 118 /// callback, although it may do so asynchronously. Both parameters may be the |
| 119 /// same object. If the user closes the sink, the adapter should ensure that |
| 120 /// the stream is closed as well. |
| 121 /// |
| 122 /// If a request is hijacked, the adapter should expect to receive a |
| 123 /// [HijackException] from the handler. This is a special exception used to |
| 124 /// indicate that hijacking has occurred. The adapter should avoid either |
| 125 /// sending a response or notifying the user of an error if a |
| 126 /// [HijackException] is caught. |
| 127 /// |
| 128 /// An adapter can check whether a request was hijacked using [canHijack], |
| 129 /// which will be `false` for a hijacked request. The adapter may throw an |
| 130 /// error if a [HijackException] is received for a non-hijacked request, or if |
| 131 /// no [HijackException] is received for a hijacked request. |
| 132 /// |
| 133 /// See also [hijack]. |
| 134 // TODO(kevmoo) finish documenting the rest of the arguments. |
| 135 Request(String method, Uri requestedUri, {String protocolVersion, |
| 136 Map<String, String> headers, String handlerPath, Uri url, body, |
| 137 Encoding encoding, Map<String, Object> context, |
| 138 OnHijackCallback onHijack}) |
| 139 : this._(method, requestedUri, |
| 140 protocolVersion: protocolVersion, |
| 141 headers: headers, |
| 142 url: url, |
| 143 handlerPath: handlerPath, |
| 144 body: body, |
| 145 encoding: encoding, |
| 146 context: context, |
| 147 onHijack: onHijack == null ? null : new _OnHijack(onHijack)); |
| 148 |
| 149 /// This constructor has the same signature as [new Request] except that |
| 150 /// accepts [onHijack] as [_OnHijack]. |
| 151 /// |
| 152 /// Any [Request] created by calling [change] will pass [_onHijack] from the |
| 153 /// source [Request] to ensure that [hijack] can only be called once, even |
| 154 /// from a changed [Request]. |
| 155 Request._(this.method, Uri requestedUri, {String protocolVersion, |
| 156 Map<String, String> headers, String handlerPath, Uri url, body, |
| 157 Encoding encoding, Map<String, Object> context, _OnHijack onHijack}) |
| 158 : this.requestedUri = requestedUri, |
| 159 this.protocolVersion = protocolVersion == null |
| 160 ? '1.1' |
| 161 : protocolVersion, |
| 162 this.url = _computeUrl(requestedUri, handlerPath, url), |
| 163 this.handlerPath = _computeHandlerPath(requestedUri, handlerPath, url), |
| 164 this._onHijack = onHijack, |
| 165 super(body, encoding: encoding, headers: headers, context: context) { |
| 166 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); |
| 167 |
| 168 if (!requestedUri.isAbsolute) { |
| 169 throw new ArgumentError( |
| 170 'requestedUri "$requestedUri" must be an absolute URL.'); |
| 171 } |
| 172 |
| 173 if (requestedUri.fragment.isNotEmpty) { |
| 174 throw new ArgumentError( |
| 175 'requestedUri "$requestedUri" may not have a fragment.'); |
| 176 } |
| 177 |
| 178 if (this.handlerPath + this.url.path != this.requestedUri.path) { |
| 179 throw new ArgumentError('handlerPath "$handlerPath" and url "$url" must ' |
| 180 'combine to equal requestedUri path "${requestedUri.path}".'); |
| 181 } |
| 182 } |
| 183 |
| 184 /// Creates a new [Request] by copying existing values and applying specified |
| 185 /// changes. |
| 186 /// |
| 187 /// New key-value pairs in [context] and [headers] will be added to the copied |
| 188 /// [Request]. If [context] or [headers] includes a key that already exists, |
| 189 /// the key-value pair will replace the corresponding entry in the copied |
| 190 /// [Request]. All other context and header values from the [Request] will be |
| 191 /// included in the copied [Request] unchanged. |
| 192 /// |
| 193 /// [body] is the request body. It may be either a [String] or a |
| 194 /// [Stream<List<int>>]. |
| 195 /// |
| 196 /// [path] is used to update both [handlerPath] and [url]. It's designed for |
| 197 /// routing middleware, and represents the path from the current handler to |
| 198 /// the next handler. It must be a prefix of [url]; [handlerPath] becomes |
| 199 /// `handlerPath + "/" + path`, and [url] becomes relative to that. For |
| 200 /// example: |
| 201 /// |
| 202 /// print(request.handlerPath); // => /static/ |
| 203 /// print(request.url); // => dir/file.html |
| 204 /// |
| 205 /// request = request.change(path: "dir"); |
| 206 /// print(request.handlerPath); // => /static/dir/ |
| 207 /// print(request.url); // => file.html |
| 208 Request change({Map<String, String> headers, Map<String, Object> context, |
| 209 String path, body}) { |
| 210 headers = updateMap(this.headers, headers); |
| 211 context = updateMap(this.context, context); |
| 212 |
| 213 if (body == null) body = getBody(this); |
| 214 |
| 215 var handlerPath = this.handlerPath; |
| 216 if (path != null) handlerPath += path; |
| 217 |
| 218 return new Request._(this.method, this.requestedUri, |
| 219 protocolVersion: this.protocolVersion, |
| 220 headers: headers, |
| 221 handlerPath: handlerPath, |
| 222 body: body, |
| 223 context: context, |
| 224 onHijack: _onHijack); |
| 225 } |
| 226 |
| 227 /// Takes control of the underlying request socket. |
| 228 /// |
| 229 /// Synchronously, this throws a [HijackException] that indicates to the |
| 230 /// adapter that it shouldn't emit a response itself. Asynchronously, |
| 231 /// [callback] is called with a [Stream<List<int>>] and |
| 232 /// [StreamSink<List<int>>], respectively, that provide access to the |
| 233 /// underlying request socket. |
| 234 /// |
| 235 /// If the sink is closed, the stream will be closed as well. The stream and |
| 236 /// sink may be the same object, as in the case of a `dart:io` `Socket` |
| 237 /// object. |
| 238 /// |
| 239 /// This may only be called when using a Shelf adapter that supports |
| 240 /// hijacking, such as the `dart:io` adapter. In addition, a given request may |
| 241 /// only be hijacked once. [canHijack] can be used to detect whether this |
| 242 /// request can be hijacked. |
| 243 void hijack(HijackCallback callback) { |
| 244 if (_onHijack == null) { |
| 245 throw new StateError("This request can't be hijacked."); |
| 246 } |
| 247 |
| 248 _onHijack.run(callback); |
| 249 throw const HijackException(); |
| 250 } |
| 251 } |
| 252 |
| 253 /// A class containing a callback for [Request.hijack] that also tracks whether |
| 254 /// the callback has been called. |
| 255 class _OnHijack { |
| 256 /// The callback. |
| 257 final OnHijackCallback _callback; |
| 258 |
| 259 /// Whether [this] has been called. |
| 260 bool called = false; |
| 261 |
| 262 _OnHijack(this._callback); |
| 263 |
| 264 /// Calls [this]. |
| 265 /// |
| 266 /// Throws a [StateError] if [this] has already been called. |
| 267 void run(HijackCallback callback) { |
| 268 if (called) throw new StateError("This request has already been hijacked."); |
| 269 called = true; |
| 270 newFuture(() => _callback(callback)); |
| 271 } |
| 272 } |
| 273 |
| 274 /// Computes `url` from the provided [Request] constructor arguments. |
| 275 /// |
| 276 /// If [url] is `null`, the value is inferred from [requestedUrl] and |
| 277 /// [handlerPath] if available. Otherwise [url] is returned. |
| 278 Uri _computeUrl(Uri requestedUri, String handlerPath, Uri url) { |
| 279 if (handlerPath != null && |
| 280 handlerPath != requestedUri.path && |
| 281 !handlerPath.endsWith("/")) { |
| 282 handlerPath += "/"; |
| 283 } |
| 284 |
| 285 if (url != null) { |
| 286 if (url.scheme.isNotEmpty || url.hasAuthority || url.fragment.isNotEmpty) { |
| 287 throw new ArgumentError('url "$url" may contain only a path and query ' |
| 288 'parameters.'); |
| 289 } |
| 290 |
| 291 if (!requestedUri.path.endsWith(url.path)) { |
| 292 throw new ArgumentError('url "$url" must be a suffix of requestedUri ' |
| 293 '"$requestedUri".'); |
| 294 } |
| 295 |
| 296 if (requestedUri.query != url.query) { |
| 297 throw new ArgumentError('url "$url" must have the same query parameters ' |
| 298 'as requestedUri "$requestedUri".'); |
| 299 } |
| 300 |
| 301 if (url.path.startsWith('/')) { |
| 302 throw new ArgumentError('url "$url" must be relative.'); |
| 303 } |
| 304 |
| 305 var startOfUrl = requestedUri.path.length - url.path.length; |
| 306 if (url.path.isNotEmpty && |
| 307 requestedUri.path.substring(startOfUrl - 1, startOfUrl) != '/') { |
| 308 throw new ArgumentError('url "$url" must be on a path boundary in ' |
| 309 'requestedUri "$requestedUri".'); |
| 310 } |
| 311 |
| 312 return url; |
| 313 } else if (handlerPath != null) { |
| 314 return new Uri( |
| 315 path: requestedUri.path.substring(handlerPath.length), |
| 316 query: requestedUri.query); |
| 317 } else { |
| 318 // Skip the initial "/". |
| 319 var path = requestedUri.path.substring(1); |
| 320 return new Uri(path: path, query: requestedUri.query); |
| 321 } |
| 322 } |
| 323 |
| 324 /// Computes `handlerPath` from the provided [Request] constructor arguments. |
| 325 /// |
| 326 /// If [handlerPath] is `null`, the value is inferred from [requestedUrl] and |
| 327 /// [url] if available. Otherwise [handlerPath] is returned. |
| 328 String _computeHandlerPath(Uri requestedUri, String handlerPath, Uri url) { |
| 329 if (handlerPath != null && |
| 330 handlerPath != requestedUri.path && |
| 331 !handlerPath.endsWith("/")) { |
| 332 handlerPath += "/"; |
| 333 } |
| 334 |
| 335 if (handlerPath != null) { |
| 336 if (!requestedUri.path.startsWith(handlerPath)) { |
| 337 throw new ArgumentError('handlerPath "$handlerPath" must be a prefix of ' |
| 338 'requestedUri path "${requestedUri.path}"'); |
| 339 } |
| 340 |
| 341 if (!handlerPath.startsWith('/')) { |
| 342 throw new ArgumentError( |
| 343 'handlerPath "$handlerPath" must be root-relative.'); |
| 344 } |
| 345 |
| 346 return handlerPath; |
| 347 } else if (url != null) { |
| 348 if (url.path.isEmpty) return requestedUri.path; |
| 349 |
| 350 var index = requestedUri.path.indexOf(url.path); |
| 351 return requestedUri.path.substring(0, index); |
| 352 } else { |
| 353 return '/'; |
| 354 } |
| 355 } |
OLD | NEW |