1 module hunt.net.util.HttpURI; 2 3 import hunt.collection.MultiMap; 4 5 import hunt.Exceptions; 6 import hunt.text.Charset; 7 import hunt.text.Common; 8 import hunt.util.StringBuilder; 9 import hunt.util.ConverterUtils; 10 import hunt.net.util.UrlEncoded; 11 12 import std.array; 13 import std.conv; 14 import std.string; 15 16 import hunt.logging.ConsoleLogger; 17 18 19 /** 20 * Http URI. Parse a HTTP URI from a string or byte array. Given a URI 21 * <code>http://user@host:port/path/info;param?query#fragment</code> this class 22 * will split it into the following undecoded optional elements: 23 * <ul> 24 * <li>{@link #getScheme()} - http:</li> 25 * <li>{@link #getAuthority()} - //name@host:port</li> 26 * <li>{@link #getHost()} - host</li> 27 * <li>{@link #getPort()} - port</li> 28 * <li>{@link #getPath()} - /path/info</li> 29 * <li>{@link #getParam()} - param</li> 30 * <li>{@link #getQuery()} - query</li> 31 * <li>{@link #getFragment()} - fragment</li> 32 * </ul> 33 * 34 https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third 35 \___/ \_/ \___/ \______________/ \__/\_______/ \_/ \___/ 36 | | | | | | \_/ | | 37 Scheme User Password Host Port Path | | Fragment 38 \_____________________________/ | Query 39 | Path parameter 40 Authority 41 * <p> 42 * Any parameters will be returned from {@link #getPath()}, but are excluded 43 * from the return value of {@link #getDecodedPath()}. If there are multiple 44 * parameters, the {@link #getParam()} method returns only the last one. 45 * 46 * See_Also: 47 * https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20 48 * https://web.archive.org/web/20151218094722/http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding 49 */ 50 class HttpURI { 51 private enum State { 52 START, HOST_OR_PATH, SCHEME_OR_PATH, HOST, IPV6, PORT, PATH, PARAM, QUERY, FRAGMENT, ASTERISK 53 } 54 55 private string _scheme; 56 private string _userInfo; 57 private string _user; 58 private string _password; 59 private string _host; 60 private int _port; 61 private string _path; 62 private string _param; 63 private string _query; 64 private string _fragment; 65 66 string _uri; 67 string _decodedPath; 68 69 /** 70 * Construct a normalized URI. Port is not set if it is the default port. 71 * 72 * @param scheme 73 * the URI scheme 74 * @param host 75 * the URI hose 76 * @param port 77 * the URI port 78 * @param path 79 * the URI path 80 * @param param 81 * the URI param 82 * @param query 83 * the URI query 84 * @param fragment 85 * the URI fragment 86 * @return the normalized URI 87 */ 88 static HttpURI createHttpURI(string scheme, string host, int port, string path, string param, string query, 89 string fragment) { 90 if (port == 80 && (scheme == "http")) 91 port = 0; 92 if (port == 443 && (scheme == "https")) 93 port = 0; 94 return new HttpURI(scheme, host, port, path, param, query, fragment); 95 } 96 97 this() { 98 } 99 100 this(string scheme, string host, int port, string path, string param, string query, string fragment) { 101 _scheme = scheme; 102 _host = host; 103 _port = port; 104 _path = path; 105 _param = param; 106 _query = query; 107 _fragment = fragment; 108 } 109 110 this(HttpURI uri) { 111 this(uri._scheme, uri._host, uri._port, uri._path, uri._param, uri._query, uri._fragment); 112 _uri = uri._uri; 113 } 114 115 this(string uri) { 116 _port = -1; 117 parse(State.START, uri); 118 } 119 120 // this(URI uri) { 121 // _uri = null; 122 123 // _scheme = uri.getScheme(); 124 // _host = uri.getHost(); 125 // if (_host is null && uri.getRawSchemeSpecificPart().startsWith("//")) 126 // _host = ""; 127 // _port = uri.getPort(); 128 // _user = uri.getUserInfo(); 129 // _path = uri.getRawPath(); 130 131 // _decodedPath = uri.getPath(); 132 // if (_decodedPath !is null) { 133 // int p = _decodedPath.lastIndexOf(';'); 134 // if (p >= 0) 135 // _param = _decodedPath.substring(p + 1); 136 // } 137 // _query = uri.getRawQuery(); 138 // _fragment = uri.getFragment(); 139 140 // _decodedPath = null; 141 // } 142 143 this(string scheme, string host, int port, string pathQuery) { 144 _uri = null; 145 146 _scheme = scheme; 147 _host = host; 148 _port = port; 149 150 parse(State.PATH, pathQuery); 151 152 } 153 154 void parse(string uri) { 155 clear(); 156 _uri = uri; 157 parse(State.START, uri); 158 } 159 160 /** 161 * Parse according to https://tools.ietf.org/html/rfc7230#section-5.3 162 * 163 * @param method 164 * the request method 165 * @param uri 166 * the request uri 167 */ 168 void parseRequestTarget(string method, string uri) { 169 clear(); 170 _uri = uri; 171 172 if (method == "CONNECT") 173 _path = uri; 174 else 175 parse(uri.startsWith("/") ? State.PATH : State.START, uri); 176 } 177 178 void parse(string uri, int offset, int length) { 179 clear(); 180 int end = offset + length; 181 _uri = uri[offset .. end]; 182 parse(State.START, uri); 183 } 184 185 private void parse(State state, string uri) { 186 bool encoded = false; 187 int end = cast(int)uri.length; 188 int mark = 0; 189 int path_mark = 0; 190 char last = '/'; 191 for (int i = 0; i < end; i++) { 192 char c = uri[i]; 193 194 final switch (state) { 195 case State.START: { 196 switch (c) { 197 case '/': 198 mark = i; 199 state = State.HOST_OR_PATH; 200 break; 201 case ';': 202 mark = i + 1; 203 state = State.PARAM; 204 break; 205 case '?': 206 // assume empty path (if seen at start) 207 _path = ""; 208 mark = i + 1; 209 state = State.QUERY; 210 break; 211 case '#': 212 mark = i + 1; 213 state = State.FRAGMENT; 214 break; 215 case '*': 216 _path = "*"; 217 state = State.ASTERISK; 218 break; 219 220 case '.': 221 path_mark = i; 222 state = State.PATH; 223 encoded = true; 224 break; 225 226 default: 227 mark = i; 228 if (_scheme is null) 229 state = State.SCHEME_OR_PATH; 230 else { 231 path_mark = i; 232 state = State.PATH; 233 } 234 break; 235 } 236 237 continue; 238 } 239 240 case State.SCHEME_OR_PATH: { 241 switch (c) { 242 case ':': 243 // must have been a scheme 244 _scheme = uri[mark .. i]; 245 // Start again with scheme set 246 state = State.START; 247 break; 248 249 case '/': 250 // must have been in a path and still are 251 state = State.PATH; 252 break; 253 254 case ';': 255 // must have been in a path 256 mark = i + 1; 257 state = State.PARAM; 258 break; 259 260 case '?': 261 // must have been in a path 262 _path = uri[mark .. i]; 263 mark = i + 1; 264 state = State.QUERY; 265 break; 266 267 case '%': 268 // must have be in an encoded path 269 encoded = true; 270 state = State.PATH; 271 break; 272 273 case '#': 274 // must have been in a path 275 _path = uri[mark .. i]; 276 state = State.FRAGMENT; 277 break; 278 279 default: 280 break; 281 } 282 continue; 283 } 284 285 case State.HOST_OR_PATH: { 286 switch (c) { 287 case '/': 288 _host = ""; 289 mark = i + 1; 290 state = State.HOST; 291 break; 292 293 case '@': 294 case ';': 295 case '?': 296 case '#': 297 // was a path, look again 298 i--; 299 path_mark = mark; 300 state = State.PATH; 301 break; 302 303 case '.': 304 // it is a path 305 encoded = true; 306 path_mark = mark; 307 state = State.PATH; 308 break; 309 310 default: 311 // it is a path 312 path_mark = mark; 313 state = State.PATH; 314 } 315 continue; 316 } 317 318 case State.HOST: { 319 switch (c) { 320 case '/': 321 _host = uri[mark .. i]; 322 path_mark = mark = i; 323 state = State.PATH; 324 break; 325 case ':': 326 if (i > mark) 327 _host = uri[mark .. i]; 328 mark = i + 1; 329 state = State.PORT; 330 break; 331 case '@': 332 if (!_userInfo.empty()) 333 throw new IllegalArgumentException("Bad authority"); 334 _userInfo = uri[mark .. i]; 335 string[] parts = _userInfo.split(":"); 336 if(parts.length>0) 337 _user = parts[0]; 338 if(parts.length>1) 339 _password = parts[1]; 340 mark = i + 1; 341 break; 342 343 case '[': 344 state = State.IPV6; 345 break; 346 347 default: 348 break; 349 } 350 break; 351 } 352 353 case State.IPV6: { 354 switch (c) { 355 case '/': 356 throw new IllegalArgumentException("No closing ']' for ipv6 in " ~ uri); 357 case ']': 358 c = uri.charAt(++i); 359 _host = uri[mark .. i]; 360 if (c == ':') { 361 mark = i + 1; 362 state = State.PORT; 363 } else { 364 path_mark = mark = i; 365 state = State.PATH; 366 } 367 break; 368 369 default: 370 break; 371 } 372 373 break; 374 } 375 376 case State.PORT: { 377 if (c == '@') { 378 if (_userInfo !is null) 379 throw new IllegalArgumentException("Bad authority"); 380 // It wasn't a port, but a password! 381 _userInfo = _host ~ ":" ~ uri[mark .. i]; 382 string[] parts = _userInfo.split(":"); 383 if(parts.length>0) 384 _user = parts[0]; 385 if(parts.length>1) 386 _password = parts[1]; 387 388 mark = i + 1; 389 state = State.HOST; 390 } else if (c == '/') { 391 // _port = ConverterUtils.parseInt(uri, mark, i - mark, 10); 392 _port = to!int(uri[mark .. i], 10); 393 path_mark = mark = i; 394 state = State.PATH; 395 } 396 break; 397 } 398 399 case State.PATH: { 400 switch (c) { 401 case ';': 402 mark = i + 1; 403 state = State.PARAM; 404 break; 405 case '?': 406 _path = uri[path_mark .. i]; 407 mark = i + 1; 408 state = State.QUERY; 409 break; 410 case '#': 411 _path = uri[path_mark .. i]; 412 mark = i + 1; 413 state = State.FRAGMENT; 414 break; 415 case '%': 416 encoded = true; 417 break; 418 case '.': 419 if ('/' == last) 420 encoded = true; 421 break; 422 423 default: 424 break; 425 } 426 break; 427 } 428 429 case State.PARAM: { 430 switch (c) { 431 case '?': 432 _path = uri[path_mark .. i]; 433 _param = uri[mark .. i]; 434 mark = i + 1; 435 state = State.QUERY; 436 break; 437 case '#': 438 _path = uri[path_mark .. i]; 439 _param = uri[mark .. i]; 440 mark = i + 1; 441 state = State.FRAGMENT; 442 break; 443 case '/': 444 encoded = true; 445 // ignore internal params 446 state = State.PATH; 447 break; 448 case ';': 449 // multiple parameters 450 mark = i + 1; 451 break; 452 453 default: 454 break; 455 } 456 break; 457 } 458 459 case State.QUERY: { 460 if (c == '#') { 461 _query = uri[mark .. i]; 462 mark = i + 1; 463 state = State.FRAGMENT; 464 } 465 break; 466 } 467 468 case State.ASTERISK: { 469 throw new IllegalArgumentException("Bad character '*'"); 470 } 471 472 case State.FRAGMENT: { 473 _fragment = uri[mark .. end]; 474 i = end; 475 break; 476 } 477 } 478 last = c; 479 } 480 481 final switch (state) { 482 case State.START: 483 break; 484 case State.SCHEME_OR_PATH: 485 _path = uri[mark .. end]; 486 break; 487 488 case State.HOST_OR_PATH: 489 _path = uri[mark .. end]; 490 break; 491 492 case State.HOST: 493 if (end > mark) 494 _host = uri[mark .. end]; 495 break; 496 497 case State.IPV6: 498 throw new IllegalArgumentException("No closing ']' for ipv6 in " ~ uri); 499 500 case State.PORT: 501 // _port = ConverterUtils.parseInt(uri, mark, end - mark, 10); 502 _port = to!int(uri[mark .. end], 10); 503 break; 504 505 case State.ASTERISK: 506 break; 507 508 case State.FRAGMENT: 509 _fragment = uri[mark .. end]; 510 break; 511 512 case State.PARAM: 513 _path = uri[path_mark .. end]; 514 _param = uri[mark .. end]; 515 break; 516 517 case State.PATH: 518 _path = uri[path_mark .. end]; 519 break; 520 521 case State.QUERY: 522 _query = uri[mark .. end]; 523 break; 524 } 525 526 if (!encoded) { 527 if (_param is null) 528 _decodedPath = _path; 529 else 530 _decodedPath = _path[0 .. _path.length - _param.length - 1]; 531 } 532 } 533 534 string getScheme() { 535 return _scheme; 536 } 537 538 string getHost() { 539 // Return null for empty host to retain compatibility with java.net.URI 540 if (_host !is null && _host.length == 0) 541 return null; 542 return _host; 543 } 544 545 int getPort() { 546 return _port; 547 } 548 549 /** 550 * The parsed Path. 551 * 552 * @return the path as parsed on valid URI. null for invalid URI. 553 */ 554 string getPath() { 555 return _path; 556 } 557 558 string getDecodedPath() { 559 if (_decodedPath.empty && !_path.empty) 560 _decodedPath = URIUtils.canonicalPath(URIUtils.decodePath(_path)); 561 return _decodedPath; 562 } 563 564 string getParam() { 565 return _param; 566 } 567 568 string getQuery() { 569 return _query; 570 } 571 572 bool hasQuery() { 573 return _query !is null && _query.length > 0; 574 } 575 576 string getFragment() { 577 return _fragment; 578 } 579 580 void decodeQueryTo(MultiMap!string parameters, string encoding = StandardCharsets.UTF_8) { 581 if (_query == _fragment) 582 return; 583 584 UrlEncoded.decodeTo(_query, parameters, encoding); 585 } 586 587 void clear() { 588 _uri = null; 589 590 _scheme = null; 591 _host = null; 592 _port = -1; 593 _path = null; 594 _param = null; 595 _query = null; 596 _fragment = null; 597 598 _decodedPath = null; 599 } 600 601 bool isAbsolute() { 602 return _scheme !is null && _scheme.length > 0; 603 } 604 605 override 606 string toString() { 607 if (_uri is null) { 608 StringBuilder ot = new StringBuilder(); 609 610 if (_scheme !is null) 611 ot.append(_scheme).append(':'); 612 613 if (_host !is null) { 614 ot.append("//"); 615 if (_userInfo !is null) 616 ot.append(_userInfo).append('@'); 617 ot.append(_host); 618 } 619 620 if (_port > 0) 621 ot.append(':').append(_port); 622 623 if (_path !is null) 624 ot.append(_path); 625 626 if (_query !is null) 627 ot.append('?').append(_query); 628 629 if (_fragment !is null) 630 ot.append('#').append(_fragment); 631 632 if (ot.length > 0) 633 _uri = ot.toString(); 634 else 635 _uri = ""; 636 } 637 return _uri; 638 } 639 640 bool equals(Object o) { 641 if (o is this) 642 return true; 643 if (!(typeid(o) == typeid(HttpURI))) 644 return false; 645 return toString().equals(o.toString()); 646 } 647 648 void setScheme(string scheme) { 649 _scheme = scheme; 650 _uri = null; 651 } 652 653 /** 654 * @param host 655 * the host 656 * @param port 657 * the port 658 */ 659 void setAuthority(string host, int port) { 660 _host = host; 661 _port = port; 662 _uri = null; 663 } 664 665 /** 666 * @param path 667 * the path 668 */ 669 void setPath(string path) { 670 _uri = null; 671 _path = path; 672 _decodedPath = null; 673 } 674 675 /** 676 * @param path 677 * the decoded path 678 */ 679 // void setDecodedPath(string path) { 680 // _uri = null; 681 // _path = URIUtils.encodePath(path); 682 // _decodedPath = path; 683 // } 684 685 void setPathQuery(string path) { 686 _uri = null; 687 _path = null; 688 _decodedPath = null; 689 _param = null; 690 _fragment = null; 691 if (path !is null) 692 parse(State.PATH, path); 693 } 694 695 void setQuery(string query) { 696 _query = query; 697 _uri = null; 698 } 699 700 // URI toURI() { 701 // return new URI(_scheme, null, _host, _port, _path, _query is null ? null : UrlEncoded.decodestring(_query), 702 // _fragment); 703 // } 704 705 string getPathQuery() { 706 if (_query is null) 707 return _path; 708 return _path ~ "?" ~ _query; 709 } 710 711 bool hasAuthority() { 712 return _host !is null; 713 } 714 715 string getAuthority() { 716 if (_port > 0) 717 return _host ~ ":" ~ to!string(_port); 718 return _host; 719 } 720 721 string getUserInfo() { 722 return _userInfo; 723 } 724 725 string getUser() { 726 return _user; 727 } 728 729 string getPassword() { 730 return _password; 731 } 732 733 } 734 735 736 /** 737 * Parse an authority string into Host and Port 738 * <p>Parse a string in the form "host:port", handling IPv4 an IPv6 hosts</p> 739 * 740 */ 741 class URIUtils 742 { 743 /* ------------------------------------------------------------ */ 744 /* Decode a URI path and strip parameters 745 */ 746 static string decodePath(string path) { 747 return decodePath(path, 0, cast(int)path.length); 748 } 749 750 /* ------------------------------------------------------------ */ 751 /* Decode a URI path and strip parameters of UTF-8 path 752 */ 753 static string decodePath(string path, int offset, int length) { 754 try { 755 StringBuilder builder = null; 756 757 int end = offset + length; 758 for (int i = offset; i < end; i++) { 759 char c = path[i]; 760 switch (c) { 761 case '%': 762 if (builder is null) { 763 builder = new StringBuilder(path.length); 764 builder.append(path, offset, i - offset); 765 } 766 if ((i + 2) < end) { 767 char u = path.charAt(i + 1); 768 if (u == 'u') { 769 // TODO this is wrong. This is a codepoint not a char 770 builder.append(cast(char) (0xffff & ConverterUtils.parseInt(path, i + 2, 4, 16))); 771 i += 5; 772 } else { 773 builder.append(cast(byte) (0xff & (ConverterUtils.convertHexDigit(u) * 16 + 774 ConverterUtils.convertHexDigit(path.charAt(i + 2))))); 775 i += 2; 776 } 777 } else { 778 throw new IllegalArgumentException("Bad URI % encoding"); 779 } 780 781 break; 782 783 case ';': 784 if (builder is null) { 785 builder = new StringBuilder(path.length); 786 builder.append(path, offset, i - offset); 787 } 788 789 while (++i < end) { 790 if (path[i] == '/') { 791 builder.append('/'); 792 break; 793 } 794 } 795 796 break; 797 798 default: 799 if (builder !is null) 800 builder.append(c); 801 break; 802 } 803 } 804 805 if (builder !is null) 806 return builder.toString(); 807 if (offset == 0 && length == path.length) 808 return path; 809 return path[offset .. end]; 810 } catch (Exception e) { 811 // System.err.println(path.substring(offset, offset + length) ~ " " ~ e); 812 error(e.toString); 813 return decodeISO88591Path(path, offset, length); 814 } 815 } 816 817 818 /* ------------------------------------------------------------ */ 819 /* Decode a URI path and strip parameters of ISO-8859-1 path 820 */ 821 private static string decodeISO88591Path(string path, int offset, int length) { 822 StringBuilder builder = null; 823 int end = offset + length; 824 for (int i = offset; i < end; i++) { 825 char c = path[i]; 826 switch (c) { 827 case '%': 828 if (builder is null) { 829 builder = new StringBuilder(path.length); 830 builder.append(path, offset, i - offset); 831 } 832 if ((i + 2) < end) { 833 char u = path.charAt(i + 1); 834 if (u == 'u') { 835 // TODO this is wrong. This is a codepoint not a char 836 builder.append(cast(char) (0xffff & ConverterUtils.parseInt(path, i + 2, 4, 16))); 837 i += 5; 838 } else { 839 builder.append(cast(byte) (0xff & (ConverterUtils.convertHexDigit(u) * 16 + ConverterUtils.convertHexDigit(path.charAt(i + 2))))); 840 i += 2; 841 } 842 } else { 843 throw new IllegalArgumentException(""); 844 } 845 846 break; 847 848 case ';': 849 if (builder is null) { 850 builder = new StringBuilder(path.length); 851 builder.append(path, offset, i - offset); 852 } 853 while (++i < end) { 854 if (path[i] == '/') { 855 builder.append('/'); 856 break; 857 } 858 } 859 break; 860 861 862 default: 863 if (builder !is null) 864 builder.append(c); 865 break; 866 } 867 } 868 869 if (builder !is null) 870 return builder.toString(); 871 if (offset == 0 && length == path.length) 872 return path; 873 return path[offset .. end]; 874 } 875 876 /* ------------------------------------------------------------ */ 877 878 /** 879 * Convert a decoded path to a canonical form. 880 * <p> 881 * All instances of "." and ".." are factored out. 882 * </p> 883 * <p> 884 * Null is returned if the path tries to .. above its root. 885 * </p> 886 * 887 * @param path the path to convert, decoded, with path separators '/' and no queries. 888 * @return the canonical path, or null if path traversal above root. 889 */ 890 static string canonicalPath(string path) { 891 if (path.empty) 892 return path; 893 894 bool slash = true; 895 int end = cast(int)path.length; 896 int i = 0; 897 898 loop: 899 while (i < end) { 900 char c = path[i]; 901 switch (c) { 902 case '/': 903 slash = true; 904 break; 905 906 case '.': 907 if (slash) 908 break loop; 909 slash = false; 910 break; 911 912 default: 913 slash = false; 914 } 915 916 i++; 917 } 918 919 if (i == end) 920 return path; 921 922 StringBuilder canonical = new StringBuilder(path.length); 923 canonical.append(path, 0, i); 924 925 int dots = 1; 926 i++; 927 while (i <= end) { 928 char c = i < end ? path[i] : '\0'; 929 switch (c) { 930 case '\0': 931 case '/': 932 switch (dots) { 933 case 0: 934 if (c != '\0') 935 canonical.append(c); 936 break; 937 938 case 1: 939 break; 940 941 case 2: 942 if (canonical.length < 2) 943 return null; 944 canonical.setLength(canonical.length - 1); 945 canonical.setLength(canonical.lastIndexOf("/") + 1); 946 break; 947 948 default: 949 while (dots-- > 0) 950 canonical.append('.'); 951 if (c != '\0') 952 canonical.append(c); 953 } 954 955 slash = true; 956 dots = 0; 957 break; 958 959 case '.': 960 if (dots > 0) 961 dots++; 962 else if (slash) 963 dots = 1; 964 else 965 canonical.append('.'); 966 slash = false; 967 break; 968 969 default: 970 while (dots-- > 0) 971 canonical.append('.'); 972 canonical.append(c); 973 dots = 0; 974 slash = false; 975 } 976 977 i++; 978 } 979 return canonical.toString(); 980 } 981 982 983 /* ------------------------------------------------------------ */ 984 985 /** 986 * Convert a path to a cananonical form. 987 * <p> 988 * All instances of "." and ".." are factored out. 989 * </p> 990 * <p> 991 * Null is returned if the path tries to .. above its root. 992 * </p> 993 * 994 * @param path the path to convert (expects URI/URL form, encoded, and with path separators '/') 995 * @return the canonical path, or null if path traversal above root. 996 */ 997 static string canonicalEncodedPath(string path) { 998 if (path.empty) 999 return path; 1000 1001 bool slash = true; 1002 int end = cast(int)path.length; 1003 int i = 0; 1004 1005 loop: 1006 while (i < end) { 1007 char c = path[i]; 1008 switch (c) { 1009 case '/': 1010 slash = true; 1011 break; 1012 1013 case '.': 1014 if (slash) 1015 break loop; 1016 slash = false; 1017 break; 1018 1019 case '?': 1020 return path; 1021 1022 default: 1023 slash = false; 1024 } 1025 1026 i++; 1027 } 1028 1029 if (i == end) 1030 return path; 1031 1032 StringBuilder canonical = new StringBuilder(path.length); 1033 canonical.append(path, 0, i); 1034 1035 int dots = 1; 1036 i++; 1037 while (i <= end) { 1038 char c = i < end ? path[i] : '\0'; 1039 switch (c) { 1040 case '\0': 1041 case '/': 1042 case '?': 1043 switch (dots) { 1044 case 0: 1045 if (c != '\0') 1046 canonical.append(c); 1047 break; 1048 1049 case 1: 1050 if (c == '?') 1051 canonical.append(c); 1052 break; 1053 1054 case 2: 1055 if (canonical.length < 2) 1056 return null; 1057 canonical.setLength(canonical.length - 1); 1058 canonical.setLength(canonical.lastIndexOf("/") + 1); 1059 if (c == '?') 1060 canonical.append(c); 1061 break; 1062 default: 1063 while (dots-- > 0) 1064 canonical.append('.'); 1065 if (c != '\0') 1066 canonical.append(c); 1067 } 1068 1069 slash = true; 1070 dots = 0; 1071 break; 1072 1073 case '.': 1074 if (dots > 0) 1075 dots++; 1076 else if (slash) 1077 dots = 1; 1078 else 1079 canonical.append('.'); 1080 slash = false; 1081 break; 1082 1083 default: 1084 while (dots-- > 0) 1085 canonical.append('.'); 1086 canonical.append(c); 1087 dots = 0; 1088 slash = false; 1089 } 1090 1091 i++; 1092 } 1093 return canonical.toString(); 1094 } 1095 1096 1097 1098 /* ------------------------------------------------------------ */ 1099 1100 /** 1101 * Convert a path to a compact form. 1102 * All instances of "//" and "///" etc. are factored out to single "/" 1103 * 1104 * @param path the path to compact 1105 * @return the compacted path 1106 */ 1107 static string compactPath(string path) { 1108 if (path is null || path.length == 0) 1109 return path; 1110 1111 int state = 0; 1112 int end = cast(int)path.length; 1113 int i = 0; 1114 1115 loop: 1116 while (i < end) { 1117 char c = path[i]; 1118 switch (c) { 1119 case '?': 1120 return path; 1121 case '/': 1122 state++; 1123 if (state == 2) 1124 break loop; 1125 break; 1126 default: 1127 state = 0; 1128 } 1129 i++; 1130 } 1131 1132 if (state < 2) 1133 return path; 1134 1135 StringBuilder buf = new StringBuilder(path.length); 1136 buf.append(path, 0, i); 1137 1138 loop2: 1139 while (i < end) { 1140 char c = path[i]; 1141 switch (c) { 1142 case '?': 1143 buf.append(path, i, end); 1144 break loop2; 1145 case '/': 1146 if (state++ == 0) 1147 buf.append(c); 1148 break; 1149 default: 1150 state = 0; 1151 buf.append(c); 1152 } 1153 i++; 1154 } 1155 1156 return buf.toString(); 1157 } 1158 1159 /* ------------------------------------------------------------ */ 1160 1161 /** 1162 * @param uri URI 1163 * @return True if the uri has a scheme 1164 */ 1165 static bool hasScheme(string uri) { 1166 for (int i = 0; i < uri.length; i++) { 1167 char c = uri[i]; 1168 if (c == ':') 1169 return true; 1170 if (!(c >= 'a' && c <= 'z' || 1171 c >= 'A' && c <= 'Z' || 1172 (i > 0 && (c >= '0' && c <= '9' || 1173 c == '.' || 1174 c == '+' || 1175 c == '-')) 1176 )) 1177 break; 1178 } 1179 return false; 1180 } 1181 } 1182 1183 1184 1185 1186 /** 1187 * A mapping from schemes to their default ports. 1188 * 1189 * This is not exhaustive. Not all schemes use ports. Not all schemes uniquely identify a port to 1190 * use even if they use ports. Entries here should be treated as best guesses. 1191 */ 1192 enum ushort[string] SchemePortMap = [ 1193 "aaa": 3868, 1194 "aaas": 5658, 1195 "acap": 674, 1196 "amqp": 5672, 1197 "cap": 1026, 1198 "coap": 5683, 1199 "coaps": 5684, 1200 "dav": 443, 1201 "dict": 2628, 1202 "ftp": 21, 1203 "git": 9418, 1204 "go": 1096, 1205 "gopher": 70, 1206 "http": 80, 1207 "https": 443, 1208 "ws": 80, 1209 "wss": 443, 1210 "iac": 4569, 1211 "icap": 1344, 1212 "imap": 143, 1213 "ipp": 631, 1214 "ipps": 631, // yes, they're both mapped to port 631 1215 "irc": 6667, // De facto default port, not the IANA reserved port. 1216 "ircs": 6697, 1217 "iris": 702, // defaults to iris.beep 1218 "iris.beep": 702, 1219 "iris.lwz": 715, 1220 "iris.xpc": 713, 1221 "iris.xpcs": 714, 1222 "jabber": 5222, // client-to-server 1223 "ldap": 389, 1224 "ldaps": 636, 1225 "msrp": 2855, 1226 "msrps": 2855, 1227 "mtqp": 1038, 1228 "mupdate": 3905, 1229 "news": 119, 1230 "nfs": 2049, 1231 "pop": 110, 1232 "redis": 6379, 1233 "reload": 6084, 1234 "rsync": 873, 1235 "rtmfp": 1935, 1236 "rtsp": 554, 1237 "shttp": 80, 1238 "sieve": 4190, 1239 "sip": 5060, 1240 "sips": 5061, 1241 "smb": 445, 1242 "smtp": 25, 1243 "snews": 563, 1244 "snmp": 161, 1245 "soap.beep": 605, 1246 "ssh": 22, 1247 "stun": 3478, 1248 "stuns": 5349, 1249 "svn": 3690, 1250 "teamspeak": 9987, 1251 "telnet": 23, 1252 "tftp": 69, 1253 "tip": 3372, 1254 "mysql": 3306, 1255 "postgresql": 5432, 1256 ];