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