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 public HttpContentEncoder() {
73 super(HttpRequest.class, HttpObject.class);
74 }
75
76 @Override
77 public boolean acceptOutboundMessage(Object msg) throws Exception {
78 return msg instanceof HttpContent || msg instanceof HttpResponse;
79 }
80
81 @Override
82 protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List<Object> out) throws Exception {
83 CharSequence acceptEncoding;
84 List<String> acceptEncodingHeaders = msg.headers().getAll(ACCEPT_ENCODING);
85 switch (acceptEncodingHeaders.size()) {
86 case 0:
87 acceptEncoding = HttpContentDecoder.IDENTITY;
88 break;
89 case 1:
90 acceptEncoding = acceptEncodingHeaders.get(0);
91 break;
92 default:
93
94 acceptEncoding = StringUtil.join(",", acceptEncodingHeaders);
95 break;
96 }
97
98 HttpMethod method = msg.method();
99 if (HttpMethod.HEAD.equals(method)) {
100 acceptEncoding = ZERO_LENGTH_HEAD;
101 } else if (HttpMethod.CONNECT.equals(method)) {
102 acceptEncoding = ZERO_LENGTH_CONNECT;
103 }
104
105 acceptEncodingQueue.add(acceptEncoding);
106 out.add(ReferenceCountUtil.retain(msg));
107 }
108
109 @Override
110 protected void encode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out) throws Exception {
111 final boolean isFull = msg instanceof HttpResponse && msg instanceof LastHttpContent;
112 switch (state) {
113 case AWAIT_HEADERS: {
114 ensureHeaders(msg);
115 assert encoder == null;
116
117 final HttpResponse res = (HttpResponse) msg;
118 final int code = res.status().code();
119 final HttpStatusClass codeClass = res.status().codeClass();
120 final CharSequence acceptEncoding;
121 if (codeClass == HttpStatusClass.INFORMATIONAL) {
122
123
124
125 acceptEncoding = null;
126 } else {
127
128 acceptEncoding = acceptEncodingQueue.poll();
129 if (acceptEncoding == null) {
130 throw new IllegalStateException("cannot send more responses than requests");
131 }
132 }
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147 if (isPassthru(res.protocolVersion(), code, acceptEncoding)) {
148 out.add(ReferenceCountUtil.retain(res));
149 if (!isFull) {
150
151 state = State.PASS_THROUGH;
152 }
153 break;
154 }
155
156 if (isFull) {
157
158 if (!((ByteBufHolder) res).content().isReadable()) {
159 out.add(ReferenceCountUtil.retain(res));
160 break;
161 }
162 }
163
164
165 final Result result = beginEncode(res, acceptEncoding.toString());
166
167
168 if (result == null) {
169 out.add(ReferenceCountUtil.retain(res));
170 if (!isFull) {
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 }