001/* 002 * Copyright 2007-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2018 Ping Identity Corporation 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 (final 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 (final 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 final 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 byteBuffer.put(b); 1003 break; 1004 case '1': 1005 byteBuffer.put((byte) (b | 0x01)); 1006 break; 1007 case '2': 1008 byteBuffer.put((byte) (b | 0x02)); 1009 break; 1010 case '3': 1011 byteBuffer.put((byte) (b | 0x03)); 1012 break; 1013 case '4': 1014 byteBuffer.put((byte) (b | 0x04)); 1015 break; 1016 case '5': 1017 byteBuffer.put((byte) (b | 0x05)); 1018 break; 1019 case '6': 1020 byteBuffer.put((byte) (b | 0x06)); 1021 break; 1022 case '7': 1023 byteBuffer.put((byte) (b | 0x07)); 1024 break; 1025 case '8': 1026 byteBuffer.put((byte) (b | 0x08)); 1027 break; 1028 case '9': 1029 byteBuffer.put((byte) (b | 0x09)); 1030 break; 1031 case 'a': 1032 case 'A': 1033 byteBuffer.put((byte) (b | 0x0A)); 1034 break; 1035 case 'b': 1036 case 'B': 1037 byteBuffer.put((byte) (b | 0x0B)); 1038 break; 1039 case 'c': 1040 case 'C': 1041 byteBuffer.put((byte) (b | 0x0C)); 1042 break; 1043 case 'd': 1044 case 'D': 1045 byteBuffer.put((byte) (b | 0x0D)); 1046 break; 1047 case 'e': 1048 case 'E': 1049 byteBuffer.put((byte) (b | 0x0E)); 1050 break; 1051 case 'f': 1052 case 'F': 1053 byteBuffer.put((byte) (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 if ((pos < length) && (s.charAt(pos) != '%')) 1062 { 1063 break; 1064 } 1065 } 1066 1067 byteBuffer.flip(); 1068 final byte[] byteArray = new byte[byteBuffer.limit()]; 1069 byteBuffer.get(byteArray); 1070 1071 buffer.append(toUTF8String(byteArray)); 1072 } 1073 else 1074 { 1075 buffer.append(c); 1076 } 1077 } 1078 1079 return buffer.toString(); 1080 } 1081 1082 1083 1084 /** 1085 * Appends an encoded version of the provided string to the given buffer. Any 1086 * special characters contained in the string will be replaced with byte 1087 * representations consisting of one percent sign and two hexadecimal digits 1088 * for each byte in the special character. 1089 * 1090 * @param s The string to be encoded. 1091 * @param buffer The buffer to which the encoded string will be written. 1092 */ 1093 private static void percentEncode(final String s, final StringBuilder buffer) 1094 { 1095 final int length = s.length(); 1096 for (int i=0; i < length; i++) 1097 { 1098 final char c = s.charAt(i); 1099 1100 switch (c) 1101 { 1102 case 'A': 1103 case 'B': 1104 case 'C': 1105 case 'D': 1106 case 'E': 1107 case 'F': 1108 case 'G': 1109 case 'H': 1110 case 'I': 1111 case 'J': 1112 case 'K': 1113 case 'L': 1114 case 'M': 1115 case 'N': 1116 case 'O': 1117 case 'P': 1118 case 'Q': 1119 case 'R': 1120 case 'S': 1121 case 'T': 1122 case 'U': 1123 case 'V': 1124 case 'W': 1125 case 'X': 1126 case 'Y': 1127 case 'Z': 1128 case 'a': 1129 case 'b': 1130 case 'c': 1131 case 'd': 1132 case 'e': 1133 case 'f': 1134 case 'g': 1135 case 'h': 1136 case 'i': 1137 case 'j': 1138 case 'k': 1139 case 'l': 1140 case 'm': 1141 case 'n': 1142 case 'o': 1143 case 'p': 1144 case 'q': 1145 case 'r': 1146 case 's': 1147 case 't': 1148 case 'u': 1149 case 'v': 1150 case 'w': 1151 case 'x': 1152 case 'y': 1153 case 'z': 1154 case '0': 1155 case '1': 1156 case '2': 1157 case '3': 1158 case '4': 1159 case '5': 1160 case '6': 1161 case '7': 1162 case '8': 1163 case '9': 1164 case '-': 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 buffer.append(c); 1180 break; 1181 1182 default: 1183 final byte[] charBytes = getBytes(new String(new char[] { c })); 1184 for (final byte b : charBytes) 1185 { 1186 buffer.append('%'); 1187 toHex(b, buffer); 1188 } 1189 break; 1190 } 1191 } 1192 } 1193 1194 1195 1196 /** 1197 * Retrieves the scheme for this LDAP URL. It will either be "ldap", "ldaps", 1198 * or "ldapi". 1199 * 1200 * @return The scheme for this LDAP URL. 1201 */ 1202 public String getScheme() 1203 { 1204 return scheme; 1205 } 1206 1207 1208 1209 /** 1210 * Retrieves the host for this LDAP URL. 1211 * 1212 * @return The host for this LDAP URL, or {@code null} if the URL does not 1213 * include a host and the client is supposed to have some external 1214 * knowledge of what the host should be. 1215 */ 1216 public String getHost() 1217 { 1218 return host; 1219 } 1220 1221 1222 1223 /** 1224 * Indicates whether the URL explicitly included a host address. 1225 * 1226 * @return {@code true} if the URL explicitly included a host address, or 1227 * {@code false} if it did not. 1228 */ 1229 public boolean hostProvided() 1230 { 1231 return (host != null); 1232 } 1233 1234 1235 1236 /** 1237 * Retrieves the port for this LDAP URL. 1238 * 1239 * @return The port for this LDAP URL. 1240 */ 1241 public int getPort() 1242 { 1243 return port; 1244 } 1245 1246 1247 1248 /** 1249 * Indicates whether the URL explicitly included a port number. 1250 * 1251 * @return {@code true} if the URL explicitly included a port number, or 1252 * {@code false} if it did not and the default should be used. 1253 */ 1254 public boolean portProvided() 1255 { 1256 return portProvided; 1257 } 1258 1259 1260 1261 /** 1262 * Retrieves the base DN for this LDAP URL. 1263 * 1264 * @return The base DN for this LDAP URL. 1265 */ 1266 public DN getBaseDN() 1267 { 1268 return baseDN; 1269 } 1270 1271 1272 1273 /** 1274 * Indicates whether the URL explicitly included a base DN. 1275 * 1276 * @return {@code true} if the URL explicitly included a base DN, or 1277 * {@code false} if it did not and the default should be used. 1278 */ 1279 public boolean baseDNProvided() 1280 { 1281 return baseDNProvided; 1282 } 1283 1284 1285 1286 /** 1287 * Retrieves the attribute list for this LDAP URL. 1288 * 1289 * @return The attribute list for this LDAP URL. 1290 */ 1291 public String[] getAttributes() 1292 { 1293 return attributes; 1294 } 1295 1296 1297 1298 /** 1299 * Indicates whether the URL explicitly included an attribute list. 1300 * 1301 * @return {@code true} if the URL explicitly included an attribute list, or 1302 * {@code false} if it did not and the default should be used. 1303 */ 1304 public boolean attributesProvided() 1305 { 1306 return attributesProvided; 1307 } 1308 1309 1310 1311 /** 1312 * Retrieves the scope for this LDAP URL. 1313 * 1314 * @return The scope for this LDAP URL. 1315 */ 1316 public SearchScope getScope() 1317 { 1318 return scope; 1319 } 1320 1321 1322 1323 /** 1324 * Indicates whether the URL explicitly included a search scope. 1325 * 1326 * @return {@code true} if the URL explicitly included a search scope, or 1327 * {@code false} if it did not and the default should be used. 1328 */ 1329 public boolean scopeProvided() 1330 { 1331 return scopeProvided; 1332 } 1333 1334 1335 1336 /** 1337 * Retrieves the filter for this LDAP URL. 1338 * 1339 * @return The filter for this LDAP URL. 1340 */ 1341 public Filter getFilter() 1342 { 1343 return filter; 1344 } 1345 1346 1347 1348 /** 1349 * Indicates whether the URL explicitly included a search filter. 1350 * 1351 * @return {@code true} if the URL explicitly included a search filter, or 1352 * {@code false} if it did not and the default should be used. 1353 */ 1354 public boolean filterProvided() 1355 { 1356 return filterProvided; 1357 } 1358 1359 1360 1361 /** 1362 * Creates a search request containing the base DN, scope, filter, and 1363 * requested attributes from this LDAP URL. 1364 * 1365 * @return The search request created from the base DN, scope, filter, and 1366 * requested attributes from this LDAP URL. 1367 */ 1368 public SearchRequest toSearchRequest() 1369 { 1370 return new SearchRequest(baseDN.toString(), scope, filter, attributes); 1371 } 1372 1373 1374 1375 /** 1376 * Retrieves a hash code for this LDAP URL. 1377 * 1378 * @return A hash code for this LDAP URL. 1379 */ 1380 @Override() 1381 public int hashCode() 1382 { 1383 return toNormalizedString().hashCode(); 1384 } 1385 1386 1387 1388 /** 1389 * Indicates whether the provided object is equal to this LDAP URL. In order 1390 * to be considered equal, the provided object must be an LDAP URL with the 1391 * same normalized string representation. 1392 * 1393 * @param o The object for which to make the determination. 1394 * 1395 * @return {@code true} if the provided object is equal to this LDAP URL, or 1396 * {@code false} if not. 1397 */ 1398 @Override() 1399 public boolean equals(final Object o) 1400 { 1401 if (o == null) 1402 { 1403 return false; 1404 } 1405 1406 if (o == this) 1407 { 1408 return true; 1409 } 1410 1411 if (! (o instanceof LDAPURL)) 1412 { 1413 return false; 1414 } 1415 1416 final LDAPURL url = (LDAPURL) o; 1417 return toNormalizedString().equals(url.toNormalizedString()); 1418 } 1419 1420 1421 1422 /** 1423 * Retrieves a string representation of this LDAP URL. 1424 * 1425 * @return A string representation of this LDAP URL. 1426 */ 1427 @Override() 1428 public String toString() 1429 { 1430 return urlString; 1431 } 1432 1433 1434 1435 /** 1436 * Retrieves a normalized string representation of this LDAP URL. 1437 * 1438 * @return A normalized string representation of this LDAP URL. 1439 */ 1440 public String toNormalizedString() 1441 { 1442 if (normalizedURLString == null) 1443 { 1444 final StringBuilder buffer = new StringBuilder(); 1445 toNormalizedString(buffer); 1446 normalizedURLString = buffer.toString(); 1447 } 1448 1449 return normalizedURLString; 1450 } 1451 1452 1453 1454 /** 1455 * Appends a normalized string representation of this LDAP URL to the provided 1456 * buffer. 1457 * 1458 * @param buffer The buffer to which to append the normalized string 1459 * representation of this LDAP URL. 1460 */ 1461 public void toNormalizedString(final StringBuilder buffer) 1462 { 1463 buffer.append(scheme); 1464 buffer.append("://"); 1465 1466 if (host != null) 1467 { 1468 if (host.indexOf(':') >= 0) 1469 { 1470 buffer.append('['); 1471 buffer.append(toLowerCase(host)); 1472 buffer.append(']'); 1473 } 1474 else 1475 { 1476 buffer.append(toLowerCase(host)); 1477 } 1478 } 1479 1480 if (! scheme.equals("ldapi")) 1481 { 1482 buffer.append(':'); 1483 buffer.append(port); 1484 } 1485 1486 buffer.append('/'); 1487 percentEncode(baseDN.toNormalizedString(), buffer); 1488 buffer.append('?'); 1489 1490 for (int i=0; i < attributes.length; i++) 1491 { 1492 if (i > 0) 1493 { 1494 buffer.append(','); 1495 } 1496 1497 buffer.append(toLowerCase(attributes[i])); 1498 } 1499 1500 buffer.append('?'); 1501 switch (scope.intValue()) 1502 { 1503 case 0: // BASE 1504 buffer.append("base"); 1505 break; 1506 case 1: // ONE 1507 buffer.append("one"); 1508 break; 1509 case 2: // SUB 1510 buffer.append("sub"); 1511 break; 1512 case 3: // SUBORDINATE_SUBTREE 1513 buffer.append("subordinates"); 1514 break; 1515 } 1516 1517 buffer.append('?'); 1518 percentEncode(filter.toNormalizedString(), buffer); 1519 } 1520}