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