/* * Copyright 2018 Mordechai Meisels * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package org.update4j.mapper; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.stream.Collectors; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.update4j.OS; import org.update4j.Property; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /* * Everything that can be replaced by a property should be stored as strings. * Resolve them in Configuration::read. * */ public class ConfigMapper extends XmlMapper { public String timestamp; public String signature; public String baseUri; public String basePath; public String updateHandler; public String launcher; public final List<Property> properties; public final List<FileMapper> files; public ConfigMapper() { properties = new ArrayList<>(); files = new ArrayList<>(); } public ConfigMapper(Node node) { this(); parse(node); } public ConfigMapper(ConfigMapper copy) { this(); timestamp = copy.timestamp; signature = copy.signature; baseUri = copy.baseUri; basePath = copy.basePath; updateHandler = copy.updateHandler; launcher = copy.launcher; properties.addAll(copy.properties); files.addAll(copy.files.stream().map(FileMapper::new).collect(Collectors.toList())); } @Override public void parse(Node node) { if (!"configuration".equals(node.getNodeName())) return; timestamp = getAttributeValue(node, "timestamp"); signature = getAttributeValue(node, "signature"); NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node n = children.item(i); if ("base".equals(n.getNodeName())) { baseUri = getAttributeValue(n, "uri"); basePath = getAttributeValue(n, "path"); } else if ("provider".equals(n.getNodeName())) { updateHandler = getAttributeValue(n, "updateHandler"); launcher = getAttributeValue(n, "launcher"); } else if ("properties".equals(n.getNodeName())) { parseProperties(n.getChildNodes()); } else if ("files".equals(n.getNodeName())) { parseFiles(n.getChildNodes()); } } } private void parseProperties(NodeList list) { for (int i = 0; i < list.getLength(); i++) { Node n = list.item(i); if ("property".equals(n.getNodeName())) { String key = getAttributeValue(n, "key"); String value = getAttributeValue(n, "value"); String os = getAttributeValue(n, "os"); OS osEnum = null; if (os != null) osEnum = OS.fromShortName(os); if (key != null && value != null) { properties.add(new Property(key, value, osEnum)); } } } } private void parseFiles(NodeList list) { for (int i = 0; i < list.getLength(); i++) { Node n = list.item(i); if ("file".equals(n.getNodeName())) { files.add(new FileMapper(n)); } } } @Override public String toXml() { StringBuilder builder = new StringBuilder(); builder.append("<configuration"); // Since anybody can modify these fields, we don't take chances and escape them if (timestamp != null) { builder.append(" timestamp=\"" + escape(timestamp) + "\""); } if (signature != null) { builder.append(" signature=\"" + escape(signature) + "\""); } String children = getChildrenXml(); if (!children.isEmpty()) { builder.append(">\n"); builder.append(children); builder.append("</configuration>"); } else { builder.append("/>\n"); } return builder.toString(); } private String getChildrenXml() { // no children if (baseUri == null && basePath == null && updateHandler == null && launcher == null && properties.isEmpty() && files.isEmpty()) { return ""; } StringBuilder builder = new StringBuilder(); if (baseUri != null || basePath != null) { builder.append(" <base"); if (baseUri != null) { builder.append(" uri=\"" + escape(baseUri) + "\""); } if (basePath != null) { builder.append(" path=\"" + escape(basePath) + "\""); } builder.append("/>\n"); } if (updateHandler != null || launcher != null) { builder.append(" <provider"); if (updateHandler != null) { builder.append(" updateHandler=\"" + escape(updateHandler) + "\""); } if (launcher != null) { builder.append(" launcher=\"" + escape(launcher) + "\""); } builder.append("/>\n"); } if (!properties.isEmpty()) { builder.append(" <properties>\n"); for (Property p : properties) { builder.append(" <property"); builder.append(" key=\"" + escape(p.getKey()) + "\""); builder.append(" value=\"" + escape(p.getValue()) + "\""); if (p.getOs() != null) builder.append(" os=\"" + p.getOs().getShortName() + "\""); builder.append("/>\n"); } builder.append(" </properties>\n"); } if (!files.isEmpty()) { builder.append(" <files>\n"); for (FileMapper fm : files) { builder.append(fm.toXml()); } builder.append(" </files>\n"); } return builder.toString(); } public String sign(PrivateKey key) { try { Signature sign = Signature.getInstance("SHA256with" + key.getAlgorithm()); sign.initSign(key); sign.update(getChildrenXml().getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(sign.sign()); } catch (InvalidKeyException | SignatureException e) { throw new RuntimeException(e); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } } public void verifySignature(PublicKey key) { if (signature == null) { throw new SecurityException("No signature in configuration root node."); } try { Signature sign = Signature.getInstance("SHA256with" + key.getAlgorithm()); sign.initVerify(key); sign.update(getChildrenXml().getBytes(StandardCharsets.UTF_8)); if (!sign.verify(Base64.getDecoder().decode(signature))) { throw new SecurityException("Signature verification failed."); } } catch (InvalidKeyException | SignatureException | NoSuchAlgorithmException e) { throw new SecurityException(e); } } public static ConfigMapper read(Reader reader) throws IOException { try { Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(reader)); NodeList list = doc.getChildNodes(); for (int i = 0; i < list.getLength(); i++) { Node n = list.item(i); if ("configuration".equals(n.getNodeName())) { return new ConfigMapper(n); } } throw new IllegalStateException("Root element must be 'configuration'."); } catch (SAXException | ParserConfigurationException e) { throw new IOException(e); } } public void write(Writer writer) throws IOException { write(writer, true); } public void write(Writer writer, boolean header) throws IOException { if (header) { writer.write("<?xml version=\"1.1\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"); writer.write("\n"); writer.write("<!-- Generated by update4j. Licensed under Apache Software License 2.0 -->\n"); } writer.write(toXml()); } }