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