zookeeper-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From an...@apache.org
Subject [zookeeper] branch master updated: ZOOKEEPER-3371: Port unification for Jetty admin server
Date Mon, 29 Jul 2019 15:09:42 GMT
This is an automated email from the ASF dual-hosted git repository.

andor pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zookeeper.git


The following commit(s) were added to refs/heads/master by this push:
     new fed6cda  ZOOKEEPER-3371: Port unification for Jetty admin server
fed6cda is described below

commit fed6cdad1e7a4fb83b568e8bf3ba3cf1da492db9
Author: Eric Lee <ericlee123@fb.com>
AuthorDate: Mon Jul 29 17:09:35 2019 +0200

    ZOOKEEPER-3371: Port unification for Jetty admin server
    
    Summary:
    Jetty does not have support for port unification. Subclassing
    of some Java networking libraries is necessary for ZooKeeper to serve
    both HTTP and HTTPS traffic on the same port.
    
    Note: The traffic is encrypted, but the authentication isn't right because the certificates
are not set up correctly for HTTP requests. This will be fixed soon.
    
    Test Plan:
    ```
    [ericlee123  ~/zookeeper] (admin-PU) > ant -Dtestcase=JettyAdminServerTest test-core-java
    ...
    junit.run-concurrent:
         [echo] Running 1 concurrent JUnit processes.
        [junit] Running org.apache.zookeeper.server.admin.JettyAdminServerTest
        [junit] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.394 sec
    
    fail.build.on.test.failure:
    
    junit.run:
    
    test-core-java:
    
    BUILD SUCCESSFUL
    Total time: 9 seconds
    ```
    
    Author: Eric Lee <ericlee123@fb.com>
    Author: Eric Lee <ericlee123@gmail.com>
    
    Reviewers: fangmin@apache.org, eolivelli@apache.org, andor@apache.org
    
    Closes #924 from ericlee123/ZOOKEEPER-3371 and squashes the following commits:
    
    74ca00d33 [Eric Lee] Merge branch 'ZOOKEEPER-3371' of https://github.com/ericlee123/zookeeper
into ZOOKEEPER-3371
    be4d9c263 [Eric Lee] cleaned up print statements from testing
    8cda69cfb [Eric Lee] Merge branch 'master' into ZOOKEEPER-3371
    329efe204 [Eric Lee] cleaned up unused imports, handles different types of key/trust store
loading, and cleaned up TLS detection
    2e5c2e5d9 [Eric Lee] updated jetty version + fixed deprecation warnings
    39279a554 [Eric Lee] small lint
    4e21f6a6a [Eric Lee] Fixed build failure + added docs
    c55a0193c [Eric Lee] [AdminPU] Port unification for Jetty admin server
---
 build.xml                                          |   2 +-
 pom.xml                                            |   2 +-
 .../src/main/resources/markdown/zookeeperAdmin.md  |   8 ++
 .../java/org/apache/zookeeper/common/X509Util.java |  52 +++++---
 .../zookeeper/server/admin/JettyAdminServer.java   |  75 ++++++++++--
 .../zookeeper/server/admin/ReadAheadEndpoint.java  | 134 +++++++++++++++++++++
 .../server/admin/UnifiedConnectionFactory.java     | 114 ++++++++++++++++++
 .../server/admin/JettyAdminServerTest.java         | 103 +++++++++++++++-
 8 files changed, 456 insertions(+), 34 deletions(-)

diff --git a/build.xml b/build.xml
index f2b81f0..8c27f3d 100644
--- a/build.xml
+++ b/build.xml
@@ -54,7 +54,7 @@ xmlns:cs="antlib:com.puppycrawl.tools.checkstyle.ant">
 
     <property name="javacc.version" value="5.0"/>
 
-    <property name="jetty.version" value="9.4.15.v20190215"/>
+    <property name="jetty.version" value="9.4.18.v20190429"/>
     <property name="jackson.version" value="2.9.9.1"/>
     <property name="dependency-check-ant.version" value="4.0.2"/>
 
diff --git a/pom.xml b/pom.xml
index e06dc48..6176f6b 100755
--- a/pom.xml
+++ b/pom.xml
@@ -279,7 +279,7 @@
     <hamcrest.version>1.3</hamcrest.version>
     <commons-cli.version>1.2</commons-cli.version>
     <netty.version>4.1.36.Final</netty.version>
-    <jetty.version>9.4.17.v20190418</jetty.version>
+    <jetty.version>9.4.18.v20190429</jetty.version>
     <jackson.version>2.9.9.1</jackson.version>
     <json.version>1.1.1</json.version>
     <jline.version>2.11</jline.version>
diff --git a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
index d38afd2..c596546 100644
--- a/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
+++ b/zookeeper-docs/src/main/resources/markdown/zookeeperAdmin.md
@@ -1462,6 +1462,14 @@ Both subsystems need to have sufficient amount of threads to achieve
peak read t
 
 #### AdminServer configuration
 
+**New in 3.6.0:** The following
+options are used to configure the [AdminServer](#sc_adminserver).
+
+* *admin.portUnification* :
+    (Java system property: **zookeeper.admin.portUnification**)
+    Enable the admin port to accept both HTTP and HTTPS traffic.
+    Defaults to disabled.
+
 **New in 3.5.0:** The following
 options are used to configure the [AdminServer](#sc_adminserver).
 
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
index 004446a..84ddd44 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java
@@ -398,6 +398,38 @@ public abstract class X509Util implements Closeable, AutoCloseable {
         }
     }
 
+    public static KeyStore loadKeyStore(
+            String keyStoreLocation,
+            String keyStorePassword,
+            String keyStoreTypeProp)
+            throws IOException, GeneralSecurityException {
+        KeyStoreFileType storeFileType =
+                KeyStoreFileType.fromPropertyValueOrFileName(
+                        keyStoreTypeProp, keyStoreLocation);
+        return FileKeyStoreLoaderBuilderProvider
+                .getBuilderForKeyStoreFileType(storeFileType)
+                .setKeyStorePath(keyStoreLocation)
+                .setKeyStorePassword(keyStorePassword)
+                .build()
+                .loadKeyStore();
+    }
+
+    public static KeyStore loadTrustStore(
+            String trustStoreLocation,
+            String trustStorePassword,
+            String trustStoreTypeProp)
+            throws IOException, GeneralSecurityException {
+        KeyStoreFileType storeFileType =
+                KeyStoreFileType.fromPropertyValueOrFileName(
+                        trustStoreTypeProp, trustStoreLocation);
+        return FileKeyStoreLoaderBuilderProvider
+                .getBuilderForKeyStoreFileType(storeFileType)
+                .setTrustStorePath(trustStoreLocation)
+                .setTrustStorePassword(trustStorePassword)
+                .build()
+                .loadTrustStore();
+    }
+
     /**
      * Creates a key manager by loading the key store from the given file of
      * the given type, optionally decrypting it using the given password.
@@ -419,15 +451,7 @@ public abstract class X509Util implements Closeable, AutoCloseable {
             keyStorePassword = "";
         }
         try {
-            KeyStoreFileType storeFileType =
-                    KeyStoreFileType.fromPropertyValueOrFileName(
-                            keyStoreTypeProp, keyStoreLocation);
-            KeyStore ks = FileKeyStoreLoaderBuilderProvider
-                    .getBuilderForKeyStoreFileType(storeFileType)
-                    .setKeyStorePath(keyStoreLocation)
-                    .setKeyStorePassword(keyStorePassword)
-                    .build()
-                    .loadKeyStore();
+            KeyStore ks = loadKeyStore(keyStoreLocation, keyStorePassword, keyStoreTypeProp);
             KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
             kmf.init(ks, keyStorePassword.toCharArray());
 
@@ -480,15 +504,7 @@ public abstract class X509Util implements Closeable, AutoCloseable {
             trustStorePassword = "";
         }
         try {
-            KeyStoreFileType storeFileType =
-                    KeyStoreFileType.fromPropertyValueOrFileName(
-                            trustStoreTypeProp, trustStoreLocation);
-            KeyStore ts = FileKeyStoreLoaderBuilderProvider
-                    .getBuilderForKeyStoreFileType(storeFileType)
-                    .setTrustStorePath(trustStoreLocation)
-                    .setTrustStorePassword(trustStorePassword)
-                    .build()
-                    .loadTrustStore();
+            KeyStore ts = loadTrustStore(trustStoreLocation, trustStorePassword, trustStoreTypeProp);
             PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new X509CertSelector());
             if (crlEnabled || ocspEnabled) {
                 pbParams.setRevocationEnabled(true);
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java
index 005c9fe..6384179 100644
--- a/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java
@@ -19,22 +19,26 @@
 package org.apache.zookeeper.server.admin;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.zookeeper.common.*;
 import org.apache.zookeeper.server.ZooKeeperServer;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.server.ServerConnector;
 import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -59,6 +63,8 @@ public class JettyAdminServer implements AdminServer {
     public static final int DEFAULT_IDLE_TIMEOUT = 30000;
     public static final String DEFAULT_COMMAND_URL = "/commands";
     private static final String DEFAULT_ADDRESS = "0.0.0.0";
+    public static final int DEFAULT_STS_MAX_AGE = 1 * 24 * 60 * 60;  // seconds in a day
+    public static final int DEFAULT_HTTP_VERSION = 11;  // based on HttpVersion.java in jetty
 
     private final Server server;
     private final String address;
@@ -67,24 +73,75 @@ public class JettyAdminServer implements AdminServer {
     private final String commandUrl;
     private ZooKeeperServer zkServer;
 
-    public JettyAdminServer() throws AdminServerException {
+    public JettyAdminServer() throws AdminServerException, IOException, GeneralSecurityException
{
         this(System.getProperty("zookeeper.admin.serverAddress", DEFAULT_ADDRESS),
              Integer.getInteger("zookeeper.admin.serverPort", DEFAULT_PORT),
              Integer.getInteger("zookeeper.admin.idleTimeout", DEFAULT_IDLE_TIMEOUT),
-             System.getProperty("zookeeper.admin.commandURL", DEFAULT_COMMAND_URL));
+             System.getProperty("zookeeper.admin.commandURL", DEFAULT_COMMAND_URL),
+             Integer.getInteger("zookeeper.admin.httpVersion", DEFAULT_HTTP_VERSION),
+             Boolean.getBoolean("zookeeper.admin.portUnification"));
     }
 
-    public JettyAdminServer(String address, int port, int timeout, String commandUrl) {
+    public JettyAdminServer(String address,
+                            int port,
+                            int timeout,
+                            String commandUrl,
+                            int httpVersion,
+                            boolean portUnification) throws IOException, GeneralSecurityException
{
         this.port = port;
         this.idleTimeout = timeout;
         this.commandUrl = commandUrl;
         this.address = address;
 
         server = new Server();
-        ServerConnector connector = new ServerConnector(server);
+        ServerConnector connector = null;
+
+        if (!portUnification) {
+            connector = new ServerConnector(server);
+        } else {
+            SecureRequestCustomizer customizer = new SecureRequestCustomizer();
+            customizer.setStsMaxAge(DEFAULT_STS_MAX_AGE);
+            customizer.setStsIncludeSubDomains(true);
+
+            HttpConfiguration config = new HttpConfiguration();
+            config.setSecureScheme("https");
+            config.addCustomizer(customizer);
+
+            try (QuorumX509Util x509Util = new QuorumX509Util()) {
+                String privateKeyType = System.getProperty(x509Util.getSslKeystoreTypeProperty(),
"");
+                String privateKeyPath = System.getProperty(x509Util.getSslKeystoreLocationProperty(),
"");
+                String privateKeyPassword = System.getProperty(x509Util.getSslKeystorePasswdProperty(),
"");
+                String certAuthType = System.getProperty(x509Util.getSslTruststoreTypeProperty(),
"");
+                String certAuthPath = System.getProperty(x509Util.getSslTruststoreLocationProperty(),
"");
+                String certAuthPassword = System.getProperty(x509Util.getSslTruststorePasswdProperty(),
"");
+                KeyStore keyStore = null, trustStore = null;
+
+                try {
+                    keyStore = X509Util.loadKeyStore(privateKeyPath, privateKeyPassword,
privateKeyType);
+                    trustStore = X509Util.loadTrustStore(certAuthPath, certAuthPassword,
certAuthType);
+                    LOG.info("Successfully loaded private key from " + privateKeyPath);
+                    LOG.info("Successfully loaded certificate authority from " + certAuthPath);
+                } catch (Exception e) {
+                    LOG.error("Failed to load authentication certificates for admin server:
" + e);
+                    throw e;
+                }
+
+                SslContextFactory sslContextFactory = new SslContextFactory.Server();
+                sslContextFactory.setKeyStore(keyStore);
+                sslContextFactory.setKeyStorePassword(privateKeyPassword);
+                sslContextFactory.setTrustStore(trustStore);
+                sslContextFactory.setTrustStorePassword(certAuthPassword);
+
+                connector = new ServerConnector(server,
+                        new UnifiedConnectionFactory(sslContextFactory, HttpVersion.fromVersion(httpVersion).asString()),
+                        new HttpConnectionFactory(config));
+            }
+        }
+
         connector.setHost(address);
         connector.setPort(port);
         connector.setIdleTimeout(idleTimeout);
+
         server.addConnector(connector);
 
         ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/ReadAheadEndpoint.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/ReadAheadEndpoint.java
new file mode 100644
index 0000000..641f891
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/ReadAheadEndpoint.java
@@ -0,0 +1,134 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+// This code was found and refactored from here:
+// https://stackoverflow.com/questions/11182192/how-do-i-serve-https-and-http-for-jetty-from-one-port/40076056#40076056
+
+package org.apache.zookeeper.server.admin;
+
+import java.lang.IllegalArgumentException;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadPendingException;
+import java.nio.channels.WritePendingException;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+
+public class ReadAheadEndpoint implements EndPoint {
+    private final EndPoint endPoint;
+    private final ByteBuffer start;
+    private final byte[] bytes;
+    private int leftToRead;
+    private IOException pendingException = null;
+
+    @Override public InetSocketAddress getLocalAddress            () { return endPoint.getLocalAddress();
}
+    @Override public InetSocketAddress getRemoteAddress           () { return endPoint.getRemoteAddress();
}
+    @Override public boolean           isOpen                     () { return endPoint.isOpen();
}
+    @Override public long              getCreatedTimeStamp        () { return endPoint.getCreatedTimeStamp();
}
+    @Override public boolean           isOutputShutdown           () { return endPoint.isOutputShutdown();
}
+    @Override public boolean           isInputShutdown            () { return endPoint.isInputShutdown();
}
+    @Override public void              shutdownOutput             () { endPoint.shutdownOutput();
}
+    @Override public void              close                      () { endPoint.close();
}
+    @Override public Object            getTransport               () { return endPoint.getTransport();
}
+    @Override public long              getIdleTimeout             () { return endPoint.getIdleTimeout();
}
+    @Override public Connection        getConnection              () { return endPoint.getConnection();
}
+    @Override public void              onOpen                     () { endPoint.onOpen();
}
+    @Override public void              onClose                    () { endPoint.onClose();
}
+    @Override public boolean           isOptimizedForDirectBuffers() { return endPoint.isOptimizedForDirectBuffers();
}
+    @Override public boolean           isFillInterested           () { return endPoint.isFillInterested();
}
+    @Override public boolean           tryFillInterested          (Callback      v) { return
endPoint.tryFillInterested(v); }
+    @Override public boolean           flush                      (ByteBuffer... v) throws
IOException { return endPoint.flush(v); }
+    @Override public void              setIdleTimeout             (long          v) { endPoint.setIdleTimeout(v);
}
+    @Override public void              write                      (Callback      v, ByteBuffer...
b) throws WritePendingException { endPoint.write(v, b); }
+    @Override public void              setConnection              (Connection    v) { endPoint.setConnection(v);
}
+    @Override public void              upgrade                    (Connection    v) { endPoint.upgrade(v);
}
+    @Override public void              fillInterested             (Callback      v) throws
ReadPendingException { endPoint.fillInterested(v); }
+
+    public ReadAheadEndpoint(final EndPoint channel, final int readAheadLength){
+        if (channel == null) {
+            throw new IllegalArgumentException("channel cannot be null");
+        }
+
+        this.endPoint = channel;
+        start = ByteBuffer.wrap(bytes = new byte[readAheadLength]);
+        start.flip();
+        leftToRead = readAheadLength;
+    }
+
+    private synchronized void readAhead() throws IOException {
+        if (leftToRead > 0) {
+            int n = 0;
+            do {
+                n = endPoint.fill(start);
+            } while (n == 0 && endPoint.isOpen() && !endPoint.isInputShutdown());
+            if (n == -1) {
+                leftToRead = -1;
+            } else {
+                leftToRead -= n;
+            }
+            if (leftToRead <= 0) start.rewind();
+        }
+    }
+
+    private int readFromStart(final ByteBuffer dst) throws IOException {
+        final int n = Math.min(dst.remaining(), start.remaining());
+        if (n > 0)  {
+            dst.put(bytes, start.position(), n);
+            start.position(start.position() + n);
+            dst.flip();
+        }
+        return n;
+    }
+
+    @Override
+    public synchronized int fill(final ByteBuffer dst) throws IOException {
+        throwPendingException();
+        if (leftToRead > 0) readAhead();
+        if (leftToRead > 0) return 0;
+        final int sr = start.remaining();
+        if (sr > 0) {
+            dst.compact();
+            final int n = readFromStart(dst);
+            if (n < sr) return n;
+        }
+        return sr + endPoint.fill(dst);
+    }
+
+    public byte[] getBytes() {
+        if (pendingException == null) {
+            try {
+                readAhead();
+            } catch (IOException e) {
+                pendingException = e;
+            }
+        }
+        byte[] ret = new byte[bytes.length];
+        System.arraycopy(bytes, 0, ret, 0, ret.length);
+        return ret;
+    }
+
+    private void throwPendingException() throws IOException {
+        if (pendingException != null) {
+            IOException e = pendingException;
+            pendingException = null;
+            throw e;
+        }
+    }
+}
diff --git a/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/UnifiedConnectionFactory.java
b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/UnifiedConnectionFactory.java
new file mode 100644
index 0000000..89018f4
--- /dev/null
+++ b/zookeeper-server/src/main/java/org/apache/zookeeper/server/admin/UnifiedConnectionFactory.java
@@ -0,0 +1,114 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.zookeeper.server.admin;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.AbstractConnectionFactory;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLSession;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The purpose of this class is to dynamically determine whether to create
+ * a plaintext or SSL connection whenever newConnection() is called. It works
+ * in conjunction with ReadAheadEndpoint to inspect bytes on the incoming
+ * connection.
+ */
+public class UnifiedConnectionFactory extends AbstractConnectionFactory {
+    private static final Logger LOG = LoggerFactory.getLogger(UnifiedConnectionFactory.class);
+
+    private final SslContextFactory sslContextFactory;
+    private final String nextProtocol;
+
+    public UnifiedConnectionFactory(String nextProtocol) { this(null, nextProtocol); }
+
+    public UnifiedConnectionFactory(SslContextFactory factory, String nextProtocol) {
+        super("SSL");
+        this.sslContextFactory = (factory == null) ? new SslContextFactory.Server() : factory;
+        this.nextProtocol = nextProtocol;
+        this.addBean(this.sslContextFactory);
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        SSLEngine engine = this.sslContextFactory.newSSLEngine();
+        SSLSession session = engine.getSession();
+        engine.setUseClientMode(false);
+        if (session.getPacketBufferSize() > this.getInputBufferSize()) {
+            this.setInputBufferSize(session.getPacketBufferSize());
+        }
+    }
+
+    @Override
+    public Connection newConnection(Connector connector, EndPoint realEndPoint) {
+        ReadAheadEndpoint aheadEndpoint = new ReadAheadEndpoint(realEndPoint, 1);
+        byte[] bytes = aheadEndpoint.getBytes();
+        boolean isSSL;
+
+        if (bytes == null || bytes.length == 0) {
+            isSSL = false;
+            LOG.warn("Incoming connection has no data");
+        } else {
+            byte b = bytes[0]; // TLS first byte is 0x16, let's not support SSLv3 and below
+            isSSL = b == 0x16; // matches SSL detection in NettyServerCnxnFactory.java
+        }
+
+        LOG.debug(String.format("UnifiedConnectionFactory: newConnection() with SSL = %b",
isSSL));
+
+        EndPoint plainEndpoint;
+        SslConnection sslConnection;
+
+        if (isSSL) {
+            SSLEngine engine = this.sslContextFactory.newSSLEngine(aheadEndpoint.getRemoteAddress());
+            engine.setUseClientMode(false);
+            sslConnection = this.newSslConnection(connector, aheadEndpoint, engine);
+            sslConnection.setRenegotiationAllowed(this.sslContextFactory.isRenegotiationAllowed());
+            this.configure(sslConnection, connector, aheadEndpoint);
+            plainEndpoint = sslConnection.getDecryptedEndPoint();
+        } else {
+            sslConnection = null;
+            plainEndpoint = aheadEndpoint;
+        }
+
+        ConnectionFactory next = connector.getConnectionFactory(nextProtocol);
+        Connection connection = next.newConnection(connector, plainEndpoint);
+        plainEndpoint.setConnection(connection);
+
+        return (sslConnection == null) ? connection : sslConnection;
+    }
+
+    protected SslConnection newSslConnection(final Connector connector, final EndPoint endPoint,
final SSLEngine engine) {
+        return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(),
endPoint, engine);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s@%x{%s->%s}", new Object[]{this.getClass().getSimpleName(),
+            Integer.valueOf(this.hashCode()), this.getProtocol(), this.nextProtocol});
+    }
+}
diff --git a/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java
b/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java
index bc8aab6..26ca2f9 100644
--- a/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java
+++ b/zookeeper-server/src/test/java/org/apache/zookeeper/server/admin/JettyAdminServerTest.java
@@ -19,27 +19,45 @@
 package org.apache.zookeeper.server.admin;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.net.MalformedURLException;
 import java.net.URL;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSession;
 
+import org.apache.zookeeper.common.KeyStoreFileType;
+import org.apache.zookeeper.common.X509Exception.SSLContextException;
 import org.apache.zookeeper.PortAssignment;
 import org.apache.zookeeper.ZKTestCase;
+import org.apache.zookeeper.common.X509KeyType;
+import org.apache.zookeeper.common.X509TestContext;
 import org.apache.zookeeper.server.ZooKeeperServerMainTest;
 import org.apache.zookeeper.server.admin.AdminServer.AdminServerException;
 import org.apache.zookeeper.server.quorum.QuorumPeerTestBase;
 import org.apache.zookeeper.test.ClientBase;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import java.security.GeneralSecurityException;
+
 public class JettyAdminServerTest extends ZKTestCase{
     protected static final Logger LOG = LoggerFactory.getLogger(JettyAdminServerTest.class);
 
     private static final String URL_FORMAT = "http://localhost:%d/commands";
+    private static final String HTTPS_URL_FORMAT = "https://localhost:%d/commands";
     private static final int jettyAdminPort = PortAssignment.unique();
 
     @Before
@@ -49,11 +67,79 @@ public class JettyAdminServerTest extends ZKTestCase{
         System.setProperty("zookeeper.admin.serverPort", "" + jettyAdminPort);
     }
 
+    @Before
+    public void setupEncryption() {
+        Security.addProvider(new BouncyCastleProvider());
+        File tmpDir = null;
+        X509TestContext x509TestContext = null;
+        try {
+            tmpDir = ClientBase.createEmptyTestDir();
+            x509TestContext = X509TestContext.newBuilder()
+                    .setTempDir(tmpDir)
+                    .setKeyStorePassword("")
+                    .setKeyStoreKeyType(X509KeyType.EC)
+                    .setTrustStorePassword("")
+                    .setTrustStoreKeyType(X509KeyType.EC)
+                    .build();
+            System.setProperty("zookeeper.ssl.quorum.keyStore.location",
+                    x509TestContext.getKeyStoreFile(KeyStoreFileType.PEM).getAbsolutePath());
+            System.setProperty("zookeeper.ssl.quorum.trustStore.location",
+                    x509TestContext.getTrustStoreFile(KeyStoreFileType.PEM).getAbsolutePath());
+        } catch (Exception e) {
+            LOG.info("Problems encountered while setting up encryption for Jetty admin server
test: " + e);
+        }
+        System.setProperty("zookeeper.ssl.quorum.keyStore.password", "");
+        System.setProperty("zookeeper.ssl.quorum.keyStore.type", "PEM");
+        System.setProperty("zookeeper.ssl.quorum.trustStore.password", "");
+        System.setProperty("zookeeper.ssl.quorum.trustStore.type", "PEM");
+        System.setProperty("zookeeper.admin.portUnification", "true");
+
+        // Create a trust manager that does not validate certificate chains
+        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
+            public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null;
}
+            public void checkClientTrusted(X509Certificate[] certs, String authType) {}
+            public void checkServerTrusted(X509Certificate[] certs, String authType) {}
+        }};
+
+        // Create all-trusting trust manager
+        SSLContext sc = null;
+        try {
+            sc = SSLContext.getInstance("SSL");
+            sc.init(null, trustAllCerts, new java.security.SecureRandom());
+        } catch (Exception e) { LOG.error("Failed to customize encryption for HTTPS: e");
}
+
+        // Create all-trusting hostname verifier
+        HostnameVerifier allValid = new HostnameVerifier() {
+            public boolean verify(String hostname, SSLSession session) { return true; }
+        };
+
+        // This is a temporary fix while we do not yet have certificates set up to make
+        // HTTPS requests correctly. This is equivalent to the "-k" option in curl.
+        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+        HttpsURLConnection.setDefaultHostnameVerifier(allValid);
+    }
+
+    @After
+    public void cleanUp() {
+        Security.removeProvider("BC");
+
+        System.clearProperty("zookeeper.admin.enableServer");
+        System.clearProperty("zookeeper.admin.serverPort");
+
+        System.clearProperty("zookeeper.ssl.quorum.keyStore.location");
+        System.clearProperty("zookeeper.ssl.quorum.keyStore.password");
+        System.clearProperty("zookeeper.ssl.quorum.keyStore.type");
+        System.clearProperty("zookeeper.ssl.quorum.trustStore.location");
+        System.clearProperty("zookeeper.ssl.quorum.trustStore.password");
+        System.clearProperty("zookeeper.ssl.quorum.trustStore.type");
+        System.clearProperty("zookeeper.admin.portUnification");
+    }
+
     /**
      * Tests that we can start and query a JettyAdminServer.
      */
     @Test
-    public void testJettyAdminServer() throws AdminServerException, IOException {
+    public void testJettyAdminServer() throws AdminServerException, IOException, SSLContextException,
GeneralSecurityException {
         JettyAdminServer server = new JettyAdminServer();;
         try {
             server.start();
@@ -146,16 +232,23 @@ public class JettyAdminServerTest extends ZKTestCase{
      * Check that we can load the commands page of an AdminServer running at
      * localhost:port. (Note that this should work even if no zk server is set.)
      */
-    private void queryAdminServer(int port) throws MalformedURLException, IOException {
-        queryAdminServer(String.format(URL_FORMAT, port));
+    private void queryAdminServer(int port) throws MalformedURLException, IOException, SSLContextException
{
+        queryAdminServer(String.format(URL_FORMAT, port), false);
+        queryAdminServer(String.format(HTTPS_URL_FORMAT, port), true);
     }
 
     /**
      * Check that loading urlStr results in a non-zero length response.
      */
-    private void queryAdminServer(String urlStr) throws MalformedURLException, IOException
{
+    private void queryAdminServer(String urlStr, boolean encrypted) throws MalformedURLException,
IOException, SSLContextException {
         URL url = new URL(urlStr);
-        BufferedReader dis = new BufferedReader(new InputStreamReader((url.openStream())));
+        BufferedReader dis;
+        if (!encrypted) {
+            dis = new BufferedReader(new InputStreamReader((url.openStream())));
+        } else {
+            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
+            dis = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+        }
         String line = dis.readLine();
         Assert.assertTrue(line.length() > 0);
     }


Mime
View raw message