OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, 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 part of dart.io; | |
6 | |
7 class _HttpHeaders implements HttpHeaders { | |
8 final Map<String, List<String>> _headers; | |
9 final String protocolVersion; | |
10 | |
11 bool _mutable = true; // Are the headers currently mutable? | |
12 List<String> _noFoldingHeaders; | |
13 | |
14 int _contentLength = -1; | |
15 bool _persistentConnection = true; | |
16 bool _chunkedTransferEncoding = false; | |
17 String _host; | |
18 int _port; | |
19 | |
20 final int _defaultPortForScheme; | |
21 | |
22 _HttpHeaders(this.protocolVersion, | |
23 {int defaultPortForScheme: HttpClient.DEFAULT_HTTP_PORT, | |
24 _HttpHeaders initialHeaders}) | |
25 : _headers = new HashMap<String, List<String>>(), | |
26 _defaultPortForScheme = defaultPortForScheme { | |
27 if (initialHeaders != null) { | |
28 initialHeaders._headers.forEach((name, value) => _headers[name] = value); | |
29 _contentLength = initialHeaders._contentLength; | |
30 _persistentConnection = initialHeaders._persistentConnection; | |
31 _chunkedTransferEncoding = initialHeaders._chunkedTransferEncoding; | |
32 _host = initialHeaders._host; | |
33 _port = initialHeaders._port; | |
34 } | |
35 if (protocolVersion == "1.0") { | |
36 _persistentConnection = false; | |
37 _chunkedTransferEncoding = false; | |
38 } | |
39 } | |
40 | |
41 List<String> operator[](String name) => _headers[name.toLowerCase()]; | |
42 | |
43 String value(String name) { | |
44 name = name.toLowerCase(); | |
45 List<String> values = _headers[name]; | |
46 if (values == null) return null; | |
47 if (values.length > 1) { | |
48 throw new HttpException("More than one value for header $name"); | |
49 } | |
50 return values[0]; | |
51 } | |
52 | |
53 void add(String name, value) { | |
54 _checkMutable(); | |
55 _addAll(_validateField(name), value); | |
56 } | |
57 | |
58 void _addAll(String name, value) { | |
59 assert(name == _validateField(name)); | |
60 if (value is Iterable) { | |
61 for (var v in value) { | |
62 _add(name, _validateValue(v)); | |
63 } | |
64 } else { | |
65 _add(name, _validateValue(value)); | |
66 } | |
67 } | |
68 | |
69 void set(String name, Object value) { | |
70 _checkMutable(); | |
71 name = _validateField(name); | |
72 _headers.remove(name); | |
73 if (name == HttpHeaders.TRANSFER_ENCODING) { | |
74 _chunkedTransferEncoding = false; | |
75 } | |
76 _addAll(name, value); | |
77 } | |
78 | |
79 void remove(String name, Object value) { | |
80 _checkMutable(); | |
81 name = _validateField(name); | |
82 value = _validateValue(value); | |
83 List<String> values = _headers[name]; | |
84 if (values != null) { | |
85 int index = values.indexOf(value); | |
86 if (index != -1) { | |
87 values.removeRange(index, index + 1); | |
88 } | |
89 if (values.length == 0) _headers.remove(name); | |
90 } | |
91 if (name == HttpHeaders.TRANSFER_ENCODING && value == "chunked") { | |
92 _chunkedTransferEncoding = false; | |
93 } | |
94 } | |
95 | |
96 void removeAll(String name) { | |
97 _checkMutable(); | |
98 name = _validateField(name); | |
99 _headers.remove(name); | |
100 } | |
101 | |
102 void forEach(void f(String name, List<String> values)) { | |
103 _headers.forEach(f); | |
104 } | |
105 | |
106 void noFolding(String name) { | |
107 if (_noFoldingHeaders == null) _noFoldingHeaders = new List<String>(); | |
108 _noFoldingHeaders.add(name); | |
109 } | |
110 | |
111 bool get persistentConnection => _persistentConnection; | |
112 | |
113 void set persistentConnection(bool persistentConnection) { | |
114 _checkMutable(); | |
115 if (persistentConnection == _persistentConnection) return; | |
116 if (persistentConnection) { | |
117 if (protocolVersion == "1.1") { | |
118 remove(HttpHeaders.CONNECTION, "close"); | |
119 } else { | |
120 if (_contentLength == -1) { | |
121 throw new HttpException( | |
122 "Trying to set 'Connection: Keep-Alive' on HTTP 1.0 headers with " | |
123 "no ContentLength"); | |
124 } | |
125 add(HttpHeaders.CONNECTION, "keep-alive"); | |
126 } | |
127 } else { | |
128 if (protocolVersion == "1.1") { | |
129 add(HttpHeaders.CONNECTION, "close"); | |
130 } else { | |
131 remove(HttpHeaders.CONNECTION, "keep-alive"); | |
132 } | |
133 } | |
134 _persistentConnection = persistentConnection; | |
135 } | |
136 | |
137 int get contentLength => _contentLength; | |
138 | |
139 void set contentLength(int contentLength) { | |
140 _checkMutable(); | |
141 if (protocolVersion == "1.0" && | |
142 persistentConnection && | |
143 contentLength == -1) { | |
144 throw new HttpException( | |
145 "Trying to clear ContentLength on HTTP 1.0 headers with " | |
146 "'Connection: Keep-Alive' set"); | |
147 } | |
148 if (_contentLength == contentLength) return; | |
149 _contentLength = contentLength; | |
150 if (_contentLength >= 0) { | |
151 if (chunkedTransferEncoding) chunkedTransferEncoding = false; | |
152 _set(HttpHeaders.CONTENT_LENGTH, contentLength.toString()); | |
153 } else { | |
154 removeAll(HttpHeaders.CONTENT_LENGTH); | |
155 if (protocolVersion == "1.1") { | |
156 chunkedTransferEncoding = true; | |
157 } | |
158 } | |
159 } | |
160 | |
161 bool get chunkedTransferEncoding => _chunkedTransferEncoding; | |
162 | |
163 void set chunkedTransferEncoding(bool chunkedTransferEncoding) { | |
164 _checkMutable(); | |
165 if (chunkedTransferEncoding && protocolVersion == "1.0") { | |
166 throw new HttpException( | |
167 "Trying to set 'Transfer-Encoding: Chunked' on HTTP 1.0 headers"); | |
168 } | |
169 if (chunkedTransferEncoding == _chunkedTransferEncoding) return; | |
170 if (chunkedTransferEncoding) { | |
171 List<String> values = _headers[HttpHeaders.TRANSFER_ENCODING]; | |
172 if ((values == null || values.last != "chunked")) { | |
173 // Headers does not specify chunked encoding - add it if set. | |
174 _addValue(HttpHeaders.TRANSFER_ENCODING, "chunked"); | |
175 } | |
176 contentLength = -1; | |
177 } else { | |
178 // Headers does specify chunked encoding - remove it if not set. | |
179 remove(HttpHeaders.TRANSFER_ENCODING, "chunked"); | |
180 } | |
181 _chunkedTransferEncoding = chunkedTransferEncoding; | |
182 } | |
183 | |
184 String get host => _host; | |
185 | |
186 void set host(String host) { | |
187 _checkMutable(); | |
188 _host = host; | |
189 _updateHostHeader(); | |
190 } | |
191 | |
192 int get port => _port; | |
193 | |
194 void set port(int port) { | |
195 _checkMutable(); | |
196 _port = port; | |
197 _updateHostHeader(); | |
198 } | |
199 | |
200 DateTime get ifModifiedSince { | |
201 List<String> values = _headers[HttpHeaders.IF_MODIFIED_SINCE]; | |
202 if (values != null) { | |
203 try { | |
204 return HttpDate.parse(values[0]); | |
205 } on Exception catch (e) { | |
206 return null; | |
207 } | |
208 } | |
209 return null; | |
210 } | |
211 | |
212 void set ifModifiedSince(DateTime ifModifiedSince) { | |
213 _checkMutable(); | |
214 // Format "ifModifiedSince" header with date in Greenwich Mean Time (GMT). | |
215 String formatted = HttpDate.format(ifModifiedSince.toUtc()); | |
216 _set(HttpHeaders.IF_MODIFIED_SINCE, formatted); | |
217 } | |
218 | |
219 DateTime get date { | |
220 List<String> values = _headers[HttpHeaders.DATE]; | |
221 if (values != null) { | |
222 try { | |
223 return HttpDate.parse(values[0]); | |
224 } on Exception catch (e) { | |
225 return null; | |
226 } | |
227 } | |
228 return null; | |
229 } | |
230 | |
231 void set date(DateTime date) { | |
232 _checkMutable(); | |
233 // Format "DateTime" header with date in Greenwich Mean Time (GMT). | |
234 String formatted = HttpDate.format(date.toUtc()); | |
235 _set("date", formatted); | |
236 } | |
237 | |
238 DateTime get expires { | |
239 List<String> values = _headers[HttpHeaders.EXPIRES]; | |
240 if (values != null) { | |
241 try { | |
242 return HttpDate.parse(values[0]); | |
243 } on Exception catch (e) { | |
244 return null; | |
245 } | |
246 } | |
247 return null; | |
248 } | |
249 | |
250 void set expires(DateTime expires) { | |
251 _checkMutable(); | |
252 // Format "Expires" header with date in Greenwich Mean Time (GMT). | |
253 String formatted = HttpDate.format(expires.toUtc()); | |
254 _set(HttpHeaders.EXPIRES, formatted); | |
255 } | |
256 | |
257 ContentType get contentType { | |
258 var values = _headers["content-type"]; | |
259 if (values != null) { | |
260 return ContentType.parse(values[0]); | |
261 } else { | |
262 return null; | |
263 } | |
264 } | |
265 | |
266 void set contentType(ContentType contentType) { | |
267 _checkMutable(); | |
268 _set(HttpHeaders.CONTENT_TYPE, contentType.toString()); | |
269 } | |
270 | |
271 void clear() { | |
272 _checkMutable(); | |
273 _headers.clear(); | |
274 _contentLength = -1; | |
275 _persistentConnection = true; | |
276 _chunkedTransferEncoding = false; | |
277 _host = null; | |
278 _port = null; | |
279 } | |
280 | |
281 // [name] must be a lower-case version of the name. | |
282 void _add(String name, value) { | |
283 assert(name == _validateField(name)); | |
284 // Use the length as index on what method to call. This is notable | |
285 // faster than computing hash and looking up in a hash-map. | |
286 switch (name.length) { | |
287 case 4: | |
288 if (HttpHeaders.DATE == name) { | |
289 _addDate(name, value); | |
290 return; | |
291 } | |
292 if (HttpHeaders.HOST == name) { | |
293 _addHost(name, value); | |
294 return; | |
295 } | |
296 break; | |
297 case 7: | |
298 if (HttpHeaders.EXPIRES == name) { | |
299 _addExpires(name, value); | |
300 return; | |
301 } | |
302 break; | |
303 case 10: | |
304 if (HttpHeaders.CONNECTION == name) { | |
305 _addConnection(name, value); | |
306 return; | |
307 } | |
308 break; | |
309 case 12: | |
310 if (HttpHeaders.CONTENT_TYPE == name) { | |
311 _addContentType(name, value); | |
312 return; | |
313 } | |
314 break; | |
315 case 14: | |
316 if (HttpHeaders.CONTENT_LENGTH == name) { | |
317 _addContentLength(name, value); | |
318 return; | |
319 } | |
320 break; | |
321 case 17: | |
322 if (HttpHeaders.TRANSFER_ENCODING == name) { | |
323 _addTransferEncoding(name, value); | |
324 return; | |
325 } | |
326 if (HttpHeaders.IF_MODIFIED_SINCE == name) { | |
327 _addIfModifiedSince(name, value); | |
328 return; | |
329 } | |
330 } | |
331 _addValue(name, value); | |
332 } | |
333 | |
334 void _addContentLength(String name, value) { | |
335 if (value is int) { | |
336 contentLength = value; | |
337 } else if (value is String) { | |
338 contentLength = int.parse(value); | |
339 } else { | |
340 throw new HttpException("Unexpected type for header named $name"); | |
341 } | |
342 } | |
343 | |
344 void _addTransferEncoding(String name, value) { | |
345 if (value == "chunked") { | |
346 chunkedTransferEncoding = true; | |
347 } else { | |
348 _addValue(HttpHeaders.TRANSFER_ENCODING, value); | |
349 } | |
350 } | |
351 | |
352 void _addDate(String name, value) { | |
353 if (value is DateTime) { | |
354 date = value; | |
355 } else if (value is String) { | |
356 _set(HttpHeaders.DATE, value); | |
357 } else { | |
358 throw new HttpException("Unexpected type for header named $name"); | |
359 } | |
360 } | |
361 | |
362 void _addExpires(String name, value) { | |
363 if (value is DateTime) { | |
364 expires = value; | |
365 } else if (value is String) { | |
366 _set(HttpHeaders.EXPIRES, value); | |
367 } else { | |
368 throw new HttpException("Unexpected type for header named $name"); | |
369 } | |
370 } | |
371 | |
372 void _addIfModifiedSince(String name, value) { | |
373 if (value is DateTime) { | |
374 ifModifiedSince = value; | |
375 } else if (value is String) { | |
376 _set(HttpHeaders.IF_MODIFIED_SINCE, value); | |
377 } else { | |
378 throw new HttpException("Unexpected type for header named $name"); | |
379 } | |
380 } | |
381 | |
382 void _addHost(String name, value) { | |
383 if (value is String) { | |
384 int pos = value.indexOf(":"); | |
385 if (pos == -1) { | |
386 _host = value; | |
387 _port = HttpClient.DEFAULT_HTTP_PORT; | |
388 } else { | |
389 if (pos > 0) { | |
390 _host = value.substring(0, pos); | |
391 } else { | |
392 _host = null; | |
393 } | |
394 if (pos + 1 == value.length) { | |
395 _port = HttpClient.DEFAULT_HTTP_PORT; | |
396 } else { | |
397 try { | |
398 _port = int.parse(value.substring(pos + 1)); | |
399 } on FormatException catch (e) { | |
400 _port = null; | |
401 } | |
402 } | |
403 } | |
404 _set(HttpHeaders.HOST, value); | |
405 } else { | |
406 throw new HttpException("Unexpected type for header named $name"); | |
407 } | |
408 } | |
409 | |
410 void _addConnection(String name, value) { | |
411 var lowerCaseValue = value.toLowerCase(); | |
412 if (lowerCaseValue == 'close') { | |
413 _persistentConnection = false; | |
414 } else if (lowerCaseValue == 'keep-alive') { | |
415 _persistentConnection = true; | |
416 } | |
417 _addValue(name, value); | |
418 } | |
419 | |
420 void _addContentType(String name, value) { | |
421 _set(HttpHeaders.CONTENT_TYPE, value); | |
422 } | |
423 | |
424 void _addValue(String name, Object value) { | |
425 List<String> values = _headers[name]; | |
426 if (values == null) { | |
427 values = new List<String>(); | |
428 _headers[name] = values; | |
429 } | |
430 if (value is DateTime) { | |
431 values.add(HttpDate.format(value)); | |
432 } else if (value is String) { | |
433 values.add(value); | |
434 } else { | |
435 values.add(_validateValue(value.toString())); | |
436 } | |
437 } | |
438 | |
439 void _set(String name, String value) { | |
440 assert(name == _validateField(name)); | |
441 List<String> values = new List<String>(); | |
442 _headers[name] = values; | |
443 values.add(value); | |
444 } | |
445 | |
446 _checkMutable() { | |
447 if (!_mutable) throw new HttpException("HTTP headers are not mutable"); | |
448 } | |
449 | |
450 _updateHostHeader() { | |
451 bool defaultPort = _port == null || _port == _defaultPortForScheme; | |
452 _set("host", defaultPort ? host : "$host:$_port"); | |
453 } | |
454 | |
455 _foldHeader(String name) { | |
456 if (name == HttpHeaders.SET_COOKIE || | |
457 (_noFoldingHeaders != null && | |
458 _noFoldingHeaders.indexOf(name) != -1)) { | |
459 return false; | |
460 } | |
461 return true; | |
462 } | |
463 | |
464 void _finalize() { | |
465 _mutable = false; | |
466 } | |
467 | |
468 int _write(Uint8List buffer, int offset) { | |
469 void write(List<int> bytes) { | |
470 int len = bytes.length; | |
471 for (int i = 0; i < len; i++) { | |
472 buffer[offset + i] = bytes[i]; | |
473 } | |
474 offset += len; | |
475 } | |
476 | |
477 // Format headers. | |
478 for (String name in _headers.keys) { | |
479 List<String> values = _headers[name]; | |
480 bool fold = _foldHeader(name); | |
481 var nameData = name.codeUnits; | |
482 write(nameData); | |
483 buffer[offset++] = _CharCode.COLON; | |
484 buffer[offset++] = _CharCode.SP; | |
485 for (int i = 0; i < values.length; i++) { | |
486 if (i > 0) { | |
487 if (fold) { | |
488 buffer[offset++] = _CharCode.COMMA; | |
489 buffer[offset++] = _CharCode.SP; | |
490 } else { | |
491 buffer[offset++] = _CharCode.CR; | |
492 buffer[offset++] = _CharCode.LF; | |
493 write(nameData); | |
494 buffer[offset++] = _CharCode.COLON; | |
495 buffer[offset++] = _CharCode.SP; | |
496 } | |
497 } | |
498 write(values[i].codeUnits); | |
499 } | |
500 buffer[offset++] = _CharCode.CR; | |
501 buffer[offset++] = _CharCode.LF; | |
502 } | |
503 return offset; | |
504 } | |
505 | |
506 String toString() { | |
507 StringBuffer sb = new StringBuffer(); | |
508 _headers.forEach((String name, List<String> values) { | |
509 sb..write(name)..write(": "); | |
510 bool fold = _foldHeader(name); | |
511 for (int i = 0; i < values.length; i++) { | |
512 if (i > 0) { | |
513 if (fold) { | |
514 sb.write(", "); | |
515 } else { | |
516 sb..write("\n")..write(name)..write(": "); | |
517 } | |
518 } | |
519 sb.write(values[i]); | |
520 } | |
521 sb.write("\n"); | |
522 }); | |
523 return sb.toString(); | |
524 } | |
525 | |
526 List<Cookie> _parseCookies() { | |
527 // Parse a Cookie header value according to the rules in RFC 6265. | |
528 var cookies = new List<Cookie>(); | |
529 void parseCookieString(String s) { | |
530 int index = 0; | |
531 | |
532 bool done() => index == -1 || index == s.length; | |
533 | |
534 void skipWS() { | |
535 while (!done()) { | |
536 if (s[index] != " " && s[index] != "\t") return; | |
537 index++; | |
538 } | |
539 } | |
540 | |
541 String parseName() { | |
542 int start = index; | |
543 while (!done()) { | |
544 if (s[index] == " " || s[index] == "\t" || s[index] == "=") break; | |
545 index++; | |
546 } | |
547 return s.substring(start, index); | |
548 } | |
549 | |
550 String parseValue() { | |
551 int start = index; | |
552 while (!done()) { | |
553 if (s[index] == " " || s[index] == "\t" || s[index] == ";") break; | |
554 index++; | |
555 } | |
556 return s.substring(start, index); | |
557 } | |
558 | |
559 bool expect(String expected) { | |
560 if (done()) return false; | |
561 if (s[index] != expected) return false; | |
562 index++; | |
563 return true; | |
564 } | |
565 | |
566 while (!done()) { | |
567 skipWS(); | |
568 if (done()) return; | |
569 String name = parseName(); | |
570 skipWS(); | |
571 if (!expect("=")) { | |
572 index = s.indexOf(';', index); | |
573 continue; | |
574 } | |
575 skipWS(); | |
576 String value = parseValue(); | |
577 try { | |
578 cookies.add(new _Cookie(name, value)); | |
579 } catch (_) { | |
580 // Skip it, invalid cookie data. | |
581 } | |
582 skipWS(); | |
583 if (done()) return; | |
584 if (!expect(";")) { | |
585 index = s.indexOf(';', index); | |
586 continue; | |
587 } | |
588 } | |
589 } | |
590 List<String> values = _headers[HttpHeaders.COOKIE]; | |
591 if (values != null) { | |
592 values.forEach((headerValue) => parseCookieString(headerValue)); | |
593 } | |
594 return cookies; | |
595 } | |
596 | |
597 static String _validateField(String field) { | |
598 for (var i = 0; i < field.length; i++) { | |
599 if (!_HttpParser._isTokenChar(field.codeUnitAt(i))) { | |
600 throw new FormatException( | |
601 "Invalid HTTP header field name: ${JSON.encode(field)}"); | |
602 } | |
603 } | |
604 return field.toLowerCase(); | |
605 } | |
606 | |
607 static _validateValue(value) { | |
608 if (value is! String) return value; | |
609 for (var i = 0; i < value.length; i++) { | |
610 if (!_HttpParser._isValueChar(value.codeUnitAt(i))) { | |
611 throw new FormatException( | |
612 "Invalid HTTP header field value: ${JSON.encode(value)}"); | |
613 } | |
614 } | |
615 return value; | |
616 } | |
617 } | |
618 | |
619 | |
620 class _HeaderValue implements HeaderValue { | |
621 String _value; | |
622 Map<String, String> _parameters; | |
623 Map<String, String> _unmodifiableParameters; | |
624 | |
625 _HeaderValue([String this._value = "", Map<String, String> parameters]) { | |
626 if (parameters != null) { | |
627 _parameters = new HashMap<String, String>.from(parameters); | |
628 } | |
629 } | |
630 | |
631 static _HeaderValue parse(String value, | |
632 {parameterSeparator: ";", | |
633 valueSeparator: null, | |
634 preserveBackslash: false}) { | |
635 // Parse the string. | |
636 var result = new _HeaderValue(); | |
637 result._parse(value, parameterSeparator, valueSeparator, preserveBackslash); | |
638 return result; | |
639 } | |
640 | |
641 String get value => _value; | |
642 | |
643 void _ensureParameters() { | |
644 if (_parameters == null) { | |
645 _parameters = new HashMap<String, String>(); | |
646 } | |
647 } | |
648 | |
649 Map<String, String> get parameters { | |
650 _ensureParameters(); | |
651 if (_unmodifiableParameters == null) { | |
652 _unmodifiableParameters = new UnmodifiableMapView(_parameters); | |
653 } | |
654 return _unmodifiableParameters; | |
655 } | |
656 | |
657 String toString() { | |
658 StringBuffer sb = new StringBuffer(); | |
659 sb.write(_value); | |
660 if (parameters != null && parameters.length > 0) { | |
661 _parameters.forEach((String name, String value) { | |
662 sb..write("; ")..write(name)..write("=")..write(value); | |
663 }); | |
664 } | |
665 return sb.toString(); | |
666 } | |
667 | |
668 void _parse(String s, | |
669 String parameterSeparator, | |
670 String valueSeparator, | |
671 bool preserveBackslash) { | |
672 int index = 0; | |
673 | |
674 bool done() => index == s.length; | |
675 | |
676 void skipWS() { | |
677 while (!done()) { | |
678 if (s[index] != " " && s[index] != "\t") return; | |
679 index++; | |
680 } | |
681 } | |
682 | |
683 String parseValue() { | |
684 int start = index; | |
685 while (!done()) { | |
686 if (s[index] == " " || | |
687 s[index] == "\t" || | |
688 s[index] == valueSeparator || | |
689 s[index] == parameterSeparator) break; | |
690 index++; | |
691 } | |
692 return s.substring(start, index); | |
693 } | |
694 | |
695 void expect(String expected) { | |
696 if (done() || s[index] != expected) { | |
697 throw new HttpException("Failed to parse header value"); | |
698 } | |
699 index++; | |
700 } | |
701 | |
702 void maybeExpect(String expected) { | |
703 if (s[index] == expected) index++; | |
704 } | |
705 | |
706 void parseParameters() { | |
707 var parameters = new HashMap<String, String>(); | |
708 _parameters = new UnmodifiableMapView(parameters); | |
709 | |
710 String parseParameterName() { | |
711 int start = index; | |
712 while (!done()) { | |
713 if (s[index] == " " || | |
714 s[index] == "\t" || | |
715 s[index] == "=" || | |
716 s[index] == parameterSeparator || | |
717 s[index] == valueSeparator) break; | |
718 index++; | |
719 } | |
720 return s.substring(start, index).toLowerCase(); | |
721 } | |
722 | |
723 String parseParameterValue() { | |
724 if (!done() && s[index] == "\"") { | |
725 // Parse quoted value. | |
726 StringBuffer sb = new StringBuffer(); | |
727 index++; | |
728 while (!done()) { | |
729 if (s[index] == "\\") { | |
730 if (index + 1 == s.length) { | |
731 throw new HttpException("Failed to parse header value"); | |
732 } | |
733 if (preserveBackslash && s[index + 1] != "\"") { | |
734 sb.write(s[index]); | |
735 } | |
736 index++; | |
737 } else if (s[index] == "\"") { | |
738 index++; | |
739 break; | |
740 } | |
741 sb.write(s[index]); | |
742 index++; | |
743 } | |
744 return sb.toString(); | |
745 } else { | |
746 // Parse non-quoted value. | |
747 var val = parseValue(); | |
748 return val == "" ? null : val; | |
749 } | |
750 } | |
751 | |
752 while (!done()) { | |
753 skipWS(); | |
754 if (done()) return; | |
755 String name = parseParameterName(); | |
756 skipWS(); | |
757 if (done()) { | |
758 parameters[name] = null; | |
759 return; | |
760 } | |
761 maybeExpect("="); | |
762 skipWS(); | |
763 if(done()) { | |
764 parameters[name] = null; | |
765 return; | |
766 } | |
767 String value = parseParameterValue(); | |
768 if (name == 'charset' && this is _ContentType) { | |
769 // Charset parameter of ContentTypes are always lower-case. | |
770 value = value.toLowerCase(); | |
771 } | |
772 parameters[name] = value; | |
773 skipWS(); | |
774 if (done()) return; | |
775 // TODO: Implement support for multi-valued parameters. | |
776 if(s[index] == valueSeparator) return; | |
777 expect(parameterSeparator); | |
778 } | |
779 } | |
780 | |
781 skipWS(); | |
782 _value = parseValue(); | |
783 skipWS(); | |
784 if (done()) return; | |
785 maybeExpect(parameterSeparator); | |
786 parseParameters(); | |
787 } | |
788 } | |
789 | |
790 | |
791 class _ContentType extends _HeaderValue implements ContentType { | |
792 String _primaryType = ""; | |
793 String _subType = ""; | |
794 | |
795 _ContentType(String primaryType, | |
796 String subType, | |
797 String charset, | |
798 Map<String, String> parameters) | |
799 : _primaryType = primaryType, _subType = subType, super("") { | |
800 if (_primaryType == null) _primaryType = ""; | |
801 if (_subType == null) _subType = ""; | |
802 _value = "$_primaryType/$_subType"; | |
803 if (parameters != null) { | |
804 _ensureParameters(); | |
805 parameters.forEach((String key, String value) { | |
806 String lowerCaseKey = key.toLowerCase(); | |
807 if (lowerCaseKey == "charset") { | |
808 value = value.toLowerCase(); | |
809 } | |
810 this._parameters[lowerCaseKey] = value; | |
811 }); | |
812 } | |
813 if (charset != null) { | |
814 _ensureParameters(); | |
815 this._parameters["charset"] = charset.toLowerCase(); | |
816 } | |
817 } | |
818 | |
819 _ContentType._(); | |
820 | |
821 static _ContentType parse(String value) { | |
822 var result = new _ContentType._(); | |
823 result._parse(value, ";", null, false); | |
824 int index = result._value.indexOf("/"); | |
825 if (index == -1 || index == (result._value.length - 1)) { | |
826 result._primaryType = result._value.trim().toLowerCase(); | |
827 result._subType = ""; | |
828 } else { | |
829 result._primaryType = | |
830 result._value.substring(0, index).trim().toLowerCase(); | |
831 result._subType = result._value.substring(index + 1).trim().toLowerCase(); | |
832 } | |
833 return result; | |
834 } | |
835 | |
836 String get mimeType => '$primaryType/$subType'; | |
837 | |
838 String get primaryType => _primaryType; | |
839 | |
840 String get subType => _subType; | |
841 | |
842 String get charset => parameters["charset"]; | |
843 } | |
844 | |
845 | |
846 class _Cookie implements Cookie { | |
847 String name; | |
848 String value; | |
849 DateTime expires; | |
850 int maxAge; | |
851 String domain; | |
852 String path; | |
853 bool httpOnly = false; | |
854 bool secure = false; | |
855 | |
856 _Cookie([this.name, this.value]) { | |
857 // Default value of httponly is true. | |
858 httpOnly = true; | |
859 _validate(); | |
860 } | |
861 | |
862 _Cookie.fromSetCookieValue(String value) { | |
863 // Parse the 'set-cookie' header value. | |
864 _parseSetCookieValue(value); | |
865 } | |
866 | |
867 // Parse a 'set-cookie' header value according to the rules in RFC 6265. | |
868 void _parseSetCookieValue(String s) { | |
869 int index = 0; | |
870 | |
871 bool done() => index == s.length; | |
872 | |
873 String parseName() { | |
874 int start = index; | |
875 while (!done()) { | |
876 if (s[index] == "=") break; | |
877 index++; | |
878 } | |
879 return s.substring(start, index).trim(); | |
880 } | |
881 | |
882 String parseValue() { | |
883 int start = index; | |
884 while (!done()) { | |
885 if (s[index] == ";") break; | |
886 index++; | |
887 } | |
888 return s.substring(start, index).trim(); | |
889 } | |
890 | |
891 void expect(String expected) { | |
892 if (done()) throw new HttpException("Failed to parse header value [$s]"); | |
893 if (s[index] != expected) { | |
894 throw new HttpException("Failed to parse header value [$s]"); | |
895 } | |
896 index++; | |
897 } | |
898 | |
899 void parseAttributes() { | |
900 String parseAttributeName() { | |
901 int start = index; | |
902 while (!done()) { | |
903 if (s[index] == "=" || s[index] == ";") break; | |
904 index++; | |
905 } | |
906 return s.substring(start, index).trim().toLowerCase(); | |
907 } | |
908 | |
909 String parseAttributeValue() { | |
910 int start = index; | |
911 while (!done()) { | |
912 if (s[index] == ";") break; | |
913 index++; | |
914 } | |
915 return s.substring(start, index).trim().toLowerCase(); | |
916 } | |
917 | |
918 while (!done()) { | |
919 String name = parseAttributeName(); | |
920 String value = ""; | |
921 if (!done() && s[index] == "=") { | |
922 index++; // Skip the = character. | |
923 value = parseAttributeValue(); | |
924 } | |
925 if (name == "expires") { | |
926 expires = HttpDate._parseCookieDate(value); | |
927 } else if (name == "max-age") { | |
928 maxAge = int.parse(value); | |
929 } else if (name == "domain") { | |
930 domain = value; | |
931 } else if (name == "path") { | |
932 path = value; | |
933 } else if (name == "httponly") { | |
934 httpOnly = true; | |
935 } else if (name == "secure") { | |
936 secure = true; | |
937 } | |
938 if (!done()) index++; // Skip the ; character | |
939 } | |
940 } | |
941 | |
942 name = parseName(); | |
943 if (done() || name.length == 0) { | |
944 throw new HttpException("Failed to parse header value [$s]"); | |
945 } | |
946 index++; // Skip the = character. | |
947 value = parseValue(); | |
948 _validate(); | |
949 if (done()) return; | |
950 index++; // Skip the ; character. | |
951 parseAttributes(); | |
952 } | |
953 | |
954 String toString() { | |
955 StringBuffer sb = new StringBuffer(); | |
956 sb..write(name)..write("=")..write(value); | |
957 if (expires != null) { | |
958 sb..write("; Expires=")..write(HttpDate.format(expires)); | |
959 } | |
960 if (maxAge != null) { | |
961 sb..write("; Max-Age=")..write(maxAge); | |
962 } | |
963 if (domain != null) { | |
964 sb..write("; Domain=")..write(domain); | |
965 } | |
966 if (path != null) { | |
967 sb..write("; Path=")..write(path); | |
968 } | |
969 if (secure) sb.write("; Secure"); | |
970 if (httpOnly) sb.write("; HttpOnly"); | |
971 return sb.toString(); | |
972 } | |
973 | |
974 void _validate() { | |
975 const SEPERATORS = const [ | |
976 "(", ")", "<", ">", "@", ",", ";", ":", "\\", | |
977 '"', "/", "[", "]", "?", "=", "{", "}"]; | |
978 for (int i = 0; i < name.length; i++) { | |
979 int codeUnit = name.codeUnits[i]; | |
980 if (codeUnit <= 32 || | |
981 codeUnit >= 127 || | |
982 SEPERATORS.indexOf(name[i]) >= 0) { | |
983 throw new FormatException( | |
984 "Invalid character in cookie name, code unit: '$codeUnit'"); | |
985 } | |
986 } | |
987 for (int i = 0; i < value.length; i++) { | |
988 int codeUnit = value.codeUnits[i]; | |
989 if (!(codeUnit == 0x21 || | |
990 (codeUnit >= 0x23 && codeUnit <= 0x2B) || | |
991 (codeUnit >= 0x2D && codeUnit <= 0x3A) || | |
992 (codeUnit >= 0x3C && codeUnit <= 0x5B) || | |
993 (codeUnit >= 0x5D && codeUnit <= 0x7E))) { | |
994 throw new FormatException( | |
995 "Invalid character in cookie value, code unit: '$codeUnit'"); | |
996 } | |
997 } | |
998 } | |
999 } | |
OLD | NEW |