| /* ConfigFileParser.java -- JAAS Login Configuration default syntax parser |
| Copyright (C) 2006 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath 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 for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package gnu.javax.security.auth.login; |
| |
| import gnu.java.security.Configuration; |
| |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.logging.Logger; |
| |
| import javax.security.auth.login.AppConfigurationEntry; |
| |
| /** |
| * A parser that knows how to interpret JAAS Login Module Configuration files |
| * written in the <i>default syntax</i> which is interpreted as adhering to |
| * the following grammar: |
| * |
| * <pre> |
| * CONFIG ::= APP_OR_OTHER_ENTRY+ |
| * APP_OR_OTHER_ENTRY ::= APP_NAME_OR_OTHER JAAS_CONFIG_BLOCK |
| * APP_NAME_OR_OTHER ::= APP_NAME |
| * | 'other' |
| * JAAS_CONFIG_BLOCK ::= '{' (LOGIN_MODULE_ENTRY ';')+ '}' ';' |
| * LOGIN_MODULE_ENTRY ::= MODULE_CLASS FLAG MODULE_OPTION* ';' |
| * FLAG ::= 'required' |
| * | 'requisite' |
| * | 'sufficient' |
| * | 'optional' |
| * MODULE_OPTION ::= PARAM_NAME '=' PARAM_VALUE |
| * |
| * APP_NAME ::= JAVA_IDENTIFIER |
| * MODULE_CLASS ::= JAVA_IDENTIFIER ('.' JAVA_IDENTIFIER)* |
| * PARAM_NAME ::= STRING |
| * PARAM_VALUE ::= '"' STRING '"' | ''' STRING ''' | STRING |
| * </pre> |
| * |
| * <p>This parser handles UTF-8 entities when used as APP_NAME and PARAM_VALUE. |
| * It also checks for the use of Java identifiers used in MODULE_CLASS, thus |
| * minimizing the risks of having {@link java.lang.ClassCastException}s thrown |
| * at runtime due to syntactically invalid names.</p> |
| * |
| * <p>In the above context, a JAVA_IDENTIFIER is a sequence of tokens, |
| * separated by the character '.'. Each of these tokens obeys the following:</p> |
| * |
| * <ol> |
| * <li>its first character yields <code>true</code> when used as an input to |
| * the {@link java.lang.Character#isJavaIdentifierStart(char)}, and</li> |
| * <li>all remaining characters, yield <code>true</code> when used as an |
| * input to {@link java.lang.Character#isJavaIdentifierPart(char)}.</li> |
| * </ol> |
| */ |
| public final class ConfigFileParser |
| { |
| private static final Logger log = Logger.getLogger(ConfigFileParser.class.getName()); |
| private ConfigFileTokenizer cft; |
| private Map map = new HashMap(); |
| |
| // default 0-arguments constructor |
| |
| /** |
| * Returns the parse result as a {@link Map} where the keys are application |
| * names, and the entries are {@link List}s of {@link AppConfigurationEntry} |
| * entries, one for each login module entry, in the order they were |
| * encountered, for that application name in the just parsed configuration |
| * file. |
| */ |
| public Map getLoginModulesMap() |
| { |
| return map; |
| } |
| |
| /** |
| * Parses the {@link Reader}'s contents assuming it is in the <i>default |
| * syntax</i>. |
| * |
| * @param r the {@link Reader} whose contents are assumed to be a JAAS Login |
| * Configuration Module file written in the <i>default syntax</i>. |
| * @throws IOException if an exception occurs while parsing the input. |
| */ |
| public void parse(Reader r) throws IOException |
| { |
| initParser(r); |
| |
| while (parseAppOrOtherEntry()) |
| ; // do nothing |
| } |
| |
| private void initParser(Reader r) throws IOException |
| { |
| map.clear(); |
| |
| cft = new ConfigFileTokenizer(r); |
| } |
| |
| /** |
| * @return <code>true</code> if an APP_OR_OTHER_ENTRY was correctly parsed. |
| * Returns <code>false</code> otherwise. |
| * @throws IOException if an exception occurs while parsing the input. |
| */ |
| private boolean parseAppOrOtherEntry() throws IOException |
| { |
| int c = cft.nextToken(); |
| if (c == ConfigFileTokenizer.TT_EOF) |
| return false; |
| |
| if (c != ConfigFileTokenizer.TT_WORD) |
| { |
| cft.pushBack(); |
| return false; |
| } |
| |
| String appName = cft.sval; |
| if (Configuration.DEBUG) |
| log.fine("APP_NAME_OR_OTHER = " + appName); |
| if (cft.nextToken() != '{') |
| abort("Missing '{' after APP_NAME_OR_OTHER"); |
| |
| List lmis = new ArrayList(); |
| while (parseACE(lmis)) |
| ; // do nothing |
| |
| c = cft.nextToken(); |
| if (c != '}') |
| abort("Was expecting '}' but found " + (char) c); |
| |
| c = cft.nextToken(); |
| if (c != ';') |
| abort("Was expecting ';' but found " + (char) c); |
| |
| List listOfACEs = (List) map.get(appName); |
| if (listOfACEs == null) |
| { |
| listOfACEs = new ArrayList(); |
| map.put(appName, listOfACEs); |
| } |
| listOfACEs.addAll(lmis); |
| return !appName.equalsIgnoreCase("other"); |
| } |
| |
| /** |
| * @return <code>true</code> if a LOGIN_MODULE_ENTRY was correctly parsed. |
| * Returns <code>false</code> otherwise. |
| * @throws IOException if an exception occurs while parsing the input. |
| */ |
| private boolean parseACE(List listOfACEs) throws IOException |
| { |
| int c = cft.nextToken(); |
| if (c != ConfigFileTokenizer.TT_WORD) |
| { |
| cft.pushBack(); |
| return false; |
| } |
| |
| String clazz = validateClassName(cft.sval); |
| if (Configuration.DEBUG) |
| log.fine("MODULE_CLASS = " + clazz); |
| |
| if (cft.nextToken() != ConfigFileTokenizer.TT_WORD) |
| abort("Was expecting FLAG but found none"); |
| |
| String flag = cft.sval; |
| if (Configuration.DEBUG) |
| log.fine("DEBUG: FLAG = " + flag); |
| AppConfigurationEntry.LoginModuleControlFlag f = null; |
| if (flag.equalsIgnoreCase("required")) |
| f = AppConfigurationEntry.LoginModuleControlFlag.REQUIRED; |
| else if (flag.equalsIgnoreCase("requisite")) |
| f = AppConfigurationEntry.LoginModuleControlFlag.REQUISITE; |
| else if (flag.equalsIgnoreCase("sufficient")) |
| f = AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT; |
| else if (flag.equalsIgnoreCase("optional")) |
| f = AppConfigurationEntry.LoginModuleControlFlag.OPTIONAL; |
| else |
| abort("Unknown Flag: " + flag); |
| |
| Map options = new HashMap(); |
| String paramName, paramValue; |
| c = cft.nextToken(); |
| while (c != ';') |
| { |
| if (c != ConfigFileTokenizer.TT_WORD) |
| abort("Was expecting PARAM_NAME but got '" + ((char) c) + "'"); |
| |
| paramName = cft.sval; |
| if (Configuration.DEBUG) |
| log.fine("PARAM_NAME = " + paramName); |
| if (cft.nextToken() != '=') |
| abort("Missing '=' after PARAM_NAME"); |
| |
| c = cft.nextToken(); |
| if (c != '"' && c != '\'') |
| { |
| if (Configuration.DEBUG) |
| log.fine("Was expecting a quoted string but got no quote character." |
| + " Assume unquoted string"); |
| } |
| paramValue = expandParamValue(cft.sval); |
| if (Configuration.DEBUG) |
| log.fine("PARAM_VALUE = " + paramValue); |
| options.put(paramName, paramValue); |
| |
| c = cft.nextToken(); |
| } |
| AppConfigurationEntry ace = new AppConfigurationEntry(clazz, f, options); |
| if (Configuration.DEBUG) |
| log.fine("LOGIN_MODULE_ENTRY = " + ace); |
| listOfACEs.add(ace); |
| return true; |
| } |
| |
| private void abort(String m) throws IOException |
| { |
| if (Configuration.DEBUG) |
| { |
| log.fine(m); |
| log.fine("Map (so far) = " + String.valueOf(map)); |
| } |
| throw new IOException(m); |
| } |
| |
| private String validateClassName(String cn) throws IOException |
| { |
| if (cn.startsWith(".") || cn.endsWith(".")) |
| abort("MODULE_CLASS MUST NOT start or end with a '.'"); |
| |
| String[] tokens = cn.split("\\."); |
| for (int i = 0; i < tokens.length; i++) |
| { |
| String t = tokens[i]; |
| if (! Character.isJavaIdentifierStart(t.charAt(0))) |
| abort("Class name [" + cn |
| + "] contains an invalid sub-package identifier: " + t); |
| |
| // we dont check the rest of the characters for isJavaIdentifierPart() |
| // because that's what the tokenizer does. |
| } |
| |
| return cn; |
| } |
| |
| /** |
| * The documentation of the {@link javax.security.auth.login.Configuration} |
| * states that: <i>"...If a String in the form, ${system.property}, occurs in |
| * the value, it will be expanded to the value of the system property."</i>. |
| * This method ensures this is the case. If such a string can not be expanded |
| * then it is left AS IS, assuming the LoginModule knows what to do with it. |
| * |
| * <p><b>IMPORTANT</b>: This implementation DOES NOT handle embedded ${} |
| * constructs. |
| * |
| * @param s the raw parameter value, incl. eventually strings of the form |
| * <code>${system.property}</code>. |
| * @return the input string with every occurence of |
| * <code>${system.property}</code> replaced with the value of the |
| * corresponding System property at the time of this method invocation. If |
| * the string is not a known System property name, then the complete sequence |
| * (incl. the ${} characters are passed AS IS. |
| */ |
| private String expandParamValue(String s) |
| { |
| String result = s; |
| try |
| { |
| int searchNdx = 0; |
| while (searchNdx < result.length()) |
| { |
| int i = s.indexOf("${", searchNdx); |
| if (i == -1) |
| break; |
| |
| int j = s.indexOf("}", i + 2); |
| if (j == -1) |
| { |
| if (Configuration.DEBUG) |
| log.fine("Found a ${ prefix with no } suffix. Ignore"); |
| break; |
| } |
| |
| String sysPropName = s.substring(i + 2, j); |
| if (Configuration.DEBUG) |
| log.fine("Found a reference to System property " + sysPropName); |
| String sysPropValue = System.getProperty(sysPropName); |
| if (Configuration.DEBUG) |
| log.fine("Resolved " + sysPropName + " to '" + sysPropValue + "'"); |
| if (sysPropValue != null) |
| { |
| result = s.substring(0, i) + sysPropValue + s.substring(j + 1); |
| searchNdx = i + sysPropValue.length(); |
| } |
| else |
| searchNdx = j + 1; |
| } |
| } |
| catch (Exception x) |
| { |
| if (Configuration.DEBUG) |
| log.fine("Exception (ignored) while expanding " + s + ": " + x); |
| } |
| |
| return result; |
| } |
| } |