1 /*
   2  * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package sun.security.provider;
  27 
  28 import java.io.*;
  29 import java.net.MalformedURLException;
  30 import java.net.URI;
  31 import java.net.URL;
  32 import java.security.AccessController;
  33 import java.security.PrivilegedAction;
  34 import java.security.PrivilegedActionException;
  35 import java.security.PrivilegedExceptionAction;
  36 import java.security.Security;
  37 import java.security.URIParameter;
  38 import java.text.MessageFormat;
  39 import java.util.*;
  40 import javax.security.auth.AuthPermission;
  41 import javax.security.auth.login.AppConfigurationEntry;
  42 import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
  43 import javax.security.auth.login.Configuration;
  44 import javax.security.auth.login.ConfigurationSpi;
  45 import sun.security.util.Debug;
  46 import sun.security.util.PropertyExpander;
  47 import sun.security.util.ResourcesMgr;
  48 
  49 /**
  50  * This class represents a default implementation for
  51  * {@code javax.security.auth.login.Configuration}.
  52  *
  53  * <p> This object stores the runtime login configuration representation,
  54  * and is the amalgamation of multiple static login configurations that
  55  * resides in files. The algorithm for locating the login configuration
  56  * file(s) and reading their information into this {@code Configuration}
  57  * object is:
  58  *
  59  * <ol>
  60  * <li>
  61  *   Loop through the security properties,
  62  *   <i>login.config.url.1</i>, <i>login.config.url.2</i>, ...,
  63  *   <i>login.config.url.X</i>.
  64  *   Each property value specifies a {@code URL} pointing to a
  65  *   login configuration file to be loaded.  Read in and load
  66  *   each configuration.
  67  *
  68  * <li>
  69  *   The {@code java.lang.System} property
  70  *   <i>java.security.auth.login.config</i>
  71  *   may also be set to a {@code URL} pointing to another
  72  *   login configuration file
  73  *   (which is the case when a user uses the -D switch at runtime).
  74  *   If this property is defined, and its use is allowed by the
  75  *   security property file (the Security property,
  76  *   <i>policy.allowSystemProperty</i> is set to <i>true</i>),
  77  *   also load that login configuration.
  78  *
  79  * <li>
  80  *   If the <i>java.security.auth.login.config</i> property is defined using
  81  *   "==" (rather than "="), then ignore all other specified
  82  *   login configurations and only load this configuration.
  83  *
  84  * <li>
  85  *   If no system or security properties were set, try to read from the file,
  86  *   ${user.home}/.java.login.config, where ${user.home} is the value
  87  *   represented by the "user.home" System property.
  88  * </ol>
  89  *
  90  * <p> The configuration syntax supported by this implementation
  91  * is exactly that syntax specified in the
  92  * {@code javax.security.auth.login.Configuration} class.
  93  *
  94  * @see javax.security.auth.login.LoginContext
  95  * @see java.security.Security security properties
  96  */
  97 public final class ConfigFile extends Configuration {
  98 
  99     private final Spi spi;
 100 
 101     public ConfigFile() {
 102         spi = new Spi();
 103     }
 104 
 105     @Override
 106     public AppConfigurationEntry[] getAppConfigurationEntry(String appName) {
 107         return spi.engineGetAppConfigurationEntry(appName);
 108     }
 109 
 110     @Override
 111     public synchronized void refresh() {
 112         spi.engineRefresh();
 113     }
 114 
 115     public static final class Spi extends ConfigurationSpi {
 116 
 117         private URL url;
 118         private boolean expandProp = true;
 119         private Map<String, List<AppConfigurationEntry>> configuration;
 120         private int linenum;
 121         private StreamTokenizer st;
 122         private int lookahead;
 123 
 124         private static Debug debugConfig = Debug.getInstance("configfile");
 125         private static Debug debugParser = Debug.getInstance("configparser");
 126 
 127         /**
 128          * Creates a new {@code ConfigurationSpi} object.
 129          *
 130          * @throws SecurityException if the {@code ConfigurationSpi} can not be
 131          *                           initialized
 132          */
 133         public Spi() {
 134             try {
 135                 init();
 136             } catch (IOException ioe) {
 137                 throw new SecurityException(ioe);
 138             }
 139         }
 140 
 141         /**
 142          * Creates a new {@code ConfigurationSpi} object from the specified
 143          * {@code URI}.
 144          *
 145          * @param uri the {@code URI}
 146          * @throws SecurityException if the {@code ConfigurationSpi} can not be
 147          *                           initialized
 148          * @throws NullPointerException if {@code uri} is null
 149          */
 150         public Spi(URI uri) {
 151             // only load config from the specified URI
 152             try {
 153                 url = uri.toURL();
 154                 init();
 155             } catch (IOException ioe) {
 156                 throw new SecurityException(ioe);
 157             }
 158         }
 159 
 160         public Spi(final Configuration.Parameters params) throws IOException {
 161 
 162             // call in a doPrivileged
 163             //
 164             // we have already passed the Configuration.getInstance
 165             // security check.  also this class is not freely accessible
 166             // (it is in the "sun" package).
 167 
 168             try {
 169                 AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
 170                     public Void run() throws IOException {
 171                         if (params == null) {
 172                             init();
 173                         } else {
 174                             if (!(params instanceof URIParameter)) {
 175                                 throw new IllegalArgumentException
 176                                         ("Unrecognized parameter: " + params);
 177                             }
 178                             URIParameter uriParam = (URIParameter)params;
 179                             url = uriParam.getURI().toURL();
 180                             init();
 181                         }
 182                         return null;
 183                     }
 184                 });
 185             } catch (PrivilegedActionException pae) {
 186                 throw (IOException)pae.getException();
 187             }
 188 
 189             // if init() throws some other RuntimeException,
 190             // let it percolate up naturally.
 191         }
 192 
 193         /**
 194          * Read and initialize the entire login Configuration from the
 195          * configured URL.
 196          *
 197          * @throws IOException if the Configuration can not be initialized
 198          * @throws SecurityException if the caller does not have permission
 199          *                           to initialize the Configuration
 200          */
 201         private void init() throws IOException {
 202 
 203             boolean initialized = false;
 204 
 205             // For policy.expandProperties, check if either a security or system
 206             // property is set to false (old code erroneously checked the system
 207             // prop so we must check both to preserve compatibility).
 208             String expand = Security.getProperty("policy.expandProperties");
 209             if (expand == null) {
 210                 expand = System.getProperty("policy.expandProperties");
 211             }
 212             if ("false".equals(expand)) {
 213                 expandProp = false;
 214             }
 215 
 216             // new configuration
 217             Map<String, List<AppConfigurationEntry>> newConfig = new HashMap<>();
 218 
 219             if (url != null) {
 220                 /**
 221                  * If the caller specified a URI via Configuration.getInstance,
 222                  * we only read from that URI
 223                  */
 224                 if (debugConfig != null) {
 225                     debugConfig.println("reading " + url);
 226                 }
 227                 init(url, newConfig);
 228                 configuration = newConfig;
 229                 return;
 230             }
 231 
 232             /**
 233              * Caller did not specify URI via Configuration.getInstance.
 234              * Read from URLs listed in the java.security properties file.
 235              */
 236             String allowSys = Security.getProperty("policy.allowSystemProperty");
 237 
 238             if ("true".equalsIgnoreCase(allowSys)) {
 239                 String extra_config = System.getProperty
 240                                       ("java.security.auth.login.config");
 241                 if (extra_config != null) {
 242                     boolean overrideAll = false;
 243                     if (extra_config.startsWith("=")) {
 244                         overrideAll = true;
 245                         extra_config = extra_config.substring(1);
 246                     }
 247                     try {
 248                         extra_config = PropertyExpander.expand(extra_config);
 249                     } catch (PropertyExpander.ExpandException peee) {
 250                         throw ioException("Unable.to.properly.expand.config",
 251                                           extra_config);
 252                     }
 253 
 254                     URL configURL = null;
 255                     try {
 256                         configURL = new URL(extra_config);
 257                     } catch (MalformedURLException mue) {
 258                         File configFile = new File(extra_config);
 259                         if (configFile.exists()) {
 260                             configURL = configFile.toURI().toURL();
 261                         } else {
 262                             throw ioException(
 263                                 "extra.config.No.such.file.or.directory.",
 264                                 extra_config);
 265                         }
 266                     }
 267 
 268                     if (debugConfig != null) {
 269                         debugConfig.println("reading "+configURL);
 270                     }
 271                     init(configURL, newConfig);
 272                     initialized = true;
 273                     if (overrideAll) {
 274                         if (debugConfig != null) {
 275                             debugConfig.println("overriding other policies!");
 276                         }
 277                         configuration = newConfig;
 278                         return;
 279                     }
 280                 }
 281             }
 282 
 283             int n = 1;
 284             String config_url;
 285             while ((config_url = Security.getProperty
 286                                      ("login.config.url."+n)) != null) {
 287                 try {
 288                     config_url = PropertyExpander.expand
 289                         (config_url).replace(File.separatorChar, '/');
 290                     if (debugConfig != null) {
 291                         debugConfig.println("\tReading config: " + config_url);
 292                     }
 293                     init(new URL(config_url), newConfig);
 294                     initialized = true;
 295                 } catch (PropertyExpander.ExpandException peee) {
 296                     throw ioException("Unable.to.properly.expand.config",
 297                                       config_url);
 298                 }
 299                 n++;
 300             }
 301 
 302             if (initialized == false && n == 1 && config_url == null) {
 303 
 304                 // get the config from the user's home directory
 305                 if (debugConfig != null) {
 306                     debugConfig.println("\tReading Policy " +
 307                                 "from ~/.java.login.config");
 308                 }
 309                 config_url = System.getProperty("user.home");
 310                 String userConfigFile = config_url + File.separatorChar +
 311                                         ".java.login.config";
 312 
 313                 // No longer throws an exception when there's no config file
 314                 // at all. Returns an empty Configuration instead.
 315                 if (new File(userConfigFile).exists()) {
 316                     init(new File(userConfigFile).toURI().toURL(), newConfig);
 317                 }
 318             }
 319 
 320             configuration = newConfig;
 321         }
 322 
 323         private void init(URL config,
 324                           Map<String, List<AppConfigurationEntry>> newConfig)
 325                           throws IOException {
 326 
 327             try (InputStreamReader isr
 328                     = new InputStreamReader(getInputStream(config), "UTF-8")) {
 329                 readConfig(isr, newConfig);
 330             } catch (FileNotFoundException fnfe) {
 331                 if (debugConfig != null) {
 332                     debugConfig.println(fnfe.toString());
 333                 }
 334                 throw new IOException(ResourcesMgr.getAuthResourceString
 335                     ("Configuration.Error.No.such.file.or.directory"));
 336             }
 337         }
 338 
 339         /**
 340          * Retrieve an entry from the Configuration using an application name
 341          * as an index.
 342          *
 343          * @param applicationName the name used to index the Configuration.
 344          * @return an array of AppConfigurationEntries which correspond to
 345          *         the stacked configuration of LoginModules for this
 346          *         application, or null if this application has no configured
 347          *         LoginModules.
 348          */
 349         @Override
 350         public AppConfigurationEntry[] engineGetAppConfigurationEntry
 351             (String applicationName) {
 352 
 353             List<AppConfigurationEntry> list = null;
 354             synchronized (configuration) {
 355                 list = configuration.get(applicationName);
 356             }
 357 
 358             if (list == null || list.size() == 0) {
 359                 return null;
 360             }
 361 
 362             AppConfigurationEntry[] entries =
 363                                     new AppConfigurationEntry[list.size()];
 364             Iterator<AppConfigurationEntry> iterator = list.iterator();
 365             for (int i = 0; iterator.hasNext(); i++) {
 366                 AppConfigurationEntry e = iterator.next();
 367                 entries[i] = new AppConfigurationEntry(e.getLoginModuleName(),
 368                                                        e.getControlFlag(),
 369                                                        e.getOptions());
 370             }
 371             return entries;
 372         }
 373 
 374         /**
 375          * Refresh and reload the Configuration by re-reading all of the
 376          * login configurations.
 377          *
 378          * @throws SecurityException if the caller does not have permission
 379          *                           to refresh the Configuration.
 380          */
 381         @Override
 382         public synchronized void engineRefresh() {
 383 
 384             SecurityManager sm = System.getSecurityManager();
 385             if (sm != null) {
 386                 sm.checkPermission(
 387                     new AuthPermission("refreshLoginConfiguration"));
 388             }
 389 
 390             AccessController.doPrivileged(new PrivilegedAction<Void>() {
 391                 public Void run() {
 392                     try {
 393                         init();
 394                     } catch (IOException ioe) {
 395                         throw new SecurityException(ioe.getLocalizedMessage(),
 396                                                     ioe);
 397                     }
 398                     return null;
 399                 }
 400             });
 401         }
 402 
 403         private void readConfig(Reader reader,
 404             Map<String, List<AppConfigurationEntry>> newConfig)
 405             throws IOException {
 406 
 407             linenum = 1;
 408 
 409             if (!(reader instanceof BufferedReader)) {
 410                 reader = new BufferedReader(reader);
 411             }
 412 
 413             st = new StreamTokenizer(reader);
 414             st.quoteChar('"');
 415             st.wordChars('$', '$');
 416             st.wordChars('_', '_');
 417             st.wordChars('-', '-');
 418             st.wordChars('*', '*');
 419             st.lowerCaseMode(false);
 420             st.slashSlashComments(true);
 421             st.slashStarComments(true);
 422             st.eolIsSignificant(true);
 423 
 424             lookahead = nextToken();
 425             while (lookahead != StreamTokenizer.TT_EOF) {
 426                 parseLoginEntry(newConfig);
 427             }
 428         }
 429 
 430         private void parseLoginEntry(
 431             Map<String, List<AppConfigurationEntry>> newConfig)
 432             throws IOException {
 433 
 434             List<AppConfigurationEntry> configEntries = new LinkedList<>();
 435 
 436             // application name
 437             String appName = st.sval;
 438             lookahead = nextToken();
 439 
 440             if (debugParser != null) {
 441                 debugParser.println("\tReading next config entry: " + appName);
 442             }
 443 
 444             match("{");
 445 
 446             // get the modules
 447             while (peek("}") == false) {
 448                 // get the module class name
 449                 String moduleClass = match("module class name");
 450 
 451                 // controlFlag (required, optional, etc)
 452                 LoginModuleControlFlag controlFlag;
 453                 String sflag = match("controlFlag").toUpperCase(Locale.ENGLISH);
 454                 switch (sflag) {
 455                     case "REQUIRED":
 456                         controlFlag = LoginModuleControlFlag.REQUIRED;
 457                         break;
 458                     case "REQUISITE":
 459                         controlFlag = LoginModuleControlFlag.REQUISITE;
 460                         break;
 461                     case "SUFFICIENT":
 462                         controlFlag = LoginModuleControlFlag.SUFFICIENT;
 463                         break;
 464                     case "OPTIONAL":
 465                         controlFlag = LoginModuleControlFlag.OPTIONAL;
 466                         break;
 467                     default:
 468                         throw ioException(
 469                             "Configuration.Error.Invalid.control.flag.flag",
 470                             sflag);
 471                 }
 472 
 473                 // get the args
 474                 Map<String, String> options = new HashMap<>();
 475                 while (peek(";") == false) {
 476                     String key = match("option key");
 477                     match("=");
 478                     try {
 479                         options.put(key, expand(match("option value")));
 480                     } catch (PropertyExpander.ExpandException peee) {
 481                         throw new IOException(peee.getLocalizedMessage());
 482                     }
 483                 }
 484 
 485                 lookahead = nextToken();
 486 
 487                 // create the new element
 488                 if (debugParser != null) {
 489                     debugParser.println("\t\t" + moduleClass + ", " + sflag);
 490                     for (String key : options.keySet()) {
 491                         debugParser.println("\t\t\t" + key +
 492                                             "=" + options.get(key));
 493                     }
 494                 }
 495                 configEntries.add(new AppConfigurationEntry(moduleClass,
 496                                                             controlFlag,
 497                                                             options));
 498             }
 499 
 500             match("}");
 501             match(";");
 502 
 503             // add this configuration entry
 504             if (newConfig.containsKey(appName)) {
 505                 throw ioException(
 506                     "Configuration.Error.Can.not.specify.multiple.entries.for.appName",
 507                     appName);
 508             }
 509             newConfig.put(appName, configEntries);
 510         }
 511 
 512         private String match(String expect) throws IOException {
 513 
 514             String value = null;
 515 
 516             switch(lookahead) {
 517             case StreamTokenizer.TT_EOF:
 518                 throw ioException(
 519                     "Configuration.Error.expected.expect.read.end.of.file.",
 520                     expect);
 521 
 522             case '"':
 523             case StreamTokenizer.TT_WORD:
 524                 if (expect.equalsIgnoreCase("module class name") ||
 525                     expect.equalsIgnoreCase("controlFlag") ||
 526                     expect.equalsIgnoreCase("option key") ||
 527                     expect.equalsIgnoreCase("option value")) {
 528                     value = st.sval;
 529                     lookahead = nextToken();
 530                 } else {
 531                     throw ioException(
 532                         "Configuration.Error.Line.line.expected.expect.found.value.",
 533                         linenum, expect, st.sval);
 534                 }
 535                 break;
 536 
 537             case '{':
 538                 if (expect.equalsIgnoreCase("{")) {
 539                     lookahead = nextToken();
 540                 } else {
 541                     throw ioException(
 542                         "Configuration.Error.Line.line.expected.expect.",
 543                         linenum, expect, st.sval);
 544                 }
 545                 break;
 546 
 547             case ';':
 548                 if (expect.equalsIgnoreCase(";")) {
 549                     lookahead = nextToken();
 550                 } else {
 551                     throw ioException(
 552                         "Configuration.Error.Line.line.expected.expect.",
 553                         linenum, expect, st.sval);
 554                 }
 555                 break;
 556 
 557             case '}':
 558                 if (expect.equalsIgnoreCase("}")) {
 559                     lookahead = nextToken();
 560                 } else {
 561                     throw ioException(
 562                         "Configuration.Error.Line.line.expected.expect.",
 563                         linenum, expect, st.sval);
 564                 }
 565                 break;
 566 
 567             case '=':
 568                 if (expect.equalsIgnoreCase("=")) {
 569                     lookahead = nextToken();
 570                 } else {
 571                     throw ioException(
 572                         "Configuration.Error.Line.line.expected.expect.",
 573                         linenum, expect, st.sval);
 574                 }
 575                 break;
 576 
 577             default:
 578                 throw ioException(
 579                     "Configuration.Error.Line.line.expected.expect.found.value.",
 580                     linenum, expect, st.sval);
 581             }
 582             return value;
 583         }
 584 
 585         private boolean peek(String expect) {
 586             switch (lookahead) {
 587                 case ',':
 588                     return expect.equalsIgnoreCase(",");
 589                 case ';':
 590                     return expect.equalsIgnoreCase(";");
 591                 case '{':
 592                     return expect.equalsIgnoreCase("{");
 593                 case '}':
 594                     return expect.equalsIgnoreCase("}");
 595                 default:
 596                     return false;
 597             }
 598         }
 599 
 600         private int nextToken() throws IOException {
 601             int tok;
 602             while ((tok = st.nextToken()) == StreamTokenizer.TT_EOL) {
 603                 linenum++;
 604             }
 605             return tok;
 606         }
 607 
 608         private InputStream getInputStream(URL url) throws IOException {
 609             if ("file".equalsIgnoreCase(url.getProtocol())) {
 610                 // Compatibility notes:
 611                 //
 612                 // Code changed from
 613                 //   String path = url.getFile().replace('/', File.separatorChar);
 614                 //   return new FileInputStream(path);
 615                 //
 616                 // The original implementation would search for "/tmp/a%20b"
 617                 // when url is "file:///tmp/a%20b". This is incorrect. The
 618                 // current codes fix this bug and searches for "/tmp/a b".
 619                 // For compatibility reasons, when the file "/tmp/a b" does
 620                 // not exist, the file named "/tmp/a%20b" will be tried.
 621                 //
 622                 // This also means that if both file exists, the behavior of
 623                 // this method is changed, and the current codes choose the
 624                 // correct one.
 625                 try {
 626                     return url.openStream();
 627                 } catch (Exception e) {
 628                     String file = url.getPath();
 629                     if (!url.getHost().isEmpty()) {  // For Windows UNC
 630                         file = "//" + url.getHost() + file;
 631                     }
 632                     if (debugConfig != null) {
 633                         debugConfig.println("cannot read " + url +
 634                                             ", try " + file);
 635                     }
 636                     return new FileInputStream(file);
 637                 }
 638             } else {
 639                 return url.openStream();
 640             }
 641         }
 642 
 643         private String expand(String value)
 644             throws PropertyExpander.ExpandException, IOException {
 645 
 646             if (value.isEmpty()) {
 647                 return value;
 648             }
 649 
 650             if (!expandProp) {
 651                 return value;
 652             }
 653             String s = PropertyExpander.expand(value);
 654             if (s == null || s.isEmpty()) {
 655                 throw ioException(
 656                     "Configuration.Error.Line.line.system.property.value.expanded.to.empty.value",
 657                     linenum, value);
 658             }
 659             return s;
 660         }
 661 
 662         private IOException ioException(String resourceKey, Object... args) {
 663             MessageFormat form = new MessageFormat(
 664                 ResourcesMgr.getAuthResourceString(resourceKey));
 665             return new IOException(form.format(args));
 666         }
 667     }
 668 }