/* * gvNIX is an open source tool for rapid application development (RAD). * Copyright (C) 2010 Generalitat Valenciana * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. * * This program 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 for more * details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.gvnix.service.roo.addon.addon.security; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.SocketException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import javax.xml.transform.Transformer; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.gvnix.service.roo.addon.addon.AnnotationsService; import org.springframework.roo.process.manager.FileManager; import org.springframework.roo.project.Dependency; import org.springframework.roo.project.LogicalPath; import org.springframework.roo.project.Path; import org.springframework.roo.project.PathResolver; import org.springframework.roo.project.ProjectOperations; import org.springframework.roo.support.util.FileUtils; import org.springframework.roo.support.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; /** * Implementation of {@link SecurityService} * * @author <a href="http://www.disid.com">DISID Corporation S.L.</a> made for <a * href="http://www.dgti.gva.es">General Directorate for Information * Technologies (DGTI)</a> */ @Component @Service public class SecurityServiceImpl implements SecurityService { private static final String DEPENDENCIES_FILE = "dependencies-wssl4.xml"; private static final String AXIS_CL_CONF_TEMPL = "client-config-axis-template.xml"; private static Logger LOGGER = Logger.getLogger(SecurityServiceImpl.class .getName()); @Reference private FileManager fileManager; @Reference private ProjectOperations projectOperations; @Reference private AnnotationsService annotationsService; /** * {@inheritDoc} * <p> * This performs this operations: * </p> * <ul> * <li>Install Apache WSSJ4 depenency in pom</li> * <li>Creates axis <code>client-config.wsdd</code> file</li> * </ul> **/ public void setupWSSJ4() { addDependencies(); createAxisClientConfigFile(); } /** * @return Returns the path to axis client wsdd config file */ private String getAxisClientConfigPath() { return projectOperations.getPathResolver().getIdentifier( LogicalPath.getInstance(Path.SRC_MAIN_RESOURCES, ""), "client-config.wsdd"); } /** * Creates client-config.wssd file in application resources folder. */ private void createAxisClientConfigFile() { String axisClientConfigPath = getAxisClientConfigPath(); if (fileManager.exists(axisClientConfigPath)) { return; } InputStream inputStream = null; OutputStream outputStream = null; try { inputStream = FileUtils.getInputStream(getClass(), AXIS_CL_CONF_TEMPL); outputStream = fileManager.createFile(axisClientConfigPath) .getOutputStream(); IOUtils.copy(inputStream, outputStream); } catch (Exception e) { throw new IllegalStateException(e); } finally { IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(outputStream); } fileManager.scan(); } /** * Adds Apache wss4j dependency to application pom. */ protected void addDependencies() { annotationsService.addAddonDependency(); InputStream templateInputStream = FileUtils.getInputStream(getClass(), DEPENDENCIES_FILE); Validate.notNull(templateInputStream, "Can't adquire dependencies file ".concat(DEPENDENCIES_FILE)); Document dependencyDoc; try { dependencyDoc = XmlUtils.getDocumentBuilder().parse( templateInputStream); } catch (Exception e) { throw new IllegalStateException(e); } Element dependencies = (Element) dependencyDoc.getFirstChild(); List<Element> dependecyElementList = XmlUtils.findElements( "/dependencies/dependency", dependencies); List<Dependency> dependencyList = new ArrayList<Dependency>(); for (Element element : dependecyElementList) { dependencyList.add(new Dependency(element)); } projectOperations.addDependencies( projectOperations.getFocusedModuleName(), dependencyList); } /** * Parse a WSDL location and if it's needed it manage keystore certs. * <p> * First it tries to parse WSDL from the given URL. If * <code>SSLHandshakeException</code> is catch because our JVM installation * is not confident with the host server certificate where WSDL is available * the method tries install required certificates with * {@link SecurityServiceImpl#installCertificates(String, String)} and retry * to parse WSDL from URL. * </p> * * @param loc Location * @param pass Password * @return WSDL document * @throws Exception */ protected Document getWsdl(String loc, String pass) throws Exception { try { // Parse the wsdl location to a DOM document Document wsdl = XmlUtils.getDocumentBuilder().parse(loc); Validate.notNull(wsdl, "No valid document format"); return wsdl; } catch (SSLHandshakeException e) { // JVM is not confident with WSDL host server certificate return installCertificates(loc, pass); } catch (SAXException e) { LOGGER.log(Level.SEVERE, "The format of the wsdl has errors: ".concat(loc), e); throw new IllegalStateException( "The format of the wsdl has errors: ".concat(loc)); } catch (IOException e) { LOGGER.log(Level.SEVERE, "The format of the wsdl has errors: ".concat(loc), e); throw new IllegalStateException( "There is not access to the wsdl: ".concat(loc)); } } /** * Get certificates in the chain of the host server and import them. * <p> * Tries to get the certificates in the certificates chain of the host * server and import them to: * <ol> * <li>A custom keystore in <code>SRC_MAIN_RESOURCES/gvnix-cacerts</code></li> * <li>The JVM cacerts keystore in * <code>$JAVA_HOME/jre/lib/security/cacerts</code>. Here we can have a * problem if JVM <code>cacerts</code> file is not writable by the user due * to file permissions. In this case we throw an exception informing about * the error.</li> * </ol> * </p> * <p> * With that operation we can try again to get the WSDL.<br/> * Also it exports the chain certificates to <code>.cer</code> files in * <code>SRC_MAIN_RESOURCES</code>, so the developer can distribute them for * its installation in other environments or just in case we reach the * problem with the JVM <code>cacerts</code> file permissions. * </p> * * @see GvNix509TrustManager#saveCertFile(String, X509Certificate, * FileManager, PathResolver) * @see <a href= * "http://download.oracle.com/javase/6/docs/technotes/tools/solaris/keytool.html" * >Java SE keytool</a>. */ protected Document installCertificates(String loc, String pass) throws NoSuchAlgorithmException, KeyStoreException, Exception, KeyManagementException, MalformedURLException, IOException, UnknownHostException, SocketException, SAXException { // Create a SSL context SSLContext context = SSLContext.getInstance("TLS"); TrustManagerFactory tmf = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); // Passphrase of the keystore: "changeit" by default char[] passArray = (StringUtils.isNotBlank(pass) ? pass.toCharArray() : "changeit".toCharArray()); // Get the project keystore and copy it from JVM if not exists File keystore = getProjectKeystore(); tmf.init(GvNix509TrustManager.loadKeyStore(keystore, passArray)); X509TrustManager defaultTrustManager = (X509TrustManager) tmf .getTrustManagers()[0]; GvNix509TrustManager tm = new GvNix509TrustManager(defaultTrustManager); context.init(null, new TrustManager[] { tm }, null); SSLSocketFactory factory = context.getSocketFactory(); // Open URL location (default 443 port if not defined) URL url = new URL(loc); String host = url.getHost(); int port = url.getPort() == -1 ? 443 : url.getPort(); SSLSocket socket = (SSLSocket) factory.createSocket(host, port); socket.setSoTimeout(10000); Document doc = null; try { socket.startHandshake(); URLConnection connection = url.openConnection(); if (connection instanceof HttpsURLConnection) { ((HttpsURLConnection) connection).setSSLSocketFactory(factory); } doc = XmlUtils.getDocumentBuilder().parse( connection.getInputStream()); socket.close(); } catch (SSLException ssle) { // Get needed certificates for this host getCerts(tm, host, keystore, passArray); doc = getWsdl(loc, pass); } catch (IOException ioe) { invalidHostCert(passArray, keystore, tm, host); } Validate.notNull(doc, "No valid document format"); return doc; } /** * Throw an illegal state exception with a invalid host cert message. * * @param pass Password * @param keystore Keystore * @param host Host destination */ protected void invalidHostCert(char[] pass, File keystore, GvNix509TrustManager tm, String host) { StringBuffer msg = new StringBuffer("There is not access to the WSDL."); X509Certificate[] certs = getCerts(tm, host, keystore, pass); if (certs != null) { msg.append(" Maybe the emited certificate does not match the hostname where WSDL resides.\n"); for (X509Certificate x509Certificate : certs) { // X.500 distinguished name String dn = x509Certificate.getSubjectX500Principal().getName(); // X.500 common name from distinguished name String cn = getCn(dn); if (cn != null) { msg.append(" * Possible hostname: ".concat(cn).concat("\n")); } else msg.append(" * Possible hostname (check Cert. Distinguished name): " .concat(dn).concat("\n")); } } throw new IllegalStateException(msg.toString()); } /** * Given a X.500 Subject Distinguished name it returns the Common Name. * <p> * If CN exists, null otherwise * </p> * * @param dn Distinguished name * @return Common name if exists, null otherwise */ private String getCn(String dn) { int i = dn.indexOf("CN="); if (i == -1) { return null; } // get the remaining DN without CN= String cn = dn.substring(i); i = cn.indexOf(","); if (i == -1) { return cn.substring(3); } return cn.substring(3, i); } /** * Stores in keystore needed certificates. * * @param tm * @param host * @param keystore * @param pass * @return */ private X509Certificate[] getCerts(GvNix509TrustManager tm, String host, File keystore, char[] pass) { X509Certificate[] certs = null; try { certs = tm.addCerts(host, keystore, pass); if (certs != null) { for (int i = 0; i < certs.length; i++) { GvNix509TrustManager.saveCertFile( host.concat("-" + (i + 1)), certs[i], fileManager, projectOperations.getPathResolver()); } // Import needed certificates to JVM cacerts keystore tm.addCerts(host, getJvmKeystore(), pass); } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error loading or saving X509 certificates in keystore", e); throw new IllegalStateException( "Error loading or saving X509 certificates in keystore"); } return certs; } /** * {@inheritDoc} */ public Document getWsdl(String url) { try { // Read WSDL with the support of the Security System return getWsdl(url, null); } catch (Exception e) { throw new IllegalStateException( "Error parsing WSDL from ".concat(url), e); } } /** * Copy, if exists, the JVM keystore file in the project resources folder. * <p> * Destination resources file name is gvnix-cacerts. * </p> * * @return File created or existing file */ private File getProjectKeystore() { // Get path to file at resources String path = projectOperations.getPathResolver().getIdentifier( LogicalPath.getInstance(Path.SRC_MAIN_RESOURCES, ""), "gvnix-cacerts"); // Get JVM keystore file and validate it File keystore = getJvmKeystore(); Validate.isTrue(keystore.isFile(), "JVM cacerts file does not exist"); // When JVM keystore is valid file and already not copied to project if (!fileManager.exists(path)) { // Write JVM keystore into resources file path InputStream inputStream = null; OutputStream outputStream = null; try { inputStream = new FileInputStream(keystore); outputStream = fileManager.createFile(path).getOutputStream(); IOUtils.copy(inputStream, outputStream); fileManager.commit(); } catch (IOException e) { throw new IllegalStateException( "Error creatin a copy of ".concat(keystore .getAbsolutePath())); } finally { IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(outputStream); } } // Return existing or created project resources keystore return new File(path); } /** * Get JVM keytore file. * <p> * "java.home" system property is required. * </p> * * @return JVM keystore file */ private File getJvmKeystore() { return new File(System.getProperty("java.home").concat(File.separator) .concat("lib").concat(File.separator).concat("security") .concat(File.separator).concat("cacerts")); } /** * {@inheritDoc} */ public void addOrUpdateAxisClientService(String serviceName, Map<String, String> parameters) throws SAXException, IOException { createAxisClientConfigFile(); String axisClientPath = getAxisClientConfigPath(); Document document = XmlUtils.getDocumentBuilder().parse( new File(axisClientPath)); Element deployment = (Element) document.getFirstChild(); Element serviceElementDescriptor = getAxisClientService(document, serviceName, parameters); List<Element> previousServices = XmlUtils.findElements( "/deployment/service[@name='".concat(serviceName).concat("']"), deployment); if (previousServices.isEmpty()) { deployment.appendChild(serviceElementDescriptor); } else { deployment.replaceChild(serviceElementDescriptor, previousServices.get(0)); } OutputStream outputStream = new ByteArrayOutputStream(); Transformer transformer = XmlUtils.createIndentingTransformer(); XmlUtils.writeXml(transformer, outputStream, document); fileManager.createOrUpdateTextFileIfRequired(axisClientPath, outputStream.toString(), false); } /** * <p> * Create a xml element for a axis security service cliente configuration * </p> * <p> * Result element will be like this: * </p> * * <pre> * <service name="{serviceName}"> * <requestFlow> * <handler type="java:org.apache.ws.axis.security.WSDoAllSender"> * <!-- For every entry in parameters --> * <parameter name="{paramKey}" value="{paramValue}"/> * <!-- End for every entry --> * </handler> * </requestFlow> * </service> * </pre> * * @param doc client-config.wsdd document * @param serviceName * @param parameters * @return */ private Element getAxisClientService(Document doc, String serviceName, Map<String, String> parameters) { // <service name="Service_Name"> Element service = doc.createElement("service"); // FIXME set service name service.setAttribute("name", serviceName); // <requestFlow> Element requestFlow = doc.createElement("requestFlow"); // <handler type="java:org.apache.ws.axis.security.WSDoAllSender"> Element handler = doc.createElement("handler"); handler.setAttribute("type", "java:org.apache.ws.axis.security.WSDoAllSender"); Element parameter; Set<Entry<String, String>> entrySet = parameters.entrySet(); for (Entry<String, String> entry : entrySet) { // <parameter name="{paramKey}" value="{paramValue}"/> parameter = doc.createElement("parameter"); parameter.setAttribute("name", entry.getKey()); parameter.setAttribute("value", entry.getValue()); handler.appendChild(parameter); } // </handler> requestFlow.appendChild(handler); // </requestFlow> service.appendChild(requestFlow); // </service> return service; } }