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

package io.undertow.server;

import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.UndertowOptions;
import io.undertow.server.handlers.Cookie;
import io.undertow.util.DateUtils;
import io.undertow.util.HeaderMap;
import io.undertow.util.HeaderValues;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.LegacyCookieSupport;
import io.undertow.util.ParameterLimitException;
import io.undertow.util.StatusCodes;
import io.undertow.util.URLUtils;
import io.undertow.connector.PooledByteBuffer;
import org.xnio.channels.StreamSourceChannel;
import org.xnio.conduits.ConduitStreamSinkChannel;

import java.io.IOException;
import java.util.Date;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;

This class provides the connector part of the HttpServerExchange API.

It contains methods that logically belong on the exchange, however should only be used by connector implementations.

Author:Stuart Douglas, Richard Opalka
/** * This class provides the connector part of the {@link HttpServerExchange} API. * <p> * It contains methods that logically belong on the exchange, however should only be used * by connector implementations. * * @author Stuart Douglas * @author <a href="mailto:ropalka@redhat.com">Richard Opalka</a> */
public class Connectors { private static final boolean[] ALLOWED_TOKEN_CHARACTERS = new boolean[256]; private static final boolean[] ALLOWED_SCHEME_CHARACTERS = new boolean[256]; static { for(int i = 0; i < ALLOWED_TOKEN_CHARACTERS.length; ++i) { if((i >='0' && i <= '9') || (i >='a' && i <= 'z') || (i >='A' && i <= 'Z')) { ALLOWED_TOKEN_CHARACTERS[i] = true; } else { switch (i) { case '!': case '#': case '$': case '%': case '&': case '\'': case '*': case '+': case '-': case '.': case '^': case '_': case '`': case '|': case '~': { ALLOWED_TOKEN_CHARACTERS[i] = true; break; } default: ALLOWED_TOKEN_CHARACTERS[i] = false; } } } for(int i = 0; i < ALLOWED_SCHEME_CHARACTERS.length; ++i) { if((i >='0' && i <= '9') || (i >='a' && i <= 'z') || (i >='A' && i <= 'Z')) { ALLOWED_SCHEME_CHARACTERS[i] = true; } else { switch (i) { case '+': case '-': case '.': { ALLOWED_SCHEME_CHARACTERS[i] = true; break; } default: ALLOWED_SCHEME_CHARACTERS[i] = false; } } } }
Flattens the exchange cookie map into the response header map. This should be called by a connector just before the response is started.
Params:
  • exchange – The server exchange
/** * Flattens the exchange cookie map into the response header map. This should be called by a * connector just before the response is started. * * @param exchange The server exchange */
public static void flattenCookies(final HttpServerExchange exchange) { boolean enableRfc6265Validation = exchange.getConnection().getUndertowOptions().get(UndertowOptions.ENABLE_RFC6265_COOKIE_VALIDATION, UndertowOptions.DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION); for (Cookie cookie : exchange.responseCookies()) { exchange.getResponseHeaders().add(Headers.SET_COOKIE, getCookieString(cookie, enableRfc6265Validation)); } }
Adds the cookie into the response header map. This should be called before the response is started.
Params:
  • exchange – The server exchange
  • cookie – The cookie
/** * Adds the cookie into the response header map. This should be called * before the response is started. * * @param exchange The server exchange * @param cookie The cookie */
public static void addCookie(final HttpServerExchange exchange, Cookie cookie) { boolean enableRfc6265Validation = exchange.getConnection().getUndertowOptions().get(UndertowOptions.ENABLE_RFC6265_COOKIE_VALIDATION, UndertowOptions.DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION); exchange.getResponseHeaders().add(Headers.SET_COOKIE, getCookieString(cookie, enableRfc6265Validation)); }
Attached buffered data to the exchange. The will generally be used to allow data to be re-read.
Params:
  • exchange – The HTTP server exchange
  • buffers – The buffers to attach
/** * Attached buffered data to the exchange. The will generally be used to allow data to be re-read. * * @param exchange The HTTP server exchange * @param buffers The buffers to attach */
public static void ungetRequestBytes(final HttpServerExchange exchange, PooledByteBuffer... buffers) { PooledByteBuffer[] existing = exchange.getAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA); PooledByteBuffer[] newArray; if (existing == null) { newArray = new PooledByteBuffer[buffers.length]; System.arraycopy(buffers, 0, newArray, 0, buffers.length); } else { newArray = new PooledByteBuffer[existing.length + buffers.length]; System.arraycopy(existing, 0, newArray, 0, existing.length); System.arraycopy(buffers, 0, newArray, existing.length, buffers.length); } exchange.putAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA, newArray); //todo: force some kind of wakeup? exchange.addExchangeCompleteListener(BufferedRequestDataCleanupListener.INSTANCE); } private enum BufferedRequestDataCleanupListener implements ExchangeCompletionListener { INSTANCE; @Override public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { PooledByteBuffer[] bufs = exchange.getAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA); if (bufs != null) { for (PooledByteBuffer i : bufs) { if(i != null) { i.close(); } } } nextListener.proceed(); } } public static void terminateRequest(final HttpServerExchange exchange) { exchange.terminateRequest(); } public static void terminateResponse(final HttpServerExchange exchange) { exchange.terminateResponse(); } public static void resetRequestChannel(final HttpServerExchange exchange) { exchange.resetRequestChannel(); } private static String getCookieString(final Cookie cookie, boolean enableRfc6265Validation) { if(enableRfc6265Validation) { return addRfc6265ResponseCookieToExchange(cookie); } else { switch (LegacyCookieSupport.adjustedCookieVersion(cookie)) { case 0: return addVersion0ResponseCookieToExchange(cookie); case 1: default: return addVersion1ResponseCookieToExchange(cookie); } } } public static void setRequestStartTime(HttpServerExchange exchange) { exchange.setRequestStartTime(System.nanoTime()); } public static void setRequestStartTime(HttpServerExchange existing, HttpServerExchange newExchange) { newExchange.setRequestStartTime(existing.getRequestStartTime()); } private static String addRfc6265ResponseCookieToExchange(final Cookie cookie) { final StringBuilder header = new StringBuilder(cookie.getName()); header.append("="); if(cookie.getValue() != null) { header.append(cookie.getValue()); } if (cookie.getPath() != null) { header.append("; Path="); header.append(cookie.getPath()); } if (cookie.getDomain() != null) { header.append("; Domain="); header.append(cookie.getDomain()); } if (cookie.isDiscard()) { header.append("; Discard"); } if (cookie.isSecure()) { header.append("; Secure"); } if (cookie.isHttpOnly()) { header.append("; HttpOnly"); } if (cookie.getMaxAge() != null) { if (cookie.getMaxAge() >= 0) { header.append("; Max-Age="); header.append(cookie.getMaxAge()); } // Microsoft IE and Microsoft Edge don't understand Max-Age so send // expires as well. Without this, persistent cookies fail with those // browsers. They do understand Expires, even with V1 cookies. // So, we add Expires header when Expires is not explicitly specified. if (cookie.getExpires() == null) { if (cookie.getMaxAge() == 0) { Date expires = new Date(); expires.setTime(0); header.append("; Expires="); header.append(DateUtils.toOldCookieDateString(expires)); } else if (cookie.getMaxAge() > 0) { Date expires = new Date(); expires.setTime(expires.getTime() + cookie.getMaxAge() * 1000L); header.append("; Expires="); header.append(DateUtils.toOldCookieDateString(expires)); } } } if (cookie.getExpires() != null) { header.append("; Expires="); header.append(DateUtils.toDateString(cookie.getExpires())); } if (cookie.getComment() != null && !cookie.getComment().isEmpty()) { header.append("; Comment="); header.append(cookie.getComment()); } if (cookie.isSameSite()) { if (cookie.getSameSiteMode() != null && !cookie.getSameSiteMode().isEmpty()) { header.append("; SameSite="); header.append(cookie.getSameSiteMode()); } } return header.toString(); } private static String addVersion0ResponseCookieToExchange(final Cookie cookie) { final StringBuilder header = new StringBuilder(cookie.getName()); header.append("="); if(cookie.getValue() != null) { LegacyCookieSupport.maybeQuote(header, cookie.getValue()); } if (cookie.getPath() != null) { header.append("; path="); LegacyCookieSupport.maybeQuote(header, cookie.getPath()); } if (cookie.getDomain() != null) { header.append("; domain="); LegacyCookieSupport.maybeQuote(header, cookie.getDomain()); } if (cookie.isSecure()) { header.append("; secure"); } if (cookie.isHttpOnly()) { header.append("; HttpOnly"); } if (cookie.getExpires() != null) { header.append("; Expires="); header.append(DateUtils.toOldCookieDateString(cookie.getExpires())); } else if (cookie.getMaxAge() != null) { if (cookie.getMaxAge() >= 0) { header.append("; Max-Age="); header.append(cookie.getMaxAge()); } if (cookie.getMaxAge() == 0) { Date expires = new Date(); expires.setTime(0); header.append("; Expires="); header.append(DateUtils.toOldCookieDateString(expires)); } else if (cookie.getMaxAge() > 0) { Date expires = new Date(); expires.setTime(expires.getTime() + cookie.getMaxAge() * 1000L); header.append("; Expires="); header.append(DateUtils.toOldCookieDateString(expires)); } } if (cookie.isSameSite()) { if (cookie.getSameSiteMode() != null && !cookie.getSameSiteMode().isEmpty()) { header.append("; SameSite="); header.append(cookie.getSameSiteMode()); } } return header.toString(); } private static String addVersion1ResponseCookieToExchange(final Cookie cookie) { final StringBuilder header = new StringBuilder(cookie.getName()); header.append("="); if(cookie.getValue() != null) { LegacyCookieSupport.maybeQuote(header, cookie.getValue()); } header.append("; Version=1"); if (cookie.getPath() != null) { header.append("; Path="); LegacyCookieSupport.maybeQuote(header, cookie.getPath()); } if (cookie.getDomain() != null) { header.append("; Domain="); LegacyCookieSupport.maybeQuote(header, cookie.getDomain()); } if (cookie.isDiscard()) { header.append("; Discard"); } if (cookie.isSecure()) { header.append("; Secure"); } if (cookie.isHttpOnly()) { header.append("; HttpOnly"); } if (cookie.getMaxAge() != null) { if (cookie.getMaxAge() >= 0) { header.append("; Max-Age="); header.append(cookie.getMaxAge()); } // Microsoft IE and Microsoft Edge don't understand Max-Age so send // expires as well. Without this, persistent cookies fail with those // browsers. They do understand Expires, even with V1 cookies. // So, we add Expires header when Expires is not explicitly specified. if (cookie.getExpires() == null) { if (cookie.getMaxAge() == 0) { Date expires = new Date(); expires.setTime(0); header.append("; Expires="); header.append(DateUtils.toOldCookieDateString(expires)); } else if (cookie.getMaxAge() > 0) { Date expires = new Date(); expires.setTime(expires.getTime() + cookie.getMaxAge() * 1000L); header.append("; Expires="); header.append(DateUtils.toOldCookieDateString(expires)); } } } if (cookie.getExpires() != null) { header.append("; Expires="); header.append(DateUtils.toDateString(cookie.getExpires())); } if (cookie.getComment() != null && !cookie.getComment().isEmpty()) { header.append("; Comment="); LegacyCookieSupport.maybeQuote(header, cookie.getComment()); } if (cookie.isSameSite()) { if (cookie.getSameSiteMode() != null && !cookie.getSameSiteMode().isEmpty()) { header.append("; SameSite="); header.append(cookie.getSameSiteMode()); } } return header.toString(); } public static void executeRootHandler(final HttpHandler handler, final HttpServerExchange exchange) { try { exchange.setInCall(true); handler.handleRequest(exchange); exchange.setInCall(false); boolean resumed = exchange.isResumed(); if (exchange.isDispatched()) { if (resumed) { UndertowLogger.REQUEST_LOGGER.resumedAndDispatched(); exchange.setStatusCode(500); exchange.endExchange(); return; } final Runnable dispatchTask = exchange.getDispatchTask(); Executor executor = exchange.getDispatchExecutor(); exchange.setDispatchExecutor(null); exchange.unDispatch(); if (dispatchTask != null) { executor = executor == null ? exchange.getConnection().getWorker() : executor; try { executor.execute(dispatchTask); } catch (RejectedExecutionException e) { UndertowLogger.REQUEST_LOGGER.debug("Failed to dispatch to worker", e); exchange.setStatusCode(StatusCodes.SERVICE_UNAVAILABLE); exchange.endExchange(); } } } else if (!resumed) { exchange.endExchange(); } else { exchange.runResumeReadWrite(); } } catch (Throwable t) { exchange.putAttachment(DefaultResponseListener.EXCEPTION, t); exchange.setInCall(false); if (!exchange.isResponseStarted()) { exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); } if(t instanceof IOException) { UndertowLogger.REQUEST_IO_LOGGER.ioException((IOException) t); } else { UndertowLogger.REQUEST_LOGGER.undertowRequestFailed(t, exchange); } exchange.endExchange(); } }
Sets the request path and query parameters, decoding to the requested charset.
Params:
  • exchange – The exchange
  • encodedPath – The encoded path
  • charset – The charset
/** * Sets the request path and query parameters, decoding to the requested charset. * * @param exchange The exchange * @param encodedPath The encoded path * @param charset The charset */
@Deprecated public static void setExchangeRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, boolean decode, final boolean allowEncodedSlash, StringBuilder decodeBuffer) { try { setExchangeRequestPath(exchange, encodedPath, charset, decode, allowEncodedSlash, decodeBuffer, exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS)); } catch (ParameterLimitException e) { throw new RuntimeException(e); } }
Sets the request path and query parameters, decoding to the requested charset.
Params:
  • exchange – The exchange
  • encodedPath – The encoded path
  • charset – The charset
/** * Sets the request path and query parameters, decoding to the requested charset. * * @param exchange The exchange * @param encodedPath The encoded path * @param charset The charset */
public static void setExchangeRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, boolean decode, final boolean allowEncodedSlash, StringBuilder decodeBuffer, int maxParameters) throws ParameterLimitException { boolean requiresDecode = false; final StringBuilder pathBuilder = new StringBuilder(); int currentPathPartIndex = 0; for (int i = 0; i < encodedPath.length(); ++i) { char c = encodedPath.charAt(i); if (c == '?') { String part; String encodedPart = encodedPath.substring(currentPathPartIndex, i); if (requiresDecode) { part = URLUtils.decode(encodedPart, charset, allowEncodedSlash,false, decodeBuffer); } else { part = encodedPart; } pathBuilder.append(part); part = pathBuilder.toString(); exchange.setRequestPath(part); exchange.setRelativePath(part); exchange.setRequestURI(encodedPath.substring(0, i)); final String qs = encodedPath.substring(i + 1); exchange.setQueryString(qs); URLUtils.parseQueryString(qs, exchange, charset, decode, maxParameters); return; } else if(c == ';') { String part; String encodedPart = encodedPath.substring(currentPathPartIndex, i); if (requiresDecode) { part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, false, decodeBuffer); } else { part = encodedPart; } pathBuilder.append(part); exchange.setRequestURI(encodedPath); currentPathPartIndex = i + 1 + URLUtils.parsePathParams(encodedPath.substring(i + 1), exchange, charset, decode, maxParameters); i = currentPathPartIndex -1 ; } else if(c == '%' || c == '+') { requiresDecode = decode; } } String part; String encodedPart = encodedPath.substring(currentPathPartIndex); if (requiresDecode) { part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, false, decodeBuffer); } else { part = encodedPart; } pathBuilder.append(part); part = pathBuilder.toString(); exchange.setRequestPath(part); exchange.setRelativePath(part); exchange.setRequestURI(encodedPath); }
Returns the existing request channel, if it exists. Otherwise returns null
Params:
  • exchange – The http server exchange
/** * Returns the existing request channel, if it exists. Otherwise returns null * * @param exchange The http server exchange */
public static StreamSourceChannel getExistingRequestChannel(final HttpServerExchange exchange) { return exchange.requestChannel; } public static boolean isEntityBodyAllowed(HttpServerExchange exchange){ int code = exchange.getStatusCode(); return isEntityBodyAllowed(code); } public static boolean isEntityBodyAllowed(int code) { if(code >= 100 && code < 200) { return false; } if(code == 204 || code == 304) { return false; } return true; } public static void updateResponseBytesSent(HttpServerExchange exchange, long bytes) { exchange.updateBytesSent(bytes); } public static ConduitStreamSinkChannel getConduitSinkChannel(HttpServerExchange exchange) { return exchange.getConnection().getSinkChannel(); }
Verifies that the contents of the HttpString are a valid token according to rfc7230.
Params:
  • header – The header to verify
/** * Verifies that the contents of the HttpString are a valid token according to rfc7230. * @param header The header to verify */
public static void verifyToken(HttpString header) { int length = header.length(); for(int i = 0; i < length; ++i) { byte c = header.byteAt(i); if(!ALLOWED_TOKEN_CHARACTERS[c]) { throw UndertowMessages.MESSAGES.invalidToken(c); } } }
Returns true if the token character is valid according to rfc7230
/** * Returns true if the token character is valid according to rfc7230 */
public static boolean isValidTokenCharacter(byte c) { return ALLOWED_TOKEN_CHARACTERS[c]; } public static boolean isValidSchemeCharacter(byte c) { return ALLOWED_SCHEME_CHARACTERS[c]; }
Verifies that the provided request headers are valid according to rfc7230. In particular: - At most one content-length or transfer encoding
/** * Verifies that the provided request headers are valid according to rfc7230. In particular: * - At most one content-length or transfer encoding */
public static boolean areRequestHeadersValid(HeaderMap headers) { HeaderValues te = headers.get(Headers.TRANSFER_ENCODING); HeaderValues cl = headers.get(Headers.CONTENT_LENGTH); if(te != null && cl != null) { return false; } else if(te != null && te.size() > 1) { return false; } else if(cl != null && cl.size() > 1) { return false; } return true; } }