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'; |
13 import 'util.dart'; | |
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); | |
12 | 22 |
13 /// Represents an HTTP request to be processed by a Shelf application. | 23 /// Represents an HTTP request to be processed by a Shelf application. |
14 class Request extends Message { | 24 class Request extends Message { |
15 /// The remainder of the [requestedUri] path and query designating the virtual | 25 /// The remainder of the [requestedUri] path and query designating the virtual |
16 /// "location" of the request's target within the handler. | 26 /// "location" of the request's target within the handler. |
17 /// | 27 /// |
18 /// [url] may be an empty, if [requestedUri]targets the handler | 28 /// [url] may be an empty, if [requestedUri]targets the handler |
19 /// root and does not have a trailing slash. | 29 /// root and does not have a trailing slash. |
20 /// | 30 /// |
21 /// [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 `/`. |
(...skipping 16 matching lines...) Expand all Loading... | |
38 /// [scriptName] and [url] combine to create a valid path that should | 48 /// [scriptName] and [url] combine to create a valid path that should |
39 /// correspond to the [requestedUri] path. | 49 /// correspond to the [requestedUri] path. |
40 final String scriptName; | 50 final String scriptName; |
41 | 51 |
42 /// 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". |
43 final String protocolVersion; | 53 final String protocolVersion; |
44 | 54 |
45 /// The original [Uri] for the request. | 55 /// The original [Uri] for the request. |
46 final Uri requestedUri; | 56 final Uri requestedUri; |
47 | 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 | |
48 /// 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 |
49 /// 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 |
50 /// response. | 71 /// response. |
51 /// | 72 /// |
52 /// 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 |
53 /// [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`. |
54 DateTime get ifModifiedSince { | 75 DateTime get ifModifiedSince { |
55 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; | 76 if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; |
56 if (!headers.containsKey('if-modified-since')) return null; | 77 if (!headers.containsKey('if-modified-since')) return null; |
57 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); | 78 _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']); |
58 return _ifModifiedSinceCache; | 79 return _ifModifiedSinceCache; |
59 } | 80 } |
60 DateTime _ifModifiedSinceCache; | 81 DateTime _ifModifiedSinceCache; |
61 | 82 |
62 /// Creates a new [Request]. | 83 /// Creates a new [Request]. |
63 /// | 84 /// |
64 /// If [url] and [scriptName] are omitted, they are inferred from | 85 /// If [url] and [scriptName] are omitted, they are inferred from |
65 /// [requestedUri]. | 86 /// [requestedUri]. |
66 /// | 87 /// |
67 /// 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 |
68 /// [ArgumentError]. | 89 /// [ArgumentError]. |
69 /// | 90 /// |
70 /// 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 | |
kevmoo
2014/05/07 19:28:22
occurred. The adapter...
nweiz
2014/05/19 20:10:29
Done.
| |
109 /// sending a response or notifying the user of an error if one of these is | |
kevmoo
2014/05/07 19:28:22
"one of these is received" -> a [HijackException]
nweiz
2014/05/19 20:10:29
Done.
| |
110 /// received. | |
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]. | |
71 // TODO(kevmoo) finish documenting the rest of the arguments. | 118 // TODO(kevmoo) finish documenting the rest of the arguments. |
72 Request(this.method, Uri requestedUri, {String protocolVersion, | 119 Request(this.method, Uri requestedUri, {String protocolVersion, |
73 Map<String, String> headers, Uri url, String scriptName, | 120 Map<String, String> headers, Uri url, String scriptName, |
74 Stream<List<int>> body, Map<String, Object> context}) | 121 Stream<List<int>> body, Map<String, Object> context, |
122 OnHijackCallback onHijack}) | |
75 : this.requestedUri = requestedUri, | 123 : this.requestedUri = requestedUri, |
76 this.protocolVersion = protocolVersion == null ? | 124 this.protocolVersion = protocolVersion == null ? |
77 '1.1' : protocolVersion, | 125 '1.1' : protocolVersion, |
78 this.url = _computeUrl(requestedUri, url, scriptName), | 126 this.url = _computeUrl(requestedUri, url, scriptName), |
79 this.scriptName = _computeScriptName(requestedUri, url, scriptName), | 127 this.scriptName = _computeScriptName(requestedUri, url, scriptName), |
128 this._onHijack = onHijack == null ? null : new _OnHijack(onHijack), | |
80 super(body == null ? new Stream.fromIterable([]) : body, | 129 super(body == null ? new Stream.fromIterable([]) : body, |
81 headers: headers, context: context) { | 130 headers: headers, context: context) { |
82 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); | 131 if (method.isEmpty) throw new ArgumentError('method cannot be empty.'); |
83 | 132 |
84 // TODO(kevmoo) use isAbsolute property on Uri once Issue 18053 is fixed | 133 // TODO(kevmoo) use isAbsolute property on Uri once Issue 18053 is fixed |
85 if (requestedUri.scheme.isEmpty) { | 134 if (requestedUri.scheme.isEmpty) { |
86 throw new ArgumentError('requstedUri must be an absolute URI.'); | 135 throw new ArgumentError('requstedUri must be an absolute URI.'); |
87 } | 136 } |
88 | 137 |
89 if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { | 138 if (this.scriptName.isNotEmpty && !this.scriptName.startsWith('/')) { |
(...skipping 10 matching lines...) Expand all Loading... | |
100 } | 149 } |
101 | 150 |
102 if (this.url.path.isNotEmpty && !this.url.path.startsWith('/')) { | 151 if (this.url.path.isNotEmpty && !this.url.path.startsWith('/')) { |
103 throw new ArgumentError('url must be empty or start with "/".'); | 152 throw new ArgumentError('url must be empty or start with "/".'); |
104 } | 153 } |
105 | 154 |
106 if (this.scriptName.isEmpty && this.url.path.isEmpty) { | 155 if (this.scriptName.isEmpty && this.url.path.isEmpty) { |
107 throw new ArgumentError('scriptName and url cannot both be empty.'); | 156 throw new ArgumentError('scriptName and url cannot both be empty.'); |
108 } | 157 } |
109 } | 158 } |
159 | |
160 /// Takes control of the underlying request socket. | |
161 /// | |
162 /// Synchronously, this throws a [HijackException] that indicates to the | |
163 /// adapter that it shouldn't emit a response itself. Asynchronously, | |
164 /// [callback] is called with a [Stream<List<int>>] and | |
165 /// [StreamSink<List<int>>], respectively, that provide access to the | |
166 /// underlying request socket. | |
167 /// | |
168 /// If the sink is closed, the stream will be closed as well. The stream and | |
169 /// sink may be the same object, as in the case of a `dart:io` `Socket` | |
170 /// object. | |
171 /// | |
172 /// This may only be called when using a Shelf adapter that supports | |
173 /// hijacking, such as the `dart:io` adapter. In addition, a given request may | |
174 /// only be hijacked once. [canHijack] can be used to detect whether this | |
175 /// request can be hijacked. | |
176 void hijack(HijackCallback callback) { | |
177 if (_onHijack == null) { | |
178 throw new StateError("This request can't be hijacked."); | |
179 } | |
180 | |
181 _onHijack.run(callback); | |
182 throw const HijackException(); | |
183 } | |
184 } | |
185 | |
186 /// A class containing a callback for [Request.hijack] that also tracks whether | |
187 /// the callback has been called. | |
188 class _OnHijack { | |
189 /// The callback. | |
190 final OnHijackCallback _callback; | |
191 | |
192 /// Whether [this] has been called. | |
193 var called = false; | |
kevmoo
2014/05/07 19:28:22
var -> bool
nweiz
2014/05/19 20:10:29
Done, under protest.
kevmoo
2014/05/19 20:14:18
:-)
| |
194 | |
195 _OnHijack(this._callback); | |
196 | |
197 /// Calls [this]. | |
198 /// | |
199 /// Throws a [StateError] if [this] has already been called. | |
200 void run(HijackCallback callback) { | |
201 if (called) throw new StateError("This request has already been hijacked."); | |
202 called = true; | |
203 newFuture(() => _callback(callback)); | |
204 } | |
110 } | 205 } |
111 | 206 |
112 /// Computes `url` from the provided [Request] constructor arguments. | 207 /// Computes `url` from the provided [Request] constructor arguments. |
113 /// | 208 /// |
114 /// If [url] and [scriptName] are `null`, infer value from [requestedUrl], | 209 /// If [url] and [scriptName] are `null`, infer value from [requestedUrl], |
115 /// otherwise return [url]. | 210 /// otherwise return [url]. |
116 /// | 211 /// |
117 /// If [url] is provided, but [scriptName] is omitted, throws an | 212 /// If [url] is provided, but [scriptName] is omitted, throws an |
118 /// [ArgumentError]. | 213 /// [ArgumentError]. |
119 Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { | 214 Uri _computeUrl(Uri requestedUri, Uri url, String scriptName) { |
(...skipping 24 matching lines...) Expand all Loading... | |
144 return ''; | 239 return ''; |
145 } | 240 } |
146 | 241 |
147 if (url != null && scriptName != null) { | 242 if (url != null && scriptName != null) { |
148 return scriptName; | 243 return scriptName; |
149 } | 244 } |
150 | 245 |
151 throw new ArgumentError( | 246 throw new ArgumentError( |
152 'url and scriptName must both be null or both be set.'); | 247 'url and scriptName must both be null or both be set.'); |
153 } | 248 } |
OLD | NEW |