ACCUMULO-3317 Restrict Jetty secure protocol to TLS by default. Warn if the user configures SSL instead of the default TLS
Project: http://git-wip-us.apache.org/repos/asf/accumulo/repo Commit: http://git-wip-us.apache.org/repos/asf/accumulo/commit/7789b200 Tree: http://git-wip-us.apache.org/repos/asf/accumulo/tree/7789b200 Diff: http://git-wip-us.apache.org/repos/asf/accumulo/diff/7789b200 Branch: refs/heads/1.6 Commit: 7789b200d84505b376955352fabd03d8a73b8cdf Parents: 2deabd3 Author: Josh Elser <[email protected]> Authored: Fri Nov 7 17:17:32 2014 -0500 Committer: Josh Elser <[email protected]> Committed: Fri Nov 7 17:53:40 2014 -0500 ---------------------------------------------------------------------- .../org/apache/accumulo/core/conf/Property.java | 75 ++++++------ .../org/apache/accumulo/server/Accumulo.java | 49 +++++--- .../accumulo/server/util/EmbeddedWebServer.java | 118 ++++++++++++++++--- 3 files changed, 172 insertions(+), 70 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/accumulo/blob/7789b200/core/src/main/java/org/apache/accumulo/core/conf/Property.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/accumulo/core/conf/Property.java b/core/src/main/java/org/apache/accumulo/core/conf/Property.java index 86f116b..1c0de78 100644 --- a/core/src/main/java/org/apache/accumulo/core/conf/Property.java +++ b/core/src/main/java/org/apache/accumulo/core/conf/Property.java @@ -57,7 +57,7 @@ public enum Property { "The URL Accumulo should use to connect to DFS. If this is blank, Accumulo will obtain this information from the Hadoop configuration", true, false), CRYPTO_DEFAULT_KEY_STRATEGY_KEY_LOCATION("crypto.default.key.strategy.key.location", "/accumulo/crypto/secret/keyEncryptionKey", PropertyType.ABSOLUTEPATH, "The absolute path of where to store the key encryption key within HDFS.", true, false), - + // instance properties (must be the same for every node in an instance) INSTANCE_PREFIX("instance.", null, PropertyType.PREFIX, "Properties in this category must be consistent throughout a cloud. This is enforced and servers won't be able to communicate if these differ."), @@ -78,7 +78,7 @@ public enum Property { "The authorizor class that accumulo will use to determine what labels a user has privilege to see"), INSTANCE_SECURITY_PERMISSION_HANDLER("instance.security.permissionHandler", "org.apache.accumulo.server.security.handler.ZKPermHandler", PropertyType.CLASSNAME, "The permission handler class that accumulo will use to determine if a user has privilege to perform an action"), - + // general properties GENERAL_PREFIX("general.", null, PropertyType.PREFIX, "Properties in this category affect the behavior of accumulo overall, but do not have to be consistent throughout a cloud."), @@ -93,7 +93,7 @@ public enum Property { GENERAL_KERBEROS_PRINCIPAL("general.kerberos.principal", "", PropertyType.STRING, "Name of the kerberos principal to use. _HOST will automatically be " + "replaced by the machines hostname in the hostname portion of the principal. Leave blank if not using kerberoized hdfs"), GENERAL_MAX_MESSAGE_SIZE("tserver.server.message.size.max", "1G", PropertyType.MEMORY, "The maximum size of a message that can be sent to a tablet server."), - + // properties that are specific to master server behavior MASTER_PREFIX("master.", null, PropertyType.PREFIX, "Properties in this category affect the behavior of the master server"), MASTER_CLIENTPORT("master.port.client", "9999", PropertyType.PORT, "The port used for handling client connections on the master"), @@ -114,7 +114,7 @@ public enum Property { "A class that implements a mechansim to steal write access to a file"), MASTER_FATE_THREADPOOL_SIZE("master.fate.threadpool.size", "4", PropertyType.COUNT, "The number of threads used to run FAult-Tolerant Executions. These are primarily table operations like merge."), - + // properties that are specific to tablet server behavior TSERV_PREFIX("tserver.", null, PropertyType.PREFIX, "Properties in this category affect the behavior of the tablet servers"), TSERV_CLIENT_TIMEOUT("tserver.client.timeout", "3s", PropertyType.TIMEDURATION, "Time to wait for clients to continue scans before closing a session."), @@ -209,7 +209,7 @@ public enum Property { "resiliency in the face of unexpected power outages, at the cost of speed. If method is not available, the legacy 'sync' method " + "will be used to ensure backwards compatibility with older Hadoop versions. A value of 'hflush' is the alternative to the default value " + "of 'hsync' which will result in faster writes, but with less durability"), - + // properties that are specific to logger server behavior LOGGER_PREFIX("logger.", null, PropertyType.PREFIX, "Properties in this category affect the behavior of the write-ahead logger servers"), LOGGER_DIR("logger.dir.walog", "walogs", PropertyType.PATH, @@ -225,7 +225,7 @@ public enum Property { GC_PORT("gc.port.client", "50091", PropertyType.PORT, "The listening port for the garbage collector's monitor service"), GC_DELETE_THREADS("gc.threads.delete", "16", PropertyType.COUNT, "The number of threads used to delete files"), GC_TRASH_IGNORE("gc.trash.ignore", "false", PropertyType.BOOLEAN, "Do not use the Trash, even if it is configured"), - + // properties that are specific to the monitor server behavior MONITOR_PREFIX("monitor.", null, PropertyType.PREFIX, "Properties in this category affect the behavior of the monitor web server."), MONITOR_PORT("monitor.port.client", "50095", PropertyType.PORT, "The listening port for the monitor's http service"), @@ -238,8 +238,9 @@ public enum Property { MONITOR_SSL_KEYSTOREPASS("monitor.ssl.keyStorePassword", "", PropertyType.STRING, "The keystore password for enabling monitor SSL.", true, false), MONITOR_SSL_TRUSTSTORE("monitor.ssl.trustStore", "", PropertyType.PATH, "The truststore for enabling monitor SSL.", true, false), MONITOR_SSL_TRUSTSTOREPASS("monitor.ssl.trustStorePassword", "", PropertyType.STRING, "The truststore password for enabling monitor SSL.", true, false), + MONITOR_SSL_INCLUDE_PROTOCOLS("monitor.ssl.include.protocols", "TLSv1,TLSv1.1,TLSv1.2", PropertyType.STRING, "A comma-separate list of allowed SSL protocols"), MONITOR_LOCK_CHECK_INTERVAL("monitor.lock.check.interval", "5s", PropertyType.TIMEDURATION, "The amount of time to sleep between checking for the Montior ZooKeeper lock"), - + TRACE_PREFIX("trace.", null, PropertyType.PREFIX, "Properties in this category affect the behavior of distributed tracing."), TRACE_PORT("trace.port.client", "12234", PropertyType.PORT, "The listening port for the trace server"), TRACE_TABLE("trace.table", "trace", PropertyType.STRING, "The name of the table to store distributed traces"), @@ -248,7 +249,7 @@ public enum Property { TRACE_TOKEN_PROPERTY_PREFIX("trace.token.property", null, PropertyType.PREFIX, "The prefix used to create a token for storing distributed traces. For each propetry required by trace.token.type, place this prefix in front of it."), TRACE_TOKEN_TYPE("trace.token.type", PasswordToken.class.getName(), PropertyType.CLASSNAME, "An AuthenticationToken type supported by the authorizer"), - + // per table properties TABLE_PREFIX("table.", null, PropertyType.PREFIX, "Properties in this category affect tablet server treatment of tablets, but can be configured " + "on a per-table basis. Setting these properties in the site file will override the default globally " @@ -346,7 +347,7 @@ public enum Property { TABLE_INTERPRETER_CLASS("table.interepreter", DefaultScanInterpreter.class.getName(), PropertyType.STRING, "The ScanInterpreter class to apply on scan arguments in the shell"), TABLE_CLASSPATH("table.classpath.context", "", PropertyType.STRING, "Per table classpath context"), - + // VFS ClassLoader properties VFS_CLASSLOADER_SYSTEM_CLASSPATH_PROPERTY(AccumuloVFSClassLoader.VFS_CLASSLOADER_SYSTEM_CLASSPATH_PROPERTY, "", PropertyType.STRING, "Configuration for a system level vfs classloader. Accumulo jar can be configured here and loaded out of HDFS."), @@ -360,13 +361,13 @@ public enum Property { VFS_CLASSLOADER_CACHE_DIR(AccumuloVFSClassLoader.VFS_CACHE_DIR, "${java.io.tmpdir}" + File.separator + "accumulo-vfs-cache-${user.name}", PropertyType.ABSOLUTEPATH, "Directory to use for the vfs cache. The cache will keep a soft reference to all of the classes loaded in the VM." + " This should be on local disk on each node with sufficient space. It defaults to ${java.io.tmpdir}/accumulo-vfs-cache-${user.name}", false, true); - + private String key, defaultValue, description; private PropertyType type; private boolean experimental; private boolean interpolated; static Logger log = Logger.getLogger(Property.class); - + private Property(String name, String defaultValue, PropertyType type, String description, boolean experimental, boolean interpolated) { this.key = name; this.defaultValue = defaultValue; @@ -378,24 +379,24 @@ public enum Property { // also shouldn't be changing. this.interpolated = interpolated; } - + private Property(String name, String defaultValue, PropertyType type, String description) { this(name, defaultValue, type, description, false, false); } - + @Override public String toString() { return this.key; } - + public String getKey() { return this.key; } - + public String getRawDefaultValue() { return this.defaultValue; } - + public String getDefaultValue() { if (this.interpolated) { PropertiesConfiguration pconf = new PropertiesConfiguration(); @@ -413,37 +414,37 @@ public enum Property { return getRawDefaultValue(); } } - + public PropertyType getType() { return this.type; } - + public String getDescription() { return this.description; } - + public boolean isExperimental() { return experimental; } - + private static HashSet<String> validTableProperties = null; private static HashSet<String> validProperties = null; private static HashSet<String> validPrefixes = null; - + private static boolean isKeyValidlyPrefixed(String key) { for (String prefix : validPrefixes) { if (key.startsWith(prefix)) return true; } - + return false; } - + public synchronized static boolean isValidPropertyKey(String key) { if (validProperties == null) { validProperties = new HashSet<String>(); validPrefixes = new HashSet<String>(); - + for (Property p : Property.values()) { if (p.getType().equals(PropertyType.PREFIX)) { validPrefixes.add(p.getKey()); @@ -452,10 +453,10 @@ public enum Property { } } } - + return validProperties.contains(key) || isKeyValidlyPrefixed(key); } - + public synchronized static boolean isValidTablePropertyKey(String key) { if (validTableProperties == null) { validTableProperties = new HashSet<String>(); @@ -465,36 +466,36 @@ public enum Property { } } } - + return validTableProperties.contains(key) || key.startsWith(Property.TABLE_CONSTRAINT_PREFIX.getKey()) || key.startsWith(Property.TABLE_ITERATOR_PREFIX.getKey()) || key.startsWith(Property.TABLE_LOCALITY_GROUP_PREFIX.getKey()); } - + private static final EnumSet<Property> fixedProperties = EnumSet.of(Property.TSERV_CLIENTPORT, Property.TSERV_NATIVEMAP_ENABLED, Property.TSERV_SCAN_MAX_OPENFILES, Property.MASTER_CLIENTPORT, Property.GC_PORT); - + public static boolean isFixedZooPropertyKey(Property key) { return fixedProperties.contains(key); } - + public static Set<Property> getFixedProperties() { return fixedProperties; } - + public static boolean isValidZooPropertyKey(String key) { // white list prefixes return key.startsWith(Property.TABLE_PREFIX.getKey()) || key.startsWith(Property.TSERV_PREFIX.getKey()) || key.startsWith(Property.LOGGER_PREFIX.getKey()) || key.startsWith(Property.MASTER_PREFIX.getKey()) || key.startsWith(Property.GC_PREFIX.getKey()) || key.startsWith(Property.MONITOR_PREFIX.getKey() + "banner.") || key.startsWith(VFS_CONTEXT_CLASSPATH_PROPERTY.getKey()); } - + public static Property getPropertyByKey(String key) { for (Property prop : Property.values()) if (prop.getKey().equals(key)) return prop; return null; } - + /** * @return true if this is a property whose value is expected to be a java class */ @@ -503,7 +504,7 @@ public enum Property { || (key.startsWith(Property.TABLE_ITERATOR_PREFIX.getKey()) && key.substring(Property.TABLE_ITERATOR_PREFIX.getKey().length()).split("\\.").length == 2) || key.equals(Property.TABLE_LOAD_BALANCER.getKey()); } - + public boolean isDeprecated() { Logger log = Logger.getLogger(getClass()); try { @@ -517,11 +518,11 @@ public enum Property { } return false; } - + public static <T> T createInstanceFromPropertyName(AccumuloConfiguration conf, Property property, Class<T> base, T defaultInstance) { String clazzName = conf.get(property); T instance = null; - + try { Class<? extends T> clazz = AccumuloVFSClassLoader.loadClass(clazzName, base); instance = clazz.newInstance(); @@ -529,7 +530,7 @@ public enum Property { } catch (Exception e) { log.warn("Failed to load class ", e); } - + if (instance == null) { log.info("Using " + defaultInstance.getClass().getName()); instance = defaultInstance; http://git-wip-us.apache.org/repos/asf/accumulo/blob/7789b200/server/src/main/java/org/apache/accumulo/server/Accumulo.java ---------------------------------------------------------------------- diff --git a/server/src/main/java/org/apache/accumulo/server/Accumulo.java b/server/src/main/java/org/apache/accumulo/server/Accumulo.java index 18c0b12..0401dab 100644 --- a/server/src/main/java/org/apache/accumulo/server/Accumulo.java +++ b/server/src/main/java/org/apache/accumulo/server/Accumulo.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.Arrays; import java.util.Map.Entry; import java.util.TreeMap; @@ -36,8 +37,8 @@ import org.apache.accumulo.core.util.AddressUtil; import org.apache.accumulo.core.util.UtilWaitThread; import org.apache.accumulo.core.util.Version; import org.apache.accumulo.core.zookeeper.ZooUtil; -import org.apache.accumulo.fate.ReadOnlyTStore; import org.apache.accumulo.fate.ReadOnlyStore; +import org.apache.accumulo.fate.ReadOnlyTStore; import org.apache.accumulo.fate.ZooStore; import org.apache.accumulo.server.client.HdfsZooInstance; import org.apache.accumulo.server.conf.ServerConfiguration; @@ -53,9 +54,9 @@ import org.apache.log4j.helpers.LogLog; import org.apache.zookeeper.KeeperException; public class Accumulo { - + private static final Logger log = Logger.getLogger(Accumulo.class); - + public static synchronized void updateAccumuloVersion(FileSystem fs) { try { if (getAccumuloPersistentVersion(fs) == Constants.PREV_DATA_VERSION) { @@ -67,7 +68,7 @@ public class Accumulo { throw new RuntimeException("Unable to set accumulo version: an error occurred.", e); } } - + public static synchronized int getAccumuloPersistentVersion(FileSystem fs) { int dataVersion; try { @@ -82,7 +83,7 @@ public class Accumulo { throw new RuntimeException("Unable to read accumulo version: an error occurred.", e); } } - + public static void enableTracing(String address, String application) { try { DistributedTrace.enable(HdfsZooInstance.getInstance(), ZooReaderWriter.getInstance(), application, address); @@ -92,20 +93,20 @@ public class Accumulo { } public static void init(FileSystem fs, ServerConfiguration config, String application) throws UnknownHostException { - + System.setProperty("org.apache.accumulo.core.application", application); - + if (System.getenv("ACCUMULO_LOG_DIR") != null) System.setProperty("org.apache.accumulo.core.dir.log", System.getenv("ACCUMULO_LOG_DIR")); else System.setProperty("org.apache.accumulo.core.dir.log", System.getenv("ACCUMULO_HOME") + "/logs/"); - + String localhost = InetAddress.getLocalHost().getHostName(); System.setProperty("org.apache.accumulo.core.ip.localhost.hostname", localhost); - + int logPort = config.getConfiguration().getPort(Property.MONITOR_LOG4J_PORT); System.setProperty("org.apache.accumulo.core.host.log.port", Integer.toString(logPort)); - + // Use a specific log config, if it exists String logConfig = String.format("%s/%s_logger.xml", System.getenv("ACCUMULO_CONF_DIR"), application); if (!new File(logConfig).exists()) { @@ -123,16 +124,16 @@ public class Accumulo { int dataVersion = Accumulo.getAccumuloPersistentVersion(fs); log.info("Data Version " + dataVersion); Accumulo.waitForZookeeperAndHdfs(fs); - + Version codeVersion = new Version(Constants.VERSION); if (dataVersion != Constants.DATA_VERSION && dataVersion != Constants.PREV_DATA_VERSION) { throw new RuntimeException("This version of accumulo (" + codeVersion + ") is not compatible with files stored using data version " + dataVersion); } - + TreeMap<String,String> sortedProps = new TreeMap<String,String>(); for (Entry<String,String> entry : config.getConfiguration()) sortedProps.put(entry.getKey(), entry.getValue()); - + for (Entry<String,String> entry : sortedProps.entrySet()) { if (entry.getKey().toLowerCase().contains("password") || entry.getKey().toLowerCase().contains("secret") || entry.getKey().startsWith(Property.TRACE_TOKEN_PROPERTY_PREFIX.getKey())) @@ -140,12 +141,22 @@ public class Accumulo { else log.info(entry.getKey() + " = " + entry.getValue()); } - + monitorSwappiness(); + + // Encourage users to configure TLS + final String SSL = "SSL"; + for (Property sslProtocolProperty : Arrays.asList(Property.MONITOR_SSL_INCLUDE_PROTOCOLS)) { + String value = config.getConfiguration().get(sslProtocolProperty); + if (value.contains(SSL)) { + log.warn("It is recommended that " + sslProtocolProperty + " only allow TLS"); + } + } + } - + /** - * + * */ public static void monitorSwappiness() { SimpleTimer.getInstance().schedule(new Runnable() { @@ -175,7 +186,7 @@ public class Accumulo { } }, 1000, 10 * 60 * 1000); } - + public static String getLocalAddress(String[] args) throws UnknownHostException { InetAddress result = InetAddress.getLocalHost(); for (int i = 0; i < args.length - 1; i++) { @@ -187,7 +198,7 @@ public class Accumulo { } return result.getHostName(); } - + public static void waitForZookeeperAndHdfs(FileSystem fs) { log.info("Attempting to talk to zookeeper"); while (true) { @@ -234,7 +245,7 @@ public class Accumulo { } log.info("Connected to HDFS"); } - + private static boolean isInSafeMode(FileSystem fs) throws IOException { if (!(fs instanceof DistributedFileSystem)) return false; http://git-wip-us.apache.org/repos/asf/accumulo/blob/7789b200/server/src/main/java/org/apache/accumulo/server/util/EmbeddedWebServer.java ---------------------------------------------------------------------- diff --git a/server/src/main/java/org/apache/accumulo/server/util/EmbeddedWebServer.java b/server/src/main/java/org/apache/accumulo/server/util/EmbeddedWebServer.java index 76f1826..8d62d66 100644 --- a/server/src/main/java/org/apache/accumulo/server/util/EmbeddedWebServer.java +++ b/server/src/main/java/org/apache/accumulo/server/util/EmbeddedWebServer.java @@ -16,10 +16,18 @@ */ package org.apache.accumulo.server.util; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; + +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; import javax.servlet.http.HttpServlet; +import org.apache.accumulo.core.conf.AccumuloConfiguration; import org.apache.accumulo.core.conf.Property; import org.apache.accumulo.server.monitor.Monitor; +import org.apache.commons.lang.StringUtils; import org.mortbay.jetty.Server; import org.mortbay.jetty.bio.SocketConnector; import org.mortbay.jetty.handler.ContextHandlerCollection; @@ -29,22 +37,22 @@ import org.mortbay.jetty.servlet.SessionHandler; public class EmbeddedWebServer { private static String EMPTY = ""; - + Server server = null; SocketConnector sock; ContextHandlerCollection handler; Context root; boolean usingSsl; - + public EmbeddedWebServer() { this("0.0.0.0", 0); } - + public EmbeddedWebServer(String host, int port) { server = new Server(); handler = new ContextHandlerCollection(); root = new Context(handler, "/", new SessionHandler(), null, null, null); - + if (EMPTY.equals(Monitor.getSystemConfiguration().get(Property.MONITOR_SSL_KEYSTORE)) || EMPTY.equals(Monitor.getSystemConfiguration().get(Property.MONITOR_SSL_KEYSTOREPASS)) || EMPTY.equals(Monitor.getSystemConfiguration().get(Property.MONITOR_SSL_TRUSTSTORE)) @@ -52,25 +60,107 @@ public class EmbeddedWebServer { sock = new SocketConnector(); usingSsl = false; } else { - sock = new SslSocketConnector(); - ((SslSocketConnector) sock).setKeystore(Monitor.getSystemConfiguration().get(Property.MONITOR_SSL_KEYSTORE)); - ((SslSocketConnector) sock).setKeyPassword(Monitor.getSystemConfiguration().get(Property.MONITOR_SSL_KEYSTOREPASS)); - ((SslSocketConnector) sock).setTruststore(Monitor.getSystemConfiguration().get(Property.MONITOR_SSL_TRUSTSTORE)); - ((SslSocketConnector) sock).setTrustPassword(Monitor.getSystemConfiguration().get(Property.MONITOR_SSL_TRUSTSTOREPASS)); + SslSocketConnector sslSock = new SslSocketConnector(); + AccumuloConfiguration conf = Monitor.getSystemConfiguration(); + + // Restrict the protocols on the server socket + final String includeProtocols = conf.get(Property.MONITOR_SSL_INCLUDE_PROTOCOLS); + if (null != includeProtocols && !includeProtocols.isEmpty()) { + String[] protocols = StringUtils.split(includeProtocols, ','); + sslSock = new TLSSocketConnector(protocols); + } + + sslSock.setKeystore(conf.get(Property.MONITOR_SSL_KEYSTORE)); + sslSock.setKeyPassword(conf.get(Property.MONITOR_SSL_KEYSTOREPASS)); + sslSock.setTruststore(conf.get(Property.MONITOR_SSL_TRUSTSTORE)); + sslSock.setTrustPassword(conf.get(Property.MONITOR_SSL_TRUSTSTOREPASS)); + usingSsl = true; + sock = sslSock; } sock.setHost(host); sock.setPort(port); } - + + /** + * Wrap the SocketConnector so the ServerSocket can be manipulated + */ + protected static class TLSSocketConnector extends SslSocketConnector { + + private final String[] protocols; + + protected TLSSocketConnector(String[] protocols) { + this.protocols = protocols; + } + + @Override + protected SSLServerSocketFactory createFactory() throws Exception { + return new TLSServerSocketFactory(super.createFactory(), protocols); + } + } + + /** + * Restrict the allowed protocols to TLS on the ServerSocket + */ + protected static class TLSServerSocketFactory extends SSLServerSocketFactory { + + private final SSLServerSocketFactory delegate; + private final String[] protocols; + + public TLSServerSocketFactory(SSLServerSocketFactory delegate, String[] protocols) { + this.delegate = delegate; + this.protocols = protocols; + } + + @Override + public ServerSocket createServerSocket() throws IOException { + SSLServerSocket socket = (SSLServerSocket) delegate.createServerSocket(); + return overrideProtocol(socket); + } + + @Override + public ServerSocket createServerSocket(int port) throws IOException { + SSLServerSocket socket = (SSLServerSocket) delegate.createServerSocket(port); + return overrideProtocol(socket); + } + + @Override + public ServerSocket createServerSocket(int port, int backlog) throws IOException { + SSLServerSocket socket = (SSLServerSocket) delegate.createServerSocket(port, backlog); + return overrideProtocol(socket); + } + + @Override + public ServerSocket createServerSocket(int port, int backlog, InetAddress ifAddress) throws IOException { + SSLServerSocket socket = (SSLServerSocket) delegate.createServerSocket(port, backlog); + return overrideProtocol(socket); + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + protected ServerSocket overrideProtocol(SSLServerSocket socket) { + socket.setEnabledProtocols(protocols); + return socket; + } + + } + public void addServlet(Class<? extends HttpServlet> klass, String where) { root.addServlet(klass, where); } - + public int getPort() { return sock.getLocalPort(); } - + public void start() { try { server.addConnector(sock); @@ -81,7 +171,7 @@ public class EmbeddedWebServer { throw new RuntimeException(e); } } - + public void stop() { try { server.stop(); @@ -89,7 +179,7 @@ public class EmbeddedWebServer { throw new RuntimeException(e); } } - + public boolean isUsingSsl() { return usingSsl; }
