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

   1: /* ConfigFileParser.java -- JAAS Login Configuration default syntax parser
   2:    Copyright (C) 2006, 2010  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 = Configuration.DEBUG ?
  95:                 Logger.getLogger(ConfigFileParser.class.getName()) : null;
  96: 
  97:   private ConfigFileTokenizer cft;
  98:   private final Map map = new HashMap();
  99: 
 100:   // default 0-arguments constructor
 101: 
 102:   /**
 103:    * Returns the parse result as a {@link Map} where the keys are application
 104:    * names, and the entries are {@link List}s of {@link AppConfigurationEntry}
 105:    * entries, one for each login module entry, in the order they were
 106:    * encountered, for that application name in the just parsed configuration
 107:    * file.
 108:    */
 109:   public Map getLoginModulesMap()
 110:   {
 111:     return map;
 112:   }
 113: 
 114:   /**
 115:    * Parses the {@link Reader}'s contents assuming it is in the <i>default
 116:    * syntax</i>.
 117:    *
 118:    * @param r the {@link Reader} whose contents are assumed to be a JAAS Login
 119:    * Configuration Module file written in the <i>default syntax</i>.
 120:    * @throws IOException if an exception occurs while parsing the input.
 121:    */
 122:   public void parse(Reader r) throws IOException
 123:   {
 124:     initParser(r);
 125: 
 126:     while (parseAppOrOtherEntry())
 127:       {
 128:         /* do nothing */
 129:       }
 130:   }
 131: 
 132:   private void initParser(Reader r) throws IOException
 133:   {
 134:     map.clear();
 135: 
 136:     cft = new ConfigFileTokenizer(r);
 137:   }
 138: 
 139:   /**
 140:    * @return <code>true</code> if an APP_OR_OTHER_ENTRY was correctly parsed.
 141:    * Returns <code>false</code> otherwise.
 142:    * @throws IOException if an exception occurs while parsing the input.
 143:    */
 144:   private boolean parseAppOrOtherEntry() throws IOException
 145:   {
 146:     int c = cft.nextToken();
 147:     if (c == ConfigFileTokenizer.TT_EOF)
 148:       return false;
 149: 
 150:     if (c != ConfigFileTokenizer.TT_WORD)
 151:       {
 152:         cft.pushBack();
 153:         return false;
 154:       }
 155: 
 156:     String appName = cft.sval;
 157:     if (Configuration.DEBUG)
 158:       log.fine("APP_NAME_OR_OTHER = " + appName);
 159:     if (cft.nextToken() != '{')
 160:       abort("Missing '{' after APP_NAME_OR_OTHER");
 161: 
 162:     List lmis = new ArrayList();
 163:     while (parseACE(lmis))
 164:       {
 165:         /* do nothing */
 166:       }
 167: 
 168:     c = cft.nextToken();
 169:     if (c != '}')
 170:       abort("Was expecting '}' but found " + (char) c);
 171: 
 172:     c = cft.nextToken();
 173:     if (c != ';')
 174:       abort("Was expecting ';' but found " + (char) c);
 175: 
 176:     List listOfACEs = (List) map.get(appName);
 177:     if (listOfACEs == null)
 178:       {
 179:         listOfACEs = new ArrayList();
 180:         map.put(appName, listOfACEs);
 181:       }
 182:     listOfACEs.addAll(lmis);
 183:     return !appName.equalsIgnoreCase("other");
 184:   }
 185: 
 186:   /**
 187:    * @return <code>true</code> if a LOGIN_MODULE_ENTRY was correctly parsed.
 188:    * Returns <code>false</code> otherwise.
 189:    * @throws IOException if an exception occurs while parsing the input.
 190:    */
 191:   private boolean parseACE(List listOfACEs) throws IOException
 192:   {
 193:     int c = cft.nextToken();
 194:     if (c != ConfigFileTokenizer.TT_WORD)
 195:       {
 196:         cft.pushBack();
 197:         return false;
 198:       }
 199: 
 200:     String clazz = validateClassName(cft.sval);
 201:     if (Configuration.DEBUG)
 202:       log.fine("MODULE_CLASS = " + clazz);
 203: 
 204:     if (cft.nextToken() != ConfigFileTokenizer.TT_WORD)
 205:       abort("Was expecting FLAG but found none");
 206: 
 207:     String flag = cft.sval;
 208:     if (Configuration.DEBUG)
 209:       log.fine("DEBUG: FLAG = " + flag);
 210:     AppConfigurationEntry.LoginModuleControlFlag f = null;
 211:     if (flag.equalsIgnoreCase("required"))
 212:       f = AppConfigurationEntry.LoginModuleControlFlag.REQUIRED;
 213:     else if (flag.equalsIgnoreCase("requisite"))
 214:       f = AppConfigurationEntry.LoginModuleControlFlag.REQUISITE;
 215:     else if (flag.equalsIgnoreCase("sufficient"))
 216:       f = AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT;
 217:     else if (flag.equalsIgnoreCase("optional"))
 218:       f = AppConfigurationEntry.LoginModuleControlFlag.OPTIONAL;
 219:     else
 220:       abort("Unknown Flag: " + flag);
 221: 
 222:     Map options = new HashMap();
 223:     String paramName, paramValue;
 224:     c = cft.nextToken();
 225:     while (c != ';')
 226:       {
 227:         if (c != ConfigFileTokenizer.TT_WORD)
 228:           abort("Was expecting PARAM_NAME but got '" + ((char) c) + "'");
 229: 
 230:         paramName = cft.sval;
 231:         if (Configuration.DEBUG)
 232:           log.fine("PARAM_NAME = " + paramName);
 233:         if (cft.nextToken() != '=')
 234:           abort("Missing '=' after PARAM_NAME");
 235: 
 236:         c = cft.nextToken();
 237:         if (c != '"' && c != '\'')
 238:           {
 239:           if (Configuration.DEBUG)
 240:             log.fine("Was expecting a quoted string but got no quote character."
 241:                      + " Assume unquoted string");
 242:           }
 243:         paramValue = expandParamValue(cft.sval);
 244:         if (Configuration.DEBUG)
 245:           log.fine("PARAM_VALUE = " + paramValue);
 246:         options.put(paramName, paramValue);
 247: 
 248:         c = cft.nextToken();
 249:       }
 250:     AppConfigurationEntry ace = new AppConfigurationEntry(clazz, f, options);
 251:     if (Configuration.DEBUG)
 252:       log.fine("LOGIN_MODULE_ENTRY = " + ace);
 253:     listOfACEs.add(ace);
 254:     return true;
 255:   }
 256: 
 257:   private void abort(String m) throws IOException
 258:   {
 259:     if (Configuration.DEBUG)
 260:       {
 261:         log.fine(m);
 262:         log.fine("Map (so far) = " + String.valueOf(map));
 263:       }
 264:     throw new IOException(m);
 265:   }
 266: 
 267:   private String validateClassName(String cn) throws IOException
 268:   {
 269:     if (cn.startsWith(".") || cn.endsWith("."))
 270:       abort("MODULE_CLASS MUST NOT start or end with a '.'");
 271: 
 272:     String[] tokens = cn.split("\\.");
 273:     for (int i = 0; i < tokens.length; i++)
 274:       {
 275:         String t = tokens[i];
 276:         if (! Character.isJavaIdentifierStart(t.charAt(0)))
 277:           abort("Class name [" + cn
 278:                 + "] contains an invalid sub-package identifier: " + t);
 279: 
 280:         // we dont check the rest of the characters for isJavaIdentifierPart()
 281:         // because that's what the tokenizer does.
 282:       }
 283: 
 284:     return cn;
 285:   }
 286: 
 287:   /**
 288:    * The documentation of the {@link javax.security.auth.login.Configuration}
 289:    * states that: <i>"...If a String in the form, ${system.property}, occurs in
 290:    * the value, it will be expanded to the value of the system property."</i>.
 291:    * This method ensures this is the case. If such a string can not be expanded
 292:    * then it is left AS IS, assuming the LoginModule knows what to do with it.
 293:    *
 294:    * <p><b>IMPORTANT</b>: This implementation DOES NOT handle embedded ${}
 295:    * constructs.
 296:    *
 297:    * @param s the raw parameter value, incl. eventually strings of the form
 298:    * <code>${system.property}</code>.
 299:    * @return the input string with every occurence of
 300:    * <code>${system.property}</code> replaced with the value of the
 301:    * corresponding System property at the time of this method invocation. If
 302:    * the string is not a known System property name, then the complete sequence
 303:    * (incl. the ${} characters are passed AS IS.
 304:    */
 305:   private String expandParamValue(String s)
 306:   {
 307:     String result = s;
 308:     try
 309:       {
 310:         int searchNdx = 0;
 311:         while (searchNdx < result.length())
 312:           {
 313:             int i = s.indexOf("${", searchNdx);
 314:             if (i == -1)
 315:               break;
 316: 
 317:             int j = s.indexOf("}", i + 2);
 318:             if (j == -1)
 319:               {
 320:                 if (Configuration.DEBUG)
 321:                   log.fine("Found a ${ prefix with no } suffix. Ignore");
 322:                 break;
 323:               }
 324: 
 325:             String sysPropName = s.substring(i + 2, j);
 326:             if (Configuration.DEBUG)
 327:               log.fine("Found a reference to System property " + sysPropName);
 328:             String sysPropValue = System.getProperty(sysPropName);
 329:             if (Configuration.DEBUG)
 330:               log.fine("Resolved " + sysPropName + " to '" + sysPropValue + "'");
 331:             if (sysPropValue != null)
 332:               {
 333:                 result = s.substring(0, i) + sysPropValue + s.substring(j + 1);
 334:                 searchNdx = i + sysPropValue.length();
 335:               }
 336:             else
 337:               searchNdx = j + 1;
 338:           }
 339:       }
 340:     catch (Exception x)
 341:       {
 342:         if (Configuration.DEBUG)
 343:           log.fine("Exception (ignored) while expanding " + s + ": " + x);
 344:       }
 345: 
 346:     return result;
 347:   }
 348: }