View Javadoc
1   /*
2    * Copyright 2015 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.codec.http2;
18  
19  import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
20  import io.netty.util.internal.UnstableApi;
21  
22  import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_LIST_SIZE;
23  import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;
24  import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_RESERVED_STREAMS;
25  import static io.netty.util.internal.ObjectUtil.checkNotNull;
26  import static io.netty.util.internal.ObjectUtil.checkPositive;
27  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
28  import static java.util.concurrent.TimeUnit.MILLISECONDS;
29  import static java.util.concurrent.TimeUnit.SECONDS;
30  
31  /**
32   * Abstract base class which defines commonly used features required to build {@link Http2ConnectionHandler} instances.
33   *
34   * <h3>Three ways to build a {@link Http2ConnectionHandler}</h3>
35   * <h4>Let the builder create a {@link Http2ConnectionHandler}</h4>
36   * Simply call all the necessary setter methods, and then use {@link #build()} to build a new
37   * {@link Http2ConnectionHandler}. Setting the following properties are prohibited because they are used for
38   * other ways of building a {@link Http2ConnectionHandler}.
39   * conflicts with this option:
40   * <ul>
41   *   <li>{@link #connection(Http2Connection)}</li>
42   *   <li>{@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)}</li>
43   * </ul>
44   *
45   *
46   * <h4>Let the builder use the {@link Http2ConnectionHandler} you specified</h4>
47   * Call {@link #connection(Http2Connection)} to tell the builder that you want to build the handler from the
48   * {@link Http2Connection} you specified. Setting the following properties are prohibited and thus will trigger
49   * an {@link IllegalStateException} because they conflict with this option.
50   * <ul>
51   *   <li>{@link #server(boolean)}</li>
52   *   <li>{@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)}</li>
53   * </ul>
54   *
55   * <h4>Let the builder use the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} you specified</h4>
56   * Call {@link #codec(Http2ConnectionDecoder, Http2ConnectionEncoder)} to tell the builder that you want to built the
57   * handler from the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} you specified. Setting the
58   * following properties are prohibited and thus will trigger an {@link IllegalStateException} because they conflict
59   * with this option:
60   * <ul>
61   *   <li>{@link #server(boolean)}</li>
62   *   <li>{@link #connection(Http2Connection)}</li>
63   *   <li>{@link #frameLogger(Http2FrameLogger)}</li>
64   *   <li>{@link #headerSensitivityDetector(SensitivityDetector)}</li>
65   *   <li>{@link #encoderEnforceMaxConcurrentStreams(boolean)}</li>
66   *   <li>{@link #encoderIgnoreMaxHeaderListSize(boolean)}</li>
67   *   <li>{@link #initialHuffmanDecodeCapacity(int)}</li>
68   * </ul>
69   *
70   * <h3>Exposing necessary methods in a subclass</h3>
71   * {@link #build()} method and all property access methods are {@code protected}. Choose the methods to expose to the
72   * users of your builder implementation and make them {@code public}.
73   *
74   * @param <T> The type of handler created by this builder.
75   * @param <B> The concrete type of this builder.
76   */
77  @UnstableApi
78  public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2ConnectionHandler,
79                                                              B extends AbstractHttp2ConnectionHandlerBuilder<T, B>> {
80  
81      private static final SensitivityDetector DEFAULT_HEADER_SENSITIVITY_DETECTOR = Http2HeadersEncoder.NEVER_SENSITIVE;
82  
83      // The properties that can always be set.
84      private Http2Settings initialSettings = Http2Settings.defaultSettings();
85      private Http2FrameListener frameListener;
86      private long gracefulShutdownTimeoutMillis = Http2CodecUtil.DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MILLIS;
87  
88      // The property that will prohibit connection() and codec() if set by server(),
89      // because this property is used only when this builder creates a Http2Connection.
90      private Boolean isServer;
91      private Integer maxReservedStreams;
92  
93      // The property that will prohibit server() and codec() if set by connection().
94      private Http2Connection connection;
95  
96      // The properties that will prohibit server() and connection() if set by codec().
97      private Http2ConnectionDecoder decoder;
98      private Http2ConnectionEncoder encoder;
99  
100     // The properties that are:
101     // * mutually exclusive against codec() and
102     // * OK to use with server() and connection()
103     private Boolean validateHeaders;
104     private Http2FrameLogger frameLogger;
105     private SensitivityDetector headerSensitivityDetector;
106     private Boolean encoderEnforceMaxConcurrentStreams;
107     private Boolean encoderIgnoreMaxHeaderListSize;
108     private int initialHuffmanDecodeCapacity = DEFAULT_INITIAL_HUFFMAN_DECODE_CAPACITY;
109 
110     /**
111      * Sets the {@link Http2Settings} to use for the initial connection settings exchange.
112      */
113     protected Http2Settings initialSettings() {
114         return initialSettings;
115     }
116 
117     /**
118      * Sets the {@link Http2Settings} to use for the initial connection settings exchange.
119      */
120     protected B initialSettings(Http2Settings settings) {
121         initialSettings = checkNotNull(settings, "settings");
122         return self();
123     }
124 
125     /**
126      * Returns the listener of inbound frames.
127      *
128      * @return {@link Http2FrameListener} if set, or {@code null} if not set.
129      */
130     protected Http2FrameListener frameListener() {
131         return frameListener;
132     }
133 
134     /**
135      * Sets the listener of inbound frames.
136      * This listener will only be set if the decoder's listener is {@code null}.
137      */
138     protected B frameListener(Http2FrameListener frameListener) {
139         this.frameListener = checkNotNull(frameListener, "frameListener");
140         return self();
141     }
142 
143     /**
144      * Returns the graceful shutdown timeout of the {@link Http2Connection} in milliseconds. Returns -1 if the
145      * timeout is indefinite.
146      */
147     protected long gracefulShutdownTimeoutMillis() {
148         return gracefulShutdownTimeoutMillis;
149     }
150 
151     /**
152      * Sets the graceful shutdown timeout of the {@link Http2Connection} in milliseconds.
153      */
154     protected B gracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) {
155         if (gracefulShutdownTimeoutMillis < -1) {
156             throw new IllegalArgumentException("gracefulShutdownTimeoutMillis: " + gracefulShutdownTimeoutMillis +
157                                                " (expected: -1 for indefinite or >= 0)");
158         }
159         this.gracefulShutdownTimeoutMillis = gracefulShutdownTimeoutMillis;
160         return self();
161     }
162 
163     /**
164      * Returns if {@link #build()} will to create a {@link Http2Connection} in server mode ({@code true})
165      * or client mode ({@code false}).
166      */
167     protected boolean isServer() {
168         return isServer != null ? isServer : true;
169     }
170 
171     /**
172      * Sets if {@link #build()} will to create a {@link Http2Connection} in server mode ({@code true})
173      * or client mode ({@code false}).
174      */
175     protected B server(boolean isServer) {
176         enforceConstraint("server", "connection", connection);
177         enforceConstraint("server", "codec", decoder);
178         enforceConstraint("server", "codec", encoder);
179 
180         this.isServer = isServer;
181         return self();
182     }
183 
184     /**
185      * Get the maximum number of streams which can be in the reserved state at any given time.
186      * <p>
187      * By default this value will be ignored on the server for local endpoint. This is because the RFC provides
188      * no way to explicitly communicate a limit to how many states can be in the reserved state, and instead relies
189      * on the peer to send RST_STREAM frames when they will be rejected.
190      */
191     protected int maxReservedStreams() {
192         return maxReservedStreams != null ? maxReservedStreams : DEFAULT_MAX_RESERVED_STREAMS;
193     }
194 
195     /**
196      * Set the maximum number of streams which can be in the reserved state at any given time.
197      */
198     protected B maxReservedStreams(int maxReservedStreams) {
199         enforceConstraint("server", "connection", connection);
200         enforceConstraint("server", "codec", decoder);
201         enforceConstraint("server", "codec", encoder);
202 
203         this.maxReservedStreams = checkPositiveOrZero(maxReservedStreams, "maxReservedStreams");
204         return self();
205     }
206 
207     /**
208      * Returns the {@link Http2Connection} to use.
209      *
210      * @return {@link Http2Connection} if set, or {@code null} if not set.
211      */
212     protected Http2Connection connection() {
213         return connection;
214     }
215 
216     /**
217      * Sets the {@link Http2Connection} to use.
218      */
219     protected B connection(Http2Connection connection) {
220         enforceConstraint("connection", "maxReservedStreams", maxReservedStreams);
221         enforceConstraint("connection", "server", isServer);
222         enforceConstraint("connection", "codec", decoder);
223         enforceConstraint("connection", "codec", encoder);
224 
225         this.connection = checkNotNull(connection, "connection");
226 
227         return self();
228     }
229 
230     /**
231      * Returns the {@link Http2ConnectionDecoder} to use.
232      *
233      * @return {@link Http2ConnectionDecoder} if set, or {@code null} if not set.
234      */
235     protected Http2ConnectionDecoder decoder() {
236         return decoder;
237     }
238 
239     /**
240      * Returns the {@link Http2ConnectionEncoder} to use.
241      *
242      * @return {@link Http2ConnectionEncoder} if set, or {@code null} if not set.
243      */
244     protected Http2ConnectionEncoder encoder() {
245         return encoder;
246     }
247 
248     /**
249      * Sets the {@link Http2ConnectionDecoder} and {@link Http2ConnectionEncoder} to use.
250      */
251     protected B codec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
252         enforceConstraint("codec", "server", isServer);
253         enforceConstraint("codec", "maxReservedStreams", maxReservedStreams);
254         enforceConstraint("codec", "connection", connection);
255         enforceConstraint("codec", "frameLogger", frameLogger);
256         enforceConstraint("codec", "validateHeaders", validateHeaders);
257         enforceConstraint("codec", "headerSensitivityDetector", headerSensitivityDetector);
258         enforceConstraint("codec", "encoderEnforceMaxConcurrentStreams", encoderEnforceMaxConcurrentStreams);
259 
260         checkNotNull(decoder, "decoder");
261         checkNotNull(encoder, "encoder");
262 
263         if (decoder.connection() != encoder.connection()) {
264             throw new IllegalArgumentException("The specified encoder and decoder have different connections.");
265         }
266 
267         this.decoder = decoder;
268         this.encoder = encoder;
269 
270         return self();
271     }
272 
273     /**
274      * Returns if HTTP headers should be validated according to
275      * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.6">RFC 7540, 8.1.2.6</a>.
276      */
277     protected boolean isValidateHeaders() {
278         return validateHeaders != null ? validateHeaders : true;
279     }
280 
281     /**
282      * Sets if HTTP headers should be validated according to
283      * <a href="https://tools.ietf.org/html/rfc7540#section-8.1.2.6">RFC 7540, 8.1.2.6</a>.
284      */
285     protected B validateHeaders(boolean validateHeaders) {
286         enforceNonCodecConstraints("validateHeaders");
287         this.validateHeaders = validateHeaders;
288         return self();
289     }
290 
291     /**
292      * Returns the logger that is used for the encoder and decoder.
293      *
294      * @return {@link Http2FrameLogger} if set, or {@code null} if not set.
295      */
296     protected Http2FrameLogger frameLogger() {
297         return frameLogger;
298     }
299 
300     /**
301      * Sets the logger that is used for the encoder and decoder.
302      */
303     protected B frameLogger(Http2FrameLogger frameLogger) {
304         enforceNonCodecConstraints("frameLogger");
305         this.frameLogger = checkNotNull(frameLogger, "frameLogger");
306         return self();
307     }
308 
309     /**
310      * Returns if the encoder should queue frames if the maximum number of concurrent streams
311      * would otherwise be exceeded.
312      */
313     protected boolean encoderEnforceMaxConcurrentStreams() {
314         return encoderEnforceMaxConcurrentStreams != null ? encoderEnforceMaxConcurrentStreams : false;
315     }
316 
317     /**
318      * Sets if the encoder should queue frames if the maximum number of concurrent streams
319      * would otherwise be exceeded.
320      */
321     protected B encoderEnforceMaxConcurrentStreams(boolean encoderEnforceMaxConcurrentStreams) {
322         enforceNonCodecConstraints("encoderEnforceMaxConcurrentStreams");
323         this.encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams;
324         return self();
325     }
326 
327     /**
328      * Returns the {@link SensitivityDetector} to use.
329      */
330     protected SensitivityDetector headerSensitivityDetector() {
331         return headerSensitivityDetector != null ? headerSensitivityDetector : DEFAULT_HEADER_SENSITIVITY_DETECTOR;
332     }
333 
334     /**
335      * Sets the {@link SensitivityDetector} to use.
336      */
337     protected B headerSensitivityDetector(SensitivityDetector headerSensitivityDetector) {
338         enforceNonCodecConstraints("headerSensitivityDetector");
339         this.headerSensitivityDetector = checkNotNull(headerSensitivityDetector, "headerSensitivityDetector");
340         return self();
341     }
342 
343     /**
344      * Sets if the <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>
345      * should be ignored when encoding headers.
346      * @param ignoreMaxHeaderListSize {@code true} to ignore
347      * <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_HEADER_LIST_SIZE</a>.
348      * @return this.
349      */
350     protected B encoderIgnoreMaxHeaderListSize(boolean ignoreMaxHeaderListSize) {
351         enforceNonCodecConstraints("encoderIgnoreMaxHeaderListSize");
352         this.encoderIgnoreMaxHeaderListSize = ignoreMaxHeaderListSize;
353         return self();
354     }
355 
356     /**
357      * Sets the initial size of an intermediate buffer used during HPACK huffman decoding.
358      * @param initialHuffmanDecodeCapacity initial size of an intermediate buffer used during HPACK huffman decoding.
359      * @return this.
360      */
361     protected B initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) {
362         enforceNonCodecConstraints("initialHuffmanDecodeCapacity");
363         this.initialHuffmanDecodeCapacity = checkPositive(initialHuffmanDecodeCapacity, "initialHuffmanDecodeCapacity");
364         return self();
365     }
366 
367     /**
368      * Create a new {@link Http2ConnectionHandler}.
369      */
370     protected T build() {
371         if (encoder != null) {
372             assert decoder != null;
373             return buildFromCodec(decoder, encoder);
374         }
375 
376         Http2Connection connection = this.connection;
377         if (connection == null) {
378             connection = new DefaultHttp2Connection(isServer(), maxReservedStreams());
379         }
380 
381         return buildFromConnection(connection);
382     }
383 
384     private T buildFromConnection(Http2Connection connection) {
385         Long maxHeaderListSize = initialSettings.maxHeaderListSize();
386         Http2FrameReader reader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(isValidateHeaders(),
387                 maxHeaderListSize == null ? DEFAULT_HEADER_LIST_SIZE : maxHeaderListSize,
388                 initialHuffmanDecodeCapacity));
389         Http2FrameWriter writer = encoderIgnoreMaxHeaderListSize == null ?
390                 new DefaultHttp2FrameWriter(headerSensitivityDetector()) :
391                 new DefaultHttp2FrameWriter(headerSensitivityDetector(), encoderIgnoreMaxHeaderListSize);
392 
393         if (frameLogger != null) {
394             reader = new Http2InboundFrameLogger(reader, frameLogger);
395             writer = new Http2OutboundFrameLogger(writer, frameLogger);
396         }
397 
398         Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, writer);
399         boolean encoderEnforceMaxConcurrentStreams = encoderEnforceMaxConcurrentStreams();
400 
401         if (encoderEnforceMaxConcurrentStreams) {
402             if (connection.isServer()) {
403                 encoder.close();
404                 reader.close();
405                 throw new IllegalArgumentException(
406                         "encoderEnforceMaxConcurrentStreams: " + encoderEnforceMaxConcurrentStreams +
407                         " not supported for server");
408             }
409             encoder = new StreamBufferingEncoder(encoder);
410         }
411 
412         Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader);
413         return buildFromCodec(decoder, encoder);
414     }
415 
416     private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
417         final T handler;
418         try {
419             // Call the abstract build method
420             handler = build(decoder, encoder, initialSettings);
421         } catch (Throwable t) {
422             encoder.close();
423             decoder.close();
424             throw new IllegalStateException("failed to build a Http2ConnectionHandler", t);
425         }
426 
427         // Setup post build options
428         handler.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis);
429         if (handler.decoder().frameListener() == null) {
430             handler.decoder().frameListener(frameListener);
431         }
432         return handler;
433     }
434 
435     /**
436      * Implement this method to create a new {@link Http2ConnectionHandler} or its subtype instance.
437      * <p>
438      * The return of this method will be subject to the following:
439      * <ul>
440      *   <li>{@link #frameListener(Http2FrameListener)} will be set if not already set in the decoder</li>
441      *   <li>{@link #gracefulShutdownTimeoutMillis(long)} will always be set</li>
442      * </ul>
443      */
444     protected abstract T build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
445                                Http2Settings initialSettings) throws Exception;
446 
447     /**
448      * Returns {@code this}.
449      */
450     @SuppressWarnings("unchecked")
451     protected final B self() {
452         return (B) this;
453     }
454 
455     private void enforceNonCodecConstraints(String rejected) {
456         enforceConstraint(rejected, "server/connection", decoder);
457         enforceConstraint(rejected, "server/connection", encoder);
458     }
459 
460     private static void enforceConstraint(String methodName, String rejectorName, Object value) {
461         if (value != null) {
462             throw new IllegalStateException(
463                     methodName + "() cannot be called because " + rejectorName + "() has been called already.");
464         }
465     }
466 }