001/* 002 * Copyright 2007-2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-2020 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) 2008-2020 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.StaticUtils; 047import com.unboundid.util.ThreadSafety; 048import com.unboundid.util.ThreadSafetyLevel; 049import com.unboundid.util.Validator; 050 051import static com.unboundid.ldap.sdk.LDAPMessages.*; 052 053 054 055/** 056 * This class provides a data structure for interacting with LDAP URLs. It may 057 * be used to encode and decode URLs, as well as access the various elements 058 * that they contain. Note that this implementation currently does not support 059 * the use of extensions in an LDAP URL. 060 * <BR><BR> 061 * The components that may be included in an LDAP URL include: 062 * <UL> 063 * <LI>Scheme -- This specifies the protocol to use when communicating with 064 * the server. The official LDAP URL specification only allows a scheme 065 * of "{@code ldap}", but this implementation also supports the use of the 066 * "{@code ldaps}" scheme to indicate that clients should attempt to 067 * perform SSL-based communication with the target server (LDAPS) rather 068 * than unencrypted LDAP. It will also accept "{@code ldapi}", which is 069 * LDAP over UNIX domain sockets, although the LDAP SDK does not directly 070 * support that mechanism of communication.</LI> 071 * <LI>Host -- This specifies the address of the directory server to which the 072 * URL refers. If no host is provided, then it is expected that the 073 * client has some prior knowledge of the host (it often implies the same 074 * server from which the URL was retrieved).</LI> 075 * <LI>Port -- This specifies the port of the directory server to which the 076 * URL refers. If no host or port is provided, then it is assumed that 077 * the client has some prior knowledge of the instance to use (it often 078 * implies the same instance from which the URL was retrieved). If a host 079 * is provided without a port, then it should be assumed that the standard 080 * LDAP port of 389 should be used (or the standard LDAPS port of 636 if 081 * the scheme is "{@code ldaps}", or a value of 0 if the scheme is 082 * "{@code ldapi}").</LI> 083 * <LI>Base DN -- This specifies the base DN for the URL. If no base DN is 084 * provided, then a default of the null DN should be assumed.</LI> 085 * <LI>Requested attributes -- This specifies the set of requested attributes 086 * for the URL. If no attributes are specified, then the behavior should 087 * be the same as if no attributes had been provided for a search request 088 * (i.e., all user attributes should be included). 089 * <BR><BR> 090 * In the string representation of an LDAP URL, the names of the requested 091 * attributes (if more than one is provided) should be separated by 092 * commas.</LI> 093 * <LI>Scope -- This specifies the scope for the URL. It should be one of the 094 * standard scope values as defined in the {@link SearchRequest} 095 * class. If no scope is provided, then it should be assumed that a 096 * scope of {@link SearchScope#BASE} should be used. 097 * <BR><BR> 098 * In the string representation, the names of the scope values that are 099 * allowed include: 100 * <UL> 101 * <LI>base -- Equivalent to {@link SearchScope#BASE}.</LI> 102 * <LI>one -- Equivalent to {@link SearchScope#ONE}.</LI> 103 * <LI>sub -- Equivalent to {@link SearchScope#SUB}.</LI> 104 * <LI>subordinates -- Equivalent to 105 * {@link SearchScope#SUBORDINATE_SUBTREE}.</LI> 106 * </UL></LI> 107 * <LI>Filter -- This specifies the filter for the URL. If no filter is 108 * provided, then a default of "{@code (objectClass=*)}" should be 109 * assumed.</LI> 110 * </UL> 111 * An LDAP URL encapsulates many of the properties of a search request, and in 112 * fact the {@link LDAPURL#toSearchRequest} method may be used to create a 113 * {@link SearchRequest} object from an LDAP URL. 114 * <BR><BR> 115 * See <A HREF="http://www.ietf.org/rfc/rfc4516.txt">RFC 4516</A> for a complete 116 * description of the LDAP URL syntax. Some examples of LDAP URLs include: 117 * <UL> 118 * <LI>{@code ldap://} -- This is the smallest possible LDAP URL that can be 119 * represented. The default values will be used for all components other 120 * than the scheme.</LI> 121 * <LI>{@code 122 * ldap://server.example.com:1234/dc=example,dc=com?cn,sn?sub?(uid=john)} 123 * -- This is an example of a URL containing all of the elements. The 124 * scheme is "{@code ldap}", the host is "{@code server.example.com}", 125 * the port is "{@code 1234}", the base DN is "{@code dc=example,dc=com}", 126 * the requested attributes are "{@code cn}" and "{@code sn}", the scope 127 * is "{@code sub}" (which indicates a subtree scope equivalent to 128 * {@link SearchScope#SUB}), and a filter of 129 * "{@code (uid=john)}".</LI> 130 * </UL> 131 */ 132@NotMutable() 133@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 134public final class LDAPURL 135 implements Serializable 136{ 137 /** 138 * The default filter that will be used if none is provided. 139 */ 140 private static final Filter DEFAULT_FILTER = 141 Filter.createPresenceFilter("objectClass"); 142 143 144 145 /** 146 * The default port number that will be used for LDAP URLs if none is 147 * provided. 148 */ 149 public static final int DEFAULT_LDAP_PORT = 389; 150 151 152 153 /** 154 * The default port number that will be used for LDAPS URLs if none is 155 * provided. 156 */ 157 public static final int DEFAULT_LDAPS_PORT = 636; 158 159 160 161 /** 162 * The default port number that will be used for LDAPI URLs if none is 163 * provided. 164 */ 165 public static final int DEFAULT_LDAPI_PORT = 0; 166 167 168 169 /** 170 * The default scope that will be used if none is provided. 171 */ 172 private static final SearchScope DEFAULT_SCOPE = SearchScope.BASE; 173 174 175 176 /** 177 * The default base DN that will be used if none is provided. 178 */ 179 private static final DN DEFAULT_BASE_DN = DN.NULL_DN; 180 181 182 183 /** 184 * The default set of attributes that will be used if none is provided. 185 */ 186 private static final String[] DEFAULT_ATTRIBUTES = StaticUtils.NO_STRINGS; 187 188 189 190 /** 191 * The serial version UID for this serializable class. 192 */ 193 private static final long serialVersionUID = 3420786933570240493L; 194 195 196 197 // Indicates whether the attribute list was provided in the URL. 198 private final boolean attributesProvided; 199 200 // Indicates whether the base DN was provided in the URL. 201 private final boolean baseDNProvided; 202 203 // Indicates whether the filter was provided in the URL. 204 private final boolean filterProvided; 205 206 // Indicates whether the port was provided in the URL. 207 private final boolean portProvided; 208 209 // Indicates whether the scope was provided in the URL. 210 private final boolean scopeProvided; 211 212 // The base DN used by this URL. 213 private final DN baseDN; 214 215 // The filter used by this URL. 216 private final Filter filter; 217 218 // The port used by this URL. 219 private final int port; 220 221 // The search scope used by this URL. 222 private final SearchScope scope; 223 224 // The host used by this URL. 225 private final String host; 226 227 // The normalized representation of this LDAP URL. 228 private volatile String normalizedURLString; 229 230 // The scheme used by this LDAP URL. The standard only accepts "ldap", but 231 // we will also accept "ldaps" and "ldapi". 232 private final String scheme; 233 234 // The string representation of this LDAP URL. 235 private final String urlString; 236 237 // The set of attributes included in this URL. 238 private final String[] attributes; 239 240 241 242 /** 243 * Creates a new LDAP URL from the provided string representation. 244 * 245 * @param urlString The string representation for this LDAP URL. It must 246 * not be {@code null}. 247 * 248 * @throws LDAPException If the provided URL string cannot be parsed as an 249 * LDAP URL. 250 */ 251 public LDAPURL(final String urlString) 252 throws LDAPException 253 { 254 Validator.ensureNotNull(urlString); 255 256 this.urlString = urlString; 257 258 259 // Find the location of the first colon. It should mark the end of the 260 // scheme. 261 final int colonPos = urlString.indexOf("://"); 262 if (colonPos < 0) 263 { 264 throw new LDAPException(ResultCode.DECODING_ERROR, 265 ERR_LDAPURL_NO_COLON_SLASHES.get()); 266 } 267 268 scheme = StaticUtils.toLowerCase(urlString.substring(0, colonPos)); 269 final int defaultPort; 270 if (scheme.equals("ldap")) 271 { 272 defaultPort = DEFAULT_LDAP_PORT; 273 } 274 else if (scheme.equals("ldaps")) 275 { 276 defaultPort = DEFAULT_LDAPS_PORT; 277 } 278 else if (scheme.equals("ldapi")) 279 { 280 defaultPort = DEFAULT_LDAPI_PORT; 281 } 282 else 283 { 284 throw new LDAPException(ResultCode.DECODING_ERROR, 285 ERR_LDAPURL_INVALID_SCHEME.get(scheme)); 286 } 287 288 289 // Look for the first slash after the "://". It will designate the end of 290 // the hostport section. 291 final int slashPos = urlString.indexOf('/', colonPos+3); 292 if (slashPos < 0) 293 { 294 // This is fine. It just means that the URL won't have a base DN, 295 // attribute list, scope, or filter, and that the rest of the value is 296 // the hostport element. 297 baseDN = DEFAULT_BASE_DN; 298 baseDNProvided = false; 299 attributes = DEFAULT_ATTRIBUTES; 300 attributesProvided = false; 301 scope = DEFAULT_SCOPE; 302 scopeProvided = false; 303 filter = DEFAULT_FILTER; 304 filterProvided = false; 305 306 final String hostPort = urlString.substring(colonPos+3); 307 final StringBuilder hostBuffer = new StringBuilder(hostPort.length()); 308 final int portValue = decodeHostPort(hostPort, hostBuffer); 309 if (portValue < 0) 310 { 311 port = defaultPort; 312 portProvided = false; 313 } 314 else 315 { 316 port = portValue; 317 portProvided = true; 318 } 319 320 if (hostBuffer.length() == 0) 321 { 322 host = null; 323 } 324 else 325 { 326 host = hostBuffer.toString(); 327 } 328 return; 329 } 330 331 final String hostPort = urlString.substring(colonPos+3, slashPos); 332 final StringBuilder hostBuffer = new StringBuilder(hostPort.length()); 333 final int portValue = decodeHostPort(hostPort, hostBuffer); 334 if (portValue < 0) 335 { 336 port = defaultPort; 337 portProvided = false; 338 } 339 else 340 { 341 port = portValue; 342 portProvided = true; 343 } 344 345 if (hostBuffer.length() == 0) 346 { 347 host = null; 348 } 349 else 350 { 351 host = hostBuffer.toString(); 352 } 353 354 355 // Look for the first question mark after the slash. It will designate the 356 // end of the base DN. 357 final int questionMarkPos = urlString.indexOf('?', slashPos+1); 358 if (questionMarkPos < 0) 359 { 360 // This is fine. It just means that the URL won't have an attribute list, 361 // scope, or filter, and that the rest of the value is the base DN. 362 attributes = DEFAULT_ATTRIBUTES; 363 attributesProvided = false; 364 scope = DEFAULT_SCOPE; 365 scopeProvided = false; 366 filter = DEFAULT_FILTER; 367 filterProvided = false; 368 369 baseDN = new DN(percentDecode(urlString.substring(slashPos+1))); 370 baseDNProvided = (! baseDN.isNullDN()); 371 return; 372 } 373 374 baseDN = new DN(percentDecode(urlString.substring(slashPos+1, 375 questionMarkPos))); 376 baseDNProvided = (! baseDN.isNullDN()); 377 378 379 // Look for the next question mark. It will designate the end of the 380 // attribute list. 381 final int questionMark2Pos = urlString.indexOf('?', questionMarkPos+1); 382 if (questionMark2Pos < 0) 383 { 384 // This is fine. It just means that the URL won't have a scope or filter, 385 // and that the rest of the value is the attribute list. 386 scope = DEFAULT_SCOPE; 387 scopeProvided = false; 388 filter = DEFAULT_FILTER; 389 filterProvided = false; 390 391 attributes = decodeAttributes(urlString.substring(questionMarkPos+1)); 392 attributesProvided = (attributes.length > 0); 393 return; 394 } 395 396 attributes = decodeAttributes(urlString.substring(questionMarkPos+1, 397 questionMark2Pos)); 398 attributesProvided = (attributes.length > 0); 399 400 401 // Look for the next question mark. It will designate the end of the scope. 402 final int questionMark3Pos = urlString.indexOf('?', questionMark2Pos+1); 403 if (questionMark3Pos < 0) 404 { 405 // This is fine. It just means that the URL won't have a filter, and that 406 // the rest of the value is the scope. 407 filter = DEFAULT_FILTER; 408 filterProvided = false; 409 410 final String scopeStr = 411 StaticUtils.toLowerCase(urlString.substring(questionMark2Pos+1)); 412 if (scopeStr.isEmpty()) 413 { 414 scope = SearchScope.BASE; 415 scopeProvided = false; 416 } 417 else if (scopeStr.equals("base")) 418 { 419 scope = SearchScope.BASE; 420 scopeProvided = true; 421 } 422 else if (scopeStr.equals("one")) 423 { 424 scope = SearchScope.ONE; 425 scopeProvided = true; 426 } 427 else if (scopeStr.equals("sub")) 428 { 429 scope = SearchScope.SUB; 430 scopeProvided = true; 431 } 432 else if (scopeStr.equals("subord") || scopeStr.equals("subordinates")) 433 { 434 scope = SearchScope.SUBORDINATE_SUBTREE; 435 scopeProvided = true; 436 } 437 else 438 { 439 throw new LDAPException(ResultCode.DECODING_ERROR, 440 ERR_LDAPURL_INVALID_SCOPE.get(scopeStr)); 441 } 442 return; 443 } 444 445 final String scopeStr = StaticUtils.toLowerCase( 446 urlString.substring(questionMark2Pos+1, questionMark3Pos)); 447 if (scopeStr.isEmpty()) 448 { 449 scope = SearchScope.BASE; 450 scopeProvided = false; 451 } 452 else if (scopeStr.equals("base")) 453 { 454 scope = SearchScope.BASE; 455 scopeProvided = true; 456 } 457 else if (scopeStr.equals("one")) 458 { 459 scope = SearchScope.ONE; 460 scopeProvided = true; 461 } 462 else if (scopeStr.equals("sub")) 463 { 464 scope = SearchScope.SUB; 465 scopeProvided = true; 466 } 467 else if (scopeStr.equals("subord") || scopeStr.equals("subordinates")) 468 { 469 scope = SearchScope.SUBORDINATE_SUBTREE; 470 scopeProvided = true; 471 } 472 else 473 { 474 throw new LDAPException(ResultCode.DECODING_ERROR, 475 ERR_LDAPURL_INVALID_SCOPE.get(scopeStr)); 476 } 477 478 479 // The remainder of the value must be the filter. 480 final String filterStr = 481 percentDecode(urlString.substring(questionMark3Pos+1)); 482 if (filterStr.isEmpty()) 483 { 484 filter = DEFAULT_FILTER; 485 filterProvided = false; 486 } 487 else 488 { 489 filter = Filter.create(filterStr); 490 filterProvided = true; 491 } 492 } 493 494 495 496 /** 497 * Creates a new LDAP URL with the provided information. 498 * 499 * @param scheme The scheme for this LDAP URL. It must not be 500 * {@code null} and must be either "ldap", "ldaps", or 501 * "ldapi". 502 * @param host The host for this LDAP URL. It may be {@code null} if 503 * no host is to be included. 504 * @param port The port for this LDAP URL. It may be {@code null} if 505 * no port is to be included. If it is provided, it must 506 * be between 1 and 65535, inclusive. 507 * @param baseDN The base DN for this LDAP URL. It may be {@code null} 508 * if no base DN is to be included. 509 * @param attributes The set of requested attributes for this LDAP URL. It 510 * may be {@code null} or empty if no attribute list is to 511 * be included. 512 * @param scope The scope for this LDAP URL. It may be {@code null} if 513 * no scope is to be included. Otherwise, it must be a 514 * value between zero and three, inclusive. 515 * @param filter The filter for this LDAP URL. It may be {@code null} 516 * if no filter is to be included. 517 * 518 * @throws LDAPException If there is a problem with any of the provided 519 * arguments. 520 */ 521 public LDAPURL(final String scheme, final String host, final Integer port, 522 final DN baseDN, final String[] attributes, 523 final SearchScope scope, final Filter filter) 524 throws LDAPException 525 { 526 Validator.ensureNotNull(scheme); 527 528 final StringBuilder buffer = new StringBuilder(); 529 530 this.scheme = StaticUtils.toLowerCase(scheme); 531 final int defaultPort; 532 if (scheme.equals("ldap")) 533 { 534 defaultPort = DEFAULT_LDAP_PORT; 535 } 536 else if (scheme.equals("ldaps")) 537 { 538 defaultPort = DEFAULT_LDAPS_PORT; 539 } 540 else if (scheme.equals("ldapi")) 541 { 542 defaultPort = DEFAULT_LDAPI_PORT; 543 } 544 else 545 { 546 throw new LDAPException(ResultCode.DECODING_ERROR, 547 ERR_LDAPURL_INVALID_SCHEME.get(scheme)); 548 } 549 550 buffer.append(scheme); 551 buffer.append("://"); 552 553 if ((host == null) || host.isEmpty()) 554 { 555 this.host = null; 556 } 557 else 558 { 559 this.host = host; 560 buffer.append(host); 561 } 562 563 if (port == null) 564 { 565 this.port = defaultPort; 566 portProvided = false; 567 } 568 else 569 { 570 this.port = port; 571 portProvided = true; 572 buffer.append(':'); 573 buffer.append(port); 574 575 if ((port < 1) || (port > 65_535)) 576 { 577 throw new LDAPException(ResultCode.PARAM_ERROR, 578 ERR_LDAPURL_INVALID_PORT.get(port)); 579 } 580 } 581 582 buffer.append('/'); 583 if (baseDN == null) 584 { 585 this.baseDN = DEFAULT_BASE_DN; 586 baseDNProvided = false; 587 } 588 else 589 { 590 this.baseDN = baseDN; 591 baseDNProvided = true; 592 percentEncode(baseDN.toString(), buffer); 593 } 594 595 final boolean continueAppending; 596 if (((attributes == null) || (attributes.length == 0)) && (scope == null) && 597 (filter == null)) 598 { 599 continueAppending = false; 600 } 601 else 602 { 603 continueAppending = true; 604 } 605 606 if (continueAppending) 607 { 608 buffer.append('?'); 609 } 610 if ((attributes == null) || (attributes.length == 0)) 611 { 612 this.attributes = DEFAULT_ATTRIBUTES; 613 attributesProvided = false; 614 } 615 else 616 { 617 this.attributes = attributes; 618 attributesProvided = true; 619 620 for (int i=0; i < attributes.length; i++) 621 { 622 if (i > 0) 623 { 624 buffer.append(','); 625 } 626 buffer.append(attributes[i]); 627 } 628 } 629 630 if (continueAppending) 631 { 632 buffer.append('?'); 633 } 634 if (scope == null) 635 { 636 this.scope = DEFAULT_SCOPE; 637 scopeProvided = false; 638 } 639 else 640 { 641 switch (scope.intValue()) 642 { 643 case 0: 644 this.scope = scope; 645 scopeProvided = true; 646 buffer.append("base"); 647 break; 648 case 1: 649 this.scope = scope; 650 scopeProvided = true; 651 buffer.append("one"); 652 break; 653 case 2: 654 this.scope = scope; 655 scopeProvided = true; 656 buffer.append("sub"); 657 break; 658 case 3: 659 this.scope = scope; 660 scopeProvided = true; 661 buffer.append("subordinates"); 662 break; 663 default: 664 throw new LDAPException(ResultCode.PARAM_ERROR, 665 ERR_LDAPURL_INVALID_SCOPE_VALUE.get(scope)); 666 } 667 } 668 669 if (continueAppending) 670 { 671 buffer.append('?'); 672 } 673 if (filter == null) 674 { 675 this.filter = DEFAULT_FILTER; 676 filterProvided = false; 677 } 678 else 679 { 680 this.filter = filter; 681 filterProvided = true; 682 percentEncode(filter.toString(), buffer); 683 } 684 685 urlString = buffer.toString(); 686 } 687 688 689 690 /** 691 * Decodes the provided string as a host and optional port number. 692 * 693 * @param hostPort The string to be decoded. 694 * @param hostBuffer The buffer to which the decoded host address will be 695 * appended. 696 * 697 * @return The port number decoded from the provided string, or -1 if there 698 * was no port number. 699 * 700 * @throws LDAPException If the provided string cannot be decoded as a 701 * hostport element. 702 */ 703 private static int decodeHostPort(final String hostPort, 704 final StringBuilder hostBuffer) 705 throws LDAPException 706 { 707 final int length = hostPort.length(); 708 if (length == 0) 709 { 710 // It's an empty string, so we'll just use the defaults. 711 return -1; 712 } 713 714 if (hostPort.charAt(0) == '[') 715 { 716 // It starts with a square bracket, which means that the address is an 717 // IPv6 literal address. Find the closing bracket, and the address 718 // will be inside them. 719 final int closingBracketPos = hostPort.indexOf(']'); 720 if (closingBracketPos < 0) 721 { 722 throw new LDAPException(ResultCode.DECODING_ERROR, 723 ERR_LDAPURL_IPV6_HOST_MISSING_BRACKET.get()); 724 } 725 726 hostBuffer.append(hostPort.substring(1, closingBracketPos).trim()); 727 if (hostBuffer.length() == 0) 728 { 729 throw new LDAPException(ResultCode.DECODING_ERROR, 730 ERR_LDAPURL_IPV6_HOST_EMPTY.get()); 731 } 732 733 // The closing bracket must either be the end of the hostport element 734 // (in which case we'll use the default port), or it must be followed by 735 // a colon and an integer (which will be the port). 736 if (closingBracketPos == (length - 1)) 737 { 738 return -1; 739 } 740 else 741 { 742 if (hostPort.charAt(closingBracketPos+1) != ':') 743 { 744 throw new LDAPException(ResultCode.DECODING_ERROR, 745 ERR_LDAPURL_IPV6_HOST_UNEXPECTED_CHAR.get( 746 hostPort.charAt(closingBracketPos+1))); 747 } 748 else 749 { 750 try 751 { 752 final int decodedPort = 753 Integer.parseInt(hostPort.substring(closingBracketPos+2)); 754 if ((decodedPort >= 1) && (decodedPort <= 65_535)) 755 { 756 return decodedPort; 757 } 758 else 759 { 760 throw new LDAPException(ResultCode.DECODING_ERROR, 761 ERR_LDAPURL_INVALID_PORT.get( 762 decodedPort)); 763 } 764 } 765 catch (final NumberFormatException nfe) 766 { 767 Debug.debugException(nfe); 768 throw new LDAPException(ResultCode.DECODING_ERROR, 769 ERR_LDAPURL_PORT_NOT_INT.get(hostPort), 770 nfe); 771 } 772 } 773 } 774 } 775 776 777 // If we've gotten here, then the address is either a resolvable name or an 778 // IPv4 address. If there is a colon in the string, then it will separate 779 // the address from the port. Otherwise, the remaining value will be the 780 // address and we'll use the default port. 781 final int colonPos = hostPort.indexOf(':'); 782 if (colonPos < 0) 783 { 784 hostBuffer.append(hostPort); 785 return -1; 786 } 787 else 788 { 789 try 790 { 791 final int decodedPort = 792 Integer.parseInt(hostPort.substring(colonPos+1)); 793 if ((decodedPort >= 1) && (decodedPort <= 65_535)) 794 { 795 hostBuffer.append(hostPort.substring(0, colonPos)); 796 return decodedPort; 797 } 798 else 799 { 800 throw new LDAPException(ResultCode.DECODING_ERROR, 801 ERR_LDAPURL_INVALID_PORT.get(decodedPort)); 802 } 803 } 804 catch (final NumberFormatException nfe) 805 { 806 Debug.debugException(nfe); 807 throw new LDAPException(ResultCode.DECODING_ERROR, 808 ERR_LDAPURL_PORT_NOT_INT.get(hostPort), nfe); 809 } 810 } 811 } 812 813 814 815 /** 816 * Decodes the contents of the provided string as an attribute list. 817 * 818 * @param s The string to decode as an attribute list. 819 * 820 * @return The array of decoded attribute names. 821 * 822 * @throws LDAPException If an error occurred while attempting to decode the 823 * attribute list. 824 */ 825 private static String[] decodeAttributes(final String s) 826 throws LDAPException 827 { 828 final int length = s.length(); 829 if (length == 0) 830 { 831 return DEFAULT_ATTRIBUTES; 832 } 833 834 final ArrayList<String> attrList = new ArrayList<>(10); 835 int startPos = 0; 836 while (startPos < length) 837 { 838 final int commaPos = s.indexOf(',', startPos); 839 if (commaPos < 0) 840 { 841 // There are no more commas, so there can only be one attribute left. 842 final String attrName = s.substring(startPos).trim(); 843 if (attrName.isEmpty()) 844 { 845 // This is only acceptable if the attribute list is empty (there was 846 // probably a space in the attribute list string, which is technically 847 // not allowed, but we'll accept it). If the attribute list is not 848 // empty, then there were two consecutive commas, which is not 849 // allowed. 850 if (attrList.isEmpty()) 851 { 852 return DEFAULT_ATTRIBUTES; 853 } 854 else 855 { 856 throw new LDAPException(ResultCode.DECODING_ERROR, 857 ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get()); 858 } 859 } 860 else 861 { 862 attrList.add(attrName); 863 break; 864 } 865 } 866 else 867 { 868 final String attrName = s.substring(startPos, commaPos).trim(); 869 if (attrName.isEmpty()) 870 { 871 throw new LDAPException(ResultCode.DECODING_ERROR, 872 ERR_LDAPURL_ATTRLIST_EMPTY_ATTRIBUTE.get()); 873 } 874 else 875 { 876 attrList.add(attrName); 877 startPos = commaPos+1; 878 if (startPos >= length) 879 { 880 throw new LDAPException(ResultCode.DECODING_ERROR, 881 ERR_LDAPURL_ATTRLIST_ENDS_WITH_COMMA.get()); 882 } 883 } 884 } 885 } 886 887 final String[] attributes = new String[attrList.size()]; 888 attrList.toArray(attributes); 889 return attributes; 890 } 891 892 893 894 /** 895 * Decodes any percent-encoded values that may be contained in the provided 896 * string. 897 * 898 * @param s The string to be decoded. 899 * 900 * @return The percent-decoded form of the provided string. 901 * 902 * @throws LDAPException If a problem occurs while attempting to decode the 903 * provided string. 904 */ 905 public static String percentDecode(final String s) 906 throws LDAPException 907 { 908 // First, see if there are any percent characters at all in the provided 909 // string. If not, then just return the string as-is. 910 int firstPercentPos = -1; 911 final int length = s.length(); 912 for (int i=0; i < length; i++) 913 { 914 if (s.charAt(i) == '%') 915 { 916 firstPercentPos = i; 917 break; 918 } 919 } 920 921 if (firstPercentPos < 0) 922 { 923 return s; 924 } 925 926 int pos = firstPercentPos; 927 final ByteStringBuffer buffer = new ByteStringBuffer(2 * length); 928 buffer.append(s.substring(0, firstPercentPos)); 929 930 while (pos < length) 931 { 932 final char c = s.charAt(pos++); 933 if (c == '%') 934 { 935 if (pos >= length) 936 { 937 throw new LDAPException(ResultCode.DECODING_ERROR, 938 ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s)); 939 } 940 941 final byte b; 942 switch (s.charAt(pos++)) 943 { 944 case '0': 945 b = 0x00; 946 break; 947 case '1': 948 b = 0x10; 949 break; 950 case '2': 951 b = 0x20; 952 break; 953 case '3': 954 b = 0x30; 955 break; 956 case '4': 957 b = 0x40; 958 break; 959 case '5': 960 b = 0x50; 961 break; 962 case '6': 963 b = 0x60; 964 break; 965 case '7': 966 b = 0x70; 967 break; 968 case '8': 969 b = (byte) 0x80; 970 break; 971 case '9': 972 b = (byte) 0x90; 973 break; 974 case 'a': 975 case 'A': 976 b = (byte) 0xA0; 977 break; 978 case 'b': 979 case 'B': 980 b = (byte) 0xB0; 981 break; 982 case 'c': 983 case 'C': 984 b = (byte) 0xC0; 985 break; 986 case 'd': 987 case 'D': 988 b = (byte) 0xD0; 989 break; 990 case 'e': 991 case 'E': 992 b = (byte) 0xE0; 993 break; 994 case 'f': 995 case 'F': 996 b = (byte) 0xF0; 997 break; 998 default: 999 throw new LDAPException(ResultCode.DECODING_ERROR, 1000 ERR_LDAPURL_INVALID_HEX_CHAR.get( 1001 s.charAt(pos-1))); 1002 } 1003 1004 if (pos >= length) 1005 { 1006 throw new LDAPException(ResultCode.DECODING_ERROR, 1007 ERR_LDAPURL_HEX_STRING_TOO_SHORT.get(s)); 1008 } 1009 1010 switch (s.charAt(pos++)) 1011 { 1012 case '0': 1013 buffer.append(b); 1014 break; 1015 case '1': 1016 buffer.append((byte) (b | 0x01)); 1017 break; 1018 case '2': 1019 buffer.append((byte) (b | 0x02)); 1020 break; 1021 case '3': 1022 buffer.append((byte) (b | 0x03)); 1023 break; 1024 case '4': 1025 buffer.append((byte) (b | 0x04)); 1026 break; 1027 case '5': 1028 buffer.append((byte) (b | 0x05)); 1029 break; 1030 case '6': 1031 buffer.append((byte) (b | 0x06)); 1032 break; 1033 case '7': 1034 buffer.append((byte) (b | 0x07)); 1035 break; 1036 case '8': 1037 buffer.append((byte) (b | 0x08)); 1038 break; 1039 case '9': 1040 buffer.append((byte) (b | 0x09)); 1041 break; 1042 case 'a': 1043 case 'A': 1044 buffer.append((byte) (b | 0x0A)); 1045 break; 1046 case 'b': 1047 case 'B': 1048 buffer.append((byte) (b | 0x0B)); 1049 break; 1050 case 'c': 1051 case 'C': 1052 buffer.append((byte) (b | 0x0C)); 1053 break; 1054 case 'd': 1055 case 'D': 1056 buffer.append((byte) (b | 0x0D)); 1057 break; 1058 case 'e': 1059 case 'E': 1060 buffer.append((byte) (b | 0x0E)); 1061 break; 1062 case 'f': 1063 case 'F': 1064 buffer.append((byte) (b | 0x0F)); 1065 break; 1066 default: 1067 throw new LDAPException(ResultCode.DECODING_ERROR, 1068 ERR_LDAPURL_INVALID_HEX_CHAR.get( 1069 s.charAt(pos-1))); 1070 } 1071 } 1072 else 1073 { 1074 buffer.append(c); 1075 } 1076 } 1077 1078 return buffer.toString(); 1079 } 1080 1081 1082 1083 /** 1084 * Appends an encoded version of the provided string to the given buffer. Any 1085 * special characters contained in the string will be replaced with byte 1086 * representations consisting of one percent sign and two hexadecimal digits 1087 * for each byte in the special character. 1088 * 1089 * @param s The string to be encoded. 1090 * @param buffer The buffer to which the encoded string will be written. 1091 */ 1092 private static void percentEncode(final String s, final StringBuilder buffer) 1093 { 1094 final int length = s.length(); 1095 for (int i=0; i < length; i++) 1096 { 1097 final char c = s.charAt(i); 1098 1099 switch (c) 1100 { 1101 case 'A': 1102 case 'B': 1103 case 'C': 1104 case 'D': 1105 case 'E': 1106 case 'F': 1107 case 'G': 1108 case 'H': 1109 case 'I': 1110 case 'J': 1111 case 'K': 1112 case 'L': 1113 case 'M': 1114 case 'N': 1115 case 'O': 1116 case 'P': 1117 case 'Q': 1118 case 'R': 1119 case 'S': 1120 case 'T': 1121 case 'U': 1122 case 'V': 1123 case 'W': 1124 case 'X': 1125 case 'Y': 1126 case 'Z': 1127 case 'a': 1128 case 'b': 1129 case 'c': 1130 case 'd': 1131 case 'e': 1132 case 'f': 1133 case 'g': 1134 case 'h': 1135 case 'i': 1136 case 'j': 1137 case 'k': 1138 case 'l': 1139 case 'm': 1140 case 'n': 1141 case 'o': 1142 case 'p': 1143 case 'q': 1144 case 'r': 1145 case 's': 1146 case 't': 1147 case 'u': 1148 case 'v': 1149 case 'w': 1150 case 'x': 1151 case 'y': 1152 case 'z': 1153 case '0': 1154 case '1': 1155 case '2': 1156 case '3': 1157 case '4': 1158 case '5': 1159 case '6': 1160 case '7': 1161 case '8': 1162 case '9': 1163 case '-': 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 buffer.append(c); 1179 break; 1180 1181 default: 1182 final byte[] charBytes = 1183 StaticUtils.getBytes(new String(new char[] { c })); 1184 for (final byte b : charBytes) 1185 { 1186 buffer.append('%'); 1187 StaticUtils.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(StaticUtils.toLowerCase(host)); 1472 buffer.append(']'); 1473 } 1474 else 1475 { 1476 buffer.append(StaticUtils.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(StaticUtils.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}