dropwizard/dropwizard

View on GitHub
dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpConnectorFactory.java

Summary

Maintainability
D
1 day
Test Coverage
package io.dropwizard.jetty;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.jetty9.InstrumentedConnectionFactory;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.dropwizard.util.DataSize;
import io.dropwizard.util.DataSizeUnit;
import io.dropwizard.util.Duration;
import io.dropwizard.validation.MinDataSize;
import io.dropwizard.validation.MinDuration;
import io.dropwizard.validation.PortRange;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.ProxyConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.ThreadPool;

import javax.annotation.Nullable;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.valueextraction.Unwrapping;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static com.codahale.metrics.MetricRegistry.name;

/**
 * Builds HTTP connectors.
 *
 * <p/>
 * <b>Configuration Parameters:</b>
 * <table>
 *     <tr>
 *         <td>Name</td>
 *         <td>Default</td>
 *         <td>Description</td>
 *     </tr>
 *     <tr>
 *         <td>{@code port}</td>
 *         <td>8080</td>
 *         <td>The TCP/IP port on which to listen for incoming connections.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code bindHost}</td>
 *         <td>(none)</td>
 *         <td>The hostname to bind to.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code inheritChannel}</td>
 *         <td>false</td>
 *         <td>
 *             Whether this connector uses a channel inherited from the JVM.
 *             Use it with <a href="https://github.com/kazuho/p5-Server-Starter">Server::Starter</a>,
 *             to launch an instance of Jetty on demand.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code headerCacheSize}</td>
 *         <td>512 bytes</td>
 *         <td>The size of the header field cache.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code outputBufferSize}</td>
 *         <td>32KiB</td>
 *         <td>
 *             The size of the buffer into which response content is aggregated before being sent to
 *             the client.  A larger buffer can improve performance by allowing a content producer
 *             to run without blocking, however larger buffers consume more memory and may induce
 *             some latency before a client starts processing the content.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code maxRequestHeaderSize}</td>
 *         <td>8KiB</td>
 *         <td>
 *             The maximum allowed size in bytes for the HTTP request line and HTTP request headers.
 *             Larger headers will allow for more and/or larger cookies plus larger form content
 *             encoded in a URL. However, larger headers consume more memory and can make a server
 *             more vulnerable to denial of service attacks.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code maxResponseHeaderSize}</td>
 *         <td>8KiB</td>
 *         <td>
 *             The maximum size of a response header. Larger headers will allow for more and/or
 *             larger cookies and longer HTTP headers (eg for redirection).  However, larger headers
 *             will also consume more memory.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code inputBufferSize}</td>
 *         <td>8KiB</td>
 *         <td>The size of the per-connection input buffer.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code idleTimeout}</td>
 *         <td>30 seconds</td>
 *         <td>
 *             The maximum idle time for a connection, which roughly translates to the
 *             {@link java.net.Socket#setSoTimeout(int)} call, although with NIO implementations
 *             other mechanisms may be used to implement the timeout.
 *             <p/>
 *             The max idle time is applied:
 *             <ul>
 *                 <li>When waiting for a new message to be received on a connection</li>
 *                 <li>When waiting for a new message to be sent on a connection</li>
 *             </ul>
 *             <p/>
 *             This value is interpreted as the maximum time between some progress being made on the
 *             connection. So if a single byte is read or written, then the timeout is reset.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code minBufferPoolSize}</td>
 *         <td>64 bytes</td>
 *         <td>The minimum size of the buffer pool.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code bufferPoolIncrement}</td>
 *         <td>1KiB</td>
 *         <td>The increment by which the buffer pool should be increased.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code maxBufferPoolSize}</td>
 *         <td>64KiB</td>
 *         <td>The maximum size of the buffer pool.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code acceptorThreads}</td>
 *         <td>(Jetty's default)</td>
 *         <td>The number of worker threads dedicated to accepting connections.
 *         By default is <i>max</i>(1, <i>min</i>(4, #CPUs/8)).</td>
 *     </tr>
 *     <tr>
 *         <td>{@code selectorThreads}</td>
 *         <td>(Jetty's default)</td>
 *         <td>The number of worker threads dedicated to sending and receiving data.
 *         By default is <i>max</i>(1, <i>min</i>(4, #CPUs/2)).</td>
 *     </tr>
 *     <tr>
 *         <td>{@code acceptQueueSize}</td>
 *         <td>(OS default)</td>
 *         <td>The size of the TCP/IP accept queue for the listening socket.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code reuseAddress}</td>
 *         <td>true</td>
 *         <td>Whether or not {@code SO_REUSEADDR} is enabled on the listening socket.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code useServerHeader}</td>
 *         <td>false</td>
 *         <td>Whether or not to add the {@code Server} header to each response.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code useDateHeader}</td>
 *         <td>true</td>
 *         <td>Whether or not to add the {@code Date} header to each response.</td>
 *     </tr>
 *     <tr>
 *         <td>{@code minResponseDataPerSecond}</td>
 *         <td>0 bytes</td>
 *         <td>
 *             The minimum response data rate in bytes per second; or &lt;=0 for no limit
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code minRequestDataPerSecond}</td>
 *         <td>0 bytes</td>
 *         <td>
 *             The minimum request data rate in bytes per second; or &lt;=0 for no limit
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code useForwardedHeaders}</td>
 *         <td>false</td>
 *         <td>
 *             Whether or not to look at {@code X-Forwarded-*} headers added by proxies. See
 *             {@link ForwardedRequestCustomizer} for details.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code useProxyProtocol}</td>
 *         <td>false</td>
 *         <td>
 *             Enable jetty proxy protocol header support.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code httpCompliance}</td>
 *         <td>RFC7230</td>
 *         <td>
 *             This sets the http compliance level used by Jetty when parsing http, this can be useful when using a
 *             non-RFC7230 compliant front end, such as nginx, which can produce multi-line headers when forwarding
 *             client certificates using proxy_set_header X-SSL-CERT $ssl_client_cert;
 *
 *             Possible values are set forth in the org.eclipse.jetty.http.HttpCompliance enum:
 *             <ul>
 *                 <li>RFC7230: Disallow header folding.</li>
 *                 <li>RFC2616: Allow header folding.</li>
 *             </ul>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code requestCookieCompliance}</td>
 *         <td>RFC6265</td>
 *         <td>
 *             This sets the cookie compliance level used by Jetty when parsing request {@code Cookie} headers,
 *             this can be useful when needing to support Version=1 cookies defined in RFC2109 (and continued in
 *             RFC2965) which allows for special/reserved characters (control, separator, et al) to be enclosed within
 *             double quotes when used in a cookie value;
 *
 *             Possible values are set forth in the org.eclipse.jetty.http.CookieCompliance enum:
 *             <ul>
 *                 <li>RFC6265: Special characters in cookie values must be encoded.</li>
 *                 <li>RFC2965: Allows for special characters enclosed within double quotes.</li>
 *             </ul>
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code responseCookieCompliance}</td>
 *         <td>RFC6265</td>
 *         <td>
 *             This sets the cookie compliance level used by Jetty when generating response {@code Set-Cookie} headers,
 *             this can be useful when needing to support Version=1 cookies defined in RFC2109 (and continued in
 *             RFC2965) which allows for special/reserved characters (control, separator, et al) to be enclosed within
 *             double quotes when used in a cookie value;
 *
 *             Possible values are set forth in the org.eclipse.jetty.http.CookieCompliance enum:
 *             <ul>
 *                 <li>RFC6265: Special characters in cookie values must be encoded.</li>
 *                 <li>RFC2965: Allows for special characters enclosed within double quotes.</li>
 *             </ul>
 *         </td>
 *     </tr>
 * </table>
 */
@JsonTypeName("http")
public class HttpConnectorFactory implements ConnectorFactory {
    public static ConnectorFactory application() {
        final HttpConnectorFactory factory = new HttpConnectorFactory();
        factory.port = 8080;
        return factory;
    }

    public static ConnectorFactory admin() {
        final HttpConnectorFactory factory = new HttpConnectorFactory();
        factory.port = 8081;
        return factory;
    }

    @PortRange
    private int port = 8080;

    @Nullable
    private String bindHost;

    private boolean inheritChannel = false;

    @NotNull
    @MinDataSize(128)
    private DataSize headerCacheSize = DataSize.bytes(512);

    @NotNull
    @MinDataSize(value = 8, unit = DataSizeUnit.KIBIBYTES)
    private DataSize outputBufferSize = DataSize.kibibytes(32);

    @NotNull
    @MinDataSize(value = 1, unit = DataSizeUnit.KIBIBYTES)
    private DataSize maxRequestHeaderSize = DataSize.kibibytes(8);

    @NotNull
    @MinDataSize(value = 1, unit = DataSizeUnit.KIBIBYTES)
    private DataSize maxResponseHeaderSize = DataSize.kibibytes(8);

    @NotNull
    @MinDataSize(value = 1, unit = DataSizeUnit.KIBIBYTES)
    private DataSize inputBufferSize = DataSize.kibibytes(8);

    @NotNull
    @MinDuration(value = 1, unit = TimeUnit.MILLISECONDS)
    private Duration idleTimeout = Duration.seconds(30);

    @NotNull
    @MinDataSize(0)
    private DataSize minResponseDataPerSecond = DataSize.bytes(0);

    @NotNull
    @MinDataSize(0)
    private DataSize minRequestDataPerSecond = DataSize.bytes(0);

    @NotNull
    @MinDataSize(value = 1, unit = DataSizeUnit.BYTES)
    private DataSize minBufferPoolSize = DataSize.bytes(64);

    @NotNull
    @MinDataSize(value = 1, unit = DataSizeUnit.BYTES)
    private DataSize bufferPoolIncrement = DataSize.bytes(1024);

    @NotNull
    @MinDataSize(value = 1, unit = DataSizeUnit.BYTES)
    private DataSize maxBufferPoolSize = DataSize.kibibytes(64);

    @Min(value = 1, payload = Unwrapping.Unwrap.class)
    private Optional<Integer> acceptorThreads = Optional.empty();

    @Min(value = 1, payload = Unwrapping.Unwrap.class)
    private Optional<Integer> selectorThreads = Optional.empty();

    @Min(0)
    @Nullable
    private Integer acceptQueueSize;

    private boolean reuseAddress = true;

    private boolean useServerHeader = false;
    private boolean useDateHeader = true;
    private boolean useForwardedHeaders = false;
    private boolean useProxyProtocol = false;
    private HttpCompliance httpCompliance = HttpCompliance.RFC7230;
    private CookieCompliance requestCookieCompliance = CookieCompliance.RFC6265;
    private CookieCompliance responseCookieCompliance = CookieCompliance.RFC6265;

    @JsonProperty
    public int getPort() {
        return port;
    }

    @JsonProperty
    public void setPort(int port) {
        this.port = port;
    }

    @JsonProperty
    @Nullable
    public String getBindHost() {
        return bindHost;
    }

    @JsonProperty
    public void setBindHost(String bindHost) {
        this.bindHost = bindHost;
    }

    @JsonProperty
    public boolean isInheritChannel() {
        return inheritChannel;
    }

    @JsonProperty
    public void setInheritChannel(boolean inheritChannel) {
        this.inheritChannel = inheritChannel;
    }

    @JsonProperty
    public DataSize getHeaderCacheSize() {
        return headerCacheSize;
    }

    @JsonProperty
    public void setHeaderCacheSize(DataSize headerCacheSize) {
        this.headerCacheSize = headerCacheSize;
    }

    @JsonProperty
    public DataSize getOutputBufferSize() {
        return outputBufferSize;
    }

    @JsonProperty
    public void setOutputBufferSize(DataSize outputBufferSize) {
        this.outputBufferSize = outputBufferSize;
    }

    @JsonProperty
    public DataSize getMaxRequestHeaderSize() {
        return maxRequestHeaderSize;
    }

    @JsonProperty
    public void setMaxRequestHeaderSize(DataSize maxRequestHeaderSize) {
        this.maxRequestHeaderSize = maxRequestHeaderSize;
    }

    @JsonProperty
    public DataSize getMaxResponseHeaderSize() {
        return maxResponseHeaderSize;
    }

    @JsonProperty
    public void setMaxResponseHeaderSize(DataSize maxResponseHeaderSize) {
        this.maxResponseHeaderSize = maxResponseHeaderSize;
    }

    @JsonProperty
    public DataSize getInputBufferSize() {
        return inputBufferSize;
    }

    @JsonProperty
    public void setInputBufferSize(DataSize inputBufferSize) {
        this.inputBufferSize = inputBufferSize;
    }

    @JsonProperty
    public Duration getIdleTimeout() {
        return idleTimeout;
    }

    @JsonProperty
    public void setIdleTimeout(Duration idleTimeout) {
        this.idleTimeout = idleTimeout;
    }

    @JsonProperty
    public DataSize getMinBufferPoolSize() {
        return minBufferPoolSize;
    }

    @JsonProperty
    public void setMinBufferPoolSize(DataSize minBufferPoolSize) {
        this.minBufferPoolSize = minBufferPoolSize;
    }

    @JsonProperty
    public DataSize getBufferPoolIncrement() {
        return bufferPoolIncrement;
    }

    @JsonProperty
    public void setBufferPoolIncrement(DataSize bufferPoolIncrement) {
        this.bufferPoolIncrement = bufferPoolIncrement;
    }

    @JsonProperty
    public DataSize getMaxBufferPoolSize() {
        return maxBufferPoolSize;
    }

    @JsonProperty
    public void setMaxBufferPoolSize(DataSize maxBufferPoolSize) {
        this.maxBufferPoolSize = maxBufferPoolSize;
    }

    @JsonProperty
    public DataSize getMinResponseDataPerSecond() {
        return minResponseDataPerSecond;
    }

    @JsonProperty
    public void setMinResponseDataPerSecond(DataSize minResponseDataPerSecond) {
        this.minResponseDataPerSecond = minResponseDataPerSecond;
    }

    @JsonProperty
    public DataSize getMinRequestDataPerSecond() {
        return minRequestDataPerSecond;
    }

    @JsonProperty
    public void setMinRequestDataPerSecond(DataSize minRequestDataPerSecond) {
        this.minRequestDataPerSecond = minRequestDataPerSecond;
    }

    @JsonProperty
    public Optional<Integer> getAcceptorThreads() {
        return acceptorThreads;
    }

    @JsonProperty
    public void setAcceptorThreads(Optional<Integer> acceptorThreads) {
        this.acceptorThreads = acceptorThreads;
    }

    @JsonProperty
    public Optional<Integer> getSelectorThreads() {
        return selectorThreads;
    }

    @JsonProperty
    public void setSelectorThreads(Optional<Integer> selectorThreads) {
        this.selectorThreads = selectorThreads;
    }

    @JsonProperty
    @Nullable
    public Integer getAcceptQueueSize() {
        return acceptQueueSize;
    }

    @JsonProperty
    public void setAcceptQueueSize(Integer acceptQueueSize) {
        this.acceptQueueSize = acceptQueueSize;
    }

    @JsonProperty
    public boolean isReuseAddress() {
        return reuseAddress;
    }

    @JsonProperty
    public void setReuseAddress(boolean reuseAddress) {
        this.reuseAddress = reuseAddress;
    }

    @JsonProperty
    public boolean isUseServerHeader() {
        return useServerHeader;
    }

    @JsonProperty
    public void setUseServerHeader(boolean useServerHeader) {
        this.useServerHeader = useServerHeader;
    }

    @JsonProperty
    public boolean isUseDateHeader() {
        return useDateHeader;
    }

    @JsonProperty
    public void setUseDateHeader(boolean useDateHeader) {
        this.useDateHeader = useDateHeader;
    }

    @JsonProperty
    public boolean isUseForwardedHeaders() {
        return useForwardedHeaders;
    }

    @JsonProperty
    public void setUseForwardedHeaders(boolean useForwardedHeaders) {
        this.useForwardedHeaders = useForwardedHeaders;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public boolean isUseProxyProtocol() {
        return useProxyProtocol;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public void setUseProxyProtocol(boolean useProxyProtocol) {
        this.useProxyProtocol = useProxyProtocol;
    }

    @JsonProperty
    public HttpCompliance getHttpCompliance() {
        return httpCompliance;
    }

    @JsonProperty
    public void setHttpCompliance(HttpCompliance httpCompliance) {
        this.httpCompliance = httpCompliance;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public CookieCompliance getRequestCookieCompliance() {
        return requestCookieCompliance;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public void setRequestCookieCompliance(CookieCompliance requestCookieCompliance) {
        this.requestCookieCompliance = requestCookieCompliance;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public CookieCompliance getResponseCookieCompliance() {
        return responseCookieCompliance;
    }

    /**
     * @since 2.0
     */
    @JsonProperty
    public void setResponseCookieCompliance(CookieCompliance responseCookieCompliance) {
        this.responseCookieCompliance = responseCookieCompliance;
    }


    @Override
    public Connector build(Server server,
                           MetricRegistry metrics,
                           String name,
                           @Nullable ThreadPool threadPool) {
        final HttpConfiguration httpConfig = buildHttpConfiguration();

        final HttpConnectionFactory httpConnectionFactory = buildHttpConnectionFactory(httpConfig);

        final Scheduler scheduler = new ScheduledExecutorScheduler();

        final ByteBufferPool bufferPool = buildBufferPool();

        return buildConnector(server, scheduler, bufferPool, name, threadPool,
                              new InstrumentedConnectionFactory(httpConnectionFactory,
                                                                metrics.timer(httpConnections())));
    }

    /**
     * Get name of the timer that tracks incoming HTTP connections
     */
    protected String httpConnections() {
        return name(HttpConnectionFactory.class,  bindHost, Integer.toString(port), "connections");
    }

    protected ServerConnector buildConnector(Server server,
                                             Scheduler scheduler,
                                             ByteBufferPool bufferPool,
                                             String name,
                                             @Nullable ThreadPool threadPool,
                                             ConnectionFactory... factories) {
        if (useProxyProtocol) {
            factories = ArrayUtil.prependToArray(new ProxyConnectionFactory(), factories, ConnectorFactory.class);
        }

        final ServerConnector connector = new ServerConnector(server,
                                                              threadPool,
                                                              scheduler,
                                                              bufferPool,
                                                              acceptorThreads.orElse(-1),
                                                              selectorThreads.orElse(-1),
                                                              factories);
        connector.setPort(port);
        connector.setHost(bindHost);
        connector.setInheritChannel(inheritChannel);
        if (acceptQueueSize != null) {
            connector.setAcceptQueueSize(acceptQueueSize);
        } else {
            // if we do not set the acceptQueueSize, when jetty
            // creates the ServerSocket, it uses the default backlog of 50, and
            // not the value from the OS.  Therefore we set to the value
            // obtained from NetUtil, which will attempt to read the value from the OS.
            // somaxconn setting
            connector.setAcceptQueueSize(NetUtil.getTcpBacklog());
        }

        connector.setReuseAddress(reuseAddress);
        connector.setIdleTimeout(idleTimeout.toMilliseconds());
        connector.setName(name);

        return connector;
    }

    protected HttpConnectionFactory buildHttpConnectionFactory(HttpConfiguration httpConfig) {
        final HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig, httpCompliance);
        httpConnectionFactory.setInputBufferSize((int) inputBufferSize.toBytes());
        return httpConnectionFactory;
    }

    protected HttpConfiguration buildHttpConfiguration() {
        final HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setHeaderCacheSize((int) headerCacheSize.toBytes());
        httpConfig.setOutputBufferSize((int) outputBufferSize.toBytes());
        httpConfig.setRequestHeaderSize((int) maxRequestHeaderSize.toBytes());
        httpConfig.setResponseHeaderSize((int) maxResponseHeaderSize.toBytes());
        httpConfig.setSendDateHeader(useDateHeader);
        httpConfig.setSendServerVersion(useServerHeader);
        httpConfig.setMinResponseDataRate(minResponseDataPerSecond.toBytes());
        httpConfig.setMinRequestDataRate(minRequestDataPerSecond.toBytes());
        httpConfig.setRequestCookieCompliance(requestCookieCompliance);
        httpConfig.setResponseCookieCompliance(responseCookieCompliance);

        if (useForwardedHeaders) {
            httpConfig.addCustomizer(new ForwardedRequestCustomizer());
        }

        return httpConfig;
    }

    protected ByteBufferPool buildBufferPool() {
        return buildBufferPool((int) minBufferPoolSize.toBytes(),
                               (int) bufferPoolIncrement.toBytes(),
                               (int) maxBufferPoolSize.toBytes());
    }

    // This method only exists so that mockito can spy on the constructor parameters.
    ByteBufferPool buildBufferPool(int minCapacity, int factor, int maxCapacity) {
        return new ArrayByteBufferPool(minCapacity, factor, maxCapacity);
    }
}