001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2008 Sun Microsystems, Inc. 025 * Portions Copyright 2014-2015 ForgeRock AS 026 */ 027package org.opends.server.authorization.dseecompat; 028 029import static org.opends.messages.AccessControlMessages.*; 030import static org.opends.messages.SchemaMessages.*; 031import static org.opends.server.util.CollectionUtils.*; 032import static org.opends.server.util.StaticUtils.*; 033 034import java.util.ArrayList; 035import java.util.List; 036 037import org.forgerock.i18n.LocalizableMessage; 038import org.forgerock.i18n.slf4j.LocalizedLogger; 039import org.forgerock.opendj.ldap.ByteString; 040import org.forgerock.opendj.ldap.ResultCode; 041import org.forgerock.util.Reject; 042import org.opends.server.types.DN; 043import org.opends.server.types.DirectoryException; 044 045/** 046 * This class is used to encapsulate DN pattern matching using wildcards. 047 * The following wildcard uses are supported. 048 * 049 * Value substring: Any number of wildcards may appear in RDN attribute 050 * values where they match zero or more characters, just like substring filters: 051 * uid=b*jensen* 052 * 053 * Whole-Type: A single wildcard may also be used to match any RDN attribute 054 * type, and the wildcard in this case may be omitted as a shorthand: 055 * *=bjensen 056 * bjensen 057 * 058 * Whole-RDN. A single wildcard may be used to match exactly one RDN component 059 * (which may be single or multi-valued): 060 * *,dc=example,dc=com 061 * 062 * Multiple-Whole-RDN: A double wildcard may be used to match one or more 063 * RDN components: 064 * uid=bjensen,**,dc=example,dc=com 065 * 066 */ 067public class PatternDN 068{ 069 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 070 071 /** 072 * If the pattern did not include any Multiple-Whole-RDN wildcards, then 073 * this is the sequence of RDN patterns in the DN pattern. Otherwise it 074 * is null. 075 */ 076 PatternRDN[] equality; 077 078 079 /** 080 * If the pattern included any Multiple-Whole-RDN wildcards, then these 081 * are the RDN pattern sequences that appear between those wildcards. 082 */ 083 PatternRDN[] subInitial; 084 List<PatternRDN[]> subAnyElements; 085 PatternRDN[] subFinal; 086 087 088 /** 089 * When there is no initial sequence, this is used to distinguish between 090 * the case where we have a suffix pattern (zero or more RDN components 091 * allowed before matching elements) and the case where it is not a 092 * suffix pattern but the pattern started with a Multiple-Whole-RDN wildcard 093 * (one or more RDN components allowed before matching elements). 094 */ 095 boolean isSuffix; 096 097 098 /** 099 * Create a DN pattern that does not include any Multiple-Whole-RDN wildcards. 100 * @param equality The sequence of RDN patterns making up the DN pattern. 101 */ 102 private PatternDN(PatternRDN[] equality) 103 { 104 this.equality = equality; 105 } 106 107 108 /** 109 * Create a DN pattern that includes Multiple-Whole-RDN wildcards. 110 * @param subInitial The sequence of RDN patterns appearing at the 111 * start of the DN, or null if there are none. 112 * @param subAnyElements The list of sequences of RDN patterns appearing 113 * in order anywhere in the DN. 114 * @param subFinal The sequence of RDN patterns appearing at the 115 * end of the DN, or null if there are none. 116 */ 117 private PatternDN(PatternRDN[] subInitial, 118 List<PatternRDN[]> subAnyElements, 119 PatternRDN[] subFinal) 120 { 121 Reject.ifNull(subAnyElements); 122 this.subInitial = subInitial; 123 this.subAnyElements = subAnyElements; 124 this.subFinal = subFinal; 125 } 126 127 128 /** 129 * Determine whether a given DN matches this pattern. 130 * @param dn The DN to be matched. 131 * @return true if the DN matches the pattern. 132 */ 133 public boolean matchesDN(DN dn) 134 { 135 if (equality != null) 136 { 137 // There are no Multiple-Whole-RDN wildcards in the pattern. 138 if (equality.length != dn.size()) 139 { 140 return false; 141 } 142 143 for (int i = 0; i < dn.size(); i++) 144 { 145 if (!equality[i].matchesRDN(dn.getRDN(i))) 146 { 147 return false; 148 } 149 } 150 151 return true; 152 } 153 else 154 { 155 // There are Multiple-Whole-RDN wildcards in the pattern. 156 int valueLength = dn.size(); 157 158 int pos = 0; 159 if (subInitial != null) 160 { 161 int initialLength = subInitial.length; 162 if (initialLength > valueLength) 163 { 164 return false; 165 } 166 167 for (; pos < initialLength; pos++) 168 { 169 if (!subInitial[pos].matchesRDN(dn.getRDN(pos))) 170 { 171 return false; 172 } 173 } 174 pos++; 175 } 176 else 177 { 178 if (!isSuffix) 179 { 180 pos++; 181 } 182 } 183 184 185 if (subAnyElements != null && ! subAnyElements.isEmpty()) 186 { 187 for (PatternRDN[] element : subAnyElements) 188 { 189 int anyLength = element.length; 190 191 int end = valueLength - anyLength; 192 boolean match = false; 193 for (; pos < end; pos++) 194 { 195 if (element[0].matchesRDN(dn.getRDN(pos))) 196 { 197 if (subMatch(dn, pos, element, anyLength)) 198 { 199 match = true; 200 break; 201 } 202 } 203 } 204 205 if (match) 206 { 207 pos += anyLength + 1; 208 } 209 else 210 { 211 return false; 212 } 213 } 214 } 215 216 217 if (subFinal != null) 218 { 219 int finalLength = subFinal.length; 220 221 if (valueLength - finalLength < pos) 222 { 223 return false; 224 } 225 226 pos = valueLength - finalLength; 227 for (int i=0; i < finalLength; i++,pos++) 228 { 229 if (!subFinal[i].matchesRDN(dn.getRDN(pos))) 230 { 231 return false; 232 } 233 } 234 } 235 236 return pos <= valueLength; 237 } 238 } 239 240 private boolean subMatch(DN dn, int pos, PatternRDN[] element, int length) 241 { 242 for (int i = 1; i < length; i++) 243 { 244 if (!element[i].matchesRDN(dn.getRDN(pos + i))) 245 { 246 return false; 247 } 248 } 249 return true; 250 } 251 252 /** 253 * Create a new DN pattern matcher to match a suffix. 254 * @param pattern The suffix pattern string. 255 * @throws org.opends.server.types.DirectoryException If the pattern string 256 * is not valid. 257 * @return A new DN pattern matcher. 258 */ 259 public static PatternDN decodeSuffix(String pattern) 260 throws DirectoryException 261 { 262 // Parse the user supplied pattern. 263 PatternDN patternDN = decode(pattern); 264 265 // Adjust the pattern so that it matches any DN ending with the pattern. 266 if (patternDN.equality != null) 267 { 268 // The pattern contained no Multiple-Whole-RDN wildcards, 269 // so we just convert the whole thing into a final fragment. 270 patternDN.subInitial = null; 271 patternDN.subFinal = patternDN.equality; 272 patternDN.subAnyElements = null; 273 patternDN.equality = null; 274 } 275 else if (patternDN.subInitial != null) 276 { 277 // The pattern had an initial fragment so we need to convert that into 278 // the head of the list of any elements. 279 patternDN.subAnyElements.add(0, patternDN.subInitial); 280 patternDN.subInitial = null; 281 } 282 patternDN.isSuffix = true; 283 return patternDN; 284 } 285 286 287 /** 288 * Create a new DN pattern matcher from a pattern string. 289 * @param dnString The DN pattern string. 290 * @throws org.opends.server.types.DirectoryException If the pattern string 291 * is not valid. 292 * @return A new DN pattern matcher. 293 */ 294 public static PatternDN decode(String dnString) 295 throws DirectoryException 296 { 297 ArrayList<PatternRDN> rdnComponents = new ArrayList<>(); 298 ArrayList<Integer> doubleWildPos = new ArrayList<>(); 299 300 // A null or empty DN is acceptable. 301 if (dnString == null) 302 { 303 return new PatternDN(new PatternRDN[0]); 304 } 305 306 int length = dnString.length(); 307 if (length == 0) 308 { 309 return new PatternDN(new PatternRDN[0]); 310 } 311 312 313 // Iterate through the DN string. The first thing to do is to get 314 // rid of any leading spaces. 315 int pos = 0; 316 char c = dnString.charAt(pos); 317 while (c == ' ') 318 { 319 pos++; 320 if (pos == length) 321 { 322 // This means that the DN was completely comprised of spaces 323 // and therefore should be considered the same as a null or empty DN. 324 return new PatternDN(new PatternRDN[0]); 325 } 326 else 327 { 328 c = dnString.charAt(pos); 329 } 330 } 331 332 // We know that it's not an empty DN, so we can do the real 333 // processing. Create a loop and iterate through all the RDN components. 334 rdnLoop: 335 while (true) 336 { 337 int attributePos = pos; 338 StringBuilder attributeName = new StringBuilder(); 339 pos = parseAttributePattern(dnString, pos, attributeName); 340 String name = attributeName.toString(); 341 342 343 // Make sure that we're not at the end of the DN string because 344 // that would be invalid. 345 if (pos >= length) 346 { 347 if (name.equals("*")) 348 { 349 rdnComponents.add(new PatternRDN(name, null, dnString)); 350 break; 351 } 352 else if (name.equals("**")) 353 { 354 doubleWildPos.add(rdnComponents.size()); 355 break; 356 } 357 else 358 { 359 pos = attributePos - 1; 360 name = "*"; 361 c = '='; 362 } 363 } 364 else 365 { 366 // Skip over any spaces between the attribute name and its 367 // value. 368 c = dnString.charAt(pos); 369 while (c == ' ') 370 { 371 pos++; 372 if (pos >= length) 373 { 374 if (name.equals("*")) 375 { 376 rdnComponents.add(new PatternRDN(name, null, dnString)); 377 break rdnLoop; 378 } 379 else if (name.equals("**")) 380 { 381 doubleWildPos.add(rdnComponents.size()); 382 break rdnLoop; 383 } 384 else 385 { 386 pos = attributePos - 1; 387 name = "*"; 388 c = '='; 389 } 390 } 391 else 392 { 393 c = dnString.charAt(pos); 394 } 395 } 396 } 397 398 399 if (c == '=') 400 { 401 pos++; 402 } 403 else if (c == ',' || c == ';') 404 { 405 if (name.equals("*")) 406 { 407 rdnComponents.add(new PatternRDN(name, null, dnString)); 408 pos++; 409 continue; 410 } 411 else if (name.equals("**")) 412 { 413 doubleWildPos.add(rdnComponents.size()); 414 pos++; 415 continue; 416 } 417 else 418 { 419 pos = attributePos; 420 name = "*"; 421 } 422 } 423 else 424 { 425 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 426 ERR_ATTR_SYNTAX_DN_NO_EQUAL.get(dnString, attributeName, c)); 427 } 428 429 // Skip over any spaces after the equal sign. 430 while (pos < length && dnString.charAt(pos) == ' ') 431 { 432 pos++; 433 } 434 435 436 // If we are at the end of the DN string, then that must mean 437 // that the attribute value was empty. This will probably never 438 // happen in a real-world environment, but technically isn't 439 // illegal. If it does happen, then go ahead and create the 440 // RDN component and return the DN. 441 if (pos >= length) 442 { 443 ArrayList<ByteString> arrayList = newArrayList(ByteString.empty()); 444 rdnComponents.add(new PatternRDN(name, arrayList, dnString)); 445 break; 446 } 447 448 449 // Parse the value for this RDN component. 450 ArrayList<ByteString> parsedValue = new ArrayList<>(); 451 pos = parseValuePattern(dnString, pos, parsedValue); 452 453 454 // Create the new RDN with the provided information. 455 PatternRDN rdn = new PatternRDN(name, parsedValue, dnString); 456 457 458 // Skip over any spaces that might be after the attribute value. 459 while (pos < length && ((c = dnString.charAt(pos)) == ' ')) 460 { 461 pos++; 462 } 463 464 465 // Most likely, we will be at either the end of the RDN 466 // component or the end of the DN. If so, then handle that appropriately. 467 if (pos >= length) 468 { 469 // We're at the end of the DN string and should have a valid DN so return it. 470 rdnComponents.add(rdn); 471 break; 472 } 473 else if (c == ',' || c == ';') 474 { 475 // We're at the end of the RDN component, so add it to the list, 476 // skip over the comma/semicolon, and start on the next component. 477 rdnComponents.add(rdn); 478 pos++; 479 continue; 480 } 481 else if (c != '+') 482 { 483 // This should not happen. At any rate, it's an illegal 484 // character, so throw an exception. 485 LocalizableMessage message = ERR_ATTR_SYNTAX_DN_INVALID_CHAR.get(dnString, c, pos); 486 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 487 } 488 489 490 // If we have gotten here, then this must be a multi-valued RDN. 491 // In that case, parse the remaining attribute/value pairs and 492 // add them to the RDN that we've already created. 493 while (true) 494 { 495 // Skip over the plus sign and any spaces that may follow it 496 // before the next attribute name. 497 pos++; 498 while (pos < length && dnString.charAt(pos) == ' ') 499 { 500 pos++; 501 } 502 503 504 // Parse the attribute name from the DN string. 505 attributeName = new StringBuilder(); 506 pos = parseAttributePattern(dnString, pos, attributeName); 507 508 509 // Make sure that we're not at the end of the DN string 510 // because that would be invalid. 511 if (pos >= length) 512 { 513 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 514 ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME.get(dnString, attributeName)); 515 } 516 517 518 name = attributeName.toString(); 519 520 // Skip over any spaces between the attribute name and its 521 // value. 522 c = dnString.charAt(pos); 523 while (c == ' ') 524 { 525 pos++; 526 if (pos >= length) 527 { 528 // This means that we hit the end of the value before 529 // finding a '='. This is illegal because there is no 530 // attribute-value separator. 531 LocalizableMessage message = 532 ERR_ATTR_SYNTAX_DN_END_WITH_ATTR_NAME.get(dnString, name); 533 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 534 message); 535 } 536 else 537 { 538 c = dnString.charAt(pos); 539 } 540 } 541 542 543 // The next character must be an equal sign. If it is not, 544 // then that's an error. 545 if (c == '=') 546 { 547 pos++; 548 } 549 else 550 { 551 LocalizableMessage message = ERR_ATTR_SYNTAX_DN_NO_EQUAL.get(dnString, name, c); 552 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 553 message); 554 } 555 556 557 // Skip over any spaces after the equal sign. 558 while (pos < length && ((c = dnString.charAt(pos)) == ' ')) 559 { 560 pos++; 561 } 562 563 564 // If we are at the end of the DN string, then that must mean 565 // that the attribute value was empty. This will probably 566 // never happen in a real-world environment, but technically 567 // isn't illegal. If it does happen, then go ahead and create 568 // the RDN component and return the DN. 569 if (pos >= length) 570 { 571 ArrayList<ByteString> arrayList = newArrayList(ByteString.empty()); 572 rdn.addValue(name, arrayList, dnString); 573 rdnComponents.add(rdn); 574 break; 575 } 576 577 578 // Parse the value for this RDN component. 579 parsedValue = new ArrayList<>(); 580 pos = parseValuePattern(dnString, pos, parsedValue); 581 582 583 // Create the new RDN with the provided information. 584 rdn.addValue(name, parsedValue, dnString); 585 586 587 // Skip over any spaces that might be after the attribute value. 588 while (pos < length && ((c = dnString.charAt(pos)) == ' ')) 589 { 590 pos++; 591 } 592 593 594 // Most likely, we will be at either the end of the RDN 595 // component or the end of the DN. If so, then handle that appropriately. 596 if (pos >= length) 597 { 598 // We're at the end of the DN string and should have a valid 599 // DN so return it. 600 rdnComponents.add(rdn); 601 break; 602 } 603 else if (c == ',' || c == ';') 604 { 605 // We're at the end of the RDN component, so add it to the 606 // list, skip over the comma/semicolon, and start on the next component. 607 rdnComponents.add(rdn); 608 pos++; 609 break; 610 } 611 else if (c != '+') 612 { 613 // This should not happen. At any rate, it's an illegal 614 // character, so throw an exception. 615 LocalizableMessage message = 616 ERR_ATTR_SYNTAX_DN_INVALID_CHAR.get(dnString, c, pos); 617 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); 618 } 619 } 620 } 621 622 if (doubleWildPos.isEmpty()) 623 { 624 PatternRDN[] patterns = new PatternRDN[rdnComponents.size()]; 625 patterns = rdnComponents.toArray(patterns); 626 return new PatternDN(patterns); 627 } 628 else 629 { 630 PatternRDN[] subInitial = null; 631 PatternRDN[] subFinal = null; 632 List<PatternRDN[]> subAnyElements = new ArrayList<>(); 633 634 int i = 0; 635 int numComponents = rdnComponents.size(); 636 637 int to = doubleWildPos.get(i); 638 if (to != 0) 639 { 640 // Initial piece. 641 subInitial = new PatternRDN[to]; 642 subInitial = rdnComponents.subList(0, to).toArray(subInitial); 643 } 644 645 int from; 646 for (; i < doubleWildPos.size() - 1; i++) 647 { 648 from = doubleWildPos.get(i); 649 to = doubleWildPos.get(i+1); 650 PatternRDN[] subAny = new PatternRDN[to-from]; 651 subAny = rdnComponents.subList(from, to).toArray(subAny); 652 subAnyElements.add(subAny); 653 } 654 655 if (i < doubleWildPos.size()) 656 { 657 from = doubleWildPos.get(i); 658 if (from != numComponents) 659 { 660 // Final piece. 661 subFinal = new PatternRDN[numComponents-from]; 662 subFinal = rdnComponents.subList(from, numComponents). 663 toArray(subFinal); 664 } 665 } 666 667 return new PatternDN(subInitial, subAnyElements, subFinal); 668 } 669 } 670 671 672 /** 673 * Parses an attribute name pattern from the provided DN pattern string 674 * starting at the specified location. 675 * 676 * @param dnString The DN pattern string to be parsed. 677 * @param pos The position at which to start parsing 678 * the attribute name pattern. 679 * @param attributeName The buffer to which to append the parsed 680 * attribute name pattern. 681 * 682 * @return The position of the first character that is not part of 683 * the attribute name pattern. 684 * 685 * @throws DirectoryException If it was not possible to parse a 686 * valid attribute name pattern from the 687 * provided DN pattern string. 688 */ 689 static int parseAttributePattern(String dnString, int pos, 690 StringBuilder attributeName) 691 throws DirectoryException 692 { 693 int length = dnString.length(); 694 695 696 // Skip over any leading spaces. 697 if (pos < length) 698 { 699 while (dnString.charAt(pos) == ' ') 700 { 701 pos++; 702 if (pos == length) 703 { 704 // This means that the remainder of the DN was completely 705 // comprised of spaces. If we have gotten here, then we 706 // know that there is at least one RDN component, and 707 // therefore the last non-space character of the DN must 708 // have been a comma. This is not acceptable. 709 LocalizableMessage message = ERR_ATTR_SYNTAX_DN_END_WITH_COMMA.get(dnString); 710 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 711 message); 712 } 713 } 714 } 715 716 // Next, we should find the attribute name for this RDN component. 717 boolean checkForOID = false; 718 boolean endOfName = false; 719 while (pos < length) 720 { 721 // To make the switch more efficient, we'll include all ASCII 722 // characters in the range of allowed values and then reject the 723 // ones that aren't allowed. 724 char c = dnString.charAt(pos); 725 switch (c) 726 { 727 case ' ': 728 // This should denote the end of the attribute name. 729 endOfName = true; 730 break; 731 732 733 case '!': 734 case '"': 735 case '#': 736 case '$': 737 case '%': 738 case '&': 739 case '\'': 740 case '(': 741 case ')': 742 // None of these are allowed in an attribute name or any 743 // character immediately following it. 744 LocalizableMessage message = 745 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 746 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 747 message); 748 749 750 case '*': 751 // Wildcard character. 752 attributeName.append(c); 753 break; 754 755 case '+': 756 // None of these are allowed in an attribute name or any 757 // character immediately following it. 758 message = 759 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 760 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 761 message); 762 763 764 case ',': 765 // This should denote the end of the attribute name. 766 endOfName = true; 767 break; 768 769 case '-': 770 // This will be allowed as long as it isn't the first 771 // character in the attribute name. 772 if (attributeName.length() > 0) 773 { 774 attributeName.append(c); 775 } 776 else 777 { 778 message = 779 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_INITIAL_DASH.get(dnString); 780 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 781 message); 782 } 783 break; 784 785 786 case '.': 787 // The period could be allowed if the attribute name is 788 // actually expressed as an OID. We'll accept it for now, 789 // but make sure to check it later. 790 attributeName.append(c); 791 checkForOID = true; 792 break; 793 794 795 case '/': 796 // This is not allowed in an attribute name or any character 797 // immediately following it. 798 message = 799 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 800 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 801 message); 802 803 804 case '0': 805 case '1': 806 case '2': 807 case '3': 808 case '4': 809 case '5': 810 case '6': 811 case '7': 812 case '8': 813 case '9': 814 // Digits are always allowed if they are not the first 815 // character. However, they may be allowed if they are the 816 // first character if the valid is an OID or if the 817 // attribute name exceptions option is enabled. Therefore, 818 // we'll accept it now and check it later. 819 attributeName.append(c); 820 break; 821 822 823 case ':': 824 // Not allowed in an attribute name or any 825 // character immediately following it. 826 message = 827 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 828 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 829 message); 830 831 832 case ';': // NOTE: attribute options are not allowed in a DN. 833 // This should denote the end of the attribute name. 834 endOfName = true; 835 break; 836 837 case '<': 838 // None of these are allowed in an attribute name or any 839 // character immediately following it. 840 message = 841 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 842 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 843 message); 844 845 846 case '=': 847 // This should denote the end of the attribute name. 848 endOfName = true; 849 break; 850 851 852 case '>': 853 case '?': 854 case '@': 855 // None of these are allowed in an attribute name or any 856 // character immediately following it. 857 message = 858 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 859 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 860 message); 861 862 863 case 'A': 864 case 'B': 865 case 'C': 866 case 'D': 867 case 'E': 868 case 'F': 869 case 'G': 870 case 'H': 871 case 'I': 872 case 'J': 873 case 'K': 874 case 'L': 875 case 'M': 876 case 'N': 877 case 'O': 878 case 'P': 879 case 'Q': 880 case 'R': 881 case 'S': 882 case 'T': 883 case 'U': 884 case 'V': 885 case 'W': 886 case 'X': 887 case 'Y': 888 case 'Z': 889 // These will always be allowed. 890 attributeName.append(c); 891 break; 892 893 894 case '[': 895 case '\\': 896 case ']': 897 case '^': 898 // None of these are allowed in an attribute name or any 899 // character immediately following it. 900 message = 901 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 902 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 903 message); 904 905 906 case '_': 907 attributeName.append(c); 908 break; 909 910 911 case '`': 912 // This is not allowed in an attribute name or any character 913 // immediately following it. 914 message = 915 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 916 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 917 message); 918 919 920 case 'a': 921 case 'b': 922 case 'c': 923 case 'd': 924 case 'e': 925 case 'f': 926 case 'g': 927 case 'h': 928 case 'i': 929 case 'j': 930 case 'k': 931 case 'l': 932 case 'm': 933 case 'n': 934 case 'o': 935 case 'p': 936 case 'q': 937 case 'r': 938 case 's': 939 case 't': 940 case 'u': 941 case 'v': 942 case 'w': 943 case 'x': 944 case 'y': 945 case 'z': 946 // These will always be allowed. 947 attributeName.append(c); 948 break; 949 950 951 default: 952 // This is not allowed in an attribute name or any character 953 // immediately following it. 954 message = 955 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_CHAR.get(dnString, c, pos); 956 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 957 message); 958 } 959 960 961 if (endOfName) 962 { 963 break; 964 } 965 966 pos++; 967 } 968 969 970 // We should now have the full attribute name. However, we may 971 // still need to perform some validation, particularly if the 972 // name contains a period or starts with a digit. It must also 973 // have at least one character. 974 if (attributeName.length() == 0) 975 { 976 LocalizableMessage message = ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME.get(dnString); 977 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 978 message); 979 } 980 else if (checkForOID) 981 { 982 boolean validOID = true; 983 984 int namePos = 0; 985 int nameLength = attributeName.length(); 986 char ch0 = attributeName.charAt(0); 987 if (ch0 == 'o' || ch0 == 'O') 988 { 989 if (nameLength <= 4) 990 { 991 validOID = false; 992 } 993 else 994 { 995 char ch1 = attributeName.charAt(1); 996 char ch2 = attributeName.charAt(2); 997 if ((ch1 == 'i' || ch1 == 'I') 998 && (ch2 == 'd' || ch2 == 'D') 999 && attributeName.charAt(3) == '.') 1000 { 1001 attributeName.delete(0, 4); 1002 nameLength -= 4; 1003 } 1004 else 1005 { 1006 validOID = false; 1007 } 1008 } 1009 } 1010 1011 while (validOID && namePos < nameLength) 1012 { 1013 char ch = attributeName.charAt(namePos++); 1014 if (isDigit(ch)) 1015 { 1016 while (validOID && namePos < nameLength && 1017 isDigit(attributeName.charAt(namePos))) 1018 { 1019 namePos++; 1020 } 1021 1022 if (namePos < nameLength && 1023 attributeName.charAt(namePos) != '.') 1024 { 1025 validOID = false; 1026 } 1027 } 1028 else if (ch == '.') 1029 { 1030 if (namePos == 1 || 1031 attributeName.charAt(namePos-2) == '.') 1032 { 1033 validOID = false; 1034 } 1035 } 1036 else 1037 { 1038 validOID = false; 1039 } 1040 } 1041 1042 1043 if (validOID && attributeName.charAt(nameLength-1) == '.') 1044 { 1045 validOID = false; 1046 } 1047 1048 if (! validOID) 1049 { 1050 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1051 ERR_ATTR_SYNTAX_DN_ATTR_ILLEGAL_PERIOD.get(dnString, attributeName)); 1052 } 1053 } 1054 1055 return pos; 1056 } 1057 1058 1059 /** 1060 * Parses the attribute value pattern from the provided DN pattern 1061 * string starting at the specified location. The value is split up 1062 * according to the wildcard locations, and the fragments are inserted 1063 * into the provided list. 1064 * 1065 * @param dnString The DN pattern string to be parsed. 1066 * @param pos The position of the first character in 1067 * the attribute value pattern to parse. 1068 * @param attributeValues The list whose elements should be set to 1069 * the parsed attribute value fragments when 1070 * this method completes successfully. 1071 * 1072 * @return The position of the first character that is not part of 1073 * the attribute value. 1074 * 1075 * @throws DirectoryException If it was not possible to parse a 1076 * valid attribute value pattern from the 1077 * provided DN string. 1078 */ 1079 private static int parseValuePattern(String dnString, int pos, 1080 ArrayList<ByteString> attributeValues) 1081 throws DirectoryException 1082 { 1083 // All leading spaces have already been stripped so we can start 1084 // reading the value. However, it may be empty so check for that. 1085 int length = dnString.length(); 1086 if (pos >= length) 1087 { 1088 return pos; 1089 } 1090 1091 1092 // Look at the first character. If it is an octothorpe (#), then 1093 // that means that the value should be a hex string. 1094 char c = dnString.charAt(pos++); 1095 if (c == '#') 1096 { 1097 // The first two characters must be hex characters. 1098 StringBuilder hexString = new StringBuilder(); 1099 if (pos+2 > length) 1100 { 1101 LocalizableMessage message = ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(dnString); 1102 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1103 message); 1104 } 1105 1106 for (int i=0; i < 2; i++) 1107 { 1108 c = dnString.charAt(pos++); 1109 if (isHexDigit(c)) 1110 { 1111 hexString.append(c); 1112 } 1113 else 1114 { 1115 LocalizableMessage message = 1116 ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(dnString, c); 1117 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1118 message); 1119 } 1120 } 1121 1122 1123 // The rest of the value must be a multiple of two hex 1124 // characters. The end of the value may be designated by the 1125 // end of the DN, a comma or semicolon, or a space. 1126 while (pos < length) 1127 { 1128 c = dnString.charAt(pos++); 1129 if (isHexDigit(c)) 1130 { 1131 hexString.append(c); 1132 1133 if (pos < length) 1134 { 1135 c = dnString.charAt(pos++); 1136 if (isHexDigit(c)) 1137 { 1138 hexString.append(c); 1139 } 1140 else 1141 { 1142 LocalizableMessage message = 1143 ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(dnString, c); 1144 throw new DirectoryException( 1145 ResultCode.INVALID_DN_SYNTAX, message); 1146 } 1147 } 1148 else 1149 { 1150 LocalizableMessage message = 1151 ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(dnString); 1152 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1153 message); 1154 } 1155 } 1156 else if (c == ' ' || c == ',' || c == ';') 1157 { 1158 // This denotes the end of the value. 1159 pos--; 1160 break; 1161 } 1162 else 1163 { 1164 LocalizableMessage message = 1165 ERR_ATTR_SYNTAX_DN_INVALID_HEX_DIGIT.get(dnString, c); 1166 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1167 message); 1168 } 1169 } 1170 1171 1172 // At this point, we should have a valid hex string. Convert it 1173 // to a byte array and set that as the value of the provided 1174 // octet string. 1175 try 1176 { 1177 byte[] bytes = hexStringToByteArray(hexString.toString()); 1178 attributeValues.add(ByteString.wrap(bytes)); 1179 return pos; 1180 } 1181 catch (Exception e) 1182 { 1183 logger.traceException(e); 1184 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1185 ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE.get(dnString, e)); 1186 } 1187 } 1188 1189 1190 // If the first character is a quotation mark, then the value 1191 // should continue until the corresponding closing quotation mark. 1192 else if (c == '"') 1193 { 1194 // Keep reading until we find an unescaped closing quotation 1195 // mark. 1196 boolean escaped = false; 1197 StringBuilder valueString = new StringBuilder(); 1198 while (true) 1199 { 1200 if (pos >= length) 1201 { 1202 // We hit the end of the DN before the closing quote. 1203 // That's an error. 1204 LocalizableMessage message = ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE.get(dnString); 1205 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1206 message); 1207 } 1208 1209 c = dnString.charAt(pos++); 1210 if (escaped) 1211 { 1212 // The previous character was an escape, so we'll take this 1213 // one no matter what. 1214 valueString.append(c); 1215 escaped = false; 1216 } 1217 else if (c == '\\') 1218 { 1219 // The next character is escaped. Set a flag to denote 1220 // this, but don't include the backslash. 1221 escaped = true; 1222 } 1223 else if (c == '"') 1224 { 1225 // This is the end of the value. 1226 break; 1227 } 1228 else 1229 { 1230 // This is just a regular character that should be in the 1231 // value. 1232 valueString.append(c); 1233 } 1234 } 1235 1236 attributeValues.add(ByteString.valueOfUtf8(valueString)); 1237 return pos; 1238 } 1239 1240 1241 // Otherwise, use general parsing to find the end of the value. 1242 else 1243 { 1244 boolean escaped; 1245 StringBuilder valueString = new StringBuilder(); 1246 StringBuilder hexChars = new StringBuilder(); 1247 1248 if (c == '\\') 1249 { 1250 escaped = true; 1251 } 1252 else if (c == '*') 1253 { 1254 escaped = false; 1255 attributeValues.add(ByteString.valueOfUtf8(valueString)); 1256 } 1257 else 1258 { 1259 escaped = false; 1260 valueString.append(c); 1261 } 1262 1263 1264 // Keep reading until we find an unescaped comma or plus sign or 1265 // the end of the DN. 1266 while (true) 1267 { 1268 if (pos >= length) 1269 { 1270 // This is the end of the DN and therefore the end of the 1271 // value. If there are any hex characters, then we need to 1272 // deal with them accordingly. 1273 appendHexChars(dnString, valueString, hexChars); 1274 break; 1275 } 1276 1277 c = dnString.charAt(pos++); 1278 if (escaped) 1279 { 1280 // The previous character was an escape, so we'll take this 1281 // one. However, this could be a hex digit, and if that's 1282 // the case then the escape would actually be in front of 1283 // two hex digits that should be treated as a special 1284 // character. 1285 if (isHexDigit(c)) 1286 { 1287 // It is a hexadecimal digit, so the next digit must be 1288 // one too. However, this could be just one in a series 1289 // of escaped hex pairs that is used in a string 1290 // containing one or more multi-byte UTF-8 characters so 1291 // we can't just treat this byte in isolation. Collect 1292 // all the bytes together and make sure to take care of 1293 // these hex bytes before appending anything else to the 1294 // value. 1295 if (pos >= length) 1296 { 1297 LocalizableMessage message = 1298 ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID.get(dnString); 1299 throw new DirectoryException( 1300 ResultCode.INVALID_DN_SYNTAX, message); 1301 } 1302 else 1303 { 1304 char c2 = dnString.charAt(pos++); 1305 if (isHexDigit(c2)) 1306 { 1307 hexChars.append(c); 1308 hexChars.append(c2); 1309 } 1310 else 1311 { 1312 LocalizableMessage message = 1313 ERR_ATTR_SYNTAX_DN_ESCAPED_HEX_VALUE_INVALID.get(dnString); 1314 throw new DirectoryException( 1315 ResultCode.INVALID_DN_SYNTAX, message); 1316 } 1317 } 1318 } 1319 else 1320 { 1321 appendHexChars(dnString, valueString, hexChars); 1322 valueString.append(c); 1323 } 1324 1325 escaped = false; 1326 } 1327 else if (c == '\\') 1328 { 1329 escaped = true; 1330 } 1331 else if (c == ',' || c == ';') 1332 { 1333 appendHexChars(dnString, valueString, hexChars); 1334 pos--; 1335 break; 1336 } 1337 else if (c == '+') 1338 { 1339 appendHexChars(dnString, valueString, hexChars); 1340 pos--; 1341 break; 1342 } 1343 else if (c == '*') 1344 { 1345 appendHexChars(dnString, valueString, hexChars); 1346 if (valueString.length() == 0) 1347 { 1348 LocalizableMessage message = 1349 WARN_PATTERN_DN_CONSECUTIVE_WILDCARDS_IN_VALUE.get(dnString); 1350 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1351 message); 1352 } 1353 attributeValues.add(ByteString.valueOfUtf8(valueString)); 1354 valueString = new StringBuilder(); 1355 hexChars = new StringBuilder(); 1356 } 1357 else 1358 { 1359 appendHexChars(dnString, valueString, hexChars); 1360 valueString.append(c); 1361 } 1362 } 1363 1364 1365 // Strip off any unescaped spaces that may be at the end of the 1366 // value. 1367 if (pos > 2 && dnString.charAt(pos-1) == ' ' && 1368 dnString.charAt(pos-2) != '\\') 1369 { 1370 int lastPos = valueString.length() - 1; 1371 while (lastPos > 0) 1372 { 1373 if (valueString.charAt(lastPos) == ' ') 1374 { 1375 valueString.delete(lastPos, lastPos+1); 1376 lastPos--; 1377 } 1378 else 1379 { 1380 break; 1381 } 1382 } 1383 } 1384 1385 1386 attributeValues.add(ByteString.valueOfUtf8(valueString)); 1387 return pos; 1388 } 1389 } 1390 1391 1392 /** 1393 * Decodes a hexadecimal string from the provided 1394 * <CODE>hexChars</CODE> buffer, converts it to a byte array, and 1395 * then converts that to a UTF-8 string. The resulting UTF-8 string 1396 * will be appended to the provided <CODE>valueString</CODE> buffer, 1397 * and the <CODE>hexChars</CODE> buffer will be cleared. 1398 * 1399 * @param dnString The DN string that is being decoded. 1400 * @param valueString The buffer containing the value to which the 1401 * decoded string should be appended. 1402 * @param hexChars The buffer containing the hexadecimal 1403 * characters to decode to a UTF-8 string. 1404 * 1405 * @throws DirectoryException If any problem occurs during the 1406 * decoding process. 1407 */ 1408 private static void appendHexChars(String dnString, 1409 StringBuilder valueString, 1410 StringBuilder hexChars) 1411 throws DirectoryException 1412 { 1413 try 1414 { 1415 byte[] hexBytes = hexStringToByteArray(hexChars.toString()); 1416 valueString.append(new String(hexBytes, "UTF-8")); 1417 hexChars.delete(0, hexChars.length()); 1418 } 1419 catch (Exception e) 1420 { 1421 logger.traceException(e); 1422 throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, 1423 ERR_ATTR_SYNTAX_DN_ATTR_VALUE_DECODE_FAILURE.get(dnString, e)); 1424 } 1425 } 1426}