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 |