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.buffer.ByteBufHolder;
20 import io.netty.channel.ChannelHandlerContext;
21 import io.netty.channel.embedded.EmbeddedChannel;
22 import io.netty.handler.codec.MessageToMessageCodec;
23 import io.netty.handler.codec.http.HttpHeaders.Names;
24 import io.netty.handler.codec.http.HttpHeaders.Values;
25 import io.netty.util.ReferenceCountUtil;
26
27 import java.util.ArrayDeque;
28 import java.util.List;
29 import java.util.Queue;
30
31 /**
32 * Encodes the content of the outbound [email protected] HttpResponse} and [email protected] HttpContent}.
33 * The original content is replaced with the new content encoded by the
34 * [email protected] EmbeddedChannel}, which is created by [email protected] #beginEncode(HttpResponse, String)}.
35 * Once encoding is finished, the value of the <tt>'Content-Encoding'</tt> header
36 * is set to the target content encoding, as returned by
37 * [email protected] #beginEncode(HttpResponse, String)}.
38 * Also, the <tt>'Content-Length'</tt> header is updated to the length of the
39 * encoded content. If there is no supported or allowed encoding in the
40 * corresponding [email protected] HttpRequest}'s [email protected] "Accept-Encoding"} header,
41 * [email protected] #beginEncode(HttpResponse, String)} should return [email protected] null} so that
42 * no encoding occurs (i.e. pass-through).
43 * <p>
44 * Please note that this is an abstract class. You have to extend this class
45 * and implement [email protected] #beginEncode(HttpResponse, String)} properly to make
46 * this class functional. For example, refer to the source code of
47 * [email protected] HttpContentCompressor}.
48 * <p>
49 * This handler must be placed after [email protected] HttpObjectEncoder} in the pipeline
50 * so that this handler can intercept HTTP responses before [email protected] HttpObjectEncoder}
51 * converts them into [email protected] ByteBuf}s.
52 */
53 public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpRequest, HttpObject> {
54
55 private enum State {
56 PASS_THROUGH,
57 AWAIT_HEADERS,
58 AWAIT_CONTENT
59 }
60
61 private static final CharSequence ZERO_LENGTH_HEAD = "HEAD";
62 private static final CharSequence ZERO_LENGTH_CONNECT = "CONNECT";
63 private static final int CONTINUE_CODE = HttpResponseStatus.CONTINUE.code();
64
65 private final Queue<CharSequence> acceptEncodingQueue = new ArrayDeque<CharSequence>();
66 private CharSequence acceptEncoding;
67 private EmbeddedChannel encoder;
68 private State state = State.AWAIT_HEADERS;
69
70 @Override
71 public boolean acceptOutboundMessage(Object msg) throws Exception {
72 return msg instanceof HttpContent || msg instanceof HttpResponse;
73 }
74
75 @Override
76 protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out)
77 throws Exception {
78 CharSequence acceptedEncoding = msg.headers().get(HttpHeaders.Names.ACCEPT_ENCODING);
79 if (acceptedEncoding == null) {
80 acceptedEncoding = HttpHeaders.Values.IDENTITY;
81 }
82
83 HttpMethod meth = msg.getMethod();
84 if (meth == HttpMethod.HEAD) {
85 acceptedEncoding = ZERO_LENGTH_HEAD;
86 } else if (meth == HttpMethod.CONNECT) {
87 acceptedEncoding = ZERO_LENGTH_CONNECT;
88 }
89
90 acceptEncodingQueue.add(acceptedEncoding);
91 out.add(ReferenceCountUtil.retain(msg));
92 }
93
94 @Override
95 protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
96 final boolean isFull = msg instanceof HttpResponse && msg instanceof LastHttpContent;
97 switch (state) {
98 case AWAIT_HEADERS: {
99 ensureHeaders(msg);
100 assert encoder == null;
101
102 final HttpResponse res = (HttpResponse) msg;
103 final int code = res.getStatus().code();
104 if (code == CONTINUE_CODE) {
105 // We need to not poll the encoding when response with CONTINUE as another response will follow
106 // for the issued request. See https://github.com/netty/netty/issues/4079
107 acceptEncoding = null;
108 } else {
109 // Get the list of encodings accepted by the peer.
110 acceptEncoding = acceptEncodingQueue.poll();
111 if (acceptEncoding == null) {
112 throw new IllegalStateException("cannot send more responses than requests");
113 }
114 }
115
116 /*
117 * per rfc2616 4.3 Message Body
118 * All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a
119 * message-body. All other responses do include a message-body, although it MAY be of zero length.
120 *
121 * 9.4 HEAD
122 * The HEAD method is identical to GET except that the server MUST NOT return a message-body
123 * in the response.
124 *
125 * This code is now inline with HttpClientDecoder.Decoder
126 */
127 if (isPassthru(code, acceptEncoding)) {
128 if (isFull) {
129 out.add(ReferenceCountUtil.retain(res));
130 } else {
131 out.add(res);
132 // Pass through all following contents.
133 state = State.PASS_THROUGH;
134 }
135 break;
136 }
137
138 if (isFull) {
139 // Pass through the full response with empty content and continue waiting for the the next resp.
140 if (!((ByteBufHolder) res).content().isReadable()) {
141 out.add(ReferenceCountUtil.retain(res));
142 break;
143 }
144 }
145
146 // Prepare to encode the content.
147 final Result result = beginEncode(res, acceptEncoding.toString());
148
149 // If unable to encode, pass through.
150 if (result == null) {
151 if (isFull) {
152 out.add(ReferenceCountUtil.retain(res));
153 } else {
154 out.add(res);
155 // Pass through all following contents.
156 state = State.PASS_THROUGH;
157 }
158 break;
159 }
160
161 encoder = result.contentEncoder();
162
163 // Encode the content and remove or replace the existing headers
164 // so that the message looks like a decoded message.
165 res.headers().set(Names.CONTENT_ENCODING, result.targetContentEncoding());
166
167 // Make the response chunked to simplify content transformation.
168 res.headers().remove(Names.CONTENT_LENGTH);
169 res.headers().set(Names.TRANSFER_ENCODING, Values.CHUNKED);
170
171 // Output the rewritten response.
172 if (isFull) {
173 // Convert full message into unfull one.
174 HttpResponse newRes = new DefaultHttpResponse(res.getProtocolVersion(), res.getStatus());
175 newRes.headers().set(res.headers());
176 out.add(newRes);
177 // Fall through to encode the content of the full response.
178 } else {
179 out.add(res);
180 state = State.AWAIT_CONTENT;
181 if (!(msg instanceof HttpContent)) {
182 // only break out the switch statement if we have not content to process
183 // See https://github.com/netty/netty/issues/2006
184 break;
185 }
186 // Fall through to encode the content
187 }
188 }
189 case AWAIT_CONTENT: {
190 ensureContent(msg);
191 if (encodeContent((HttpContent) msg, out)) {
192 state = State.AWAIT_HEADERS;
193 }
194 break;
195 }
196 case PASS_THROUGH: {
197 ensureContent(msg);
198 out.add(ReferenceCountUtil.retain(msg));
199 // Passed through all following contents of the current response.
200 if (msg instanceof LastHttpContent) {
201 state = State.AWAIT_HEADERS;
202 }
203 break;
204 }
205 }
206 }
207
208 private static boolean isPassthru(int code, CharSequence httpMethod) {
209 return code < 200 || code == 204 || code == 304 ||
210 (httpMethod == ZERO_LENGTH_HEAD || (httpMethod == ZERO_LENGTH_CONNECT && code == 200));
211 }
212
213 private static void ensureHeaders(HttpObject msg) {
214 if (!(msg instanceof HttpResponse)) {
215 throw new IllegalStateException(
216 "unexpected message type: " +
217 msg.getClass().getName() + " (expected: " + HttpResponse.class.getSimpleName() + ')');
218 }
219 }
220
221 private static void ensureContent(HttpObject msg) {
222 if (!(msg instanceof HttpContent)) {
223 throw new IllegalStateException(
224 "unexpected message type: " +
225 msg.getClass().getName() + " (expected: " + HttpContent.class.getSimpleName() + ')');
226 }
227 }
228
229 private boolean encodeContent(HttpContent c, List<Object> out) {
230 ByteBuf content = c.content();
231
232 encode(content, out);
233
234 if (c instanceof LastHttpContent) {
235 finishEncode(out);
236 LastHttpContent last = (LastHttpContent) c;
237
238 // Generate an additional chunk if the decoder produced
239 // the last product on closure,
240 HttpHeaders headers = last.trailingHeaders();
241 if (headers.isEmpty()) {
242 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
243 } else {
244 out.add(new ComposedLastHttpContent(headers));
245 }
246 return true;
247 }
248 return false;
249 }
250
251 /**
252 * Prepare to encode the HTTP message content.
253 *
254 * @param headers
255 * the headers
256 * @param acceptEncoding
257 * the value of the [email protected] "Accept-Encoding"} header
258 *
259 * @return the result of preparation, which is composed of the determined
260 * target content encoding and a new [email protected] EmbeddedChannel} that
261 * encodes the content into the target content encoding.
262 * [email protected] null} if [email protected] acceptEncoding} is unsupported or rejected
263 * and thus the content should be handled as-is (i.e. no encoding).
264 */
265 protected abstract Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception;
266
267 @Override
268 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
269 cleanup();
270 super.handlerRemoved(ctx);
271 }
272
273 @Override
274 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
275 cleanup();
276 super.channelInactive(ctx);
277 }
278
279 private void cleanup() {
280 if (encoder != null) {
281 // Clean-up the previous encoder if not cleaned up correctly.
282 if (encoder.finish()) {
283 for (;;) {
284 ByteBuf buf = (ByteBuf) encoder.readOutbound();
285 if (buf == null) {
286 break;
287 }
288 // Release the buffer
289 // https://github.com/netty/netty/issues/1524
290 buf.release();
291 }
292 }
293 encoder = null;
294 }
295 }
296
297 private void encode(ByteBuf in, List<Object> out) {
298 // call retain here as it will call release after its written to the channel
299 encoder.writeOutbound(in.retain());
300 fetchEncoderOutput(out);
301 }
302
303 private void finishEncode(List<Object> out) {
304 if (encoder.finish()) {
305 fetchEncoderOutput(out);
306 }
307 encoder = null;
308 }
309
310 private void fetchEncoderOutput(List<Object> out) {
311 for (;;) {
312 ByteBuf buf = (ByteBuf) encoder.readOutbound();
313 if (buf == null) {
314 break;
315 }
316 if (!buf.isReadable()) {
317 buf.release();
318 continue;
319 }
320 out.add(new DefaultHttpContent(buf));
321 }
322 }
323
324 public static final class Result {
325 private final String targetContentEncoding;
326 private final EmbeddedChannel contentEncoder;
327
328 public Result(String targetContentEncoding, EmbeddedChannel contentEncoder) {
329 if (targetContentEncoding == null) {
330 throw new NullPointerException("targetContentEncoding");
331 }
332 if (contentEncoder == null) {
333 throw new NullPointerException("contentEncoder");
334 }
335
336 this.targetContentEncoding = targetContentEncoding;
337 this.contentEncoder = contentEncoder;
338 }
339
340 public String targetContentEncoding() {
341 return targetContentEncoding;
342 }
343
344 public EmbeddedChannel contentEncoder() {
345 return contentEncoder;
346 }
347 }
348 }