1 module hunt.http.HttpFields; 2 3 import hunt.http.HttpField; 4 import hunt.http.HttpHeader; 5 import hunt.http.QuotedCSV; 6 7 import hunt.collection; 8 import hunt.Exceptions; 9 import hunt.logging; 10 import hunt.text.Common; 11 import hunt.text.QuotedStringTokenizer; 12 import hunt.util.StringBuilder; 13 import hunt.text.StringTokenizer; 14 import hunt.text.StringUtils; 15 import hunt.util.Common; 16 17 import std.array; 18 import std.algorithm; 19 import std.container.array; 20 import std.conv; 21 import std.datetime; 22 import std.string; 23 import std.range; 24 25 26 /** 27 * HTTP Fields. A collection of HTTP header and or Trailer fields. 28 * 29 * <p> 30 * This class is not synchronized as it is expected that modifications will only 31 * be performed by a single thread. 32 * 33 * <p> 34 * The cookie handling provided by this class is guided by the Servlet 35 * specification and RFC6265. 36 * 37 */ 38 class HttpFields : Iterable!HttpField { 39 // static string __separators = ", \t"; 40 41 private HttpField[] _fields; 42 private int _size; 43 44 /** 45 * Initialize an empty HttpFields. 46 */ 47 this() { 48 _fields = new HttpField[20]; 49 } 50 51 /** 52 * Initialize an empty HttpFields. 53 * 54 * @param capacity 55 * the capacity of the http fields 56 */ 57 this(int capacity) { 58 _fields = new HttpField[capacity]; 59 } 60 61 /** 62 * Initialize HttpFields from copy. 63 * 64 * @param fields 65 * the fields to copy data from 66 */ 67 this(HttpFields fields) { 68 _fields = fields._fields.dup ~ new HttpField[10]; 69 _size = fields.size(); 70 } 71 72 int size() { 73 return _size; 74 } 75 76 InputRange!HttpField iterator() { 77 return inputRangeObject(_fields[0 .. _size]); 78 } 79 80 int opApply(scope int delegate(ref HttpField) dg) { 81 int result = 0; 82 foreach (HttpField v; _fields[0 .. _size]) { 83 result = dg(v); 84 if (result != 0) 85 return result; 86 } 87 return result; 88 } 89 90 int opApply(scope int delegate(string name, string value) dg) { 91 int result = 0; 92 foreach (HttpField v; _fields[0 .. _size]) { 93 result = dg(v.getName(), v.getValue()); 94 if (result != 0) 95 return result; 96 } 97 return result; 98 } 99 100 /** 101 * Get Collection of header names. 102 * 103 * @return the unique set of field names. 104 */ 105 Set!string getFieldNamesCollection() { 106 Set!string set = new HashSet!string(_size); 107 foreach (HttpField f; _fields[0 .. _size]) { 108 if (f !is null) 109 set.add(f.getName()); 110 } 111 return set; 112 } 113 114 /** 115 * Get enumeration of header _names. Returns an enumeration of strings 116 * representing the header _names for this request. 117 * 118 * @return an enumeration of field names 119 */ 120 InputRange!string getFieldNames() { 121 bool[string] set; 122 foreach (HttpField f; _fields[0 .. _size]) { 123 if (f !is null) 124 set[f.getName()] = true; 125 } 126 return inputRangeObject(set.keys); 127 } 128 129 // InputRange!string getFieldNames() { 130 // // return Collections.enumeration(getFieldNamesCollection()); 131 // // return getFieldNamesCollection().toArray(); 132 // Array!string set; 133 // foreach (HttpField f ; _fields[0.._size]) { 134 // if (f !is null) 135 // set.insertBack(f.getName()); 136 // } 137 // // Enumeration!string r = new RangeEnumeration!string(inputRangeObject(set[].array)); 138 // return inputRangeObject(set[].array); 139 // } 140 141 HttpField[] allFields() { 142 return _fields[0 .. _size]; 143 } 144 145 /** 146 * Get a Field by index. 147 * 148 * @param index 149 * the field index 150 * @return A Field value or null if the Field value has not been set 151 */ 152 HttpField getField(int index) { 153 if (index >= _size) 154 throw new NoSuchElementException(""); 155 return _fields[index]; 156 } 157 158 HttpField getField(HttpHeader header) { 159 for (int i = 0; i < _size; i++) { 160 HttpField f = _fields[i]; 161 if (f.getHeader() == header) 162 return f; 163 } 164 return null; 165 } 166 167 HttpField getField(string name) { 168 for (int i = 0; i < _size; i++) { 169 HttpField f = _fields[i]; 170 if (f.getName().equalsIgnoreCase(name)) 171 return f; 172 } 173 return null; 174 } 175 176 bool contains(HttpField field) { 177 for (int i = _size; i-- > 0;) { 178 HttpField f = _fields[i]; 179 if (f.isSameName(field) && (f.opEquals(field) || f.contains(field.getValue()))) 180 return true; 181 } 182 return false; 183 } 184 185 bool contains(HttpHeader header, string value) { 186 for (int i = _size; i-- > 0;) { 187 HttpField f = _fields[i]; 188 if (f.getHeader() == header && f.contains(value)) 189 return true; 190 } 191 return false; 192 } 193 194 bool contains(string name, string value) { 195 for (int i = _size; i-- > 0;) { 196 HttpField f = _fields[i]; 197 if (f.getName().equalsIgnoreCase(name) && f.contains(value)) 198 return true; 199 } 200 return false; 201 } 202 203 bool contains(HttpHeader header) { 204 for (int i = _size; i-- > 0;) { 205 HttpField f = _fields[i]; 206 if (f.getHeader() == header) 207 return true; 208 } 209 return false; 210 } 211 212 bool containsKey(string name) { 213 for (int i = _size; i-- > 0;) { 214 HttpField f = _fields[i]; 215 if (std..string.icmp(f.getName(), name) == 0) 216 return true; 217 } 218 return false; 219 } 220 221 string get(HttpHeader header) { 222 for (int i = 0; i < _size; i++) { 223 HttpField f = _fields[i]; 224 if (f.getHeader() == header) 225 return f.getValue(); 226 } 227 return null; 228 } 229 230 string get(string header) { 231 for (int i = 0; i < _size; i++) { 232 HttpField f = _fields[i]; 233 if (f.getName().equalsIgnoreCase(header)) 234 return f.getValue(); 235 } 236 return null; 237 } 238 239 /** 240 * Get multiple header of the same name 241 * 242 * @return List the values 243 * @param header 244 * the header 245 */ 246 string[] getValuesList(HttpHeader header) { 247 Array!(string) list; 248 foreach (HttpField f; this) { 249 if (f.getHeader() == header) 250 list.insertBack(f.getValue()); 251 } 252 return list.array(); 253 } 254 255 /** 256 * Get multiple header of the same name 257 * 258 * @return List the header values 259 * @param name 260 * the case-insensitive field name 261 */ 262 string[] getValuesList(string name) { 263 Array!(string) list; 264 foreach (HttpField f; this) { 265 if (f.getName().equalsIgnoreCase(name)) 266 list.insertBack(f.getValue()); 267 } 268 return list.array(); 269 } 270 271 /** 272 * Add comma separated values, but only if not already present. 273 * 274 * @param header 275 * The header to add the value(s) to 276 * @param values 277 * The value(s) to add 278 * @return True if headers were modified 279 */ 280 // bool addCSV(HttpHeader header, string... values) { 281 // QuotedCSV existing = null; 282 // for (HttpField f : this) { 283 // if (f.getHeader() == header) { 284 // if (existing == null) 285 // existing = new QuotedCSV(false); 286 // existing.addValue(f.getValue()); 287 // } 288 // } 289 290 // string value = addCSV(existing, values); 291 // if (value != null) { 292 // add(header, value); 293 // return true; 294 // } 295 // return false; 296 // } 297 298 /** 299 * Add comma separated values, but only if not already present. 300 * 301 * @param name 302 * The header to add the value(s) to 303 * @param values 304 * The value(s) to add 305 * @return True if headers were modified 306 */ 307 bool addCSV(string name, string[] values...) { 308 QuotedCSV existing = null; 309 foreach (HttpField f; this) { 310 if (f.getName().equalsIgnoreCase(name)) { 311 if (existing is null) 312 existing = new QuotedCSV(false); 313 existing.addValue(f.getValue()); 314 } 315 } 316 string value = addCSV(existing, values); 317 if (value != null) { 318 add(name, value); 319 return true; 320 } 321 return false; 322 } 323 324 protected string addCSV(QuotedCSV existing, string[] values...) { 325 // remove any existing values from the new values 326 bool add = true; 327 if (existing !is null && !existing.isEmpty()) { 328 add = false; 329 330 for (size_t i = values.length; i-- > 0;) { 331 string unquoted = QuotedCSV.unquote(values[i]); 332 if (existing.getValues().contains(unquoted)) 333 values[i] = null; 334 else 335 add = true; 336 } 337 } 338 339 if (add) { 340 StringBuilder value = new StringBuilder(); 341 foreach (string v; values) { 342 if (v == null) 343 continue; 344 if (value.length > 0) 345 value.append(", "); 346 value.append(v); 347 } 348 if (value.length > 0) 349 return value.toString(); 350 } 351 352 return null; 353 } 354 355 /** 356 * Get multiple field values of the same name, split as a {@link QuotedCSV} 357 * 358 * @return List the values with OWS stripped 359 * @param header 360 * The header 361 * @param keepQuotes 362 * True if the fields are kept quoted 363 */ 364 string[] getCSV(HttpHeader header, bool keepQuotes) { 365 QuotedCSV values = null; 366 foreach (HttpField f; _fields[0 .. _size]) { 367 if (f.getHeader() == header) { 368 if (values is null) 369 values = new QuotedCSV(keepQuotes); 370 values.addValue(f.getValue()); 371 } 372 } 373 // Array!string ar = values.getValues(); 374 // return inputRangeObject(values.getValues()[].array); 375 376 return values is null ? cast(string[]) null : values.getValues().array; 377 } 378 379 /** 380 * Get multiple field values of the same name as a {@link QuotedCSV} 381 * 382 * @return List the values with OWS stripped 383 * @param name 384 * the case-insensitive field name 385 * @param keepQuotes 386 * True if the fields are kept quoted 387 */ 388 List!string getCSV(string name, bool keepQuotes) { 389 QuotedCSV values = null; 390 foreach (HttpField f; _fields[0 .. _size]) { 391 if (f.getName().equalsIgnoreCase(name)) { 392 if (values is null) 393 values = new QuotedCSV(keepQuotes); 394 values.addValue(f.getValue()); 395 } 396 } 397 return values is null ? null : new ArrayList!string(values.getValues().array); 398 // return inputRangeObject(values.getValues()[].array); 399 } 400 401 string[] getCsvAsArray(string name, bool keepQuotes) { 402 QuotedCSV values = null; 403 foreach (HttpField f; _fields[0 .. _size]) { 404 if (f.getName().equalsIgnoreCase(name)) { 405 if (values is null) 406 values = new QuotedCSV(keepQuotes); 407 values.addValue(f.getValue()); 408 } 409 } 410 return values is null ? null : values.getValues().array; 411 // return inputRangeObject(values.getValues()[].array); 412 } 413 414 /** 415 * Get multiple field values of the same name, split and sorted as a 416 * {@link QuotedQualityCSV} 417 * 418 * @return List the values in quality order with the q param and OWS 419 * stripped 420 * @param header 421 * The header 422 */ 423 // List!string getQualityCSV(HttpHeader header) { 424 // QuotedQualityCSV values = null; 425 // for (HttpField f : this) { 426 // if (f.getHeader() == header) { 427 // if (values == null) 428 // values = new QuotedQualityCSV(); 429 // values.addValue(f.getValue()); 430 // } 431 // } 432 433 // return values == null ? Collections.emptyList() : values.getValues(); 434 // } 435 436 /** 437 * Get multiple field values of the same name, split and sorted as a 438 * {@link QuotedQualityCSV} 439 * 440 * @return List the values in quality order with the q param and OWS 441 * stripped 442 * @param name 443 * the case-insensitive field name 444 */ 445 // List!string getQualityCSV(string name) { 446 // QuotedQualityCSV values = null; 447 // for (HttpField f : this) { 448 // if (f.getName().equalsIgnoreCase(name)) { 449 // if (values == null) 450 // values = new QuotedQualityCSV(); 451 // values.addValue(f.getValue()); 452 // } 453 // } 454 // return values == null ? Collections.emptyList() : values.getValues(); 455 // } 456 457 /** 458 * Get multi headers 459 * 460 * @return Enumeration of the values 461 * @param name 462 * the case-insensitive field name 463 */ 464 InputRange!string getValues(string name) { 465 Array!string r; 466 467 for (int i = 0; i < _size; i++) { 468 HttpField f = _fields[i]; 469 if (f.getName().equalsIgnoreCase(name)) { 470 string v = f.getValue(); 471 if (!v.empty) 472 r.insertBack(v); 473 } 474 } 475 476 return inputRangeObject(r[].array); 477 } 478 479 void put(HttpField field) { 480 bool put = false; 481 for (int i = _size; i-- > 0;) { 482 HttpField f = _fields[i]; 483 if (f.isSameName(field)) { 484 if (put) { 485 --_size; 486 _fields[i + 1 .. _size + 1] = _fields[i .. _size]; 487 } else { 488 _fields[i] = field; 489 put = true; 490 } 491 } 492 } 493 if (!put) 494 add(field); 495 } 496 497 /** 498 * Set a field. 499 * 500 * @param name 501 * the name of the field 502 * @param value 503 * the value of the field. If null the field is cleared. 504 */ 505 void put(string name, string value) { 506 if (value == null) 507 remove(name); 508 else 509 put(new HttpField(name, value)); 510 } 511 512 void put(HttpHeader header, HttpHeaderValue value) { 513 put(header, value.toString()); 514 } 515 516 /** 517 * Set a field. 518 * 519 * @param header 520 * the header name of the field 521 * @param value 522 * the value of the field. If null the field is cleared. 523 */ 524 void put(HttpHeader header, string value) { 525 if (value == null) 526 remove(header); 527 else 528 put(new HttpField(header, value)); 529 } 530 531 /** 532 * Set a field. 533 * 534 * @param name 535 * the name of the field 536 * @param list 537 * the List value of the field. If null the field is cleared. 538 */ 539 void put(string name, List!string list) { 540 remove(name); 541 foreach (string v; list) 542 if (!v.empty) 543 add(name, v); 544 } 545 546 /** 547 * Add to or set a field. If the field is allowed to have multiple values, 548 * add will add multiple headers of the same name. 549 * 550 * @param name 551 * the name of the field 552 * @param value 553 * the value of the field. 554 */ 555 void add(string name, string value) { 556 HttpField field = new HttpField(name, value); 557 add(field); 558 } 559 560 void add(HttpHeader header, HttpHeaderValue value) { 561 add(header, value.toString()); 562 } 563 564 /** 565 * Add to or set a field. If the field is allowed to have multiple values, 566 * add will add multiple headers of the same name. 567 * 568 * @param header 569 * the header 570 * @param value 571 * the value of the field. 572 */ 573 void add(HttpHeader header, string value) { 574 if (value.empty) 575 throw new IllegalArgumentException("The value is null for header: " ~ header.toString()); 576 577 HttpField field = new HttpField(header, value); 578 add(field); 579 } 580 581 /** 582 * Remove a field. 583 * 584 * @param name 585 * the field to remove 586 * @return the header that was removed 587 */ 588 HttpField remove(HttpHeader name) { 589 590 HttpField removed = null; 591 for (int i = _size; i-- > 0;) { 592 HttpField f = _fields[i]; 593 if (f.getHeader() == name) { 594 removed = f; 595 --_size; 596 for (int j = i; j < size; j++) 597 _fields[j] = _fields[j + 1]; 598 } 599 } 600 return removed; 601 } 602 603 /** 604 * Remove a field. 605 * 606 * @param name 607 * the field to remove 608 * @return the header that was removed 609 */ 610 HttpField remove(string name) { 611 HttpField removed = null; 612 for (int i = _size; i-- > 0;) { 613 HttpField f = _fields[i]; 614 if (icmp(f.getName(), name) == 0) { 615 removed = f; 616 --_size; 617 for (int j = i; j < size; j++) 618 _fields[j] = _fields[j + 1]; 619 } 620 } 621 return removed; 622 } 623 624 /** 625 * Get a header as an long value. Returns the value of an integer field or 626 * -1 if not found. The case of the field name is ignored. 627 * 628 * @param name 629 * the case-insensitive field name 630 * @return the value of the field as a long 631 * @exception NumberFormatException 632 * If bad long found 633 */ 634 long getLongField(string name) { 635 HttpField field = getField(name); 636 return field is null ? -1L : field.getLongValue(); 637 } 638 639 /** 640 * Get a header as a date value. Returns the value of a date field, or -1 if 641 * not found. The case of the field name is ignored. 642 * 643 * @param name 644 * the case-insensitive field name 645 * @return the value of the field as a number of milliseconds since unix 646 * epoch 647 */ 648 long getDateField(string name) { 649 HttpField field = getField(name); 650 if (field is null) 651 return -1; 652 653 string val = valueParameters(field.getValue(), null); 654 if (val.empty) 655 return -1; 656 657 // TODO: Tasks pending completion -@zxp at 6/21/2018, 10:59:24 AM 658 // 659 long date = SysTime.fromISOExtString(val).stdTime(); // DateParser.parseDate(val); 660 if (date == -1) 661 throw new IllegalArgumentException("Cannot convert date: " ~ val); 662 return date; 663 } 664 665 /** 666 * Sets the value of an long field. 667 * 668 * @param name 669 * the field name 670 * @param value 671 * the field long value 672 */ 673 void putLongField(HttpHeader name, long value) { 674 string v = to!string(value); 675 put(name, v); 676 } 677 678 /** 679 * Sets the value of an long field. 680 * 681 * @param name 682 * the field name 683 * @param value 684 * the field long value 685 */ 686 void putLongField(string name, long value) { 687 string v = to!string(value); 688 put(name, v); 689 } 690 691 /** 692 * Sets the value of a date field. 693 * 694 * @param name 695 * the field name 696 * @param date 697 * the field date value 698 */ 699 void putDateField(HttpHeader name, long date) { 700 // TODO: Tasks pending completion -@zxp at 6/21/2018, 10:42:44 AM 701 // 702 // string d = DateGenerator.formatDate(date); 703 string d = SysTime(date).toISOExtString(); 704 put(name, d); 705 } 706 707 /** 708 * Sets the value of a date field. 709 * 710 * @param name 711 * the field name 712 * @param date 713 * the field date value 714 */ 715 void putDateField(string name, long date) { 716 // TODO: Tasks pending completion -@zxp at 6/21/2018, 11:04:46 AM 717 // 718 // string d = DateGenerator.formatDate(date); 719 string d = SysTime(date).toISOExtString(); 720 put(name, d); 721 } 722 723 /** 724 * Sets the value of a date field. 725 * 726 * @param name 727 * the field name 728 * @param date 729 * the field date value 730 */ 731 void addDateField(string name, long date) { 732 // string d = DateGenerator.formatDate(date); 733 string d = SysTime(date).toISOExtString(); 734 add(name, d); 735 } 736 737 override size_t toHash() @trusted nothrow { 738 int hash = 0; 739 foreach (HttpField field; _fields[0 .. _size]) 740 hash += field.toHash(); 741 return hash; 742 } 743 744 override bool opEquals(Object o) { 745 if (o is this) 746 return true; 747 748 if (!object.opEquals(this, o)) 749 return false; 750 HttpFields that = cast(HttpFields) o; 751 if (that is null) 752 return false; 753 754 // Order is not important, so we cannot rely on List.equals(). 755 if (size() != that.size()) 756 return false; 757 758 foreach (HttpField fi; this) { 759 bool isContinue = false; 760 foreach (HttpField fa; that) { 761 if (fi == fa) { 762 isContinue = true; 763 break; 764 } 765 } 766 if (!isContinue) 767 return false; 768 } 769 return true; 770 } 771 772 override string toString() { 773 try { 774 StringBuilder buffer = new StringBuilder(); 775 foreach (HttpField field; this) { 776 if (field !is null) { 777 string tmp = field.getName(); 778 if (tmp != null) 779 buffer.append(tmp); 780 buffer.append(": "); 781 tmp = field.getValue(); 782 if (tmp != null) 783 buffer.append(tmp); 784 buffer.append("\r\n"); 785 } 786 } 787 buffer.append("\r\n"); 788 return buffer.toString(); 789 } catch (Exception e) { 790 warningf("http fields toString exception", e); 791 return e.toString(); 792 } 793 } 794 795 void clear() { 796 _size = 0; 797 } 798 799 void add(HttpField field) { 800 version(HUNT_HTTP_DEBUG) { 801 // infof("header: %s", field.toString()); 802 } 803 804 if (field !is null) { 805 if (_size == _fields.length) 806 _fields = _fields.dup ~ new HttpField[_size]; 807 _fields[_size++] = field; 808 } 809 } 810 811 void addAll(HttpFields fields) { 812 for (int i = 0; i < fields._size; i++) 813 add(fields._fields[i]); 814 } 815 816 /** 817 * Add fields from another HttpFields instance. Single valued fields are 818 * replaced, while all others are added. 819 * 820 * @param fields 821 * the fields to add 822 */ 823 void add(HttpFields fields) { 824 if (fields is null) 825 return; 826 827 // Enumeration<string> e = fields.getFieldNames(); 828 // while (e.hasMoreElements()) { 829 // string name = e.nextElement(); 830 // Enumeration<string> values = fields.getValues(name); 831 // while (values.hasMoreElements()) 832 // add(name, values.nextElement()); 833 // } 834 835 auto fieldNames = fields.getFieldNames(); 836 foreach (string n; fieldNames) { 837 auto values = fields.getValues(n); 838 foreach (string v; values) 839 add(n, v); 840 } 841 } 842 843 /** 844 * Get field value without parameters. Some field values can have 845 * parameters. This method separates the value from the parameters and 846 * optionally populates a map with the parameters. For example: 847 * 848 * <PRE> 849 * 850 * FieldName : Value ; param1=val1 ; param2=val2 851 * 852 * </PRE> 853 * 854 * @param value 855 * The Field value, possibly with parameters. 856 * @return The value. 857 */ 858 static string stripParameters(string value) { 859 if (value == null) 860 return null; 861 862 int i = cast(int) value.indexOf(';'); 863 if (i < 0) 864 return value; 865 return value.substring(0, i).strip(); 866 } 867 868 /** 869 * Get field value parameters. Some field values can have parameters. This 870 * method separates the value from the parameters and optionally populates a 871 * map with the parameters. For example: 872 * 873 * <PRE> 874 * 875 * FieldName : Value ; param1=val1 ; param2=val2 876 * 877 * </PRE> 878 * 879 * @param value 880 * The Field value, possibly with parameters. 881 * @param parameters 882 * A map to populate with the parameters, or null 883 * @return The value. 884 */ 885 static string valueParameters(string value, Map!(string, string) parameters) { 886 if (value is null) 887 return null; 888 889 int i = cast(int) value.indexOf(';'); 890 if (i < 0) 891 return value; 892 if (parameters is null) 893 return value.substring(0, i).strip(); 894 895 StringTokenizer tok1 = new QuotedStringTokenizer(value.substring(i), ";", false, true); 896 while (tok1.hasMoreTokens()) { 897 string token = tok1.nextToken(); 898 StringTokenizer tok2 = new QuotedStringTokenizer(token, "= "); 899 if (tok2.hasMoreTokens()) { 900 string paramName = tok2.nextToken(); 901 string paramVal = null; 902 if (tok2.hasMoreTokens()) 903 paramVal = tok2.nextToken(); 904 parameters.put(paramName, paramVal); 905 } 906 } 907 908 return value.substring(0, i).strip(); 909 } 910 }