/*
* The MIT License (MIT)
*
* Copyright (c) 2013 Brien L. Wheeler (brienwheeler@yahoo.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.brienwheeler.apps.tomcat;
import java.io.*;
import java.net.URL;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfoBuilder;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Required;
public class TomcatBean implements InitializingBean, DisposableBean
{
private static final Log log = LogFactory.getLog(TomcatBean.class);
private static final Pattern KEY_PATTERN = Pattern.compile(
"-+BEGIN (.*)PRIVATE KEY-+([^-]*)-+END .*PRIVATE KEY-+");
private static final Pattern CERT_PATTERN = Pattern.compile(
"-+BEGIN CERTIFICATE-+([^-]*)-+END CERTIFICATE-+");
private static final String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private int port;
private int sslPort;
private File sslKeyFile;
private File sslCertFile;
private String baseDirectory;
private String webAppBase = null;
private String contextRoot = null;
private final Tomcat tomcat;
public TomcatBean()
{
tomcat = new Tomcat();
}
@Override
public void afterPropertiesSet() throws Exception
{
tomcat.setBaseDir(baseDirectory);
tomcat.getHost().setAppBase(baseDirectory);
configureNetwork();
extractWarFile();
tomcat.start();
}
@Override
public void destroy() throws Exception
{
tomcat.stop();
tomcat.getServer().await();
}
@Required
public void setBaseDirectory(String baseDirectory)
{
this.baseDirectory = baseDirectory;
}
@Required
public void setWebAppBase(String webAppBase)
{
this.webAppBase = webAppBase;
}
public void setContextRoot(String contextRoot)
{
this.contextRoot = contextRoot;
}
@Required
public void setPort(int port)
{
this.port = port;
}
@Required
public void setSslPort(int sslPort)
{
this.sslPort = sslPort;
}
@Required
public void setSslKeyFile(File sslKeyFile)
{
this.sslKeyFile = sslKeyFile;
}
@Required
public void setSslCertFile(File sslCertFile)
{
this.sslCertFile = sslCertFile;
}
private void configureNetwork() throws Exception
{
if (port > 0) {
tomcat.setPort(port);
}
else {
tomcat.getService().removeConnector(tomcat.getConnector());
}
if (sslPort > 0) {
StringBuffer randomPass = new StringBuffer();
for (int i=0; i<10; i++)
randomPass.append(characters.charAt((int) (characters.length() * Math.random())));
String keystorePass = randomPass.toString();
RSAPrivateKey privateKey = readKeyFile();
log.info("successfully read SSL private key from " + sslKeyFile.getAbsolutePath());
X509Certificate certificate = readCertFile();
log.info("successfully read SSL certificate from " + sslCertFile.getAbsolutePath());
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(null);
keyStore.setCertificateEntry("cert-alias", certificate);
keyStore.setKeyEntry("key-alias", privateKey, keystorePass.toCharArray(), new Certificate[]{certificate});
File keyStoreFile = new File("tcks");
keyStore.store(new FileOutputStream(keyStoreFile), keystorePass.toCharArray());
Connector sslConnector = new Connector();
sslConnector.setPort(sslPort);
sslConnector.setSecure(true);
sslConnector.setScheme("https");
sslConnector.setAttribute("keystoreFile", keyStoreFile.getAbsolutePath());
sslConnector.setAttribute("keystorePass", keystorePass);
sslConnector.setAttribute("clientAuth", "false");
sslConnector.setAttribute("sslProtocol", "TLS");
sslConnector.setAttribute("SSLEnabled", true);
tomcat.getService().addConnector(sslConnector);
}
}
private void extractWarFile()
{
if (webAppBase != null && webAppBase.length() > 0) {
ProtectionDomain protectionDomain = this.getClass().getProtectionDomain();
URL location = protectionDomain.getCodeSource().getLocation();
log.info("detected run JAR at " + location);
if (!location.toExternalForm().startsWith("file:") ||
!location.toExternalForm().endsWith(".jar"))
throw new IllegalArgumentException("invalid code location: " + location);
try {
ZipFile zipFile = new ZipFile(new File(location.toURI()));
Enumeration<? extends ZipEntry> entryEnum = zipFile.entries();
ZipEntry warEntry = null;
while (entryEnum.hasMoreElements()) {
ZipEntry entry = entryEnum.nextElement();
String entryName = entry.getName();
if (entryName.startsWith(webAppBase) && entryName.endsWith(".war")) {
warEntry = entry;
break;
}
}
if (warEntry == null)
throw new RuntimeException("can't find JAR entry for " + webAppBase + "*.war");
log.info("extracting WAR file " + warEntry.getName());
// extract web app WAR to current directory
InputStream inputStream = zipFile.getInputStream(warEntry);
OutputStream outputStream = new FileOutputStream(new File(warEntry.getName()));
byte buf[] = new byte[1024];
int nread;
while ((nread = inputStream.read(buf, 0, 1024)) > 0) {
outputStream.write(buf, 0, nread);
}
outputStream.close();
inputStream.close();
zipFile.close();
String launchContextRoot = contextRoot != null ? contextRoot : webAppBase;
if (!launchContextRoot.startsWith("/"))
launchContextRoot = "/" + launchContextRoot;
log.info("launching WAR file " + warEntry.getName() + " at context root " + launchContextRoot);
// add web app to Tomcat
Context context = tomcat.addWebapp(launchContextRoot, warEntry.getName());
if (context instanceof StandardContext)
((StandardContext) context).setUnpackWAR(false);
}
catch (Exception e) {
throw new RuntimeException("error extracting WAR file", e);
}
}
}
private RSAPrivateKey readKeyFile() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
String parse[] = readPEMFile(sslKeyFile, KEY_PATTERN, 2);
if (parse == null)
throw new IllegalArgumentException("invalid key file contents");
if (parse[0].length() == 0) { // BEGIN PRIVATE KEY
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.decode(parse[1])));
}
if (parse[0].contains("RSA")) { // BEGIN RSA PRIVATE KEY
Security.addProvider(new BouncyCastleProvider());
PEMParser pemParser = new PEMParser(new FileReader(sslKeyFile));
Object parsedObject = pemParser.readObject();
if (!(parsedObject instanceof PEMKeyPair))
throw new IllegalArgumentException("invalid key file contents");
PEMKeyPair keyPair = (PEMKeyPair) parsedObject;
RSAPrivateKey privateKey = (RSAPrivateKey) BouncyCastleProvider.getPrivateKey(keyPair.getPrivateKeyInfo());
if (privateKey == null)
throw new IllegalArgumentException("invalid key file contents");
return privateKey;
}
throw new IllegalArgumentException("invalid key file contents");
}
private X509Certificate readCertFile() throws IOException, CertificateException {
String parse[] = readPEMFile(sslCertFile, CERT_PATTERN, 1);
if (parse == null)
throw new IllegalArgumentException("invalid certificate file contents");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(parse[0])));
}
private String[] readPEMFile(File inFile, Pattern pattern, int groups) throws IOException {
StringBuffer buffer = new StringBuffer();
BufferedReader reader = new BufferedReader(new FileReader(inFile));
for (String line=reader.readLine(); line!=null; line=reader.readLine())
buffer.append(line);
reader.close();
Matcher matcher = pattern.matcher(buffer.toString());
if (!matcher.matches())
return null;
String[] ret = new String[groups];
for (int i=0; i<groups; i++)
ret[i] = matcher.group(i + 1);
return ret;
}
}