001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 * 019 */ 020package org.apache.directory.api.ldap.model.url; 021 022 023import java.io.ByteArrayOutputStream; 024import java.nio.charset.StandardCharsets; 025import java.text.ParseException; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Set; 031import java.util.regex.Matcher; 032import java.util.regex.Pattern; 033 034import org.apache.directory.api.i18n.I18n; 035import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException; 036import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException; 037import org.apache.directory.api.ldap.model.exception.LdapUriException; 038import org.apache.directory.api.ldap.model.exception.UrlDecoderException; 039import org.apache.directory.api.ldap.model.filter.FilterParser; 040import org.apache.directory.api.ldap.model.message.SearchScope; 041import org.apache.directory.api.ldap.model.name.Dn; 042import org.apache.directory.api.util.Chars; 043import org.apache.directory.api.util.StringConstants; 044import org.apache.directory.api.util.Strings; 045import org.apache.directory.api.util.Unicode; 046 047 048/** 049 * Decodes a LdapUrl, and checks that it complies with 050 * the RFC 4516. The grammar is the following : 051 * <pre> 052 * ldapurl = scheme "://" [host [ ":" port]] ["/" 053 * dn ["?" [attributes] ["?" [scope] 054 * ["?" [filter] ["?" extensions]]]]] 055 * scheme = "ldap" 056 * dn = Dn 057 * attributes = attrdesc ["," attrdesc]* 058 * attrdesc = selector ["," selector]* 059 * selector = attributeSelector (from Section 4.5.1 of RFC4511) 060 * scope = "base" / "one" / "sub" 061 * extensions = extension ["," extension]* 062 * extension = ["!"] extype ["=" exvalue] 063 * extype = oid (from Section 1.4 of RFC4512) 064 * exvalue = LDAPString (from Section 4.1.2 of RFC4511) 065 * host = host from Section 3.2.2 of RFC3986 066 * port = port from Section 3.2.3 of RFC3986 067 * filter = filter from Section 3 of RFC 4515 068 * </pre> 069 * 070 * From Section 3.2.1/2 of RFC3986 071 * <pre> 072 * host = IP-literal / IPv4address / reg-name 073 * port = *DIGIT 074 * IP-literal = "[" ( IPv6address / IPvFuture ) "]" 075 * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) 076 * IPv6address = 6( h16 ":" ) ls32 077 * | "::" 5( h16 ":" ) ls32 078 * | [ h16 ] "::" 4( h16 ":" ) ls32 079 * | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 080 * | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 081 * | [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 082 * | [ *4( h16 ":" ) h16 ] "::" ls32 083 * | [ *5( h16 ":" ) h16 ] "::" h16 084 * | [ *6( h16 ":" ) h16 ] "::" 085 * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet 086 * dec-octet = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5] 087 * reg-name = *( unreserved / pct-encoded / sub-delims ) 088 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 089 * pct-encoded = "%" HEXDIG HEXDIG 090 * sub-delims = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" 091 * h16 = 1*4HEXDIG 092 * ls32 = ( h16 ":" h16 ) / IPv4address 093 * DIGIT = 0..9 094 * ALPHA = A-Z / a-z 095 * HEXDIG = DIGIT / A-F / a-f 096 * </pre> 097 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 098 */ 099public class LdapUrl 100{ 101 /** The constant for "ldaps://" scheme. */ 102 public static final String LDAPS_SCHEME = "ldaps://"; 103 104 /** The constant for "ldap://" scheme. */ 105 public static final String LDAP_SCHEME = "ldap://"; 106 107 /** A null LdapUrl */ 108 public static final LdapUrl EMPTY_URL = new LdapUrl(); 109 110 /** The scheme */ 111 private String scheme; 112 113 /** The host */ 114 private String host; 115 116 /** The port */ 117 private int port; 118 119 /** The Dn */ 120 private Dn dn; 121 122 /** The attributes */ 123 private List<String> attributes; 124 125 /** The scope */ 126 private SearchScope scope; 127 128 /** The filter as a string */ 129 private String filter; 130 131 /** The extensions. */ 132 private List<Extension> extensionList; 133 134 /** Stores the LdapUrl as a String */ 135 private String string; 136 137 /** Stores the LdapUrl as a byte array */ 138 private byte[] bytes; 139 140 /** modal parameter that forces explicit scope rendering in toString */ 141 private boolean forceScopeRendering; 142 143 /** The type of host we use */ 144 private HostTypeEnum hostType = HostTypeEnum.REGULAR_NAME; 145 146 /** A regexp for attributes */ 147 private static final Pattern ATTRIBUTE = Pattern 148 .compile( "(?:(?:\\d|[1-9]\\d*)(?:\\.(?:\\d|[1-9]\\d*))+)|(?:[a-zA-Z][a-zA-Z0-9-]*)" ); 149 150 151 /** 152 * Construct an empty LdapUrl 153 */ 154 public LdapUrl() 155 { 156 scheme = LDAP_SCHEME; 157 host = null; 158 port = -1; 159 dn = null; 160 attributes = new ArrayList<>(); 161 scope = SearchScope.OBJECT; 162 filter = null; 163 extensionList = new ArrayList<>( 2 ); 164 } 165 166 167 /** 168 * Create a new LdapUrl from a String after having parsed it. 169 * 170 * @param string TheString that contains the LdapUrl 171 * @throws LdapURLEncodingException If the String does not comply with RFC 2255 172 */ 173 public LdapUrl( String string ) throws LdapURLEncodingException 174 { 175 if ( string == null ) 176 { 177 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13041_INVALID_LDAP_URL_EMPTY_STRING ) ); 178 } 179 180 bytes = Strings.getBytesUtf8( string ); 181 this.string = string; 182 parse( string.toCharArray() ); 183 } 184 185 186 /** 187 * Parse a LdapUrl. 188 * 189 * @param chars The chars containing the URL 190 * @throws org.apache.directory.api.ldap.model.exception.LdapURLEncodingException If the URL is invalid 191 */ 192 private void parse( char[] chars ) throws LdapURLEncodingException 193 { 194 scheme = LDAP_SCHEME; 195 host = null; 196 port = -1; 197 dn = null; 198 attributes = new ArrayList<>(); 199 scope = SearchScope.OBJECT; 200 filter = null; 201 extensionList = new ArrayList<>( 2 ); 202 203 if ( ( chars == null ) || ( chars.length == 0 ) ) 204 { 205 host = ""; 206 return; 207 } 208 209 // ldapurl = scheme "://" [hostport] ["/" 210 // [dn ["?" [attributes] ["?" [scope] 211 // ["?" [filter] ["?" extensions]]]]]] 212 // scheme = "ldap" 213 // The scheme 214 int pos = Strings.areEquals( chars, 0, LDAP_SCHEME ); 215 216 if ( pos == StringConstants.NOT_EQUAL ) 217 { 218 pos = Strings.areEquals( chars, 0, LDAPS_SCHEME ); 219 if ( pos == StringConstants.NOT_EQUAL ) 220 { 221 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13030_LDAP_URL_MUST_START_WITH_LDAP ) ); 222 } 223 } 224 scheme = new String( chars, 0, pos ); 225 226 // The hostport 227 pos = parseHostPort( chars, pos ); 228 if ( pos == -1 ) 229 { 230 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13031_INVALID_HOST_PORT ) ); 231 } 232 233 if ( pos == chars.length ) 234 { 235 return; 236 } 237 238 // An optional '/' 239 if ( !Chars.isCharASCII( chars, pos, '/' ) ) 240 { 241 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13032_SLASH_EXPECTED, pos, chars[pos] ) ); 242 } 243 244 pos++; 245 246 if ( pos == chars.length ) 247 { 248 return; 249 } 250 251 // An optional Dn 252 pos = parseDN( chars, pos ); 253 if ( pos == -1 ) 254 { 255 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13033_INVALID_DN ) ); 256 } 257 258 if ( pos == chars.length ) 259 { 260 return; 261 } 262 263 // Optionals attributes 264 if ( !Chars.isCharASCII( chars, pos, '?' ) ) 265 { 266 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13034_QUESTION_MARK_EXPECTED, pos, chars[pos] ) ); 267 } 268 269 pos++; 270 271 pos = parseAttributes( chars, pos ); 272 if ( pos == -1 ) 273 { 274 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13035_INVALID_ATTRIBUTES ) ); 275 } 276 277 if ( pos == chars.length ) 278 { 279 return; 280 } 281 282 // Optional scope 283 if ( !Chars.isCharASCII( chars, pos, '?' ) ) 284 { 285 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13034_QUESTION_MARK_EXPECTED, pos, chars[pos] ) ); 286 } 287 288 pos++; 289 290 pos = parseScope( chars, pos ); 291 if ( pos == -1 ) 292 { 293 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13036_INVALID_SCOPE ) ); 294 } 295 296 if ( pos == chars.length ) 297 { 298 return; 299 } 300 301 // Optional filter 302 if ( !Chars.isCharASCII( chars, pos, '?' ) ) 303 { 304 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13034_QUESTION_MARK_EXPECTED, pos, chars[pos] ) ); 305 } 306 307 pos++; 308 309 if ( pos == chars.length ) 310 { 311 return; 312 } 313 314 pos = parseFilter( chars, pos ); 315 if ( pos == -1 ) 316 { 317 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13037_INVALID_FILTER ) ); 318 } 319 320 if ( pos == chars.length ) 321 { 322 return; 323 } 324 325 // Optional extensions 326 if ( !Chars.isCharASCII( chars, pos, '?' ) ) 327 { 328 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13034_QUESTION_MARK_EXPECTED, pos, chars[pos] ) ); 329 } 330 331 pos++; 332 333 pos = parseExtensions( chars, pos ); 334 if ( pos == -1 ) 335 { 336 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13038_INVALID_EXTENSIONS ) ); 337 } 338 339 if ( pos != chars.length ) 340 { 341 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13039_INVALID_CHAR_AT_LDAP_URL_END ) ); 342 } 343 } 344 345 346 /** 347 * Parse this rule : <br> 348 * <pre> 349 * host = IP-literal / IPv4address / reg-name 350 * port = *DIGIT 351 * <host> ::= <hostname> ':' <hostnumber> 352 * <hostname> ::= *[ <domainlabel> "." ] <toplabel> 353 * <domainlabel> ::= <alphadigit> | <alphadigit> *[<alphadigit> | "-" ] <alphadigit> 354 * <toplabel> ::= <alpha> | <alpha> *[ <alphadigit> | "-" ] <alphadigit> 355 * <hostnumber> ::= <digits> "." <digits> "." <digits> "." <digits> 356 * </pre> 357 * 358 * @param chars The buffer to parse 359 * @param pos The current position in the byte buffer 360 * @return The new position in the byte buffer, or -1 if the rule does not 361 * apply to the byte buffer TODO check that the topLabel is valid 362 * (it must start with an alpha) 363 */ 364 private int parseHost( char[] chars, int pos ) 365 { 366 int start = pos; 367 368 // The host will be followed by a '/' or a ':', or by nothing if it's 369 // the end. 370 // We will search the end of the host part, and we will check some 371 // elements. 372 switch ( chars[pos] ) 373 { 374 case '[': 375 // This is an IP Literal address 376 return parseIpLiteral( chars, pos + 1 ); 377 378 case '0': 379 case '1': 380 case '2': 381 case '3': 382 case '4': 383 case '5': 384 case '6': 385 case '7': 386 case '8': 387 case '9': 388 // Probably an IPV4 address, but may be a reg-name 389 // try to parse an IPV4 address first 390 int currentPos = parseIPV4( chars, pos ); 391 392 if ( currentPos != -1 ) 393 { 394 host = new String( chars, start, currentPos - start ); 395 396 return currentPos; 397 } 398 //fallback to reg-name 399 400 case 'a' : case 'b' : case 'c' : case 'd' : case 'e' : 401 case 'A' : case 'B' : case 'C' : case 'D' : case 'E' : 402 case 'f' : case 'g' : case 'h' : case 'i' : case 'j' : 403 case 'F' : case 'G' : case 'H' : case 'I' : case 'J' : 404 case 'k' : case 'l' : case 'm' : case 'n' : case 'o' : 405 case 'K' : case 'L' : case 'M' : case 'N' : case 'O' : 406 case 'p' : case 'q' : case 'r' : case 's' : case 't' : 407 case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' : 408 case 'u' : case 'v' : case 'w' : case 'x' : case 'y' : 409 case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' : 410 case 'z' : case 'Z' : case '-' : case '.' : case '_' : 411 case '~' : case '%' : case '!' : case '$' : case '&' : 412 case '\'' : case '(' : case ')' : case '*' : case '+' : 413 case ',' : case ';' : case '=' : 414 // A reg-name 415 return parseRegName( chars, pos ); 416 417 default: 418 break; 419 } 420 421 host = new String( chars, start, pos - start ); 422 423 return pos; 424 } 425 426 427 /** 428 * parse these rules : 429 * <pre> 430 * IP-literal = "[" ( IPv6address / IPvFuture ) "]" 431 * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) 432 * IPv6address = 6( h16 ":" ) ls32 433 * | "::" 5( h16 ":" ) ls32 434 * | [ h16 ] "::" 4( h16 ":" ) ls32 435 * | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 436 * | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 437 * | [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 438 * | [ *4( h16 ":" ) h16 ] "::" ls32 439 * | [ *5( h16 ":" ) h16 ] "::" h16 440 * | [ *6( h16 ":" ) h16 ] "::" 441 * h16 = 1*4HEXDIG 442 * ls32 = ( h16 ":" h16 ) / IPv4address 443 * </pre> 444 * 445 * @param chars The chars to parse 446 * @param pos The position in the chars 447 * @return The new position, or -1 if we had an error 448 */ 449 private int parseIpLiteral( char[] chars, int pos ) 450 { 451 int start = pos; 452 453 if ( Chars.isCharASCII( chars, pos, 'v' ) ) 454 { 455 // This is an IPvFuture 456 pos++; 457 hostType = HostTypeEnum.IPV_FUTURE; 458 459 pos = parseIPvFuture( chars, pos ); 460 461 if ( pos != -1 ) 462 { 463 // We don't keep the last char, which is a ']' 464 host = new String( chars, start, pos - start - 1 ); 465 } 466 467 return pos; 468 } 469 else 470 { 471 // An IPV6 host 472 hostType = HostTypeEnum.IPV6; 473 474 return parseIPV6( chars, pos ); 475 } 476 } 477 478 479 /** 480 * Validates an IPv4 address. Returns true if valid. 481 * 482 * @param inet4Address the IPv4 address to validate 483 * @return true if the argument contains a valid IPv4 address 484 */ 485 public boolean isValidInet4Address( String inet4Address ) 486 { 487 return parseIPV4( inet4Address.toCharArray(), 0 ) != -1; 488 } 489 490 491 /** 492 * This code source was taken from commons.validator 1.5.0 493 * 494 * Validates an IPv6 address. Returns true if valid. 495 * @param inet6Address the IPv6 address to validate 496 * @return true if the argument contains a valid IPv6 address 497 * 498 * @since 1.4.1 499 */ 500 public boolean isValidInet6Address( String inet6Address ) 501 { 502 boolean containsCompressedZeroes = inet6Address.contains( "::" ); 503 504 if ( containsCompressedZeroes && ( inet6Address.indexOf( "::" ) != inet6Address.lastIndexOf( "::" ) ) ) 505 { 506 return false; 507 } 508 509 if ( ( inet6Address.startsWith( ":" ) && !inet6Address.startsWith( "::" ) ) 510 || ( inet6Address.endsWith( ":" ) && !inet6Address.endsWith( "::" ) ) ) 511 { 512 return false; 513 } 514 515 String[] octets = inet6Address.split( ":" ); 516 517 if ( containsCompressedZeroes ) 518 { 519 List<String> octetList = new ArrayList<>( Arrays.asList( octets ) ); 520 521 if ( inet6Address.endsWith( "::" ) ) 522 { 523 // String.split() drops ending empty segments 524 octetList.add( "" ); 525 } 526 else if ( inet6Address.startsWith( "::" ) && !octetList.isEmpty() ) 527 { 528 octetList.remove( 0 ); 529 } 530 531 octets = octetList.toArray( new String[octetList.size()] ); 532 } 533 534 if ( octets.length > 8 ) 535 { 536 return false; 537 } 538 539 int validOctets = 0; 540 int emptyOctets = 0; 541 542 for ( int index = 0; index < octets.length; index++ ) 543 { 544 String octet = octets[index]; 545 546 if ( octet.length() == 0 ) 547 { 548 emptyOctets++; 549 550 if ( emptyOctets > 1 ) 551 { 552 return false; 553 } 554 } 555 else 556 { 557 emptyOctets = 0; 558 559 if ( octet.contains( "." ) ) 560 { // contains is Java 1.5+ 561 if ( !inet6Address.endsWith( octet ) ) 562 { 563 return false; 564 } 565 566 if ( index > octets.length - 1 || index > 6 ) 567 { 568 // IPV4 occupies last two octets 569 return false; 570 } 571 572 if ( !isValidInet4Address( octet ) ) 573 { 574 return false; 575 } 576 577 validOctets += 2; 578 579 continue; 580 } 581 582 if ( octet.length() > 4 ) 583 { 584 return false; 585 } 586 587 int octetInt = 0; 588 589 try 590 { 591 octetInt = Integer.valueOf( octet, 16 ).intValue(); 592 } 593 catch ( NumberFormatException e ) 594 { 595 return false; 596 } 597 598 if ( octetInt < 0 || octetInt > 0xffff ) 599 { 600 return false; 601 } 602 } 603 604 validOctets++; 605 } 606 607 if ( validOctets < 8 && !containsCompressedZeroes ) 608 { 609 return false; 610 } 611 612 return true; 613 } 614 615 616 /** 617 * Parse the following rules : 618 * <pre> 619 * IPv6address = 6( h16 ":" ) ls32 620 * | "::" 5( h16 ":" ) ls32 621 * | [ h16 ] "::" 4( h16 ":" ) ls32 622 * | [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 623 * | [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 624 * | [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 625 * | [ *4( h16 ":" ) h16 ] "::" ls32 626 * | [ *5( h16 ":" ) h16 ] "::" h16 627 * | [ *6( h16 ":" ) h16 ] "::" 628 * h16 = 1*4HEXDIG 629 * ls32 = ( h16 ":" h16 ) / IPv4address 630 * </pre> 631 * 632 * @param chars The chars to parse 633 * @param pos The position in the chars 634 * @return The new position, or -1 if we had an error 635 */ 636 private int parseIPV6( char[] chars, int pos ) 637 { 638 // Search for the closing ']' 639 int start = pos; 640 641 while ( !Chars.isCharASCII( chars, pos, ']' ) ) 642 { 643 pos++; 644 } 645 646 if ( Chars.isCharASCII( chars, pos, ']' ) ) 647 { 648 String hostString = new String( chars, start, pos - start ); 649 650 if ( isValidInet6Address( hostString ) ) 651 { 652 host = hostString; 653 654 return pos + 1; 655 } 656 else 657 { 658 return -1; 659 } 660 } 661 662 return -1; 663 } 664 665 666 /** 667 * Parse these rules : 668 * <pre> 669 * IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) 670 * </pre> 671 * (the "v" has already been parsed) 672 * 673 * @param chars The chars to parse 674 * @param pos The position in the chars 675 * @return The new position, or -1 if we had an error 676 */ 677 private int parseIPvFuture( char[] chars, int pos ) 678 { 679 // We should have at least one hex digit 680 boolean hexFound = false; 681 682 while ( Chars.isHex( chars, pos ) ) 683 { 684 hexFound = true; 685 pos++; 686 } 687 688 if ( !hexFound ) 689 { 690 return -1; 691 } 692 693 // a dot is expected 694 if ( !Chars.isCharASCII( chars, pos, '.' ) ) 695 { 696 return -1; 697 } 698 699 // Now, we should have at least one char in unreserved / sub-delims / ":" 700 boolean valueFound = false; 701 702 while ( !Chars.isCharASCII( chars, pos, ']' ) ) 703 { 704 switch ( chars[pos] ) 705 { 706 // Unserserved 707 // ALPHA 708 case 'a' : case 'b' : case 'c' : case 'd' : case 'e' : 709 case 'A' : case 'B' : case 'C' : case 'D' : case 'E' : 710 case 'f' : case 'g' : case 'h' : case 'i' : case 'j' : 711 case 'F' : case 'G' : case 'H' : case 'I' : case 'J' : 712 case 'k' : case 'l' : case 'm' : case 'n' : case 'o' : 713 case 'K' : case 'L' : case 'M' : case 'N' : case 'O' : 714 case 'p' : case 'q' : case 'r' : case 's' : case 't' : 715 case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' : 716 case 'u' : case 'v' : case 'w' : case 'x' : case 'y' : 717 case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' : 718 case 'z' : case 'Z' : 719 720 // DIGITs 721 case '0' : case '1' : case '2' : case '3' : case '4' : 722 case '5' : case '6' : case '7' : case '8' : case '9' : 723 724 // others 725 case '-' : case '.' : case '_' : case '~' : 726 727 // sub-delims 728 case '!' : case '$' : case '&' : case '\'' : 729 case '(' : case ')' : case '*' : case '+' : case ',' : 730 case ';' : case '=' : 731 732 // Special case for ':' 733 case ':': 734 pos++; 735 valueFound = true; 736 break; 737 738 default: 739 // Wrong char 740 return -1; 741 } 742 } 743 744 if ( !valueFound ) 745 { 746 return -1; 747 } 748 749 return pos; 750 } 751 752 753 /** 754 * parse these rules : 755 * <pre> 756 * reg-name = *( unreserved / pct-encoded / sub-delims ) 757 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 758 * pct-encoded = "%" HEXDIG HEXDIG 759 * sub-delims = "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" 760 * HEXDIG = DIGIT / A-F / a-f 761 * </pre> 762 * 763 * @param chars The chars to parse 764 * @param pos The position in the chars 765 * @return The new position, or -1 if we had an error 766 */ 767 private int parseRegName( char[] chars, int pos ) 768 { 769 int start = pos; 770 771 while ( !Chars.isCharASCII( chars, pos, ':' ) && !Chars.isCharASCII( chars, pos, '/' ) && ( pos < chars.length ) ) 772 { 773 switch ( chars[pos] ) 774 { 775 // Unserserved 776 // ALPHA 777 case 'a' : case 'b' : case 'c' : case 'd' : case 'e' : 778 case 'A' : case 'B' : case 'C' : case 'D' : case 'E' : 779 case 'f' : case 'g' : case 'h' : case 'i' : case 'j' : 780 case 'F' : case 'G' : case 'H' : case 'I' : case 'J' : 781 case 'k' : case 'l' : case 'm' : case 'n' : case 'o' : 782 case 'K' : case 'L' : case 'M' : case 'N' : case 'O' : 783 case 'p' : case 'q' : case 'r' : case 's' : case 't' : 784 case 'P' : case 'Q' : case 'R' : case 'S' : case 'T' : 785 case 'u' : case 'v' : case 'w' : case 'x' : case 'y' : 786 case 'U' : case 'V' : case 'W' : case 'X' : case 'Y' : 787 case 'z' : case 'Z' : 788 789 // DIGITs 790 case '0' : case '1' : case '2' : case '3' : case '4' : 791 case '5' : case '6' : case '7' : case '8' : case '9' : 792 793 // others 794 case '-' : case '.' : case '_' : case '~' : 795 796 // sub-delims 797 case '!' : case '$' : case '&' : case '\'' : 798 case '(' : case ')' : case '*' : case '+' : case ',' : 799 case ';' : case '=' : 800 pos++; 801 break; 802 803 // pct-encoded 804 case '%': 805 if ( Chars.isHex( chars, pos + 1 ) && Chars.isHex( chars, pos + 2 ) ) 806 { 807 pos += 3; 808 } 809 else 810 { 811 return -1; 812 } 813 814 break; 815 816 default: 817 // Wrong char 818 return -1; 819 } 820 } 821 822 host = new String( chars, start, pos - start ); 823 hostType = HostTypeEnum.REGULAR_NAME; 824 825 return pos; 826 } 827 828 829 /** 830 * Parse these rules : 831 * <pre> 832 * IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet 833 * dec-octet = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5] 834 * </pre> 835 * 836 * @param chars The buffer to parse 837 * @param pos The current position in the byte buffer 838 * 839 * @return The new position or -1 if this is not an IPV4 address 840 */ 841 private int parseIPV4( char[] chars, int pos ) 842 { 843 int[] ipElem = new int[4]; 844 int ipPos = pos; 845 int start = pos; 846 847 for ( int i = 0; i < 3; i++ ) 848 { 849 ipPos = parseDecOctet( chars, ipPos, ipElem, i ); 850 851 if ( ipPos == -1 ) 852 { 853 // Not an IPV4 address 854 return -1; 855 } 856 857 if ( chars[ipPos] != '.' ) 858 { 859 // Not an IPV4 address 860 return -1; 861 } 862 else 863 { 864 ipPos++; 865 } 866 } 867 868 ipPos = parseDecOctet( chars, ipPos, ipElem, 3 ); 869 870 if ( ipPos == -1 ) 871 { 872 // Not an IPV4 address 873 return -1; 874 } 875 else 876 { 877 pos = ipPos; 878 host = new String( chars, start, pos - start ); 879 hostType = HostTypeEnum.IPV4; 880 881 return pos; 882 } 883 } 884 885 886 /** 887 * Parse this rule : 888 * <pre> 889 * dec-octet = DIGIT | [1-9] DIGIT | "1" 2DIGIT | "2" [0-4] DIGIT | "25" [0-5] 890 * </pre> 891 * 892 * @param chars The chars to parse 893 * @param pos The position in the chars 894 * @param ipElem The IP elements to update 895 * @param octetNb The IP octet being processed 896 * @return The new position, or -1 if the IP octet is invalid 897 */ 898 private int parseDecOctet( char[] chars, int pos, int[] ipElem, int octetNb ) 899 { 900 int ipElemValue = 0; 901 boolean ipElemSeen = false; 902 boolean hasHeadingZeroes = false; 903 904 while ( Chars.isDigit( chars, pos ) ) 905 { 906 ipElemSeen = true; 907 908 if ( chars[pos] == '0' ) 909 { 910 if ( hasHeadingZeroes ) 911 { 912 // Two 0 at the beginning : not allowed 913 return -1; 914 } 915 916 if ( ipElemValue > 0 ) 917 { 918 ipElemValue = ipElemValue * 10; 919 } 920 else 921 { 922 hasHeadingZeroes = true; 923 } 924 } 925 else 926 { 927 hasHeadingZeroes = false; 928 ipElemValue = ( ipElemValue * 10 ) + ( chars[pos] - '0' ); 929 } 930 931 if ( ipElemValue > 255 ) 932 { 933 return -1; 934 } 935 936 pos++; 937 } 938 939 if ( ipElemSeen ) 940 { 941 ipElem[octetNb] = ipElemValue; 942 943 return pos; 944 } 945 else 946 { 947 return -1; 948 } 949 } 950 951 952 /** 953 * Parse this rule : <br> 954 * <pre> 955 * <port> ::= <digit>+ 956 * <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 957 * </pre> 958 * The port must be between 0 and 65535. 959 * 960 * @param chars The buffer to parse 961 * @param pos The current position in the byte buffer 962 * @return The new position in the byte buffer, or -1 if the rule does not 963 * apply to the byte buffer 964 */ 965 private int parsePort( char[] chars, int pos ) 966 { 967 968 if ( !Chars.isDigit( chars, pos ) ) 969 { 970 return -1; 971 } 972 973 port = chars[pos] - '0'; 974 975 pos++; 976 977 while ( Chars.isDigit( chars, pos ) ) 978 { 979 port = ( port * 10 ) + ( chars[pos] - '0' ); 980 981 if ( port > 65535 ) 982 { 983 return -1; 984 } 985 986 pos++; 987 } 988 989 return pos; 990 } 991 992 993 /** 994 * Parse this rule : <br> 995 * <pre> 996 * <hostport> ::= <host> [':' <port>] 997 * </pre> 998 * 999 * @param chars The char array to parse 1000 * @param pos The current position in the byte buffer 1001 * @return The new position in the byte buffer, or -1 if the rule does not 1002 * apply to the byte buffer 1003 */ 1004 private int parseHostPort( char[] chars, int pos ) 1005 { 1006 int hostPos = pos; 1007 1008 pos = parseHost( chars, pos ); 1009 if ( pos == -1 ) 1010 { 1011 return -1; 1012 } 1013 1014 // We may have a port. 1015 if ( Chars.isCharASCII( chars, pos, ':' ) ) 1016 { 1017 if ( pos == hostPos ) 1018 { 1019 // We should not have a port if we have no host 1020 return -1; 1021 } 1022 1023 pos++; 1024 } 1025 else 1026 { 1027 return pos; 1028 } 1029 1030 // As we have a ':', we must have a valid port (between 0 and 65535). 1031 pos = parsePort( chars, pos ); 1032 if ( pos == -1 ) 1033 { 1034 return -1; 1035 } 1036 1037 return pos; 1038 } 1039 1040 1041 /** 1042 * Converts the specified string to byte array of ASCII characters. 1043 * 1044 * @param data the string to be encoded 1045 * @return The string as a byte array. 1046 */ 1047 private static byte[] getAsciiBytes( final String data ) 1048 { 1049 if ( data == null ) 1050 { 1051 throw new IllegalArgumentException( I18n.err( I18n.ERR_17028_PARAMETER_CANT_BE_NULL ) ); 1052 } 1053 1054 return Strings.getBytesUtf8( data ); 1055 } 1056 1057 1058 /** 1059 * From commons-codec. Decodes an array of URL safe 7-bit characters into an 1060 * array of original bytes. Escaped characters are converted back to their 1061 * original representation. 1062 * 1063 * @param bytes array of URL safe characters 1064 * @return array of original bytes 1065 * @throws UrlDecoderException Thrown if URL decoding is unsuccessful 1066 */ 1067 private static byte[] decodeUrl( byte[] bytes ) throws UrlDecoderException 1068 { 1069 if ( bytes == null ) 1070 { 1071 return Strings.EMPTY_BYTES; 1072 } 1073 1074 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 1075 1076 for ( int i = 0; i < bytes.length; i++ ) 1077 { 1078 int b = bytes[i]; 1079 1080 if ( b == '%' ) 1081 { 1082 try 1083 { 1084 int u = Character.digit( ( char ) bytes[++i], 16 ); 1085 int l = Character.digit( ( char ) bytes[++i], 16 ); 1086 1087 if ( ( u == -1 ) || ( l == -1 ) ) 1088 { 1089 throw new UrlDecoderException( I18n.err( I18n.ERR_13040_INVALID_URL_ENCODING ) ); 1090 } 1091 1092 buffer.write( ( char ) ( ( u << 4 ) + l ) ); 1093 } 1094 catch ( ArrayIndexOutOfBoundsException aioobe ) 1095 { 1096 throw new UrlDecoderException( I18n.err( I18n.ERR_13040_INVALID_URL_ENCODING ), aioobe ); 1097 } 1098 } 1099 else 1100 { 1101 buffer.write( b ); 1102 } 1103 } 1104 1105 return buffer.toByteArray(); 1106 } 1107 1108 1109 /** 1110 * From commons-httpclients. Unescape and decode a given string regarded as 1111 * an escaped string with the default protocol charset. 1112 * 1113 * @param escaped a string 1114 * @return the unescaped string 1115 * @throws LdapUriException if the string cannot be decoded (invalid) 1116 */ 1117 private static String decode( String escaped ) throws LdapUriException 1118 { 1119 try 1120 { 1121 byte[] rawdata = decodeUrl( getAsciiBytes( escaped ) ); 1122 return Strings.getString( rawdata, StandardCharsets.UTF_8 ); 1123 } 1124 catch ( UrlDecoderException e ) 1125 { 1126 throw new LdapUriException( e.getMessage(), e ); 1127 } 1128 } 1129 1130 1131 /** 1132 * Parse a string and check that it complies with RFC 2253. Here, we will 1133 * just call the Dn parser to do the job. 1134 * 1135 * @param chars The char array to be checked 1136 * @param pos the starting position 1137 * @return -1 if the char array does not contains a Dn 1138 */ 1139 private int parseDN( char[] chars, int pos ) 1140 { 1141 1142 int end = pos; 1143 1144 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 1145 { 1146 end++; 1147 } 1148 1149 try 1150 { 1151 String dnStr = new String( chars, pos, end - pos ); 1152 dn = new Dn( decode( dnStr ) ); 1153 } 1154 catch ( LdapUriException | LdapInvalidDnException e ) 1155 { 1156 return -1; 1157 } 1158 1159 return end; 1160 } 1161 1162 1163 /** 1164 * Parse the following rule : 1165 * <pre> 1166 * oid ::= numericOid | descr 1167 * descr ::= keystring 1168 * keystring ::= leadkeychar *keychar 1169 * leadkeychar ::= [a-zA-Z] 1170 * keychar ::= [a-zA-Z0-0-] 1171 * numericOid ::= number 1*( DOT number ) 1172 * number ::= 0 | [1-9][0-9]* 1173 * </pre> 1174 * 1175 * @param attribute The attribute to validate 1176 * @throws LdapURLEncodingException If teh attribute is invalid 1177 */ 1178 private void validateAttribute( String attribute ) throws LdapURLEncodingException 1179 { 1180 Matcher matcher = ATTRIBUTE.matcher( attribute ); 1181 1182 if ( !matcher.matches() ) 1183 { 1184 throw new LdapURLEncodingException( I18n.err( I18n.ERR_13011_ATTRIBUTE_INVALID, attribute ) ); 1185 } 1186 } 1187 1188 1189 /** 1190 * Parse the attributes part 1191 * 1192 * @param chars The char array to be checked 1193 * @param pos the starting position 1194 * @return -1 if the char array does not contains attributes 1195 */ 1196 private int parseAttributes( char[] chars, int pos ) 1197 { 1198 int start = pos; 1199 int end = pos; 1200 Set<String> hAttributes = new HashSet<>(); 1201 boolean hadComma = false; 1202 1203 try 1204 { 1205 1206 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 1207 { 1208 1209 if ( Chars.isCharASCII( chars, i, ',' ) ) 1210 { 1211 hadComma = true; 1212 1213 if ( ( end - start ) == 0 ) 1214 { 1215 1216 // An attributes must not be null 1217 return -1; 1218 } 1219 else 1220 { 1221 // get the attribute. It must not be blank 1222 String attribute = new String( chars, start, end - start ).trim(); 1223 1224 if ( attribute.length() == 0 ) 1225 { 1226 return -1; 1227 } 1228 1229 // Check that the attribute is valid 1230 try 1231 { 1232 validateAttribute( attribute ); 1233 } 1234 catch ( LdapURLEncodingException luee ) 1235 { 1236 return -1; 1237 } 1238 1239 String decodedAttr = decode( attribute ); 1240 1241 if ( !hAttributes.contains( decodedAttr ) ) 1242 { 1243 attributes.add( decodedAttr ); 1244 hAttributes.add( decodedAttr ); 1245 } 1246 } 1247 1248 start = i + 1; 1249 } 1250 else 1251 { 1252 hadComma = false; 1253 } 1254 1255 end++; 1256 } 1257 1258 if ( hadComma ) 1259 { 1260 1261 // We are not allowed to have a comma at the end of the 1262 // attributes 1263 return -1; 1264 } 1265 else 1266 { 1267 1268 if ( end == start ) 1269 { 1270 1271 // We don't have any attributes. This is valid. 1272 return end; 1273 } 1274 1275 // Store the last attribute 1276 // get the attribute. It must not be blank 1277 String attribute = new String( chars, start, end - start ).trim(); 1278 1279 if ( attribute.length() == 0 ) 1280 { 1281 return -1; 1282 } 1283 1284 String decodedAttr = decode( attribute ); 1285 1286 if ( !hAttributes.contains( decodedAttr ) ) 1287 { 1288 attributes.add( decodedAttr ); 1289 hAttributes.add( decodedAttr ); 1290 } 1291 } 1292 1293 return end; 1294 } 1295 catch ( LdapUriException ue ) 1296 { 1297 return -1; 1298 } 1299 } 1300 1301 1302 /** 1303 * Parse the filter part. We will use the FilterParserImpl class 1304 * 1305 * @param chars The char array to be checked 1306 * @param pos the starting position 1307 * @return -1 if the char array does not contains a filter 1308 */ 1309 private int parseFilter( char[] chars, int pos ) 1310 { 1311 1312 int end = pos; 1313 1314 for ( int i = pos; ( i < chars.length ) && ( chars[i] != '?' ); i++ ) 1315 { 1316 end++; 1317 } 1318 1319 if ( end == pos ) 1320 { 1321 // We have no filter 1322 return end; 1323 } 1324 1325 try 1326 { 1327 filter = decode( new String( chars, pos, end - pos ) ); 1328 FilterParser.parse( filter ); 1329 } 1330 catch ( LdapUriException | ParseException e ) 1331 { 1332 return -1; 1333 } 1334 1335 return end; 1336 } 1337 1338 1339 /** 1340 * Parse the scope part. 1341 * 1342 * @param chars The char array to be checked 1343 * @param pos the starting position 1344 * @return -1 if the char array does not contains a scope 1345 */ 1346 private int parseScope( char[] chars, int pos ) 1347 { 1348 1349 if ( Chars.isCharASCII( chars, pos, 'b' ) || Chars.isCharASCII( chars, pos, 'B' ) ) 1350 { 1351 pos++; 1352 1353 if ( Chars.isCharASCII( chars, pos, 'a' ) || Chars.isCharASCII( chars, pos, 'A' ) ) 1354 { 1355 pos++; 1356 1357 if ( Chars.isCharASCII( chars, pos, 's' ) || Chars.isCharASCII( chars, pos, 'S' ) ) 1358 { 1359 pos++; 1360 1361 if ( Chars.isCharASCII( chars, pos, 'e' ) || Chars.isCharASCII( chars, pos, 'E' ) ) 1362 { 1363 pos++; 1364 scope = SearchScope.OBJECT; 1365 return pos; 1366 } 1367 } 1368 } 1369 } 1370 else if ( Chars.isCharASCII( chars, pos, 'o' ) || Chars.isCharASCII( chars, pos, 'O' ) ) 1371 { 1372 pos++; 1373 1374 if ( Chars.isCharASCII( chars, pos, 'n' ) || Chars.isCharASCII( chars, pos, 'N' ) ) 1375 { 1376 pos++; 1377 1378 if ( Chars.isCharASCII( chars, pos, 'e' ) || Chars.isCharASCII( chars, pos, 'E' ) ) 1379 { 1380 pos++; 1381 1382 scope = SearchScope.ONELEVEL; 1383 return pos; 1384 } 1385 } 1386 } 1387 else if ( Chars.isCharASCII( chars, pos, 's' ) || Chars.isCharASCII( chars, pos, 'S' ) ) 1388 { 1389 pos++; 1390 1391 if ( Chars.isCharASCII( chars, pos, 'u' ) || Chars.isCharASCII( chars, pos, 'U' ) ) 1392 { 1393 pos++; 1394 1395 if ( Chars.isCharASCII( chars, pos, 'b' ) || Chars.isCharASCII( chars, pos, 'B' ) ) 1396 { 1397 pos++; 1398 1399 scope = SearchScope.SUBTREE; 1400 return pos; 1401 } 1402 } 1403 } 1404 else if ( Chars.isCharASCII( chars, pos, '?' ) ) 1405 { 1406 // An empty scope. This is valid 1407 return pos; 1408 } 1409 else if ( pos == chars.length ) 1410 { 1411 // An empty scope at the end of the URL. This is valid 1412 return pos; 1413 } 1414 1415 // The scope is not one of "one", "sub" or "base". It's an error 1416 return -1; 1417 } 1418 1419 1420 /** 1421 * Parse extensions and critical extensions. 1422 * 1423 * The grammar is : 1424 * extensions ::= extension [ ',' extension ]* 1425 * extension ::= [ '!' ] ( token | ( 'x-' | 'X-' ) token ) ) [ '=' exvalue ] 1426 * 1427 * @param chars The char array to be checked 1428 * @param pos the starting position 1429 * @return -1 if the char array does not contains valid extensions or 1430 * critical extensions 1431 */ 1432 private int parseExtensions( char[] chars, int pos ) 1433 { 1434 int start = pos; 1435 boolean isCritical = false; 1436 boolean isNewExtension = true; 1437 boolean hasValue = false; 1438 String extension = null; 1439 String value = null; 1440 1441 if ( pos == chars.length ) 1442 { 1443 return pos; 1444 } 1445 1446 try 1447 { 1448 for ( int i = pos; i < chars.length; i++ ) 1449 { 1450 if ( Chars.isCharASCII( chars, i, ',' ) ) 1451 { 1452 if ( isNewExtension ) 1453 { 1454 // a ',' is not allowed when we have already had one 1455 // or if we just started to parse the extensions. 1456 return -1; 1457 } 1458 else 1459 { 1460 if ( extension == null ) 1461 { 1462 extension = decode( new String( chars, start, i - start ) ).trim(); 1463 } 1464 else 1465 { 1466 value = decode( new String( chars, start, i - start ) ).trim(); 1467 } 1468 1469 Extension ext = new Extension( isCritical, extension, value ); 1470 extensionList.add( ext ); 1471 1472 isNewExtension = true; 1473 hasValue = false; 1474 isCritical = false; 1475 start = i + 1; 1476 extension = null; 1477 value = null; 1478 } 1479 } 1480 else if ( Chars.isCharASCII( chars, i, '=' ) ) 1481 { 1482 if ( hasValue ) 1483 { 1484 // We may have two '=' for the same extension 1485 continue; 1486 } 1487 1488 // An optionnal value 1489 extension = decode( new String( chars, start, i - start ) ).trim(); 1490 1491 if ( extension.length() == 0 ) 1492 { 1493 // We must have an extension 1494 return -1; 1495 } 1496 1497 hasValue = true; 1498 start = i + 1; 1499 } 1500 else if ( Chars.isCharASCII( chars, i, '!' ) ) 1501 { 1502 if ( hasValue ) 1503 { 1504 // We may have two '!' in the value 1505 continue; 1506 } 1507 1508 if ( !isNewExtension ) 1509 { 1510 // '!' must appears first 1511 return -1; 1512 } 1513 1514 isCritical = true; 1515 start++; 1516 } 1517 else 1518 { 1519 isNewExtension = false; 1520 } 1521 } 1522 1523 if ( extension == null ) 1524 { 1525 extension = decode( new String( chars, start, chars.length - start ) ).trim(); 1526 } 1527 else 1528 { 1529 value = decode( new String( chars, start, chars.length - start ) ).trim(); 1530 } 1531 1532 Extension ext = new Extension( isCritical, extension, value ); 1533 extensionList.add( ext ); 1534 1535 return chars.length; 1536 } 1537 catch ( LdapUriException ue ) 1538 { 1539 return -1; 1540 } 1541 } 1542 1543 1544 /** 1545 * Encode a String to avoid special characters. 1546 * 1547 * <pre> 1548 * RFC 4516, section 2.1. (Percent-Encoding) 1549 * 1550 * A generated LDAP URL MUST consist only of the restricted set of 1551 * characters included in one of the following three productions defined 1552 * in [RFC3986]: 1553 * 1554 * <reserved> 1555 * <unreserved> 1556 * <pct-encoded> 1557 * 1558 * Implementations SHOULD accept other valid UTF-8 strings [RFC3629] as 1559 * input. An octet MUST be encoded using the percent-encoding mechanism 1560 * described in section 2.1 of [RFC3986] in any of these situations: 1561 * 1562 * The octet is not in the reserved set defined in section 2.2 of 1563 * [RFC3986] or in the unreserved set defined in section 2.3 of 1564 * [RFC3986]. 1565 * 1566 * It is the single Reserved character '?' and occurs inside a <dn>, 1567 * <filter>, or other element of an LDAP URL. 1568 * 1569 * It is a comma character ',' that occurs inside an <exvalue>. 1570 * 1571 * RFC 3986, section 2.2 (Reserved Characters) 1572 * 1573 * reserved = gen-delims / sub-delims 1574 * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 1575 * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 1576 * / "*" / "+" / "," / ";" / "=" 1577 * 1578 * RFC 3986, section 2.3 (Unreserved Characters) 1579 * 1580 * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 1581 * </pre> 1582 * 1583 * @param url The String to encode 1584 * @param doubleEncode Set if we need to encode the comma 1585 * @return An encoded string 1586 */ 1587 public static String urlEncode( String url, boolean doubleEncode ) 1588 { 1589 StringBuilder sb = new StringBuilder(); 1590 1591 for ( int i = 0; i < url.length(); i++ ) 1592 { 1593 char c = url.charAt( i ); 1594 1595 switch ( c ) 1596 1597 { 1598 // reserved and unreserved characters: 1599 // just append to the buffer 1600 1601 // reserved gen-delims, excluding '?' 1602 // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 1603 case ':': 1604 case '/': 1605 case '#': 1606 case '[': 1607 case ']': 1608 case '@': 1609 1610 // reserved sub-delims, excluding ',' 1611 // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 1612 // / "*" / "+" / "," / ";" / "=" 1613 case '!': 1614 case '$': 1615 case '&': 1616 case '\'': 1617 case '(': 1618 case ')': 1619 case '*': 1620 case '+': 1621 case ';': 1622 case '=': 1623 1624 // unreserved 1625 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 1626 case 'a': 1627 case 'b': 1628 case 'c': 1629 case 'd': 1630 case 'e': 1631 case 'f': 1632 case 'g': 1633 case 'h': 1634 case 'i': 1635 case 'j': 1636 case 'k': 1637 case 'l': 1638 case 'm': 1639 case 'n': 1640 case 'o': 1641 case 'p': 1642 case 'q': 1643 case 'r': 1644 case 's': 1645 case 't': 1646 case 'u': 1647 case 'v': 1648 case 'w': 1649 case 'x': 1650 case 'y': 1651 case 'z': 1652 1653 case 'A': 1654 case 'B': 1655 case 'C': 1656 case 'D': 1657 case 'E': 1658 case 'F': 1659 case 'G': 1660 case 'H': 1661 case 'I': 1662 case 'J': 1663 case 'K': 1664 case 'L': 1665 case 'M': 1666 case 'N': 1667 case 'O': 1668 case 'P': 1669 case 'Q': 1670 case 'R': 1671 case 'S': 1672 case 'T': 1673 case 'U': 1674 case 'V': 1675 case 'W': 1676 case 'X': 1677 case 'Y': 1678 case 'Z': 1679 1680 case '0': 1681 case '1': 1682 case '2': 1683 case '3': 1684 case '4': 1685 case '5': 1686 case '6': 1687 case '7': 1688 case '8': 1689 case '9': 1690 1691 case '-': 1692 case '.': 1693 case '_': 1694 case '~': 1695 1696 sb.append( c ); 1697 break; 1698 1699 case ',': 1700 1701 // special case for comma 1702 if ( doubleEncode ) 1703 { 1704 sb.append( "%2c" ); 1705 } 1706 else 1707 { 1708 sb.append( c ); 1709 } 1710 break; 1711 1712 default: 1713 1714 // percent encoding 1715 byte[] bytes = Unicode.charToBytes( c ); 1716 char[] hex = Strings.toHexString( bytes ).toCharArray(); 1717 for ( int j = 0; j < hex.length; j++ ) 1718 { 1719 if ( j % 2 == 0 ) 1720 { 1721 sb.append( '%' ); 1722 } 1723 sb.append( hex[j] ); 1724 1725 } 1726 1727 break; 1728 } 1729 } 1730 1731 return sb.toString(); 1732 } 1733 1734 1735 /** 1736 * Get a string representation of a LdapUrl. 1737 * 1738 * @return A LdapUrl string 1739 */ 1740 @Override 1741 public String toString() 1742 { 1743 StringBuilder sb = new StringBuilder(); 1744 1745 sb.append( scheme ); 1746 1747 if ( host != null ) 1748 { 1749 switch ( hostType ) 1750 { 1751 case IPV4: 1752 case REGULAR_NAME: 1753 sb.append( host ); 1754 break; 1755 1756 case IPV6: 1757 case IPV_FUTURE: 1758 sb.append( '[' ).append( host ).append( ']' ); 1759 break; 1760 1761 default: 1762 throw new IllegalArgumentException( I18n.err( I18n.ERR_13012_UNEXPECTED_HOST_TYPE_ENUM, hostType ) ); 1763 } 1764 } 1765 1766 if ( port != -1 ) 1767 { 1768 sb.append( ':' ).append( port ); 1769 } 1770 1771 if ( dn != null ) 1772 { 1773 sb.append( '/' ).append( urlEncode( dn.getName(), false ) ); 1774 1775 if ( !attributes.isEmpty() || forceScopeRendering 1776 || ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || !extensionList.isEmpty() ) ) 1777 { 1778 sb.append( '?' ); 1779 1780 boolean isFirst = true; 1781 1782 for ( String attribute : attributes ) 1783 { 1784 if ( isFirst ) 1785 { 1786 isFirst = false; 1787 } 1788 else 1789 { 1790 sb.append( ',' ); 1791 } 1792 1793 sb.append( urlEncode( attribute, false ) ); 1794 } 1795 } 1796 1797 if ( forceScopeRendering ) 1798 { 1799 sb.append( '?' ); 1800 1801 sb.append( scope.getLdapUrlValue() ); 1802 } 1803 else 1804 { 1805 if ( ( scope != SearchScope.OBJECT ) || ( filter != null ) || !extensionList.isEmpty() ) 1806 { 1807 sb.append( '?' ); 1808 1809 switch ( scope ) 1810 { 1811 case ONELEVEL: 1812 case SUBTREE: 1813 sb.append( scope.getLdapUrlValue() ); 1814 break; 1815 1816 default: 1817 break; 1818 } 1819 1820 if ( ( filter != null ) || ( !extensionList.isEmpty() ) ) 1821 { 1822 sb.append( "?" ); 1823 1824 if ( filter != null ) 1825 { 1826 sb.append( urlEncode( filter, false ) ); 1827 } 1828 1829 if ( !extensionList.isEmpty() ) 1830 { 1831 sb.append( '?' ); 1832 1833 boolean isFirst = true; 1834 1835 if ( !extensionList.isEmpty() ) 1836 { 1837 for ( Extension extension : extensionList ) 1838 { 1839 if ( !isFirst ) 1840 { 1841 sb.append( ',' ); 1842 } 1843 else 1844 { 1845 isFirst = false; 1846 } 1847 1848 if ( extension.isCritical ) 1849 { 1850 sb.append( '!' ); 1851 } 1852 sb.append( urlEncode( extension.type, false ) ); 1853 1854 if ( extension.value != null ) 1855 { 1856 sb.append( '=' ); 1857 sb.append( urlEncode( extension.value, true ) ); 1858 } 1859 } 1860 } 1861 } 1862 } 1863 } 1864 } 1865 } 1866 else 1867 { 1868 sb.append( '/' ); 1869 } 1870 1871 return sb.toString(); 1872 } 1873 1874 1875 /** 1876 * @return Returns the attributes. 1877 */ 1878 public List<String> getAttributes() 1879 { 1880 return attributes; 1881 } 1882 1883 1884 /** 1885 * @return Returns the dn. 1886 */ 1887 public Dn getDn() 1888 { 1889 return dn; 1890 } 1891 1892 1893 /** 1894 * @return Returns the extensions. 1895 */ 1896 public List<Extension> getExtensions() 1897 { 1898 return extensionList; 1899 } 1900 1901 1902 /** 1903 * Gets the extension. 1904 * 1905 * @param type the extension type, case-insensitive 1906 * 1907 * @return Returns the extension, null if this URL does not contain 1908 * such an extension. 1909 */ 1910 public Extension getExtension( String type ) 1911 { 1912 for ( Extension extension : getExtensions() ) 1913 { 1914 if ( extension.getType().equalsIgnoreCase( type ) ) 1915 { 1916 return extension; 1917 } 1918 } 1919 return null; 1920 } 1921 1922 1923 /** 1924 * Gets the extension value. 1925 * 1926 * @param type the extension type, case-insensitive 1927 * 1928 * @return Returns the extension value, null if this URL does not 1929 * contain such an extension or if the extension value is null. 1930 */ 1931 public String getExtensionValue( String type ) 1932 { 1933 for ( Extension extension : getExtensions() ) 1934 { 1935 if ( extension.getType().equalsIgnoreCase( type ) ) 1936 { 1937 return extension.getValue(); 1938 } 1939 } 1940 return null; 1941 } 1942 1943 1944 /** 1945 * @return Returns the filter. 1946 */ 1947 public String getFilter() 1948 { 1949 return filter; 1950 } 1951 1952 1953 /** 1954 * @return Returns the host. 1955 */ 1956 public String getHost() 1957 { 1958 return host; 1959 } 1960 1961 1962 /** 1963 * @return Returns the port. 1964 */ 1965 public int getPort() 1966 { 1967 return port; 1968 } 1969 1970 1971 /** 1972 * Returns the scope, one of {@link SearchScope#OBJECT}, 1973 * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}. 1974 * 1975 * @return Returns the scope. 1976 */ 1977 public SearchScope getScope() 1978 { 1979 return scope; 1980 } 1981 1982 1983 /** 1984 * @return Returns the scheme. 1985 */ 1986 public String getScheme() 1987 { 1988 return scheme; 1989 } 1990 1991 1992 /** 1993 * @return the number of bytes for this LdapUrl 1994 */ 1995 public int getNbBytes() 1996 { 1997 return bytes != null ? bytes.length : 0; 1998 } 1999 2000 2001 /** 2002 * @return a reference on the interned bytes representing this LdapUrl 2003 */ 2004 public byte[] getBytesReference() 2005 { 2006 return bytes; 2007 } 2008 2009 2010 /** 2011 * @return a copy of the bytes representing this LdapUrl 2012 */ 2013 public byte[] getBytesCopy() 2014 { 2015 if ( bytes != null ) 2016 { 2017 byte[] copy = new byte[bytes.length]; 2018 System.arraycopy( bytes, 0, copy, 0, bytes.length ); 2019 return copy; 2020 } 2021 else 2022 { 2023 return null; 2024 } 2025 } 2026 2027 2028 /** 2029 * @return the LdapUrl as a String 2030 */ 2031 public String getString() 2032 { 2033 return string; 2034 } 2035 2036 2037 /** 2038 * {@inheritDoc} 2039 */ 2040 @Override 2041 public int hashCode() 2042 { 2043 return this.toString().hashCode(); 2044 } 2045 2046 2047 /** 2048 * {@inheritDoc} 2049 */ 2050 @Override 2051 public boolean equals( Object obj ) 2052 { 2053 if ( this == obj ) 2054 { 2055 return true; 2056 } 2057 if ( obj == null ) 2058 { 2059 return false; 2060 } 2061 if ( getClass() != obj.getClass() ) 2062 { 2063 return false; 2064 } 2065 2066 final LdapUrl other = ( LdapUrl ) obj; 2067 return this.toString().equals( other.toString() ); 2068 } 2069 2070 2071 /** 2072 * Sets the scheme. Must be "ldap://" or "ldaps://", otherwise "ldap://" is assumed as default. 2073 * 2074 * @param scheme the new scheme 2075 */ 2076 public void setScheme( String scheme ) 2077 { 2078 if ( ( ( scheme != null ) && LDAP_SCHEME.equals( scheme ) ) || LDAPS_SCHEME.equals( scheme ) ) 2079 { 2080 this.scheme = scheme; 2081 } 2082 else 2083 { 2084 this.scheme = LDAP_SCHEME; 2085 } 2086 2087 } 2088 2089 2090 /** 2091 * Sets the host. 2092 * 2093 * @param host the new host 2094 */ 2095 public void setHost( String host ) 2096 { 2097 this.host = host; 2098 } 2099 2100 2101 /** 2102 * Sets the port. Must be between 1 and 65535, otherwise -1 is assumed as default. 2103 * 2104 * @param port the new port 2105 */ 2106 public void setPort( int port ) 2107 { 2108 if ( ( port < 1 ) || ( port > 65535 ) ) 2109 { 2110 this.port = -1; 2111 } 2112 else 2113 { 2114 this.port = port; 2115 } 2116 } 2117 2118 2119 /** 2120 * Sets the dn. 2121 * 2122 * @param dn the new dn 2123 */ 2124 public void setDn( Dn dn ) 2125 { 2126 this.dn = dn; 2127 } 2128 2129 2130 /** 2131 * Sets the attributes, null removes all existing attributes. 2132 * 2133 * @param attributes the new attributes 2134 */ 2135 public void setAttributes( List<String> attributes ) 2136 { 2137 if ( attributes == null ) 2138 { 2139 this.attributes.clear(); 2140 } 2141 else 2142 { 2143 this.attributes = attributes; 2144 } 2145 } 2146 2147 2148 /** 2149 * Sets the scope. Must be one of {@link SearchScope#OBJECT}, 2150 * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}, 2151 * otherwise {@link SearchScope#OBJECT} is assumed as default. 2152 * 2153 * @param scope the new scope 2154 */ 2155 public void setScope( int scope ) 2156 { 2157 try 2158 { 2159 this.scope = SearchScope.getSearchScope( scope ); 2160 } 2161 catch ( IllegalArgumentException iae ) 2162 { 2163 this.scope = SearchScope.OBJECT; 2164 } 2165 } 2166 2167 2168 /** 2169 * Sets the scope. Must be one of {@link SearchScope#OBJECT}, 2170 * {@link SearchScope#ONELEVEL} or {@link SearchScope#SUBTREE}, 2171 * otherwise {@link SearchScope#OBJECT} is assumed as default. 2172 * 2173 * @param scope the new scope 2174 */ 2175 public void setScope( SearchScope scope ) 2176 { 2177 if ( scope == null ) 2178 { 2179 this.scope = SearchScope.OBJECT; 2180 } 2181 else 2182 { 2183 this.scope = scope; 2184 } 2185 } 2186 2187 2188 /** 2189 * Sets the filter. 2190 * 2191 * @param filter the new filter 2192 */ 2193 public void setFilter( String filter ) 2194 { 2195 this.filter = filter; 2196 } 2197 2198 2199 /** 2200 * If set to true forces the toString method to render the scope 2201 * regardless of optional nature. Use this when you want explicit 2202 * search URL scope rendering. 2203 * 2204 * @param forceScopeRendering the forceScopeRendering to set 2205 */ 2206 public void setForceScopeRendering( boolean forceScopeRendering ) 2207 { 2208 this.forceScopeRendering = forceScopeRendering; 2209 } 2210 2211 /** 2212 * An inner bean to hold extension information. 2213 * 2214 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> 2215 */ 2216 public static class Extension 2217 { 2218 private boolean isCritical; 2219 private String type; 2220 private String value; 2221 2222 2223 /** 2224 * Creates a new instance of Extension. 2225 * 2226 * @param isCritical true for critical extension 2227 * @param type the extension type 2228 * @param value the extension value 2229 */ 2230 public Extension( boolean isCritical, String type, String value ) 2231 { 2232 super(); 2233 this.isCritical = isCritical; 2234 this.type = type; 2235 this.value = value; 2236 } 2237 2238 2239 /** 2240 * Checks if is critical. 2241 * 2242 * @return true, if is critical 2243 */ 2244 public boolean isCritical() 2245 { 2246 return isCritical; 2247 } 2248 2249 2250 /** 2251 * Sets the critical flag. 2252 * 2253 * @param critical the new critical flag 2254 */ 2255 public void setCritical( boolean critical ) 2256 { 2257 this.isCritical = critical; 2258 } 2259 2260 2261 /** 2262 * Gets the type. 2263 * 2264 * @return the type 2265 */ 2266 public String getType() 2267 { 2268 return type; 2269 } 2270 2271 2272 /** 2273 * Sets the type. 2274 * 2275 * @param type the new type 2276 */ 2277 public void setType( String type ) 2278 { 2279 this.type = type; 2280 } 2281 2282 2283 /** 2284 * Gets the value. 2285 * 2286 * @return the value 2287 */ 2288 public String getValue() 2289 { 2290 return value; 2291 } 2292 2293 2294 /** 2295 * Sets the value. 2296 * 2297 * @param value the new value 2298 */ 2299 public void setValue( String value ) 2300 { 2301 this.value = value; 2302 } 2303 } 2304}