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