| /* |
| * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code 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 |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package sun.security.provider; |
| |
| import java.io.*; |
| import java.net.MalformedURLException; |
| import java.net.URI; |
| import java.net.URL; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.security.PrivilegedActionException; |
| import java.security.PrivilegedExceptionAction; |
| import java.security.Security; |
| import java.security.URIParameter; |
| import java.text.MessageFormat; |
| import java.util.*; |
| import javax.security.auth.AuthPermission; |
| import javax.security.auth.login.AppConfigurationEntry; |
| import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; |
| import javax.security.auth.login.Configuration; |
| import javax.security.auth.login.ConfigurationSpi; |
| import sun.security.util.Debug; |
| import sun.security.util.PropertyExpander; |
| import sun.security.util.ResourcesMgr; |
| |
| /** |
| * This class represents a default implementation for |
| * {@code javax.security.auth.login.Configuration}. |
| * |
| * <p> This object stores the runtime login configuration representation, |
| * and is the amalgamation of multiple static login configurations that |
| * resides in files. The algorithm for locating the login configuration |
| * file(s) and reading their information into this {@code Configuration} |
| * object is: |
| * |
| * <ol> |
| * <li> |
| * Loop through the security properties, |
| * <i>login.config.url.1</i>, <i>login.config.url.2</i>, ..., |
| * <i>login.config.url.X</i>. |
| * Each property value specifies a {@code URL} pointing to a |
| * login configuration file to be loaded. Read in and load |
| * each configuration. |
| * |
| * <li> |
| * The {@code java.lang.System} property |
| * <i>java.security.auth.login.config</i> |
| * may also be set to a {@code URL} pointing to another |
| * login configuration file |
| * (which is the case when a user uses the -D switch at runtime). |
| * If this property is defined, and its use is allowed by the |
| * security property file (the Security property, |
| * <i>policy.allowSystemProperty</i> is set to <i>true</i>), |
| * also load that login configuration. |
| * |
| * <li> |
| * If the <i>java.security.auth.login.config</i> property is defined using |
| * "==" (rather than "="), then ignore all other specified |
| * login configurations and only load this configuration. |
| * |
| * <li> |
| * If no system or security properties were set, try to read from the file, |
| * ${user.home}/.java.login.config, where ${user.home} is the value |
| * represented by the "user.home" System property. |
| * </ol> |
| * |
| * <p> The configuration syntax supported by this implementation |
| * is exactly that syntax specified in the |
| * {@code javax.security.auth.login.Configuration} class. |
| * |
| * @see javax.security.auth.login.LoginContext |
| * @see java.security.Security security properties |
| */ |
| public final class ConfigFile extends Configuration { |
| |
| private final Spi spi; |
| |
| public ConfigFile() { |
| spi = new Spi(); |
| } |
| |
| @Override |
| public AppConfigurationEntry[] getAppConfigurationEntry(String appName) { |
| return spi.engineGetAppConfigurationEntry(appName); |
| } |
| |
| @Override |
| public synchronized void refresh() { |
| spi.engineRefresh(); |
| } |
| |
| public final static class Spi extends ConfigurationSpi { |
| |
| private URL url; |
| private boolean expandProp = true; |
| private Map<String, List<AppConfigurationEntry>> configuration; |
| private int linenum; |
| private StreamTokenizer st; |
| private int lookahead; |
| |
| private static Debug debugConfig = Debug.getInstance("configfile"); |
| private static Debug debugParser = Debug.getInstance("configparser"); |
| |
| /** |
| * Creates a new {@code ConfigurationSpi} object. |
| * |
| * @throws SecurityException if the {@code ConfigurationSpi} can not be |
| * initialized |
| */ |
| public Spi() { |
| try { |
| init(); |
| } catch (IOException ioe) { |
| throw new SecurityException(ioe); |
| } |
| } |
| |
| /** |
| * Creates a new {@code ConfigurationSpi} object from the specified |
| * {@code URI}. |
| * |
| * @param uri the {@code URI} |
| * @throws SecurityException if the {@code ConfigurationSpi} can not be |
| * initialized |
| * @throws NullPointerException if {@code uri} is null |
| */ |
| public Spi(URI uri) { |
| // only load config from the specified URI |
| try { |
| url = uri.toURL(); |
| init(); |
| } catch (IOException ioe) { |
| throw new SecurityException(ioe); |
| } |
| } |
| |
| public Spi(final Configuration.Parameters params) throws IOException { |
| |
| // call in a doPrivileged |
| // |
| // we have already passed the Configuration.getInstance |
| // security check. also this class is not freely accessible |
| // (it is in the "sun" package). |
| |
| try { |
| AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() { |
| public Void run() throws IOException { |
| if (params == null) { |
| init(); |
| } else { |
| if (!(params instanceof URIParameter)) { |
| throw new IllegalArgumentException |
| ("Unrecognized parameter: " + params); |
| } |
| URIParameter uriParam = (URIParameter)params; |
| url = uriParam.getURI().toURL(); |
| init(); |
| } |
| return null; |
| } |
| }); |
| } catch (PrivilegedActionException pae) { |
| throw (IOException)pae.getException(); |
| } |
| |
| // if init() throws some other RuntimeException, |
| // let it percolate up naturally. |
| } |
| |
| /** |
| * Read and initialize the entire login Configuration from the |
| * configured URL. |
| * |
| * @throws IOException if the Configuration can not be initialized |
| * @throws SecurityException if the caller does not have permission |
| * to initialize the Configuration |
| */ |
| private void init() throws IOException { |
| |
| boolean initialized = false; |
| |
| // For policy.expandProperties, check if either a security or system |
| // property is set to false (old code erroneously checked the system |
| // prop so we must check both to preserve compatibility). |
| String expand = Security.getProperty("policy.expandProperties"); |
| if (expand == null) { |
| expand = System.getProperty("policy.expandProperties"); |
| } |
| if ("false".equals(expand)) { |
| expandProp = false; |
| } |
| |
| // new configuration |
| Map<String, List<AppConfigurationEntry>> newConfig = new HashMap<>(); |
| |
| if (url != null) { |
| /** |
| * If the caller specified a URI via Configuration.getInstance, |
| * we only read from that URI |
| */ |
| if (debugConfig != null) { |
| debugConfig.println("reading " + url); |
| } |
| init(url, newConfig); |
| configuration = newConfig; |
| return; |
| } |
| |
| /** |
| * Caller did not specify URI via Configuration.getInstance. |
| * Read from URLs listed in the java.security properties file. |
| */ |
| String allowSys = Security.getProperty("policy.allowSystemProperty"); |
| |
| if ("true".equalsIgnoreCase(allowSys)) { |
| String extra_config = System.getProperty |
| ("java.security.auth.login.config"); |
| if (extra_config != null) { |
| boolean overrideAll = false; |
| if (extra_config.startsWith("=")) { |
| overrideAll = true; |
| extra_config = extra_config.substring(1); |
| } |
| try { |
| extra_config = PropertyExpander.expand(extra_config); |
| } catch (PropertyExpander.ExpandException peee) { |
| throw ioException("Unable.to.properly.expand.config", |
| extra_config); |
| } |
| |
| URL configURL = null; |
| try { |
| configURL = new URL(extra_config); |
| } catch (MalformedURLException mue) { |
| File configFile = new File(extra_config); |
| if (configFile.exists()) { |
| configURL = configFile.toURI().toURL(); |
| } else { |
| throw ioException( |
| "extra.config.No.such.file.or.directory.", |
| extra_config); |
| } |
| } |
| |
| if (debugConfig != null) { |
| debugConfig.println("reading "+configURL); |
| } |
| init(configURL, newConfig); |
| initialized = true; |
| if (overrideAll) { |
| if (debugConfig != null) { |
| debugConfig.println("overriding other policies!"); |
| } |
| configuration = newConfig; |
| return; |
| } |
| } |
| } |
| |
| int n = 1; |
| String config_url; |
| while ((config_url = Security.getProperty |
| ("login.config.url."+n)) != null) { |
| try { |
| config_url = PropertyExpander.expand |
| (config_url).replace(File.separatorChar, '/'); |
| if (debugConfig != null) { |
| debugConfig.println("\tReading config: " + config_url); |
| } |
| init(new URL(config_url), newConfig); |
| initialized = true; |
| } catch (PropertyExpander.ExpandException peee) { |
| throw ioException("Unable.to.properly.expand.config", |
| config_url); |
| } |
| n++; |
| } |
| |
| if (initialized == false && n == 1 && config_url == null) { |
| |
| // get the config from the user's home directory |
| if (debugConfig != null) { |
| debugConfig.println("\tReading Policy " + |
| "from ~/.java.login.config"); |
| } |
| config_url = System.getProperty("user.home"); |
| String userConfigFile = config_url + File.separatorChar + |
| ".java.login.config"; |
| |
| // No longer throws an exception when there's no config file |
| // at all. Returns an empty Configuration instead. |
| if (new File(userConfigFile).exists()) { |
| init(new File(userConfigFile).toURI().toURL(), newConfig); |
| } |
| } |
| |
| configuration = newConfig; |
| } |
| |
| private void init(URL config, |
| Map<String, List<AppConfigurationEntry>> newConfig) |
| throws IOException { |
| |
| try (InputStreamReader isr |
| = new InputStreamReader(getInputStream(config), "UTF-8")) { |
| readConfig(isr, newConfig); |
| } catch (FileNotFoundException fnfe) { |
| if (debugConfig != null) { |
| debugConfig.println(fnfe.toString()); |
| } |
| throw new IOException(ResourcesMgr.getString |
| ("Configuration.Error.No.such.file.or.directory", |
| "sun.security.util.AuthResources")); |
| } |
| } |
| |
| /** |
| * Retrieve an entry from the Configuration using an application name |
| * as an index. |
| * |
| * @param applicationName the name used to index the Configuration. |
| * @return an array of AppConfigurationEntries which correspond to |
| * the stacked configuration of LoginModules for this |
| * application, or null if this application has no configured |
| * LoginModules. |
| */ |
| @Override |
| public AppConfigurationEntry[] engineGetAppConfigurationEntry |
| (String applicationName) { |
| |
| List<AppConfigurationEntry> list = null; |
| synchronized (configuration) { |
| list = configuration.get(applicationName); |
| } |
| |
| if (list == null || list.size() == 0) { |
| return null; |
| } |
| |
| AppConfigurationEntry[] entries = |
| new AppConfigurationEntry[list.size()]; |
| Iterator<AppConfigurationEntry> iterator = list.iterator(); |
| for (int i = 0; iterator.hasNext(); i++) { |
| AppConfigurationEntry e = iterator.next(); |
| entries[i] = new AppConfigurationEntry(e.getLoginModuleName(), |
| e.getControlFlag(), |
| e.getOptions()); |
| } |
| return entries; |
| } |
| |
| /** |
| * Refresh and reload the Configuration by re-reading all of the |
| * login configurations. |
| * |
| * @throws SecurityException if the caller does not have permission |
| * to refresh the Configuration. |
| */ |
| @Override |
| public synchronized void engineRefresh() { |
| |
| SecurityManager sm = System.getSecurityManager(); |
| if (sm != null) { |
| sm.checkPermission( |
| new AuthPermission("refreshLoginConfiguration")); |
| } |
| |
| AccessController.doPrivileged(new PrivilegedAction<Void>() { |
| public Void run() { |
| try { |
| init(); |
| } catch (IOException ioe) { |
| throw new SecurityException(ioe.getLocalizedMessage(), |
| ioe); |
| } |
| return null; |
| } |
| }); |
| } |
| |
| private void readConfig(Reader reader, |
| Map<String, List<AppConfigurationEntry>> newConfig) |
| throws IOException { |
| |
| linenum = 1; |
| |
| if (!(reader instanceof BufferedReader)) { |
| reader = new BufferedReader(reader); |
| } |
| |
| st = new StreamTokenizer(reader); |
| st.quoteChar('"'); |
| st.wordChars('$', '$'); |
| st.wordChars('_', '_'); |
| st.wordChars('-', '-'); |
| st.wordChars('*', '*'); |
| st.lowerCaseMode(false); |
| st.slashSlashComments(true); |
| st.slashStarComments(true); |
| st.eolIsSignificant(true); |
| |
| lookahead = nextToken(); |
| while (lookahead != StreamTokenizer.TT_EOF) { |
| parseLoginEntry(newConfig); |
| } |
| } |
| |
| private void parseLoginEntry( |
| Map<String, List<AppConfigurationEntry>> newConfig) |
| throws IOException { |
| |
| List<AppConfigurationEntry> configEntries = new LinkedList<>(); |
| |
| // application name |
| String appName = st.sval; |
| lookahead = nextToken(); |
| |
| if (debugParser != null) { |
| debugParser.println("\tReading next config entry: " + appName); |
| } |
| |
| match("{"); |
| |
| // get the modules |
| while (peek("}") == false) { |
| // get the module class name |
| String moduleClass = match("module class name"); |
| |
| // controlFlag (required, optional, etc) |
| LoginModuleControlFlag controlFlag; |
| String sflag = match("controlFlag").toUpperCase(Locale.ENGLISH); |
| switch (sflag) { |
| case "REQUIRED": |
| controlFlag = LoginModuleControlFlag.REQUIRED; |
| break; |
| case "REQUISITE": |
| controlFlag = LoginModuleControlFlag.REQUISITE; |
| break; |
| case "SUFFICIENT": |
| controlFlag = LoginModuleControlFlag.SUFFICIENT; |
| break; |
| case "OPTIONAL": |
| controlFlag = LoginModuleControlFlag.OPTIONAL; |
| break; |
| default: |
| throw ioException( |
| "Configuration.Error.Invalid.control.flag.flag", |
| sflag); |
| } |
| |
| // get the args |
| Map<String, String> options = new HashMap<>(); |
| while (peek(";") == false) { |
| String key = match("option key"); |
| match("="); |
| try { |
| options.put(key, expand(match("option value"))); |
| } catch (PropertyExpander.ExpandException peee) { |
| throw new IOException(peee.getLocalizedMessage()); |
| } |
| } |
| |
| lookahead = nextToken(); |
| |
| // create the new element |
| if (debugParser != null) { |
| debugParser.println("\t\t" + moduleClass + ", " + sflag); |
| for (String key : options.keySet()) { |
| debugParser.println("\t\t\t" + key + |
| "=" + options.get(key)); |
| } |
| } |
| configEntries.add(new AppConfigurationEntry(moduleClass, |
| controlFlag, |
| options)); |
| } |
| |
| match("}"); |
| match(";"); |
| |
| // add this configuration entry |
| if (newConfig.containsKey(appName)) { |
| throw ioException( |
| "Configuration.Error.Can.not.specify.multiple.entries.for.appName", |
| appName); |
| } |
| newConfig.put(appName, configEntries); |
| } |
| |
| private String match(String expect) throws IOException { |
| |
| String value = null; |
| |
| switch(lookahead) { |
| case StreamTokenizer.TT_EOF: |
| throw ioException( |
| "Configuration.Error.expected.expect.read.end.of.file.", |
| expect); |
| |
| case '"': |
| case StreamTokenizer.TT_WORD: |
| if (expect.equalsIgnoreCase("module class name") || |
| expect.equalsIgnoreCase("controlFlag") || |
| expect.equalsIgnoreCase("option key") || |
| expect.equalsIgnoreCase("option value")) { |
| value = st.sval; |
| lookahead = nextToken(); |
| } else { |
| throw ioException( |
| "Configuration.Error.Line.line.expected.expect.found.value.", |
| new Integer(linenum), expect, st.sval); |
| } |
| break; |
| |
| case '{': |
| if (expect.equalsIgnoreCase("{")) { |
| lookahead = nextToken(); |
| } else { |
| throw ioException( |
| "Configuration.Error.Line.line.expected.expect.", |
| new Integer(linenum), expect, st.sval); |
| } |
| break; |
| |
| case ';': |
| if (expect.equalsIgnoreCase(";")) { |
| lookahead = nextToken(); |
| } else { |
| throw ioException( |
| "Configuration.Error.Line.line.expected.expect.", |
| new Integer(linenum), expect, st.sval); |
| } |
| break; |
| |
| case '}': |
| if (expect.equalsIgnoreCase("}")) { |
| lookahead = nextToken(); |
| } else { |
| throw ioException( |
| "Configuration.Error.Line.line.expected.expect.", |
| new Integer(linenum), expect, st.sval); |
| } |
| break; |
| |
| case '=': |
| if (expect.equalsIgnoreCase("=")) { |
| lookahead = nextToken(); |
| } else { |
| throw ioException( |
| "Configuration.Error.Line.line.expected.expect.", |
| new Integer(linenum), expect, st.sval); |
| } |
| break; |
| |
| default: |
| throw ioException( |
| "Configuration.Error.Line.line.expected.expect.found.value.", |
| new Integer(linenum), expect, st.sval); |
| } |
| return value; |
| } |
| |
| private boolean peek(String expect) { |
| switch (lookahead) { |
| case ',': |
| return expect.equalsIgnoreCase(","); |
| case ';': |
| return expect.equalsIgnoreCase(";"); |
| case '{': |
| return expect.equalsIgnoreCase("{"); |
| case '}': |
| return expect.equalsIgnoreCase("}"); |
| default: |
| return false; |
| } |
| } |
| |
| private int nextToken() throws IOException { |
| int tok; |
| while ((tok = st.nextToken()) == StreamTokenizer.TT_EOL) { |
| linenum++; |
| } |
| return tok; |
| } |
| |
| private InputStream getInputStream(URL url) throws IOException { |
| if ("file".equalsIgnoreCase(url.getProtocol())) { |
| // Compatibility notes: |
| // |
| // Code changed from |
| // String path = url.getFile().replace('/', File.separatorChar); |
| // return new FileInputStream(path); |
| // |
| // The original implementation would search for "/tmp/a%20b" |
| // when url is "file:///tmp/a%20b". This is incorrect. The |
| // current codes fix this bug and searches for "/tmp/a b". |
| // For compatibility reasons, when the file "/tmp/a b" does |
| // not exist, the file named "/tmp/a%20b" will be tried. |
| // |
| // This also means that if both file exists, the behavior of |
| // this method is changed, and the current codes choose the |
| // correct one. |
| try { |
| return url.openStream(); |
| } catch (Exception e) { |
| String file = url.getPath(); |
| if (url.getHost().length() > 0) { // For Windows UNC |
| file = "//" + url.getHost() + file; |
| } |
| if (debugConfig != null) { |
| debugConfig.println("cannot read " + url + |
| ", try " + file); |
| } |
| return new FileInputStream(file); |
| } |
| } else { |
| return url.openStream(); |
| } |
| } |
| |
| private String expand(String value) |
| throws PropertyExpander.ExpandException, IOException { |
| |
| if (value.isEmpty()) { |
| return value; |
| } |
| |
| if (!expandProp) { |
| return value; |
| } |
| String s = PropertyExpander.expand(value); |
| if (s == null || s.length() == 0) { |
| throw ioException( |
| "Configuration.Error.Line.line.system.property.value.expanded.to.empty.value", |
| new Integer(linenum), value); |
| } |
| return s; |
| } |
| |
| private IOException ioException(String resourceKey, Object... args) { |
| MessageFormat form = new MessageFormat(ResourcesMgr.getString |
| (resourceKey, "sun.security.util.AuthResources")); |
| return new IOException(form.format(args)); |
| } |
| } |
| } |