/* * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.jndi.toolkit.dir; import javax.naming.*; import javax.naming.directory.*; import java.util.Enumeration; import java.util.StringTokenizer; import java.util.Vector; import java.util.Locale; /** * A class for parsing LDAP search filters (defined in RFC 1960, 2254) * * @author Jon Ruiz * @author Rosanna Lee */ public class SearchFilter implements AttrFilter { interface StringFilter extends AttrFilter { public void parse() throws InvalidSearchFilterException; } // %%% "filter" and "pos" are not declared "private" due to bug 4064984. String filter; int pos; private StringFilter rootFilter; protected static final boolean debug = false; protected static final char BEGIN_FILTER_TOKEN = '('; protected static final char END_FILTER_TOKEN = ')'; protected static final char AND_TOKEN = '&'; protected static final char OR_TOKEN = '|'; protected static final char NOT_TOKEN = '!'; protected static final char EQUAL_TOKEN = '='; protected static final char APPROX_TOKEN = '~'; protected static final char LESS_TOKEN = '<'; protected static final char GREATER_TOKEN = '>'; protected static final char EXTEND_TOKEN = ':'; protected static final char WILDCARD_TOKEN = '*'; public SearchFilter(String filter) throws InvalidSearchFilterException { this.filter = filter; pos = 0; normalizeFilter(); rootFilter = this.createNextFilter(); } // Returns true if targetAttrs passes the filter public boolean check(Attributes targetAttrs) throws NamingException { if (targetAttrs == null) return false; return rootFilter.check(targetAttrs); } /* * Utility routines used by member classes */ // does some pre-processing on the string to make it look exactly lik // what the parser expects. This only needs to be called once. protected void normalizeFilter() { skipWhiteSpace(); // get rid of any leading whitespaces // Sometimes, search filters don't have "(" and ")" - add them if(getCurrentChar() != BEGIN_FILTER_TOKEN) { filter = BEGIN_FILTER_TOKEN + filter + END_FILTER_TOKEN; } // this would be a good place to strip whitespace if desired if(debug) {System.out.println("SearchFilter: normalized filter:" + filter);} } private void skipWhiteSpace() { while (Character.isWhitespace(getCurrentChar())) { consumeChar(); } } protected StringFilter createNextFilter() throws InvalidSearchFilterException { StringFilter filter; skipWhiteSpace(); try { // make sure every filter starts with "(" if(getCurrentChar() != BEGIN_FILTER_TOKEN) { throw new InvalidSearchFilterException("expected \"" + BEGIN_FILTER_TOKEN + "\" at position " + pos); } // skip past the "(" this.consumeChar(); skipWhiteSpace(); // use the next character to determine the type of filter switch(getCurrentChar()) { case AND_TOKEN: if (debug) {System.out.println("SearchFilter: creating AND");} filter = new CompoundFilter(true); filter.parse(); break; case OR_TOKEN: if (debug) {System.out.println("SearchFilter: creating OR");} filter = new CompoundFilter(false); filter.parse(); break; case NOT_TOKEN: if (debug) {System.out.println("SearchFilter: creating OR");} filter = new NotFilter(); filter.parse(); break; default: if (debug) {System.out.println("SearchFilter: creating SIMPLE");} filter = new AtomicFilter(); filter.parse(); break; } skipWhiteSpace(); // make sure every filter ends with ")" if(getCurrentChar() != END_FILTER_TOKEN) { throw new InvalidSearchFilterException("expected \"" + END_FILTER_TOKEN + "\" at position " + pos); } // skip past the ")" this.consumeChar(); } catch (InvalidSearchFilterException e) { if (debug) {System.out.println("rethrowing e");} throw e; // just rethrow these // catch all - any uncaught exception while parsing will end up here } catch (Exception e) { if(debug) {System.out.println(e.getMessage());e.printStackTrace();} throw new InvalidSearchFilterException("Unable to parse " + "character " + pos + " in \""+ this.filter + "\""); } return filter; } protected char getCurrentChar() { return filter.charAt(pos); } protected char relCharAt(int i) { return filter.charAt(pos + i); } protected void consumeChar() { pos++; } protected void consumeChars(int i) { pos += i; } protected int relIndexOf(int ch) { return filter.indexOf(ch, pos) - pos; } protected String relSubstring(int beginIndex, int endIndex){ if(debug){System.out.println("relSubString: " + beginIndex + " " + endIndex);} return filter.substring(beginIndex+pos, endIndex+pos); } /** * A class for dealing with compound filters ("and" & "or" filters). */ final class CompoundFilter implements StringFilter { private Vector subFilters; private boolean polarity; CompoundFilter(boolean polarity) { subFilters = new Vector<>(); this.polarity = polarity; } public void parse() throws InvalidSearchFilterException { SearchFilter.this.consumeChar(); // consume the "&" while(SearchFilter.this.getCurrentChar() != END_FILTER_TOKEN) { if (debug) {System.out.println("CompoundFilter: adding");} StringFilter filter = SearchFilter.this.createNextFilter(); subFilters.addElement(filter); skipWhiteSpace(); } } public boolean check(Attributes targetAttrs) throws NamingException { for(int i = 0; i= 0) { return true; } break; case LESS_MATCH: if (debug) {System.out.println("Atomic: LESS match");} if(val.compareTo(this.value) <= 0) { return true; } break; default: if (debug) {System.out.println("AtomicFilter: unkown " + "matchType");} } } return false; } // used for substring comparisons (where proto has "*" wildcards private boolean substringMatch(String proto, String value) { // simple case 1: "*" means attribute presence is being tested if(proto.equals(Character.toString(WILDCARD_TOKEN))) { if(debug) {System.out.println("simple presence assertion");} return true; } // simple case 2: if there are no wildcards, call String.equals() if(proto.indexOf(WILDCARD_TOKEN) == -1) { return proto.equalsIgnoreCase(value); } if(debug) {System.out.println("doing substring comparison");} // do the work: make sure all the substrings are present int currentPos = 0; StringTokenizer subStrs = new StringTokenizer(proto, "*", false); // do we need to begin with the first token? if(proto.charAt(0) != WILDCARD_TOKEN && !value.toLowerCase(Locale.ENGLISH).startsWith( subStrs.nextToken().toLowerCase(Locale.ENGLISH))) { if(debug) { System.out.println("faild initial test"); } return false; } while(subStrs.hasMoreTokens()) { String currentStr = subStrs.nextToken(); if (debug) {System.out.println("looking for \"" + currentStr +"\"");} currentPos = value.toLowerCase(Locale.ENGLISH).indexOf( currentStr.toLowerCase(Locale.ENGLISH), currentPos); if(currentPos == -1) { return false; } currentPos += currentStr.length(); } // do we need to end with the last token? if(proto.charAt(proto.length() - 1) != WILDCARD_TOKEN && currentPos != value.length() ) { if(debug) {System.out.println("faild final test");} return false; } return true; } } /* AtomicFilter */ // ----- static methods for producing string filters given attribute set // ----- or object array /** * Creates an LDAP filter as a conjuction of the attributes supplied. */ public static String format(Attributes attrs) throws NamingException { if (attrs == null || attrs.size() == 0) { return "objectClass=*"; } String answer; answer = "(& "; Attribute attr; for (NamingEnumeration e = attrs.getAll(); e.hasMore(); ) { attr = e.next(); if (attr.size() == 0 || (attr.size() == 1 && attr.get() == null)) { // only checking presence of attribute answer += "(" + attr.getID() + "=" + "*)"; } else { for (NamingEnumeration ve = attr.getAll(); ve.hasMore(); ) { String val = getEncodedStringRep(ve.next()); if (val != null) { answer += "(" + attr.getID() + "=" + val + ")"; } } } } answer += ")"; //System.out.println("filter: " + answer); return answer; } // Writes the hex representation of a byte to a StringBuffer. private static void hexDigit(StringBuffer buf, byte x) { char c; c = (char) ((x >> 4) & 0xf); if (c > 9) c = (char) ((c-10) + 'A'); else c = (char)(c + '0'); buf.append(c); c = (char) (x & 0xf); if (c > 9) c = (char)((c-10) + 'A'); else c = (char)(c + '0'); buf.append(c); } /** * Returns the string representation of an object (such as an attr value). * If obj is a byte array, encode each item as \xx, where xx is hex encoding * of the byte value. * Else, if obj is not a String, use its string representation (toString()). * Special characters in obj (or its string representation) are then * encoded appropriately according to RFC 2254. * * \2a * ( \28 * ) \29 * \ \5c * NUL \00 */ private static String getEncodedStringRep(Object obj) throws NamingException { String str; if (obj == null) return null; if (obj instanceof byte[]) { // binary data must be encoded as \hh where hh is a hex char byte[] bytes = (byte[])obj; StringBuffer b1 = new StringBuffer(bytes.length*3); for (int i = 0; i < bytes.length; i++) { b1.append('\\'); hexDigit(b1, bytes[i]); } return b1.toString(); } if (!(obj instanceof String)) { str = obj.toString(); } else { str = (String)obj; } int len = str.length(); StringBuilder sb = new StringBuilder(len); char ch; for (int i = 0; i < len; i++) { switch (ch=str.charAt(i)) { case '*': sb.append("\\2a"); break; case '(': sb.append("\\28"); break; case ')': sb.append("\\29"); break; case '\\': sb.append("\\5c"); break; case 0: sb.append("\\00"); break; default: sb.append(ch); } } return sb.toString(); } /** * Finds the first occurrence of ch in val starting * from position start. It doesn't count if ch * has been escaped by a backslash (\) */ public static int findUnescaped(char ch, String val, int start) { int len = val.length(); while (start < len) { int where = val.indexOf(ch, start); // if at start of string, or not there at all, or if not escaped if (where == start || where == -1 || val.charAt(where-1) != '\\') return where; // start search after escaped star start = where + 1; } return -1; } /** * Formats the expression expr using arguments from the array * args. * * {i} specifies the i'th element from * the array args is to be substituted for the * string "{i}". * * To escape '{' or '}' (or any other character), use '\'. * * Uses getEncodedStringRep() to do encoding. */ public static String format(String expr, Object[] args) throws NamingException { int param; int where = 0, start = 0; StringBuilder answer = new StringBuilder(expr.length()); while ((where = findUnescaped('{', expr, start)) >= 0) { int pstart = where + 1; // skip '{' int pend = expr.indexOf('}', pstart); if (pend < 0) { throw new InvalidSearchFilterException("unbalanced {: " + expr); } // at this point, pend should be pointing at '}' try { param = Integer.parseInt(expr.substring(pstart, pend)); } catch (NumberFormatException e) { throw new InvalidSearchFilterException( "integer expected inside {}: " + expr); } if (param >= args.length) { throw new InvalidSearchFilterException( "number exceeds argument list: " + param); } answer.append(expr.substring(start, where)).append(getEncodedStringRep(args[param])); start = pend + 1; // skip '}' } if (start < expr.length()) answer.append(expr.substring(start)); return answer.toString(); } /* * returns an Attributes instance containing only attributeIDs given in * "attributeIDs" whose values come from the given DSContext. */ public static Attributes selectAttributes(Attributes originals, String[] attrIDs) throws NamingException { if (attrIDs == null) return originals; Attributes result = new BasicAttributes(); for(int i=0; i