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 org.jboss.netty.handler.codec.http;
17
18 import org.jboss.netty.buffer.ChannelBuffer;
19 import org.jboss.netty.buffer.ChannelBuffers;
20 import org.jboss.netty.channel.ChannelHandlerContext;
21 import org.jboss.netty.channel.ChannelStateEvent;
22 import org.jboss.netty.channel.Channels;
23 import org.jboss.netty.channel.LifeCycleAwareChannelHandler;
24 import org.jboss.netty.channel.MessageEvent;
25 import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
26 import org.jboss.netty.handler.codec.embedder.DecoderEmbedder;
27
28 /**
29 * Decodes the content of the received {@link HttpRequest} and {@link HttpChunk}.
30 * The original content is replaced with the new content decoded by the
31 * {@link DecoderEmbedder}, which is created by {@link #newContentDecoder(String)}.
32 * Once decoding is finished, the value of the <tt>'Content-Encoding'</tt>
33 * header is set to the target content encoding, as returned by {@link #getTargetContentEncoding(String)}.
34 * Also, the <tt>'Content-Length'</tt> header is updated to the length of the
35 * decoded content. If the content encoding of the original is not supported
36 * by the decoder, {@link #newContentDecoder(String)} should return {@code null}
37 * so that no decoding occurs (i.e. pass-through).
38 * <p>
39 * Please note that this is an abstract class. You have to extend this class
40 * and implement {@link #newContentDecoder(String)} properly to make this class
41 * functional. For example, refer to the source code of {@link HttpContentDecompressor}.
42 * <p>
43 * This handler must be placed after {@link HttpMessageDecoder} in the pipeline
44 * so that this handler can intercept HTTP requests after {@link HttpMessageDecoder}
45 * converts {@link ChannelBuffer}s into HTTP requests.
46 */
47 public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler
48 implements LifeCycleAwareChannelHandler {
49
50 private DecoderEmbedder<ChannelBuffer> decoder;
51
52 /**
53 * Creates a new instance.
54 */
55 protected HttpContentDecoder() {
56 }
57
58 @Override
59 public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
60 Object msg = e.getMessage();
61 if (msg instanceof HttpResponse && ((HttpResponse) msg).getStatus().getCode() == 100) {
62 // 100-continue response must be passed through.
63 ctx.sendUpstream(e);
64 } else if (msg instanceof HttpMessage) {
65 HttpMessage m = (HttpMessage) msg;
66
67 // Clean-up the previous decoder if not cleaned up correctly.
68 finishDecode();
69
70 // Determine the content encoding.
71 String contentEncoding = m.headers().get(HttpHeaders.Names.CONTENT_ENCODING);
72 if (contentEncoding != null) {
73 contentEncoding = contentEncoding.trim();
74 } else {
75 contentEncoding = HttpHeaders.Values.IDENTITY;
76 }
77
78 boolean hasContent = m.isChunked() || m.getContent().readable();
79 if (hasContent && (decoder = newContentDecoder(contentEncoding)) != null) {
80 // Decode the content and remove or replace the existing headers
81 // so that the message looks like a decoded message.
82 String targetContentEncoding = getTargetContentEncoding(contentEncoding);
83 if (HttpHeaders.Values.IDENTITY.equals(targetContentEncoding)) {
84 // Do NOT set the 'Content-Encoding' header if the target encoding is 'identity'
85 // as per: http://tools.ietf.org/html/rfc2616#section-14.11
86 m.headers().remove(HttpHeaders.Names.CONTENT_ENCODING);
87 } else {
88 m.headers().set(HttpHeaders.Names.CONTENT_ENCODING, targetContentEncoding);
89 }
90
91 if (!m.isChunked()) {
92 ChannelBuffer content = m.getContent();
93 // Decode the content
94 content = ChannelBuffers.wrappedBuffer(
95 decode(content), finishDecode());
96
97 // Replace the content.
98 m.setContent(content);
99 if (m.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
100 m.headers().set(
101 HttpHeaders.Names.CONTENT_LENGTH,
102 Integer.toString(content.readableBytes()));
103 }
104 }
105 }
106
107 // Because HttpMessage is a mutable object, we can simply forward the received event.
108 ctx.sendUpstream(e);
109 } else if (msg instanceof HttpChunk) {
110 HttpChunk c = (HttpChunk) msg;
111 ChannelBuffer content = c.getContent();
112
113 // Decode the chunk if necessary.
114 if (decoder != null) {
115 if (!c.isLast()) {
116 content = decode(content);
117 if (content.readable()) {
118 c.setContent(content);
119 ctx.sendUpstream(e);
120 }
121 } else {
122 ChannelBuffer lastProduct = finishDecode();
123
124 // Generate an additional chunk if the decoder produced
125 // the last product on closure,
126 if (lastProduct.readable()) {
127 Channels.fireMessageReceived(
128 ctx, new DefaultHttpChunk(lastProduct), e.getRemoteAddress());
129 }
130
131 // Emit the last chunk.
132 ctx.sendUpstream(e);
133 }
134 } else {
135 ctx.sendUpstream(e);
136 }
137 } else {
138 ctx.sendUpstream(e);
139 }
140 }
141
142 @Override
143 public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
144 // Clean-up the previous decoder if not cleaned up correctly.
145 finishDecode();
146
147 super.channelClosed(ctx, e);
148 }
149
150 /**
151 * Returns a new {@link DecoderEmbedder} that decodes the HTTP message
152 * content encoded in the specified <tt>contentEncoding</tt>.
153 *
154 * @param contentEncoding the value of the {@code "Content-Encoding"} header
155 * @return a new {@link DecoderEmbedder} if the specified encoding is supported.
156 * {@code null} otherwise (alternatively, you can throw an exception
157 * to block unknown encoding).
158 */
159 protected abstract DecoderEmbedder<ChannelBuffer> newContentDecoder(String contentEncoding) throws Exception;
160
161 /**
162 * Returns the expected content encoding of the decoded content.
163 * This method returns {@code "identity"} by default, which is the case for
164 * most decoders.
165 *
166 * @param contentEncoding the value of the {@code "Content-Encoding"} header
167 * @return the expected content encoding of the new content
168 */
169 protected String getTargetContentEncoding(String contentEncoding) throws Exception {
170 return HttpHeaders.Values.IDENTITY;
171 }
172
173 private ChannelBuffer decode(ChannelBuffer buf) {
174 decoder.offer(buf);
175 return ChannelBuffers.wrappedBuffer(decoder.pollAll(new ChannelBuffer[decoder.size()]));
176 }
177
178 private ChannelBuffer finishDecode() {
179 if (decoder == null) {
180 return ChannelBuffers.EMPTY_BUFFER;
181 }
182
183 ChannelBuffer result;
184 if (decoder.finish()) {
185 result = ChannelBuffers.wrappedBuffer(decoder.pollAll(new ChannelBuffer[decoder.size()]));
186 } else {
187 result = ChannelBuffers.EMPTY_BUFFER;
188 }
189 decoder = null;
190 return result;
191 }
192
193 public void beforeAdd(ChannelHandlerContext ctx) throws Exception {
194 // NOOP
195 }
196
197 public void afterAdd(ChannelHandlerContext ctx) throws Exception {
198 // NOOP
199 }
200
201 public void beforeRemove(ChannelHandlerContext ctx) throws Exception {
202 // NOOP
203 }
204
205 public void afterRemove(ChannelHandlerContext ctx) throws Exception {
206 finishDecode();
207 }
208 }