001/* 002 * Copyright 2007-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2017 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk; 022 023 024 025import java.io.Serializable; 026import java.nio.ByteBuffer; 027import java.util.ArrayList; 028 029import com.unboundid.util.NotMutable; 030import com.unboundid.util.ThreadSafety; 031import com.unboundid.util.ThreadSafetyLevel; 032 033import static com.unboundid.ldap.sdk.LDAPMessages.*; 034import static com.unboundid.util.Debug.*; 035import static com.unboundid.util.StaticUtils.*; 036import static com.unboundid.util.Validator.*; 037 038 039 040/** 041 * This class provides a data structure for interacting with LDAP URLs. It may 042 * be used to encode and decode URLs, as well as access the various elements 043 * that they contain. Note that this implementation currently does not support 044 * the use of extensions in an LDAP URL. 045 * <BR><BR> 046 * The components that may be included in an LDAP URL include: 047 * <UL> 048 * <LI>Scheme -- This specifies the protocol to use when communicating with 049 * the server. The official LDAP URL specification only allows a scheme 050 * of "{@code ldap}", but this implementation also supports the use of the 051 * "{@code ldaps}" scheme to indicate that clients should attempt to 052 * perform SSL-based communication with the target server (LDAPS) rather 053 * than unencrypted LDAP. It will also accept "{@code ldapi}", which is 054 * LDAP over UNIX domain sockets, although the LDAP SDK does not directly 055 * support that mechanism of communication.</LI> 056 * <LI>Host -- This specifies the address of the directory server to which the 057 * URL refers. If no host is provided, then it is expected that the 058 * client has some prior knowledge of the host (it often implies the same 059 * server from which the URL was retrieved).</LI> 060 * <LI>Port -- This specifies the port of the directory server to which the 061 * URL refers. If no host or port is provided, then it is assumed that 062 * the client has some prior knowledge of the instance to use (it often 063 * implies the same instance from which the URL was retrieved). If a host 064 * is provided without a port, then it should be assumed that the standard 065 * LDAP port of 389 should be used (or the standard LDAPS port of 636 if 066 * the scheme is "{@code ldaps}", or a value of 0 if the scheme is 067 * "{@code ldapi}").</LI> 068 * <LI>Base DN -- This specifies the base DN for the URL. If no base DN is 069 * provided, then a default of the null DN should be assumed.</LI> 070 * <LI>Requested attributes -- This specifies the set of requested attributes 071 * for the URL. If no attributes are specified, then the behavior should 072 * be the same as if no attributes had been provided for a search request 073 * (i.e., all user attributes should be included). 074 * <BR><BR> 075 * In the string representation of an LDAP URL, the names of the requested 076 * attributes (if more than one is provided) should be separated by 077 * commas.</LI> 078 * <LI>Scope -- This specifies the scope for the URL. It should be one of the 079 * standard scope values as defined in the {@link SearchRequest} 080 * class. If no scope is provided, then it should be assumed that a 081 * scope of {@link SearchScope#BASE} should be used. 082 * <BR><BR> 083 * In the string representation, the names of the scope values that are 084 * allowed include: 085 * <UL> 086 * <LI>base -- Equivalent to {@link SearchScope#BASE}.</LI> 087 * <LI>one -- Equivalent to {@link SearchScope#ONE}.</LI> 088 * <LI>sub -- Equivalent to {@link SearchScope#SUB}.</LI> 089 * <LI>subordinates -- Equivalent to 090 * {@link SearchScope#SUBORDINATE_SUBTREE}.</LI> 091 * </UL></LI> 092 * <LI>Filter -- This specifies the filter for the URL. If no filter is 093 * provided, then a default of "{@code (objectClass=*)}" should be 094 * assumed.</LI> 095 * </UL> 096 * An LDAP URL encapsulates many of the properties of a search request, and in 097 * fact the {@link LDAPURL#toSearchRequest} method may be used to create a 098 * {@link SearchRequest} object from an LDAP URL. 099 * <BR><BR> 100 * See <A HREF="http://www.ietf.org/rfc/rfc4516.txt">RFC 4516</A> for a complete 101 * description of the LDAP URL syntax. Some examples of LDAP URLs include: 102 * <UL> 103 * <LI>{@code ldap://} -- This is the smallest possible LDAP URL that can be 104 * represented. The default values will be used for all components other 105 * than the scheme.</LI> 106 * <LI>{@code 107 * ldap://server.example.com:1234/dc=example,dc=com?cn,sn?sub?(uid=john)} 108 * -- This is an example of a URL containing all of the elements. The 109 * scheme is "{@code ldap}", the host is "{@code server.example.com}", 110 * the port is "{@code 1234}", the base DN is "{@code dc=example,dc=com}", 111 * the requested attributes are "{@code cn}" and "{@code sn}", the scope 112 * is "{@code sub}" (which indicates a subtree scope equivalent to 113 * {@link SearchScope#SUB}), and a filter of 114 * "{@code (uid=john)}".</LI> 115 * </UL> 116 */ 117@NotMutable() 118@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 119public final class LDAPURL 120 implements Serializable 121{ 122 /** 123 * The default filter that will be used if none is provided. 124 */ 125 private static final Filter DEFAULT_FILTER = 126 Filter.createPresenceFilter("objectClass"); 127 128 129 130 /** 131 * The default port number that will be used for LDAP URLs if none is 132 * provided. 133 */ 134 public static final int DEFAULT_LDAP_PORT = 389; 135 136 137 138 /** 139 * The default port number that will be used for LDAPS URLs if none is 140 * provided. 141 */ 142 public static final int DEFAULT_LDAPS_PORT = 636; 143 144 145 146 /** 147 * The default port number that will be used for LDAPI URLs if none is 148 * provided. 149 */ 150 public static final int DEFAULT_LDAPI_PORT = 0; 151 152 153 154 /** 155 * The default scope that will be used if none is provided. 156 */ 157 private static final SearchScope DEFAULT_SCOPE = SearchScope.BASE; 158 159 160 161 /** 162 * The default base DN that will be used if none is provided. 163 */ 164 private static final DN DEFAULT_BASE_DN = DN.NULL_DN; 165 166 167 168 /** 169 * The default set of attributes that will be used if none is provided. 170 */ 171 private static final String[] DEFAULT_ATTRIBUTES = NO_STRINGS; 172 173 174 175 /** 176 * The serial version UID for this serializable class. 177 */ 178 private static final long serialVersionUID = 3420786933570240493L; 179 180 181 182 // Indicates whether the attribute list was provided in the URL. 183 private final boolean attributesProvided; 184 185 // Indicates whether the base DN was provided in the URL. 186 private final boolean baseDNProvided; 187 188 // Indicates whether the filter was provided in the URL. 189 private final boolean filterProvided; 190 191 // Indicates whether the port was provided in the URL. 192 private final boolean portProvided; 193 194 // Indicates whether the scope was provided in the URL. 195 private final boolean scopeProvided; 196 197 // The base DN used by this URL. 198 private final DN baseDN; 199 200 // The filter used by this URL. 201 private final Filter filter; 202 203 // The port used by this URL. 204 private final int port; 205 206 // The search scope used by this URL. 207 private final SearchScope scope; 208 209 // The host used by this URL. 210 private final String host; 211 212 // The normalized representation of this LDAP URL. 213 private volatile String normalizedURLString; 214 215 // The scheme used by this LDAP URL. The standard only accepts "ldap", but 216 // we will also accept "ldaps" and "ldapi". 217 private final String scheme; 218 219 // The string representation of this LDAP URL. 220 private final String urlString; 221 222 // The set of attributes included in this URL. 223 private final String[] attributes; 224 225 226 227 /** 228 * Creates a new LDAP URL from the provided string representation. 229 * 230 * @param urlString The string representation for this LDAP URL. It must 231 * not be {@code null}. 232 * 233 * @throws LDAPException If the provided URL string cannot be parsed as an 234 * LDAP URL. 235 */ 236 public LDAPURL(final String urlString) 237 throws LDAPException 238 { 239 ensureNotNull(urlString); 240 241 this.urlString = urlString; 242 243 244 // Find the location of the first colon. It should mark the end of the 245 // scheme. 246 final int colonPos = urlString.indexOf("://"); 247 if (colonPos < 0) 248 { 249 throw new LDAPException(ResultCode.DECODING_ERROR, 250 ERR_LDAPURL_NO_COLON_SLASHES.get()); 251 } 252 253 scheme = toLowerCase(urlString.substring(0, colonPos)); 254 final int defaultPort; 255 if (scheme.equals("ldap")) 256 { 257 defaultPort = DEFAULT_LDAP_PORT; 258 } 259 else if (scheme.equals("ldaps")) 260 { 261 defaultPort = DEFAULT_LDAPS_PORT; 262 } 263 else if (scheme.equals("ldapi")) 264 { 265 defaultPort = DEFAULT_LDAPI_PORT; 266 } 267 else 268 { 269 throw new LDAPException(ResultCode.DECODING_ERROR, 270 ERR_LDAPURL_INVALID_SCHEME.get(scheme)); 271 } 272 273 274 // Look for the first slash after the "://". It will designate the end of 275 // the hostport section. 276 final int slashPos = urlString.indexOf('/', colonPos+3); 277 if (slashPos < 0) 278 { 279 // This is fine. It just means that the URL won't have a base DN, 280 // attribute list, scope, or filter, and that the rest of the value is 281 // the hostport element. 282 baseDN = DEFAULT_BASE_DN; 283 baseDNProvided = false; 284 attributes = DEFAULT_ATTRIBUTES; 285 attributesProvided = false; 286 scope = DEFAULT_SCOPE; 287 scopeProvided = false; 288 filter = DEFAULT_FILTER; 289 filterProvided = false; 290 291 final String hostPort = urlString.substring(colonPos+3); 292 final StringBuilder hostBuffer = new StringBuilder(hostPort.length()); 293 final int portValue = decodeHostPort(hostPort, hostBuffer); 294 if (portValue < 0) 295 { 296 port = defaultPort; 297 portProvided = false; 298 } 299 else 300 { 301 port = portValue; 302 portProvided = true; 303 } 304 305 if (hostBuffer.length() == 0) 306 { 307 host = null; 308 } 309 else 310 { 311 host = hostBuffer.toString(); 312 } 313 return; 314 } 315 316 final String hostPort = urlString.substring(colonPos+3, slashPos); 317 final StringBuilder hostBuffer = new StringBuilder(hostPort.length()); 318 final int portValue = decodeHostPort(hostPort, hostBuffer); 319 if (portValue < 0) 320 { 321 port = defaultPort; 322 portProvided = false; 323 } 324 else 325 { 326 port = portValue; 327 portProvided = true; 328 } 329 330 if (hostBuffer.length() == 0) 331 { 332 host = null; 333 } 334 else 335 { 336 host = hostBuffer.toString(); 337 } 338 339 340 // Look for the first question mark after the slash. It will designate the 341 // end of the base DN. 342 final int questionMarkPos = urlString.indexOf('?', slashPos+1); 343 if (questionMarkPos < 0) 344 { 345 // This is fine. It just means that the URL won't have an attribute list, 346 // scope, or filter, and that the rest of the value is the base DN. 347 attributes = DEFAULT_ATTRIBUTES; 348 attributesProvided = false; 349 scope = DEFAULT_SCOPE; 350 scopeProvided = false; 351 filter = DEFAULT_FILTER; 352 filterProvided = false; 353 354 baseDN = new DN(percentDecode(urlString.substring(slashPos+1))); 355 baseDNProvided = (! baseDN.isNullDN()); 356 return; 357 } 358 359 baseDN = new DN(percentDecode(urlString.substring(slashPos+1, 360 questionMarkPos))); 361 baseDNProvided = (! baseDN.isNullDN()); 362 363 364 // Look for the next question mark. It will designate the end of the 365 // attribute list. 366 final int questionMark2Pos = urlString.indexOf('?', questionMarkPos+1); 367 if (questionMark2Pos < 0) 368 { 369 // This is fine. It just means that the URL won't have a scope or filter, 370 // and that the rest of the value is the attribute list. 371 scope = DEFAULT_SCOPE; 372 scopeProvided = false; 373 filter = DEFAULT_FILTER; 374 filterProvided = false; 375 376 attributes = decodeAttributes(urlString.substring(questionMarkPos+1)); 377 attributesProvided = (attributes.length > 0); 378 return; 379 } 380 381 attributes = decodeAttributes(urlString.substring(questionMarkPos+1, 382 questionMark2Pos)); 383 attributesProvided = (attributes.length > 0); 384 385 386 // Look for the next question mark. It will designate the end of the scope. 387 final int questionMark3Pos = urlString.indexOf('?', questionMark2Pos+1); 388 if (questionMark3Pos < 0) 389 { 390 // This is fine. It just means that the URL won't have a filter, and that 391 // the rest of the value is the scope. 392 filter = DEFAULT_FILTER; 393 filterProvided = false; 394 395 final String scopeStr = 396 toLowerCase(urlString.substring(questionMark2Pos+1)); 397 if (scopeStr.length() == 0) 398 { 399 scope = SearchScope.BASE; 400 scopeProvided = false; 401 } 402 else if (scopeStr.equals("base")) 403 { 404 scope = SearchScope.BASE; 405 scopeProvided = true; 406 } 407 else if (scopeStr.equals("one")) 408 { 409 scope = SearchScope.ONE; 410 scopeProvided = true; 411 } 412 else if (scopeStr.equals("sub")) 413 { 414 scope = SearchScope.SUB; 415 scopeProvided = true; 416 } 417 else if (scopeStr.equals("subord") || scopeStr.equals("subordinates")) 418 { 419 scope = SearchScope.SUBORDINATE_SUBTREE; 420 scopeProvided = true; 421 } 422 else 423 { 424 throw new LDAPException(ResultCode.DECODING_ERROR, 425 ERR_LDAPURL_INVALID_SCOPE.get(scopeStr)); 426 } 427 return; 428 } 429 430 final String scopeStr = 431 toLowerCase(urlString.substring(questionMark2Pos+1, questionMark3Pos)); 432 if (scopeStr.length() == 0) 433 { 434 scope = SearchScope.BASE; 435 scopeProvided = false; 436 } 437 else if (scopeStr.equals("base")) 438 { 439 scope = SearchScope.BASE; 440 scopeProvided = true; 441 } 442 else if (scopeStr.equals("one")) 443 { 444 scope = SearchScope.ONE; 445 scopeProvided = true; 446 } 447 else if (scopeStr.equals("sub")) 448 { 449 scope = SearchScope.SUB; 450 scopeProvided = true; 451 } 452 else if (scopeStr.equals("subord") || scopeStr.equals("subordinates")) 453 { 454 scope = SearchScope.SUBORDINATE_SUBTREE; 455 scopeProvided = true; 456 } 457 else 458 { 459 throw new LDAPException(ResultCode.DECODING_ERROR, 460 ERR_LDAPURL_INVALID_SCOPE.get(scopeStr)); 461 } 462 463 464 // The remainder of the value must be the filter. 465 final String filterStr = 466 percentDecode(urlString.substring(questionMark3Pos+1)); 467 if (filterStr.length() == 0) 468 { 469 filter = DEFAULT_FILTER; 470 filterProvided = false; 471 } 472 else 473 { 474 filter = Filter.create(filterStr); 475 filterProvided = true; 476 } 477 } 478 479 480 481 /** 482 * Creates a new LDAP URL with the provided information. 483 * 484 * @param scheme The scheme for this LDAP URL. It must not be 485 * {@code null} and must be either "ldap", "ldaps", or 486 * "ldapi". 487 * @param host The host for this LDAP URL. It may be {@code null} if 488 * no host is to be included. 489 * @param port The port for this LDAP URL. It may be {@code null} if 490 * no port is to be included. If it is provided, it must 491 * be between 1 and 65535, inclusive. 492 * @param baseDN The base DN for this LDAP URL. It may be {@code null} 493 * if no base DN is to be included. 494 * @param attributes The set of requested attributes for this LDAP URL. It 495 * may be {@code null} or empty if no attribute list is to 496 * be included. 497 * @param scope The scope for this LDAP URL. It may be {@code null} if 498 * no scope is to be included. Otherwise, it must be a 499 * value between zero and three, inclusive. 500 * @param filter The filter for this LDAP URL. It may be {@code null} 501 * if no filter is to be included. 502 * 503 * @throws LDAPException If there is a problem with any of the provided 504 * arguments. 505 */ 506 public LDAPURL(final String scheme, final String host, final Integer port, 507 final DN baseDN, final String[] attributes, 508 final SearchScope scope, final Filter filter) 509 throws LDAPException 510 { 511 ensureNotNull(scheme); 512 513 final StringBuilder buffer = new StringBuilder(); 514 515 this.scheme = toLowerCase(scheme); 516 final int defaultPort; 517 if (scheme.equals("ldap")) 518 { 519 defaultPort = DEFAULT_LDAP_PORT; 520 } 521 else if (scheme.equals("ldaps")) 522 { 523 defaultPort = DEFAULT_LDAPS_PORT; 524 } 525 else if (scheme.equals("ldapi")) 526 { 527 defaultPort = DEFAULT_LDAPI_PORT; 528 } 529 else 530 { 531 throw new LDAPException(ResultCode.DECODING_ERROR, 532 ERR_LDAPURL_INVALID_SCHEME.get(scheme)); 533 } 534 535 buffer.append(scheme); 536 buffer.append("://"); 537 538 if ((host == null) || (host.length() == 0)) 539 { 540 this.host = null; 541 } 542 else 543 { 544 this.host = host; 545 buffer.append(host); 546 } 547 548 if (port == null) 549 { 550 this.port = defaultPort; 551 portProvided = false; 552 } 553 else 554 { 555 this.port = port; 556 portProvided = true; 557 buffer.append(':'); 558 buffer.append(port); 559 560 if ((port < 1) || (port > 65535)) 561 { 562 throw new LDAPException(ResultCode.PARAM_ERROR, 563 ERR_LDAPURL_INVALID_PORT.get(port)); 564 } 565 } 566 567 buffer.append('/'); 568 if (baseDN == null) 569 { 570 this.baseDN = DEFAULT_BASE_DN; 571 baseDNProvided = false; 572 } 573 else 574 { 575 this.baseDN = baseDN; 576 baseDNProvided = true; 577 percentEncode(baseDN.toString(), buffer); 578 } 579 580 final boolean continueAppending; 581 if (((attributes == null) || (attributes.length == 0)) && (scope == null) && 582 (filter == null)) 583 { 584 continueAppending = false; 585 } 586 else 587 { 588 continueAppending = true; 589 } 590 591 if (continueAppending) 592 { 593 buffer.append('?'); 594 } 595 if ((attributes == null) || (attributes.length == 0)) 596 { 597 this.attributes = DEFAULT_ATTRIBUTES; 598 attributesProvided = false; 599 } 600 else 601 { 602 this.attributes = attributes; 603 attributesProvided = true; 604 605 for (int i=0; i < attributes.length; i++) 606 { 607 if (i > 0) 608 { 609 buffer.append(','); 610 } 611 buffer.append(attributes[i]); 612 } 613 } 614 615 if (continueAppending) 616 { 617 buffer.append('?'); 618 } 619 if (scope == null) 620 { 621 this.scope = DEFAULT_SCOPE; 622 scopeProvided = false; 623 } 624 else 625 { 626 switch (scope.intValue()) 627 { 628 case 0: 629 this.scope = scope; 630 scopeProvided = true; 631 buffer.append("base"); 632 break; 633 case 1: 634 this.scope = scope; 635 scopeProvided = true; 636 buffer.append("one"); 637 break; 638 case 2: 639 this.scope = scope; 640 scopeProvided = true; 641 buffer.append("sub"); 642 break; 643 case 3: 644 this.scope = scope; 645 scopeProvided = true; 646 buffer.append("subordinates"); 647 break; 648 default: 649 throw new LDAPException(ResultCode.PARAM_ERROR, 650 ERR_LDAPURL_INVALID_SCOPE_VALUE.get(scope)); 651 } 652 } 653 654 if (continueAppending) 655 { 656 buffer.append('?'); 657 } 658 if (filter == null) 659 { 660 this.filter = DEFAULT_FILTER; 661 filterProvided = false; 662 } 663 else 664 { 665 this.filter = filter; 666 filterProvided = true; 667 percentEncode(filter.toString(), buffer); 668 } 669 670 urlString = buffer.toString(); 671 } 672 673 674 675 /** 676 * Decodes the provided string as a host and optional port number. 677 * 678 * @param hostPort The string to be decoded. 679 * @param hostBuffer The buffer to which the decoded host address will be 680 * appended. 681 * 682 * @return The port number decoded from the provided string, or -1 if there 683 * was no port number. 684 * 685 * @throws LDAPException If the provided string cannot be decoded as a 686 * hostport element. 687 */ 688 private static int decodeHostPort(final String hostPort, 689 final StringBuilder hostBuffer) 690 throws LDAPException 691 { 692 final int length = hostPort.length(); 693 if (length == 0) 694 { 695 // It's an empty string, so we'll just use the defaults. 696 return -1; 697 } 698 699 if (hostPort.charAt(0) == '[') 700 { 701 // It starts with a square bracket, which means that the address is an 702 // IPv6 literal address. Find the closing bracket, and the address 703 // will be inside them. 704 final int closingBracketPos = hostPort.indexOf(']'); 705 if (closingBracketPos < 0) 706 { 707 throw new LDAPException(ResultCode.DECODING_ERROR, 708 ERR_LDAPURL_IPV6_HOST_MISSING_BRACKET.get()); 709 } 710 711 hostBuffer.append(hostPort.substring(1, closingBracketPos).trim()); 712 if (hostBuffer.length() == 0) 713 { 714 throw new LDAPException(ResultCode.DECODING_ERROR, 715 ERR_LDAPURL_IPV6_HOST_EMPTY.get()); 716 } 717 718 // The closing bracket must either be the end of the hostport element 719 // (in which case we'll use the default port), or it must be followed by 720 // a colon and an integer (which will be the port). 721 if (closingBracketPos == (length - 1)) 722 { 723 return -1; 724 } 725 else 726 { 727 if (hostPort.charAt(closingBracketPos+1) != ':') 728 { 729 throw new LDAPException(ResultCode.DECODING_ERROR, 730 ERR_LDAPURL_IPV6_HOST_UNEXPECTED_CHAR.get( 731 hostPort.charAt(closingBracketPos+1))); 732 } 733 else 734 { 735 try 736 { 737 final int decodedPort = 738 Integer.parseInt(hostPort.substring(closingBracketPos+2)); 739 if ((decodedPort >= 1) && (decodedPort <= 65535)) 740 { 741 return decodedPort; 742 } 743 else 744 { 745 throw new LDAPException(ResultCode.DECODING_ERROR, 746 ERR_LDAPURL_INVALID_PORT.get( 747 decodedPort)); 748 } 749 } 750 catch (NumberFormatException nfe) 751 { 752 debugException(nfe); 753 throw new LDAPException(ResultCode.DECODING_ERROR, 754 ERR_LDAPURL_PORT_NOT_INT.get(hostPort), 755 nfe); 756 } 757 } 758 } 759 } 760 761 762 // If we've gotten here, then the address is either a resolvable name or an 763 // IPv4 address. If there is a colon in the string, then it will separate 764 // the address from the port. Otherwise, the remaining value will be the 765 // address and we'll use the default port. 766 final int colonPos = hostPort.indexOf(':'); 767 if (colonPos < 0) 768 { 769 hostBuffer.append(hostPort); 770 return -1; 771 } 772 else 773 { 774 try 775 { 776 final int decodedPort = 777 Integer.parseInt(hostPort.substring(colonPos+1)); 778 if ((decodedPort >= 1) && (decodedPort <= 65535)) 779 { 780 hostBuffer.append(hostPort.substring(0, colonPos)); 781 return decodedPort; 782 } 783 else 784 { 785 throw new LDAPException(ResultCode.DECODING_ERROR, 786 ERR_LDAPURL_INVALID_PORT.get(decodedPort)); 787 } 788 } 789 catch (NumberFormatException nfe) 790 { 791 debugException(nfe); 792 throw new LDAPException(ResultCode.DECODING_ERROR, 793 ERR_LDAPURL_PORT_NOT_INT.get(hostPort), nfe); 794 } 795 } 796 } 797 798 799 800 /** 801 * Decodes the contents of the provided string as an attribute list. 802 * 803 * @param s The string to decode as an attribute list. 804 * 805 * @return The array of decoded attribute names. 806 * 807 * @throws LDAPException If an error occurred while attempting to decode the 808 * attribute list. 809 */ 810 private static String[] decodeAttributes(final String s) 811 throws LDAPException 812 { 813 final int length = s.length(); 814 if (length == 0) 815 { 816 return DEFAULT_ATTRIBUTES; 817 } 818 819 final ArrayList<String> attrList = new ArrayList<String>(); 820 int startPos = 0; 821 while (startPos < length) 822 { 823 final int commaPos = s.indexOf(',', startPos); 824 if (commaPos < 0) 825 { 826 // There are no more commas, so there can only be one attribute left. 827 final String attrName = s.substring(startPos).trim(); 828 if (attrName.length() == 0) 829 { 830 // This is only acceptable if the attribute list is empty (there was 831 // probably a space in the attribute list string, which is technically 832 // not allowed, but we'll accept it). If the attribute list is not 833 // empty, then there were two consecutive commas, which is not 834 // allowed. 835 if (attrList.isEmpty()) 836 { 837 return DEFAULT_ATTRIBUTES; 838 } 839 else 840 { 841 throw new LDAPException(ResultCode.DECODING_ERROR, 842 ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get()); 843 } 844 } 845 else 846 { 847 attrList.add(attrName); 848 break; 849 } 850 } 851 else 852 { 853 final String attrName = s.substring(startPos, commaPos).trim(); 854 if (attrName.length() == 0) 855 { 856 throw new LDAPException(ResultCode.DECODING_ERROR, 857 ERR_LDAPURL_ATTRLIST_EMPTY_ATTRIBUTE.get()); 858 } 859 else 860 { 861 attrList.add(attrName); 862 startPos = commaPos+1; 863 if (startPos >= length) 864 { 865 throw new LDAPException(ResultCode.DECODING_ERROR, 866 ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get()); 867 } 868 } 869 } 870 } 871 872 final String[] attributes = new String[attrList.size()]; 873 attrList.toArray(attributes); 874 return attributes; 875 } 876 877 878 879 /** 880 * Decodes any percent-encoded values that may be contained in the provided 881 * string. 882 * 883 * @param s The string to be decoded. 884 * 885 * @return The percent-decoded form of the provided string. 886 * 887 * @throws LDAPException If a problem occurs while attempting to decode the 888 * provided string. 889 */ 890 public static String percentDecode(final String s) 891 throws LDAPException 892 { 893 // First, see if there are any percent characters at all in the provided 894 // string. If not, then just return the string as-is. 895 int firstPercentPos = -1; 896 final int length = s.length(); 897 for (int i=0; i < length; i++) 898 { 899 if (s.charAt(i) == '%') 900 { 901 firstPercentPos = i; 902 break; 903 } 904 } 905 906 if (firstPercentPos < 0) 907 { 908 return s; 909 } 910 911 int pos = firstPercentPos; 912 final StringBuilder buffer = new StringBuilder(2 * length); 913 buffer.append(s.substring(0, firstPercentPos)); 914 915 while (pos < length) 916 { 917 final char c = s.charAt(pos++); 918 if (c == '%') 919 { 920 if (pos >= length) 921 { 922 throw new LDAPException(ResultCode.DECODING_ERROR, 923 ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s)); 924 } 925 926 927 final ByteBuffer byteBuffer = ByteBuffer.allocate(length - pos); 928 while (pos < length) 929 { 930 byte b; 931 switch (s.charAt(pos++)) 932 { 933 case '0': 934 b = 0x00; 935 break; 936 case '1': 937 b = 0x10; 938 break; 939 case '2': 940 b = 0x20; 941 break; 942 case '3': 943 b = 0x30; 944 break; 945 case '4': 946 b = 0x40; 947 break; 948 case '5': 949 b = 0x50; 950 break; 951 case '6': 952 b = 0x60; 953 break; 954 case '7': 955 b = 0x70; 956 break; 957 case '8': 958 b = (byte) 0x80; 959 break; 960 case '9': 961 b = (byte) 0x90; 962 break; 963 case 'a': 964 case 'A': 965 b = (byte) 0xA0; 966 break; 967 case 'b': 968 case 'B': 969 b = (byte) 0xB0; 970 break; 971 case 'c': 972 case 'C': 973 b = (byte) 0xC0; 974 break; 975 case 'd': 976 case 'D': 977 b = (byte) 0xD0; 978 break; 979 case 'e': 980 case 'E': 981 b = (byte) 0xE0; 982 break; 983 case 'f': 984 case 'F': 985 b = (byte) 0xF0; 986 break; 987 default: 988 throw new LDAPException(ResultCode.DECODING_ERROR, 989 ERR_LDAPURL_INVALID_HEX_CHAR.get( 990 s.charAt(pos-1))); 991 } 992 993 if (pos >= length) 994 { 995 throw new LDAPException(ResultCode.DECODING_ERROR, 996 ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s)); 997 } 998 999 switch (s.charAt(pos++)) 1000 { 1001 case '0': 1002 b |= 0x00; 1003 break; 1004 case '1': 1005 b |= 0x01; 1006 break; 1007 case '2': 1008 b |= 0x02; 1009 break; 1010 case '3': 1011 b |= 0x03; 1012 break; 1013 case '4': 1014 b |= 0x04; 1015 break; 1016 case '5': 1017 b |= 0x05; 1018 break; 1019 case '6': 1020 b |= 0x06; 1021 break; 1022 case '7': 1023 b |= 0x07; 1024 break; 1025 case '8': 1026 b |= 0x08; 1027 break; 1028 case '9': 1029 b |= 0x09; 1030 break; 1031 case 'a': 1032 case 'A': 1033 b |= 0x0A; 1034 break; 1035 case 'b': 1036 case 'B': 1037 b |= 0x0B; 1038 break; 1039 case 'c': 1040 case 'C': 1041 b |= 0x0C; 1042 break; 1043 case 'd': 1044 case 'D': 1045 b |= 0x0D; 1046 break; 1047 case 'e': 1048 case 'E': 1049 b |= 0x0E; 1050 break; 1051 case 'f': 1052 case 'F': 1053 b |= 0x0F; 1054 break; 1055 default: 1056 throw new LDAPException(ResultCode.DECODING_ERROR, 1057 ERR_LDAPURL_INVALID_HEX_CHAR.get( 1058 s.charAt(pos-1))); 1059 } 1060 1061 byteBuffer.put(b); 1062 if ((pos < length) && (s.charAt(pos) != '%')) 1063 { 1064 break; 1065 } 1066 } 1067 1068 byteBuffer.flip(); 1069 final byte[] byteArray = new byte[byteBuffer.limit()]; 1070 byteBuffer.get(byteArray); 1071 1072 buffer.append(toUTF8String(byteArray)); 1073 } 1074 else 1075 { 1076 buffer.append(c); 1077 } 1078 } 1079 1080 return buffer.toString(); 1081 } 1082 1083 1084 1085 /** 1086 * Appends an encoded version of the provided string to the given buffer. Any 1087 * special characters contained in the string will be replaced with byte 1088 * representations consisting of one percent sign and two hexadecimal digits 1089 * for each byte in the special character. 1090 * 1091 * @param s The string to be encoded. 1092 * @param buffer The buffer to which the encoded string will be written. 1093 */ 1094 private static void percentEncode(final String s, final StringBuilder buffer) 1095 { 1096 final int length = s.length(); 1097 for (int i=0; i < length; i++) 1098 { 1099 final char c = s.charAt(i); 1100 1101 switch (c) 1102 { 1103 case 'A': 1104 case 'B': 1105 case 'C': 1106 case 'D': 1107 case 'E': 1108 case 'F': 1109 case 'G': 1110 case 'H': 1111 case 'I': 1112 case 'J': 1113 case 'K': 1114 case 'L': 1115 case 'M': 1116 case 'N': 1117 case 'O': 1118 case 'P': 1119 case 'Q': 1120 case 'R': 1121 case 'S': 1122 case 'T': 1123 case 'U': 1124 case 'V': 1125 case 'W': 1126 case 'X': 1127 case 'Y': 1128 case 'Z': 1129 case 'a': 1130 case 'b': 1131 case 'c': 1132 case 'd': 1133 case 'e': 1134 case 'f': 1135 case 'g': 1136 case 'h': 1137 case 'i': 1138 case 'j': 1139 case 'k': 1140 case 'l': 1141 case 'm': 1142 case 'n': 1143 case 'o': 1144 case 'p': 1145 case 'q': 1146 case 'r': 1147 case 's': 1148 case 't': 1149 case 'u': 1150 case 'v': 1151 case 'w': 1152 case 'x': 1153 case 'y': 1154 case 'z': 1155 case '0': 1156 case '1': 1157 case '2': 1158 case '3': 1159 case '4': 1160 case '5': 1161 case '6': 1162 case '7': 1163 case '8': 1164 case '9': 1165 case '-': 1166 case '.': 1167 case '_': 1168 case '~': 1169 case '!': 1170 case '$': 1171 case '&': 1172 case '\'': 1173 case '(': 1174 case ')': 1175 case '*': 1176 case '+': 1177 case ',': 1178 case ';': 1179 case '=': 1180 buffer.append(c); 1181 break; 1182 1183 default: 1184 final byte[] charBytes = getBytes(new String(new char[] { c })); 1185 for (final byte b : charBytes) 1186 { 1187 buffer.append('%'); 1188 toHex(b, buffer); 1189 } 1190 break; 1191 } 1192 } 1193 } 1194 1195 1196 1197 /** 1198 * Retrieves the scheme for this LDAP URL. It will either be "ldap", "ldaps", 1199 * or "ldapi". 1200 * 1201 * @return The scheme for this LDAP URL. 1202 */ 1203 public String getScheme() 1204 { 1205 return scheme; 1206 } 1207 1208 1209 1210 /** 1211 * Retrieves the host for this LDAP URL. 1212 * 1213 * @return The host for this LDAP URL, or {@code null} if the URL does not 1214 * include a host and the client is supposed to have some external 1215 * knowledge of what the host should be. 1216 */ 1217 public String getHost() 1218 { 1219 return host; 1220 } 1221 1222 1223 1224 /** 1225 * Indicates whether the URL explicitly included a host address. 1226 * 1227 * @return {@code true} if the URL explicitly included a host address, or 1228 * {@code false} if it did not. 1229 */ 1230 public boolean hostProvided() 1231 { 1232 return (host != null); 1233 } 1234 1235 1236 1237 /** 1238 * Retrieves the port for this LDAP URL. 1239 * 1240 * @return The port for this LDAP URL. 1241 */ 1242 public int getPort() 1243 { 1244 return port; 1245 } 1246 1247 1248 1249 /** 1250 * Indicates whether the URL explicitly included a port number. 1251 * 1252 * @return {@code true} if the URL explicitly included a port number, or 1253 * {@code false} if it did not and the default should be used. 1254 */ 1255 public boolean portProvided() 1256 { 1257 return portProvided; 1258 } 1259 1260 1261 1262 /** 1263 * Retrieves the base DN for this LDAP URL. 1264 * 1265 * @return The base DN for this LDAP URL. 1266 */ 1267 public DN getBaseDN() 1268 { 1269 return baseDN; 1270 } 1271 1272 1273 1274 /** 1275 * Indicates whether the URL explicitly included a base DN. 1276 * 1277 * @return {@code true} if the URL explicitly included a base DN, or 1278 * {@code false} if it did not and the default should be used. 1279 */ 1280 public boolean baseDNProvided() 1281 { 1282 return baseDNProvided; 1283 } 1284 1285 1286 1287 /** 1288 * Retrieves the attribute list for this LDAP URL. 1289 * 1290 * @return The attribute list for this LDAP URL. 1291 */ 1292 public String[] getAttributes() 1293 { 1294 return attributes; 1295 } 1296 1297 1298 1299 /** 1300 * Indicates whether the URL explicitly included an attribute list. 1301 * 1302 * @return {@code true} if the URL explicitly included an attribute list, or 1303 * {@code false} if it did not and the default should be used. 1304 */ 1305 public boolean attributesProvided() 1306 { 1307 return attributesProvided; 1308 } 1309 1310 1311 1312 /** 1313 * Retrieves the scope for this LDAP URL. 1314 * 1315 * @return The scope for this LDAP URL. 1316 */ 1317 public SearchScope getScope() 1318 { 1319 return scope; 1320 } 1321 1322 1323 1324 /** 1325 * Indicates whether the URL explicitly included a search scope. 1326 * 1327 * @return {@code true} if the URL explicitly included a search scope, or 1328 * {@code false} if it did not and the default should be used. 1329 */ 1330 public boolean scopeProvided() 1331 { 1332 return scopeProvided; 1333 } 1334 1335 1336 1337 /** 1338 * Retrieves the filter for this LDAP URL. 1339 * 1340 * @return The filter for this LDAP URL. 1341 */ 1342 public Filter getFilter() 1343 { 1344 return filter; 1345 } 1346 1347 1348 1349 /** 1350 * Indicates whether the URL explicitly included a search filter. 1351 * 1352 * @return {@code true} if the URL explicitly included a search filter, or 1353 * {@code false} if it did not and the default should be used. 1354 */ 1355 public boolean filterProvided() 1356 { 1357 return filterProvided; 1358 } 1359 1360 1361 1362 /** 1363 * Creates a search request containing the base DN, scope, filter, and 1364 * requested attributes from this LDAP URL. 1365 * 1366 * @return The search request created from the base DN, scope, filter, and 1367 * requested attributes from this LDAP URL. 1368 */ 1369 public SearchRequest toSearchRequest() 1370 { 1371 return new SearchRequest(baseDN.toString(), scope, filter, attributes); 1372 } 1373 1374 1375 1376 /** 1377 * Retrieves a hash code for this LDAP URL. 1378 * 1379 * @return A hash code for this LDAP URL. 1380 */ 1381 @Override() 1382 public int hashCode() 1383 { 1384 return toNormalizedString().hashCode(); 1385 } 1386 1387 1388 1389 /** 1390 * Indicates whether the provided object is equal to this LDAP URL. In order 1391 * to be considered equal, the provided object must be an LDAP URL with the 1392 * same normalized string representation. 1393 * 1394 * @param o The object for which to make the determination. 1395 * 1396 * @return {@code true} if the provided object is equal to this LDAP URL, or 1397 * {@code false} if not. 1398 */ 1399 @Override() 1400 public boolean equals(final Object o) 1401 { 1402 if (o == null) 1403 { 1404 return false; 1405 } 1406 1407 if (o == this) 1408 { 1409 return true; 1410 } 1411 1412 if (! (o instanceof LDAPURL)) 1413 { 1414 return false; 1415 } 1416 1417 final LDAPURL url = (LDAPURL) o; 1418 return toNormalizedString().equals(url.toNormalizedString()); 1419 } 1420 1421 1422 1423 /** 1424 * Retrieves a string representation of this LDAP URL. 1425 * 1426 * @return A string representation of this LDAP URL. 1427 */ 1428 @Override() 1429 public String toString() 1430 { 1431 return urlString; 1432 } 1433 1434 1435 1436 /** 1437 * Retrieves a normalized string representation of this LDAP URL. 1438 * 1439 * @return A normalized string representation of this LDAP URL. 1440 */ 1441 public String toNormalizedString() 1442 { 1443 if (normalizedURLString == null) 1444 { 1445 final StringBuilder buffer = new StringBuilder(); 1446 toNormalizedString(buffer); 1447 normalizedURLString = buffer.toString(); 1448 } 1449 1450 return normalizedURLString; 1451 } 1452 1453 1454 1455 /** 1456 * Appends a normalized string representation of this LDAP URL to the provided 1457 * buffer. 1458 * 1459 * @param buffer The buffer to which to append the normalized string 1460 * representation of this LDAP URL. 1461 */ 1462 public void toNormalizedString(final StringBuilder buffer) 1463 { 1464 buffer.append(scheme); 1465 buffer.append("://"); 1466 1467 if (host != null) 1468 { 1469 if (host.indexOf(':') >= 0) 1470 { 1471 buffer.append('['); 1472 buffer.append(toLowerCase(host)); 1473 buffer.append(']'); 1474 } 1475 else 1476 { 1477 buffer.append(toLowerCase(host)); 1478 } 1479 } 1480 1481 if (! scheme.equals("ldapi")) 1482 { 1483 buffer.append(':'); 1484 buffer.append(port); 1485 } 1486 1487 buffer.append('/'); 1488 percentEncode(baseDN.toNormalizedString(), buffer); 1489 buffer.append('?'); 1490 1491 for (int i=0; i < attributes.length; i++) 1492 { 1493 if (i > 0) 1494 { 1495 buffer.append(','); 1496 } 1497 1498 buffer.append(toLowerCase(attributes[i])); 1499 } 1500 1501 buffer.append('?'); 1502 switch (scope.intValue()) 1503 { 1504 case 0: // BASE 1505 buffer.append("base"); 1506 break; 1507 case 1: // ONE 1508 buffer.append("one"); 1509 break; 1510 case 2: // SUB 1511 buffer.append("sub"); 1512 break; 1513 case 3: // SUBORDINATE_SUBTREE 1514 buffer.append("subordinates"); 1515 break; 1516 } 1517 1518 buffer.append('?'); 1519 percentEncode(filter.toNormalizedString(), buffer); 1520 } 1521}