1 /* 2 * Copyright (c) 1999, 2020, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package com.sun.jndi.toolkit.dir; 26 27 import javax.naming.*; 28 import javax.naming.directory.*; 29 import java.util.Enumeration; 30 import java.util.HexFormat; 31 import java.util.StringTokenizer; 32 import java.util.Vector; 33 import java.util.Locale; 34 35 /** 36 * A class for parsing LDAP search filters (defined in RFC 1960, 2254) 37 * 38 * @author Jon Ruiz 39 * @author Rosanna Lee 40 */ 41 public class SearchFilter implements AttrFilter { 42 43 interface StringFilter extends AttrFilter { parse()44 public void parse() throws InvalidSearchFilterException; 45 } 46 47 // %%% "filter" and "pos" are not declared "private" due to bug 4064984. 48 String filter; 49 int pos; 50 private StringFilter rootFilter; 51 52 protected static final boolean debug = false; 53 54 protected static final char BEGIN_FILTER_TOKEN = '('; 55 protected static final char END_FILTER_TOKEN = ')'; 56 protected static final char AND_TOKEN = '&'; 57 protected static final char OR_TOKEN = '|'; 58 protected static final char NOT_TOKEN = '!'; 59 protected static final char EQUAL_TOKEN = '='; 60 protected static final char APPROX_TOKEN = '~'; 61 protected static final char LESS_TOKEN = '<'; 62 protected static final char GREATER_TOKEN = '>'; 63 protected static final char EXTEND_TOKEN = ':'; 64 protected static final char WILDCARD_TOKEN = '*'; 65 SearchFilter(String filter)66 public SearchFilter(String filter) throws InvalidSearchFilterException { 67 this.filter = filter; 68 pos = 0; 69 normalizeFilter(); 70 rootFilter = this.createNextFilter(); 71 } 72 73 // Returns true if targetAttrs passes the filter check(Attributes targetAttrs)74 public boolean check(Attributes targetAttrs) throws NamingException { 75 if (targetAttrs == null) 76 return false; 77 78 return rootFilter.check(targetAttrs); 79 } 80 81 /* 82 * Utility routines used by member classes 83 */ 84 85 // does some pre-processing on the string to make it look exactly lik 86 // what the parser expects. This only needs to be called once. normalizeFilter()87 protected void normalizeFilter() { 88 skipWhiteSpace(); // get rid of any leading whitespaces 89 90 // Sometimes, search filters don't have "(" and ")" - add them 91 if(getCurrentChar() != BEGIN_FILTER_TOKEN) { 92 filter = BEGIN_FILTER_TOKEN + filter + END_FILTER_TOKEN; 93 } 94 // this would be a good place to strip whitespace if desired 95 96 if(debug) {System.out.println("SearchFilter: normalized filter:" + 97 filter);} 98 } 99 skipWhiteSpace()100 private void skipWhiteSpace() { 101 while (Character.isWhitespace(getCurrentChar())) { 102 consumeChar(); 103 } 104 } 105 createNextFilter()106 protected StringFilter createNextFilter() 107 throws InvalidSearchFilterException { 108 StringFilter filter; 109 110 skipWhiteSpace(); 111 112 try { 113 // make sure every filter starts with "(" 114 if(getCurrentChar() != BEGIN_FILTER_TOKEN) { 115 throw new InvalidSearchFilterException("expected \"" + 116 BEGIN_FILTER_TOKEN + 117 "\" at position " + 118 pos); 119 } 120 121 // skip past the "(" 122 this.consumeChar(); 123 124 skipWhiteSpace(); 125 126 // use the next character to determine the type of filter 127 switch(getCurrentChar()) { 128 case AND_TOKEN: 129 if (debug) {System.out.println("SearchFilter: creating AND");} 130 filter = new CompoundFilter(true); 131 filter.parse(); 132 break; 133 case OR_TOKEN: 134 if (debug) {System.out.println("SearchFilter: creating OR");} 135 filter = new CompoundFilter(false); 136 filter.parse(); 137 break; 138 case NOT_TOKEN: 139 if (debug) {System.out.println("SearchFilter: creating OR");} 140 filter = new NotFilter(); 141 filter.parse(); 142 break; 143 default: 144 if (debug) {System.out.println("SearchFilter: creating SIMPLE");} 145 filter = new AtomicFilter(); 146 filter.parse(); 147 break; 148 } 149 150 skipWhiteSpace(); 151 152 // make sure every filter ends with ")" 153 if(getCurrentChar() != END_FILTER_TOKEN) { 154 throw new InvalidSearchFilterException("expected \"" + 155 END_FILTER_TOKEN + 156 "\" at position " + 157 pos); 158 } 159 160 // skip past the ")" 161 this.consumeChar(); 162 } catch (InvalidSearchFilterException e) { 163 if (debug) {System.out.println("rethrowing e");} 164 throw e; // just rethrow these 165 166 // catch all - any uncaught exception while parsing will end up here 167 } catch (Exception e) { 168 if(debug) {System.out.println(e.getMessage());e.printStackTrace();} 169 throw new InvalidSearchFilterException("Unable to parse " + 170 "character " + pos + " in \""+ 171 this.filter + "\""); 172 } 173 174 return filter; 175 } 176 getCurrentChar()177 protected char getCurrentChar() { 178 return filter.charAt(pos); 179 } 180 relCharAt(int i)181 protected char relCharAt(int i) { 182 return filter.charAt(pos + i); 183 } 184 consumeChar()185 protected void consumeChar() { 186 pos++; 187 } 188 consumeChars(int i)189 protected void consumeChars(int i) { 190 pos += i; 191 } 192 relIndexOf(int ch)193 protected int relIndexOf(int ch) { 194 return filter.indexOf(ch, pos) - pos; 195 } 196 relSubstring(int beginIndex, int endIndex)197 protected String relSubstring(int beginIndex, int endIndex){ 198 if(debug){System.out.println("relSubString: " + beginIndex + 199 " " + endIndex);} 200 return filter.substring(beginIndex+pos, endIndex+pos); 201 } 202 203 204 /** 205 * A class for dealing with compound filters ("and" & "or" filters). 206 */ 207 final class CompoundFilter implements StringFilter { 208 private Vector<StringFilter> subFilters; 209 private boolean polarity; 210 CompoundFilter(boolean polarity)211 CompoundFilter(boolean polarity) { 212 subFilters = new Vector<>(); 213 this.polarity = polarity; 214 } 215 parse()216 public void parse() throws InvalidSearchFilterException { 217 SearchFilter.this.consumeChar(); // consume the "&" 218 while(SearchFilter.this.getCurrentChar() != END_FILTER_TOKEN) { 219 if (debug) {System.out.println("CompoundFilter: adding");} 220 StringFilter filter = SearchFilter.this.createNextFilter(); 221 subFilters.addElement(filter); 222 skipWhiteSpace(); 223 } 224 } 225 check(Attributes targetAttrs)226 public boolean check(Attributes targetAttrs) throws NamingException { 227 for(int i = 0; i<subFilters.size(); i++) { 228 StringFilter filter = subFilters.elementAt(i); 229 if(filter.check(targetAttrs) != this.polarity) { 230 return !polarity; 231 } 232 } 233 return polarity; 234 } 235 } /* CompoundFilter */ 236 237 /** 238 * A class for dealing with NOT filters 239 */ 240 final class NotFilter implements StringFilter { 241 private StringFilter filter; 242 parse()243 public void parse() throws InvalidSearchFilterException { 244 SearchFilter.this.consumeChar(); // consume the "!" 245 filter = SearchFilter.this.createNextFilter(); 246 } 247 check(Attributes targetAttrs)248 public boolean check(Attributes targetAttrs) throws NamingException { 249 return !filter.check(targetAttrs); 250 } 251 } /* notFilter */ 252 253 // note: declared here since member classes can't have static variables 254 static final int EQUAL_MATCH = 1; 255 static final int APPROX_MATCH = 2; 256 static final int GREATER_MATCH = 3; 257 static final int LESS_MATCH = 4; 258 259 /** 260 * A class for dealing with atomic filters 261 */ 262 final class AtomicFilter implements StringFilter { 263 private String attrID; 264 private String value; 265 private int matchType; 266 parse()267 public void parse() throws InvalidSearchFilterException { 268 269 skipWhiteSpace(); 270 271 try { 272 // find the end 273 int endPos = SearchFilter.this.relIndexOf(END_FILTER_TOKEN); 274 275 //determine the match type 276 int i = SearchFilter.this.relIndexOf(EQUAL_TOKEN); 277 if(debug) {System.out.println("AtomicFilter: = at " + i);} 278 int qualifier = SearchFilter.this.relCharAt(i-1); 279 switch(qualifier) { 280 case APPROX_TOKEN: 281 if (debug) {System.out.println("Atomic: APPROX found");} 282 matchType = APPROX_MATCH; 283 attrID = SearchFilter.this.relSubstring(0, i-1); 284 value = SearchFilter.this.relSubstring(i+1, endPos); 285 break; 286 287 case GREATER_TOKEN: 288 if (debug) {System.out.println("Atomic: GREATER found");} 289 matchType = GREATER_MATCH; 290 attrID = SearchFilter.this.relSubstring(0, i-1); 291 value = SearchFilter.this.relSubstring(i+1, endPos); 292 break; 293 294 case LESS_TOKEN: 295 if (debug) {System.out.println("Atomic: LESS found");} 296 matchType = LESS_MATCH; 297 attrID = SearchFilter.this.relSubstring(0, i-1); 298 value = SearchFilter.this.relSubstring(i+1, endPos); 299 break; 300 301 case EXTEND_TOKEN: 302 if(debug) {System.out.println("Atomic: EXTEND found");} 303 throw new OperationNotSupportedException("Extensible match not supported"); 304 305 default: 306 if (debug) {System.out.println("Atomic: EQUAL found");} 307 matchType = EQUAL_MATCH; 308 attrID = SearchFilter.this.relSubstring(0,i); 309 value = SearchFilter.this.relSubstring(i+1, endPos); 310 break; 311 } 312 313 attrID = attrID.trim(); 314 value = value.trim(); 315 316 //update our position 317 SearchFilter.this.consumeChars(endPos); 318 319 } catch (Exception e) { 320 if (debug) {System.out.println(e.getMessage()); 321 e.printStackTrace();} 322 InvalidSearchFilterException sfe = 323 new InvalidSearchFilterException("Unable to parse " + 324 "character " + SearchFilter.this.pos + " in \""+ 325 SearchFilter.this.filter + "\""); 326 sfe.setRootCause(e); 327 throw(sfe); 328 } 329 330 if(debug) {System.out.println("AtomicFilter: " + attrID + "=" + 331 value);} 332 } 333 check(Attributes targetAttrs)334 public boolean check(Attributes targetAttrs) { 335 Enumeration<?> candidates; 336 337 try { 338 Attribute attr = targetAttrs.get(attrID); 339 if(attr == null) { 340 return false; 341 } 342 candidates = attr.getAll(); 343 } catch (NamingException ne) { 344 if (debug) {System.out.println("AtomicFilter: should never " + 345 "here");} 346 return false; 347 } 348 349 while(candidates.hasMoreElements()) { 350 String val = candidates.nextElement().toString(); 351 if (debug) {System.out.println("Atomic: comparing: " + val);} 352 switch(matchType) { 353 case APPROX_MATCH: 354 case EQUAL_MATCH: 355 if(substringMatch(this.value, val)) { 356 if (debug) {System.out.println("Atomic: EQUAL match");} 357 return true; 358 } 359 break; 360 case GREATER_MATCH: 361 if (debug) {System.out.println("Atomic: GREATER match");} 362 if(val.compareTo(this.value) >= 0) { 363 return true; 364 } 365 break; 366 case LESS_MATCH: 367 if (debug) {System.out.println("Atomic: LESS match");} 368 if(val.compareTo(this.value) <= 0) { 369 return true; 370 } 371 break; 372 default: 373 if (debug) {System.out.println("AtomicFilter: unknown " + 374 "matchType");} 375 } 376 } 377 return false; 378 } 379 380 // used for substring comparisons (where proto has "*" wildcards substringMatch(String proto, String value)381 private boolean substringMatch(String proto, String value) { 382 // simple case 1: "*" means attribute presence is being tested 383 if(proto.equals(Character.toString(WILDCARD_TOKEN))) { 384 if(debug) {System.out.println("simple presence assertion");} 385 return true; 386 } 387 388 // simple case 2: if there are no wildcards, call String.equals() 389 if(proto.indexOf(WILDCARD_TOKEN) == -1) { 390 return proto.equalsIgnoreCase(value); 391 } 392 393 if(debug) {System.out.println("doing substring comparison");} 394 // do the work: make sure all the substrings are present 395 int currentPos = 0; 396 StringTokenizer subStrs = new StringTokenizer(proto, "*", false); 397 398 // do we need to begin with the first token? 399 if(proto.charAt(0) != WILDCARD_TOKEN && 400 !value.toLowerCase(Locale.ENGLISH).startsWith( 401 subStrs.nextToken().toLowerCase(Locale.ENGLISH))) { 402 if(debug) { 403 System.out.println("faild initial test"); 404 } 405 return false; 406 } 407 408 while(subStrs.hasMoreTokens()) { 409 String currentStr = subStrs.nextToken(); 410 if (debug) {System.out.println("looking for \"" + 411 currentStr +"\"");} 412 currentPos = value.toLowerCase(Locale.ENGLISH).indexOf( 413 currentStr.toLowerCase(Locale.ENGLISH), currentPos); 414 415 if(currentPos == -1) { 416 return false; 417 } 418 currentPos += currentStr.length(); 419 } 420 421 // do we need to end with the last token? 422 if(proto.charAt(proto.length() - 1) != WILDCARD_TOKEN && 423 currentPos != value.length() ) { 424 if(debug) {System.out.println("faild final test");} 425 return false; 426 } 427 428 return true; 429 } 430 431 } /* AtomicFilter */ 432 433 // ----- static methods for producing string filters given attribute set 434 // ----- or object array 435 436 437 /** 438 * Creates an LDAP filter as a conjunction of the attributes supplied. 439 */ format(Attributes attrs)440 public static String format(Attributes attrs) throws NamingException { 441 if (attrs == null || attrs.size() == 0) { 442 return "objectClass=*"; 443 } 444 445 String answer; 446 answer = "(& "; 447 Attribute attr; 448 for (NamingEnumeration<? extends Attribute> e = attrs.getAll(); 449 e.hasMore(); ) { 450 attr = e.next(); 451 if (attr.size() == 0 || (attr.size() == 1 && attr.get() == null)) { 452 // only checking presence of attribute 453 answer += "(" + attr.getID() + "=" + "*)"; 454 } else { 455 for (NamingEnumeration<?> ve = attr.getAll(); 456 ve.hasMore(); ) { 457 String val = getEncodedStringRep(ve.next()); 458 if (val != null) { 459 answer += "(" + attr.getID() + "=" + val + ")"; 460 } 461 } 462 } 463 } 464 465 answer += ")"; 466 //System.out.println("filter: " + answer); 467 return answer; 468 } 469 470 /** 471 * Returns the string representation of an object (such as an attr value). 472 * If obj is a byte array, encode each item as \xx, where xx is hex encoding 473 * of the byte value. 474 * Else, if obj is not a String, use its string representation (toString()). 475 * Special characters in obj (or its string representation) are then 476 * encoded appropriately according to RFC 2254. 477 * * \2a 478 * ( \28 479 * ) \29 480 * \ \5c 481 * NUL \00 482 */ getEncodedStringRep(Object obj)483 private static String getEncodedStringRep(Object obj) throws NamingException { 484 String str; 485 if (obj == null) 486 return null; 487 488 if (obj instanceof byte[]) { 489 // binary data must be encoded as \hh where hh is a hex char 490 HexFormat hex = HexFormat.of().withUpperCase().withPrefix("\\"); 491 byte[] bytes = (byte[])obj; 492 return hex.formatHex(bytes); 493 } 494 if (!(obj instanceof String)) { 495 str = obj.toString(); 496 } else { 497 str = (String)obj; 498 } 499 int len = str.length(); 500 StringBuilder sb = new StringBuilder(len); 501 char ch; 502 for (int i = 0; i < len; i++) { 503 switch (ch=str.charAt(i)) { 504 case '*': 505 sb.append("\\2a"); 506 break; 507 case '(': 508 sb.append("\\28"); 509 break; 510 case ')': 511 sb.append("\\29"); 512 break; 513 case '\\': 514 sb.append("\\5c"); 515 break; 516 case 0: 517 sb.append("\\00"); 518 break; 519 default: 520 sb.append(ch); 521 } 522 } 523 return sb.toString(); 524 } 525 526 527 /** 528 * Finds the first occurrence of {@code ch} in {@code val} starting 529 * from position {@code start}. It doesn't count if {@code ch} 530 * has been escaped by a backslash (\) 531 */ findUnescaped(char ch, String val, int start)532 public static int findUnescaped(char ch, String val, int start) { 533 int len = val.length(); 534 535 while (start < len) { 536 int where = val.indexOf(ch, start); 537 // if at start of string, or not there at all, or if not escaped 538 if (where == start || where == -1 || val.charAt(where-1) != '\\') 539 return where; 540 541 // start search after escaped star 542 start = where + 1; 543 } 544 return -1; 545 } 546 547 /** 548 * Formats the expression {@code expr} using arguments from the array 549 * {@code args}. 550 * 551 * <code>{i}</code> specifies the <code>i</code>'th element from 552 * the array <code>args</code> is to be substituted for the 553 * string "<code>{i}</code>". 554 * 555 * To escape '{' or '}' (or any other character), use '\'. 556 * 557 * Uses getEncodedStringRep() to do encoding. 558 */ 559 format(String expr, Object[] args)560 public static String format(String expr, Object[] args) 561 throws NamingException { 562 563 int param; 564 int where = 0, start = 0; 565 StringBuilder answer = new StringBuilder(expr.length()); 566 567 while ((where = findUnescaped('{', expr, start)) >= 0) { 568 int pstart = where + 1; // skip '{' 569 int pend = expr.indexOf('}', pstart); 570 571 if (pend < 0) { 572 throw new InvalidSearchFilterException("unbalanced {: " + expr); 573 } 574 575 // at this point, pend should be pointing at '}' 576 try { 577 param = Integer.parseInt(expr.substring(pstart, pend)); 578 } catch (NumberFormatException e) { 579 throw new InvalidSearchFilterException( 580 "integer expected inside {}: " + expr); 581 } 582 583 if (param >= args.length) { 584 throw new InvalidSearchFilterException( 585 "number exceeds argument list: " + param); 586 } 587 588 answer.append(expr.substring(start, where)).append(getEncodedStringRep(args[param])); 589 start = pend + 1; // skip '}' 590 } 591 592 if (start < expr.length()) 593 answer.append(expr.substring(start)); 594 595 return answer.toString(); 596 } 597 598 /* 599 * returns an Attributes instance containing only attributeIDs given in 600 * "attributeIDs" whose values come from the given DSContext. 601 */ selectAttributes(Attributes originals, String[] attrIDs)602 public static Attributes selectAttributes(Attributes originals, 603 String[] attrIDs) throws NamingException { 604 605 if (attrIDs == null) 606 return originals; 607 608 Attributes result = new BasicAttributes(); 609 610 for(int i=0; i<attrIDs.length; i++) { 611 Attribute attr = originals.get(attrIDs[i]); 612 if(attr != null) { 613 result.put(attr); 614 } 615 } 616 617 return result; 618 } 619 620 /* For testing filter 621 public static void main(String[] args) { 622 623 Attributes attrs = new BasicAttributes(LdapClient.caseIgnore); 624 attrs.put("cn", "Rosanna Lee"); 625 attrs.put("sn", "Lee"); 626 attrs.put("fn", "Rosanna"); 627 attrs.put("id", "10414"); 628 attrs.put("machine", "jurassic"); 629 630 631 try { 632 System.out.println(format(attrs)); 633 634 String expr = "(&(Age = {0})(Account Balance <= {1}))"; 635 Object[] fargs = new Object[2]; 636 // fill in the parameters 637 fargs[0] = new Integer(65); 638 fargs[1] = new Float(5000); 639 640 System.out.println(format(expr, fargs)); 641 642 643 System.out.println(format("bin={0}", 644 new Object[] {new byte[] {0, 1, 2, 3, 4, 5}})); 645 646 System.out.println(format("bin=\\{anything}", null)); 647 648 } catch (NamingException e) { 649 e.printStackTrace(); 650 } 651 } 652 */ 653 654 } 655