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:convert'; | 8 import 'dart:convert'; |
9 | 9 |
10 import 'package:http_parser/http_parser.dart'; | 10 import 'package:http_parser/http_parser.dart'; |
11 | 11 |
12 import 'hijack_exception.dart'; | 12 import 'hijack_exception.dart'; |
13 import 'message.dart'; | 13 import 'message.dart'; |
14 import 'util.dart'; | 14 import 'util.dart'; |
15 | 15 |
16 /// A callback provided by a Shelf handler that's passed to [Request.hijack]. | 16 /// A callback provided by a Shelf handler that's passed to [Request.hijack]. |
17 typedef void HijackCallback( | 17 typedef void HijackCallback( |
18 Stream<List<int>> stream, StreamSink<List<int>> sink); | 18 Stream<List<int>> stream, StreamSink<List<int>> sink); |
19 | 19 |
20 /// A callback provided by a Shelf adapter that's used by [Request.hijack] to | 20 /// A callback provided by a Shelf adapter that's used by [Request.hijack] to |
21 /// provide a [HijackCallback] with a socket. | 21 /// provide a [HijackCallback] with a socket. |
22 typedef void OnHijackCallback(HijackCallback callback); | 22 typedef void OnHijackCallback(HijackCallback callback); |
23 | 23 |
24 /// Represents an HTTP request to be processed by a Shelf application. | 24 /// Represents an HTTP request to be processed by a Shelf application. |
25 class Request extends Message { | 25 class Request extends Message { |
26 /// The remainder of the [requestedUri] path and query designating the virtual | 26 /// The URL path from the current handler to the requested resource, relative |
27 /// "location" of the request's target within the handler. | 27 /// to [handlerPath], plus any query parameters. |
28 /// | 28 /// |
29 /// [url] may be an empty, if [requestedUri]targets the handler | 29 /// This should be used by handlers for determining which resource to serve, |
30 /// root and does not have a trailing slash. | 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]. |
31 /// | 34 /// |
32 /// [url] is never null. If it is not empty, it will start with `/`. | 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]. |
33 /// | 38 /// |
34 /// [scriptName] and [url] combine to create a valid path that should | 39 /// [handlerPath] and [url]'s path combine to create [requestedUri]'s path. |
35 /// correspond to the [requestedUri] path. | |
36 final Uri url; | 40 final Uri url; |
37 | 41 |
38 /// The HTTP request method, such as "GET" or "POST". | 42 /// The HTTP request method, such as "GET" or "POST". |
39 final String method; | 43 final String method; |
40 | 44 |
41 /// The initial portion of the [requestedUri] path that corresponds to the | 45 /// The URL path to the current handler. |
42 /// handler. | |
43 /// | 46 /// |
44 /// [scriptName] allows a handler to know its virtual "location". | 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]. |
45 /// | 50 /// |
46 /// If the handler corresponds to the "root" of a server, it will be an | 51 /// [handlerPath] is always a root-relative URL path; that is, it always |
47 /// empty string, otherwise it will start with a `/` | 52 /// starts with `/`. It will also end with `/` whenever [url]'s path is |
| 53 /// non-empty, or if [requestUri]'s path ends with `/`. |
48 /// | 54 /// |
49 /// [scriptName] and [url] combine to create a valid path that should | 55 /// [handlerPath] and [url]'s path combine to create [requestedUri]'s path. |
50 /// correspond to the [requestedUri] path. | 56 final String handlerPath; |
51 final String scriptName; | |
52 | 57 |
53 /// The HTTP protocol version used in the request, either "1.0" or "1.1". | 58 /// The HTTP protocol version used in the request, either "1.0" or "1.1". |
54 final String protocolVersion; | 59 final String protocolVersion; |
55 | 60 |
56 /// The original [Uri] for the request. | 61 /// The original [Uri] for the request. |
57 final Uri requestedUri; | 62 final Uri requestedUri; |
58 | 63 |
59 /// The callback wrapper for hijacking this request. | 64 /// The callback wrapper for hijacking this request. |
60 /// | 65 /// |
61 /// This will be `null` if this request can't be hijacked. | 66 /// This will be `null` if this request can't be hijacked. |
(...skipping 14 matching lines...) Expand all Loading... |
76 DateTime get ifModifiedSince { | 81 DateTime get ifModifiedSince { |
77 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; | 82 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; |
78 if (!headers.containsKey('if-modified-since')) return null; | 83 if (!headers.containsKey('if-modified-since')) return null; |
79 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); | 84 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); |
80 return _ifModifiedSinceCache; | 85 return _ifModifiedSinceCache; |
81 } | 86 } |
82 DateTime _ifModifiedSinceCache; | 87 DateTime _ifModifiedSinceCache; |
83 | 88 |
84 /// Creates a new [Request]. | 89 /// Creates a new [Request]. |
85 /// | 90 /// |
86 /// If [url] and [scriptName] are omitted, they are inferred from | 91 /// [handlerPath] must be root-relative. [url]'s path must be fully relative, |
87 /// [requestedUri]. | 92 /// and it must have the same query parameters as [requestedUri]. |
88 /// | 93 /// [handlerPath] and [url]'s path must combine to be the path component of |
89 /// Setting one of [url] or [scriptName] and not the other will throw an | 94 /// [requestedUri]. If they're not passed, [handlerPath] will default to `/` |
90 /// [ArgumentError]. | 95 /// and [url] to `requestedUri.path` without the initial `/`. If only one is |
| 96 /// passed, the other will be inferred. |
91 /// | 97 /// |
92 /// [body] is the request body. It may be either a [String], a | 98 /// [body] is the request body. It may be either a [String], a |
93 /// [Stream<List<int>>], or `null` to indicate no body. | 99 /// [Stream<List<int>>], or `null` to indicate no body. |
94 /// If it's a [String], [encoding] is used to encode it to a | 100 /// If it's a [String], [encoding] is used to encode it to a |
95 /// [Stream<List<int>>]. The default encoding is UTF-8. | 101 /// [Stream<List<int>>]. The default encoding is UTF-8. |
96 /// | 102 /// |
97 /// If [encoding] is passed, the "encoding" field of the Content-Type header | 103 /// If [encoding] is passed, the "encoding" field of the Content-Type header |
98 /// in [headers] will be set appropriately. If there is no existing | 104 /// in [headers] will be set appropriately. If there is no existing |
99 /// Content-Type header, it will be set to "application/octet-stream". | 105 /// Content-Type header, it will be set to "application/octet-stream". |
100 /// | 106 /// |
(...skipping 19 matching lines...) Expand all Loading... |
120 /// [HijackException] is caught. | 126 /// [HijackException] is caught. |
121 /// | 127 /// |
122 /// An adapter can check whether a request was hijacked using [canHijack], | 128 /// An adapter can check whether a request was hijacked using [canHijack], |
123 /// which will be `false` for a hijacked request. The adapter may throw an | 129 /// which will be `false` for a hijacked request. The adapter may throw an |
124 /// error if a [HijackException] is received for a non-hijacked request, or if | 130 /// error if a [HijackException] is received for a non-hijacked request, or if |
125 /// no [HijackException] is received for a hijacked request. | 131 /// no [HijackException] is received for a hijacked request. |
126 /// | 132 /// |
127 /// See also [hijack]. | 133 /// See also [hijack]. |
128 // TODO(kevmoo) finish documenting the rest of the arguments. | 134 // TODO(kevmoo) finish documenting the rest of the arguments. |
129 Request(String method, Uri requestedUri, {String protocolVersion, | 135 Request(String method, Uri requestedUri, {String protocolVersion, |
130 Map<String, String> headers, Uri url, String scriptName, body, | 136 Map<String, String> headers, String handlerPath, Uri url, body, |
131 Encoding encoding, Map<String, Object> context, | 137 Encoding encoding, Map<String, Object> context, |
132 OnHijackCallback onHijack}) | 138 OnHijackCallback onHijack}) |
133 : this._(method, requestedUri, | 139 : this._(method, requestedUri, |
134 protocolVersion: protocolVersion, | 140 protocolVersion: protocolVersion, |
135 headers: headers, | 141 headers: headers, |
136 url: url, | 142 url: url, |
137 scriptName: scriptName, | 143 handlerPath: handlerPath, |
138 body: body, | 144 body: body, |
139 encoding: encoding, | 145 encoding: encoding, |
140 context: context, | 146 context: context, |
141 onHijack: onHijack == null ? null : new _OnHijack(onHijack)); | 147 onHijack: onHijack == null ? null : new _OnHijack(onHijack)); |
142 | 148 |
143 /// This constructor has the same signature as [new Request] except that | 149 /// This constructor has the same signature as [new Request] except that |
144 /// accepts [onHijack] as [_OnHijack]. | 150 /// accepts [onHijack] as [_OnHijack]. |
145 /// | 151 /// |
146 /// Any [Request] created by calling [change] will pass [_onHijack] from the | 152 /// Any [Request] created by calling [change] will pass [_onHijack] from the |
147 /// source [Request] to ensure that [hijack] can only be called once, even | 153 /// source [Request] to ensure that [hijack] can only be called once, even |
148 /// from a changed [Request]. | 154 /// from a changed [Request]. |
149 Request._(this.method, Uri requestedUri, {String protocolVersion, | 155 Request._(this.method, Uri requestedUri, {String protocolVersion, |
150 Map<String, String> headers, Uri url, String scriptName, body, | 156 Map<String, String> headers, String handlerPath, Uri url, body, |
151 Encoding encoding, Map<String, Object> context, _OnHijack onHijack}) | 157 Encoding encoding, Map<String, Object> context, _OnHijack onHijack}) |
152 : this.requestedUri = requestedUri, | 158 : this.requestedUri = requestedUri, |
153 this.protocolVersion = protocolVersion == null | 159 this.protocolVersion = protocolVersion == null |
154 ? '1.1' | 160 ? '1.1' |
155 : protocolVersion, | 161 : protocolVersion, |
156 this.url = _computeUrl(requestedUri, url, scriptName), | 162 this.url = _computeUrl(requestedUri, handlerPath, url), |
157 this.scriptName = _computeScriptName(requestedUri, url, scriptName), | 163 this.handlerPath = _computeHandlerPath(requestedUri, handlerPath, url), |
158 this._onHijack = onHijack, | 164 this._onHijack = onHijack, |
159 super(body, encoding: encoding, headers: headers, context: context) { | 165 super(body, encoding: encoding, headers: headers, context: context) { |
160 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); | 166 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); |
161 | 167 |
162 if (!requestedUri.isAbsolute) { | 168 if (!requestedUri.isAbsolute) { |
163 throw new ArgumentError('requstedUri must be an absolute URI.'); | 169 throw new ArgumentError( |
| 170 'requestedUri "$requestedUri" must be an absolute URL.'); |
164 } | 171 } |
165 | 172 |
166 // TODO(kevmoo) if defined, check that scriptName is a fully-encoded, valid | 173 if (requestedUri.fragment.isNotEmpty) { |
167 // path component | 174 throw new ArgumentError( |
168 if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { | 175 'requestedUri "$requestedUri" may not have a fragment.'); |
169 throw new ArgumentError('scriptName must be empty or start with "/".'); | |
170 } | 176 } |
171 | 177 |
172 if (this.scriptName == '/') { | 178 if (this.handlerPath + this.url.path != this.requestedUri.path) { |
173 throw new ArgumentError( | 179 throw new ArgumentError('handlerPath "$handlerPath" and url "$url" must ' |
174 'scriptName can never be "/". It should be empty instead.'); | 180 'combine to equal requestedUri path "${requestedUri.path}".'); |
175 } | |
176 | |
177 if (this.scriptName.endsWith('/')) { | |
178 throw new ArgumentError('scriptName must not end with "/".'); | |
179 } | |
180 | |
181 if (this.url.path.isNotEmpty && !this.url.path.startsWith('/')) { | |
182 throw new ArgumentError('url must be empty or start with "/".'); | |
183 } | |
184 | |
185 if (this.scriptName.isEmpty && this.url.path.isEmpty) { | |
186 throw new ArgumentError('scriptName and url cannot both be empty.'); | |
187 } | 181 } |
188 } | 182 } |
189 | 183 |
190 /// Creates a new [Request] by copying existing values and applying specified | 184 /// Creates a new [Request] by copying existing values and applying specified |
191 /// changes. | 185 /// changes. |
192 /// | 186 /// |
193 /// New key-value pairs in [context] and [headers] will be added to the copied | 187 /// New key-value pairs in [context] and [headers] will be added to the copied |
194 /// [Request]. | 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. |
195 /// | 192 /// |
196 /// If [context] or [headers] includes a key that already exists, the | 193 /// [path] is used to update both [handlerPath] and [url]. It's designed for |
197 /// key-value pair will replace the corresponding entry in the copied | 194 /// routing middleware, and represents the path from the current handler to |
198 /// [Request]. | 195 /// the next handler. It must be a prefix of [url]; [handlerPath] becomes |
| 196 /// `handlerPath + "/" + path`, and [url] becomes relative to that. For |
| 197 /// example: |
199 /// | 198 /// |
200 /// All other context and header values from the [Request] will be included | 199 /// print(request.handlerPath); // => /static/ |
201 /// in the copied [Request] unchanged. | 200 /// print(request.url); // => dir/file.html |
202 /// | 201 /// |
203 /// If [scriptName] is provided and [url] is not, [scriptName] must be a | 202 /// request = request.change(path: "dir"); |
204 /// prefix of [this.url]. [url] will default to [this.url] with this prefix | 203 /// print(request.handlerPath); // => /static/dir/ |
205 /// removed. Useful for routing middleware that sends requests to an inner | 204 /// print(request.url); // => file.html |
206 /// [Handler]. | |
207 Request change({Map<String, String> headers, Map<String, Object> context, | 205 Request change({Map<String, String> headers, Map<String, Object> context, |
208 String scriptName, Uri url}) { | 206 String path}) { |
209 headers = updateMap(this.headers, headers); | 207 headers = updateMap(this.headers, headers); |
210 context = updateMap(this.context, context); | 208 context = updateMap(this.context, context); |
211 | 209 |
212 if (scriptName != null && url == null) { | 210 var handlerPath = this.handlerPath; |
213 var path = this.url.path; | 211 if (path != null) handlerPath += path; |
214 if (path.startsWith(scriptName)) { | |
215 path = path.substring(scriptName.length); | |
216 url = new Uri(path: path, query: this.url.query); | |
217 } else { | |
218 throw new ArgumentError('If scriptName is provided without url, it must' | |
219 ' be a prefix of the existing url path.'); | |
220 } | |
221 } | |
222 | |
223 if (url == null) url = this.url; | |
224 if (scriptName == null) scriptName = this.scriptName; | |
225 | 212 |
226 return new Request._(this.method, this.requestedUri, | 213 return new Request._(this.method, this.requestedUri, |
227 protocolVersion: this.protocolVersion, | 214 protocolVersion: this.protocolVersion, |
228 headers: headers, | 215 headers: headers, |
229 url: url, | 216 handlerPath: handlerPath, |
230 scriptName: scriptName, | |
231 body: this.read(), | 217 body: this.read(), |
232 context: context, | 218 context: context, |
233 onHijack: _onHijack); | 219 onHijack: _onHijack); |
234 } | 220 } |
235 | 221 |
236 /// Takes control of the underlying request socket. | 222 /// Takes control of the underlying request socket. |
237 /// | 223 /// |
238 /// Synchronously, this throws a [HijackException] that indicates to the | 224 /// Synchronously, this throws a [HijackException] that indicates to the |
239 /// adapter that it shouldn't emit a response itself. Asynchronously, | 225 /// adapter that it shouldn't emit a response itself. Asynchronously, |
240 /// [callback] is called with a [Stream<List<int>>] and | 226 /// [callback] is called with a [Stream<List<int>>] and |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
275 /// Throws a [StateError] if [this] has already been called. | 261 /// Throws a [StateError] if [this] has already been called. |
276 void run(HijackCallback callback) { | 262 void run(HijackCallback callback) { |
277 if (called) throw new StateError("This request has already been hijacked."); | 263 if (called) throw new StateError("This request has already been hijacked."); |
278 called = true; | 264 called = true; |
279 newFuture(() => _callback(callback)); | 265 newFuture(() => _callback(callback)); |
280 } | 266 } |
281 } | 267 } |
282 | 268 |
283 /// Computes `url` from the provided [Request] constructor arguments. | 269 /// Computes `url` from the provided [Request] constructor arguments. |
284 /// | 270 /// |
285 /// If [url] and [scriptName] are `null`, infer value from [requestedUrl], | 271 /// If [url] is `null`, the value is inferred from [requestedUrl] and |
286 /// otherwise return [url]. | 272 /// [handlerPath] if available. Otherwise [url] is returned. |
287 /// | 273 Uri _computeUrl(Uri requestedUri, String handlerPath, Uri url) { |
288 /// If [url] is provided, but [scriptName] is omitted, throws an | 274 if (handlerPath != null && |
289 /// [ArgumentError]. | 275 handlerPath != requestedUri.path && |
290 Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { | 276 !handlerPath.endsWith("/")) { |
291 if (url == null && scriptName == null) { | 277 handlerPath += "/"; |
292 return new Uri(path: requestedUri.path, query: requestedUri.query); | |
293 } | 278 } |
294 | 279 |
295 if (url != null && scriptName != null) { | 280 if (url != null) { |
296 if (url.scheme.isNotEmpty) throw new ArgumentError('url must be relative.'); | 281 if (url.scheme.isNotEmpty || url.hasAuthority || url.fragment.isNotEmpty) { |
| 282 throw new ArgumentError('url "$url" may contain only a path and query ' |
| 283 'parameters.'); |
| 284 } |
| 285 |
| 286 if (!requestedUri.path.endsWith(url.path)) { |
| 287 throw new ArgumentError('url "$url" must be a suffix of requestedUri ' |
| 288 '"$requestedUri".'); |
| 289 } |
| 290 |
| 291 if (requestedUri.query != url.query) { |
| 292 throw new ArgumentError('url "$url" must have the same query parameters ' |
| 293 'as requestedUri "$requestedUri".'); |
| 294 } |
| 295 |
| 296 if (url.path.startsWith('/')) { |
| 297 throw new ArgumentError('url "$url" must be relative.'); |
| 298 } |
| 299 |
| 300 var startOfUrl = requestedUri.path.length - url.path.length; |
| 301 if (requestedUri.path.substring(startOfUrl - 1, startOfUrl) != '/') { |
| 302 throw new ArgumentError('url "$url" must be on a path boundary in ' |
| 303 'requestedUri "$requestedUri".'); |
| 304 } |
| 305 |
297 return url; | 306 return url; |
| 307 } else if (handlerPath != null) { |
| 308 return new Uri( |
| 309 path: requestedUri.path.substring(handlerPath.length), |
| 310 query: requestedUri.query); |
| 311 } else { |
| 312 // Skip the initial "/". |
| 313 var path = requestedUri.path.substring(1); |
| 314 return new Uri(path: path, query: requestedUri.query); |
| 315 } |
| 316 } |
| 317 |
| 318 /// Computes `handlerPath` from the provided [Request] constructor arguments. |
| 319 /// |
| 320 /// If [handlerPath] is `null`, the value is inferred from [requestedUrl] and |
| 321 /// [url] if available. Otherwise [handlerPath] is returned. |
| 322 String _computeHandlerPath(Uri requestedUri, String handlerPath, Uri url) { |
| 323 if (handlerPath != null && |
| 324 handlerPath != requestedUri.path && |
| 325 !handlerPath.endsWith("/")) { |
| 326 handlerPath += "/"; |
298 } | 327 } |
299 | 328 |
300 throw new ArgumentError( | 329 if (handlerPath != null) { |
301 'url and scriptName must both be null or both be set.'); | 330 if (!requestedUri.path.startsWith(handlerPath)) { |
| 331 throw new ArgumentError('handlerPath "$handlerPath" must be a prefix of ' |
| 332 'requestedUri path "${requestedUri.path}"'); |
| 333 } |
| 334 |
| 335 if (!handlerPath.startsWith('/')) { |
| 336 throw new ArgumentError( |
| 337 'handlerPath "$handlerPath" must be root-relative.'); |
| 338 } |
| 339 |
| 340 return handlerPath; |
| 341 } else if (url != null) { |
| 342 var index = requestedUri.path.indexOf(url.path); |
| 343 return requestedUri.path.substring(0, index); |
| 344 } else { |
| 345 return '/'; |
| 346 } |
302 } | 347 } |
303 | |
304 /// Computes `scriptName` from the provided [Request] constructor arguments. | |
305 /// | |
306 /// If [url] and [scriptName] are `null` it returns an empty string, otherwise | |
307 /// [scriptName] is returned. | |
308 /// | |
309 /// If [script] is provided, but [url] is omitted, throws an | |
310 /// [ArgumentError]. | |
311 String _computeScriptName(Uri requstedUri, Uri url, String scriptName) { | |
312 if (url == null && scriptName == null) { | |
313 return ''; | |
314 } | |
315 | |
316 if (url != null && scriptName != null) { | |
317 return scriptName; | |
318 } | |
319 | |
320 throw new ArgumentError( | |
321 'url and scriptName must both be null or both be set.'); | |
322 } | |
OLD | NEW |