| 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 |