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