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