1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18
19 package io.undertow.server.protocol.http;
20
21 import io.undertow.UndertowLogger;
22 import io.undertow.UndertowOptions;
23 import io.undertow.conduits.ChunkedStreamSinkConduit;
24 import io.undertow.conduits.ChunkedStreamSourceConduit;
25 import io.undertow.conduits.ConduitListener;
26 import io.undertow.conduits.FinishableStreamSinkConduit;
27 import io.undertow.conduits.FixedLengthStreamSourceConduit;
28 import io.undertow.conduits.HeadStreamSinkConduit;
29 import io.undertow.conduits.PreChunkedStreamSinkConduit;
30 import io.undertow.server.Connectors;
31 import io.undertow.server.HttpServerExchange;
32 import io.undertow.util.DateUtils;
33 import io.undertow.util.HeaderMap;
34 import io.undertow.util.Headers;
35 import io.undertow.util.HttpString;
36 import io.undertow.util.Methods;
37 import io.undertow.util.StatusCodes;
38
39 import org.jboss.logging.Logger;
40 import org.xnio.conduits.ConduitStreamSourceChannel;
41 import org.xnio.conduits.StreamSinkConduit;
42 import org.xnio.conduits.StreamSourceConduit;
43
44 /**
45  * Class that is  responsible for HTTP transfer encoding, this could be part of the {@link HttpReadListener},
46  * but is separated out for clarity.
47  * <p>
48  * For more info see http://tools.ietf.org/html/rfc2616#section-4.4
49  *
50  * @author Stuart Douglas
51  * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
52  */

53 class HttpTransferEncoding {
54
55     private static final Logger log = Logger.getLogger("io.undertow.server.handler.transfer-encoding");
56
57
58     /**
59      * Construct a new instance.
60      */

61     private HttpTransferEncoding() {
62     }
63
64     public static void setupRequest(final HttpServerExchange exchange) {
65         final HeaderMap requestHeaders = exchange.getRequestHeaders();
66         final String connectionHeader = requestHeaders.getFirst(Headers.CONNECTION);
67         final String transferEncodingHeader = requestHeaders.getLast(Headers.TRANSFER_ENCODING);
68         final String contentLengthHeader = requestHeaders.getFirst(Headers.CONTENT_LENGTH);
69
70         final HttpServerConnection connection = (HttpServerConnection) exchange.getConnection();
71         //if we are already using the pipelineing buffer add it to the exchange
72         PipeliningBufferingStreamSinkConduit pipeliningBuffer = connection.getPipelineBuffer();
73         if (pipeliningBuffer != null) {
74             pipeliningBuffer.setupPipelineBuffer(exchange);
75         }
76         ConduitStreamSourceChannel sourceChannel = connection.getChannel().getSourceChannel();
77         sourceChannel.setConduit(connection.getReadDataStreamSourceConduit());
78
79         boolean persistentConnection = persistentConnection(exchange, connectionHeader);
80
81         if (transferEncodingHeader == null && contentLengthHeader == null) {
82             if (persistentConnection
83                     && connection.getExtraBytes() != null
84                     && pipeliningBuffer == null
85                     && connection.getUndertowOptions().get(UndertowOptions.BUFFER_PIPELINED_DATA, false)) {
86                 pipeliningBuffer = new PipeliningBufferingStreamSinkConduit(connection.getOriginalSinkConduit(), connection.getByteBufferPool());
87                 connection.setPipelineBuffer(pipeliningBuffer);
88                 pipeliningBuffer.setupPipelineBuffer(exchange);
89             }
90             // no content - immediately start the next request, returning an empty stream for this one
91             Connectors.terminateRequest(exchange);
92         } else {
93             persistentConnection = handleRequestEncoding(exchange, transferEncodingHeader, contentLengthHeader, connection, pipeliningBuffer, persistentConnection);
94         }
95
96         exchange.setPersistent(persistentConnection);
97
98         if (!exchange.isRequestComplete() || connection.getExtraBytes() != null) {
99             //if there is more data we suspend reads
100             sourceChannel.setReadListener(null);
101             sourceChannel.suspendReads();
102         }
103
104     }
105
106     private static boolean handleRequestEncoding(final HttpServerExchange exchange, String transferEncodingHeader, String contentLengthHeader, HttpServerConnection connection, PipeliningBufferingStreamSinkConduit pipeliningBuffer, boolean persistentConnection) {
107
108         HttpString transferEncoding = Headers.IDENTITY;
109         if (transferEncodingHeader != null) {
110             transferEncoding = new HttpString(transferEncodingHeader);
111         }
112         if (transferEncodingHeader != null && !transferEncoding.equals(Headers.IDENTITY)) {
113             ConduitStreamSourceChannel sourceChannel = ((HttpServerConnection) exchange.getConnection()).getChannel().getSourceChannel();
114             sourceChannel.setConduit(new ChunkedStreamSourceConduit(sourceChannel.getConduit(), exchange, chunkedDrainListener(exchange)));
115         } else if (contentLengthHeader != null) {
116             final long contentLength;
117             contentLength = parsePositiveLong(contentLengthHeader);
118             if (contentLength == 0L) {
119                 log.trace("No content, starting next request");
120                 // no content - immediately start the next request, returning an empty stream for this one
121                 Connectors.terminateRequest(exchange);
122             } else {
123                 // fixed-length content - add a wrapper for a fixed-length stream
124                 ConduitStreamSourceChannel sourceChannel = ((HttpServerConnection) exchange.getConnection()).getChannel().getSourceChannel();
125                 sourceChannel.setConduit(fixedLengthStreamSourceConduitWrapper(contentLength, sourceChannel.getConduit(), exchange));
126             }
127         } else if (transferEncodingHeader != null) {
128             //identity transfer encoding
129             log.trace("Connection not persistent (no content length and identity transfer encoding)");
130             // make it not persistent
131             persistentConnection = false;
132         } else if (persistentConnection) {
133             //we have no content and a persistent request. This may mean we need to use the pipelining buffer to improve
134             //performance
135             if (connection.getExtraBytes() != null
136                     && pipeliningBuffer == null
137                     && connection.getUndertowOptions().get(UndertowOptions.BUFFER_PIPELINED_DATA, false)) {
138                 pipeliningBuffer = new PipeliningBufferingStreamSinkConduit(connection.getOriginalSinkConduit(), connection.getByteBufferPool());
139                 connection.setPipelineBuffer(pipeliningBuffer);
140                 pipeliningBuffer.setupPipelineBuffer(exchange);
141             }
142
143             // no content - immediately start the next request, returning an empty stream for this one
144             Connectors.terminateRequest(exchange);
145         } else {
146             //assume there is no content
147             //we still know there is no content
148             Connectors.terminateRequest(exchange);
149         }
150         return persistentConnection;
151     }
152
153     private static boolean persistentConnection(HttpServerExchange exchange, String connectionHeader) {
154         if (exchange.isHttp11()) {
155             return !(connectionHeader != null && Headers.CLOSE.equalToString(connectionHeader));
156         } else if (exchange.isHttp10()) {
157             if (connectionHeader != null) {
158                 if (Headers.KEEP_ALIVE.equals(new HttpString(connectionHeader))) {
159                     return true;
160                 }
161             }
162         }
163         log.trace("Connection not persistent");
164         return false;
165     }
166
167     private static StreamSourceConduit fixedLengthStreamSourceConduitWrapper(final long contentLength, final StreamSourceConduit conduit, final HttpServerExchange exchange) {
168         return new FixedLengthStreamSourceConduit(conduit, contentLength, fixedLengthDrainListener(exchange), exchange);
169     }
170
171     private static ConduitListener<FixedLengthStreamSourceConduit> fixedLengthDrainListener(final HttpServerExchange exchange) {
172         return new ConduitListener<FixedLengthStreamSourceConduit>() {
173             public void handleEvent(final FixedLengthStreamSourceConduit fixedLengthConduit) {
174                 long remaining = fixedLengthConduit.getRemaining();
175                 if (remaining > 0L) {
176                     UndertowLogger.REQUEST_LOGGER.requestWasNotFullyConsumed();
177                     exchange.setPersistent(false);
178                 }
179                 Connectors.terminateRequest(exchange);
180             }
181         };
182     }
183
184     private static ConduitListener<ChunkedStreamSourceConduit> chunkedDrainListener(final HttpServerExchange exchange) {
185         return new ConduitListener<ChunkedStreamSourceConduit>() {
186             public void handleEvent(final ChunkedStreamSourceConduit chunkedStreamSourceConduit) {
187                 if (!chunkedStreamSourceConduit.isFinished()) {
188                     UndertowLogger.REQUEST_LOGGER.requestWasNotFullyConsumed();
189                     exchange.setPersistent(false);
190                 }
191                 Connectors.terminateRequest(exchange);
192             }
193         };
194     }
195
196     private static ConduitListener<StreamSinkConduit> terminateResponseListener(final HttpServerExchange exchange) {
197         return new ConduitListener<StreamSinkConduit>() {
198             public void handleEvent(final StreamSinkConduit channel) {
199                 Connectors.terminateResponse(exchange);
200             }
201         };
202     }
203
204     static StreamSinkConduit createSinkConduit(final HttpServerExchange exchange) {
205         DateUtils.addDateHeaderIfRequired(exchange);
206
207         boolean headRequest = exchange.getRequestMethod().equals(Methods.HEAD);
208         HttpServerConnection serverConnection = (HttpServerConnection) exchange.getConnection();
209
210         HttpResponseConduit responseConduit = serverConnection.getResponseConduit();
211         responseConduit.reset(exchange);
212         StreamSinkConduit channel = responseConduit;
213         if (headRequest) {
214             //if this is a head request we add a head channel underneath the content encoding channel
215             //this will just discard the data
216             //we still go through with the rest of the logic, to make sure all headers are set correctly
217             channel = new HeadStreamSinkConduit(channel, terminateResponseListener(exchange));
218         } else if(!Connectors.isEntityBodyAllowed(exchange)) {
219             //we are not allowed to send an entity body for some requests
220             exchange.getResponseHeaders().remove(Headers.CONTENT_LENGTH);
221             exchange.getResponseHeaders().remove(Headers.TRANSFER_ENCODING);
222             channel = new HeadStreamSinkConduit(channel, terminateResponseListener(exchange));
223             return channel;
224         }
225
226         final HeaderMap responseHeaders = exchange.getResponseHeaders();
227         // test to see if we're still persistent
228         String connection = responseHeaders.getFirst(Headers.CONNECTION);
229         if(exchange.getStatusCode() == StatusCodes.EXPECTATION_FAILED) {
230             //417 responses are never persistent, as we have no idea if there is a response body
231             //still coming on the wire.
232             exchange.setPersistent(false);
233         }
234         if (!exchange.isPersistent()) {
235             responseHeaders.put(Headers.CONNECTION, Headers.CLOSE.toString());
236         } else if (exchange.isPersistent() && connection != null) {
237             if (HttpString.tryFromString(connection).equals(Headers.CLOSE)) {
238                 exchange.setPersistent(false);
239             }
240         } else if (exchange.getConnection().getUndertowOptions().get(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, true)) {
241             responseHeaders.put(Headers.CONNECTION, Headers.KEEP_ALIVE.toString());
242         }
243         //according to the HTTP RFC we should ignore content length if a transfer coding is specified
244         final String transferEncodingHeader = responseHeaders.getLast(Headers.TRANSFER_ENCODING);
245         if(transferEncodingHeader == null) {
246             final String contentLengthHeader = responseHeaders.getFirst(Headers.CONTENT_LENGTH);
247             if (contentLengthHeader != null) {
248                 StreamSinkConduit res = handleFixedLength(exchange, headRequest, channel, responseHeaders, contentLengthHeader, serverConnection);
249                 if (res != null) {
250                     return res;
251                 }
252             }
253         } else {
254             responseHeaders.remove(Headers.CONTENT_LENGTH); //if there is a transfer-encoding header we remove content length if present
255         }
256         return handleResponseConduit(exchange, headRequest, channel, responseHeaders, terminateResponseListener(exchange), transferEncodingHeader);
257     }
258
259     private static StreamSinkConduit handleFixedLength(HttpServerExchange exchange, boolean headRequest, StreamSinkConduit channel, HeaderMap responseHeaders, String contentLengthHeader, HttpServerConnection connection) {
260         try {
261             final long contentLength = parsePositiveLong(contentLengthHeader);
262             if (headRequest) {
263                 return channel;
264             }
265             // fixed-length response
266             ServerFixedLengthStreamSinkConduit fixed = connection.getFixedLengthStreamSinkConduit();
267             fixed.reset(contentLength, exchange);
268             return fixed;
269         } catch (NumberFormatException e) {
270             //we just fix it for them
271             responseHeaders.remove(Headers.CONTENT_LENGTH);
272         }
273         return null;
274     }
275
276     private static StreamSinkConduit handleResponseConduit(HttpServerExchange exchange, boolean headRequest, StreamSinkConduit channel, HeaderMap responseHeaders, ConduitListener<StreamSinkConduit> finishListener, String transferEncodingHeader) {
277
278         if (transferEncodingHeader == null) {
279             if (exchange.isHttp11()) {
280                 if (exchange.isPersistent()) {
281                     responseHeaders.put(Headers.TRANSFER_ENCODING, Headers.CHUNKED.toString());
282
283                     if (headRequest) {
284                         return channel;
285                     }
286                     return new ChunkedStreamSinkConduit(channel, exchange.getConnection().getByteBufferPool(), true, !exchange.isPersistent(), responseHeaders, finishListener, exchange);
287                 } else {
288                     if (headRequest) {
289                         return channel;
290                     }
291                     return new FinishableStreamSinkConduit(channel, finishListener);
292                 }
293             } else {
294                 exchange.setPersistent(false);
295                 responseHeaders.put(Headers.CONNECTION, Headers.CLOSE.toString());
296                 if (headRequest) {
297                     return channel;
298                 }
299                 return new FinishableStreamSinkConduit(channel, finishListener);
300             }
301         } else {
302             //moved outside because this is rarely used
303             //and makes the method small enough to be inlined
304             return handleExplicitTransferEncoding(exchange, channel, finishListener, responseHeaders, transferEncodingHeader, headRequest);
305         }
306     }
307
308     private static StreamSinkConduit handleExplicitTransferEncoding(HttpServerExchange exchange, StreamSinkConduit channel, ConduitListener<StreamSinkConduit> finishListener, HeaderMap responseHeaders, String transferEncodingHeader, boolean headRequest) {
309         HttpString transferEncoding = new HttpString(transferEncodingHeader);
310         if (transferEncoding.equals(Headers.CHUNKED)) {
311             if (headRequest) {
312                 return channel;
313             }
314             Boolean preChunked = exchange.getAttachment(HttpAttachments.PRE_CHUNKED_RESPONSE);
315             if(preChunked != null && preChunked) {
316                 return new PreChunkedStreamSinkConduit(channel, finishListener, exchange);
317             } else {
318                 return new ChunkedStreamSinkConduit(channel, exchange.getConnection().getByteBufferPool(), true, !exchange.isPersistent(), responseHeaders, finishListener, exchange);
319             }
320         } else {
321
322             if (headRequest) {
323                 return channel;
324             }
325             log.trace("Cancelling persistence because response is identity with no content length");
326             // make it not persistent - very unfortunate for the next request handler really...
327             exchange.setPersistent(false);
328             responseHeaders.put(Headers.CONNECTION, Headers.CLOSE.toString());
329             return new FinishableStreamSinkConduit(channel, terminateResponseListener(exchange));
330         }
331     }
332
333     /**
334      * fast long parsing algorithm
335      *
336      * @param str The string
337      * @return The long
338      */

339     public static long parsePositiveLong(String str) {
340         long value = 0;
341         final int length = str.length();
342
343         if (length == 0) {
344             throw new NumberFormatException(str);
345         }
346
347         long multiplier = 1;
348         for (int i = length - 1; i >= 0; --i) {
349             char c = str.charAt(i);
350
351             if (c < '0' || c > '9') {
352                 throw new NumberFormatException(str);
353             }
354             long digit = c - '0';
355             value += digit * multiplier;
356             multiplier *= 10;
357         }
358         return value;
359     }
360
361 }
362