Jelajahi Sumber

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
Eric Lee 5 tahun lalu
induk
melakukan
fed6cdad1e

+ 1 - 1
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"/>
 

+ 1 - 1
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>

+ 8 - 0
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).
 

+ 34 - 18
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);

+ 66 - 9
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);

+ 134 - 0
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;
+        }
+    }
+}

+ 114 - 0
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});
+    }
+}

+ 98 - 5
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);
     }