| 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 import 'dart:collection'; | |
| 9 | 8 |
| 10 // TODO(kevmoo): use UnmodifiableMapView from SDK once 1.4 ships | |
| 11 import 'package:collection/wrappers.dart' as pc; | |
| 12 import 'package:http_parser/http_parser.dart'; | 9 import 'package:http_parser/http_parser.dart'; |
| 13 import 'package:path/path.dart' as p; | |
| 14 | 10 |
| 15 import 'message.dart'; | 11 import 'message.dart'; |
| 16 | 12 |
| 17 /// Represents an HTTP request to be processed by a Shelf application. | 13 /// Represents an HTTP request to be processed by a Shelf application. |
| 18 class Request extends Message { | 14 class Request extends Message { |
| 19 /// The remainder of the [requestedUri] path designating the virtual | 15 /// The remainder of the [requestedUri] path and query designating the virtual |
| 20 /// "location" of the request's target within the handler. | 16 /// "location" of the request's target within the handler. |
| 21 /// | 17 /// |
| 22 /// [pathInfo] may be an empty string, if [requestedUri ]targets the handler | 18 /// [url] may be an empty, if [requestedUri]targets the handler |
| 23 /// root and does not have a trailing slash. | 19 /// root and does not have a trailing slash. |
| 24 /// | 20 /// |
| 25 /// [pathInfo] is never null. If it is not empty, it will start with `/`. | 21 /// [url] is never null. If it is not empty, it will start with `/`. |
| 26 /// | 22 /// |
| 27 /// [scriptName] and [pathInfo] combine to create a valid path that should | 23 /// [scriptName] and [url] combine to create a valid path that should |
| 28 /// correspond to the [requestedUri] path. | 24 /// correspond to the [requestedUri] path. |
| 29 final String pathInfo; | 25 final Uri url; |
| 30 | |
| 31 /// The portion of the request URL that follows the "?", if any. | |
| 32 final String queryString; | |
| 33 | 26 |
| 34 /// The HTTP request method, such as "GET" or "POST". | 27 /// The HTTP request method, such as "GET" or "POST". |
| 35 final String method; | 28 final String method; |
| 36 | 29 |
| 37 /// The initial portion of the [requestedUri] path that corresponds to the | 30 /// The initial portion of the [requestedUri] path that corresponds to the |
| 38 /// handler. | 31 /// handler. |
| 39 /// | 32 /// |
| 40 /// [scriptName] allows a handler to know its virtual "location". | 33 /// [scriptName] allows a handler to know its virtual "location". |
| 41 /// | 34 /// |
| 42 /// If the handler corresponds to the "root" of a server, it will be an | 35 /// If the handler corresponds to the "root" of a server, it will be an |
| 43 /// empty string, otherwise it will start with a `/` | 36 /// empty string, otherwise it will start with a `/` |
| 44 /// | 37 /// |
| 45 /// [scriptName] and [pathInfo] combine to create a valid path that should | 38 /// [scriptName] and [url] combine to create a valid path that should |
| 46 /// correspond to the [requestedUri] path. | 39 /// correspond to the [requestedUri] path. |
| 47 final String scriptName; | 40 final String scriptName; |
| 48 | 41 |
| 49 /// The HTTP protocol version used in the request, either "1.0" or "1.1". | 42 /// The HTTP protocol version used in the request, either "1.0" or "1.1". |
| 50 final String protocolVersion; | 43 final String protocolVersion; |
| 51 | 44 |
| 52 /// The original [Uri] for the request. | 45 /// The original [Uri] for the request. |
| 53 final Uri requestedUri; | 46 final Uri requestedUri; |
| 54 | 47 |
| 55 /// If this is non-`null` and the requested resource hasn't been modified | 48 /// If this is non-`null` and the requested resource hasn't been modified |
| 56 /// since this date and time, the server should return a 304 Not Modified | 49 /// since this date and time, the server should return a 304 Not Modified |
| 57 /// response. | 50 /// response. |
| 58 /// | 51 /// |
| 59 /// This is parsed from the If-Modified-Since header in [headers]. If | 52 /// This is parsed from the If-Modified-Since header in [headers]. If |
| 60 /// [headers] doesn't have an If-Modified-Since header, this will be `null`. | 53 /// [headers] doesn't have an If-Modified-Since header, this will be `null`. |
| 61 DateTime get ifModifiedSince { | 54 DateTime get ifModifiedSince { |
| 62 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; | 55 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; |
| 63 if (!headers.containsKey('if-modified-since')) return null; | 56 if (!headers.containsKey('if-modified-since')) return null; |
| 64 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); | 57 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); |
| 65 return _ifModifiedSinceCache; | 58 return _ifModifiedSinceCache; |
| 66 } | 59 } |
| 67 DateTime _ifModifiedSinceCache; | 60 DateTime _ifModifiedSinceCache; |
| 68 | 61 |
| 69 Request(this.pathInfo, String queryString, this.method, | 62 /// Creates a new [Request]. |
| 70 this.scriptName, this.protocolVersion, this.requestedUri, | 63 /// |
| 71 Map<String, String> headers, {Stream<List<int>> body}) | 64 /// If [url] and [scriptName] are omitted, they are inferred from |
| 72 : this.queryString = queryString == null ? '' : queryString, | 65 /// [requestedUri]. |
| 73 super(new pc.UnmodifiableMapView(new HashMap.from(headers)), | 66 /// |
| 74 body == null ? new Stream.fromIterable([]) : body) { | 67 /// Setting one of [url] or [scriptName] and not the other will throw an |
| 68 /// [ArgumentError]. |
| 69 /// |
| 70 /// The default value for [protocolVersion] is '1.1'. |
| 71 // TODO(kevmoo) finish documenting the rest of the arguments. |
| 72 Request(this.method, Uri requestedUri, {String protocolVersion, |
| 73 Map<String, String> headers, Uri url, String scriptName, |
| 74 Stream<List<int>> body}) |
| 75 : this.requestedUri = requestedUri, |
| 76 this.protocolVersion = protocolVersion == null ? |
| 77 '1.1' : protocolVersion, |
| 78 this.url = _computeUrl(requestedUri, url, scriptName), |
| 79 this.scriptName = _computeScriptName(requestedUri, url, scriptName), |
| 80 super(body == null ? new Stream.fromIterable([]) : body, |
| 81 headers: headers) { |
| 75 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); | 82 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); |
| 76 | 83 |
| 77 if (scriptName.isNotEmpty && !scriptName.startsWith('/')) { | 84 // TODO(kevmoo) use isAbsolute property on Uri once Issue 18053 is fixed |
| 85 if (requestedUri.scheme.isEmpty) { |
| 86 throw new ArgumentError('requstedUri must be an absolute URI.'); |
| 87 } |
| 88 |
| 89 if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { |
| 78 throw new ArgumentError('scriptName must be empty or start with "/".'); | 90 throw new ArgumentError('scriptName must be empty or start with "/".'); |
| 79 } | 91 } |
| 80 | 92 |
| 81 if (scriptName == '/') { | 93 if (this.scriptName == '/') { |
| 82 throw new ArgumentError( | 94 throw new ArgumentError( |
| 83 'scriptName can never be "/". It should be empty instead.'); | 95 'scriptName can never be "/". It should be empty instead.'); |
| 84 } | 96 } |
| 85 | 97 |
| 86 if (scriptName.endsWith('/')) { | 98 if (this.scriptName.endsWith('/')) { |
| 87 throw new ArgumentError('scriptName must not end with "/".'); | 99 throw new ArgumentError('scriptName must not end with "/".'); |
| 88 } | 100 } |
| 89 | 101 |
| 90 if (pathInfo.isNotEmpty && !pathInfo.startsWith('/')) { | 102 if (this.url.path.isNotEmpty && !this.url.path.startsWith('/')) { |
| 91 throw new ArgumentError('pathInfo must be empty or start with "/".'); | 103 throw new ArgumentError('url must be empty or start with "/".'); |
| 92 } | 104 } |
| 93 | 105 |
| 94 if (scriptName.isEmpty && pathInfo.isEmpty) { | 106 if (this.scriptName.isEmpty && this.url.path.isEmpty) { |
| 95 throw new ArgumentError('scriptName and pathInfo cannot both be empty.'); | 107 throw new ArgumentError('scriptName and url cannot both be empty.'); |
| 96 } | 108 } |
| 97 } | 109 } |
| 110 } |
| 98 | 111 |
| 99 /// Convenience property to access [pathInfo] data as a [List]. | 112 /// Computes `url` from the provided [Request] constructor arguments. |
| 100 List<String> get pathSegments { | 113 /// |
| 101 var segs = p.url.split(pathInfo); | 114 /// If [url] and [scriptName] are `null`, infer value from [requestedUrl], |
| 102 if (segs.length > 0) { | 115 /// otherwise return [url]. |
| 103 assert(segs.first == p.url.separator); | 116 /// |
| 104 segs.removeAt(0); | 117 /// If [url] is provided, but [scriptName] is omitted, throws an |
| 105 } | 118 /// [ArgumentError]. |
| 106 return segs; | 119 Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { |
| 120 if (url == null && scriptName == null) { |
| 121 return new Uri(path: requestedUri.path, query: requestedUri.query, |
| 122 fragment: requestedUri.fragment); |
| 107 } | 123 } |
| 124 |
| 125 if (url != null && scriptName != null) { |
| 126 // TODO(kevmoo) use isAbsolute property on Uri once Issue 18053 is fixed |
| 127 if (url.scheme.isNotEmpty) throw new ArgumentError('url must be relative.'); |
| 128 return url; |
| 129 } |
| 130 |
| 131 throw new ArgumentError( |
| 132 'url and scriptName must both be null or both be set.'); |
| 108 } | 133 } |
| 134 |
| 135 /// Computes `scriptName` from the provided [Request] constructor arguments. |
| 136 /// |
| 137 /// If [url] and [scriptName] are `null` it returns an empty string, otherwise |
| 138 /// [scriptName] is returned. |
| 139 /// |
| 140 /// If [script] is provided, but [url] is omitted, throws an |
| 141 /// [ArgumentError]. |
| 142 String _computeScriptName(Uri requstedUri, Uri url, String scriptName) { |
| 143 if (url == null && scriptName == null) { |
| 144 return ''; |
| 145 } |
| 146 |
| 147 if (url != null && scriptName != null) { |
| 148 return scriptName; |
| 149 } |
| 150 |
| 151 throw new ArgumentError( |
| 152 'url and scriptName must both be null or both be set.'); |
| 153 } |
| OLD | NEW |