Source for gnu.javax.security.auth.login.ConfigFileParser

   1: /* ConfigFileParser.java -- JAAS Login Configuration default syntax parser
   2:    Copyright (C) 2006 Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10: 
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: 
  39: package gnu.javax.security.auth.login;
  40: 
  41: import gnu.java.security.Configuration;
  42: 
  43: import java.io.IOException;
  44: import java.io.Reader;
  45: import java.util.ArrayList;
  46: import java.util.HashMap;
  47: import java.util.List;
  48: import java.util.Map;
  49: import java.util.logging.Logger;
  50: 
  51: import javax.security.auth.login.AppConfigurationEntry;
  52: 
  53: /**
  54:  * A parser that knows how to interpret JAAS Login Module Configuration files
  55:  * written in the <i>default syntax</i> which is interpreted as adhering to
  56:  * the following grammar:
  57:  *
  58:  * <pre>
  59:  *   CONFIG              ::= APP_OR_OTHER_ENTRY+
  60:  *   APP_OR_OTHER_ENTRY  ::= APP_NAME_OR_OTHER JAAS_CONFIG_BLOCK
  61:  *   APP_NAME_OR_OTHER   ::= APP_NAME
  62:  *                         | 'other'
  63:  *   JAAS_CONFIG_BLOCK   ::= '{' (LOGIN_MODULE_ENTRY ';')+ '}' ';'
  64:  *   LOGIN_MODULE_ENTRY  ::= MODULE_CLASS FLAG MODULE_OPTION* ';'
  65:  *   FLAG                ::= 'required'
  66:  *                         | 'requisite'
  67:  *                         | 'sufficient'
  68:  *                         | 'optional'
  69:  *   MODULE_OPTION       ::= PARAM_NAME '=' PARAM_VALUE
  70:  *
  71:  *   APP_NAME     ::= JAVA_IDENTIFIER
  72:  *   MODULE_CLASS ::= JAVA_IDENTIFIER ('.' JAVA_IDENTIFIER)*
  73:  *   PARAM_NAME   ::= STRING
  74:  *   PARAM_VALUE  ::= '"' STRING '"' | ''' STRING ''' | STRING
  75:  * </pre>
  76:  *
  77:  * <p>This parser handles UTF-8 entities when used as APP_NAME and PARAM_VALUE.
  78:  * It also checks for the use of Java identifiers used in MODULE_CLASS, thus
  79:  * minimizing the risks of having {@link java.lang.ClassCastException}s thrown
  80:  * at runtime due to syntactically invalid names.</p>
  81:  *
  82:  * <p>In the above context, a JAVA_IDENTIFIER is a sequence of tokens,
  83:  * separated by the character '.'. Each of these tokens obeys the following:</p>
  84:  * 
  85:  * <ol>
  86:  *   <li>its first character yields <code>true</code> when used as an input to
  87:  *   the {@link java.lang.Character#isJavaIdentifierStart(char)}, and</li>
  88:  *   <li>all remaining characters, yield <code>true</code> when used as an
  89:  *   input to {@link java.lang.Character#isJavaIdentifierPart(char)}.</li>
  90:  * </ol>
  91:  */
  92: public final class ConfigFileParser
  93: {
  94:   private static final Logger log = Logger.getLogger(ConfigFileParser.class.getName());
  95:   private ConfigFileTokenizer cft;
  96:   private Map map = new HashMap();
  97: 
  98:   // default 0-arguments constructor
  99: 
 100:   /**
 101:    * Returns the parse result as a {@link Map} where the keys are application
 102:    * names, and the entries are {@link List}s of {@link AppConfigurationEntry}
 103:    * entries, one for each login module entry, in the order they were
 104:    * encountered, for that application name in the just parsed configuration
 105:    * file.
 106:    */
 107:   public Map getLoginModulesMap()
 108:   {
 109:     return map;
 110:   }
 111: 
 112:   /**
 113:    * Parses the {@link Reader}'s contents assuming it is in the <i>default
 114:    * syntax</i>.
 115:    *
 116:    * @param r the {@link Reader} whose contents are assumed to be a JAAS Login
 117:    * Configuration Module file written in the <i>default syntax</i>.
 118:    * @throws IOException if an exception occurs while parsing the input.
 119:    */
 120:   public void parse(Reader r) throws IOException
 121:   {
 122:     initParser(r);
 123: 
 124:     while (parseAppOrOtherEntry())
 125:       ; // do nothing
 126:   }
 127: 
 128:   private void initParser(Reader r) throws IOException
 129:   {
 130:     map.clear();
 131: 
 132:     cft = new ConfigFileTokenizer(r);
 133:   }
 134: 
 135:   /**
 136:    * @return <code>true</code> if an APP_OR_OTHER_ENTRY was correctly parsed.
 137:    * Returns <code>false</code> otherwise.
 138:    * @throws IOException if an exception occurs while parsing the input.
 139:    */
 140:   private boolean parseAppOrOtherEntry() throws IOException
 141:   {
 142:     int c = cft.nextToken();
 143:     if (c == ConfigFileTokenizer.TT_EOF)
 144:       return false;
 145: 
 146:     if (c != ConfigFileTokenizer.TT_WORD)
 147:       {
 148:         cft.pushBack();
 149:         return false;
 150:       }
 151: 
 152:     String appName = cft.sval;
 153:     if (Configuration.DEBUG)
 154:       log.fine("APP_NAME_OR_OTHER = " + appName);
 155:     if (cft.nextToken() != '{')
 156:       abort("Missing '{' after APP_NAME_OR_OTHER");
 157: 
 158:     List lmis = new ArrayList();
 159:     while (parseACE(lmis))
 160:       ; // do nothing
 161: 
 162:     c = cft.nextToken();
 163:     if (c != '}')
 164:       abort("Was expecting '}' but found " + (char) c);
 165: 
 166:     c = cft.nextToken();
 167:     if (c != ';')
 168:       abort("Was expecting ';' but found " + (char) c);
 169: 
 170:     List listOfACEs = (List) map.get(appName);
 171:     if (listOfACEs == null)
 172:       {
 173:         listOfACEs = new ArrayList();
 174:         map.put(appName, listOfACEs);
 175:       }
 176:     listOfACEs.addAll(lmis);
 177:     return !appName.equalsIgnoreCase("other");
 178:   }
 179: 
 180:   /**
 181:    * @return <code>true</code> if a LOGIN_MODULE_ENTRY was correctly parsed.
 182:    * Returns <code>false</code> otherwise. 
 183:    * @throws IOException if an exception occurs while parsing the input.
 184:    */
 185:   private boolean parseACE(List listOfACEs) throws IOException
 186:   {
 187:     int c = cft.nextToken();
 188:     if (c != ConfigFileTokenizer.TT_WORD)
 189:       {
 190:         cft.pushBack();
 191:         return false;
 192:       }
 193: 
 194:     String clazz = validateClassName(cft.sval);
 195:     if (Configuration.DEBUG)
 196:       log.fine("MODULE_CLASS = " + clazz);
 197: 
 198:     if (cft.nextToken() != ConfigFileTokenizer.TT_WORD)
 199:       abort("Was expecting FLAG but found none");
 200: 
 201:     String flag = cft.sval;
 202:     if (Configuration.DEBUG)
 203:       log.fine("DEBUG: FLAG = " + flag);
 204:     AppConfigurationEntry.LoginModuleControlFlag f = null;
 205:     if (flag.equalsIgnoreCase("required"))
 206:       f = AppConfigurationEntry.LoginModuleControlFlag.REQUIRED;
 207:     else if (flag.equalsIgnoreCase("requisite"))
 208:       f = AppConfigurationEntry.LoginModuleControlFlag.REQUISITE;
 209:     else if (flag.equalsIgnoreCase("sufficient"))
 210:       f = AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT;
 211:     else if (flag.equalsIgnoreCase("optional"))
 212:       f = AppConfigurationEntry.LoginModuleControlFlag.OPTIONAL;
 213:     else
 214:       abort("Unknown Flag: " + flag);
 215: 
 216:     Map options = new HashMap();
 217:     String paramName, paramValue;
 218:     c = cft.nextToken();
 219:     while (c != ';')
 220:       {
 221:         if (c != ConfigFileTokenizer.TT_WORD)
 222:           abort("Was expecting PARAM_NAME but got '" + ((char) c) + "'");
 223: 
 224:         paramName = cft.sval;
 225:         if (Configuration.DEBUG)
 226:           log.fine("PARAM_NAME = " + paramName);
 227:         if (cft.nextToken() != '=')
 228:           abort("Missing '=' after PARAM_NAME");
 229: 
 230:         c = cft.nextToken();
 231:         if (c != '"' && c != '\'')
 232:           {
 233:           if (Configuration.DEBUG)
 234:             log.fine("Was expecting a quoted string but got no quote character."
 235:                      + " Assume unquoted string");
 236:           }
 237:         paramValue = expandParamValue(cft.sval);
 238:         if (Configuration.DEBUG)
 239:           log.fine("PARAM_VALUE = " + paramValue);
 240:         options.put(paramName, paramValue);
 241: 
 242:         c = cft.nextToken();
 243:       }
 244:     AppConfigurationEntry ace = new AppConfigurationEntry(clazz, f, options);
 245:     if (Configuration.DEBUG)
 246:       log.fine("LOGIN_MODULE_ENTRY = " + ace);
 247:     listOfACEs.add(ace);
 248:     return true;
 249:   }
 250: 
 251:   private void abort(String m) throws IOException
 252:   {
 253:     if (Configuration.DEBUG)
 254:       {
 255:         log.fine(m);
 256:         log.fine("Map (so far) = " + String.valueOf(map));
 257:       }
 258:     throw new IOException(m);
 259:   }
 260: 
 261:   private String validateClassName(String cn) throws IOException
 262:   {
 263:     if (cn.startsWith(".") || cn.endsWith("."))
 264:       abort("MODULE_CLASS MUST NOT start or end with a '.'");
 265: 
 266:     String[] tokens = cn.split("\\.");
 267:     for (int i = 0; i < tokens.length; i++)
 268:       {
 269:         String t = tokens[i];
 270:         if (! Character.isJavaIdentifierStart(t.charAt(0)))
 271:           abort("Class name [" + cn
 272:                 + "] contains an invalid sub-package identifier: " + t);
 273: 
 274:         // we dont check the rest of the characters for isJavaIdentifierPart()
 275:         // because that's what the tokenizer does.
 276:       }
 277:     
 278:     return cn;
 279:   }
 280: 
 281:   /**
 282:    * The documentation of the {@link javax.security.auth.login.Configuration}
 283:    * states that: <i>"...If a String in the form, ${system.property}, occurs in
 284:    * the value, it will be expanded to the value of the system property."</i>.
 285:    * This method ensures this is the case. If such a string can not be expanded
 286:    * then it is left AS IS, assuming the LoginModule knows what to do with it.
 287:    *
 288:    * <p><b>IMPORTANT</b>: This implementation DOES NOT handle embedded ${}
 289:    * constructs.
 290:    *
 291:    * @param s the raw parameter value, incl. eventually strings of the form
 292:    * <code>${system.property}</code>.
 293:    * @return the input string with every occurence of
 294:    * <code>${system.property}</code> replaced with the value of the
 295:    * corresponding System property at the time of this method invocation. If
 296:    * the string is not a known System property name, then the complete sequence
 297:    * (incl. the ${} characters are passed AS IS.
 298:    */
 299:   private String expandParamValue(String s)
 300:   {
 301:     String result = s;
 302:     try
 303:       {
 304:         int searchNdx = 0;
 305:         while (searchNdx < result.length())
 306:           {
 307:             int i = s.indexOf("${", searchNdx);
 308:             if (i == -1)
 309:               break;
 310: 
 311:             int j = s.indexOf("}", i + 2);
 312:             if (j == -1)
 313:               {
 314:                 if (Configuration.DEBUG)
 315:                   log.fine("Found a ${ prefix with no } suffix. Ignore");
 316:                 break;
 317:               }
 318: 
 319:             String sysPropName = s.substring(i + 2, j);
 320:             if (Configuration.DEBUG)
 321:               log.fine("Found a reference to System property " + sysPropName);
 322:             String sysPropValue = System.getProperty(sysPropName);
 323:             if (Configuration.DEBUG)
 324:               log.fine("Resolved " + sysPropName + " to '" + sysPropValue + "'");
 325:             if (sysPropValue != null)
 326:               {
 327:                 result = s.substring(0, i) + sysPropValue + s.substring(j + 1);
 328:                 searchNdx = i + sysPropValue.length();
 329:               }
 330:             else
 331:               searchNdx = j + 1;
 332:           }
 333:       }
 334:     catch (Exception x)
 335:       {
 336:         if (Configuration.DEBUG)
 337:           log.fine("Exception (ignored) while expanding " + s + ": " + x);
 338:       }
 339: 
 340:     return result;
 341:   }
 342: }