
com.proofpoint.http.server.HttpServer Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2010 Proofpoint, Inc.
*
* Licensed 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 com.proofpoint.http.server;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import com.proofpoint.bootstrap.AcceptRequests;
import com.proofpoint.http.server.HttpServerBinder.HttpResourceBinding;
import com.proofpoint.node.NodeInfo;
import com.proofpoint.stats.MaxGauge;
import jakarta.annotation.Nullable;
import jakarta.annotation.PreDestroy;
import jakarta.servlet.Filter;
import jakarta.servlet.Servlet;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.servlet.SessionHandler;
import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http2.server.AuthorityCustomizer;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.jmx.MBeanContainer;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.HostHeaderCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.EventsHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.weakref.jmx.Flatten;
import org.weakref.jmx.Managed;
import org.weakref.jmx.Nested;
import javax.management.MBeanServer;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.ServerSocketChannel;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executor;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.lang.Math.toIntExact;
import static java.time.temporal.ChronoUnit.DAYS;
import static java.util.Collections.list;
import static java.util.Comparator.naturalOrder;
import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNullElse;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.eclipse.jetty.http.MimeTypes.Type.TEXT_PLAIN;
import static org.eclipse.jetty.security.Constraint.ALLOWED;
import static org.eclipse.jetty.util.VirtualThreads.getNamedVirtualThreadsExecutor;
public class HttpServer
{
private static final String[] ENABLED_PROTOCOLS = {"TLSv1.2", "TLSv1.3"};
private static final String[] ENABLED_CIPHERS = {
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_EMPTY_RENEGOTIATION_INFO_SCSV",
};
private final Server server;
private final RequestStats stats;
private final MaxGauge busyThreads = new MaxGauge();
private final RequestLog requestLog;
private final ClientAddressExtractor clientAddressExtractor;
private final HttpServerInfo httpServerInfo;
private final NodeInfo nodeInfo;
private final HttpServerConfig config;
private final Optional certificateExpiration;
private final HttpServerModuleOptions moduleOptions;
/**
* @deprecated Will no longer be public
*/
@Deprecated
public HttpServer(HttpServerInfo httpServerInfo,
NodeInfo nodeInfo,
HttpServerConfig config,
Servlet theServlet,
Map parameters,
Set filters,
Set resources,
@Nullable Servlet theAdminServlet,
@Nullable Map adminParameters,
@Nullable Set adminFilters,
@Nullable MBeanServer mbeanServer,
@Nullable LoginService loginService,
@Nullable SessionHandler sessionHandler,
QueryStringFilter queryStringFilter,
RequestStats stats,
DetailedRequestStats detailedRequestStats,
@Nullable RequestLog requestLog,
ClientAddressExtractor clientAddressExtractor)
{
this(httpServerInfo, nodeInfo, config, theServlet, parameters,
filters, resources, theAdminServlet, adminParameters, adminFilters, mbeanServer,
loginService, sessionHandler, queryStringFilter, stats, detailedRequestStats,
requestLog, clientAddressExtractor, new HttpServerModuleOptions());
}
public HttpServer(HttpServerInfo httpServerInfo,
NodeInfo nodeInfo,
HttpServerConfig config,
Servlet theServlet,
Map parameters,
Set filters,
Set resources,
@Nullable Servlet theAdminServlet,
@Nullable Map adminParameters,
@Nullable Set adminFilters,
@Nullable MBeanServer mbeanServer,
@Nullable LoginService loginService,
@Nullable SessionHandler sessionHandler,
QueryStringFilter queryStringFilter,
RequestStats stats,
DetailedRequestStats detailedRequestStats,
@Nullable RequestLog requestLog,
ClientAddressExtractor clientAddressExtractor,
HttpServerModuleOptions moduleOptions)
{
this.httpServerInfo = requireNonNull(httpServerInfo, "httpServerInfo is null");
this.nodeInfo = requireNonNull(nodeInfo, "nodeInfo is null");
this.config = requireNonNull(config, "config is null");
requireNonNull(theServlet, "theServlet is null");
requireNonNull(parameters, "parameters is null");
requireNonNull(filters, "filters is null");
requireNonNull(resources, "resources is null");
requireNonNull(queryStringFilter, "queryStringFilter is null");
this.stats = requireNonNull(stats, "stats is null");
requireNonNull(detailedRequestStats, "detailedRequestStats is null");
this.requestLog = requestLog;
this.clientAddressExtractor = requireNonNull(clientAddressExtractor, "clientAddressExtractor is null");
this.moduleOptions = requireNonNull(moduleOptions, "httpServerModuleOptions is null");
QueuedThreadPool threadPool = new QueuedThreadPool(config.getMaxThreads())
{
@Override
protected void runJob(Runnable job)
{
try {
busyThreads.add(1);
super.runJob(job);
}
finally {
busyThreads.add(-1);
}
}
};
threadPool.setMinThreads(config.getMinThreads());
threadPool.setIdleTimeout(toIntExact(config.getThreadMaxIdleTime().toMillis()));
threadPool.setName("http-worker");
if (moduleOptions.isEnableVirtualThreads()) {
Executor executor = getNamedVirtualThreadsExecutor("http-worker#v");
verify(executor != null, "Could not create virtual threads executor");
threadPool.setVirtualThreadsExecutor(executor);
}
server = new Server(threadPool);
server.setStopTimeout(config.getStopTimeout().toMillis());
boolean showStackTrace = config.isShowStackTrace();
if (mbeanServer != null) {
// export jmx mbeans if a server was provided
MBeanContainer mbeanContainer = new MBeanContainer(mbeanServer);
server.addBean(mbeanContainer);
}
// register a channel listener if logging is enabled
/*
* structure is:
*
* server
* |- request logging handler (optional)
* |--- statistics handler
* |--- context handler
* | |--- (no) admin filter
* | |--- timing filter
* | |--- query string filter
* | |--- trace token filter
* | |--- gzip request filter
* | |--- security handler
* | |--- resource filters
* | |--- user provided filters
* | |--- gzip response filter
* | |--- the servlet (normally GuiceContainer)
* | |--- session handler (optional)
* |--- log handler
* |-- admin context handler
* |--- timing filter
* |--- query string filter
* |--- trace token filter
* |--- gzip request filter
* |--- security handler
* |--- resource filters
* |--- user provided admin filters
* |--- gzip response filter
* \--- the servlet
*/
// add handlers to Jetty
StatisticsHandler statsHandler = new StatisticsHandler();
statsHandler.setHandler(createServletContext(theServlet, resources, parameters, false, filters, queryStringFilter, loginService, nodeInfo, sessionHandler, Set.of("http", "https"), showStackTrace));
ContextHandlerCollection rootHandlers = new ContextHandlerCollection();
if (theAdminServlet != null && config.isAdminEnabled()) {
rootHandlers.addHandler(createServletContext(theAdminServlet, resources, adminParameters, true, adminFilters, queryStringFilter, loginService, nodeInfo, null, Set.of("admin"), showStackTrace));
}
rootHandlers.addHandler(statsHandler);
DispatchingRequestLogHandler dispatchingHandler = new DispatchingRequestLogHandler(requestLog, stats, detailedRequestStats, clientAddressExtractor);
EventsHandler eventsHandler = new RequestTimingEventHandler(rootHandlers);
server.setRequestLog(dispatchingHandler);
server.setHandler(eventsHandler);
ErrorHandler errorHandler = new ErrorHandler();
errorHandler.setShowMessageInTitle(showStackTrace);
errorHandler.setShowCauses(showStackTrace);
errorHandler.setShowStacks(showStackTrace);
errorHandler.setDefaultResponseMimeType(TEXT_PLAIN.asString());
server.setErrorHandler(errorHandler);
certificateExpiration = loadAllX509Certificates(config).stream()
.map(X509Certificate::getNotAfter)
.min(naturalOrder())
.map(date -> ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()));
}
// Protect against finalizer attacks, as constructor can throw exception.
@SuppressWarnings("deprecation")
@Override
protected final void finalize()
{
}
private ServerConnector createHttpsServerConnector(HttpServerConfig config, ServerSocketChannel serverSocketChannel, HttpConfiguration configuration, Executor threadPool, int acceptors, int selectors)
throws IOException
{
SecureRequestCustomizer customizer = new SecureRequestCustomizer();
customizer.setSniHostCheck(false);
customizer.setSniRequired(false);
configuration.addCustomizer(customizer);
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setEndpointIdentificationAlgorithm("HTTPS");
sslContextFactory.setKeyStorePath(config.getKeystorePath());
sslContextFactory.setKeyStorePassword(config.getKeystorePassword());
sslContextFactory.setExcludeProtocols();
sslContextFactory.setIncludeProtocols(ENABLED_PROTOCOLS);
sslContextFactory.setExcludeCipherSuites();
sslContextFactory.setIncludeCipherSuites(ENABLED_CIPHERS);
sslContextFactory.setCipherComparator(Ordering.explicit("", ENABLED_CIPHERS));
sslContextFactory.setSslSessionTimeout((int) config.getSslSessionTimeout().getValue(SECONDS));
sslContextFactory.setSslSessionCacheSize(config.getSslSessionCacheSize());
sslContextFactory.setSniRequired(false);
sslContextFactory.setRenegotiationAllowed(false);
List connectionFactories = new ArrayList<>();
connectionFactories.add(new SslConnectionFactory(sslContextFactory, "alpn"));
connectionFactories.add(new HttpConnectionFactory(configuration));
connectionFactories.add(new ALPNServerConnectionFactory("h2", "http/1.1"));
HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(configuration);
http2.setInitialSessionRecvWindow(toIntExact(config.getHttp2InitialSessionReceiveWindowSize().toBytes()));
http2.setInitialStreamRecvWindow(toIntExact(config.getHttp2InitialStreamReceiveWindowSize().toBytes()));
http2.setMaxConcurrentStreams(config.getHttp2MaxConcurrentStreams());
http2.setInputBufferSize(toIntExact(config.getHttp2InputBufferSize().toBytes()));
http2.setStreamIdleTimeout(config.getHttp2StreamIdleTimeout().toMillis());
connectionFactories.add(http2);
return createServerConnector(
serverSocketChannel,
server,
threadPool,
acceptors,
selectors,
connectionFactories.toArray(new ConnectionFactory[0]));
}
private ServletContextHandler createServletContext(Servlet theServlet,
Set resources,
Map parameters,
boolean isAdmin,
Set filters,
QueryStringFilter queryStringFilter,
LoginService loginService,
NodeInfo nodeInfo,
SessionHandler sessionHandler,
Set connectorNames,
boolean showStackTrace)
{
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
if (moduleOptions.isAllowAmbiguousUris()) {
ServletHandler servletHandler = new ServletHandler();
servletHandler.setDecodeAmbiguousURIs(true);
context.setServletHandler(servletHandler);
}
ErrorHandler errorHandler = new ErrorHandler();
errorHandler.setShowCauses(showStackTrace);
errorHandler.setShowStacks(showStackTrace);
errorHandler.setShowMessageInTitle(showStackTrace);
errorHandler.setDefaultResponseMimeType(TEXT_PLAIN.asString());
context.setErrorHandler(errorHandler);
if (!isAdmin) {
// Filter out any /admin JAX-RS resources that were implicitly bound.
// May be removed once we require explicit JAX-RS binding.
context.addFilter(new FilterHolder(new AdminFilter(false)), "/*", null);
}
context.addFilter(new FilterHolder(queryStringFilter), "/*", null);
context.addFilter(new FilterHolder(new TraceTokenFilter(nodeInfo.getInternalIp(), clientAddressExtractor)), "/*", null);
// -- gzip request filter
context.addFilter(GZipRequestFilter.class, "/*", null);
// -- security handler
if (loginService != null) {
SecurityHandler securityHandler = createSecurityHandler(loginService);
context.setSecurityHandler(securityHandler);
}
// -- user provided filters
for (Filter filter : filters) {
context.addFilter(new FilterHolder(filter), "/*", null);
}
// -- static resources
for (HttpResourceBinding resource : resources) {
ClassPathResourceFilter servlet = new ClassPathResourceFilter(
resource.getBaseUri(),
resource.getClassPathResourceBase(),
resource.getWelcomeFiles());
String pathSpec = servlet.getBaseUri() + "/*";
if (pathSpec.equals("//*")) {
pathSpec = "/*";
}
context.addFilter(new FilterHolder(servlet), pathSpec, null);
}
// -- gzip handler
context.insertHandler(new GzipHandler());
// -- add SessionHandler
if (sessionHandler != null) {
context.setSessionHandler(sessionHandler);
}
// -- the servlet
ServletHolder servletHolder = new ServletHolder(theServlet);
servletHolder.setInitParameters(ImmutableMap.copyOf(parameters));
context.addServlet(servletHolder, "/*");
// Starting with Jetty 9 there is no way to specify connectors directly, but
// there is this wonky @ConnectorName virtual hosts automatically added
List virtualHosts = connectorNames.stream()
.map(connectorName -> "@" + connectorName)
.collect(toImmutableList());
context.setVirtualHosts(virtualHosts);
return context;
}
private static SecurityHandler createSecurityHandler(LoginService loginService)
{
ConstraintMapping constraintMapping = new ConstraintMapping();
constraintMapping.setConstraint(ALLOWED);
constraintMapping.setPathSpec("/*");
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setLoginService(loginService);
// TODO: support for other auth schemes (digest, etc)
securityHandler.setAuthenticator(new BasicAuthenticator());
securityHandler.setConstraintMappings(List.of(constraintMapping));
return securityHandler;
}
@Managed
public Long getDaysUntilCertificateExpiration()
{
return certificateExpiration.map(date -> ZonedDateTime.now().until(date, DAYS))
.orElse(null);
}
@AcceptRequests
public void start()
throws Exception
{
HttpConfiguration baseHttpConfiguration = new HttpConfiguration();
baseHttpConfiguration.setSendServerVersion(false);
baseHttpConfiguration.setSendXPoweredBy(false);
baseHttpConfiguration.setNotifyRemoteAsyncErrors(true); // Pass remote exceptions to AsyncContext
// Adds :authority pseudoheader on HTTP/2
baseHttpConfiguration.addCustomizer(new AuthorityCustomizer());
// Adds :host header on HTTP/1.0 and HTTP/2
baseHttpConfiguration.addCustomizer(new HostHeaderCustomizer());
if (config.getMaxRequestHeaderSize() != null) {
baseHttpConfiguration.setRequestHeaderSize(toIntExact(config.getMaxRequestHeaderSize().toBytes()));
}
if (moduleOptions.isAllowAmbiguousUris()) {
baseHttpConfiguration.setUriCompliance(UriCompliance.from(Set.of(
UriCompliance.Violation.AMBIGUOUS_EMPTY_SEGMENT,
UriCompliance.Violation.AMBIGUOUS_PATH_ENCODING,
UriCompliance.Violation.AMBIGUOUS_PATH_PARAMETER,
UriCompliance.Violation.AMBIGUOUS_PATH_SEGMENT,
UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR)));
}
// set up HTTP connector
ServerConnector httpConnector;
if (config.isHttpEnabled()) {
HttpConfiguration httpConfiguration = new HttpConfiguration(baseHttpConfiguration);
// if https is enabled, set the CONFIDENTIAL and INTEGRAL redirection information
if (config.isHttpsEnabled()) {
httpConfiguration.setSecureScheme("https");
httpConfiguration.setSecurePort(httpServerInfo.getHttpsUri().getPort());
}
Integer acceptors = config.getHttpAcceptorThreads();
Integer selectors = config.getHttpSelectorThreads();
HttpConnectionFactory http1 = new HttpConnectionFactory(httpConfiguration);
HTTP2CServerConnectionFactory http2c = new HTTP2CServerConnectionFactory(httpConfiguration);
http2c.setInitialSessionRecvWindow(toIntExact(config.getHttp2InitialSessionReceiveWindowSize().toBytes()));
http2c.setInitialStreamRecvWindow(toIntExact(config.getHttp2InitialStreamReceiveWindowSize().toBytes()));
http2c.setMaxConcurrentStreams(config.getHttp2MaxConcurrentStreams());
http2c.setInputBufferSize(toIntExact(config.getHttp2InputBufferSize().toBytes()));
http2c.setStreamIdleTimeout(config.getHttp2StreamIdleTimeout().toMillis());
httpConnector = createServerConnector(
httpServerInfo.getHttpChannel(),
server,
null,
requireNonNullElse(acceptors, -1),
requireNonNullElse(selectors, -1),
http1,
http2c);
httpConnector.setName("http");
httpConnector.setPort(httpServerInfo.getHttpUri().getPort());
httpConnector.setIdleTimeout(config.getNetworkMaxIdleTime().toMillis());
httpConnector.setHost(nodeInfo.getBindIp().getHostAddress());
httpConnector.setAcceptQueueSize(config.getHttpAcceptQueueSize());
server.addConnector(httpConnector);
}
// set up NIO-based HTTPS connector
ServerConnector httpsConnector;
if (config.isHttpsEnabled()) {
HttpConfiguration httpsConfiguration = new HttpConfiguration(baseHttpConfiguration);
Integer acceptors = config.getHttpsAcceptorThreads();
Integer selectors = config.getHttpsSelectorThreads();
httpsConnector = createHttpsServerConnector(
config,
httpServerInfo.getHttpsChannel(),
httpsConfiguration,
null,
requireNonNullElse(acceptors, -1),
requireNonNullElse(selectors, -1));
httpsConnector.setName("https");
httpsConnector.setPort(httpServerInfo.getHttpsUri().getPort());
httpsConnector.setIdleTimeout(config.getNetworkMaxIdleTime().toMillis());
httpsConnector.setHost(nodeInfo.getBindIp().getHostAddress());
httpsConnector.setAcceptQueueSize(config.getHttpAcceptQueueSize());
server.addConnector(httpsConnector);
}
// set up NIO-based Admin connector
ServerConnector adminConnector;
if (config.isAdminEnabled()) {
HttpConfiguration adminConfiguration = new HttpConfiguration(baseHttpConfiguration);
QueuedThreadPool adminThreadPool = new QueuedThreadPool(config.getAdminMaxThreads());
adminThreadPool.setName("http-admin-worker");
adminThreadPool.setMinThreads(config.getAdminMinThreads());
adminThreadPool.setIdleTimeout(toIntExact(config.getThreadMaxIdleTime().toMillis()));
if (moduleOptions.isEnableVirtualThreads()) {
Executor executor = getNamedVirtualThreadsExecutor("http-admin-worker#v");
if (executor != null) {
adminThreadPool.setVirtualThreadsExecutor(executor);
}
}
if (config.isHttpsEnabled()) {
adminConnector = createHttpsServerConnector(
config,
httpServerInfo.getAdminChannel(),
adminConfiguration,
adminThreadPool,
0,
-1);
}
else {
HttpConnectionFactory http1 = new HttpConnectionFactory(adminConfiguration);
HTTP2CServerConnectionFactory http2c = new HTTP2CServerConnectionFactory(adminConfiguration);
http2c.setMaxConcurrentStreams(config.getHttp2MaxConcurrentStreams());
adminConnector = createServerConnector(
httpServerInfo.getAdminChannel(),
server,
adminThreadPool,
-1,
-1,
http1,
http2c);
}
adminConnector.setName("admin");
adminConnector.setPort(httpServerInfo.getAdminUri().getPort());
adminConnector.setIdleTimeout(config.getNetworkMaxIdleTime().toMillis());
adminConnector.setHost(nodeInfo.getBindIp().getHostAddress());
adminConnector.setAcceptQueueSize(config.getHttpAcceptQueueSize());
server.addConnector(adminConnector);
}
server.start();
checkState(server.isStarted(), "server is not started");
}
@PreDestroy
public void stop()
throws Exception
{
server.stop();
if (requestLog != null) {
requestLog.stop();
}
}
@Flatten
public RequestStats getStats()
{
return stats;
}
@Nested
public MaxGauge getBusyThreads()
{
return busyThreads;
}
private static Set loadAllX509Certificates(HttpServerConfig config)
{
ImmutableSet.Builder certificates = ImmutableSet.builder();
if (config.isHttpsEnabled()) {
try (InputStream keystoreInputStream = new FileInputStream(config.getKeystorePath())) {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(keystoreInputStream, config.getKeystorePassword().toCharArray());
for (String alias : list(keystore.aliases())) {
try {
Certificate certificate = keystore.getCertificate(alias);
if (certificate instanceof X509Certificate x509Certificate) {
certificates.add(x509Certificate);
}
}
catch (KeyStoreException ignored) {
}
}
}
catch (IOException | CertificateException | NoSuchAlgorithmException | KeyStoreException ignored) {
}
}
return certificates.build();
}
private static ServerConnector createServerConnector(
ServerSocketChannel channel,
Server server,
Executor executor,
int acceptors,
int selectors,
ConnectionFactory... factories)
throws IOException
{
ServerConnector connector = new ServerConnector(server, executor, null, null, acceptors, selectors, factories);
connector.open(channel);
return connector;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy