1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.buffer.Unpooled;
21 import io.netty.channel.ChannelHandlerContext;
22 import io.netty.channel.embedded.EmbeddedChannel;
23 import io.netty.handler.codec.DecoderResult;
24 import io.netty.handler.codec.MessageToMessageCodec;
25 import io.netty.util.ReferenceCountUtil;
26 import io.netty.util.internal.ObjectUtil;
27 import io.netty.util.internal.StringUtil;
28
29 import java.util.ArrayDeque;
30 import java.util.List;
31 import java.util.Queue;
32
33 import static io.netty.handler.codec.http.HttpHeaderNames.*;
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpRequest, HttpObject> {
58 public static final int DEFAULT_MAX_PIPELINE_DEPTH = 128;
59
60 private enum State {
61 PASS_THROUGH,
62 AWAIT_HEADERS,
63 AWAIT_CONTENT
64 }
65
66 private static final CharSequence ZERO_LENGTH_HEAD = "HEAD";
67 private static final CharSequence ZERO_LENGTH_CONNECT = "CONNECT";
68
69 private final int maxPipelineDepth;
70 private final Queue<CharSequence> acceptEncodingQueue = new ArrayDeque<CharSequence>();
71 private EmbeddedChannel encoder;
72 private State state = State.AWAIT_HEADERS;
73
74 public HttpContentEncoder() {
75 this(DEFAULT_MAX_PIPELINE_DEPTH);
76 }
77
78 public HttpContentEncoder(int maxPipelineDepth) {
79 super(HttpRequest.class, HttpObject.class);
80 this.maxPipelineDepth = ObjectUtil.checkPositive(maxPipelineDepth, "maxPipelineDepth");
81 }
82
83 @Override
84 public boolean acceptOutboundMessage(Object msg) throws Exception {
85 return msg instanceof HttpContent || msg instanceof HttpResponse;
86 }
87
88 @Override
89 protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) throws Exception {
90 if (maxPipelineDepth <= acceptEncodingQueue.size()) {
91 throw new IllegalStateException("maxPipelineDepth exceeded: " + maxPipelineDepth);
92 }
93 CharSequence acceptEncoding;
94 List<String> acceptEncodingHeaders = msg.headers().getAll(ACCEPT_ENCODING);
95 switch (acceptEncodingHeaders.size()) {
96 case 0:
97 acceptEncoding = HttpContentDecoder.IDENTITY;
98 break;
99 case 1:
100 acceptEncoding = acceptEncodingHeaders.get(0);
101 break;
102 default:
103
104 acceptEncoding = StringUtil.join(",", acceptEncodingHeaders);
105 break;
106 }
107
108 HttpMethod method = msg.method();
109 if (HttpMethod.HEAD.equals(method)) {
110 acceptEncoding = ZERO_LENGTH_HEAD;
111 } else if (HttpMethod.CONNECT.equals(method)) {
112 acceptEncoding = ZERO_LENGTH_CONNECT;
113 }
114
115 acceptEncodingQueue.add(acceptEncoding);
116 out.add(ReferenceCountUtil.retain(msg));
117 }
118
119 @Override
120 protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
121 final boolean isFull = msg instanceof HttpResponse && msg instanceof LastHttpContent;
122 switch (state) {
123 case AWAIT_HEADERS: {
124 ensureHeaders(msg);
125 assert encoder == null;
126
127 final HttpResponse res = (HttpResponse) msg;
128 final int code = res.status().code();
129 final HttpStatusClass codeClass = res.status().codeClass();
130 final CharSequence acceptEncoding;
131 if (codeClass == HttpStatusClass.INFORMATIONAL) {
132
133
134
135 acceptEncoding = null;
136 } else {
137
138 acceptEncoding = acceptEncodingQueue.poll();
139 if (acceptEncoding == null) {
140 throw new IllegalStateException("cannot send more responses than requests");
141 }
142 }
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157 if (isPassthru(res.protocolVersion(), code, acceptEncoding)) {
158 if (isFull) {
159 out.add(ReferenceCountUtil.retain(res));
160 } else {
161 out.add(ReferenceCountUtil.retain(res));
162
163 state = State.PASS_THROUGH;
164 }
165 break;
166 }
167
168 if (isFull) {
169
170 if (!((ByteBufHolder) res).content().isReadable()) {
171 out.add(ReferenceCountUtil.retain(res));
172 break;
173 }
174 }
175
176
177 final Result result = beginEncode(res, acceptEncoding.toString());
178
179
180 if (result == null) {
181 if (isFull) {
182 out.add(ReferenceCountUtil.retain(res));
183 } else {
184 out.add(ReferenceCountUtil.retain(res));
185
186 state = State.PASS_THROUGH;
187 }
188 break;
189 }
190
191 EmbeddedChannel contentEncoder = result.contentEncoder();
192 try {
193
194
195 res.headers().set(HttpHeaderNames.CONTENT_ENCODING, result.targetContentEncoding());
196
197
198 if (isFull) {
199
200 HttpResponse newRes = new DefaultHttpResponse(res.protocolVersion(), res.status());
201 newRes.headers().set(res.headers());
202 out.add(newRes);
203
204 ensureContent(res);
205 encoder = contentEncoder;
206 encodeFullResponse(newRes, (HttpContent) res, out);
207 contentEncoder = null;
208 break;
209 } else {
210
211 res.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
212 res.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
213
214 out.add(ReferenceCountUtil.retain(res));
215 state = State.AWAIT_CONTENT;
216 encoder = contentEncoder;
217 contentEncoder = null;
218 if (!(msg instanceof HttpContent)) {
219
220
221 break;
222 }
223
224 }
225 } finally {
226 if (contentEncoder != null) {
227 if (encoder == contentEncoder) {
228 encoder = null;
229 }
230 contentEncoder.finishAndReleaseAll();
231 }
232 }
233 }
234 case AWAIT_CONTENT: {
235 ensureContent(msg);
236 if (encodeContent((HttpContent) msg, out)) {
237 state = State.AWAIT_HEADERS;
238 } else if (out.isEmpty()) {
239
240 out.add(new DefaultHttpContent(Unpooled.EMPTY_BUFFER));
241 }
242 break;
243 }
244 case PASS_THROUGH: {
245 ensureContent(msg);
246 out.add(ReferenceCountUtil.retain(msg));
247
248 if (msg instanceof LastHttpContent) {
249 state = State.AWAIT_HEADERS;
250 }
251 break;
252 }
253 }
254 }
255
256 private void encodeFullResponse(HttpResponse newRes, HttpContent content, List<Object> out) {
257 int existingMessages = out.size();
258 encodeContent(content, out);
259
260 if (HttpUtil.isContentLengthSet(newRes)) {
261
262 int messageSize = 0;
263 for (int i = existingMessages; i < out.size(); i++) {
264 Object item = out.get(i);
265 if (item instanceof HttpContent) {
266 messageSize += ((HttpContent) item).content().readableBytes();
267 }
268 }
269 HttpUtil.setContentLength(newRes, messageSize);
270 } else {
271 newRes.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
272 }
273 }
274
275 private static boolean isPassthru(HttpVersion version, int code, CharSequence httpMethod) {
276 return code < 200 || code == 204 || code == 304 ||
277 (httpMethod == ZERO_LENGTH_HEAD || (httpMethod == ZERO_LENGTH_CONNECT && code == 200)) ||
278 version == HttpVersion.HTTP_1_0;
279 }
280
281 private static void ensureHeaders(HttpObject msg) {
282 if (!(msg instanceof HttpResponse)) {
283 throw new IllegalStateException(
284 "unexpected message type: " +
285 msg.getClass().getName() + " (expected: " + HttpResponse.class.getSimpleName() + ')');
286 }
287 }
288
289 private static void ensureContent(HttpObject msg) {
290 if (!(msg instanceof HttpContent)) {
291 throw new IllegalStateException(
292 "unexpected message type: " +
293 msg.getClass().getName() + " (expected: " + HttpContent.class.getSimpleName() + ')');
294 }
295 }
296
297 private boolean encodeContent(HttpContent c, List<Object> out) {
298 ByteBuf content = c.content();
299
300 encode(content, out);
301
302 if (c instanceof LastHttpContent) {
303 finishEncode(out);
304 LastHttpContent last = (LastHttpContent) c;
305
306
307
308 HttpHeaders headers = last.trailingHeaders();
309 if (headers.isEmpty()) {
310 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
311 } else {
312 out.add(new ComposedLastHttpContent(headers, DecoderResult.SUCCESS));
313 }
314 return true;
315 }
316 return false;
317 }
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333 protected abstract Result beginEncode(HttpResponse httpResponse, String acceptEncoding) throws Exception;
334
335 @Override
336 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
337 cleanupSafely(ctx);
338 super.handlerRemoved(ctx);
339 }
340
341 @Override
342 public void channelInactive(ChannelHandlerContext ctx) throws Exception {
343 cleanupSafely(ctx);
344 super.channelInactive(ctx);
345 }
346
347 private void cleanup() {
348 if (encoder != null) {
349
350 encoder.finishAndReleaseAll();
351 encoder = null;
352 }
353 }
354
355 private void cleanupSafely(ChannelHandlerContext ctx) {
356 try {
357 cleanup();
358 } catch (Throwable cause) {
359
360
361 ctx.fireExceptionCaught(cause);
362 }
363 }
364
365 private void encode(ByteBuf in, List<Object> out) {
366
367 encoder.writeOutbound(in.retain());
368 fetchEncoderOutput(out);
369 }
370
371 private void finishEncode(List<Object> out) {
372 if (encoder.finish()) {
373 fetchEncoderOutput(out);
374 }
375 encoder = null;
376 }
377
378 private void fetchEncoderOutput(List<Object> out) {
379 for (;;) {
380 ByteBuf buf = encoder.readOutbound();
381 if (buf == null) {
382 break;
383 }
384 if (!buf.isReadable()) {
385 buf.release();
386 continue;
387 }
388 out.add(new DefaultHttpContent(buf));
389 }
390 }
391
392 public static final class Result {
393 private final String targetContentEncoding;
394 private final EmbeddedChannel contentEncoder;
395
396 public Result(String targetContentEncoding, EmbeddedChannel contentEncoder) {
397 this.targetContentEncoding = ObjectUtil.checkNotNull(targetContentEncoding, "targetContentEncoding");
398 this.contentEncoder = ObjectUtil.checkNotNull(contentEncoder, "contentEncoder");
399 }
400
401 public String targetContentEncoding() {
402 return targetContentEncoding;
403 }
404
405 public EmbeddedChannel contentEncoder() {
406 return contentEncoder;
407 }
408 }
409 }