diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Action.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Action.java new file mode 100644 index 000000000..531a0ae9b --- /dev/null +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Action.java @@ -0,0 +1,37 @@ +package net.adoptopenjdk.icedteaweb.deploymentrules; + +/** + * Action object of Rule from the ruleset file + * Stores the attributes value from id tag permission and version. + * If permission is run, then location which is the url whitelisted is permitted to be accessible. + */ +class Action { + + private String permission; + private String version; + private String message; + + public String getPermission() { + return permission; + } + + public void setPermission(final String permission) { + this.permission = permission; + } + + public String getVersion() { + return version; + } + + public void setVersion(final String version) { + this.version = version; + } + + public String getMessage() { + return message; + } + + public void setMessage(final String message) { + this.message = message; + } +} diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Certificate.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Certificate.java new file mode 100644 index 000000000..5282ccd60 --- /dev/null +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Certificate.java @@ -0,0 +1,20 @@ +package net.adoptopenjdk.icedteaweb.deploymentrules; + +/** + * Certificate object of Rule from the ruleset file + * Stores the attributes value from action tag hash. + * This is class is rarely used yet and can be extended when a + * UI component to display the entire ruleset.xml file and edit it will be enhanced + */ +class Certificate { + + private String hash; + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } +} diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Rule.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Rule.java new file mode 100644 index 000000000..4057882a7 --- /dev/null +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/Rule.java @@ -0,0 +1,47 @@ +package net.adoptopenjdk.icedteaweb.deploymentrules; + +import java.net.URL; + +/** + * See https://docs.oracle.com/javase/8/docs/technotes/guides/deploy/deployment_rules.html#CIHDCEDE + */ +class Rule { + private String location; + private Certificate certificate; + private Action action; + + public String getLocation() { + return location; + } + + public void setLocation(final String location) { + this.location = location; + } + + public Certificate getCertificate() { + return certificate; + } + + public void setCertificate(final Certificate certificate) { + this.certificate = certificate; + } + + public Action getAction() { + return action; + } + + public void setAction(final Action action) { + this.action = action; + } + + public boolean matches(URL url) { + // TODO: implement according to https://docs.oracle.com/javase/10/deploy/deployment-rule-set.htm#GUID-413F29CF-81B5-4154-9C52-22D993819C2B + // Maybe take some inspiration from ParsedWhitelistEntry.matches(URL) + return false; + } + + public boolean isAllowedToRun() { + // TODO: return true if this is allowed to run according to the action + return false; + } +} diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/RulesetJarFile.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/RulesetJarFile.java new file mode 100644 index 000000000..3302b5c08 --- /dev/null +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/RulesetJarFile.java @@ -0,0 +1,69 @@ +package net.adoptopenjdk.icedteaweb.deploymentrules; + +import net.adoptopenjdk.icedteaweb.io.IOUtils; +import net.adoptopenjdk.icedteaweb.xmlparser.ParseException; +import net.adoptopenjdk.icedteaweb.xmlparser.XMLParser; +import net.adoptopenjdk.icedteaweb.xmlparser.XmlNode; +import net.adoptopenjdk.icedteaweb.xmlparser.XmlParserFactory; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import static net.adoptopenjdk.icedteaweb.xmlparser.ParserType.MALFORMED; + +class RulesetJarFile { + + private static final String RULESET_XML = "ruleset.xml"; + + private final File jarFile; + + public RulesetJarFile(String rulesetPath) { + jarFile = new File(rulesetPath); + } + + public XmlNode getRulesetXml() throws ParseException { + final File rulesetJarFile = jarFile; + + if (!rulesetJarFile.exists()) { + throw new RuntimeException("Ruleset jar file is missing"); + } + + final String content = getRulesetXmlContent(rulesetJarFile); + return parseXml(content); + } + + public String getRulesetXmlContent(File rulesetJarFile) throws ParseException { + try { + final JarFile file = new JarFile(rulesetJarFile); + final JarEntry entry = file.getJarEntry(RULESET_XML); + if (entry == null) { + throw new ParseException("could not find a " + RULESET_XML + " in the jar " + rulesetJarFile); + } + + try (final InputStream in = file.getInputStream(entry)) { + return IOUtils.readContentAsUtf8String(in); + } + } catch (IOException e) { + throw new ParseException("file IO exception accessing the ruleset or some network issues", e); + } + } + + private XmlNode parseXml(String content) throws ParseException { + try { + final InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + final XMLParser xmlParser = XmlParserFactory.getParser(MALFORMED); + return xmlParser.getRootNode(is); + } catch (ParseException e) { + throw new ParseException("Could not parser the root Node" + e.getMessage()); + } + } + + public boolean isNotPresent() { + return !jarFile.exists(); + } +} diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/RulesetParser.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/RulesetParser.java new file mode 100644 index 000000000..f477afca4 --- /dev/null +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/RulesetParser.java @@ -0,0 +1,111 @@ +package net.adoptopenjdk.icedteaweb.deploymentrules; + +import net.adoptopenjdk.icedteaweb.xmlparser.ParseException; +import net.adoptopenjdk.icedteaweb.xmlparser.XmlNode; + +import java.util.ArrayList; +import java.util.List; + +import static net.adoptopenjdk.icedteaweb.xmlparser.NodeUtils.getAttribute; +import static net.adoptopenjdk.icedteaweb.xmlparser.NodeUtils.getChildNodes; + + +/** + * Contains methods to parse an XML document into a DeploymentRuleSetFile. + * Implements JNLP specification version 1.0. + * + * See DTD: https://docs.oracle.com/javase/8/docs/technotes/guides/deploy/deployment_rules.html#CIHBAJBB + * See: https://docs.oracle.com/javase/8/docs/technotes/guides/deploy/deployment_rules.html#CIHGIDJI + */ +class RulesetParser { + + private static final String ID_ELEMENT = "id"; + private static final String RULE_SET_ELEMENT = "ruleset"; + + //From rule starts the actual list of rule and locations stored. + private static final String RULE_ELEMENT = "rule"; + private static final String ACTION_ELEMENT = "action"; + //id element + private static final String LOCATION_ATTRIBUTE = "location"; + //certificate element + private static final String HASH_ATTRIBUTE = "hash"; + //action element + private static final String VERSION_ATTRIBUTE = "version"; + private static final String PERMISSION_ATTRIBUTE = "permission"; + + /** + * Create a parser for the Deployment rule set file + * Reads the jar and ruleset.xml file is read and parsed. Adds a deploymentRuleSet tag to cover the legalities + * If any with using a Oracle ruleset.xml. + *

+ * + * @param root the root XmlNode + * @throws ParseException if the DeploymentRuleSet string is invalid + */ + public List getRules(final XmlNode root) throws ParseException { + // ensure it's a DeploymentRuleSet node + if (root == null || !root.getNodeName().equals(RULE_SET_ELEMENT)) { + throw new ParseException("Root element is not a <" + RULE_SET_ELEMENT + "> element."); + } + return getRulesFromRuleset(root); + } + + /** + * Extracts rules from the xml tree. + * The {@code version} attribute of the {@code ruleset} element is ignored + * since there will be no new version of the schema in the future. + * + * @param parent the root node + * @return all rules found. + * @throws ParseException if the xml is not valid. + */ + private List getRulesFromRuleset(final XmlNode parent) throws ParseException { + final List result = new ArrayList<>(); + final XmlNode[] rules = getChildNodes(parent, RULE_ELEMENT); + + if (rules.length == 0) { + throw new ParseException("No rule element specified."); + } + + for (final XmlNode rule : rules) { + result.add(getRule(rule)); + } + return result; + } + + private Rule getRule(final XmlNode node) { + + // create rules + final Rule rule = new Rule(); + + // step through the elements + // first populate the id tag attribute + final XmlNode potentialIdPart = node.getFirstChild(); + if (potentialIdPart.getNodeName().equals(ID_ELEMENT)) { + //certificate element + final String hash = getAttribute(potentialIdPart, HASH_ATTRIBUTE, null); + //id element + final Certificate certs = new Certificate(); + certs.setHash(hash); + + final String location = getAttribute(potentialIdPart, LOCATION_ATTRIBUTE, null); + rule.setCertificate(certs); + rule.setLocation(location); + } + + // next populate the action tag attribute. + final XmlNode potentialActionPart = potentialIdPart.getNextSibling(); + if (potentialActionPart.getNodeName().equals(ACTION_ELEMENT)) { + final Action action = new Action(); + //action element + final String permission = getAttribute(potentialActionPart, PERMISSION_ATTRIBUTE, null); + final String version = getAttribute(potentialActionPart, VERSION_ATTRIBUTE, null); + action.setPermission(permission); + action.setVersion(version); + rule.setAction(action); + } + + return rule; + } +} + diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/UrlDeploymentRulesSetUtils.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/UrlDeploymentRulesSetUtils.java new file mode 100644 index 000000000..c507ae5a8 --- /dev/null +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/deploymentrules/UrlDeploymentRulesSetUtils.java @@ -0,0 +1,71 @@ +package net.adoptopenjdk.icedteaweb.deploymentrules; + +import net.adoptopenjdk.icedteaweb.Assert; +import net.adoptopenjdk.icedteaweb.logging.Logger; +import net.adoptopenjdk.icedteaweb.logging.LoggerFactory; +import net.adoptopenjdk.icedteaweb.xmlparser.ParseException; +import net.adoptopenjdk.icedteaweb.xmlparser.XmlNode; +import net.sourceforge.jnlp.runtime.JNLPRuntime; +import net.sourceforge.jnlp.util.IpUtil; + +import java.net.URL; +import java.util.List; + +import static java.util.Collections.emptyList; +import static net.sourceforge.jnlp.config.ConfigurationConstants.KEY_DEPLOYMENT_RULE_SET; + +public class UrlDeploymentRulesSetUtils { + + private static final Logger LOG = LoggerFactory.getLogger(UrlDeploymentRulesSetUtils.class); + + private static List deploymentRules; + + public static boolean isUrlInDeploymentRuleSet(final URL url) { + Assert.requireNonNull(url, "url"); + return isUrlInDeploymentRuleSetUrl(url, getApplicationLinkDeploymentRuleSetList()); + } + + private static boolean isUrlInDeploymentRuleSetUrl(final URL url, final List deploymentRuleSetList) { + if (deploymentRuleSetList.isEmpty()) { + return true; // empty deploymentRuleset == allow all connection + } + + if (IpUtil.isLocalhostOrLoopback(url)) { + return true; // localhost need not be in whitelist + } + + return deploymentRuleSetList.stream() + .filter(wlEntry -> wlEntry.matches(url)) + .findFirst() + .map(Rule::isAllowedToRun) + .orElse(Boolean.TRUE); + } + + private static List getApplicationLinkDeploymentRuleSetList() { + if (deploymentRules == null) { + deploymentRules = loadDeploymentRuleSetLinksFromConfiguration(); + } + return deploymentRules; + } + + private static List loadDeploymentRuleSetLinksFromConfiguration() { + try { + final String rulesetPath = JNLPRuntime.getConfiguration().getProperty(KEY_DEPLOYMENT_RULE_SET); + return parseDeploymentRuleSet(rulesetPath); + } catch (ParseException e) { + LOG.error("Please Check config property " + KEY_DEPLOYMENT_RULE_SET + ". This should point to a valid DeploymentRuleSet jar file: ", e); + return emptyList(); + } + } + + private static List parseDeploymentRuleSet(String rulesetPath) throws ParseException { + final RulesetJarFile rulesetJarFile = new RulesetJarFile(rulesetPath); + if (rulesetJarFile.isNotPresent()) { + return emptyList(); + } + + final XmlNode root = rulesetJarFile.getRulesetXml(); + final RulesetParser parser = new RulesetParser(); + return parser.getRules(root); + } +} diff --git a/core/src/main/java/net/adoptopenjdk/icedteaweb/resources/ResourceHandler.java b/core/src/main/java/net/adoptopenjdk/icedteaweb/resources/ResourceHandler.java index e0295f251..46ff471fa 100644 --- a/core/src/main/java/net/adoptopenjdk/icedteaweb/resources/ResourceHandler.java +++ b/core/src/main/java/net/adoptopenjdk/icedteaweb/resources/ResourceHandler.java @@ -16,6 +16,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.Future; +import static net.adoptopenjdk.icedteaweb.deploymentrules.UrlDeploymentRulesSetUtils.isUrlInDeploymentRuleSet; import static net.adoptopenjdk.icedteaweb.resources.ResourceStatus.DOWNLOADED; import static net.adoptopenjdk.icedteaweb.resources.ResourceStatus.ERROR; import static net.sourceforge.jnlp.cache.CacheUtil.isNonCacheable; @@ -89,10 +90,12 @@ private static Resource processResource(final Resource resource) { } private static void validateWithWhitelist(URL url) { - // Validate with whitelist specified in deployment.properties. localhost is considered valid. - if (isUrlInWhitelist(url, getApplicationUrlWhiteList())) { + // Validate with whitelist specified in deployment.properties or deployment ruleset. + // localhost is considered valid. + if (isUrlInWhitelist(url, getApplicationUrlWhiteList()) || isUrlInDeploymentRuleSet(url)) { return; } + BasicExceptionDialog.show(new SecurityException(Translator.R("SWPInvalidURL") + ": " + url)); LOG.error("Resource URL not In Whitelist: {}", url); JNLPRuntime.exit(-1); diff --git a/core/src/main/java/net/sourceforge/jnlp/config/ConfigurationConstants.java b/core/src/main/java/net/sourceforge/jnlp/config/ConfigurationConstants.java index 1b63e1d07..7a9f51ef6 100644 --- a/core/src/main/java/net/sourceforge/jnlp/config/ConfigurationConstants.java +++ b/core/src/main/java/net/sourceforge/jnlp/config/ConfigurationConstants.java @@ -299,4 +299,8 @@ public interface ConfigurationConstants { */ String KEY_HTTPCONNECTION_CONNECT_TIMEOUT = "deployment.connection.connectTimeout"; String KEY_HTTPCONNECTION_READ_TIMEOUT = "deployment.connection.readTimeout"; + + /* deployment ruleset properties*/ + String KEY_DEPLOYMENT_RULE_SET = "deployment.deploymentruleset.jar"; + }