/* * Copyright 2015 The Netty Project * * The Netty Project 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 io.netty.handler.codec.http2; import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector; import io.netty.util.internal.UnstableApi; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_RESERVED_STREAMS; import static io.netty.util.internal.ObjectUtil.checkNotNull; import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; /** * Abstract base class which defines commonly used features required to build {@link Http2ConnectionHandler} instances. * * <h3>Three ways to build a {@link Http2ConnectionHandler}</h3> * <h4>Let the builder create a {@link Http2ConnectionHandler}</h4> * Simply call all the necessary setter methods, and then use {@link #build()} to build a new * {@link Http2ConnectionHandler}. Setting the following properties are prohibited because they are used for * other ways of building a {@link Http2ConnectionHandler}. * conflicts with this option: * <ul> * <li>{@link #connection(Http2Connection)}</li> * <li>{@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)}</li> * </ul> * * * <h4>Let the builder use the {@link Http2ConnectionHandler} you specified</h4> * Call {@link #connection(Http2Connection)} to tell the builder that you want to build the handler from the * {@link Http2Connection} you specified. Setting the following properties are prohibited and thus will trigger * an {@link IllegalStateException} because they conflict with this option. * <ul> * <li>{@link #server(boolean)}</li> * <li>{@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)}</li> * </ul> * * <h4>Let the builder use the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} you specified</h4> * Call {@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)} to tell the builder that you want to built the * handler from the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} you specified. Setting the * following properties are prohibited and thus will trigger an {@link IllegalStateException} because they conflict * with this option: * <ul> * <li>{@link #server(boolean)}</li> * <li>{@link #connection(Http2Connection)}</li> * <li>{@link #frameLogger(Http2FrameLogger)}</li> * <li>{@link #headerSensitivityDetector(SensitivityDetector)}</li> * <li>{@link #encoderEnforceMaxConcurrentStreams(boolean)}</li> * <li>{@link #encoderIgnoreMaxHeaderListSize(boolean)}</li> * </ul> * * <h3>Exposing necessary methods in a subclass</h3> * {@link #build()} method and all property access methods are {@code protected}. Choose the methods to expose to the * users of your builder implementation and make them {@code public}. * * @param <T> The type of handler created by this builder. * @param <B> The concrete type of this builder. */ @UnstableApi public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2ConnectionHandler, B extends AbstractHttp2ConnectionHandlerBuilder<T, B>> { private static final long DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS = MILLISECONDS.convert(30, SECONDS); private static final SensitivityDetector DEFAULT_HEADER_SENSITIVITY_DETECTOR = Http2HeadersEncoder.NEVER_SENSITIVE; // The properties that can always be set. private Http2Settings initialSettings = Http2Settings.defaultSettings(); private Http2FrameListener frameListener; private long gracefulShutdownTimeoutMillis = DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS; // The property that will prohibit connection() and codec() if set by server(), // because this property is used only when this builder creates a Http2Connection. private Boolean isServer; private Integer maxReservedStreams; // The property that will prohibit server() and codec() if set by connection(). private Http2Connection connection; // The properties that will prohibit server() and connection() if set by codec(). private Http2ConnectionDecoder decoder; private Http2ConnectionEncoder encoder; // The properties that are: // * mutually exclusive against codec() and // * OK to use with server() and connection() private Boolean validateHeaders; private Http2FrameLogger frameLogger; private SensitivityDetector headerSensitivityDetector; private Boolean encoderEnforceMaxConcurrentStreams; private Boolean encoderIgnoreMaxHeaderListSize; /** * Sets the {@link Http2Settings} to use for the initial connection settings exchange. */ protected Http2Settings initialSettings() { return initialSettings; } /** * Sets the {@link Http2Settings} to use for the initial connection settings exchange. */ protected B initialSettings(Http2Settings settings) { initialSettings = checkNotNull(settings, "settings"); return self(); } /** * Returns the listener of inbound frames. * * @return {@link Http2FrameListener} if set, or {@code null} if not set. */ protected Http2FrameListener frameListener() { return frameListener; } /** * Sets the listener of inbound frames. * This listener will only be set if the decoder's listener is {@code null}. */ protected B frameListener(Http2FrameListener frameListener) { this.frameListener = checkNotNull(frameListener, "frameListener"); return self(); } /** * Returns the graceful shutdown timeout of the {@link Http2Connection} in milliseconds. */ protected long gracefulShutdownTimeoutMillis() { return gracefulShutdownTimeoutMillis; } /** * Sets the graceful shutdown timeout of the {@link Http2Connection} in milliseconds. */ protected B gracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) { this.gracefulShutdownTimeoutMillis = gracefulShutdownTimeoutMillis; return self(); } /** * Returns if {@link #build()} will to create a {@link Http2Connection} in server mode ({@code true}) * or client mode ({@code false}). */ protected boolean isServer() { return isServer != null ? isServer : true; } /** * Sets if {@link #build()} will to create a {@link Http2Connection} in server mode ({@code true}) * or client mode ({@code false}). */ protected B server(boolean isServer) { enforceConstraint("server", "connection", connection); enforceConstraint("server", "codec", decoder); enforceConstraint("server", "codec", encoder); this.isServer = isServer; return self(); } /** * Get the maximum number of streams which can be in the reserved state at any given time. * <p> * By default this value will be ignored on the server for local endpoint. This is because the RFC provides * no way to explicitly communicate a limit to how many states can be in the reserved state, and instead relies * on the peer to send RST_STREAM frames when they will be rejected. */ protected int maxReservedStreams() { return maxReservedStreams != null ? maxReservedStreams : DEFAULT_MAX_RESERVED_STREAMS; } /** * Set the maximum number of streams which can be in the reserved state at any given time. */ protected B maxReservedStreams(int maxReservedStreams) { enforceConstraint("server", "connection", connection); enforceConstraint("server", "codec", decoder); enforceConstraint("server", "codec", encoder); this.maxReservedStreams = checkPositiveOrZero(maxReservedStreams, "maxReservedStreams"); return self(); } /** * Returns the {@link Http2Connection} to use. * * @return {@link Http2Connection} if set, or {@code null} if not set. */ protected Http2Connection connection() { return connection; } /** * Sets the {@link Http2Connection} to use. */ protected B connection(Http2Connection connection) { enforceConstraint("connection", "maxReservedStreams", maxReservedStreams); enforceConstraint("connection", "server", isServer); enforceConstraint("connection", "codec", decoder); enforceConstraint("connection", "codec", encoder); this.connection = checkNotNull(connection, "connection"); return self(); } /** * Returns the {@link Http2ConnectionDecoder} to use. * * @return {@link Http2ConnectionDecoder} if set, or {@code null} if not set. */ protected Http2ConnectionDecoder decoder() { return decoder; } /** * Returns the {@link Http2ConnectionEncoder} to use. * * @return {@link Http2ConnectionEncoder} if set, or {@code null} if not set. */ protected Http2ConnectionEncoder encoder() { return encoder; } /** * Sets the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} to use. */ protected B codec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) { enforceConstraint("codec", "server", isServer); enforceConstraint("codec", "maxReservedStreams", maxReservedStreams); enforceConstraint("codec", "connection", connection); enforceConstraint("codec", "frameLogger", frameLogger); enforceConstraint("codec", "validateHeaders", validateHeaders); enforceConstraint("codec", "headerSensitivityDetector", headerSensitivityDetector); enforceConstraint("codec", "encoderEnforceMaxConcurrentStreams", encoderEnforceMaxConcurrentStreams); checkNotNull(decoder, "decoder"); checkNotNull(encoder, "encoder"); if (decoder.connection() != encoder.connection()) { throw new IllegalArgumentException("The specified encoder and decoder have different connections."); } this.decoder = decoder; this.encoder = encoder; return self(); } /** * Returns if HTTP headers should be validated according to * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.6">RFC 7540, 8.1.2.6</a>. */ protected boolean isValidateHeaders() { return validateHeaders != null ? validateHeaders : true; } /** * Sets if HTTP headers should be validated according to * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.6">RFC 7540, 8.1.2.6</a>. */ protected B validateHeaders(boolean validateHeaders) { enforceNonCodecConstraints("validateHeaders"); this.validateHeaders = validateHeaders; return self(); } /** * Returns the logger that is used for the encoder and decoder. * * @return {@link Http2FrameLogger} if set, or {@code null} if not set. */ protected Http2FrameLogger frameLogger() { return frameLogger; } /** * Sets the logger that is used for the encoder and decoder. */ protected B frameLogger(Http2FrameLogger frameLogger) { enforceNonCodecConstraints("frameLogger"); this.frameLogger = checkNotNull(frameLogger, "frameLogger"); return self(); } /** * Returns if the encoder should queue frames if the maximum number of concurrent streams * would otherwise be exceeded. */ protected boolean encoderEnforceMaxConcurrentStreams() { return encoderEnforceMaxConcurrentStreams != null ? encoderEnforceMaxConcurrentStreams : false; } /** * Sets if the encoder should queue frames if the maximum number of concurrent streams * would otherwise be exceeded. */ protected B encoderEnforceMaxConcurrentStreams(boolean encoderEnforceMaxConcurrentStreams) { enforceNonCodecConstraints("encoderEnforceMaxConcurrentStreams"); this.encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams; return self(); } /** * Returns the {@link SensitivityDetector} to use. */ protected SensitivityDetector headerSensitivityDetector() { return headerSensitivityDetector != null ? headerSensitivityDetector : DEFAULT_HEADER_SENSITIVITY_DETECTOR; } /** * Sets the {@link SensitivityDetector} to use. */ protected B headerSensitivityDetector(SensitivityDetector headerSensitivityDetector) { enforceNonCodecConstraints("headerSensitivityDetector"); this.headerSensitivityDetector = checkNotNull(headerSensitivityDetector, "headerSensitivityDetector"); return self(); } /** * Sets if the <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a> * should be ignored when encoding headers. * @param ignoreMaxHeaderListSize {@code true} to ignore * <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>. * @return this. */ protected B encoderIgnoreMaxHeaderListSize(boolean ignoreMaxHeaderListSize) { enforceNonCodecConstraints("encoderIgnoreMaxHeaderListSize"); this.encoderIgnoreMaxHeaderListSize = ignoreMaxHeaderListSize; return self(); } /** * Create a new {@link Http2ConnectionHandler}. */ protected T build() { if (encoder != null) { assert decoder != null; return buildFromCodec(decoder, encoder); } Http2Connection connection = this.connection; if (connection == null) { connection = new DefaultHttp2Connection(isServer(), maxReservedStreams()); } return buildFromConnection(connection); } private T buildFromConnection(Http2Connection connection) { Long maxHeaderListSize = initialSettings.maxHeaderListSize(); Http2FrameReader reader = new DefaultHttp2FrameReader(maxHeaderListSize == null ? new DefaultHttp2HeadersDecoder(isValidateHeaders()) : new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize)); Http2FrameWriter writer = encoderIgnoreMaxHeaderListSize == null ? new DefaultHttp2FrameWriter(headerSensitivityDetector()) : new DefaultHttp2FrameWriter(headerSensitivityDetector(), encoderIgnoreMaxHeaderListSize); if (frameLogger != null) { reader = new Http2InboundFrameLogger(reader, frameLogger); writer = new Http2OutboundFrameLogger(writer, frameLogger); } Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, writer); boolean encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams(); if (encoderEnforceMaxConcurrentStreams) { if (connection.isServer()) { encoder.close(); reader.close(); throw new IllegalArgumentException( "encoderEnforceMaxConcurrentStreams: " + encoderEnforceMaxConcurrentStreams + " not supported for server"); } encoder = new StreamBufferingEncoder(encoder); } Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader); return buildFromCodec(decoder, encoder); } private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) { final T handler; try { // Call the abstract build method handler = build(decoder, encoder, initialSettings); } catch (Throwable t) { encoder.close(); decoder.close(); throw new IllegalStateException("failed to build a Http2ConnectionHandler", t); } // Setup post build options handler.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis); if (handler.decoder().frameListener() == null) { handler.decoder().frameListener(frameListener); } return handler; } /** * Implement this method to create a new {@link Http2ConnectionHandler} or its subtype instance. * <p> * The return of this method will be subject to the following: * <ul> * <li>{@link #frameListener(Http2FrameListener)} will be set if not already set in the decoder</li> * <li>{@link #gracefulShutdownTimeoutMillis(long)} will always be set</li> * </ul> */ protected abstract T build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings) throws Exception; /** * Returns {@code this}. */ @SuppressWarnings("unchecked") protected final B self() { return (B) this; } private void enforceNonCodecConstraints(String rejected) { enforceConstraint(rejected, "server/connection", decoder); enforceConstraint(rejected, "server/connection", encoder); } private static void enforceConstraint(String methodName, String rejectorName, Object value) { if (value != null) { throw new IllegalStateException( methodName + "() cannot be called because " + rejectorName + "() has been called already."); } } }