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;
20
21 import io.undertow.UndertowLogger;
22 import io.undertow.UndertowMessages;
23 import io.undertow.UndertowOptions;
24 import io.undertow.server.handlers.Cookie;
25 import io.undertow.util.DateUtils;
26 import io.undertow.util.HeaderMap;
27 import io.undertow.util.HeaderValues;
28 import io.undertow.util.Headers;
29 import io.undertow.util.HttpString;
30 import io.undertow.util.LegacyCookieSupport;
31 import io.undertow.util.ParameterLimitException;
32 import io.undertow.util.StatusCodes;
33 import io.undertow.util.URLUtils;
34 import io.undertow.connector.PooledByteBuffer;
35 import org.xnio.channels.StreamSourceChannel;
36 import org.xnio.conduits.ConduitStreamSinkChannel;
37
38 import java.io.IOException;
39 import java.util.Date;
40 import java.util.Map;
41 import java.util.concurrent.Executor;
42 import java.util.concurrent.RejectedExecutionException;
43
44 /**
45  * This class provides the connector part of the {@link HttpServerExchange} API.
46  * <p>
47  * It contains methods that logically belong on the exchange, however should only be used
48  * by connector implementations.
49  *
50  * @author Stuart Douglas
51  */

52 public class Connectors {
53
54     private static final boolean[] ALLOWED_TOKEN_CHARACTERS = new boolean[256];
55
56     static {
57         for(int i = 0; i < ALLOWED_TOKEN_CHARACTERS.length; ++i) {
58             if((i >='0' && i <= '9') ||
59                     (i >='a' && i <= 'z') ||
60                     (i >='A' && i <= 'Z')) {
61                 ALLOWED_TOKEN_CHARACTERS[i] = true;
62             } else {
63                 switch (i) {
64                     case '!':
65                     case '#':
66                     case '$':
67                     case '%':
68                     case '&':
69                     case '\'':
70                     case '*':
71                     case '+':
72                     case '-':
73                     case '.':
74                     case '^':
75                     case '_':
76                     case '`':
77                     case '|':
78                     case '~': {
79                         ALLOWED_TOKEN_CHARACTERS[i] = true;
80                         break;
81                     }
82                     default:
83                         ALLOWED_TOKEN_CHARACTERS[i] = false;
84                 }
85             }
86         }
87     }
88     /**
89      * Flattens the exchange cookie map into the response header map. This should be called by a
90      * connector just before the response is started.
91      *
92      * @param exchange The server exchange
93      */

94     public static void flattenCookies(final HttpServerExchange exchange) {
95         Map<String, Cookie> cookies = exchange.getResponseCookiesInternal();
96         boolean enableRfc6265Validation = exchange.getConnection().getUndertowOptions().get(UndertowOptions.ENABLE_RFC6265_COOKIE_VALIDATION, UndertowOptions.DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION);
97         if (cookies != null) {
98             for (Map.Entry<String, Cookie> entry : cookies.entrySet()) {
99                 exchange.getResponseHeaders().add(Headers.SET_COOKIE, getCookieString(entry.getValue(), enableRfc6265Validation));
100             }
101         }
102     }
103
104     /**
105      * Adds the cookie into the response header map. This should be called
106      * before the response is started.
107      *
108      * @param exchange The server exchange
109      * @param cookie   The cookie
110      */

111     public static void addCookie(final HttpServerExchange exchange, Cookie cookie) {
112         boolean enableRfc6265Validation = exchange.getConnection().getUndertowOptions().get(UndertowOptions.ENABLE_RFC6265_COOKIE_VALIDATION, UndertowOptions.DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION);
113         exchange.getResponseHeaders().add(Headers.SET_COOKIE, getCookieString(cookie, enableRfc6265Validation));
114     }
115
116     /**
117      * Attached buffered data to the exchange. The will generally be used to allow data to be re-read.
118      *
119      * @param exchange The HTTP server exchange
120      * @param buffers  The buffers to attach
121      */

122     public static void ungetRequestBytes(final HttpServerExchange exchange, PooledByteBuffer... buffers) {
123         PooledByteBuffer[] existing = exchange.getAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA);
124         PooledByteBuffer[] newArray;
125         if (existing == null) {
126             newArray = new PooledByteBuffer[buffers.length];
127             System.arraycopy(buffers, 0, newArray, 0, buffers.length);
128         } else {
129             newArray = new PooledByteBuffer[existing.length + buffers.length];
130             System.arraycopy(existing, 0, newArray, 0, existing.length);
131             System.arraycopy(buffers, 0, newArray, existing.length, buffers.length);
132         }
133         exchange.putAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA, newArray); //todo: force some kind of wakeup?
134         exchange.addExchangeCompleteListener(BufferedRequestDataCleanupListener.INSTANCE);
135     }
136
137     private enum BufferedRequestDataCleanupListener implements ExchangeCompletionListener {
138         INSTANCE;
139
140         @Override
141         public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) {
142             PooledByteBuffer[] bufs = exchange.getAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA);
143             if (bufs != null) {
144                 for (PooledByteBuffer i : bufs) {
145                     if(i != null) {
146                         i.close();
147                     }
148                 }
149             }
150             nextListener.proceed();
151         }
152     }
153
154     public static void terminateRequest(final HttpServerExchange exchange) {
155         exchange.terminateRequest();
156     }
157
158     public static void terminateResponse(final HttpServerExchange exchange) {
159         exchange.terminateResponse();
160     }
161
162     public static void resetRequestChannel(final HttpServerExchange exchange) {
163         exchange.resetRequestChannel();
164     }
165
166     private static String getCookieString(final Cookie cookie, boolean enableRfc6265Validation) {
167         if(enableRfc6265Validation) {
168             return addRfc6265ResponseCookieToExchange(cookie);
169         } else {
170             switch (LegacyCookieSupport.adjustedCookieVersion(cookie)) {
171                 case 0:
172                     return addVersion0ResponseCookieToExchange(cookie);
173                 case 1:
174                 default:
175                     return addVersion1ResponseCookieToExchange(cookie);
176             }
177         }
178     }
179
180     public static void setRequestStartTime(HttpServerExchange exchange) {
181         exchange.setRequestStartTime(System.nanoTime());
182     }
183
184     public static void setRequestStartTime(HttpServerExchange existing, HttpServerExchange newExchange) {
185         newExchange.setRequestStartTime(existing.getRequestStartTime());
186     }
187
188     private static String addRfc6265ResponseCookieToExchange(final Cookie cookie) {
189         final StringBuilder header = new StringBuilder(cookie.getName());
190         header.append("=");
191         if(cookie.getValue() != null) {
192             header.append(cookie.getValue());
193         }
194         if (cookie.getPath() != null) {
195             header.append("; Path=");
196             header.append(cookie.getPath());
197         }
198         if (cookie.getDomain() != null) {
199             header.append("; Domain=");
200             header.append(cookie.getDomain());
201         }
202         if (cookie.isDiscard()) {
203             header.append("; Discard");
204         }
205         if (cookie.isSecure()) {
206             header.append("; Secure");
207         }
208         if (cookie.isHttpOnly()) {
209             header.append("; HttpOnly");
210         }
211         if (cookie.getMaxAge() != null) {
212             if (cookie.getMaxAge() >= 0) {
213                 header.append("; Max-Age=");
214                 header.append(cookie.getMaxAge());
215             }
216             // Microsoft IE and Microsoft Edge don't understand Max-Age so send
217             // expires as well. Without this, persistent cookies fail with those
218             // browsers. They do understand Expires, even with V1 cookies.
219             // So, we add Expires header when Expires is not explicitly specified.
220             if (cookie.getExpires() == null) {
221                 if (cookie.getMaxAge() == 0) {
222                     Date expires = new Date();
223                     expires.setTime(0);
224                     header.append("; Expires=");
225                     header.append(DateUtils.toOldCookieDateString(expires));
226                 } else if (cookie.getMaxAge() > 0) {
227                     Date expires = new Date();
228                     expires.setTime(expires.getTime() + cookie.getMaxAge() * 1000L);
229                     header.append("; Expires=");
230                     header.append(DateUtils.toOldCookieDateString(expires));
231                 }
232             }
233         }
234         if (cookie.getExpires() != null) {
235             header.append("; Expires=");
236             header.append(DateUtils.toDateString(cookie.getExpires()));
237         }
238         if (cookie.getComment() != null && !cookie.getComment().isEmpty()) {
239             header.append("; Comment=");
240             header.append(cookie.getComment());
241         }
242         if (cookie.isSameSite()) {
243             if (cookie.getSameSiteMode() != null && !cookie.getSameSiteMode().isEmpty()) {
244                 header.append("; SameSite=");
245                 header.append(cookie.getSameSiteMode());
246             }
247         }
248         return header.toString();
249     }
250
251     private static String addVersion0ResponseCookieToExchange(final Cookie cookie) {
252         final StringBuilder header = new StringBuilder(cookie.getName());
253         header.append("=");
254         if(cookie.getValue() != null) {
255             LegacyCookieSupport.maybeQuote(header, cookie.getValue());
256         }
257
258         if (cookie.getPath() != null) {
259             header.append("; path=");
260             LegacyCookieSupport.maybeQuote(header, cookie.getPath());
261         }
262         if (cookie.getDomain() != null) {
263             header.append("; domain=");
264             LegacyCookieSupport.maybeQuote(header, cookie.getDomain());
265         }
266         if (cookie.isSecure()) {
267             header.append("; secure");
268         }
269         if (cookie.isHttpOnly()) {
270             header.append("; HttpOnly");
271         }
272         if (cookie.getExpires() != null) {
273             header.append("; Expires=");
274             header.append(DateUtils.toOldCookieDateString(cookie.getExpires()));
275         } else if (cookie.getMaxAge() != null) {
276             if (cookie.getMaxAge() >= 0) {
277                 header.append("; Max-Age=");
278                 header.append(cookie.getMaxAge());
279             }
280             if (cookie.getMaxAge() == 0) {
281                 Date expires = new Date();
282                 expires.setTime(0);
283                 header.append("; Expires=");
284                 header.append(DateUtils.toOldCookieDateString(expires));
285             } else if (cookie.getMaxAge() > 0) {
286                 Date expires = new Date();
287                 expires.setTime(expires.getTime() + cookie.getMaxAge() * 1000L);
288                 header.append("; Expires=");
289                 header.append(DateUtils.toOldCookieDateString(expires));
290             }
291         }
292         if (cookie.isSameSite()) {
293             if (cookie.getSameSiteMode() != null && !cookie.getSameSiteMode().isEmpty()) {
294                 header.append("; SameSite=");
295                 header.append(cookie.getSameSiteMode());
296             }
297         }
298         return header.toString();
299
300     }
301
302     private static String addVersion1ResponseCookieToExchange(final Cookie cookie) {
303
304         final StringBuilder header = new StringBuilder(cookie.getName());
305         header.append("=");
306         if(cookie.getValue() != null) {
307             LegacyCookieSupport.maybeQuote(header, cookie.getValue());
308         }
309         header.append("; Version=1");
310         if (cookie.getPath() != null) {
311             header.append("; Path=");
312             LegacyCookieSupport.maybeQuote(header, cookie.getPath());
313         }
314         if (cookie.getDomain() != null) {
315             header.append("; Domain=");
316             LegacyCookieSupport.maybeQuote(header, cookie.getDomain());
317         }
318         if (cookie.isDiscard()) {
319             header.append("; Discard");
320         }
321         if (cookie.isSecure()) {
322             header.append("; Secure");
323         }
324         if (cookie.isHttpOnly()) {
325             header.append("; HttpOnly");
326         }
327         if (cookie.getMaxAge() != null) {
328             if (cookie.getMaxAge() >= 0) {
329                 header.append("; Max-Age=");
330                 header.append(cookie.getMaxAge());
331             }
332             // Microsoft IE and Microsoft Edge don't understand Max-Age so send
333             // expires as well. Without this, persistent cookies fail with those
334             // browsers. They do understand Expires, even with V1 cookies.
335             // So, we add Expires header when Expires is not explicitly specified.
336             if (cookie.getExpires() == null) {
337                 if (cookie.getMaxAge() == 0) {
338                     Date expires = new Date();
339                     expires.setTime(0);
340                     header.append("; Expires=");
341                     header.append(DateUtils.toOldCookieDateString(expires));
342                 } else if (cookie.getMaxAge() > 0) {
343                     Date expires = new Date();
344                     expires.setTime(expires.getTime() + cookie.getMaxAge() * 1000L);
345                     header.append("; Expires=");
346                     header.append(DateUtils.toOldCookieDateString(expires));
347                 }
348             }
349         }
350         if (cookie.getExpires() != null) {
351             header.append("; Expires=");
352             header.append(DateUtils.toDateString(cookie.getExpires()));
353         }
354         if (cookie.getComment() != null && !cookie.getComment().isEmpty()) {
355             header.append("; Comment=");
356             LegacyCookieSupport.maybeQuote(header, cookie.getComment());
357         }
358         if (cookie.isSameSite()) {
359             if (cookie.getSameSiteMode() != null && !cookie.getSameSiteMode().isEmpty()) {
360                 header.append("; SameSite=");
361                 header.append(cookie.getSameSiteMode());
362             }
363         }
364         return header.toString();
365     }
366
367     public static void executeRootHandler(final HttpHandler handler, final HttpServerExchange exchange) {
368         try {
369             exchange.setInCall(true);
370             handler.handleRequest(exchange);
371             exchange.setInCall(false);
372             boolean resumed = exchange.isResumed();
373             if (exchange.isDispatched()) {
374                 if (resumed) {
375                     UndertowLogger.REQUEST_LOGGER.resumedAndDispatched();
376                     exchange.setStatusCode(500);
377                     exchange.endExchange();
378                     return;
379                 }
380                 final Runnable dispatchTask = exchange.getDispatchTask();
381                 Executor executor = exchange.getDispatchExecutor();
382                 exchange.setDispatchExecutor(null);
383                 exchange.unDispatch();
384                 if (dispatchTask != null) {
385                     executor = executor == null ? exchange.getConnection().getWorker() : executor;
386                     try {
387                         executor.execute(dispatchTask);
388                     } catch (RejectedExecutionException e) {
389                         UndertowLogger.REQUEST_LOGGER.debug("Failed to dispatch to worker", e);
390                         exchange.setStatusCode(StatusCodes.SERVICE_UNAVAILABLE);
391                         exchange.endExchange();
392                     }
393                 }
394             } else if (!resumed) {
395                 exchange.endExchange();
396             } else {
397                 exchange.runResumeReadWrite();
398             }
399         } catch (Throwable t) {
400             exchange.putAttachment(DefaultResponseListener.EXCEPTION, t);
401             exchange.setInCall(false);
402             if (!exchange.isResponseStarted()) {
403                 exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR);
404             }
405             if(t instanceof IOException) {
406                 UndertowLogger.REQUEST_IO_LOGGER.ioException((IOException) t);
407             } else {
408                 UndertowLogger.REQUEST_LOGGER.undertowRequestFailed(t, exchange);
409             }
410             exchange.endExchange();
411         }
412     }
413
414     /**
415      * Sets the request path and query parameters, decoding to the requested charset.
416      *
417      * @param exchange    The exchange
418      * @param encodedPath        The encoded path
419      * @param charset     The charset
420      */

421     @Deprecated
422     public static void setExchangeRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, boolean decode, final boolean allowEncodedSlash, StringBuilder decodeBuffer) {
423         try {
424             setExchangeRequestPath(exchange, encodedPath, charset, decode, allowEncodedSlash, decodeBuffer, exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS));
425         } catch (ParameterLimitException e) {
426             throw new RuntimeException(e);
427         }
428     }
429         /**
430          * Sets the request path and query parameters, decoding to the requested charset.
431          *
432          * @param exchange    The exchange
433          * @param encodedPath        The encoded path
434          * @param charset     The charset
435          */

436     public static void setExchangeRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, boolean decode, final boolean allowEncodedSlash, StringBuilder decodeBuffer, int maxParameters) throws ParameterLimitException {
437         boolean requiresDecode = false;
438         final StringBuilder pathBuilder = new StringBuilder();
439         int currentPathPartIndex = 0;
440         for (int i = 0; i < encodedPath.length(); ++i) {
441             char c = encodedPath.charAt(i);
442             if (c == '?') {
443                 String part;
444                 String encodedPart = encodedPath.substring(currentPathPartIndex, i);
445                 if (requiresDecode) {
446                     part = URLUtils.decode(encodedPart, charset, allowEncodedSlash,false, decodeBuffer);
447                 } else {
448                     part = encodedPart;
449                 }
450                 pathBuilder.append(part);
451                 part = pathBuilder.toString();
452                 exchange.setRequestPath(part);
453                 exchange.setRelativePath(part);
454                 exchange.setRequestURI(encodedPath.substring(0, i));
455                 final String qs = encodedPath.substring(i + 1);
456                 exchange.setQueryString(qs);
457                 URLUtils.parseQueryString(qs, exchange, charset, decode, maxParameters);
458                 return;
459             } else if(c == ';') {
460                 String part;
461                 String encodedPart = encodedPath.substring(currentPathPartIndex, i);
462                 if (requiresDecode) {
463                     part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, false, decodeBuffer);
464                 } else {
465                     part = encodedPart;
466                 }
467                 pathBuilder.append(part);
468                 exchange.setRequestURI(encodedPath);
469                 currentPathPartIndex = i + 1 + URLUtils.parsePathParams(encodedPath.substring(i + 1), exchange, charset, decode, maxParameters);
470                 i = currentPathPartIndex -1 ;
471             } else if(c == '%' || c == '+') {
472                 requiresDecode = decode;
473             }
474         }
475
476         String part;
477         String encodedPart = encodedPath.substring(currentPathPartIndex);
478         if (requiresDecode) {
479             part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, false, decodeBuffer);
480         } else {
481             part = encodedPart;
482         }
483         pathBuilder.append(part);
484         part = pathBuilder.toString();
485         exchange.setRequestPath(part);
486         exchange.setRelativePath(part);
487         exchange.setRequestURI(encodedPath);
488     }
489
490
491     /**
492      * Returns the existing request channel, if it exists. Otherwise returns null
493      *
494      * @param exchange The http server exchange
495      */

496     public static StreamSourceChannel getExistingRequestChannel(final HttpServerExchange exchange) {
497         return exchange.requestChannel;
498     }
499
500     public static boolean isEntityBodyAllowed(HttpServerExchange exchange){
501         int code = exchange.getStatusCode();
502         return isEntityBodyAllowed(code);
503     }
504
505     public static boolean isEntityBodyAllowed(int code) {
506         if(code >= 100 && code < 200) {
507             return false;
508         }
509         if(code == 204 || code == 304) {
510             return false;
511         }
512         return true;
513     }
514
515     public static void updateResponseBytesSent(HttpServerExchange exchange, long bytes) {
516         exchange.updateBytesSent(bytes);
517     }
518
519     public static ConduitStreamSinkChannel getConduitSinkChannel(HttpServerExchange exchange) {
520         return exchange.getConnection().getSinkChannel();
521     }
522
523     /**
524      * Verifies that the contents of the HttpString are a valid token according to rfc7230.
525      * @param header The header to verify
526      */

527     public static void verifyToken(HttpString header) {
528         int length = header.length();
529         for(int i = 0; i < length; ++i) {
530             byte c = header.byteAt(i);
531             if(!ALLOWED_TOKEN_CHARACTERS[c]) {
532                 throw UndertowMessages.MESSAGES.invalidToken(c);
533             }
534         }
535     }
536
537     /**
538      * Returns true if the token character is valid according to rfc7230
539      */

540     public static boolean isValidTokenCharacter(byte c) {
541         return ALLOWED_TOKEN_CHARACTERS[c];
542     }
543
544
545     /**
546      * Verifies that the provided request headers are valid according to rfc7230. In particular:
547      * - At most one content-length or transfer encoding
548      */

549     public static boolean areRequestHeadersValid(HeaderMap headers) {
550         HeaderValues te = headers.get(Headers.TRANSFER_ENCODING);
551         HeaderValues cl = headers.get(Headers.CONTENT_LENGTH);
552         if(te != null && cl != null) {
553             return false;
554         } else if(te != null && te.size() > 1) {
555             return false;
556         } else if(cl != null && cl.size() > 1) {
557             return false;
558         }
559         return true;
560     }
561 }
562