1 /*
2 * Copyright 2012 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.netty.handler.codec.http;
17
18 import io.netty.buffer.ByteBuf;
19 import io.netty.channel.ChannelHandlerContext;
20 import io.netty.channel.ChannelInboundHandlerAdapter;
21 import io.netty.channel.embedded.EmbeddedChannel;
22 import io.netty.handler.codec.CodecException;
23 import io.netty.handler.codec.DecoderResult;
24 import io.netty.handler.codec.MessageToMessageDecoder;
25 import io.netty.util.ReferenceCountUtil;
26
27 import java.util.List;
28
29 /**
30 * Decodes the content of the received {@link HttpRequest} and {@link HttpContent}.
31 * The original content is replaced with the new content decoded by the
32 * {@link EmbeddedChannel}, which is created by {@link #newContentDecoder(String)}.
33 * Once decoding is finished, the value of the <tt>'Content-Encoding'</tt>
34 * header is set to the target content encoding, as returned by {@link #getTargetContentEncoding(String)}.
35 * Also, the <tt>'Content-Length'</tt> header is updated to the length of the
36 * decoded content. If the content encoding of the original is not supported
37 * by the decoder, {@link #newContentDecoder(String)} should return {@code null}
38 * so that no decoding occurs (i.e. pass-through).
39 * <p>
40 * Please note that this is an abstract class. You have to extend this class
41 * and implement {@link #newContentDecoder(String)} properly to make this class
42 * functional. For example, refer to the source code of {@link HttpContentDecompressor}.
43 * <p>
44 * This handler must be placed after {@link HttpObjectDecoder} in the pipeline
45 * so that this handler can intercept HTTP requests after {@link HttpObjectDecoder}
46 * converts {@link ByteBuf}s into HTTP requests.
47 */
48 public abstract class HttpContentDecoder extends MessageToMessageDecoder<HttpObject> {
49
50 static final String IDENTITY = HttpHeaderValues.IDENTITY.toString();
51
52 protected ChannelHandlerContext ctx;
53 private EmbeddedChannel decoder;
54 private boolean continueResponse;
55 private boolean needRead = true;
56 private ByteBufForwarder forwarder;
57
58 public HttpContentDecoder() {
59 super(HttpObject.class);
60 }
61
62 @Override
63 protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
64 needRead = true;
65 if (msg instanceof HttpResponse && ((HttpResponse) msg).status().code() == 100) {
66
67 if (!(msg instanceof LastHttpContent)) {
68 continueResponse = true;
69 }
70 // 100-continue response must be passed through.
71 needRead = false;
72 ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
73 return;
74 }
75
76 if (continueResponse) {
77 if (msg instanceof LastHttpContent) {
78 continueResponse = false;
79 }
80 // 100-continue response must be passed through.
81 needRead = false;
82 ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
83 return;
84 }
85
86 if (msg instanceof HttpMessage) {
87 cleanup();
88 final HttpMessage message = (HttpMessage) msg;
89 final HttpHeaders headers = message.headers();
90
91 // Determine the content encoding.
92 String contentEncoding = headers.get(HttpHeaderNames.CONTENT_ENCODING);
93 if (contentEncoding != null) {
94 contentEncoding = contentEncoding.trim();
95 } else {
96 String transferEncoding = headers.get(HttpHeaderNames.TRANSFER_ENCODING);
97 if (transferEncoding != null) {
98 int idx = transferEncoding.indexOf(',');
99 if (idx != -1) {
100 contentEncoding = transferEncoding.substring(0, idx).trim();
101 } else {
102 contentEncoding = transferEncoding.trim();
103 }
104 } else {
105 contentEncoding = IDENTITY;
106 }
107 }
108 decoder = newContentDecoder(contentEncoding);
109
110 if (decoder == null) {
111 if (message instanceof HttpContent) {
112 ((HttpContent) message).retain();
113 }
114 needRead = false;
115 ctx.fireChannelRead(message);
116 return;
117 }
118 decoder.pipeline().addLast(forwarder);
119 // Remove content-length header:
120 // the correct value can be set only after all chunks are processed/decoded.
121 // If buffering is not an issue, add HttpObjectAggregator down the chain, it will set the header.
122 // Otherwise, rely on LastHttpContent message.
123 if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
124 headers.remove(HttpHeaderNames.CONTENT_LENGTH);
125 headers.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
126 }
127 // Either it is already chunked or EOF terminated.
128 // See https://github.com/netty/netty/issues/5892
129
130 // set new content encoding,
131 CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding);
132 if (HttpHeaderValues.IDENTITY.contentEquals(targetContentEncoding)) {
133 // Do NOT set the 'Content-Encoding' header if the target encoding is 'identity'
134 // as per: https://tools.ietf.org/html/rfc2616#section-14.11
135 headers.remove(HttpHeaderNames.CONTENT_ENCODING);
136 } else {
137 headers.set(HttpHeaderNames.CONTENT_ENCODING, targetContentEncoding);
138 }
139
140 if (message instanceof HttpContent) {
141 // If message is a full request or response object (headers + data), don't copy data part into out.
142 // Output headers only; data part will be decoded below.
143 // Note: "copy" object must not be an instance of LastHttpContent class,
144 // as this would (erroneously) indicate the end of the HttpMessage to other handlers.
145 HttpMessage copy;
146 if (message instanceof HttpRequest) {
147 HttpRequest r = (HttpRequest) message; // HttpRequest or FullHttpRequest
148 copy = new DefaultHttpRequest(r.protocolVersion(), r.method(), r.uri());
149 } else if (message instanceof HttpResponse) {
150 HttpResponse r = (HttpResponse) message; // HttpResponse or FullHttpResponse
151 copy = new DefaultHttpResponse(r.protocolVersion(), r.status());
152 } else {
153 throw new CodecException("Object of class " + message.getClass().getName() +
154 " is not an HttpRequest or HttpResponse");
155 }
156 copy.headers().set(message.headers());
157 copy.setDecoderResult(message.decoderResult());
158 needRead = false;
159 ctx.fireChannelRead(copy);
160 } else {
161 needRead = false;
162 ctx.fireChannelRead(message);
163 }
164 }
165
166 if (msg instanceof HttpContent) {
167 final HttpContent c = (HttpContent) msg;
168 if (decoder == null) {
169 needRead = false;
170 ctx.fireChannelRead(c.retain());
171 } else {
172 // call retain here as it will call release after its written to the channel
173 decoder.writeInbound(c.content().retain());
174
175 if (c instanceof LastHttpContent) {
176 boolean notEmpty = decoder.finish();
177 decoder = null;
178 assert !notEmpty;
179 LastHttpContent last = (LastHttpContent) c;
180 // Generate an additional chunk if the decoder produced
181 // the last product on closure,
182 HttpHeaders headers = last.trailingHeaders();
183 needRead = false;
184 if (headers.isEmpty()) {
185 ctx.fireChannelRead(LastHttpContent.EMPTY_LAST_CONTENT);
186 } else {
187 ctx.fireChannelRead(new ComposedLastHttpContent(headers, DecoderResult.SUCCESS));
188 }
189 }
190 }
191 }
192 }
193
194 @Override
195 public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
196 boolean needRead = this.needRead;
197 this.needRead = true;
198
199 try {
200 ctx.fireChannelReadComplete();
201 } finally {
202 if (needRead && !ctx.channel().config().isAutoRead()) {
203 ctx.read();
204 }
205 }
206 }
207
208 /**
209 * Returns a new {@link EmbeddedChannel} that decodes the HTTP message
210 * content encoded in the specified <tt>contentEncoding</tt>.
211 *
212 * @param contentEncoding the value of the {@code "Content-Encoding"} header
213 * @return a new {@link EmbeddedChannel} if the specified encoding is supported.
214 * {@code null} otherwise (alternatively, you can throw an exception
215 * to block unknown encoding).
216 */
217 protected abstract EmbeddedChannel newContentDecoder(String contentEncoding) throws Exception;
218
219 /**
220 * Returns the expected content encoding of the decoded content.
221 * This getMethod returns {@code "identity"} by default, which is the case for
222 * most decoders.
223 *
224 * @param contentEncoding the value of the {@code "Content-Encoding"} header
225 * @return the expected content encoding of the new content
226 */
227 protected String getTargetContentEncoding(
228 @SuppressWarnings("UnusedParameters") String contentEncoding) throws Exception {
229 return IDENTITY;
230 }
231
232 @Override
233 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
234 cleanupSafely(ctx);
235 super.handlerRemoved(ctx);
236 }
237
238 @Override
239 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
240 cleanupSafely(ctx);
241 super.channelInactive(ctx);
242 }
243
244 @Override
245 public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
246 this.ctx = ctx;
247 forwarder = new ByteBufForwarder(ctx);
248 super.handlerAdded(ctx);
249 }
250
251 private void cleanup() {
252 if (decoder != null) {
253 // Clean-up the previous decoder if not cleaned up correctly.
254 boolean nonEmpty = decoder.finishAndReleaseAll();
255 decoder = null;
256 assert !nonEmpty;
257 }
258 }
259
260 private void cleanupSafely(ChannelHandlerContext ctx) {
261 try {
262 cleanup();
263 } catch (Throwable cause) {
264 // If cleanup throws any error we need to propagate it through the pipeline
265 // so we don't fail to propagate pipeline events.
266 ctx.fireExceptionCaught(cause);
267 }
268 }
269
270 private final class ByteBufForwarder extends ChannelInboundHandlerAdapter {
271
272 private final ChannelHandlerContext targetCtx;
273
274 ByteBufForwarder(ChannelHandlerContext targetCtx) {
275 this.targetCtx = targetCtx;
276 }
277
278 @Override
279 public boolean isSharable() {
280 // We need to mark the handler as sharable as we will add it to every EmbeddedChannel that is
281 // generated.
282 return true;
283 }
284
285 @Override
286 public void channelRead(ChannelHandlerContext ctx, Object msg) {
287 ByteBuf buf = (ByteBuf) msg;
288 if (!buf.isReadable()) {
289 buf.release();
290 return;
291 }
292 needRead = false;
293 targetCtx.fireChannelRead(new DefaultHttpContent(buf));
294 }
295 }
296 }