View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5    * "License"); you may not use this file except in compliance with the License. You may obtain a
6    * 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 distributed under the License
11   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing permissions and limitations under
13   * the License.
14   */
15  package io.netty.handler.codec.http2;
16  
17  import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
18  import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
19  import static io.netty.handler.codec.http2.Http2Exception.connectionError;
20  import static io.netty.util.internal.ObjectUtil.checkNotNull;
21  import io.netty.buffer.ByteBuf;
22  import io.netty.channel.ChannelHandlerContext;
23  import io.netty.handler.codec.TooLongFrameException;
24  import io.netty.handler.codec.http.FullHttpMessage;
25  import io.netty.handler.codec.http.FullHttpRequest;
26  import io.netty.handler.codec.http.FullHttpResponse;
27  import io.netty.handler.codec.http.HttpHeaderNames;
28  import io.netty.handler.codec.http.HttpHeaderUtil;
29  import io.netty.handler.codec.http.HttpStatusClass;
30  import io.netty.util.collection.IntObjectHashMap;
31  import io.netty.util.collection.IntObjectMap;
32  
33  /**
34   * This adapter provides just header/data events from the HTTP message flow defined
35   * here <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>.
36   * <p>
37   * See {@link HttpToHttp2ConnectionHandler} to get translation from HTTP/1.x objects to HTTP/2 frames for writes.
38   */
39  public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
40      private static final ImmediateSendDetector DEFAULT_SEND_DETECTOR = new ImmediateSendDetector() {
41          @Override
42          public boolean mustSendImmediately(FullHttpMessage msg) {
43              if (msg instanceof FullHttpResponse) {
44                  return ((FullHttpResponse) msg).status().codeClass() == HttpStatusClass.INFORMATIONAL;
45              }
46              if (msg instanceof FullHttpRequest) {
47                  return msg.headers().contains(HttpHeaderNames.EXPECT);
48              }
49              return false;
50          }
51  
52          @Override
53          public FullHttpMessage copyIfNeeded(FullHttpMessage msg) {
54              if (msg instanceof FullHttpRequest) {
55                  FullHttpRequest copy = ((FullHttpRequest) msg).copy(null);
56                  copy.headers().remove(HttpHeaderNames.EXPECT);
57                  return copy;
58              }
59              return null;
60          }
61      };
62  
63      private final int maxContentLength;
64      protected final Http2Connection connection;
65      protected final boolean validateHttpHeaders;
66      private final ImmediateSendDetector sendDetector;
67      protected final IntObjectMap<FullHttpMessage> messageMap;
68      private final boolean propagateSettings;
69  
70      public static class Builder {
71  
72          private final Http2Connection connection;
73          private int maxContentLength;
74          private boolean validateHttpHeaders;
75          private boolean propagateSettings;
76  
77          /**
78           * Creates a new {@link InboundHttp2ToHttpAdapter} builder for the specified {@link Http2Connection}.
79           *
80           * @param connection The object which will provide connection notification events for the current connection
81           */
82          public Builder(Http2Connection connection) {
83              this.connection = connection;
84          }
85  
86          /**
87           * Specifies the maximum length of the message content.
88           *
89           * @param maxContentLength the maximum length of the message content. If the length of the message content
90           *        exceeds this value, a {@link TooLongFrameException} will be raised
91           * @return {@link Builder} the builder for the {@link InboundHttp2ToHttpAdapter}
92           */
93          public Builder maxContentLength(int maxContentLength) {
94              this.maxContentLength = maxContentLength;
95              return this;
96          }
97  
98          /**
99           * Specifies whether validation of HTTP headers should be performed.
100          *
101          * @param validate
102          * <ul>
103          * <li>{@code true} to validate HTTP headers in the http-codec</li>
104          * <li>{@code false} not to validate HTTP headers in the http-codec</li>
105          * </ul>
106          * @return {@link Builder} the builder for the {@link InboundHttp2ToHttpAdapter}
107          */
108         public Builder validateHttpHeaders(boolean validate) {
109             validateHttpHeaders = validate;
110             return this;
111         }
112 
113         /**
114          * Specifies whether a read settings frame should be propagated alone the channel pipeline.
115          *
116          * @param propagate if {@code true} read settings will be passed along the pipeline. This can be useful
117          *                     to clients that need hold off sending data until they have received the settings.
118          * @return {@link Builder} the builder for the {@link InboundHttp2ToHttpAdapter}
119          */
120         public Builder propagateSettings(boolean propagate) {
121             propagateSettings = propagate;
122             return this;
123         }
124 
125         /**
126          * Builds/creates a new {@link InboundHttp2ToHttpAdapter} instance using this builders current settings.
127          */
128         public InboundHttp2ToHttpAdapter build() {
129             InboundHttp2ToHttpAdapter instance = new InboundHttp2ToHttpAdapter(this);
130             connection.addListener(instance);
131             return instance;
132         }
133     }
134 
135     protected InboundHttp2ToHttpAdapter(Builder builder) {
136         checkNotNull(builder.connection, "connection");
137         if (builder.maxContentLength <= 0) {
138             throw new IllegalArgumentException("maxContentLength must be a positive integer: "
139                     + builder.maxContentLength);
140         }
141         connection = builder.connection;
142         maxContentLength = builder.maxContentLength;
143         validateHttpHeaders = builder.validateHttpHeaders;
144         propagateSettings = builder.propagateSettings;
145         sendDetector = DEFAULT_SEND_DETECTOR;
146         messageMap = new IntObjectHashMap<FullHttpMessage>();
147     }
148 
149     /**
150      * The streamId is out of scope for the HTTP message flow and will no longer be tracked
151      * @param streamId The stream id to remove associated state with
152      */
153     protected void removeMessage(int streamId) {
154         messageMap.remove(streamId);
155     }
156 
157     @Override
158     public void streamRemoved(Http2Stream stream) {
159         removeMessage(stream.id());
160     }
161 
162     /**
163      * Set final headers and fire a channel read event
164      *
165      * @param ctx The context to fire the event on
166      * @param msg The message to send
167      * @param streamId the streamId of the message which is being fired
168      */
169     protected void fireChannelRead(ChannelHandlerContext ctx, FullHttpMessage msg, int streamId) {
170         removeMessage(streamId);
171         HttpHeaderUtil.setContentLength(msg, msg.content().readableBytes());
172         ctx.fireChannelRead(msg);
173     }
174 
175     /**
176      * Create a new {@link FullHttpMessage} based upon the current connection parameters
177      *
178      * @param streamId The stream id to create a message for
179      * @param headers The headers associated with {@code streamId}
180      * @param validateHttpHeaders
181      * <ul>
182      * <li>{@code true} to validate HTTP headers in the http-codec</li>
183      * <li>{@code false} not to validate HTTP headers in the http-codec</li>
184      * </ul>
185      * @throws Http2Exception
186      */
187     protected FullHttpMessage newMessage(int streamId, Http2Headers headers, boolean validateHttpHeaders)
188             throws Http2Exception {
189         return connection.isServer() ? HttpUtil.toHttpRequest(streamId, headers,
190                 validateHttpHeaders) : HttpUtil.toHttpResponse(streamId, headers, validateHttpHeaders);
191     }
192 
193     /**
194      * Provides translation between HTTP/2 and HTTP header objects while ensuring the stream
195      * is in a valid state for additional headers.
196      *
197      * @param ctx The context for which this message has been received.
198      * Used to send informational header if detected.
199      * @param streamId The stream id the {@code headers} apply to
200      * @param headers The headers to process
201      * @param endOfStream {@code true} if the {@code streamId} has received the end of stream flag
202      * @param allowAppend
203      * <ul>
204      * <li>{@code true} if headers will be appended if the stream already exists.</li>
205      * <li>if {@code false} and the stream already exists this method returns {@code null}.</li>
206      * </ul>
207      * @param appendToTrailer
208      * <ul>
209      * <li>{@code true} if a message {@code streamId} already exists then the headers
210      * should be added to the trailing headers.</li>
211      * <li>{@code false} then appends will be done to the initial headers.</li>
212      * </ul>
213      * @return The object used to track the stream corresponding to {@code streamId}. {@code null} if
214      *         {@code allowAppend} is {@code false} and the stream already exists.
215      * @throws Http2Exception If the stream id is not in the correct state to process the headers request
216      */
217     protected FullHttpMessage processHeadersBegin(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
218                 boolean endOfStream, boolean allowAppend, boolean appendToTrailer) throws Http2Exception {
219         FullHttpMessage msg = messageMap.get(streamId);
220         if (msg == null) {
221             msg = newMessage(streamId, headers, validateHttpHeaders);
222         } else if (allowAppend) {
223             try {
224                 HttpUtil.addHttp2ToHttpHeaders(streamId, headers, msg, appendToTrailer);
225             } catch (Http2Exception e) {
226                 removeMessage(streamId);
227                 throw e;
228             }
229         } else {
230             msg = null;
231         }
232 
233         if (sendDetector.mustSendImmediately(msg)) {
234             // Copy the message (if necessary) before sending. The content is not expected to be copied (or used) in
235             // this operation but just in case it is used do the copy before sending and the resource may be released
236             final FullHttpMessage copy = endOfStream ? null : sendDetector.copyIfNeeded(msg);
237             fireChannelRead(ctx, msg, streamId);
238             return copy;
239         }
240 
241         return msg;
242     }
243 
244     /**
245      * After HTTP/2 headers have been processed by {@link #processHeadersBegin} this method either
246      * sends the result up the pipeline or retains the message for future processing.
247      *
248      * @param ctx The context for which this message has been received
249      * @param streamId The stream id the {@code objAccumulator} corresponds to
250      * @param msg The object which represents all headers/data for corresponding to {@code streamId}
251      * @param endOfStream {@code true} if this is the last event for the stream
252      */
253     private void processHeadersEnd(ChannelHandlerContext ctx, int streamId,
254             FullHttpMessage msg, boolean endOfStream) {
255         if (endOfStream) {
256             fireChannelRead(ctx, msg, streamId);
257         } else {
258             messageMap.put(streamId, msg);
259         }
260     }
261 
262     @Override
263     public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
264                     throws Http2Exception {
265         FullHttpMessage msg = messageMap.get(streamId);
266         if (msg == null) {
267             throw connectionError(PROTOCOL_ERROR, "Data Frame recieved for unknown stream id %d", streamId);
268         }
269 
270         ByteBuf content = msg.content();
271         final int dataReadableBytes = data.readableBytes();
272         if (content.readableBytes() > maxContentLength - dataReadableBytes) {
273             throw connectionError(INTERNAL_ERROR,
274                             "Content length exceeded max of %d for stream id %d", maxContentLength, streamId);
275         }
276 
277         content.writeBytes(data, data.readerIndex(), dataReadableBytes);
278 
279         if (endOfStream) {
280             fireChannelRead(ctx, msg, streamId);
281         }
282 
283         // All bytes have been processed.
284         return dataReadableBytes + padding;
285     }
286 
287     @Override
288     public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
289                     boolean endOfStream) throws Http2Exception {
290         FullHttpMessage msg = processHeadersBegin(ctx, streamId, headers, endOfStream, true, true);
291         if (msg != null) {
292             processHeadersEnd(ctx, streamId, msg, endOfStream);
293         }
294     }
295 
296     @Override
297     public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
298                     short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
299         FullHttpMessage msg = processHeadersBegin(ctx, streamId, headers, endOfStream, true, true);
300         if (msg != null) {
301             processHeadersEnd(ctx, streamId, msg, endOfStream);
302         }
303     }
304 
305     @Override
306     public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
307         FullHttpMessage msg = messageMap.get(streamId);
308         if (msg != null) {
309             fireChannelRead(ctx, msg, streamId);
310         }
311     }
312 
313     @Override
314     public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
315             Http2Headers headers, int padding) throws Http2Exception {
316         // A push promise should not be allowed to add headers to an existing stream
317         FullHttpMessage msg = processHeadersBegin(ctx, promisedStreamId, headers, false, false, false);
318         if (msg == null) {
319             throw connectionError(PROTOCOL_ERROR, "Push Promise Frame recieved for pre-existing stream id %d",
320                             promisedStreamId);
321         }
322 
323         msg.headers().setInt(HttpUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), streamId);
324 
325         processHeadersEnd(ctx, promisedStreamId, msg, false);
326     }
327 
328     @Override
329     public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
330         if (propagateSettings) {
331             // Provide an interface for non-listeners to capture settings
332             ctx.fireChannelRead(settings);
333         }
334     }
335 
336     /**
337      * Allows messages to be sent up the pipeline before the next phase in the
338      * HTTP message flow is detected.
339      */
340     private interface ImmediateSendDetector {
341         /**
342          * Determine if the response should be sent immediately, or wait for the end of the stream
343          *
344          * @param msg The response to test
345          * @return {@code true} if the message should be sent immediately
346          *         {@code false) if we should wait for the end of the stream
347          */
348         boolean mustSendImmediately(FullHttpMessage msg);
349 
350         /**
351          * Determine if a copy must be made after an immediate send happens.
352          * <p>
353          * An example of this use case is if a request is received
354          * with a 'Expect: 100-continue' header. The message will be sent immediately,
355          * and the data will be queued and sent at the end of the stream.
356          *
357          * @param msg The message which has just been sent due to {@link #mustSendImmediately(FullHttpMessage)}
358          * @return A modified copy of the {@code msg} or {@code null} if a copy is not needed.
359          */
360         FullHttpMessage copyIfNeeded(FullHttpMessage msg);
361     }
362 }