/*
* Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net>
* Distributed under the terms of either:
* - the common development and distribution license (CDDL), v1.0; or
* - the GNU Lesser General Public License, v2.1 or later
*/
package winstone;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import winstone.cmdline.Option;
import javax.net.ssl.KeyManagerFactory;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.RSAPrivateKeySpec;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.Map;
import java.util.logging.Level;
/**
* Implements the main listener daemon thread. This is the class that gets
* launched by the command line, and owns the server socket, etc.
*
* @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a>
* @version $Id: HttpsConnectorFactory.java,v 1.10 2007/06/13 15:27:35 rickknowles Exp $
*/
public class HttpsConnectorFactory implements ConnectorFactory {
private static final WinstoneResourceBundle SSL_RESOURCES = new WinstoneResourceBundle("winstone.LocalStrings");
private KeyStore keystore;
private String keystorePassword;
public boolean start(Map args, Server server) throws IOException {
int listenPort = Option.HTTPS_PORT.get(args);
String listenAddress = Option.HTTPS_LISTEN_ADDRESS.get(args);
int keepAliveTimeout = Option.HTTPS_KEEP_ALIVE_TIMEOUT.get(args);
if (listenPort<0) {
// not running HTTPS listener
return false;
}
try {
File opensslCert = Option.HTTPS_CERTIFICATE.get(args);
File opensslKey = Option.HTTPS_PRIVATE_KEY.get(args);
File keyStore = Option.HTTPS_KEY_STORE.get(args);
String pwd = Option.HTTPS_KEY_STORE_PASSWORD.get(args);
if ((opensslCert!=null ^ opensslKey!=null))
throw new WinstoneException(MessageFormat.format("--{0} and --{1} need to be used together", Option.HTTPS_CERTIFICATE, Option.HTTPS_PRIVATE_KEY));
if (keyStore!=null && opensslKey!=null)
throw new WinstoneException(MessageFormat.format("--{0} and --{1} are mutually exclusive", Option.HTTPS_KEY_STORE, Option.HTTPS_PRIVATE_KEY));
if (keyStore!=null) {
// load from Java style JKS
if (!keyStore.exists() || !keyStore.isFile())
throw new WinstoneException(SSL_RESOURCES.getString(
"HttpsListener.KeyStoreNotFound", keyStore.getPath()));
this.keystorePassword = pwd;
keystore = KeyStore.getInstance("JKS");
keystore.load(new FileInputStream(keyStore), this.keystorePassword.toCharArray());
} else if (opensslCert!=null) {
// load from openssl style key files
CertificateFactory cf = CertificateFactory.getInstance("X509");
Certificate cert = cf.generateCertificate(new FileInputStream(opensslCert));
PrivateKey key = readPEMRSAPrivateKey(new FileReader(opensslKey));
this.keystorePassword = "changeit";
keystore = KeyStore.getInstance("JKS");
keystore.load(null);
keystore.setKeyEntry("hudson", key, keystorePassword.toCharArray(), new Certificate[]{cert});
} else {
// use self-signed certificate
this.keystorePassword = "changeit";
System.out.println("Using one-time self-signed certificate");
X509Certificate cert;
PrivateKey privKey;
Object ckg;
try { // TODO switch to (shaded?) Bouncy Castle
// TODO: Cleanup when JDK 7 support is removed.
try {
ckg = Class.forName("sun.security.x509.CertAndKeyGen").getDeclaredConstructor(String.class, String.class, String.class).newInstance("RSA", "SHA1WithRSA", null);
} catch (ClassNotFoundException cnfe) {
// Java 8
ckg = Class.forName("sun.security.tools.keytool.CertAndKeyGen").getDeclaredConstructor(String.class, String.class, String.class).newInstance("RSA", "SHA1WithRSA", null);
}
ckg.getClass().getDeclaredMethod("generate", int.class).invoke(ckg, 1024);
privKey = (PrivateKey) ckg.getClass().getMethod("getPrivateKey").invoke(ckg);
Class<?> x500Name = Class.forName("sun.security.x509.X500Name");
Object xn = x500Name.getConstructor(String.class, String.class, String.class, String.class).newInstance("Test site", "Unknown", "Unknown", "Unknown");
cert = (X509Certificate) ckg.getClass().getMethod("getSelfCertificate", x500Name, long.class).invoke(ckg, xn, 3650L * 24 * 60 * 60);
} catch (Exception x) {
throw new WinstoneException(SSL_RESOURCES.getString("HttpsConnectorFactory.SelfSignedError"), x);
}
Logger.log(Level.WARNING, SSL_RESOURCES, "HttpsConnectorFactory.SelfSigned");
keystore = KeyStore.getInstance("JKS");
keystore.load(null);
keystore.setKeyEntry("hudson", privKey, keystorePassword.toCharArray(), new Certificate[]{cert});
}
} catch (GeneralSecurityException e) {
throw (IOException)new IOException("Failed to handle keys").initCause(e);
}
ServerConnector connector = createConnector(server,args);
connector.setPort(listenPort);
connector.setHost(listenAddress);
connector.setIdleTimeout(keepAliveTimeout);
HttpConfiguration config = connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration();
config.addCustomizer(new ForwardedRequestCustomizer());
config.setRequestHeaderSize(Option.REQUEST_HEADER_SIZE.get(args));
server.addConnector(connector);
return true;
}
private ServerConnector createConnector(Server server, Map args) {
SslContextFactory sslcf = getSSLContext(args);
return new ServerConnector(server,sslcf);
}
private static PrivateKey readPEMRSAPrivateKey(Reader reader) throws IOException, GeneralSecurityException {
// TODO: should have more robust format error handling
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
BufferedReader r = new BufferedReader(reader);
String line;
boolean in = false;
while ((line=r.readLine())!=null) {
if (line.startsWith("-----")) {
in = !in;
continue;
}
if (in) {
baos.write(B64Code.decode(line));
}
}
} finally {
reader.close();
}
BigInteger mod, privExpo;
try {
Class<?> disC = Class.forName("sun.security.util.DerInputStream");
Object dis = disC.getConstructor(byte[].class).newInstance((Object) baos.toByteArray());
Object[] seq = (Object[]) disC.getMethod("getSequence", int.class).invoke(dis, 0);
Method getBigInteger = seq[0].getClass().getMethod("getBigInteger");
// int v = seq[0].getInteger();
mod = (BigInteger) getBigInteger.invoke(seq[1]);
// pubExpo
// p1, p2, exp1, exp2, crtCoef
privExpo = (BigInteger) getBigInteger.invoke(seq[3]);
} catch (Exception x) {
throw new WinstoneException(SSL_RESOURCES.getString("HttpsConnectorFactory.LoadPrivateKeyError"), x);
}
Logger.log(Level.WARNING, SSL_RESOURCES, "HttpsConnectorFactory.LoadPrivateKey");
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate (new RSAPrivateKeySpec(mod,privExpo));
}
/**
* Used to get the base ssl context in which to create the server socket.
* This is basically just so we can have a custom location for key stores.
*/
SslContextFactory getSSLContext(Map args) {
try {
String privateKeyPassword;
// There are many legacy setups in which the KeyStore password and the
// key password are identical and people will not even be aware that these
// are two different things
// Therefore if no httpsPrivateKeyPassword is explicitely set we try to
// use the KeyStore password also for the key password not to break
// backward compatibility
// Otherwise the following code will completely break the startup of
// Jenkins in case the --httpsPrivateKeyPassword parameter is not set
privateKeyPassword = Option.HTTPS_PRIVATE_KEY_PASSWORD.get(args, keystorePassword);
// Dump the content of the keystore if log level is FULL_DEBUG
// Note: The kmf is instantiated here only to access the keystore,
// the SslContextFactory will instantiate its own KeyManager
KeyManagerFactory kmf = KeyManagerFactory.getInstance(Option.HTTPS_KEY_MANAGER_TYPE.get(args));
// In case the KeyStore password and the KeyPassword are not the same,
// the KeyManagerFactory needs the KeyPassword because it will access the individual key(s)
kmf.init(keystore, keystorePassword.toCharArray());
Logger.log(Logger.FULL_DEBUG, SSL_RESOURCES,
"HttpsListener.KeyCount", keystore.size() + "");
for (Enumeration e = keystore.aliases(); e.hasMoreElements();) {
String alias = (String) e.nextElement();
Logger.log(Logger.FULL_DEBUG, SSL_RESOURCES,
"HttpsListener.KeyFound", alias,
keystore.getCertificate(alias) + "");
}
SslContextFactory ssl = new SslContextFactory();
ssl.setKeyStore(keystore);
ssl.setKeyStorePassword(keystorePassword);
ssl.setKeyManagerPassword(privateKeyPassword);
ssl.setKeyManagerFactoryAlgorithm(Option.HTTPS_KEY_MANAGER_TYPE.get(args));
ssl.setCertAlias(Option.HTTPS_CERTIFICATE_ALIAS.get(args));
ssl.setExcludeProtocols("SSLv3", "SSLv2", "SSLv2Hello");
/**
* If true, request the client certificate ala "SSLVerifyClient require" Apache directive.
* If false, which is the default, don't do so.
* Technically speaking, there's the equivalent of "SSLVerifyClient optional", but IE doesn't
* recognize it and it always prompt the certificate chooser dialog box, so in practice
* it's useless.
* <p>
* See http://hudson.361315.n4.nabble.com/winstone-container-and-ssl-td383501.html for this failure mode in IE.
*/
ssl.setNeedClientAuth(Option.HTTPS_VERIFY_CLIENT.get(args));
return ssl;
} catch (Throwable err) {
throw new WinstoneException(SSL_RESOURCES
.getString("HttpsListener.ErrorGettingContext"), err);
}
}
}