View Javadoc
1   /*
2    * Copyright 2016 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    *   https://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  package io.netty5.handler.codec.http2;
17  
18  import io.netty5.buffer.api.Buffer;
19  import io.netty5.buffer.api.BufferAllocator;
20  import io.netty5.channel.Channel;
21  import io.netty5.channel.ChannelHandler;
22  import io.netty5.channel.ChannelHandlerContext;
23  import io.netty5.handler.codec.EncoderException;
24  import io.netty5.handler.codec.MessageToMessageCodec;
25  import io.netty5.handler.codec.http.DefaultHttpContent;
26  import io.netty5.handler.codec.http.DefaultLastHttpContent;
27  import io.netty5.handler.codec.http.FullHttpMessage;
28  import io.netty5.handler.codec.http.FullHttpResponse;
29  import io.netty5.handler.codec.http.HttpContent;
30  import io.netty5.handler.codec.http.HttpHeaderNames;
31  import io.netty5.handler.codec.http.HttpHeaderValues;
32  import io.netty5.handler.codec.http.HttpMessage;
33  import io.netty5.handler.codec.http.HttpObject;
34  import io.netty5.handler.codec.http.HttpRequest;
35  import io.netty5.handler.codec.http.HttpResponse;
36  import io.netty5.handler.codec.http.HttpResponseStatus;
37  import io.netty5.handler.codec.http.HttpScheme;
38  import io.netty5.handler.codec.http.HttpUtil;
39  import io.netty5.handler.codec.http.HttpVersion;
40  import io.netty5.handler.codec.http.LastHttpContent;
41  import io.netty5.handler.ssl.SslHandler;
42  import io.netty5.util.Attribute;
43  import io.netty5.util.AttributeKey;
44  import io.netty5.util.internal.UnstableApi;
45  
46  import java.util.List;
47  
48  /**
49   * This handler converts from {@link Http2StreamFrame} to {@link HttpObject},
50   * and back. It can be used as an adapter in conjunction with {@link
51   * Http2MultiplexHandler} to make http/2 connections backward-compatible with
52   * {@link ChannelHandler}s expecting {@link HttpObject}
53   *
54   * For simplicity, it converts to chunked encoding unless the entire stream
55   * is a single header.
56   */
57  @UnstableApi
58  public class Http2StreamFrameToHttpObjectCodec extends MessageToMessageCodec<Http2StreamFrame, HttpObject> {
59  
60      private static final AttributeKey<HttpScheme> SCHEME_ATTR_KEY =
61          AttributeKey.valueOf(HttpScheme.class, "STREAMFRAMECODEC_SCHEME");
62  
63      private final boolean isServer;
64      private final boolean validateHeaders;
65  
66      public Http2StreamFrameToHttpObjectCodec(final boolean isServer,
67                                               final boolean validateHeaders) {
68          this.isServer = isServer;
69          this.validateHeaders = validateHeaders;
70      }
71  
72      public Http2StreamFrameToHttpObjectCodec(final boolean isServer) {
73          this(isServer, true);
74      }
75  
76      @Override
77      public boolean isSharable() {
78          return true;
79      }
80  
81      @Override
82      public boolean acceptInboundMessage(Object msg) throws Exception {
83          return msg instanceof Http2HeadersFrame || msg instanceof Http2DataFrame;
84      }
85  
86      @Override
87      protected void decodeAndClose(ChannelHandlerContext ctx, Http2StreamFrame frame) throws Exception {
88          if (frame instanceof Http2HeadersFrame) {
89              Http2HeadersFrame headersFrame = (Http2HeadersFrame) frame;
90              Http2Headers headers = headersFrame.headers();
91              Http2FrameStream stream = headersFrame.stream();
92              int id = stream == null ? 0 : stream.id();
93  
94              final CharSequence status = headers.status();
95  
96              // 100-continue response is a special case where Http2HeadersFrame#isEndStream=false
97              // but we need to decode it as a FullHttpResponse to play nice with HttpObjectAggregator.
98              if (null != status && HttpResponseStatus.CONTINUE.codeAsText().contentEquals(status)) {
99                  final FullHttpMessage<?> fullMsg = newFullMessage(id, headers, ctx.bufferAllocator());
100                 ctx.fireChannelRead(fullMsg);
101                 return;
102             }
103 
104             if (headersFrame.isEndStream()) {
105                 if (headers.method() == null && status == null) {
106                     LastHttpContent<?> last = new DefaultLastHttpContent(ctx.bufferAllocator().allocate(0),
107                             validateHeaders);
108                     HttpConversionUtil.addHttp2ToHttpHeaders(id, headers, last.trailingHeaders(),
109                                                              HttpVersion.HTTP_1_1, true, true);
110                     ctx.fireChannelRead(last);
111                 } else {
112                     FullHttpMessage<?> full = newFullMessage(id, headers, ctx.bufferAllocator());
113                     ctx.fireChannelRead(full);
114                 }
115             } else {
116                 HttpMessage req = newMessage(id, headers);
117                 if (!HttpUtil.isContentLengthSet(req)) {
118                     req.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
119                 }
120                 ctx.fireChannelRead(req);
121             }
122         } else if (frame instanceof Http2DataFrame) {
123             Http2DataFrame dataFrame = (Http2DataFrame) frame;
124             if (dataFrame.isEndStream()) {
125                 ctx.fireChannelRead(new DefaultLastHttpContent(dataFrame.content(), validateHeaders));
126             } else {
127                 ctx.fireChannelRead(new DefaultHttpContent(dataFrame.content()));
128             }
129         }
130     }
131 
132     private void encodeLastContent(LastHttpContent<?> last, List<Object> out) {
133         boolean needFiller = !(last instanceof FullHttpMessage) && last.trailingHeaders().isEmpty();
134         final Buffer payload = last.payload();
135         if (payload.readableBytes() > 0 || needFiller) {
136             out.add(new DefaultHttp2DataFrame(payload.send(), last.trailingHeaders().isEmpty()));
137         }
138         if (!last.trailingHeaders().isEmpty()) {
139             Http2Headers headers = HttpConversionUtil.toHttp2Headers(last.trailingHeaders(), validateHeaders);
140             out.add(new DefaultHttp2HeadersFrame(headers, true));
141         }
142     }
143 
144     /**
145      * Encode from an {@link HttpObject} to an {@link Http2StreamFrame}. This method will
146      * be called for each written message that can be handled by this encoder.
147      *
148      * NOTE: 100-Continue responses that are NOT {@link FullHttpResponse} will be rejected.
149      *
150      * @param ctx           the {@link ChannelHandlerContext} which this handler belongs to
151      * @param obj           the {@link HttpObject} message to encode
152      * @param out           the {@link List} into which the encoded msg should be added
153      *                      needs to do some kind of aggregation
154      * @throws Exception    is thrown if an error occurs
155      */
156     @Override
157     protected void encodeAndClose(ChannelHandlerContext ctx, HttpObject obj, List<Object> out) throws Exception {
158         // 100-continue is typically a FullHttpResponse, but the decoded
159         // Http2HeadersFrame should not be marked as endStream=true
160         if (obj instanceof HttpResponse) {
161             final HttpResponse res = (HttpResponse) obj;
162             if (res.status().equals(HttpResponseStatus.CONTINUE)) {
163                 if (res instanceof FullHttpResponse) {
164                     final Http2Headers headers = toHttp2Headers(ctx, res);
165                     out.add(new DefaultHttp2HeadersFrame(headers, false));
166                     return;
167                 } else {
168                     throw new EncoderException(
169                             HttpResponseStatus.CONTINUE + " must be a FullHttpResponse");
170                 }
171             }
172         }
173 
174         if (obj instanceof HttpMessage) {
175             Http2Headers headers = toHttp2Headers(ctx, (HttpMessage) obj);
176             boolean noMoreFrames = false;
177             if (obj instanceof FullHttpMessage) {
178                 FullHttpMessage<?> full = (FullHttpMessage<?>) obj;
179                 noMoreFrames = full.payload().readableBytes() == 0 && full.trailingHeaders().isEmpty();
180             }
181 
182             out.add(new DefaultHttp2HeadersFrame(headers, noMoreFrames));
183         }
184 
185         if (obj instanceof LastHttpContent) {
186             LastHttpContent<?> last = (LastHttpContent<?>) obj;
187             encodeLastContent(last, out);
188         } else if (obj instanceof HttpContent) {
189             HttpContent<?> cont = (HttpContent<?>) obj;
190             final Buffer payload = cont.payload();
191             out.add(new DefaultHttp2DataFrame(payload.send(), false));
192         }
193     }
194 
195     private Http2Headers toHttp2Headers(final ChannelHandlerContext ctx, final HttpMessage msg) {
196         if (msg instanceof HttpRequest) {
197             msg.headers().set(
198                     HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(),
199                     connectionScheme(ctx));
200         }
201 
202         return HttpConversionUtil.toHttp2Headers(msg, validateHeaders);
203     }
204 
205     private HttpMessage newMessage(final int id,
206                                    final Http2Headers headers) throws Http2Exception {
207         return isServer ?
208                 HttpConversionUtil.toHttpRequest(id, headers, validateHeaders) :
209                 HttpConversionUtil.toHttpResponse(id, headers, validateHeaders);
210     }
211 
212     private FullHttpMessage<?> newFullMessage(final int id,
213                                            final Http2Headers headers,
214                                            final BufferAllocator alloc) throws Http2Exception {
215         return isServer ?
216                 HttpConversionUtil.toFullHttpRequest(id, headers, alloc, validateHeaders) :
217                 HttpConversionUtil.toFullHttpResponse(id, headers, alloc, validateHeaders);
218     }
219 
220     @Override
221     public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
222         super.handlerAdded(ctx);
223 
224         // this handler is typically used on an Http2StreamChannel. At this
225         // stage, ssl handshake should've been established. checking for the
226         // presence of SslHandler in the parent's channel pipeline to
227         // determine the HTTP scheme should suffice, even for the case where
228         // SniHandler is used.
229         final Attribute<HttpScheme> schemeAttribute = connectionSchemeAttribute(ctx);
230         if (schemeAttribute.get() == null) {
231             final HttpScheme scheme = isSsl(ctx) ? HttpScheme.HTTPS : HttpScheme.HTTP;
232             schemeAttribute.set(scheme);
233         }
234     }
235 
236     protected boolean isSsl(final ChannelHandlerContext ctx) {
237         final Channel connChannel = connectionChannel(ctx);
238         return null != connChannel.pipeline().get(SslHandler.class);
239     }
240 
241     private static HttpScheme connectionScheme(ChannelHandlerContext ctx) {
242         final HttpScheme scheme = connectionSchemeAttribute(ctx).get();
243         return scheme == null ? HttpScheme.HTTP : scheme;
244     }
245 
246     private static Attribute<HttpScheme> connectionSchemeAttribute(ChannelHandlerContext ctx) {
247         final Channel ch = connectionChannel(ctx);
248         return ch.attr(SCHEME_ATTR_KEY);
249     }
250 
251     private static Channel connectionChannel(ChannelHandlerContext ctx) {
252         final Channel ch = ctx.channel();
253         return ch instanceof Http2StreamChannel ? ch.parent() : ch;
254     }
255 }