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