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