Skip to content

Commit

Permalink
Merge pull request #39 from olamy/feature/http2
Browse files Browse the repository at this point in the history
[JENKINS-45438] Add HTTP/2 connector
  • Loading branch information
olamy committed Jul 20, 2017
2 parents e995aaf + 67e49eb commit f589eca
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 194 deletions.
22 changes: 20 additions & 2 deletions README.md
@@ -1,6 +1,6 @@
# What is Winstone?
Winstone is a command line interface around [Jetty](http://www.eclipse.org/jetty/) 9.4, which implements
Servlet 3.1 and WebSocket/JSR-356. It is used as the default
Winstone is a command line interface around Jetty 9.4, which implements
servlet 3.1, WebSocket/JSR-356, and HTTP/2 support. It is used as the default
embedded servlet container in Jenkins (via [executable-war](https://github.com/jenkinsci/extras-executable-war) module)
and can be used by any other web applications that want to be self-contained.

Expand Down Expand Up @@ -79,6 +79,8 @@ To run different web applications for diffent virtual hosts:
--httpsKeyManagerType = the SSL KeyManagerFactory type (eg SunX509, IbmX509). Default is SunX509
--httpsPrivateKey = this switch with --httpsCertificate can be used to run HTTPS with OpenSSL secret key
/ --httpsCertificate file and the corresponding certificate file
--http2Port = set the http2 listening port. -1 to disable, Default is disabled
--http2ListenAddress = set the http2 listening address. Default is all interfaces
--controlPort = set the shutdown/control port. -1 to disable, Default disabled

--handlerCountMax = set the max no of worker threads to allow. Default is 40
Expand Down Expand Up @@ -157,6 +159,10 @@ becomes the default host.
* `java -jar winstone.jar --hostsDir=<dir containing multiple host directories>`

## Recent additions
New features in v4.0:

* Jetty is now upgraded to 9.4 to bring HTTP/2 support. Winstone now requires Java8.

New features in v3.0:

* Jetty is now upgraded to 9.2 to bring the servlet 3.1 support. Winstone now requires Java7.
Expand All @@ -168,6 +174,18 @@ New features in v2.0:

For pre-1.0 history, see [the upstream changelog](http://winstone.sourceforge.net/#recent)

##HTTP/2 Support

Please note Java8 doesn't come with ALPN support. So you need to include alpn jar in the bootclasspath.

The version to use depends on your used jvm version.

To include in the bootclasspath, simply add an option ``` -Xbootclasspath/p: ``` to your jvm start script, this must contains
a path to the alpn boot jar.
Sample

``` -Xbootclasspath/p:/Users/olamy/repository/org/mortbay/jetty/alpn/alpn-boot/8.1.11.v20170118/alpn-boot-8.1.11.v20170118.jar ```

## Development
If you have some unit test failures you may add an interface/ip alias such

Expand Down
28 changes: 28 additions & 0 deletions pom.xml
Expand Up @@ -197,6 +197,9 @@
</goals>
<configuration>
<createSourcesJar>true</createSourcesJar>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
Expand Down Expand Up @@ -294,6 +297,30 @@
<version>${jetty.version}</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>${jetty.version}</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-hpack</artifactId>
<version>${jetty.version}</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${jetty.version}</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty.alpn</groupId>
<artifactId>alpn-api</artifactId>
<version>${alpn.api.version}</version>
</dependency>

<!-- See https://github.com/eclipse/jetty.project/issues/134
<dependency>
<groupId>org.eclipse.jetty</groupId>
Expand All @@ -306,6 +333,7 @@

<properties>
<jetty.version>9.4.5.v20170502</jetty.version>
<alpn.api.version>1.1.3.v20160715</alpn.api.version>
<java.level>8</java.level>
</properties>
</project>
220 changes: 220 additions & 0 deletions src/java/winstone/AbstractSecuredConnectorFactory.java
@@ -0,0 +1,220 @@
/*
* 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.Server;
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;

/**
*
*/
public abstract class AbstractSecuredConnectorFactory implements ConnectorFactory
{
protected static final WinstoneResourceBundle SSL_RESOURCES = new WinstoneResourceBundle("winstone.LocalStrings");
protected KeyStore keystore;
protected String keystorePassword;

protected void configureSsl( Map args, Server server ) throws IOException
{
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);
}
}


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.
*/
protected 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);
}
}

}

0 comments on commit f589eca

Please sign in to comment.