CVE-2024-4027
Description
A flaw was found in Undertow. Servlets using a method that calls HttpServletRequestImpl.getParameterNames() can cause an OutOfMemoryError when the client sends a request with large parameter names. This issue can be exploited by an unauthorized user to cause a remote denial-of-service (DoS) attack.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.undertow:undertow-coreMaven | < 2.2.39.Final | 2.2.39.Final |
io.undertow:undertow-coreMaven | >= 2.4.0.Alpha1, < 2.4.0.Beta1 | 2.4.0.Beta1 |
io.undertow:undertow-coreMaven | >= 2.3.0.Alpha1, < 2.3.21.Final | 2.3.21.Final |
Patches
4fb14baa51b61Merge pull request #1894 from fl4via/backport-fixes_2.4.x
37 files changed · +1145 −115
core/src/main/java/io/undertow/attribute/QueryStringAttribute.java+1 −1 modified@@ -42,7 +42,7 @@ private QueryStringAttribute(boolean includeQuestionMark) { @Override public String readAttribute(final HttpServerExchange exchange) { - String qs = exchange.getQueryString(); + String qs = exchange.getDecodedQueryString(); if(qs.isEmpty() || !includeQuestionMark) { return qs; }
core/src/main/java/io/undertow/attribute/RequestLineAttribute.java+2 −2 modified@@ -42,9 +42,9 @@ public String readAttribute(final HttpServerExchange exchange) { .append(exchange.getRequestMethod().toString()) .append(' ') .append(exchange.getRequestURI()); - if (!exchange.getQueryString().isEmpty()) { + if (!exchange.getDecodedQueryString().isEmpty()) { sb.append('?'); - sb.append(exchange.getQueryString()); + sb.append(exchange.getDecodedQueryString()); } sb.append(' ') .append(exchange.getProtocol().toString()).toString();
core/src/main/java/io/undertow/conduits/FixedLengthStreamSourceConduit.java+5 −0 modified@@ -371,6 +371,11 @@ private void exitRead(long consumed, Throwable readError) throws IOException { } long newVal = oldVal - consumed; state = newVal; + if (allAreClear(state, MASK_COUNT)) { + if (allAreClear(state, FLAG_FINISHED)) { + next.suspendReads(); + } + } } private void invokeFinishListener() {
core/src/main/java/io/undertow/Handlers.java+12 −0 modified@@ -32,6 +32,7 @@ import io.undertow.server.handlers.DisableCacheHandler; import io.undertow.server.handlers.ExceptionHandler; import io.undertow.server.handlers.GracefulShutdownHandler; +import io.undertow.server.handlers.HostHeaderHandler; import io.undertow.server.handlers.HttpContinueAcceptingHandler; import io.undertow.server.handlers.HttpContinueReadHandler; import io.undertow.server.handlers.HttpTraceHandler; @@ -600,6 +601,17 @@ public static LearningPushHandler learningPushHandler(int maxEntries, HttpHandle return new LearningPushHandler(maxEntries, -1, next); } + /** + * Creates a handler that automatically vets Host header content/absence/presence according to + * https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 and related + * + * @param next The next handler + * @return A host header handler + */ + public static HostHeaderHandler hostHeaderHandler(HttpHandler next) { + return new HostHeaderHandler(next); + } + private Handlers() { }
core/src/main/java/io/undertow/security/handlers/SinglePortConfidentialityHandler.java+1 −1 modified@@ -65,7 +65,7 @@ protected URI getRedirectURI(final HttpServerExchange exchange, final int port) } } uriBuilder.append(uri); - final String queryString = exchange.getQueryString(); + final String queryString = exchange.getDecodedQueryString(); if (queryString != null && !queryString.isEmpty()) { uriBuilder.append("?").append(queryString); }
core/src/main/java/io/undertow/security/impl/DigestAuthenticationMechanism.java+4 −4 modified@@ -235,17 +235,17 @@ private AuthenticationMechanismOutcome handleDigestHeader(HttpServerExchange exc if(parsedHeader.containsKey(DigestAuthorizationToken.DIGEST_URI)) { String uri = parsedHeader.get(DigestAuthorizationToken.DIGEST_URI); String requestURI = exchange.getRequestURI(); - if(!exchange.getQueryString().isEmpty()) { - requestURI = requestURI + "?" + exchange.getQueryString(); + if(!exchange.getDecodedQueryString().isEmpty()) { + requestURI = requestURI + "?" + exchange.getDecodedQueryString(); } if(!uri.equals(requestURI)) { //it is possible we were given an absolute URI //we reconstruct the URI from the host header to make sure they match up //I am not sure if this is overly strict, however I think it is better //to be safe than sorry requestURI = exchange.getRequestURL(); - if(!exchange.getQueryString().isEmpty()) { - requestURI = requestURI + "?" + exchange.getQueryString(); + if(!exchange.getDecodedQueryString().isEmpty()) { + requestURI = requestURI + "?" + exchange.getDecodedQueryString(); } if(!uri.equals(requestURI)) { //just end the auth process
core/src/main/java/io/undertow/server/Connectors.java+2 −3 modified@@ -569,10 +569,9 @@ public static void setExchangeRequestPath(final HttpServerExchange exchange, fin } if(requiresDecode && allowUnescapedCharactersInUrl) { final String decodedQS = URLUtils.decode(qs, charset, decodeSlashFlag,false, decodeBuffer); - exchange.setQueryString(decodedQS); - } else { - exchange.setQueryString(qs); + exchange.setDecodedQueryString(decodedQS); } + exchange.setQueryString(qs); URLUtils.parseQueryString(qs, exchange, charset, decodeQueryString, maxParameters); return;
core/src/main/java/io/undertow/server/handlers/accesslog/ExtendedAccessLogParser.java+2 −2 modified@@ -328,15 +328,15 @@ protected ExchangeAttribute getClientToServerElement( return new ExchangeAttribute() { @Override public String readAttribute(HttpServerExchange exchange) { - String query = exchange.getQueryString(); + String query = exchange.getDecodedQueryString(); if (query.isEmpty()) { return exchange.getRequestURI(); } else { StringBuilder buf = new StringBuilder(); buf.append(exchange.getRequestURI()); buf.append('?'); - buf.append(exchange.getQueryString()); + buf.append(exchange.getDecodedQueryString()); return buf.toString(); } }
core/src/main/java/io/undertow/server/handlers/form/MultiPartParserDefinition.java+1 −1 modified@@ -109,7 +109,7 @@ public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener.proceed(); } }); - Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE); + Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MULTIPART_MAX_ENTITY_SIZE ); if(sizeLimit != null && sizeLimit > 0) { // do not overwrite the entity size with sizeLimit that is <= 0 exchange.setMaxEntitySize(sizeLimit); }
core/src/main/java/io/undertow/server/handlers/HostHeaderHandler.java+295 −0 added@@ -0,0 +1,295 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; + +import java.util.regex.Pattern; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.NetworkUtils; +import io.undertow.util.Protocols; +import io.undertow.util.StatusCodes; + +/** + * Handler which check if Host header is properly formed and present. + * + * @author baranowb + */ +public class HostHeaderHandler implements HttpHandler { + + public static final HandlerWrapper WRAPPER = new Wrapper(); + + public static final String STATUS_NO_HOST_HEADER = "No Host Header"; + public static final String STATUS_TOO_MANY_HOST_HEADERS = "Only One Host Header Allowed"; + public static final String STATUS_MALFORMED_PORT = "Host Header Malformed Port"; + public static final String STATUS_MALFORMED_IP_LITERAL = "Host Header Malformed IP-Literal"; + public static final String STATUS_MALFORMED_IP_LITERAL_BAD_CHARS = "Host Header Bad Characters"; + public static final String STATUS_HOST_NO_MATCH = "URI Host Header NO MATCH"; + private static final Pattern IP4_EXACT = Pattern.compile(NetworkUtils.IP4_EXACT); + private static final Pattern IP6_EXACT = Pattern.compile(NetworkUtils.IP6_EXACT); + private static final boolean[] ALLOWED_REGNAME_CHARACTERS = new boolean[256]; + private static final boolean[] HEX_CHARACTERS = new boolean[256]; + private static final boolean[] ALLOWED_IPv_FUTURE_CHARACTERS = new boolean[256]; // this is almost the same as + // ALLOWED_REGNAME_CHARACTERS with bonus + // ":" but having it as array rather than + // extra check is just faster + static { + // reg-name = *( unreserved / pct-encoded / sub-delims ) + // ALPHA / DIGIT / "-" / "." / "_" / "~" , %%, and "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + for (int i = 0; i < ALLOWED_REGNAME_CHARACTERS.length; ++i) { + if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z')) { + ALLOWED_REGNAME_CHARACTERS[i] = true; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = true; + } else { + switch (i) { + case '-': + case '.': + case '_': + case '~': + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': { + ALLOWED_REGNAME_CHARACTERS[i] = true; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = true; + break; + } + default: + ALLOWED_REGNAME_CHARACTERS[i] = false; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = false; + } + } + + } + + ALLOWED_IPv_FUTURE_CHARACTERS[':'] = true; + + for (int i = 0; i < HEX_CHARACTERS.length; ++i) { + if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) { + HEX_CHARACTERS[i] = true; + } else + HEX_CHARACTERS[i] = false; + } + + } + private final HttpHandler next; + + public HostHeaderHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + // TODO: add debug/warn log? + // 400 if in case of no Host header or more than one: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 + // 400 if value violate rules. Host = uri-host [ ":" port ] + // uri-host https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 + // port https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3 + // NOTE: that 3.2.2 is NOT as restrictive as pure DNS it allows subdelims, percent etc by design as to not be + // restricted to pure DNS, ALTHOUGH DNS compliant content is preferred. + final HeaderMap headerMap = exchange.getRequestHeaders(); + final HeaderValues headerValues = headerMap.get(Headers.HOST); + final HttpString protocol = exchange.getProtocol(); + + if((protocol.equals(Protocols.HTTP_0_9) || protocol.equals(Protocols.HTTP_1_0))){ + if (headerValues == null) { + //TODO: should we fake Host till we make it? + next.handleRequest(exchange); + return; + } + //else { + //clients want to be good citizens and send it anyway. + //fall through to below, first check will be false, but rest is the same as for HTTP1.1+ + //} + } + + if (headerValues == null || headerValues.size() == 0) { + // isEmpty - we assume http/https ? so authority is defined for this type, it cant be empty? + terminate(exchange, STATUS_NO_HOST_HEADER); + return; + } else if (headerValues.size() > 1) { + terminate(exchange, STATUS_TOO_MANY_HOST_HEADERS); + return; + } + + + // parsing time. + final String headerValue = headerValues.element(); + // uri-host [ ":" port ] + // This is tricky, IP-Literal contain :, which is port delimiter in pair + // Lets just try to take take care of port first + final int rightBracketIndex = headerValue.lastIndexOf(']'); + final int lastColonIndex = headerValue.lastIndexOf(':'); + + final String hostHeaderURI; + // in case of IPv4, rightBracketIndex will be -1, in case of IPv6, it MUST be less than last : + if (rightBracketIndex < lastColonIndex) { + // we have port or potentially malformed IP Literal: + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" - without right bracket + if (rightBracketIndex == -1 && headerValue.startsWith("[")) { + // bad [ = 0, ] = -1, : = n+ + // good [ = 0, ] = n , : = n+x + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + // we have valid host-uri with port + final String portString = headerValue.substring(lastColonIndex + 1); + try { + int port = Integer.parseInt(portString); + if (port <= 0 || port > 65535) { + // sanity check + // NOTE: 3.2.3 does not have provision like for IPv4 - decimal between 0-255. + // so this might be too restrictive + terminate(exchange, STATUS_MALFORMED_PORT); + return; + } + // fall through to uri-host checks + } catch (NumberFormatException nfe) { + terminate(exchange, STATUS_MALFORMED_PORT); + return; + } + + hostHeaderURI = headerValue.substring(0, lastColonIndex); + } else { + hostHeaderURI = headerValue; + } + + // at this point we either have IP-Literal, IPv4 address or custom name + if (rightBracketIndex > 0 && hostHeaderURI.indexOf('[') != 0) { + // 1:2:4]* + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } else if (rightBracketIndex > 0 && hostHeaderURI.indexOf('[') == 0) { + // IPv6 or IPFuture + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" + // IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + final String debracked = hostHeaderURI.substring(1, hostHeaderURI.length() - 1); + if (debracked.startsWith("v")) { + final int dotIndex = debracked.indexOf("."); + if (dotIndex < 2) { + // we need at least one HEX + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + + final String hex = debracked.substring(1, dotIndex); + for (int i = 0; i < hex.length(); i++) { + final char c1 = hex.charAt(i); + if (!HEX_CHARACTERS[c1]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } + if (dotIndex + 1 >= debracked.length()) { + // we need some character behind dot. + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + + for (int i = dotIndex + 1; i < debracked.length(); i++) { + final char c = debracked.charAt(i); + if (!ALLOWED_IPv_FUTURE_CHARACTERS[c]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } + } else { + // This will match IPv6 and IPv6 with embedded IPv4 + if (!IP6_EXACT.matcher(debracked).matches()) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + // TODO: as in case of IPv6 do we need to vet some weir daddresses? + } + + } else if (IP4_EXACT.matcher(hostHeaderURI).matches()) { + // IPv4 + // NOTE: above will match only valid range 0-255, rest will fall through to reg-name, which is ok, since its + // essentially + // superset of IP, given DIGIT + "." ( unreserved ) + } else { + // registered name can contain . and digits, so it technically overlap IPv4. + // above wont cover -192.168.1.1/355.0.0.0, which technically is correct 'reg-name' + // at this point we can really only check if its valid char or percent encoding. + for (int index = 0; index < hostHeaderURI.length(); index++) { + final char c = hostHeaderURI.charAt(index); + if (c == '%') { + // we need at least two more that are HEX + if (index + 2 < hostHeaderURI.length()) { + char c1 = hostHeaderURI.charAt(++index); + char c2 = hostHeaderURI.charAt(++index); + if (!(HEX_CHARACTERS[c1] && HEX_CHARACTERS[c2])) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } else { + // we dont have at least two chars(hex), so its not proper percent escape + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + } else if (!ALLOWED_REGNAME_CHARACTERS[c]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + // valid + } + } + // NOTE: at this point if userinfo("@") was present in host, it would have failed + // we need only to check if Host header value is contained within URI if its absolute or authority + if (exchange.isHostIncludedInRequestURI()) { + if (!exchange.getRequestURI().contains(hostHeaderURI)) { + terminate(exchange, STATUS_HOST_NO_MATCH); + return; + } else if (hostHeaderURI.isEmpty()) { + terminate(exchange, STATUS_HOST_NO_MATCH); + return; + } + } + + // in the end... + next.handleRequest(exchange); + } + + private void terminate(final HttpServerExchange exchange, final String message) { + exchange.setStatusCode(StatusCodes.BAD_REQUEST); + exchange.setResponseContentLength(0); + exchange.getResponseHeaders().add(Headers.CONNECTION, Headers.CLOSE.toString()); + exchange.setReasonPhrase(message); + exchange.endExchange(); + } + + private static class Wrapper implements HandlerWrapper { + + @Override + public HttpHandler wrap(HttpHandler handler) { + return new HostHeaderHandler(handler); + } + } +}
core/src/main/java/io/undertow/server/handlers/HttpTraceHandler.java+2 −2 modified@@ -49,9 +49,9 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "message/http"); StringBuilder body = new StringBuilder("TRACE "); body.append(exchange.getRequestURI()); - if(!exchange.getQueryString().isEmpty()) { + if(!exchange.getDecodedQueryString().isEmpty()) { body.append('?'); - body.append(exchange.getQueryString()); + body.append(exchange.getDecodedQueryString()); } body.append(' '); body.append(exchange.getProtocol().toString());
core/src/main/java/io/undertow/server/handlers/JDBCLogHandler.java+1 −1 modified@@ -135,7 +135,7 @@ public void logMessage(String pattern, HttpServerExchange exchange) { } else { jdbcLogAttribute.user = sc.getAuthenticatedAccount().getPrincipal().getName(); } - jdbcLogAttribute.query = exchange.getQueryString(); + jdbcLogAttribute.query = exchange.getDecodedQueryString(); jdbcLogAttribute.bytes = exchange.getResponseContentLength(); if (jdbcLogAttribute.bytes < 0) {
core/src/main/java/io/undertow/server/handlers/LearningPushHandler.java+3 −3 modified@@ -77,12 +77,12 @@ public LearningPushHandler(int maxPathEtries, int maxPathAge, int maxPushEtries, public void handleRequest(HttpServerExchange exchange) throws Exception { String fullPath; String requestPath; - if(exchange.getQueryString().isEmpty()) { + if(exchange.getDecodedQueryString().isEmpty()) { fullPath = exchange.getRequestURL(); requestPath = exchange.getRequestPath(); } else{ - fullPath = exchange.getRequestURL() + "?" + exchange.getQueryString(); - requestPath = exchange.getRequestPath() + "?" + exchange.getQueryString(); + fullPath = exchange.getRequestURL() + "?" + exchange.getDecodedQueryString(); + requestPath = exchange.getRequestPath() + "?" + exchange.getDecodedQueryString(); } doPush(exchange, fullPath);
core/src/main/java/io/undertow/server/handlers/proxy/ProxyHandler.java+1 −1 modified@@ -449,7 +449,7 @@ public void run() { } requestURI.append(targetURI); - String qs = exchange.getNonDecodedQueryString(); + String qs = exchange.getQueryString(); if (qs != null && !qs.isEmpty()) { requestURI.append('?'); requestURI.append(qs);
core/src/main/java/io/undertow/server/handlers/RequestDumpingHandler.java+1 −1 modified@@ -98,7 +98,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { sb.append("\n"); } sb.append(" protocol=" + exchange.getProtocol() + "\n"); - sb.append(" queryString=" + exchange.getQueryString() + "\n"); + sb.append(" queryString=" + exchange.getDecodedQueryString() + "\n"); sb.append(" remoteAddr=" + exchange.getSourceAddress() + "\n"); sb.append(" remoteHost=" + exchange.getSourceAddress().getHostName() + "\n"); sb.append(" scheme=" + exchange.getRequestScheme() + "\n");
core/src/main/java/io/undertow/server/handlers/resource/DirectoryUtils.java+2 −2 modified@@ -66,12 +66,12 @@ public static boolean sendRequestedBlobs(HttpServerExchange exchange) { String type = null; String etag = null; String quotedEtag = null; - if ("css".equals(exchange.getQueryString())) { + if ("css".equals(exchange.getDecodedQueryString())) { buffer = Blobs.FILE_CSS_BUFFER.duplicate(); type = "text/css"; etag = Blobs.FILE_CSS_ETAG; quotedEtag = Blobs.FILE_CSS_ETAG_QUOTED; - } else if ("js".equals(exchange.getQueryString())) { + } else if ("js".equals(exchange.getDecodedQueryString())) { buffer = Blobs.FILE_JS_BUFFER.duplicate(); type = "application/javascript"; etag = Blobs.FILE_JS_ETAG;
core/src/main/java/io/undertow/server/handlers/StuckThreadDetectionHandler.java+1 −1 modified@@ -154,7 +154,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { // GC'ing, as the reference is removed from the Map in the finally clause Long key = Thread.currentThread().getId(); - MonitoredThread monitoredThread = new MonitoredThread(Thread.currentThread(), exchange.getRequestURI() + exchange.getQueryString()); + MonitoredThread monitoredThread = new MonitoredThread(Thread.currentThread(), exchange.getRequestURI() + exchange.getDecodedQueryString()); activeThreads.put(key, monitoredThread); if(timerKey == null) { synchronized (this) {
core/src/main/java/io/undertow/server/handlers/URLDecodingHandler.java+1 −1 modified@@ -83,7 +83,7 @@ private static void decodePath(HttpServerExchange exchange, String charset, Stri } private static void decodeQueryString(HttpServerExchange exchange, String charset, StringBuilder sb) { - if (!exchange.getQueryString().isEmpty()) { + if (!exchange.getDecodedQueryString().isEmpty()) { final TreeMap<String, Deque<String>> newParams = new TreeMap<>(); for (Map.Entry<String, Deque<String>> param : exchange.getQueryParameters().entrySet()) { final Deque<String> newValues = new ArrayDeque<>(param.getValue().size());
core/src/main/java/io/undertow/server/HttpServerExchange.java+56 −18 modified@@ -208,13 +208,14 @@ public final class HttpServerExchange extends AbstractAttachable { private String resolvedPath = ""; /** - * the query string - percent encoded + * the unencoded query string (i.e. percent encoded), in its original form as it appears in the received request. */ private String queryString = ""; + /** - * the non-decoded query string. Set only when query string goes through decoding + * the decoded query string, if there was any decoding done */ - private String nonDecodedQueryString = null; + private String decodedQueryString = null; private int requestWrapperCount = 0; private ConduitWrapper<StreamSourceConduit>[] requestWrappers; //we don't allocate these by default, as for get requests they are not used @@ -581,24 +582,25 @@ public HttpServerExchange setResolvedPath(final String resolvedPath) { } /** + * Returns the query string for this request. * - * @return The query string, without the leading ? + * @return The query string as originally appeared in the request, without the leading ? */ public String getQueryString() { - return queryString; + return this.queryString; } /** - * Set query string. Leading {@code '?'} char will be removed automatically. + * Sets the query string, unencoded and in its original form as it appears in the received request. + * Leading {@code '?'} char will be removed automatically.<p> * + * @param queryString the query string as originally contained in the request, without any decoding * @return this http server exchange */ public HttpServerExchange setQueryString(final String queryString) { - // Clean leading ? - if( queryString.length() > 0 && queryString.charAt(0) == '?' ) { - this.queryString = queryString.substring(1); - } else { - this.queryString = queryString; + this.queryString = cleanQueryString(queryString); + if (this.queryString == null) { + this.queryString = ""; } return this; } @@ -608,27 +610,63 @@ public HttpServerExchange setQueryString(final String queryString) { * The returned string does not contain the leading {@code '?'} char. * * @return The request query string, without the leading {@code '?'}, non-decoded. + * + * @deprecated use {@link #getQueryString()} instead */ + @Deprecated(forRemoval = true, since="2.3.20.Final") public String getNonDecodedQueryString() { - return this.nonDecodedQueryString == null? this.queryString: this.nonDecodedQueryString; + return getQueryString(); } /** * Sets the non-decoded query string. Leading {@code '?'} char will be removed automatically.<p> * Must be invoked only if the {@link #getQueryString() query string} has gone through decoding. In such case, we expect * that both forms of the query string will be set in the exchange: {@link #setQueryString decoded} and non-decoded. * - * @param nonDecodedQueryString the query string as originally contained in the request, without any decoding + * @param unencodedQueryString the query string as originally contained in the request, without any decoding * @return this http server exchange + * + * @deprecated Use #setQueryString instead */ - public HttpServerExchange setNonDecodedQueryString(String nonDecodedQueryString) { + @Deprecated(forRemoval = true, since="2.3.20.Final") + public HttpServerExchange setNonDecodedQueryString(String unencodedQueryString) { + return setQueryString(unencodedQueryString); + } + + /** + * Returns the query string in its decoded form if available, which will depend on configs such as + * {@link UndertowOptions#ALLOW_UNESCAPED_CHARACTERS_IN_URL}. + * If unavailable, the decoded query string is just the same as {@link #getQueryString} + * + * @return The request query string, without the leading {@code '?'}, post parsing, decoded. + */ + public String getDecodedQueryString() { + return this.decodedQueryString != null && this.decodedQueryString.length() > 0 ? this.decodedQueryString : this.queryString; + } + + /** + * Sets the decoded query string. + * Leading {@code '?'} char will be removed automatically.<p> + * Must be invoked only if the {@link #getQueryString() query string} has gone through decoding. In such case, we expect + * that both forms of the query string will be set in the exchange: decoded and {@link #setQueryString non-decoded} + * + * @param decodedQueryString the request query string, without the leading {@code '?'}, post parsing, decoded. + * @return this http server exchange + */ + public HttpServerExchange setDecodedQueryString(String decodedQueryString) { + this.decodedQueryString = cleanQueryString(decodedQueryString); + return this; + } + + private String cleanQueryString(String queryString) { // Clean leading ? - if( nonDecodedQueryString.length() > 0 && nonDecodedQueryString.charAt(0) == '?' ) { - this.nonDecodedQueryString = nonDecodedQueryString.substring(1); + if (queryString == null) { + return queryString; + } else if( queryString.length() > 0 && queryString.charAt(0) == '?' ) { + return queryString.substring(1); } else { - this.nonDecodedQueryString = nonDecodedQueryString; + return queryString; } - return this; } /**
core/src/main/java/io/undertow/server/protocol/ajp/AjpRequestParser.java+21 −18 modified@@ -434,7 +434,7 @@ public void parse(final ByteBuffer buf, final AjpRequestParseState state, final state.currentAttribute = result.value; state.currentIntegerPart = -1; } - String result; + String result, decodedResult = null; boolean decodingAlreadyDone = false; boolean decodeUnescapedCharacters = false; final StringHolder resultHolder; @@ -453,70 +453,73 @@ public void parse(final ByteBuffer buf, final AjpRequestParseState state, final return; } decodeUnescapedCharacters = resultHolder.containsUrlCharacters && allowUnescapedCharactersInUrl; + result = resultHolder.value; if(resultHolder.containsUnencodedCharacters || decodeUnescapedCharacters) { try { - result = decode(resultHolder.value, true); + decodedResult = decode(resultHolder.value, true); } catch (UrlDecodeException | UnsupportedEncodingException e) { UndertowLogger.REQUEST_IO_LOGGER.failedToParseRequest(e); state.badRequest = true; result = resultHolder.value; } decodingAlreadyDone = true; - } else { - result = resultHolder.value; } } + final String finalResult = decodedResult != null ? decodedResult : result; //query string. if (state.currentAttribute.equals(QUERY_STRING)) { String resultAsQueryString = result == null ? "" : result; exchange.setQueryString(resultAsQueryString); + exchange.setDecodedQueryString(decodedResult); try { if (decodeUnescapedCharacters) { // decoding needs to be done again here, to preserve the parameters and form decoding, even if it has been done at resultAsQueryString // for more info see UNDERTOW-2312 and UNDERTOW-2555 URLUtils.parseQueryString(resultHolder == null || resultHolder.value == null ? "" : resultHolder.value, exchange, encoding, doDecode, maxParameters); + } else if (decodingAlreadyDone) { + URLUtils.parseQueryString(decodedResult, exchange, encoding, + false, maxParameters); } else { - URLUtils.parseQueryString(resultAsQueryString, exchange, encoding, - doDecode && !decodingAlreadyDone, maxParameters); + URLUtils.parseQueryString(resultAsQueryString, exchange, encoding, doDecode, maxParameters); } } catch (ParameterLimitException | IllegalArgumentException e) { UndertowLogger.REQUEST_IO_LOGGER.failedToParseRequest(e); state.badRequest = true; } } else if (state.currentAttribute.equals(REMOTE_USER)) { - exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_PRINCIPAL, result); - exchange.putAttachment(HttpServerExchange.REMOTE_USER, result); + exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_PRINCIPAL, finalResult); + exchange.putAttachment(HttpServerExchange.REMOTE_USER, finalResult); } else if (state.currentAttribute.equals(AUTH_TYPE)) { - exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_AUTHENTICATION_TYPE, result); + exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_AUTHENTICATION_TYPE, finalResult); } else if (state.currentAttribute.equals(STORED_METHOD)) { - HttpString requestMethod = new HttpString(result); + HttpString requestMethod = new HttpString(finalResult); Connectors.verifyToken(requestMethod); exchange.setRequestMethod(requestMethod); } else if (state.currentAttribute.equals(AJP_REMOTE_PORT)) { - state.remotePort = Integer.parseInt(result); + state.remotePort = Integer.parseInt(finalResult); } else if (state.currentAttribute.equals(SSL_SESSION)) { - state.sslSessionId = result; + state.sslSessionId = finalResult; } else if (state.currentAttribute.equals(SSL_CIPHER)) { - state.sslCipher = result; + state.sslCipher = finalResult; } else if (state.currentAttribute.equals(SSL_CERT)) { - state.sslCert = result; + state.sslCert = finalResult; } else if (state.currentAttribute.equals(SSL_KEY_SIZE)) { - state.sslKeySize = result; + state.sslKeySize = finalResult; } else if (state.currentAttribute.equals(AJP_SSL_PROTOCOL)) { - state.secureProtocol = result; + state.secureProtocol = finalResult; } else { // other attributes if (state.attributes == null) { state.attributes = new TreeMap<>(); } if (ATTR_SET.contains(state.currentAttribute)) { // known attirubtes - state.attributes.put(state.currentAttribute, result); + state.attributes.put(state.currentAttribute, finalResult); } else if (allowedRequestAttributesPattern != null) { // custom allowed attributes Matcher m = allowedRequestAttributesPattern.matcher(state.currentAttribute); if (m.matches()) { - state.attributes.put(state.currentAttribute, result); + state.attributes.put(state.currentAttribute, finalResult); } } }
core/src/main/java/io/undertow/server/protocol/http2/Http2ReceiveListener.java+1 −0 modified@@ -243,6 +243,7 @@ void handleInitialRequest(HttpServerExchange initial, Http2Channel channel, byte exchange.setRequestScheme(initial.getRequestScheme()); exchange.setRequestMethod(initial.getRequestMethod()); exchange.setQueryString(initial.getQueryString()); + exchange.setDecodedQueryString(initial.getDecodedQueryString()); for (Map.Entry<String, Deque<String>> pathParamEntry: initial.getPathParameters().entrySet()) { for (String pathParamValue : pathParamEntry.getValue()) { exchange.addPathParam(pathParamEntry.getKey(), pathParamValue);
core/src/main/java/io/undertow/server/protocol/http/HttpReadListener.java+8 −16 modified@@ -27,12 +27,11 @@ import io.undertow.server.ConnectorStatisticsImpl; import io.undertow.server.Connectors; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.HostHeaderHandler; import io.undertow.server.protocol.ParseTimeoutUpdater; import io.undertow.server.protocol.http2.Http2ReceiveListener; import io.undertow.util.ClosingChannelExceptionHandler; import io.undertow.util.ConnectionUtils; -import io.undertow.util.HeaderValues; -import io.undertow.util.Headers; import io.undertow.util.HttpString; import io.undertow.util.Methods; import io.undertow.util.Protocols; @@ -78,7 +77,6 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann private final long maxEntitySize; private final boolean recordRequestStartTime; private final boolean allowUnknownProtocols; - private final boolean requireHostHeader; //0 = new request ok, reads resumed //1 = request running, new request not ok @@ -98,7 +96,6 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann this.maxRequestSize = connection.getUndertowOptions().get(UndertowOptions.MAX_HEADER_SIZE, UndertowOptions.DEFAULT_MAX_HEADER_SIZE); this.maxEntitySize = connection.getUndertowOptions().get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); this.recordRequestStartTime = connection.getUndertowOptions().get(UndertowOptions.RECORD_REQUEST_START_TIME, false); - this.requireHostHeader = connection.getUndertowOptions().get(UndertowOptions.REQUIRE_HOST_HTTP11, true); this.allowUnknownProtocols = connection.getUndertowOptions().get(UndertowOptions.ALLOW_UNKNOWN_PROTOCOLS, UndertowOptions.DEFAULT_ALLOW_UNKNOWN_PROTOCOLS); int requestParseTimeout = connection.getUndertowOptions().get(UndertowOptions.REQUEST_PARSE_TIMEOUT, -1); int requestIdleTimeout = connection.getUndertowOptions().get(UndertowOptions.NO_REQUEST_TIMEOUT, -1); @@ -109,6 +106,10 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann connection.addCloseListener(parseTimeoutUpdater); } state = new ParseState(connection.getUndertowOptions().get(UndertowOptions.HTTP_HEADERS_CACHE_SIZE, UndertowOptions.DEFAULT_HTTP_HEADERS_CACHE_SIZE)); + + if (connection.getUndertowOptions().contains(UndertowOptions.REQUIRE_HOST_HTTP11)) { + UndertowLogger.ROOT_LOGGER.configurationNotSupported("REQUIRE_HOST_HTTP11"); + } } public void newRequest() { @@ -247,22 +248,13 @@ public void handleEventWithNoRunningRequest(final ConduitStreamSourceChannel cha channel.suspendReads(); } - HeaderValues host = httpServerExchange.getRequestHeaders().get(Headers.HOST); - if(host != null && host.size() > 1) { - sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.moreThanOneHostHeader()); - return; - } - if(requireHostHeader && httpServerExchange.getProtocol().equals(Protocols.HTTP_1_1)) { - if(host == null || host.size() ==0 || host.getFirst().isEmpty()) { - sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.noHostInHttp11Request()); - return; - } - } if(!Connectors.areRequestHeadersValid(httpServerExchange.getRequestHeaders())) { sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.invalidHeaders()); return; } - Connectors.executeRootHandler(connection.getRootHandler(), httpServerExchange); + + //TODO: make this configurable/dynamic, to either allow users provide their own here or in chain + Connectors.executeRootHandler(HostHeaderHandler.WRAPPER.wrap(connection.getRootHandler()), httpServerExchange); } catch (Throwable t) { sendBadRequestAndClose(connection.getChannel(), t); return;
core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java+2 −2 modified@@ -568,11 +568,11 @@ final void handleQueryParameters(ByteBuffer buffer, ParseState state, HttpServer } if (next == ' ' || next == '\t') { String queryString = stringBuilder.toString(); + exchange.setQueryString(queryString); if(urlDecodeRequired && this.allowUnescapedCharactersInUrl) { - exchange.setNonDecodedQueryString(queryString); queryString = decode(queryString, urlDecodeRequired, state, slashDecodingFlag, false); + exchange.setDecodedQueryString(queryString); } - exchange.setQueryString(queryString); if (nextQueryParam == null) { if (queryParamPos != stringBuilder.length()) { exchange.addQueryParam(decode(stringBuilder.substring(queryParamPos), nextQueryParamDecodeRequired, state, true, true), "");
core/src/main/java/io/undertow/server/RequestStatistics.java+1 −1 modified@@ -48,7 +48,7 @@ public long getProcessingTime() { } public String getQueryString() { - return exchange.getQueryString(); + return exchange.getDecodedQueryString(); } public String getUri() {
core/src/main/java/io/undertow/UndertowLogger.java+4 −0 modified@@ -488,4 +488,8 @@ void nodeConfigCreated(URI connectionURI, String balancer, String domain, String @LogMessage(level = WARN) @Message(id = 5107, value = "Failed to set web socket timeout.") void failedToSetWSTimeout(@Cause Exception e); + + @LogMessage(level = WARN) + @Message(id = 5108, value = "Configuration option is no longer supported: %s.") + void configurationNotSupported(String string); } \ No newline at end of file
core/src/main/java/io/undertow/UndertowOptions.java+7 −2 modified@@ -51,9 +51,14 @@ public class UndertowOptions { public static final Option<Long> MULTIPART_MAX_ENTITY_SIZE = Option.simple(UndertowOptions.class, "MULTIPART_MAX_ENTITY_SIZE", Long.class); /** - * We do not have a default upload limit + * Default maximum upload size 2MB */ - public static final long DEFAULT_MAX_ENTITY_SIZE = -1; + public static final long DEFAULT_MAX_ENTITY_SIZE = 2097152; + + /** + * Default maximum multipart upload size 2MB + */ + public static final long DEFAULT_MULTIPART_MAX_ENTITY_SIZE = 2097152; /** * If we should buffer pipelined requests. Defaults to false.
core/src/main/java/io/undertow/util/NetworkUtils.java+31 −14 modified@@ -33,29 +33,46 @@ */ public class NetworkUtils { - public static final String IP4_EXACT = "(?:\\d{1,3}\\.){3}\\d{1,3}"; + /** + * IPv4Segment: (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]) + */ + public static final String IP4_SEGMENT = "(25[0-5]|(?:2[0-4]|(?:(1{0,1}[0-9]))){0,1}(?:([0-9])))"; + /** + * ?: - unnamed groups are used for performance reasons. + * IPv4Segment: 25[0-5]|((2[0-4]|1){0,1}[0-9]){0,1}[0-9]) -extra () is not needed but makes it clear for OR + * IPv4Address: (IPv4Segment\.){3,3}IPv4Segment + */ + public static final String IP4_EXACT = "(?:"+IP4_SEGMENT+"\\.){3,3}(?:"+IP4_SEGMENT+")"; + /** + * IPv6Segment: ([0-9a-fA-F]{1,4}) - 1 to 4 hex digits + */ + public static final String IP6_SEGMENT = "([0-9a-fA-F]{1,4})"; /** * IPV6 match. ?: - unnamed groups are used for performance reasons. * Requirements: * - match full or partial IPV6 ( sliding '::') * - match end to start - ^$ to ensure it does not match part of some random (\d:){n,m} * - IPv4-Embedded IPv6 Address + * - TODO: special types of addresses ? + * Explanation: + * IPv6Segment: ([0-9a-fA-F]{1,4}) - 1 to 4 hex digits + * IPv6Address: (IPv6Segment:){1,n}IPv6Segment or (IPv6Segment:){1,x}(:IPv6Segment){1,y} where x+y<8 + * ^ shift of segments ~left to right with qualifiers + * Above is just general form for pure IPv6 without any spices * - * NO: - * - IPv4 mapped/translated into IPv6 - * - * ^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4} - full address - * |(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:)) - last compressed - * |(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4}) - second to last - * |(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2} - etc - * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3} - * |(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4} - * |(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5} - * |(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6} - * |(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))))$ - all the way compressed + * ^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4} - 1:2:3:4:5:6:7:8 + * |(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:)) - 1:: 1:2:3:4:5:6:7:: + * |(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4}) - 1::8 1:2:3:4:5:6::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2} - 1::7:8 1:2:3:4:5::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3} - 1::6:7:8 1:2:3:4::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4} - 1::5:6:7:8 1:2:3::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5} - 1::4:5:6:7:8 1:2::8 + * |(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6} - 1::3:4:5:6:7:8 1::8 + * |(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))) - ::2:3:4:5:6:7:8 ::8 :: + * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:IPv4Address)))$ - 1.2.3.4::192.168.1.1 1::192.168.1.1 */ - public static final String IP6_EXACT = "^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:))|(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4})|(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2}|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3}|(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5}|(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6}|(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))))$"; + public static final String IP6_EXACT = "^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:))|(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4})|(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2}|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3}|(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5}|(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6}|(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:))))|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:"+IP4_EXACT+")))$"; public static String formatPossibleIpv6Address(String address) { if (address == null) {
core/src/main/java/io/undertow/util/WeakCopyOnWriteMap.java+170 −0 added@@ -0,0 +1,170 @@ +/* + * 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.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.WeakHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +/** + * A basic copy on write map. It simply delegates to an underlying map, that is swapped out + * every time the map is updated. + * + * Note: this is not a secure map. It should not be used in situations where the map is populated + * from user input. + * + * @author Stuart Douglas + */ +public class WeakCopyOnWriteMap<K,V> implements ConcurrentMap<K, V> { + + private volatile Map<K, V> delegate = Collections.emptyMap(); + + public WeakCopyOnWriteMap() { + } + + public WeakCopyOnWriteMap(Map<K, V> existing) { + this.delegate = new WeakHashMap<>(existing); + } + + @Override + public synchronized V putIfAbsent(K key, V value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing != null) { + return existing; + } + putInternal(key, value); + return null; + } + + @Override + public synchronized boolean remove(Object key, Object value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing.equals(value)) { + removeInternal(key); + return true; + } + return false; + } + + @Override + public synchronized boolean replace(K key, V oldValue, V newValue) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing.equals(oldValue)) { + putInternal(key, newValue); + return true; + } + return false; + } + + @Override + public synchronized V replace(K key, V value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing != null) { + putInternal(key, value); + return existing; + } + return null; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public V get(Object key) { + return delegate.get(key); + } + + @Override + public synchronized V put(K key, V value) { + return putInternal(key, value); + } + + @Override + public synchronized V remove(Object key) { + return removeInternal(key); + } + + @Override + public synchronized void putAll(Map<? extends K, ? extends V> m) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + for(Entry<? extends K, ? extends V> e : m.entrySet()) { + delegate.put(e.getKey(), e.getValue()); + } + this.delegate = delegate; + } + + @Override + public synchronized void clear() { + delegate = Collections.emptyMap(); + } + + @Override + public Set<K> keySet() { + return delegate.keySet(); + } + + @Override + public Collection<V> values() { + return delegate.values(); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return delegate.entrySet(); + } + + //must be called under lock + private V putInternal(final K key, final V value) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + V existing = delegate.put(key, value); + this.delegate = delegate; + return existing; + } + + public V removeInternal(final Object key) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + V existing = delegate.remove(key); + this.delegate = delegate; + return existing; + } +}
core/src/main/java/io/undertow/websockets/core/WebSocketChannel.java+3 −3 modified@@ -214,7 +214,7 @@ protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException { } catch (WebSocketException e) { //the data was corrupt //send a close message - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); markReadsBroken(e); if (WebSocketLogger.REQUEST_LOGGER.isDebugEnabled()) { WebSocketLogger.REQUEST_LOGGER.debugf(e, "receive failed due to Exception"); @@ -278,10 +278,10 @@ protected void handleBrokenSourceChannel(Throwable e) { getFramePriority().immediateCloseFrame(); WebSockets.sendClose(new CloseMessage(CloseMessage.MSG_CONTAINS_INVALID_DATA, e.getMessage()).toByteBuffer(), this, null); } else if (e instanceof WebSocketInvalidCloseCodeException) { - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); } else if (e instanceof WebSocketFrameCorruptedException) { getFramePriority().immediateCloseFrame(); - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); } }
core/src/test/java/io/undertow/server/ConnectionTerminationTestCase.java+22 −8 modified@@ -18,21 +18,24 @@ package io.undertow.server; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpOneOnly; +import io.undertow.testutils.ProxyIgnore; +import io.undertow.util.FileUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xnio.IoUtils; +import org.xnio.OptionMap; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.xnio.IoUtils; -import io.undertow.testutils.DefaultServer; -import io.undertow.testutils.HttpOneOnly; -import io.undertow.testutils.ProxyIgnore; -import io.undertow.util.FileUtils; +import static io.undertow.UndertowOptions.MAX_ENTITY_SIZE; /** * Tests abnormal connection termination @@ -96,4 +99,15 @@ public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener IoUtils.safeClose(socket); } } + + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + }
core/src/test/java/io/undertow/server/ExactLengthReadTimeoutTestCase.java+143 −0 added@@ -0,0 +1,143 @@ +/* + * 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 java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import io.undertow.server.handlers.BlockingHandler; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpOneOnly; +import io.undertow.testutils.TestHttpClient; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; +import org.apache.http.HttpResponse; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xnio.OptionMap; +import org.xnio.Options; + +/** + * + * Tests to ensure no read timeout after an exact read of the content-length + * + * @author Stuart Douglas + * @author Flavia Rainone + * @author Aaron Ogburn + */ +@RunWith(DefaultServer.class) +@HttpOneOnly +public class ExactLengthReadTimeoutTestCase { + + private static volatile String message; + + private static final String DATA = "1234567890ABCDEF"; + + private static final int DATA_MULTIPLE = 2048; + + @BeforeClass + public static void setup() { + final BlockingHandler blockingHandler = new BlockingHandler(); + DefaultServer.setRootHandler(blockingHandler); + blockingHandler.setRootHandler(new HttpHandler() { + @Override + public void handleRequest(final HttpServerExchange exchange) { + try { + final OutputStream outputStream = exchange.getOutputStream(); + final InputStream inputStream = exchange.getInputStream(); + + long length = exchange.getRequestContentLength(); + byte[] b = new byte[DATA_MULTIPLE * DATA.length()]; + int i = 1; + StringBuilder builder = new StringBuilder(); + // read exact content length + while (i > 0 && length > 0) { + i = inputStream.read(b); + if (i > 0) { + length -=i; + builder.append(new String(b, 0, i)); + } + } + + // this shouldn't cause timeout after complete read + try { + Thread.sleep(200); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + Assert.assertEquals(message, builder.toString()); + inputStream.close(); + outputStream.close(); + } catch (IOException e) { + exchange.getResponseHeaders().put(Headers.CONNECTION, "close"); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); + throw new RuntimeException(e); + } + } + }); + } + + @DefaultServer.BeforeServerStarts + public static void beforeClass() { + DefaultServer.setServerOptions(OptionMap.create(Options.READ_TIMEOUT, 100)); + } + + @DefaultServer.AfterServerStops + public static void afterClass() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + + @Test + public void testExactLengthReadTimeout() throws InterruptedException, IOException { + StringBuilder builder = new StringBuilder(1000 * DATA.length()); + + for (int i = 0; i < DATA_MULTIPLE; ++i) { + try { + builder.append(DATA); + } catch (Throwable e) { + throw new RuntimeException("test failed with i equal to " + i, e); + } + } + + message = builder.toString(); + final TestHttpClient client = new TestHttpClient(); + try { + HttpPost post = new HttpPost(DefaultServer.getDefaultServerURL() + "/path"); + post.setEntity(new StringEntity(message)); + post.addHeader(Headers.CONNECTION_STRING, "close"); + boolean socketFailure = false; + try { + // Request should succeed. + HttpResponse result = client.execute(post); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + } catch (NoHttpResponseException e) { + Assert.fail("No response was received, this was presumably caused by read-timeout closing the connection."); + } + } finally { + client.getConnectionManager().shutdown(); + } + } +}
core/src/test/java/io/undertow/server/handlers/blocking/SimpleBlockingServerTestCase.java+13 −0 modified@@ -23,6 +23,7 @@ import java.io.InputStream; import java.io.OutputStream; +import io.undertow.UndertowOptions; import io.undertow.io.IoCallback; import io.undertow.io.Sender; import io.undertow.server.HttpHandler; @@ -44,6 +45,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; /** * @author Stuart Douglas @@ -57,6 +59,7 @@ public class SimpleBlockingServerTestCase { public static void setup() { final BlockingHandler blockingHandler = new BlockingHandler(); DefaultServer.setRootHandler(blockingHandler); + blockingHandler.setRootHandler(new HttpHandler() { @Override public void handleRequest(final HttpServerExchange exchange) { @@ -112,6 +115,16 @@ public void onException(final HttpServerExchange exchange, final Sender sender, }); } + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(UndertowOptions.MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + @Test public void sendHttpRequest() throws IOException { message = "My HTTP Request!";
core/src/test/java/io/undertow/server/handlers/ForwardedHandlerTestCase.java+17 −0 modified@@ -1,3 +1,20 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; import io.undertow.server.HttpHandler;
core/src/test/java/io/undertow/server/handlers/HostHandlerTestCase.java+282 −0 added@@ -0,0 +1,282 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; + +import java.io.IOException; + +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.conn.params.ConnRoutePNames; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.ProxyIgnore; +import io.undertow.testutils.TestHttpClient; +import io.undertow.util.Headers; + +@RunWith(DefaultServer.class) +@ProxyIgnore +public class HostHandlerTestCase { + + @BeforeClass + public static void setup() { + DefaultServer.setRootHandler(Handlers.hostHeaderHandler(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseSender().send("OK"); + } + })); + } + + @Test + @Ignore // ignore, since client will add one if there no present + public void testNoHostHeader() throws Exception { + test(new String[] {}, null, 400, null); + } + + @Test + public void testTooManyHostHeader() throws Exception { + test(new String[] { "212.138.1.1", "data.com" }, null, 400, null); + } + + @Test + public void testIPv4HostHeader() throws Exception { + // dont test bad IPv4, as this will be valid... reg-name + test(new String[] { "212.138.1.1" }, null, 200, null); + } + + @Test + public void testIPv4AndPortHostHeader() throws Exception { + test(new String[] { "212.138.1.1:80" }, null, 200, null); + } + + @Test + public void testIPv6HostHeader() throws Exception { + test(new String[] { "[1:2:3:4::]" }, null, 200, null); + } + + @Test + public void testIPv6AndPortHostHeader() throws Exception { + test(new String[] { "[1:2:3:4::]:80" }, null, 200, null); + } + + @Test + public void testIPv6HostHeader2() throws Exception { + test(new String[] { "[1:2:3:4:::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader2() throws Exception { + test(new String[] { "[1:2:3:4::]:80000" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_PORT); + } + + @Test + public void testIPv6HostHeader3() throws Exception { + test(new String[] { "[1:2:3:4::" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader3() throws Exception { + test(new String[] { "[1:2:3:4:::80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6HostHeader4() throws Exception { + test(new String[] { "1:2:3:4::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6HostHeader5() throws Exception { + test(new String[] { "1:2:3:4::" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_PORT); + } + + @Test + public void testIPv6HostHeader6() throws Exception { + //this will just fall into reg-name + test(new String[] { "1:2:3:4:5:6:7:8" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testIPv6AndPortHostHeader4() throws Exception { + test(new String[] { "1:2:3:4::]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader6() throws Exception { + test(new String[] { "1:2:3:4:5:6:7:8]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader() throws Exception { + test(new String[] { "[1:2:3:4::192.168.32.1]" }, null, 200, null); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader2() throws Exception { + test(new String[] { "[1:2:3:4::192.168.32.1]:80" }, null, 200, null); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader3() throws Exception { + test(new String[] { "[1:2:3:4::192.355.32.1]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader() throws Exception { + test(new String[] { "[vAF.1:2:3:4::]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader2() throws Exception { + test(new String[] { "[vG.1:2:3:4::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testIPvFutureHostHeader3() throws Exception { + test(new String[] { "[vAF.abdc:abcd_-~AF]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader4() throws Exception { + test(new String[] { "[vAF.ImVal1.com-._~!$&'()*+,;=:]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader5() throws Exception { + test(new String[] { "[vAF.]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader6() throws Exception { + test(new String[] { "[vAF]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader7() throws Exception { + test(new String[] { "[v.abcd]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPRegNameHostHeader() throws Exception { + test(new String[] { "366.66.12.12" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader2() throws Exception { + test(new String[] { "domain.com%20" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader3() throws Exception { + test(new String[] { "domain.com%2" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPRegNameHostHeader4() throws Exception { + test(new String[] { "doma&n.com%20" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader5() throws Exception { + test(new String[] { "ImVal1.com-._~!$&'()*+,;=" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader6() throws Exception { + // test userinfo presence? + test(new String[] { "juicyUserInfo@ImVal1.com-._~!$&'()*+,;=" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testAbsoluteURLBad() throws Exception { + // test userinfo presence? + test(new String[] { "wrong.com:8080" }, new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 400, HostHeaderHandler.STATUS_HOST_NO_MATCH); + } + + @Test + public void testAbsoluteURLGood() throws Exception { + // test userinfo presence? + test(new String[] { DefaultServer.getHostAddress() + ":" + DefaultServer.getHostPort() }, + new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 200, null); + } + + @Test + public void testEmptyHost() throws Exception { + // test userinfo presence? + test(new String[] { "" }, + null, 200, null); + } + + @Test + public void testEmptyHost2() throws Exception { + // test userinfo presence? + test(new String[] { "" }, + new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 400, HostHeaderHandler.STATUS_HOST_NO_MATCH); + } + + public void test(final String[] headers, final HttpHost proxy, final int resultCode, final String statusMessage) + throws ClientProtocolException, IOException { + TestHttpClient client = new TestHttpClient(); + if (proxy != null) { + client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + } + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL()); + for (String i : headers) { + get.addHeader(Headers.HOST_STRING, i); + } + HttpResponse result = client.execute(get, HttpClientContext.create()); + Assert.assertEquals(result.getStatusLine().getReasonPhrase(), resultCode, result.getStatusLine().getStatusCode()); + if (statusMessage != null) { + Assert.assertEquals(statusMessage, result.getStatusLine().getReasonPhrase()); + } + + } finally { + client.getConnectionManager().shutdown(); + } + } + + public void testProxyMode(final String[] headers, final int resultCode, final String statusMessage) throws ClientProtocolException, IOException { + // this has to be done this way in order to trick apache to use absolute form... + HttpHost proxy = new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()); + TestHttpClient client = new TestHttpClient(); + client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL()); + for (String i : headers) { + get.addHeader(Headers.HOST_STRING, i); + } + HttpResponse result = client.execute(get, HttpClientContext.create()); + Assert.assertEquals(result.getStatusLine().getReasonPhrase(), resultCode, result.getStatusLine().getStatusCode()); + + } finally { + client.getConnectionManager().shutdown(); + } + } + +}
core/src/test/java/io/undertow/server/handlers/QueryParametersWithAllowUnescapedCharactersTestCase.java+11 −6 modified@@ -59,16 +59,21 @@ public static void clearProxyOptions() { public static void setQueryStringsArray() { // format is: {queryString, expected result} queryStrings = new String[][] { new String[] { "/path?unicode=Iñtërnâtiônàližætiøn", - "unicode=Iñtërnâtiônàližætiøn{unicode=>Iñtërnâtiônàližætiøn}" }, - new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb bb{a=>b,value=>bb bb}" }, + //"unicode=Iñtërnâtiônàližætiøn{unicode=>Iñtërnâtiônàližætiøn}" }, + "unicode=I%C3%B1t%C3%ABrn%C3%A2ti%C3%B4n%C3%A0li%C5%BE%C3%A6ti%C3%B8n{unicode=>Iñtërnâtiônàližætiøn}" }, + //new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb bb{a=>b,value=>bb bb}" }, + new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb%20bb{a=>b,value=>bb bb}" }, new String[] { "/path?a=b&value=bb&value=cc", "a=b&value=bb&value=cc{a=>b,value=>[bb,cc]}" }, new String[] { "/path?&a=b&value=bb&&value=cc", "&a=b&value=bb&&value=cc{a=>b,value=>[bb,cc]}" }, - // Specifing some query parameters with empty by intentional for the test purpose. These should be ignored. + // Specifying some query parameters with empty by intentional for the test purpose. These should be ignored. new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20", - "a=b&value=bb&value=cc&s &t {a=>b,s =>,t =>,value=>[bb,cc]}" }, + //"a=b&value=bb&value=cc&s &t {a=>b,s =>,t =>,value=>[bb,cc]}" }, + "a=b&value=bb&value=cc&s%20&t%20{a=>b,s =>,t =>,value=>[bb,cc]}" }, new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20&", - "a=b&value=bb&value=cc&s &t &{a=>b,s =>,t =>,value=>[bb,cc]}" }, + //"a=b&value=bb&value=cc&s &t &{a=>b,s =>,t =>,value=>[bb,cc]}" }, + "a=b&value=bb&value=cc&s%20&t%20&{a=>b,s =>,t =>,value=>[bb,cc]}" }, new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20&u", - "a=b&value=bb&value=cc&s &t &u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; + //"a=b&value=bb&value=cc&s &t &u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; + "a=b&value=bb&value=cc&s%20&t%20&u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; } }
core/src/test/java/io/undertow/server/handlers/ReceiverTestCase.java+14 −0 modified@@ -35,6 +35,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; import java.io.IOException; import java.io.OutputStream; @@ -44,6 +45,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import static io.undertow.UndertowOptions.MAX_ENTITY_SIZE; + /** * @author Stuart Douglas */ @@ -61,6 +64,17 @@ public void error(HttpServerExchange exchange, IOException e) { } }; + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + + @BeforeClass public static void setup() { HttpHandler testFullString = new HttpHandler() {
core/src/test/java/io/undertow/server/protocol/ajp/AjpParsingUnitTestCase.java+2 −1 modified@@ -126,7 +126,8 @@ public void testCharsetHandling() throws Exception { Assert.assertFalse(state.badRequest); Assert.assertEquals("/한글이름", result.getRequestPath()); Assert.assertEquals("/한글이름", result.getRequestURI()); - Assert.assertEquals("param=한글이름", result.getQueryString()); + Assert.assertEquals("param=한글이름", result.getDecodedQueryString()); + Assert.assertEquals("param=í\u0095\u009Cê¸\u0080ì\u009D´ë¦\u0084", result.getQueryString()); } @Test
cb854c779b9eMerge pull request #1882 from fl4via/backport-fixes_2.2.x
46 files changed · +1232 −151
build.metadata+15 −0 added@@ -0,0 +1,15 @@ +# Created by buildmetadata-maven-plugin 1.7.0 ( SHA: 6f444cae ) +build.artifactId=undertow-websockets-jsr +build.groupId=io.undertow +build.java.compiler=HotSpot 64-Bit Tiered Compilers +build.java.runtime.name=OpenJDK Runtime Environment +build.java.runtime.version=1.8.0_422-b05 +build.java.vendor=Red Hat, Inc. +build.java.vm=OpenJDK 64-Bit Server VM +build.maven.execution.cmdline=-Djavax.net.ssl.trustStore\=/home/aogburn/bin/builder/maven.truststore -Djavax.net.ssl.trustStorePassword\=rhmaven -s /home/aogburn/bin/builder/eap-build-settings.xml clean install -DskipTests +build.maven.version=3.9.6 +build.scmRevision.date=17.09.2024 +build.scmRevision.id=aaa36f6ad214aecd7d0d611cad582aa058f3a3e8 +build.scmRevision.url=scm\:git\://github.com/undertow-io/undertow.git/undertow-websockets-jsr +build.version=2.2.33.SP2-redhat-00001 +build.version.full=2.2.33.SP2-redhat-00001raaa36f6ad214aecd7d0d611cad582aa058f3a3e8
core/src/main/java/io/undertow/attribute/QueryStringAttribute.java+1 −1 modified@@ -42,7 +42,7 @@ private QueryStringAttribute(boolean includeQuestionMark) { @Override public String readAttribute(final HttpServerExchange exchange) { - String qs = exchange.getQueryString(); + String qs = exchange.getDecodedQueryString(); if(qs.isEmpty() || !includeQuestionMark) { return qs; }
core/src/main/java/io/undertow/attribute/RequestLineAttribute.java+2 −2 modified@@ -42,9 +42,9 @@ public String readAttribute(final HttpServerExchange exchange) { .append(exchange.getRequestMethod().toString()) .append(' ') .append(exchange.getRequestURI()); - if (!exchange.getQueryString().isEmpty()) { + if (!exchange.getDecodedQueryString().isEmpty()) { sb.append('?'); - sb.append(exchange.getQueryString()); + sb.append(exchange.getDecodedQueryString()); } sb.append(' ') .append(exchange.getProtocol().toString()).toString();
core/src/main/java/io/undertow/conduits/FixedLengthStreamSourceConduit.java+5 −0 modified@@ -371,6 +371,11 @@ private void exitRead(long consumed, Throwable readError) throws IOException { } long newVal = oldVal - consumed; state = newVal; + if (allAreClear(state, MASK_COUNT)) { + if (allAreClear(state, FLAG_FINISHED)) { + next.suspendReads(); + } + } } private void invokeFinishListener() {
core/src/main/java/io/undertow/Handlers.java+12 −0 modified@@ -32,6 +32,7 @@ import io.undertow.server.handlers.DisableCacheHandler; import io.undertow.server.handlers.ExceptionHandler; import io.undertow.server.handlers.GracefulShutdownHandler; +import io.undertow.server.handlers.HostHeaderHandler; import io.undertow.server.handlers.HttpContinueAcceptingHandler; import io.undertow.server.handlers.HttpContinueReadHandler; import io.undertow.server.handlers.HttpTraceHandler; @@ -600,6 +601,17 @@ public static LearningPushHandler learningPushHandler(int maxEntries, HttpHandle return new LearningPushHandler(maxEntries, -1, next); } + /** + * Creates a handler that automatically vets Host header content/absence/presence according to + * https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 and related + * + * @param next The next handler + * @return A host header handler + */ + public static HostHeaderHandler hostHeaderHandler(HttpHandler next) { + return new HostHeaderHandler(next); + } + private Handlers() { }
core/src/main/java/io/undertow/security/handlers/SinglePortConfidentialityHandler.java+1 −1 modified@@ -65,7 +65,7 @@ protected URI getRedirectURI(final HttpServerExchange exchange, final int port) } } uriBuilder.append(uri); - final String queryString = exchange.getQueryString(); + final String queryString = exchange.getDecodedQueryString(); if (queryString != null && !queryString.isEmpty()) { uriBuilder.append("?").append(queryString); }
core/src/main/java/io/undertow/security/impl/DigestAuthenticationMechanism.java+4 −4 modified@@ -235,17 +235,17 @@ private AuthenticationMechanismOutcome handleDigestHeader(HttpServerExchange exc if(parsedHeader.containsKey(DigestAuthorizationToken.DIGEST_URI)) { String uri = parsedHeader.get(DigestAuthorizationToken.DIGEST_URI); String requestURI = exchange.getRequestURI(); - if(!exchange.getQueryString().isEmpty()) { - requestURI = requestURI + "?" + exchange.getQueryString(); + if(!exchange.getDecodedQueryString().isEmpty()) { + requestURI = requestURI + "?" + exchange.getDecodedQueryString(); } if(!uri.equals(requestURI)) { //it is possible we were given an absolute URI //we reconstruct the URI from the host header to make sure they match up //I am not sure if this is overly strict, however I think it is better //to be safe than sorry requestURI = exchange.getRequestURL(); - if(!exchange.getQueryString().isEmpty()) { - requestURI = requestURI + "?" + exchange.getQueryString(); + if(!exchange.getDecodedQueryString().isEmpty()) { + requestURI = requestURI + "?" + exchange.getDecodedQueryString(); } if(!uri.equals(requestURI)) { //just end the auth process
core/src/main/java/io/undertow/server/Connectors.java+2 −3 modified@@ -546,10 +546,9 @@ public static void setExchangeRequestPath(final HttpServerExchange exchange, fin } if(requiresDecode && allowUnescapedCharactersInUrl) { final String decodedQS = URLUtils.decode(qs, charset, decodeSlashFlag,false, decodeBuffer); - exchange.setQueryString(decodedQS); - } else { - exchange.setQueryString(qs); + exchange.setDecodedQueryString(decodedQS); } + exchange.setQueryString(qs); URLUtils.parseQueryString(qs, exchange, charset, decodeQueryString, maxParameters); return;
core/src/main/java/io/undertow/server/handlers/accesslog/ExtendedAccessLogParser.java+2 −2 modified@@ -328,15 +328,15 @@ protected ExchangeAttribute getClientToServerElement( return new ExchangeAttribute() { @Override public String readAttribute(HttpServerExchange exchange) { - String query = exchange.getQueryString(); + String query = exchange.getDecodedQueryString(); if (query.isEmpty()) { return exchange.getRequestURI(); } else { StringBuilder buf = new StringBuilder(); buf.append(exchange.getRequestURI()); buf.append('?'); - buf.append(exchange.getQueryString()); + buf.append(exchange.getDecodedQueryString()); return buf.toString(); } }
core/src/main/java/io/undertow/server/handlers/form/MultiPartParserDefinition.java+1 −1 modified@@ -109,7 +109,7 @@ public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener.proceed(); } }); - Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE); + Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MULTIPART_MAX_ENTITY_SIZE ); if(sizeLimit != null && sizeLimit > 0) { // do not overwrite the entity size with sizeLimit that is <= 0 exchange.setMaxEntitySize(sizeLimit); }
core/src/main/java/io/undertow/server/handlers/HostHeaderHandler.java+295 −0 added@@ -0,0 +1,295 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; + +import java.util.regex.Pattern; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.NetworkUtils; +import io.undertow.util.Protocols; +import io.undertow.util.StatusCodes; + +/** + * Handler which check if Host header is properly formed and present. + * + * @author baranowb + */ +public class HostHeaderHandler implements HttpHandler { + + public static final HandlerWrapper WRAPPER = new Wrapper(); + + public static final String STATUS_NO_HOST_HEADER = "No Host Header"; + public static final String STATUS_TOO_MANY_HOST_HEADERS = "Only One Host Header Allowed"; + public static final String STATUS_MALFORMED_PORT = "Host Header Malformed Port"; + public static final String STATUS_MALFORMED_IP_LITERAL = "Host Header Malformed IP-Literal"; + public static final String STATUS_MALFORMED_IP_LITERAL_BAD_CHARS = "Host Header Bad Characters"; + public static final String STATUS_HOST_NO_MATCH = "URI Host Header NO MATCH"; + private static final Pattern IP4_EXACT = Pattern.compile(NetworkUtils.IP4_EXACT); + private static final Pattern IP6_EXACT = Pattern.compile(NetworkUtils.IP6_EXACT); + private static final boolean[] ALLOWED_REGNAME_CHARACTERS = new boolean[256]; + private static final boolean[] HEX_CHARACTERS = new boolean[256]; + private static final boolean[] ALLOWED_IPv_FUTURE_CHARACTERS = new boolean[256]; // this is almost the same as + // ALLOWED_REGNAME_CHARACTERS with bonus + // ":" but having it as array rather than + // extra check is just faster + static { + // reg-name = *( unreserved / pct-encoded / sub-delims ) + // ALPHA / DIGIT / "-" / "." / "_" / "~" , %%, and "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + for (int i = 0; i < ALLOWED_REGNAME_CHARACTERS.length; ++i) { + if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z')) { + ALLOWED_REGNAME_CHARACTERS[i] = true; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = true; + } else { + switch (i) { + case '-': + case '.': + case '_': + case '~': + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': { + ALLOWED_REGNAME_CHARACTERS[i] = true; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = true; + break; + } + default: + ALLOWED_REGNAME_CHARACTERS[i] = false; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = false; + } + } + + } + + ALLOWED_IPv_FUTURE_CHARACTERS[':'] = true; + + for (int i = 0; i < HEX_CHARACTERS.length; ++i) { + if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) { + HEX_CHARACTERS[i] = true; + } else + HEX_CHARACTERS[i] = false; + } + + } + private final HttpHandler next; + + public HostHeaderHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + // TODO: add debug/warn log? + // 400 if in case of no Host header or more than one: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 + // 400 if value violate rules. Host = uri-host [ ":" port ] + // uri-host https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 + // port https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3 + // NOTE: that 3.2.2 is NOT as restrictive as pure DNS it allows subdelims, percent etc by design as to not be + // restricted to pure DNS, ALTHOUGH DNS compliant content is preferred. + final HeaderMap headerMap = exchange.getRequestHeaders(); + final HeaderValues headerValues = headerMap.get(Headers.HOST); + final HttpString protocol = exchange.getProtocol(); + + if((protocol.equals(Protocols.HTTP_0_9) || protocol.equals(Protocols.HTTP_1_0))){ + if (headerValues == null) { + //TODO: should we fake Host till we make it? + next.handleRequest(exchange); + return; + } + //else { + //clients want to be good citizens and send it anyway. + //fall through to below, first check will be false, but rest is the same as for HTTP1.1+ + //} + } + + if (headerValues == null || headerValues.size() == 0) { + // isEmpty - we assume http/https ? so authority is defined for this type, it cant be empty? + terminate(exchange, STATUS_NO_HOST_HEADER); + return; + } else if (headerValues.size() > 1) { + terminate(exchange, STATUS_TOO_MANY_HOST_HEADERS); + return; + } + + + // parsing time. + final String headerValue = headerValues.element(); + // uri-host [ ":" port ] + // This is tricky, IP-Literal contain :, which is port delimiter in pair + // Lets just try to take take care of port first + final int rightBracketIndex = headerValue.lastIndexOf(']'); + final int lastColonIndex = headerValue.lastIndexOf(':'); + + final String hostHeaderURI; + // in case of IPv4, rightBracketIndex will be -1, in case of IPv6, it MUST be less than last : + if (rightBracketIndex < lastColonIndex) { + // we have port or potentially malformed IP Literal: + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" - without right bracket + if (rightBracketIndex == -1 && headerValue.startsWith("[")) { + // bad [ = 0, ] = -1, : = n+ + // good [ = 0, ] = n , : = n+x + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + // we have valid host-uri with port + final String portString = headerValue.substring(lastColonIndex + 1); + try { + int port = Integer.parseInt(portString); + if (port <= 0 || port > 65535) { + // sanity check + // NOTE: 3.2.3 does not have provision like for IPv4 - decimal between 0-255. + // so this might be too restrictive + terminate(exchange, STATUS_MALFORMED_PORT); + return; + } + // fall through to uri-host checks + } catch (NumberFormatException nfe) { + terminate(exchange, STATUS_MALFORMED_PORT); + return; + } + + hostHeaderURI = headerValue.substring(0, lastColonIndex); + } else { + hostHeaderURI = headerValue; + } + + // at this point we either have IP-Literal, IPv4 address or custom name + if (rightBracketIndex > 0 && hostHeaderURI.indexOf('[') != 0) { + // 1:2:4]* + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } else if (rightBracketIndex > 0 && hostHeaderURI.indexOf('[') == 0) { + // IPv6 or IPFuture + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" + // IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + final String debracked = hostHeaderURI.substring(1, hostHeaderURI.length() - 1); + if (debracked.startsWith("v")) { + final int dotIndex = debracked.indexOf("."); + if (dotIndex < 2) { + // we need at least one HEX + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + + final String hex = debracked.substring(1, dotIndex); + for (int i = 0; i < hex.length(); i++) { + final char c1 = hex.charAt(i); + if (!HEX_CHARACTERS[c1]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } + if (dotIndex + 1 >= debracked.length()) { + // we need some character behind dot. + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + + for (int i = dotIndex + 1; i < debracked.length(); i++) { + final char c = debracked.charAt(i); + if (!ALLOWED_IPv_FUTURE_CHARACTERS[c]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } + } else { + // This will match IPv6 and IPv6 with embedded IPv4 + if (!IP6_EXACT.matcher(debracked).matches()) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + // TODO: as in case of IPv6 do we need to vet some weir daddresses? + } + + } else if (IP4_EXACT.matcher(hostHeaderURI).matches()) { + // IPv4 + // NOTE: above will match only valid range 0-255, rest will fall through to reg-name, which is ok, since its + // essentially + // superset of IP, given DIGIT + "." ( unreserved ) + } else { + // registered name can contain . and digits, so it technically overlap IPv4. + // above wont cover -192.168.1.1/355.0.0.0, which technically is correct 'reg-name' + // at this point we can really only check if its valid char or percent encoding. + for (int index = 0; index < hostHeaderURI.length(); index++) { + final char c = hostHeaderURI.charAt(index); + if (c == '%') { + // we need at least two more that are HEX + if (index + 2 < hostHeaderURI.length()) { + char c1 = hostHeaderURI.charAt(++index); + char c2 = hostHeaderURI.charAt(++index); + if (!(HEX_CHARACTERS[c1] && HEX_CHARACTERS[c2])) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } else { + // we dont have at least two chars(hex), so its not proper percent escape + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + } else if (!ALLOWED_REGNAME_CHARACTERS[c]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + // valid + } + } + // NOTE: at this point if userinfo("@") was present in host, it would have failed + // we need only to check if Host header value is contained within URI if its absolute or authority + if (exchange.isHostIncludedInRequestURI()) { + if (!exchange.getRequestURI().contains(hostHeaderURI)) { + terminate(exchange, STATUS_HOST_NO_MATCH); + return; + } else if (hostHeaderURI.isEmpty()) { + terminate(exchange, STATUS_HOST_NO_MATCH); + return; + } + } + + // in the end... + next.handleRequest(exchange); + } + + private void terminate(final HttpServerExchange exchange, final String message) { + exchange.setStatusCode(StatusCodes.BAD_REQUEST); + exchange.setResponseContentLength(0); + exchange.getResponseHeaders().add(Headers.CONNECTION, Headers.CLOSE.toString()); + exchange.setReasonPhrase(message); + exchange.endExchange(); + } + + private static class Wrapper implements HandlerWrapper { + + @Override + public HttpHandler wrap(HttpHandler handler) { + return new HostHeaderHandler(handler); + } + } +}
core/src/main/java/io/undertow/server/handlers/HttpTraceHandler.java+2 −2 modified@@ -49,9 +49,9 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "message/http"); StringBuilder body = new StringBuilder("TRACE "); body.append(exchange.getRequestURI()); - if(!exchange.getQueryString().isEmpty()) { + if(!exchange.getDecodedQueryString().isEmpty()) { body.append('?'); - body.append(exchange.getQueryString()); + body.append(exchange.getDecodedQueryString()); } body.append(' '); body.append(exchange.getProtocol().toString());
core/src/main/java/io/undertow/server/handlers/JDBCLogHandler.java+1 −1 modified@@ -135,7 +135,7 @@ public void logMessage(String pattern, HttpServerExchange exchange) { } else { jdbcLogAttribute.user = sc.getAuthenticatedAccount().getPrincipal().getName(); } - jdbcLogAttribute.query = exchange.getQueryString(); + jdbcLogAttribute.query = exchange.getDecodedQueryString(); jdbcLogAttribute.bytes = exchange.getResponseContentLength(); if (jdbcLogAttribute.bytes < 0) {
core/src/main/java/io/undertow/server/handlers/LearningPushHandler.java+3 −3 modified@@ -77,12 +77,12 @@ public LearningPushHandler(int maxPathEtries, int maxPathAge, int maxPushEtries, public void handleRequest(HttpServerExchange exchange) throws Exception { String fullPath; String requestPath; - if(exchange.getQueryString().isEmpty()) { + if(exchange.getDecodedQueryString().isEmpty()) { fullPath = exchange.getRequestURL(); requestPath = exchange.getRequestPath(); } else{ - fullPath = exchange.getRequestURL() + "?" + exchange.getQueryString(); - requestPath = exchange.getRequestPath() + "?" + exchange.getQueryString(); + fullPath = exchange.getRequestURL() + "?" + exchange.getDecodedQueryString(); + requestPath = exchange.getRequestPath() + "?" + exchange.getDecodedQueryString(); } doPush(exchange, fullPath);
core/src/main/java/io/undertow/server/handlers/proxy/ProxyHandler.java+1 −1 modified@@ -446,7 +446,7 @@ public void run() { } requestURI.append(targetURI); - String qs = exchange.getNonDecodedQueryString(); + String qs = exchange.getQueryString(); if (qs != null && !qs.isEmpty()) { requestURI.append('?'); requestURI.append(qs);
core/src/main/java/io/undertow/server/handlers/RequestDumpingHandler.java+1 −1 modified@@ -98,7 +98,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { sb.append("\n"); } sb.append(" protocol=" + exchange.getProtocol() + "\n"); - sb.append(" queryString=" + exchange.getQueryString() + "\n"); + sb.append(" queryString=" + exchange.getDecodedQueryString() + "\n"); sb.append(" remoteAddr=" + exchange.getSourceAddress() + "\n"); sb.append(" remoteHost=" + exchange.getSourceAddress().getHostName() + "\n"); sb.append(" scheme=" + exchange.getRequestScheme() + "\n");
core/src/main/java/io/undertow/server/handlers/resource/DirectoryUtils.java+2 −2 modified@@ -60,12 +60,12 @@ public static boolean sendRequestedBlobs(HttpServerExchange exchange) { String type = null; String etag = null; String quotedEtag = null; - if ("css".equals(exchange.getQueryString())) { + if ("css".equals(exchange.getDecodedQueryString())) { buffer = Blobs.FILE_CSS_BUFFER.duplicate(); type = "text/css"; etag = Blobs.FILE_CSS_ETAG; quotedEtag = Blobs.FILE_CSS_ETAG_QUOTED; - } else if ("js".equals(exchange.getQueryString())) { + } else if ("js".equals(exchange.getDecodedQueryString())) { buffer = Blobs.FILE_JS_BUFFER.duplicate(); type = "application/javascript"; etag = Blobs.FILE_JS_ETAG;
core/src/main/java/io/undertow/server/handlers/StuckThreadDetectionHandler.java+1 −1 modified@@ -154,7 +154,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { // GC'ing, as the reference is removed from the Map in the finally clause Long key = Thread.currentThread().getId(); - MonitoredThread monitoredThread = new MonitoredThread(Thread.currentThread(), exchange.getRequestURI() + exchange.getQueryString()); + MonitoredThread monitoredThread = new MonitoredThread(Thread.currentThread(), exchange.getRequestURI() + exchange.getDecodedQueryString()); activeThreads.put(key, monitoredThread); if(timerKey == null) { synchronized (this) {
core/src/main/java/io/undertow/server/handlers/URLDecodingHandler.java+1 −1 modified@@ -83,7 +83,7 @@ private static void decodePath(HttpServerExchange exchange, String charset, Stri } private static void decodeQueryString(HttpServerExchange exchange, String charset, StringBuilder sb) { - if (!exchange.getQueryString().isEmpty()) { + if (!exchange.getDecodedQueryString().isEmpty()) { final TreeMap<String, Deque<String>> newParams = new TreeMap<>(); for (Map.Entry<String, Deque<String>> param : exchange.getQueryParameters().entrySet()) { final Deque<String> newValues = new ArrayDeque<>(param.getValue().size());
core/src/main/java/io/undertow/server/HttpServerExchange.java+56 −18 modified@@ -207,13 +207,14 @@ public final class HttpServerExchange extends AbstractAttachable { private String resolvedPath = ""; /** - * the query string - percent encoded + * the unencoded query string (i.e. percent encoded), in its original form as it appears in the received request. */ private String queryString = ""; + /** - * the non-decoded query string. Set only when query string goes through decoding + * the decoded query string, if there was any decoding done */ - private String nonDecodedQueryString = null; + private String decodedQueryString = null; private int requestWrapperCount = 0; private ConduitWrapper<StreamSourceConduit>[] requestWrappers; //we don't allocate these by default, as for get requests they are not used @@ -573,24 +574,25 @@ public HttpServerExchange setResolvedPath(final String resolvedPath) { } /** + * Returns the query string for this request. * - * @return The query string, without the leading ? + * @return The query string as originally appeared in the request, without the leading ? */ public String getQueryString() { - return queryString; + return this.queryString; } /** - * Set query string. Leading {@code '?'} char will be removed automatically. + * Sets the query string, unencoded and in its original form as it appears in the received request. + * Leading {@code '?'} char will be removed automatically.<p> * + * @param queryString the query string as originally contained in the request, without any decoding * @return this http server exchange */ public HttpServerExchange setQueryString(final String queryString) { - // Clean leading ? - if( queryString.length() > 0 && queryString.charAt(0) == '?' ) { - this.queryString = queryString.substring(1); - } else { - this.queryString = queryString; + this.queryString = cleanQueryString(queryString); + if (this.queryString == null) { + this.queryString = ""; } return this; } @@ -600,27 +602,63 @@ public HttpServerExchange setQueryString(final String queryString) { * The returned string does not contain the leading {@code '?'} char. * * @return The request query string, without the leading {@code '?'}, non-decoded. + * + * @deprecated use {@link #getQueryString()} instead */ + @Deprecated public String getNonDecodedQueryString() { - return this.nonDecodedQueryString == null? this.queryString: this.nonDecodedQueryString; + return getQueryString(); } /** * Sets the non-decoded query string. Leading {@code '?'} char will be removed automatically.<p> * Must be invoked only if the {@link #getQueryString() query string} has gone through decoding. In such case, we expect * that both forms of the query string will be set in the exchange: {@link #setQueryString decoded} and non-decoded. * - * @param nonDecodedQueryString the query string as originally contained in the request, without any decoding + * @param unencodedQueryString the query string as originally contained in the request, without any decoding * @return this http server exchange + * + * @deprecated Use #setQueryString instead + */ + @Deprecated + public HttpServerExchange setNonDecodedQueryString(String unencodedQueryString) { + return setQueryString(unencodedQueryString); + } + + /** + * Returns the query string in its decoded form if available, which will depend on configs such as + * {@link UndertowOptions#ALLOW_UNESCAPED_CHARACTERS_IN_URL}. + * If unavailable, the decoded query string is just the same as {@link #getQueryString} + * + * @return The request query string, without the leading {@code '?'}, post parsing, decoded. */ - public HttpServerExchange setNonDecodedQueryString(String nonDecodedQueryString) { + public String getDecodedQueryString() { + return this.decodedQueryString != null && this.decodedQueryString.length() > 0 ? this.decodedQueryString : this.queryString; + } + + /** + * Sets the decoded query string. + * Leading {@code '?'} char will be removed automatically.<p> + * Must be invoked only if the {@link #getQueryString() query string} has gone through decoding. In such case, we expect + * that both forms of the query string will be set in the exchange: decoded and {@link #setQueryString non-decoded} + * + * @param decodedQueryString the request query string, without the leading {@code '?'}, post parsing, decoded. + * @return this http server exchange + */ + public HttpServerExchange setDecodedQueryString(String decodedQueryString) { + this.decodedQueryString = cleanQueryString(decodedQueryString); + return this; + } + + private String cleanQueryString(String queryString) { // Clean leading ? - if( nonDecodedQueryString.length() > 0 && nonDecodedQueryString.charAt(0) == '?' ) { - this.nonDecodedQueryString = nonDecodedQueryString.substring(1); + if (queryString == null) { + return queryString; + } else if( queryString.length() > 0 && queryString.charAt(0) == '?' ) { + return queryString.substring(1); } else { - this.nonDecodedQueryString = nonDecodedQueryString; + return queryString; } - return this; } /**
core/src/main/java/io/undertow/server/protocol/ajp/AjpRequestParser.java+20 −17 modified@@ -432,7 +432,7 @@ public void parse(final ByteBuffer buf, final AjpRequestParseState state, final state.currentAttribute = result.value; state.currentIntegerPart = -1; } - String result; + String result, decodedResult = null; boolean decodingAlreadyDone = false; boolean decodeUnescapedCharacters = false; final StringHolder resultHolder; @@ -451,68 +451,71 @@ public void parse(final ByteBuffer buf, final AjpRequestParseState state, final return; } decodeUnescapedCharacters = resultHolder.containsUrlCharacters && allowUnescapedCharactersInUrl; + result = resultHolder.value; if(resultHolder.containsUnencodedCharacters || decodeUnescapedCharacters) { try { - result = decode(resultHolder.value, true); + decodedResult = decode(resultHolder.value, true); } catch (UrlDecodeException | UnsupportedEncodingException e) { UndertowLogger.REQUEST_IO_LOGGER.failedToParseRequest(e); state.badRequest = true; result = resultHolder.value; } decodingAlreadyDone = true; - } else { - result = resultHolder.value; } } + final String finalResult = decodedResult != null ? decodedResult : result; //query string. if (state.currentAttribute.equals(QUERY_STRING)) { String resultAsQueryString = result == null ? "" : result; exchange.setQueryString(resultAsQueryString); + exchange.setDecodedQueryString(decodedResult); try { if (decodeUnescapedCharacters) { // decoding needs to be done again here, to preserve the parameters and form decoding, even if it has been done at resultAsQueryString // for more info see UNDERTOW-2312 and UNDERTOW-2555 URLUtils.parseQueryString(resultHolder == null || resultHolder.value == null ? "" : resultHolder.value, exchange, encoding, doDecode, maxParameters); + } else if (decodingAlreadyDone) { + URLUtils.parseQueryString(decodedResult, exchange, encoding, + false, maxParameters); } else { - URLUtils.parseQueryString(resultAsQueryString, exchange, encoding, - doDecode && !decodingAlreadyDone, maxParameters); + URLUtils.parseQueryString(resultAsQueryString, exchange, encoding, doDecode, maxParameters); } } catch (ParameterLimitException | IllegalArgumentException e) { UndertowLogger.REQUEST_IO_LOGGER.failedToParseRequest(e); state.badRequest = true; } } else if (state.currentAttribute.equals(REMOTE_USER)) { - exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_PRINCIPAL, result); - exchange.putAttachment(HttpServerExchange.REMOTE_USER, result); + exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_PRINCIPAL, finalResult); + exchange.putAttachment(HttpServerExchange.REMOTE_USER, finalResult); } else if (state.currentAttribute.equals(AUTH_TYPE)) { - exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_AUTHENTICATION_TYPE, result); + exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_AUTHENTICATION_TYPE, finalResult); } else if (state.currentAttribute.equals(STORED_METHOD)) { - HttpString requestMethod = new HttpString(result); + HttpString requestMethod = new HttpString(finalResult); Connectors.verifyToken(requestMethod); exchange.setRequestMethod(requestMethod); } else if (state.currentAttribute.equals(AJP_REMOTE_PORT)) { - state.remotePort = Integer.parseInt(result); + state.remotePort = Integer.parseInt(finalResult); } else if (state.currentAttribute.equals(SSL_SESSION)) { - state.sslSessionId = result; + state.sslSessionId = finalResult; } else if (state.currentAttribute.equals(SSL_CIPHER)) { - state.sslCipher = result; + state.sslCipher = finalResult; } else if (state.currentAttribute.equals(SSL_CERT)) { - state.sslCert = result; + state.sslCert = finalResult; } else if (state.currentAttribute.equals(SSL_KEY_SIZE)) { - state.sslKeySize = result; + state.sslKeySize = finalResult; } else { // other attributes if (state.attributes == null) { state.attributes = new TreeMap<>(); } if (ATTR_SET.contains(state.currentAttribute)) { // known attirubtes - state.attributes.put(state.currentAttribute, result); + state.attributes.put(state.currentAttribute, finalResult); } else if (allowedRequestAttributesPattern != null) { // custom allowed attributes Matcher m = allowedRequestAttributesPattern.matcher(state.currentAttribute); if (m.matches()) { - state.attributes.put(state.currentAttribute, result); + state.attributes.put(state.currentAttribute, finalResult); } } }
core/src/main/java/io/undertow/server/protocol/http2/Http2ReceiveListener.java+1 −0 modified@@ -243,6 +243,7 @@ void handleInitialRequest(HttpServerExchange initial, Http2Channel channel, byte exchange.setRequestScheme(initial.getRequestScheme()); exchange.setRequestMethod(initial.getRequestMethod()); exchange.setQueryString(initial.getQueryString()); + exchange.setDecodedQueryString(initial.getDecodedQueryString()); for (Map.Entry<String, Deque<String>> pathParamEntry: initial.getPathParameters().entrySet()) { for (String pathParamValue : pathParamEntry.getValue()) { exchange.addPathParam(pathParamEntry.getKey(), pathParamValue);
core/src/main/java/io/undertow/server/protocol/http/HttpReadListener.java+8 −16 modified@@ -27,12 +27,11 @@ import io.undertow.server.ConnectorStatisticsImpl; import io.undertow.server.Connectors; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.HostHeaderHandler; import io.undertow.server.protocol.ParseTimeoutUpdater; import io.undertow.server.protocol.http2.Http2ReceiveListener; import io.undertow.util.ClosingChannelExceptionHandler; import io.undertow.util.ConnectionUtils; -import io.undertow.util.HeaderValues; -import io.undertow.util.Headers; import io.undertow.util.HttpString; import io.undertow.util.Methods; import io.undertow.util.Protocols; @@ -78,7 +77,6 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann private final long maxEntitySize; private final boolean recordRequestStartTime; private final boolean allowUnknownProtocols; - private final boolean requireHostHeader; //0 = new request ok, reads resumed //1 = request running, new request not ok @@ -98,7 +96,6 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann this.maxRequestSize = connection.getUndertowOptions().get(UndertowOptions.MAX_HEADER_SIZE, UndertowOptions.DEFAULT_MAX_HEADER_SIZE); this.maxEntitySize = connection.getUndertowOptions().get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); this.recordRequestStartTime = connection.getUndertowOptions().get(UndertowOptions.RECORD_REQUEST_START_TIME, false); - this.requireHostHeader = connection.getUndertowOptions().get(UndertowOptions.REQUIRE_HOST_HTTP11, true); this.allowUnknownProtocols = connection.getUndertowOptions().get(UndertowOptions.ALLOW_UNKNOWN_PROTOCOLS, false); int requestParseTimeout = connection.getUndertowOptions().get(UndertowOptions.REQUEST_PARSE_TIMEOUT, -1); int requestIdleTimeout = connection.getUndertowOptions().get(UndertowOptions.NO_REQUEST_TIMEOUT, -1); @@ -109,6 +106,10 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann connection.addCloseListener(parseTimeoutUpdater); } state = new ParseState(connection.getUndertowOptions().get(UndertowOptions.HTTP_HEADERS_CACHE_SIZE, UndertowOptions.DEFAULT_HTTP_HEADERS_CACHE_SIZE)); + + if (connection.getUndertowOptions().contains(UndertowOptions.REQUIRE_HOST_HTTP11)) { + UndertowLogger.ROOT_LOGGER.configurationNotSupported("REQUIRE_HOST_HTTP11"); + } } public void newRequest() { @@ -247,22 +248,13 @@ public void handleEventWithNoRunningRequest(final ConduitStreamSourceChannel cha channel.suspendReads(); } - HeaderValues host = httpServerExchange.getRequestHeaders().get(Headers.HOST); - if(host != null && host.size() > 1) { - sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.moreThanOneHostHeader()); - return; - } - if(requireHostHeader && httpServerExchange.getProtocol().equals(Protocols.HTTP_1_1)) { - if(host == null || host.size() ==0 || host.getFirst().isEmpty()) { - sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.noHostInHttp11Request()); - return; - } - } if(!Connectors.areRequestHeadersValid(httpServerExchange.getRequestHeaders())) { sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.invalidHeaders()); return; } - Connectors.executeRootHandler(connection.getRootHandler(), httpServerExchange); + + //TODO: make this configurable/dynamic, to either allow users provide their own here or in chain + Connectors.executeRootHandler(HostHeaderHandler.WRAPPER.wrap(connection.getRootHandler()), httpServerExchange); } catch (Throwable t) { sendBadRequestAndClose(connection.getChannel(), t); return;
core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java+2 −2 modified@@ -552,11 +552,11 @@ final void handleQueryParameters(ByteBuffer buffer, ParseState state, HttpServer } if (next == ' ' || next == '\t') { String queryString = stringBuilder.toString(); + exchange.setQueryString(queryString); if(urlDecodeRequired && this.allowUnescapedCharactersInUrl) { - exchange.setNonDecodedQueryString(queryString); queryString = decode(queryString, urlDecodeRequired, state, slashDecodingFlag, false); + exchange.setDecodedQueryString(queryString); } - exchange.setQueryString(queryString); if (nextQueryParam == null) { if (queryParamPos != stringBuilder.length()) { exchange.addQueryParam(decode(stringBuilder.substring(queryParamPos), nextQueryParamDecodeRequired, state, true, true), "");
core/src/main/java/io/undertow/UndertowLogger.java+4 −0 modified@@ -478,4 +478,8 @@ void nodeConfigCreated(URI connectionURI, String balancer, String domain, String @LogMessage(level = WARN) @Message(id = 5107, value = "Failed to set web socket timeout.") void failedToSetWSTimeout(@Cause Exception e); + + @LogMessage(level = WARN) + @Message(id = 5108, value = "Configuration option is no longer supported: %s.") + void configurationNotSupported(String string); } \ No newline at end of file
core/src/main/java/io/undertow/UndertowOptions.java+7 −2 modified@@ -51,9 +51,14 @@ public class UndertowOptions { public static final Option<Long> MULTIPART_MAX_ENTITY_SIZE = Option.simple(UndertowOptions.class, "MULTIPART_MAX_ENTITY_SIZE", Long.class); /** - * We do not have a default upload limit + * Default maximum upload size 2MB */ - public static final long DEFAULT_MAX_ENTITY_SIZE = -1; + public static final long DEFAULT_MAX_ENTITY_SIZE = 2097152; + + /** + * Default maximum multipart upload size 2MB + */ + public static final long DEFAULT_MULTIPART_MAX_ENTITY_SIZE = 2097152; /** * If we should buffer pipelined requests. Defaults to false.
core/src/main/java/io/undertow/util/NetworkUtils.java+31 −14 modified@@ -32,29 +32,46 @@ */ public class NetworkUtils { - public static final String IP4_EXACT = "(?:\\d{1,3}\\.){3}\\d{1,3}"; + /** + * IPv4Segment: (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]) + */ + public static final String IP4_SEGMENT = "(25[0-5]|(?:2[0-4]|(?:(1{0,1}[0-9]))){0,1}(?:([0-9])))"; + /** + * ?: - unnamed groups are used for performance reasons. + * IPv4Segment: 25[0-5]|((2[0-4]|1){0,1}[0-9]){0,1}[0-9]) -extra () is not needed but makes it clear for OR + * IPv4Address: (IPv4Segment\.){3,3}IPv4Segment + */ + public static final String IP4_EXACT = "(?:"+IP4_SEGMENT+"\\.){3,3}(?:"+IP4_SEGMENT+")"; + /** + * IPv6Segment: ([0-9a-fA-F]{1,4}) - 1 to 4 hex digits + */ + public static final String IP6_SEGMENT = "([0-9a-fA-F]{1,4})"; /** * IPV6 match. ?: - unnamed groups are used for performance reasons. * Requirements: * - match full or partial IPV6 ( sliding '::') * - match end to start - ^$ to ensure it does not match part of some random (\d:){n,m} * - IPv4-Embedded IPv6 Address + * - TODO: special types of addresses ? + * Explanation: + * IPv6Segment: ([0-9a-fA-F]{1,4}) - 1 to 4 hex digits + * IPv6Address: (IPv6Segment:){1,n}IPv6Segment or (IPv6Segment:){1,x}(:IPv6Segment){1,y} where x+y<8 + * ^ shift of segments ~left to right with qualifiers + * Above is just general form for pure IPv6 without any spices * - * NO: - * - IPv4 mapped/translated into IPv6 - * - * ^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4} - full address - * |(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:)) - last compressed - * |(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4}) - second to last - * |(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2} - etc - * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3} - * |(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4} - * |(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5} - * |(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6} - * |(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))))$ - all the way compressed + * ^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4} - 1:2:3:4:5:6:7:8 + * |(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:)) - 1:: 1:2:3:4:5:6:7:: + * |(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4}) - 1::8 1:2:3:4:5:6::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2} - 1::7:8 1:2:3:4:5::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3} - 1::6:7:8 1:2:3:4::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4} - 1::5:6:7:8 1:2:3::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5} - 1::4:5:6:7:8 1:2::8 + * |(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6} - 1::3:4:5:6:7:8 1::8 + * |(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))) - ::2:3:4:5:6:7:8 ::8 :: + * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:IPv4Address)))$ - 1.2.3.4::192.168.1.1 1::192.168.1.1 */ - public static final String IP6_EXACT = "^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:))|(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4})|(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2}|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3}|(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5}|(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6}|(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))))$"; + public static final String IP6_EXACT = "^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:))|(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4})|(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2}|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3}|(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5}|(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6}|(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:))))|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:"+IP4_EXACT+")))$"; public static String formatPossibleIpv6Address(String address) { if (address == null) {
core/src/main/java/io/undertow/util/WeakCopyOnWriteMap.java+170 −0 added@@ -0,0 +1,170 @@ +/* + * 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.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.WeakHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +/** + * A basic copy on write map. It simply delegates to an underlying map, that is swapped out + * every time the map is updated. + * + * Note: this is not a secure map. It should not be used in situations where the map is populated + * from user input. + * + * @author Stuart Douglas + */ +public class WeakCopyOnWriteMap<K,V> implements ConcurrentMap<K, V> { + + private volatile Map<K, V> delegate = Collections.emptyMap(); + + public WeakCopyOnWriteMap() { + } + + public WeakCopyOnWriteMap(Map<K, V> existing) { + this.delegate = new WeakHashMap<>(existing); + } + + @Override + public synchronized V putIfAbsent(K key, V value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing != null) { + return existing; + } + putInternal(key, value); + return null; + } + + @Override + public synchronized boolean remove(Object key, Object value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing.equals(value)) { + removeInternal(key); + return true; + } + return false; + } + + @Override + public synchronized boolean replace(K key, V oldValue, V newValue) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing.equals(oldValue)) { + putInternal(key, newValue); + return true; + } + return false; + } + + @Override + public synchronized V replace(K key, V value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing != null) { + putInternal(key, value); + return existing; + } + return null; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public V get(Object key) { + return delegate.get(key); + } + + @Override + public synchronized V put(K key, V value) { + return putInternal(key, value); + } + + @Override + public synchronized V remove(Object key) { + return removeInternal(key); + } + + @Override + public synchronized void putAll(Map<? extends K, ? extends V> m) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + for(Entry<? extends K, ? extends V> e : m.entrySet()) { + delegate.put(e.getKey(), e.getValue()); + } + this.delegate = delegate; + } + + @Override + public synchronized void clear() { + delegate = Collections.emptyMap(); + } + + @Override + public Set<K> keySet() { + return delegate.keySet(); + } + + @Override + public Collection<V> values() { + return delegate.values(); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return delegate.entrySet(); + } + + //must be called under lock + private V putInternal(final K key, final V value) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + V existing = delegate.put(key, value); + this.delegate = delegate; + return existing; + } + + public V removeInternal(final Object key) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + V existing = delegate.remove(key); + this.delegate = delegate; + return existing; + } +}
core/src/main/java/io/undertow/websockets/core/WebSocketChannel.java+3 −3 modified@@ -214,7 +214,7 @@ protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException { } catch (WebSocketException e) { //the data was corrupt //send a close message - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); markReadsBroken(e); if (WebSocketLogger.REQUEST_LOGGER.isDebugEnabled()) { WebSocketLogger.REQUEST_LOGGER.debugf(e, "receive failed due to Exception"); @@ -278,10 +278,10 @@ protected void handleBrokenSourceChannel(Throwable e) { getFramePriority().immediateCloseFrame(); WebSockets.sendClose(new CloseMessage(CloseMessage.MSG_CONTAINS_INVALID_DATA, e.getMessage()).toByteBuffer(), this, null); } else if (e instanceof WebSocketInvalidCloseCodeException) { - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); } else if (e instanceof WebSocketFrameCorruptedException) { getFramePriority().immediateCloseFrame(); - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); } }
core/src/test/java/io/undertow/server/ConnectionTerminationTestCase.java+22 −8 modified@@ -18,21 +18,24 @@ package io.undertow.server; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpOneOnly; +import io.undertow.testutils.ProxyIgnore; +import io.undertow.util.FileUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xnio.IoUtils; +import org.xnio.OptionMap; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.xnio.IoUtils; -import io.undertow.testutils.DefaultServer; -import io.undertow.testutils.HttpOneOnly; -import io.undertow.testutils.ProxyIgnore; -import io.undertow.util.FileUtils; +import static io.undertow.UndertowOptions.MAX_ENTITY_SIZE; /** * Tests abnormal connection termination @@ -96,4 +99,15 @@ public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener IoUtils.safeClose(socket); } } + + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + }
core/src/test/java/io/undertow/server/ExactLengthReadTimeoutTestCase.java+143 −0 added@@ -0,0 +1,143 @@ +/* + * 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 java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import io.undertow.server.handlers.BlockingHandler; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpOneOnly; +import io.undertow.testutils.TestHttpClient; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; +import org.apache.http.HttpResponse; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xnio.OptionMap; +import org.xnio.Options; + +/** + * + * Tests to ensure no read timeout after an exact read of the content-length + * + * @author Stuart Douglas + * @author Flavia Rainone + * @author Aaron Ogburn + */ +@RunWith(DefaultServer.class) +@HttpOneOnly +public class ExactLengthReadTimeoutTestCase { + + private static volatile String message; + + private static final String DATA = "1234567890ABCDEF"; + + private static final int DATA_MULTIPLE = 2048; + + @BeforeClass + public static void setup() { + final BlockingHandler blockingHandler = new BlockingHandler(); + DefaultServer.setRootHandler(blockingHandler); + blockingHandler.setRootHandler(new HttpHandler() { + @Override + public void handleRequest(final HttpServerExchange exchange) { + try { + final OutputStream outputStream = exchange.getOutputStream(); + final InputStream inputStream = exchange.getInputStream(); + + long length = exchange.getRequestContentLength(); + byte[] b = new byte[DATA_MULTIPLE * DATA.length()]; + int i = 1; + StringBuilder builder = new StringBuilder(); + // read exact content length + while (i > 0 && length > 0) { + i = inputStream.read(b); + if (i > 0) { + length -=i; + builder.append(new String(b, 0, i)); + } + } + + // this shouldn't cause timeout after complete read + try { + Thread.sleep(200); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + Assert.assertEquals(message, builder.toString()); + inputStream.close(); + outputStream.close(); + } catch (IOException e) { + exchange.getResponseHeaders().put(Headers.CONNECTION, "close"); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); + throw new RuntimeException(e); + } + } + }); + } + + @DefaultServer.BeforeServerStarts + public static void beforeClass() { + DefaultServer.setServerOptions(OptionMap.create(Options.READ_TIMEOUT, 100)); + } + + @DefaultServer.AfterServerStops + public static void afterClass() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + + @Test + public void testExactLengthReadTimeout() throws InterruptedException, IOException { + StringBuilder builder = new StringBuilder(1000 * DATA.length()); + + for (int i = 0; i < DATA_MULTIPLE; ++i) { + try { + builder.append(DATA); + } catch (Throwable e) { + throw new RuntimeException("test failed with i equal to " + i, e); + } + } + + message = builder.toString(); + final TestHttpClient client = new TestHttpClient(); + try { + HttpPost post = new HttpPost(DefaultServer.getDefaultServerURL() + "/path"); + post.setEntity(new StringEntity(message)); + post.addHeader(Headers.CONNECTION_STRING, "close"); + boolean socketFailure = false; + try { + // Request should succeed. + HttpResponse result = client.execute(post); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + } catch (NoHttpResponseException e) { + Assert.fail("No response was received, this was presumably caused by read-timeout closing the connection."); + } + } finally { + client.getConnectionManager().shutdown(); + } + } +}
core/src/test/java/io/undertow/server/handlers/blocking/SimpleBlockingServerTestCase.java+13 −0 modified@@ -23,6 +23,7 @@ import java.io.InputStream; import java.io.OutputStream; +import io.undertow.UndertowOptions; import io.undertow.io.IoCallback; import io.undertow.io.Sender; import io.undertow.server.HttpHandler; @@ -44,6 +45,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; /** * @author Stuart Douglas @@ -57,6 +59,7 @@ public class SimpleBlockingServerTestCase { public static void setup() { final BlockingHandler blockingHandler = new BlockingHandler(); DefaultServer.setRootHandler(blockingHandler); + blockingHandler.setRootHandler(new HttpHandler() { @Override public void handleRequest(final HttpServerExchange exchange) { @@ -112,6 +115,16 @@ public void onException(final HttpServerExchange exchange, final Sender sender, }); } + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(UndertowOptions.MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + @Test public void sendHttpRequest() throws IOException { message = "My HTTP Request!";
core/src/test/java/io/undertow/server/handlers/ForwardedHandlerTestCase.java+17 −0 modified@@ -1,3 +1,20 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; import io.undertow.server.HttpHandler;
core/src/test/java/io/undertow/server/handlers/HostHandlerTestCase.java+282 −0 added@@ -0,0 +1,282 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; + +import java.io.IOException; + +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.conn.params.ConnRoutePNames; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.ProxyIgnore; +import io.undertow.testutils.TestHttpClient; +import io.undertow.util.Headers; + +@RunWith(DefaultServer.class) +@ProxyIgnore +public class HostHandlerTestCase { + + @BeforeClass + public static void setup() { + DefaultServer.setRootHandler(Handlers.hostHeaderHandler(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseSender().send("OK"); + } + })); + } + + @Test + @Ignore // ignore, since client will add one if there no present + public void testNoHostHeader() throws Exception { + test(new String[] {}, null, 400, null); + } + + @Test + public void testTooManyHostHeader() throws Exception { + test(new String[] { "212.138.1.1", "data.com" }, null, 400, null); + } + + @Test + public void testIPv4HostHeader() throws Exception { + // dont test bad IPv4, as this will be valid... reg-name + test(new String[] { "212.138.1.1" }, null, 200, null); + } + + @Test + public void testIPv4AndPortHostHeader() throws Exception { + test(new String[] { "212.138.1.1:80" }, null, 200, null); + } + + @Test + public void testIPv6HostHeader() throws Exception { + test(new String[] { "[1:2:3:4::]" }, null, 200, null); + } + + @Test + public void testIPv6AndPortHostHeader() throws Exception { + test(new String[] { "[1:2:3:4::]:80" }, null, 200, null); + } + + @Test + public void testIPv6HostHeader2() throws Exception { + test(new String[] { "[1:2:3:4:::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader2() throws Exception { + test(new String[] { "[1:2:3:4::]:80000" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_PORT); + } + + @Test + public void testIPv6HostHeader3() throws Exception { + test(new String[] { "[1:2:3:4::" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader3() throws Exception { + test(new String[] { "[1:2:3:4:::80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6HostHeader4() throws Exception { + test(new String[] { "1:2:3:4::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6HostHeader5() throws Exception { + test(new String[] { "1:2:3:4::" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_PORT); + } + + @Test + public void testIPv6HostHeader6() throws Exception { + //this will just fall into reg-name + test(new String[] { "1:2:3:4:5:6:7:8" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testIPv6AndPortHostHeader4() throws Exception { + test(new String[] { "1:2:3:4::]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader6() throws Exception { + test(new String[] { "1:2:3:4:5:6:7:8]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader() throws Exception { + test(new String[] { "[1:2:3:4::192.168.32.1]" }, null, 200, null); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader2() throws Exception { + test(new String[] { "[1:2:3:4::192.168.32.1]:80" }, null, 200, null); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader3() throws Exception { + test(new String[] { "[1:2:3:4::192.355.32.1]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader() throws Exception { + test(new String[] { "[vAF.1:2:3:4::]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader2() throws Exception { + test(new String[] { "[vG.1:2:3:4::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testIPvFutureHostHeader3() throws Exception { + test(new String[] { "[vAF.abdc:abcd_-~AF]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader4() throws Exception { + test(new String[] { "[vAF.ImVal1.com-._~!$&'()*+,;=:]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader5() throws Exception { + test(new String[] { "[vAF.]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader6() throws Exception { + test(new String[] { "[vAF]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader7() throws Exception { + test(new String[] { "[v.abcd]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPRegNameHostHeader() throws Exception { + test(new String[] { "366.66.12.12" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader2() throws Exception { + test(new String[] { "domain.com%20" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader3() throws Exception { + test(new String[] { "domain.com%2" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPRegNameHostHeader4() throws Exception { + test(new String[] { "doma&n.com%20" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader5() throws Exception { + test(new String[] { "ImVal1.com-._~!$&'()*+,;=" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader6() throws Exception { + // test userinfo presence? + test(new String[] { "juicyUserInfo@ImVal1.com-._~!$&'()*+,;=" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testAbsoluteURLBad() throws Exception { + // test userinfo presence? + test(new String[] { "wrong.com:8080" }, new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 400, HostHeaderHandler.STATUS_HOST_NO_MATCH); + } + + @Test + public void testAbsoluteURLGood() throws Exception { + // test userinfo presence? + test(new String[] { DefaultServer.getHostAddress() + ":" + DefaultServer.getHostPort() }, + new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 200, null); + } + + @Test + public void testEmptyHost() throws Exception { + // test userinfo presence? + test(new String[] { "" }, + null, 200, null); + } + + @Test + public void testEmptyHost2() throws Exception { + // test userinfo presence? + test(new String[] { "" }, + new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 400, HostHeaderHandler.STATUS_HOST_NO_MATCH); + } + + public void test(final String[] headers, final HttpHost proxy, final int resultCode, final String statusMessage) + throws ClientProtocolException, IOException { + TestHttpClient client = new TestHttpClient(); + if (proxy != null) { + client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + } + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL()); + for (String i : headers) { + get.addHeader(Headers.HOST_STRING, i); + } + HttpResponse result = client.execute(get, HttpClientContext.create()); + Assert.assertEquals(result.getStatusLine().getReasonPhrase(), resultCode, result.getStatusLine().getStatusCode()); + if (statusMessage != null) { + Assert.assertEquals(statusMessage, result.getStatusLine().getReasonPhrase()); + } + + } finally { + client.getConnectionManager().shutdown(); + } + } + + public void testProxyMode(final String[] headers, final int resultCode, final String statusMessage) throws ClientProtocolException, IOException { + // this has to be done this way in order to trick apache to use absolute form... + HttpHost proxy = new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()); + TestHttpClient client = new TestHttpClient(); + client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL()); + for (String i : headers) { + get.addHeader(Headers.HOST_STRING, i); + } + HttpResponse result = client.execute(get, HttpClientContext.create()); + Assert.assertEquals(result.getStatusLine().getReasonPhrase(), resultCode, result.getStatusLine().getStatusCode()); + + } finally { + client.getConnectionManager().shutdown(); + } + } + +}
core/src/test/java/io/undertow/server/handlers/QueryParametersWithAllowUnescapedCharactersTestCase.java+11 −6 modified@@ -59,16 +59,21 @@ public static void clearProxyOptions() { public static void setQueryStringsArray() { // format is: {queryString, expected result} queryStrings = new String[][] { new String[] { "/path?unicode=Iñtërnâtiônàližætiøn", - "unicode=Iñtërnâtiônàližætiøn{unicode=>Iñtërnâtiônàližætiøn}" }, - new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb bb{a=>b,value=>bb bb}" }, + //"unicode=Iñtërnâtiônàližætiøn{unicode=>Iñtërnâtiônàližætiøn}" }, + "unicode=I%C3%B1t%C3%ABrn%C3%A2ti%C3%B4n%C3%A0li%C5%BE%C3%A6ti%C3%B8n{unicode=>Iñtërnâtiônàližætiøn}" }, + //new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb bb{a=>b,value=>bb bb}" }, + new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb%20bb{a=>b,value=>bb bb}" }, new String[] { "/path?a=b&value=bb&value=cc", "a=b&value=bb&value=cc{a=>b,value=>[bb,cc]}" }, new String[] { "/path?&a=b&value=bb&&value=cc", "&a=b&value=bb&&value=cc{a=>b,value=>[bb,cc]}" }, - // Specifing some query parameters with empty by intentional for the test purpose. These should be ignored. + // Specifying some query parameters with empty by intentional for the test purpose. These should be ignored. new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20", - "a=b&value=bb&value=cc&s &t {a=>b,s =>,t =>,value=>[bb,cc]}" }, + //"a=b&value=bb&value=cc&s &t {a=>b,s =>,t =>,value=>[bb,cc]}" }, + "a=b&value=bb&value=cc&s%20&t%20{a=>b,s =>,t =>,value=>[bb,cc]}" }, new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20&", - "a=b&value=bb&value=cc&s &t &{a=>b,s =>,t =>,value=>[bb,cc]}" }, + //"a=b&value=bb&value=cc&s &t &{a=>b,s =>,t =>,value=>[bb,cc]}" }, + "a=b&value=bb&value=cc&s%20&t%20&{a=>b,s =>,t =>,value=>[bb,cc]}" }, new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20&u", - "a=b&value=bb&value=cc&s &t &u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; + //"a=b&value=bb&value=cc&s &t &u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; + "a=b&value=bb&value=cc&s%20&t%20&u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; } }
core/src/test/java/io/undertow/server/handlers/ReceiverTestCase.java+14 −0 modified@@ -35,6 +35,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; import java.io.IOException; import java.io.OutputStream; @@ -44,6 +45,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import static io.undertow.UndertowOptions.MAX_ENTITY_SIZE; + /** * @author Stuart Douglas */ @@ -61,6 +64,17 @@ public void error(HttpServerExchange exchange, IOException e) { } }; + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + + @BeforeClass public static void setup() { HttpHandler testFullString = new HttpHandler() {
core/src/test/java/io/undertow/server/protocol/ajp/AjpParsingUnitTestCase.java+2 −1 modified@@ -126,7 +126,8 @@ public void testCharsetHandling() throws Exception { Assert.assertFalse(state.badRequest); Assert.assertEquals("/한글이름", result.getRequestPath()); Assert.assertEquals("/한글이름", result.getRequestURI()); - Assert.assertEquals("param=한글이름", result.getQueryString()); + Assert.assertEquals("param=한글이름", result.getDecodedQueryString()); + Assert.assertEquals("param=í\u0095\u009Cê¸\u0080ì\u009D´ë¦\u0084", result.getQueryString()); } @Test
servlet/src/main/java/io/undertow/servlet/attribute/ServletRelativePathAttribute.java+2 −3 modified@@ -22,7 +22,6 @@ import io.undertow.attribute.ExchangeAttributeBuilder; import io.undertow.attribute.ReadOnlyAttributeException; import io.undertow.attribute.RelativePathAttribute; -import io.undertow.attribute.RequestURLAttribute; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.handlers.ServletRequestContext; @@ -48,12 +47,12 @@ private ServletRelativePathAttribute() { public String readAttribute(final HttpServerExchange exchange) { ServletRequestContext src = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); if(src == null) { - return RequestURLAttribute.INSTANCE.readAttribute(exchange); + return RelativePathAttribute.INSTANCE.readAttribute(exchange); } String path = (String) src.getServletRequest().getAttribute(RequestDispatcher.FORWARD_PATH_INFO); String sp = (String) src.getServletRequest().getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH); if(path == null && sp == null) { - return RequestURLAttribute.INSTANCE.readAttribute(exchange); + return RelativePathAttribute.INSTANCE.readAttribute(exchange); } if(sp == null) { return path;
servlet/src/main/java/io/undertow/servlet/attribute/ServletRequestLineAttribute.java+2 −2 modified@@ -57,9 +57,9 @@ public String readAttribute(final HttpServerExchange exchange) { if (query != null && !query.isEmpty()) { sb.append('?'); sb.append(query); - } else if (!exchange.getQueryString().isEmpty()) { + } else if (!exchange.getDecodedQueryString().isEmpty()) { sb.append('?'); - sb.append(exchange.getQueryString()); + sb.append(exchange.getDecodedQueryString()); } sb.append(' ') .append(exchange.getProtocol().toString()).toString();
servlet/src/main/java/io/undertow/servlet/spec/HttpServletRequestImpl.java+1 −1 modified@@ -309,7 +309,7 @@ public String getContextPath() { @Override public String getQueryString() { - return exchange.getQueryString().isEmpty() ? null : exchange.getQueryString(); + return exchange.getDecodedQueryString().isEmpty() ? null : exchange.getDecodedQueryString(); } @Override
servlet/src/main/java/io/undertow/servlet/util/DispatchUtils.java+4 −4 modified@@ -229,18 +229,18 @@ private static String assignRequestPath(final String path, final HttpServletRequ if (include) { // include does not modify exchange paths, just add the query string and request uri // the rest of attributes are added via the match later - requestImpl.setAttribute(INCLUDE_QUERY_STRING, fake.getQueryString()); + requestImpl.setAttribute(INCLUDE_QUERY_STRING, fake.getDecodedQueryString()); requestImpl.setAttribute(INCLUDE_REQUEST_URI, fake.getRequestURI()); } else { exchange.setRelativePath(newRequestPath); exchange.setRequestPath(fake.getRequestPath()); exchange.setRequestURI(fake.getRequestURI()); - if (!fake.getQueryString().isEmpty()) { - exchange.setQueryString(fake.getQueryString()); + if (!fake.getDecodedQueryString().isEmpty()) { + exchange.setDecodedQueryString(fake.getDecodedQueryString()); } } // both forward and include merge parameters by spec - if (!fake.getQueryString().isEmpty()) { + if (!fake.getDecodedQueryString().isEmpty()) { final Map<String, Deque<String>> merged = QueryParameterUtils.mergeQueryParameters(fake.getQueryParameters(), exchange.getQueryParameters()); requestImpl.setQueryParameters(null); exchange.getQueryParameters().clear();
servlet/src/test/java/io/undertow/servlet/test/multipart/MultiPartTestCase.java+16 −3 modified@@ -32,7 +32,6 @@ import io.undertow.servlet.test.util.DeploymentUtils; import io.undertow.testutils.DefaultServer; import io.undertow.testutils.HttpClientUtils; -import io.undertow.testutils.ProxyIgnore; import io.undertow.testutils.TestHttpClient; import io.undertow.util.StatusCodes; import org.apache.http.HttpResponse; @@ -44,18 +43,20 @@ import org.apache.http.entity.mime.content.StringBody; import org.jboss.logging.Logger; import org.junit.Assert; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; +import static io.undertow.UndertowOptions.MULTIPART_MAX_ENTITY_SIZE; import static io.undertow.servlet.Servlets.multipartConfig; import static io.undertow.servlet.Servlets.servlet; /** * @author Stuart Douglas */ @RunWith(DefaultServer.class) -@ProxyIgnore public class MultiPartTestCase { @@ -82,6 +83,16 @@ public void handleDeployment(DeploymentInfo deploymentInfo, ServletContext servl .setMultipartConfig(multipartConfig(null, 3, 0, 0))); } + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(MULTIPART_MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + @Test public void testMultiPartRequestWithNoMultipartConfig() throws IOException { TestHttpClient client = new TestHttpClient(); @@ -180,7 +191,9 @@ public void testMultiPartRequestWithAddedServlet() throws IOException { } @Test - public void testMultiPartRequestToLarge() throws IOException { + public void testMultiPartRequestTooLarge() throws IOException { + // FIXME UNDERTOW-2572 + Assume.assumeFalse(DefaultServer.isH2()); TestHttpClient client = new TestHttpClient(); try { String uri = DefaultServer.getDefaultServerURL() + "/servletContext/2";
servlet/src/test/java/io/undertow/servlet/test/streams/ServletInputStreamEarlyCloseClientSideTestCase.java+15 −6 modified@@ -18,31 +18,31 @@ package io.undertow.servlet.test.streams; +import io.undertow.UndertowOptions; import io.undertow.servlet.api.ServletInfo; import io.undertow.servlet.test.util.DeploymentUtils; import io.undertow.testutils.DefaultServer; -import io.undertow.testutils.HttpOneOnly; import io.undertow.testutils.TestHttpClient; import org.junit.Assert; -import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; import javax.servlet.ServletException; import java.io.OutputStream; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; /** * Tests the behaviour of the input stream when the connection is closed on the client side * <p> - * https://issues.jboss.org/browse/WFLY-4827 + * (see WFLY-4827) * * @author Stuart Douglas */ @RunWith(DefaultServer.class) -@HttpOneOnly public class ServletInputStreamEarlyCloseClientSideTestCase { public static final String SERVLET = "servlet"; @@ -54,9 +54,18 @@ public static void setup() throws ServletException { .addMapping("/" + SERVLET)); } + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(UndertowOptions.MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + @Test public void testServletInputStreamEarlyClose() throws Exception { - Assume.assumeFalse(DefaultServer.isH2()); TestHttpClient client = new TestHttpClient(); EarlyCloseClientServlet.reset(); try (Socket socket = new Socket()) { @@ -70,7 +79,7 @@ public void testServletInputStreamEarlyClose() throws Exception { String request = "POST /servletContext/" + SERVLET + " HTTP/1.1\r\nHost:localhost\r\nContent-Length:" + sb.length() + 100 + "\r\n\r\n" + sb.toString(); OutputStream outputStream = socket.getOutputStream(); - outputStream.write(request.getBytes("US-ASCII")); + outputStream.write(request.getBytes(StandardCharsets.US_ASCII)); outputStream.flush(); socket.close();
websockets-jsr/src/main/java/io/undertow/websockets/jsr/FrameHandler.java+4 −1 modified@@ -269,8 +269,9 @@ private void invokeTextHandler(final BufferedTextMessage data, final HandlerWrap @Override public void run() { MessageHandler mHandler = handler.getHandler(); + final ClassLoader oldCL = Thread.currentThread().getContextClassLoader(); try { - + Thread.currentThread().setContextClassLoader(mHandler.getClass().getClassLoader()); if (mHandler instanceof MessageHandler.Partial) { if (handler.decodingNeeded) { Object object = getSession().getEncoding().decodeText(handler.getMessageType(), message); @@ -292,6 +293,8 @@ public void run() { } } catch (Exception e) { invokeOnError(e); + } finally { + Thread.currentThread().setContextClassLoader(oldCL); } } });
websockets-jsr/src/main/java/io/undertow/websockets/jsr/ServerWebSocketContainer.java+2 −2 modified@@ -28,7 +28,7 @@ import io.undertow.servlet.util.ConstructorInstanceFactory; import io.undertow.servlet.util.ImmediateInstanceHandle; import io.undertow.servlet.websockets.ServletWebSocketHttpExchange; -import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.WeakCopyOnWriteMap; import io.undertow.util.PathTemplate; import io.undertow.util.StatusCodes; import io.undertow.websockets.WebSocketExtension; @@ -104,7 +104,7 @@ public class ServerWebSocketContainer implements ServerContainer, Closeable { private final ClassIntrospecter classIntrospecter; - private final Map<Class<?>, ConfiguredClientEndpoint> clientEndpoints = new CopyOnWriteMap<>(); + private final Map<Class<?>, ConfiguredClientEndpoint> clientEndpoints = new WeakCopyOnWriteMap<>(); private final List<ConfiguredServerEndpoint> configuredServerEndpoints = new ArrayList<>(); private final Set<Class<?>> annotatedEndpointClasses = new HashSet<>();
websockets-jsr/src/main/java/io/undertow/websockets/jsr/UndertowContainerProvider.java+28 −16 modified@@ -84,26 +84,38 @@ static ServerWebSocketContainer getDefaultContainer() { //this is not great, as we have no way to control the lifecycle //but there is not much we can do //todo: what options should we use here? - ByteBufferPool buffers = new DefaultByteBufferPool(directBuffers, 1024, 100, 12); - defaultContainer = new ServerWebSocketContainer(defaultIntrospector, UndertowContainerProvider.class.getClassLoader(), new Supplier<XnioWorker>() { - volatile XnioWorker worker; - - @Override - public XnioWorker get() { - if(worker == null) { - synchronized (this) { - if(worker == null) { - try { - worker = Xnio.getInstance().createWorker(OptionMap.create(Options.THREAD_DAEMON, true)); - } catch (IOException e) { - throw new RuntimeException(e); + //final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + try { + //Thread.currentThread().setContextClassLoader(null); + ByteBufferPool buffers = new DefaultByteBufferPool(directBuffers, 1024, 100, 12); + defaultContainer = new ServerWebSocketContainer(defaultIntrospector, UndertowContainerProvider.class.getClassLoader(), new Supplier<XnioWorker>() { + volatile XnioWorker worker; + + @Override + public XnioWorker get() { + final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(null); + if (worker == null) { + synchronized (this) { + if (worker == null) { + try { + worker = Xnio.getInstance().createWorker(OptionMap.create(Options.THREAD_DAEMON, true)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } + } finally { + Thread.currentThread().setContextClassLoader(tccl); } + return worker; } - return worker; - } - }, buffers, Collections.EMPTY_LIST, !invokeInIoThread); + }, buffers, Collections.EMPTY_LIST, !invokeInIoThread); + } finally { + //Thread.currentThread().setContextClassLoader(tccl); + } } return defaultContainer; }
8318dd36fdc2Merge pull request #1860 from fl4via/backport-fixes_2.3.x
49 files changed · +1245 −165
build.metadata+15 −0 added@@ -0,0 +1,15 @@ +# Created by buildmetadata-maven-plugin 1.7.0 ( SHA: 6f444cae ) +build.artifactId=undertow-websockets-jsr +build.groupId=io.undertow +build.java.compiler=HotSpot 64-Bit Tiered Compilers +build.java.runtime.name=OpenJDK Runtime Environment +build.java.runtime.version=1.8.0_422-b05 +build.java.vendor=Red Hat, Inc. +build.java.vm=OpenJDK 64-Bit Server VM +build.maven.execution.cmdline=-Djavax.net.ssl.trustStore\=/home/aogburn/bin/builder/maven.truststore -Djavax.net.ssl.trustStorePassword\=rhmaven -s /home/aogburn/bin/builder/eap-build-settings.xml clean install -DskipTests +build.maven.version=3.9.6 +build.scmRevision.date=17.09.2024 +build.scmRevision.id=aaa36f6ad214aecd7d0d611cad582aa058f3a3e8 +build.scmRevision.url=scm\:git\://github.com/undertow-io/undertow.git/undertow-websockets-jsr +build.version=2.2.33.SP2-redhat-00001 +build.version.full=2.2.33.SP2-redhat-00001raaa36f6ad214aecd7d0d611cad582aa058f3a3e8
core/src/main/java/io/undertow/attribute/QueryStringAttribute.java+1 −1 modified@@ -42,7 +42,7 @@ private QueryStringAttribute(boolean includeQuestionMark) { @Override public String readAttribute(final HttpServerExchange exchange) { - String qs = exchange.getQueryString(); + String qs = exchange.getDecodedQueryString(); if(qs.isEmpty() || !includeQuestionMark) { return qs; }
core/src/main/java/io/undertow/attribute/RequestLineAttribute.java+2 −2 modified@@ -42,9 +42,9 @@ public String readAttribute(final HttpServerExchange exchange) { .append(exchange.getRequestMethod().toString()) .append(' ') .append(exchange.getRequestURI()); - if (!exchange.getQueryString().isEmpty()) { + if (!exchange.getDecodedQueryString().isEmpty()) { sb.append('?'); - sb.append(exchange.getQueryString()); + sb.append(exchange.getDecodedQueryString()); } sb.append(' ') .append(exchange.getProtocol().toString()).toString();
core/src/main/java/io/undertow/conduits/FixedLengthStreamSourceConduit.java+5 −0 modified@@ -371,6 +371,11 @@ private void exitRead(long consumed, Throwable readError) throws IOException { } long newVal = oldVal - consumed; state = newVal; + if (allAreClear(state, MASK_COUNT)) { + if (allAreClear(state, FLAG_FINISHED)) { + next.suspendReads(); + } + } } private void invokeFinishListener() {
core/src/main/java/io/undertow/Handlers.java+12 −0 modified@@ -32,6 +32,7 @@ import io.undertow.server.handlers.DisableCacheHandler; import io.undertow.server.handlers.ExceptionHandler; import io.undertow.server.handlers.GracefulShutdownHandler; +import io.undertow.server.handlers.HostHeaderHandler; import io.undertow.server.handlers.HttpContinueAcceptingHandler; import io.undertow.server.handlers.HttpContinueReadHandler; import io.undertow.server.handlers.HttpTraceHandler; @@ -600,6 +601,17 @@ public static LearningPushHandler learningPushHandler(int maxEntries, HttpHandle return new LearningPushHandler(maxEntries, -1, next); } + /** + * Creates a handler that automatically vets Host header content/absence/presence according to + * https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 and related + * + * @param next The next handler + * @return A host header handler + */ + public static HostHeaderHandler hostHeaderHandler(HttpHandler next) { + return new HostHeaderHandler(next); + } + private Handlers() { }
core/src/main/java/io/undertow/security/handlers/SinglePortConfidentialityHandler.java+1 −1 modified@@ -65,7 +65,7 @@ protected URI getRedirectURI(final HttpServerExchange exchange, final int port) } } uriBuilder.append(uri); - final String queryString = exchange.getQueryString(); + final String queryString = exchange.getDecodedQueryString(); if (queryString != null && !queryString.isEmpty()) { uriBuilder.append("?").append(queryString); }
core/src/main/java/io/undertow/security/impl/DigestAuthenticationMechanism.java+4 −4 modified@@ -235,17 +235,17 @@ private AuthenticationMechanismOutcome handleDigestHeader(HttpServerExchange exc if(parsedHeader.containsKey(DigestAuthorizationToken.DIGEST_URI)) { String uri = parsedHeader.get(DigestAuthorizationToken.DIGEST_URI); String requestURI = exchange.getRequestURI(); - if(!exchange.getQueryString().isEmpty()) { - requestURI = requestURI + "?" + exchange.getQueryString(); + if(!exchange.getDecodedQueryString().isEmpty()) { + requestURI = requestURI + "?" + exchange.getDecodedQueryString(); } if(!uri.equals(requestURI)) { //it is possible we were given an absolute URI //we reconstruct the URI from the host header to make sure they match up //I am not sure if this is overly strict, however I think it is better //to be safe than sorry requestURI = exchange.getRequestURL(); - if(!exchange.getQueryString().isEmpty()) { - requestURI = requestURI + "?" + exchange.getQueryString(); + if(!exchange.getDecodedQueryString().isEmpty()) { + requestURI = requestURI + "?" + exchange.getDecodedQueryString(); } if(!uri.equals(requestURI)) { //just end the auth process
core/src/main/java/io/undertow/server/Connectors.java+2 −3 modified@@ -546,10 +546,9 @@ public static void setExchangeRequestPath(final HttpServerExchange exchange, fin } if(requiresDecode && allowUnescapedCharactersInUrl) { final String decodedQS = URLUtils.decode(qs, charset, decodeSlashFlag,false, decodeBuffer); - exchange.setQueryString(decodedQS); - } else { - exchange.setQueryString(qs); + exchange.setDecodedQueryString(decodedQS); } + exchange.setQueryString(qs); URLUtils.parseQueryString(qs, exchange, charset, decodeQueryString, maxParameters); return;
core/src/main/java/io/undertow/server/handlers/accesslog/ExtendedAccessLogParser.java+2 −2 modified@@ -328,15 +328,15 @@ protected ExchangeAttribute getClientToServerElement( return new ExchangeAttribute() { @Override public String readAttribute(HttpServerExchange exchange) { - String query = exchange.getQueryString(); + String query = exchange.getDecodedQueryString(); if (query.isEmpty()) { return exchange.getRequestURI(); } else { StringBuilder buf = new StringBuilder(); buf.append(exchange.getRequestURI()); buf.append('?'); - buf.append(exchange.getQueryString()); + buf.append(exchange.getDecodedQueryString()); return buf.toString(); } }
core/src/main/java/io/undertow/server/handlers/form/MultiPartParserDefinition.java+1 −1 modified@@ -109,7 +109,7 @@ public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener.proceed(); } }); - Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE); + Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MULTIPART_MAX_ENTITY_SIZE ); if(sizeLimit != null && sizeLimit > 0) { // do not overwrite the entity size with sizeLimit that is <= 0 exchange.setMaxEntitySize(sizeLimit); }
core/src/main/java/io/undertow/server/handlers/HostHeaderHandler.java+295 −0 added@@ -0,0 +1,295 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; + +import java.util.regex.Pattern; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.NetworkUtils; +import io.undertow.util.Protocols; +import io.undertow.util.StatusCodes; + +/** + * Handler which check if Host header is properly formed and present. + * + * @author baranowb + */ +public class HostHeaderHandler implements HttpHandler { + + public static final HandlerWrapper WRAPPER = new Wrapper(); + + public static final String STATUS_NO_HOST_HEADER = "No Host Header"; + public static final String STATUS_TOO_MANY_HOST_HEADERS = "Only One Host Header Allowed"; + public static final String STATUS_MALFORMED_PORT = "Host Header Malformed Port"; + public static final String STATUS_MALFORMED_IP_LITERAL = "Host Header Malformed IP-Literal"; + public static final String STATUS_MALFORMED_IP_LITERAL_BAD_CHARS = "Host Header Bad Characters"; + public static final String STATUS_HOST_NO_MATCH = "URI Host Header NO MATCH"; + private static final Pattern IP4_EXACT = Pattern.compile(NetworkUtils.IP4_EXACT); + private static final Pattern IP6_EXACT = Pattern.compile(NetworkUtils.IP6_EXACT); + private static final boolean[] ALLOWED_REGNAME_CHARACTERS = new boolean[256]; + private static final boolean[] HEX_CHARACTERS = new boolean[256]; + private static final boolean[] ALLOWED_IPv_FUTURE_CHARACTERS = new boolean[256]; // this is almost the same as + // ALLOWED_REGNAME_CHARACTERS with bonus + // ":" but having it as array rather than + // extra check is just faster + static { + // reg-name = *( unreserved / pct-encoded / sub-delims ) + // ALPHA / DIGIT / "-" / "." / "_" / "~" , %%, and "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + for (int i = 0; i < ALLOWED_REGNAME_CHARACTERS.length; ++i) { + if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z')) { + ALLOWED_REGNAME_CHARACTERS[i] = true; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = true; + } else { + switch (i) { + case '-': + case '.': + case '_': + case '~': + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': { + ALLOWED_REGNAME_CHARACTERS[i] = true; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = true; + break; + } + default: + ALLOWED_REGNAME_CHARACTERS[i] = false; + ALLOWED_IPv_FUTURE_CHARACTERS[i] = false; + } + } + + } + + ALLOWED_IPv_FUTURE_CHARACTERS[':'] = true; + + for (int i = 0; i < HEX_CHARACTERS.length; ++i) { + if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) { + HEX_CHARACTERS[i] = true; + } else + HEX_CHARACTERS[i] = false; + } + + } + private final HttpHandler next; + + public HostHeaderHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + // TODO: add debug/warn log? + // 400 if in case of no Host header or more than one: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 + // 400 if value violate rules. Host = uri-host [ ":" port ] + // uri-host https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 + // port https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3 + // NOTE: that 3.2.2 is NOT as restrictive as pure DNS it allows subdelims, percent etc by design as to not be + // restricted to pure DNS, ALTHOUGH DNS compliant content is preferred. + final HeaderMap headerMap = exchange.getRequestHeaders(); + final HeaderValues headerValues = headerMap.get(Headers.HOST); + final HttpString protocol = exchange.getProtocol(); + + if((protocol.equals(Protocols.HTTP_0_9) || protocol.equals(Protocols.HTTP_1_0))){ + if (headerValues == null) { + //TODO: should we fake Host till we make it? + next.handleRequest(exchange); + return; + } + //else { + //clients want to be good citizens and send it anyway. + //fall through to below, first check will be false, but rest is the same as for HTTP1.1+ + //} + } + + if (headerValues == null || headerValues.size() == 0) { + // isEmpty - we assume http/https ? so authority is defined for this type, it cant be empty? + terminate(exchange, STATUS_NO_HOST_HEADER); + return; + } else if (headerValues.size() > 1) { + terminate(exchange, STATUS_TOO_MANY_HOST_HEADERS); + return; + } + + + // parsing time. + final String headerValue = headerValues.element(); + // uri-host [ ":" port ] + // This is tricky, IP-Literal contain :, which is port delimiter in pair + // Lets just try to take take care of port first + final int rightBracketIndex = headerValue.lastIndexOf(']'); + final int lastColonIndex = headerValue.lastIndexOf(':'); + + final String hostHeaderURI; + // in case of IPv4, rightBracketIndex will be -1, in case of IPv6, it MUST be less than last : + if (rightBracketIndex < lastColonIndex) { + // we have port or potentially malformed IP Literal: + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" - without right bracket + if (rightBracketIndex == -1 && headerValue.startsWith("[")) { + // bad [ = 0, ] = -1, : = n+ + // good [ = 0, ] = n , : = n+x + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + // we have valid host-uri with port + final String portString = headerValue.substring(lastColonIndex + 1); + try { + int port = Integer.parseInt(portString); + if (port <= 0 || port > 65535) { + // sanity check + // NOTE: 3.2.3 does not have provision like for IPv4 - decimal between 0-255. + // so this might be too restrictive + terminate(exchange, STATUS_MALFORMED_PORT); + return; + } + // fall through to uri-host checks + } catch (NumberFormatException nfe) { + terminate(exchange, STATUS_MALFORMED_PORT); + return; + } + + hostHeaderURI = headerValue.substring(0, lastColonIndex); + } else { + hostHeaderURI = headerValue; + } + + // at this point we either have IP-Literal, IPv4 address or custom name + if (rightBracketIndex > 0 && hostHeaderURI.indexOf('[') != 0) { + // 1:2:4]* + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } else if (rightBracketIndex > 0 && hostHeaderURI.indexOf('[') == 0) { + // IPv6 or IPFuture + // IP-literal = "[" ( IPv6address / IPvFuture ) "]" + // IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + final String debracked = hostHeaderURI.substring(1, hostHeaderURI.length() - 1); + if (debracked.startsWith("v")) { + final int dotIndex = debracked.indexOf("."); + if (dotIndex < 2) { + // we need at least one HEX + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + + final String hex = debracked.substring(1, dotIndex); + for (int i = 0; i < hex.length(); i++) { + final char c1 = hex.charAt(i); + if (!HEX_CHARACTERS[c1]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } + if (dotIndex + 1 >= debracked.length()) { + // we need some character behind dot. + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + + for (int i = dotIndex + 1; i < debracked.length(); i++) { + final char c = debracked.charAt(i); + if (!ALLOWED_IPv_FUTURE_CHARACTERS[c]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } + } else { + // This will match IPv6 and IPv6 with embedded IPv4 + if (!IP6_EXACT.matcher(debracked).matches()) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + // TODO: as in case of IPv6 do we need to vet some weir daddresses? + } + + } else if (IP4_EXACT.matcher(hostHeaderURI).matches()) { + // IPv4 + // NOTE: above will match only valid range 0-255, rest will fall through to reg-name, which is ok, since its + // essentially + // superset of IP, given DIGIT + "." ( unreserved ) + } else { + // registered name can contain . and digits, so it technically overlap IPv4. + // above wont cover -192.168.1.1/355.0.0.0, which technically is correct 'reg-name' + // at this point we can really only check if its valid char or percent encoding. + for (int index = 0; index < hostHeaderURI.length(); index++) { + final char c = hostHeaderURI.charAt(index); + if (c == '%') { + // we need at least two more that are HEX + if (index + 2 < hostHeaderURI.length()) { + char c1 = hostHeaderURI.charAt(++index); + char c2 = hostHeaderURI.charAt(++index); + if (!(HEX_CHARACTERS[c1] && HEX_CHARACTERS[c2])) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + } else { + // we dont have at least two chars(hex), so its not proper percent escape + terminate(exchange, STATUS_MALFORMED_IP_LITERAL); + return; + } + } else if (!ALLOWED_REGNAME_CHARACTERS[c]) { + terminate(exchange, STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + return; + } + // valid + } + } + // NOTE: at this point if userinfo("@") was present in host, it would have failed + // we need only to check if Host header value is contained within URI if its absolute or authority + if (exchange.isHostIncludedInRequestURI()) { + if (!exchange.getRequestURI().contains(hostHeaderURI)) { + terminate(exchange, STATUS_HOST_NO_MATCH); + return; + } else if (hostHeaderURI.isEmpty()) { + terminate(exchange, STATUS_HOST_NO_MATCH); + return; + } + } + + // in the end... + next.handleRequest(exchange); + } + + private void terminate(final HttpServerExchange exchange, final String message) { + exchange.setStatusCode(StatusCodes.BAD_REQUEST); + exchange.setResponseContentLength(0); + exchange.getResponseHeaders().add(Headers.CONNECTION, Headers.CLOSE.toString()); + exchange.setReasonPhrase(message); + exchange.endExchange(); + } + + private static class Wrapper implements HandlerWrapper { + + @Override + public HttpHandler wrap(HttpHandler handler) { + return new HostHeaderHandler(handler); + } + } +}
core/src/main/java/io/undertow/server/handlers/HttpTraceHandler.java+2 −2 modified@@ -49,9 +49,9 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "message/http"); StringBuilder body = new StringBuilder("TRACE "); body.append(exchange.getRequestURI()); - if(!exchange.getQueryString().isEmpty()) { + if(!exchange.getDecodedQueryString().isEmpty()) { body.append('?'); - body.append(exchange.getQueryString()); + body.append(exchange.getDecodedQueryString()); } body.append(' '); body.append(exchange.getProtocol().toString());
core/src/main/java/io/undertow/server/handlers/IPAddressAccessControlHandler.java+4 −4 modified@@ -59,12 +59,12 @@ public class IPAddressAccessControlHandler implements HttpHandler { /** * Standard IP address, with some octets replaced by a '*' */ - private static final Pattern IP4_WILDCARD = Pattern.compile("(?:(?:\\d{1,3}|\\*)\\.){3}(?:\\d{1,3}|\\*)"); + private static final Pattern IP4_WILDCARD = Pattern.compile("(?:(?:"+NetworkUtils.IP4_SEGMENT+"|\\*)\\.){3}(?:"+NetworkUtils.IP4_SEGMENT+"|\\*)"); /** * IPv4 address with subnet specified via slash notation */ - private static final Pattern IP4_SLASH = Pattern.compile("(?:\\d{1,3}\\.){3}\\d{1,3}\\/\\d\\d?"); + private static final Pattern IP4_SLASH = Pattern.compile("(?:"+NetworkUtils.IP4_SEGMENT+"\\.){3}(?:"+NetworkUtils.IP4_SEGMENT+")\\/\\d\\d?"); /** * Standard full IPv6 address @@ -74,12 +74,12 @@ public class IPAddressAccessControlHandler implements HttpHandler { /** * Standard full IPv6 address, with some parts replaced by a '*' */ - private static final Pattern IP6_WILDCARD = Pattern.compile("(?:(?:[a-zA-Z0-9]{1,4}|\\*):){7}(?:[a-zA-Z0-9]{1,4}|\\*)"); + private static final Pattern IP6_WILDCARD = Pattern.compile("(?:(?:"+NetworkUtils.IP6_SEGMENT+"|\\*):){7}(?:"+NetworkUtils.IP6_SEGMENT+"|\\*)"); /** * Standard full IPv6 address with subnet specified via slash notation */ - private static final Pattern IP6_SLASH = Pattern.compile("(?:[a-zA-Z0-9]{1,4}:){7}[a-zA-Z0-9]{1,4}\\/\\d{1,3}"); + private static final Pattern IP6_SLASH = Pattern.compile("(?:"+NetworkUtils.IP6_SEGMENT+":){7}"+NetworkUtils.IP6_SEGMENT+"\\/\\d{1,3}"); private volatile HttpHandler next; private volatile boolean defaultAllow = false;
core/src/main/java/io/undertow/server/handlers/JDBCLogHandler.java+1 −1 modified@@ -135,7 +135,7 @@ public void logMessage(String pattern, HttpServerExchange exchange) { } else { jdbcLogAttribute.user = sc.getAuthenticatedAccount().getPrincipal().getName(); } - jdbcLogAttribute.query = exchange.getQueryString(); + jdbcLogAttribute.query = exchange.getDecodedQueryString(); jdbcLogAttribute.bytes = exchange.getResponseContentLength(); if (jdbcLogAttribute.bytes < 0) {
core/src/main/java/io/undertow/server/handlers/LearningPushHandler.java+3 −3 modified@@ -77,12 +77,12 @@ public LearningPushHandler(int maxPathEtries, int maxPathAge, int maxPushEtries, public void handleRequest(HttpServerExchange exchange) throws Exception { String fullPath; String requestPath; - if(exchange.getQueryString().isEmpty()) { + if(exchange.getDecodedQueryString().isEmpty()) { fullPath = exchange.getRequestURL(); requestPath = exchange.getRequestPath(); } else{ - fullPath = exchange.getRequestURL() + "?" + exchange.getQueryString(); - requestPath = exchange.getRequestPath() + "?" + exchange.getQueryString(); + fullPath = exchange.getRequestURL() + "?" + exchange.getDecodedQueryString(); + requestPath = exchange.getRequestPath() + "?" + exchange.getDecodedQueryString(); } doPush(exchange, fullPath);
core/src/main/java/io/undertow/server/handlers/proxy/ProxyHandler.java+1 −1 modified@@ -449,7 +449,7 @@ public void run() { } requestURI.append(targetURI); - String qs = exchange.getNonDecodedQueryString(); + String qs = exchange.getQueryString(); if (qs != null && !qs.isEmpty()) { requestURI.append('?'); requestURI.append(qs);
core/src/main/java/io/undertow/server/handlers/RequestDumpingHandler.java+1 −1 modified@@ -98,7 +98,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { sb.append("\n"); } sb.append(" protocol=" + exchange.getProtocol() + "\n"); - sb.append(" queryString=" + exchange.getQueryString() + "\n"); + sb.append(" queryString=" + exchange.getDecodedQueryString() + "\n"); sb.append(" remoteAddr=" + exchange.getSourceAddress() + "\n"); sb.append(" remoteHost=" + exchange.getSourceAddress().getHostName() + "\n"); sb.append(" scheme=" + exchange.getRequestScheme() + "\n");
core/src/main/java/io/undertow/server/handlers/resource/DirectoryUtils.java+2 −2 modified@@ -60,12 +60,12 @@ public static boolean sendRequestedBlobs(HttpServerExchange exchange) { String type = null; String etag = null; String quotedEtag = null; - if ("css".equals(exchange.getQueryString())) { + if ("css".equals(exchange.getDecodedQueryString())) { buffer = Blobs.FILE_CSS_BUFFER.duplicate(); type = "text/css"; etag = Blobs.FILE_CSS_ETAG; quotedEtag = Blobs.FILE_CSS_ETAG_QUOTED; - } else if ("js".equals(exchange.getQueryString())) { + } else if ("js".equals(exchange.getDecodedQueryString())) { buffer = Blobs.FILE_JS_BUFFER.duplicate(); type = "application/javascript"; etag = Blobs.FILE_JS_ETAG;
core/src/main/java/io/undertow/server/handlers/StuckThreadDetectionHandler.java+1 −1 modified@@ -154,7 +154,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { // GC'ing, as the reference is removed from the Map in the finally clause Long key = Thread.currentThread().getId(); - MonitoredThread monitoredThread = new MonitoredThread(Thread.currentThread(), exchange.getRequestURI() + exchange.getQueryString()); + MonitoredThread monitoredThread = new MonitoredThread(Thread.currentThread(), exchange.getRequestURI() + exchange.getDecodedQueryString()); activeThreads.put(key, monitoredThread); if(timerKey == null) { synchronized (this) {
core/src/main/java/io/undertow/server/handlers/URLDecodingHandler.java+1 −1 modified@@ -83,7 +83,7 @@ private static void decodePath(HttpServerExchange exchange, String charset, Stri } private static void decodeQueryString(HttpServerExchange exchange, String charset, StringBuilder sb) { - if (!exchange.getQueryString().isEmpty()) { + if (!exchange.getDecodedQueryString().isEmpty()) { final TreeMap<String, Deque<String>> newParams = new TreeMap<>(); for (Map.Entry<String, Deque<String>> param : exchange.getQueryParameters().entrySet()) { final Deque<String> newValues = new ArrayDeque<>(param.getValue().size());
core/src/main/java/io/undertow/server/HttpServerExchange.java+56 −18 modified@@ -207,13 +207,14 @@ public final class HttpServerExchange extends AbstractAttachable { private String resolvedPath = ""; /** - * the query string - percent encoded + * the unencoded query string (i.e. percent encoded), in its original form as it appears in the received request. */ private String queryString = ""; + /** - * the non-decoded query string. Set only when query string goes through decoding + * the decoded query string, if there was any decoding done */ - private String nonDecodedQueryString = null; + private String decodedQueryString = null; private int requestWrapperCount = 0; private ConduitWrapper<StreamSourceConduit>[] requestWrappers; //we don't allocate these by default, as for get requests they are not used @@ -580,24 +581,25 @@ public HttpServerExchange setResolvedPath(final String resolvedPath) { } /** + * Returns the query string for this request. * - * @return The query string, without the leading ? + * @return The query string as originally appeared in the request, without the leading ? */ public String getQueryString() { - return queryString; + return this.queryString; } /** - * Set query string. Leading {@code '?'} char will be removed automatically. + * Sets the query string, unencoded and in its original form as it appears in the received request. + * Leading {@code '?'} char will be removed automatically.<p> * + * @param queryString the query string as originally contained in the request, without any decoding * @return this http server exchange */ public HttpServerExchange setQueryString(final String queryString) { - // Clean leading ? - if( queryString.length() > 0 && queryString.charAt(0) == '?' ) { - this.queryString = queryString.substring(1); - } else { - this.queryString = queryString; + this.queryString = cleanQueryString(queryString); + if (this.queryString == null) { + this.queryString = ""; } return this; } @@ -607,27 +609,63 @@ public HttpServerExchange setQueryString(final String queryString) { * The returned string does not contain the leading {@code '?'} char. * * @return The request query string, without the leading {@code '?'}, non-decoded. + * + * @deprecated use {@link #getQueryString()} instead */ + @Deprecated(forRemoval = true, since="2.3.20.Final") public String getNonDecodedQueryString() { - return this.nonDecodedQueryString == null? this.queryString: this.nonDecodedQueryString; + return getQueryString(); } /** * Sets the non-decoded query string. Leading {@code '?'} char will be removed automatically.<p> * Must be invoked only if the {@link #getQueryString() query string} has gone through decoding. In such case, we expect * that both forms of the query string will be set in the exchange: {@link #setQueryString decoded} and non-decoded. * - * @param nonDecodedQueryString the query string as originally contained in the request, without any decoding + * @param unencodedQueryString the query string as originally contained in the request, without any decoding * @return this http server exchange + * + * @deprecated Use #setQueryString instead */ - public HttpServerExchange setNonDecodedQueryString(String nonDecodedQueryString) { + @Deprecated(forRemoval = true, since="2.3.20.Final") + public HttpServerExchange setNonDecodedQueryString(String unencodedQueryString) { + return setQueryString(unencodedQueryString); + } + + /** + * Returns the query string in its decoded form if available, which will depend on configs such as + * {@link UndertowOptions#ALLOW_UNESCAPED_CHARACTERS_IN_URL}. + * If unavailable, the decoded query string is just the same as {@link #getQueryString} + * + * @return The request query string, without the leading {@code '?'}, post parsing, decoded. + */ + public String getDecodedQueryString() { + return this.decodedQueryString != null && this.decodedQueryString.length() > 0 ? this.decodedQueryString : this.queryString; + } + + /** + * Sets the decoded query string. + * Leading {@code '?'} char will be removed automatically.<p> + * Must be invoked only if the {@link #getQueryString() query string} has gone through decoding. In such case, we expect + * that both forms of the query string will be set in the exchange: decoded and {@link #setQueryString non-decoded} + * + * @param decodedQueryString the request query string, without the leading {@code '?'}, post parsing, decoded. + * @return this http server exchange + */ + public HttpServerExchange setDecodedQueryString(String decodedQueryString) { + this.decodedQueryString = cleanQueryString(decodedQueryString); + return this; + } + + private String cleanQueryString(String queryString) { // Clean leading ? - if( nonDecodedQueryString.length() > 0 && nonDecodedQueryString.charAt(0) == '?' ) { - this.nonDecodedQueryString = nonDecodedQueryString.substring(1); + if (queryString == null) { + return queryString; + } else if( queryString.length() > 0 && queryString.charAt(0) == '?' ) { + return queryString.substring(1); } else { - this.nonDecodedQueryString = nonDecodedQueryString; + return queryString; } - return this; } /**
core/src/main/java/io/undertow/server/protocol/ajp/AjpRequestParser.java+20 −17 modified@@ -432,7 +432,7 @@ public void parse(final ByteBuffer buf, final AjpRequestParseState state, final state.currentAttribute = result.value; state.currentIntegerPart = -1; } - String result; + String result, decodedResult = null; boolean decodingAlreadyDone = false; boolean decodeUnescapedCharacters = false; final StringHolder resultHolder; @@ -451,68 +451,71 @@ public void parse(final ByteBuffer buf, final AjpRequestParseState state, final return; } decodeUnescapedCharacters = resultHolder.containsUrlCharacters && allowUnescapedCharactersInUrl; + result = resultHolder.value; if(resultHolder.containsUnencodedCharacters || decodeUnescapedCharacters) { try { - result = decode(resultHolder.value, true); + decodedResult = decode(resultHolder.value, true); } catch (UrlDecodeException | UnsupportedEncodingException e) { UndertowLogger.REQUEST_IO_LOGGER.failedToParseRequest(e); state.badRequest = true; result = resultHolder.value; } decodingAlreadyDone = true; - } else { - result = resultHolder.value; } } + final String finalResult = decodedResult != null ? decodedResult : result; //query string. if (state.currentAttribute.equals(QUERY_STRING)) { String resultAsQueryString = result == null ? "" : result; exchange.setQueryString(resultAsQueryString); + exchange.setDecodedQueryString(decodedResult); try { if (decodeUnescapedCharacters) { // decoding needs to be done again here, to preserve the parameters and form decoding, even if it has been done at resultAsQueryString // for more info see UNDERTOW-2312 and UNDERTOW-2555 URLUtils.parseQueryString(resultHolder == null || resultHolder.value == null ? "" : resultHolder.value, exchange, encoding, doDecode, maxParameters); + } else if (decodingAlreadyDone) { + URLUtils.parseQueryString(decodedResult, exchange, encoding, + false, maxParameters); } else { - URLUtils.parseQueryString(resultAsQueryString, exchange, encoding, - doDecode && !decodingAlreadyDone, maxParameters); + URLUtils.parseQueryString(resultAsQueryString, exchange, encoding, doDecode, maxParameters); } } catch (ParameterLimitException | IllegalArgumentException e) { UndertowLogger.REQUEST_IO_LOGGER.failedToParseRequest(e); state.badRequest = true; } } else if (state.currentAttribute.equals(REMOTE_USER)) { - exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_PRINCIPAL, result); - exchange.putAttachment(HttpServerExchange.REMOTE_USER, result); + exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_PRINCIPAL, finalResult); + exchange.putAttachment(HttpServerExchange.REMOTE_USER, finalResult); } else if (state.currentAttribute.equals(AUTH_TYPE)) { - exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_AUTHENTICATION_TYPE, result); + exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_AUTHENTICATION_TYPE, finalResult); } else if (state.currentAttribute.equals(STORED_METHOD)) { - HttpString requestMethod = new HttpString(result); + HttpString requestMethod = new HttpString(finalResult); Connectors.verifyToken(requestMethod); exchange.setRequestMethod(requestMethod); } else if (state.currentAttribute.equals(AJP_REMOTE_PORT)) { - state.remotePort = Integer.parseInt(result); + state.remotePort = Integer.parseInt(finalResult); } else if (state.currentAttribute.equals(SSL_SESSION)) { - state.sslSessionId = result; + state.sslSessionId = finalResult; } else if (state.currentAttribute.equals(SSL_CIPHER)) { - state.sslCipher = result; + state.sslCipher = finalResult; } else if (state.currentAttribute.equals(SSL_CERT)) { - state.sslCert = result; + state.sslCert = finalResult; } else if (state.currentAttribute.equals(SSL_KEY_SIZE)) { - state.sslKeySize = result; + state.sslKeySize = finalResult; } else { // other attributes if (state.attributes == null) { state.attributes = new TreeMap<>(); } if (ATTR_SET.contains(state.currentAttribute)) { // known attirubtes - state.attributes.put(state.currentAttribute, result); + state.attributes.put(state.currentAttribute, finalResult); } else if (allowedRequestAttributesPattern != null) { // custom allowed attributes Matcher m = allowedRequestAttributesPattern.matcher(state.currentAttribute); if (m.matches()) { - state.attributes.put(state.currentAttribute, result); + state.attributes.put(state.currentAttribute, finalResult); } } }
core/src/main/java/io/undertow/server/protocol/http2/Http2ReceiveListener.java+1 −0 modified@@ -243,6 +243,7 @@ void handleInitialRequest(HttpServerExchange initial, Http2Channel channel, byte exchange.setRequestScheme(initial.getRequestScheme()); exchange.setRequestMethod(initial.getRequestMethod()); exchange.setQueryString(initial.getQueryString()); + exchange.setDecodedQueryString(initial.getDecodedQueryString()); for (Map.Entry<String, Deque<String>> pathParamEntry: initial.getPathParameters().entrySet()) { for (String pathParamValue : pathParamEntry.getValue()) { exchange.addPathParam(pathParamEntry.getKey(), pathParamValue);
core/src/main/java/io/undertow/server/protocol/http/HttpReadListener.java+8 −16 modified@@ -27,12 +27,11 @@ import io.undertow.server.ConnectorStatisticsImpl; import io.undertow.server.Connectors; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.HostHeaderHandler; import io.undertow.server.protocol.ParseTimeoutUpdater; import io.undertow.server.protocol.http2.Http2ReceiveListener; import io.undertow.util.ClosingChannelExceptionHandler; import io.undertow.util.ConnectionUtils; -import io.undertow.util.HeaderValues; -import io.undertow.util.Headers; import io.undertow.util.HttpString; import io.undertow.util.Methods; import io.undertow.util.Protocols; @@ -78,7 +77,6 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann private final long maxEntitySize; private final boolean recordRequestStartTime; private final boolean allowUnknownProtocols; - private final boolean requireHostHeader; //0 = new request ok, reads resumed //1 = request running, new request not ok @@ -98,7 +96,6 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann this.maxRequestSize = connection.getUndertowOptions().get(UndertowOptions.MAX_HEADER_SIZE, UndertowOptions.DEFAULT_MAX_HEADER_SIZE); this.maxEntitySize = connection.getUndertowOptions().get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); this.recordRequestStartTime = connection.getUndertowOptions().get(UndertowOptions.RECORD_REQUEST_START_TIME, false); - this.requireHostHeader = connection.getUndertowOptions().get(UndertowOptions.REQUIRE_HOST_HTTP11, true); this.allowUnknownProtocols = connection.getUndertowOptions().get(UndertowOptions.ALLOW_UNKNOWN_PROTOCOLS, false); int requestParseTimeout = connection.getUndertowOptions().get(UndertowOptions.REQUEST_PARSE_TIMEOUT, -1); int requestIdleTimeout = connection.getUndertowOptions().get(UndertowOptions.NO_REQUEST_TIMEOUT, -1); @@ -109,6 +106,10 @@ final class HttpReadListener implements ChannelListener<ConduitStreamSourceChann connection.addCloseListener(parseTimeoutUpdater); } state = new ParseState(connection.getUndertowOptions().get(UndertowOptions.HTTP_HEADERS_CACHE_SIZE, UndertowOptions.DEFAULT_HTTP_HEADERS_CACHE_SIZE)); + + if (connection.getUndertowOptions().contains(UndertowOptions.REQUIRE_HOST_HTTP11)) { + UndertowLogger.ROOT_LOGGER.configurationNotSupported("REQUIRE_HOST_HTTP11"); + } } public void newRequest() { @@ -247,22 +248,13 @@ public void handleEventWithNoRunningRequest(final ConduitStreamSourceChannel cha channel.suspendReads(); } - HeaderValues host = httpServerExchange.getRequestHeaders().get(Headers.HOST); - if(host != null && host.size() > 1) { - sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.moreThanOneHostHeader()); - return; - } - if(requireHostHeader && httpServerExchange.getProtocol().equals(Protocols.HTTP_1_1)) { - if(host == null || host.size() ==0 || host.getFirst().isEmpty()) { - sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.noHostInHttp11Request()); - return; - } - } if(!Connectors.areRequestHeadersValid(httpServerExchange.getRequestHeaders())) { sendBadRequestAndClose(connection.getChannel(), UndertowMessages.MESSAGES.invalidHeaders()); return; } - Connectors.executeRootHandler(connection.getRootHandler(), httpServerExchange); + + //TODO: make this configurable/dynamic, to either allow users provide their own here or in chain + Connectors.executeRootHandler(HostHeaderHandler.WRAPPER.wrap(connection.getRootHandler()), httpServerExchange); } catch (Throwable t) { sendBadRequestAndClose(connection.getChannel(), t); return;
core/src/main/java/io/undertow/server/protocol/http/HttpRequestParser.java+2 −2 modified@@ -568,11 +568,11 @@ final void handleQueryParameters(ByteBuffer buffer, ParseState state, HttpServer } if (next == ' ' || next == '\t') { String queryString = stringBuilder.toString(); + exchange.setQueryString(queryString); if(urlDecodeRequired && this.allowUnescapedCharactersInUrl) { - exchange.setNonDecodedQueryString(queryString); queryString = decode(queryString, urlDecodeRequired, state, slashDecodingFlag, false); + exchange.setDecodedQueryString(queryString); } - exchange.setQueryString(queryString); if (nextQueryParam == null) { if (queryParamPos != stringBuilder.length()) { exchange.addQueryParam(decode(stringBuilder.substring(queryParamPos), nextQueryParamDecodeRequired, state, true, true), "");
core/src/main/java/io/undertow/server/RequestStatistics.java+1 −1 modified@@ -48,7 +48,7 @@ public long getProcessingTime() { } public String getQueryString() { - return exchange.getQueryString(); + return exchange.getDecodedQueryString(); } public String getUri() {
core/src/main/java/io/undertow/UndertowLogger.java+4 −0 modified@@ -488,4 +488,8 @@ void nodeConfigCreated(URI connectionURI, String balancer, String domain, String @LogMessage(level = WARN) @Message(id = 5107, value = "Failed to set web socket timeout.") void failedToSetWSTimeout(@Cause Exception e); + + @LogMessage(level = WARN) + @Message(id = 5108, value = "Configuration option is no longer supported: %s.") + void configurationNotSupported(String string); } \ No newline at end of file
core/src/main/java/io/undertow/UndertowOptions.java+7 −2 modified@@ -51,9 +51,14 @@ public class UndertowOptions { public static final Option<Long> MULTIPART_MAX_ENTITY_SIZE = Option.simple(UndertowOptions.class, "MULTIPART_MAX_ENTITY_SIZE", Long.class); /** - * We do not have a default upload limit + * Default maximum upload size 2MB */ - public static final long DEFAULT_MAX_ENTITY_SIZE = -1; + public static final long DEFAULT_MAX_ENTITY_SIZE = 2097152; + + /** + * Default maximum multipart upload size 2MB + */ + public static final long DEFAULT_MULTIPART_MAX_ENTITY_SIZE = 2097152; /** * If we should buffer pipelined requests. Defaults to false.
core/src/main/java/io/undertow/util/NetworkUtils.java+31 −14 modified@@ -33,29 +33,46 @@ */ public class NetworkUtils { - public static final String IP4_EXACT = "(?:\\d{1,3}\\.){3}\\d{1,3}"; + /** + * IPv4Segment: (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]) + */ + public static final String IP4_SEGMENT = "(25[0-5]|(?:2[0-4]|(?:(1{0,1}[0-9]))){0,1}(?:([0-9])))"; + /** + * ?: - unnamed groups are used for performance reasons. + * IPv4Segment: 25[0-5]|((2[0-4]|1){0,1}[0-9]){0,1}[0-9]) -extra () is not needed but makes it clear for OR + * IPv4Address: (IPv4Segment\.){3,3}IPv4Segment + */ + public static final String IP4_EXACT = "(?:"+IP4_SEGMENT+"\\.){3,3}(?:"+IP4_SEGMENT+")"; + /** + * IPv6Segment: ([0-9a-fA-F]{1,4}) - 1 to 4 hex digits + */ + public static final String IP6_SEGMENT = "([0-9a-fA-F]{1,4})"; /** * IPV6 match. ?: - unnamed groups are used for performance reasons. * Requirements: * - match full or partial IPV6 ( sliding '::') * - match end to start - ^$ to ensure it does not match part of some random (\d:){n,m} * - IPv4-Embedded IPv6 Address + * - TODO: special types of addresses ? + * Explanation: + * IPv6Segment: ([0-9a-fA-F]{1,4}) - 1 to 4 hex digits + * IPv6Address: (IPv6Segment:){1,n}IPv6Segment or (IPv6Segment:){1,x}(:IPv6Segment){1,y} where x+y<8 + * ^ shift of segments ~left to right with qualifiers + * Above is just general form for pure IPv6 without any spices * - * NO: - * - IPv4 mapped/translated into IPv6 - * - * ^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4} - full address - * |(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:)) - last compressed - * |(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4}) - second to last - * |(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2} - etc - * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3} - * |(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4} - * |(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5} - * |(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6} - * |(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))))$ - all the way compressed + * ^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4} - 1:2:3:4:5:6:7:8 + * |(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:)) - 1:: 1:2:3:4:5:6:7:: + * |(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4}) - 1::8 1:2:3:4:5:6::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2} - 1::7:8 1:2:3:4:5::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3} - 1::6:7:8 1:2:3:4::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4} - 1::5:6:7:8 1:2:3::8 + * |(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5} - 1::4:5:6:7:8 1:2::8 + * |(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6} - 1::3:4:5:6:7:8 1::8 + * |(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))) - ::2:3:4:5:6:7:8 ::8 :: + * |(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:IPv4Address)))$ - 1.2.3.4::192.168.1.1 1::192.168.1.1 */ - public static final String IP6_EXACT = "^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:))|(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4})|(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2}|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3}|(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5}|(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6}|(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:)))))$"; + public static final String IP6_EXACT = "^(?:([0-9a-fA-F]{1,4}:){7,7}(?:[0-9a-fA-F]){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,7}(?:(:))|(?:([0-9a-fA-F]{1,4}:)){1,6}(?:(:[0-9a-fA-F]){1,4})|(?:([0-9a-fA-F]{1,4}:)){1,5}(?:(:[0-9a-fA-F]{1,4})){1,2}|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:[0-9a-fA-F]{1,4})){1,3}|(?:([0-9a-fA-F]{1,4}:)){1,3}(?:(:[0-9a-fA-F]{1,4})){1,4}|(?:([0-9a-fA-F]{1,4}:)){1,2}(?:(:[0-9a-fA-F]{1,4})){1,5}|(?:([0-9a-fA-F]{1,4}:))(?:(:[0-9a-fA-F]{1,4})){1,6}|(?:(:))(?:((:[0-9a-fA-F]{1,4}){1,7}|(?:(:))))|(?:([0-9a-fA-F]{1,4}:)){1,4}(?:(:"+IP4_EXACT+")))$"; public static String formatPossibleIpv6Address(String address) { if (address == null) {
core/src/main/java/io/undertow/util/WeakCopyOnWriteMap.java+170 −0 added@@ -0,0 +1,170 @@ +/* + * 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.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.WeakHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +/** + * A basic copy on write map. It simply delegates to an underlying map, that is swapped out + * every time the map is updated. + * + * Note: this is not a secure map. It should not be used in situations where the map is populated + * from user input. + * + * @author Stuart Douglas + */ +public class WeakCopyOnWriteMap<K,V> implements ConcurrentMap<K, V> { + + private volatile Map<K, V> delegate = Collections.emptyMap(); + + public WeakCopyOnWriteMap() { + } + + public WeakCopyOnWriteMap(Map<K, V> existing) { + this.delegate = new WeakHashMap<>(existing); + } + + @Override + public synchronized V putIfAbsent(K key, V value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing != null) { + return existing; + } + putInternal(key, value); + return null; + } + + @Override + public synchronized boolean remove(Object key, Object value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing.equals(value)) { + removeInternal(key); + return true; + } + return false; + } + + @Override + public synchronized boolean replace(K key, V oldValue, V newValue) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing.equals(oldValue)) { + putInternal(key, newValue); + return true; + } + return false; + } + + @Override + public synchronized V replace(K key, V value) { + final Map<K, V> delegate = this.delegate; + V existing = delegate.get(key); + if(existing != null) { + putInternal(key, value); + return existing; + } + return null; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public V get(Object key) { + return delegate.get(key); + } + + @Override + public synchronized V put(K key, V value) { + return putInternal(key, value); + } + + @Override + public synchronized V remove(Object key) { + return removeInternal(key); + } + + @Override + public synchronized void putAll(Map<? extends K, ? extends V> m) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + for(Entry<? extends K, ? extends V> e : m.entrySet()) { + delegate.put(e.getKey(), e.getValue()); + } + this.delegate = delegate; + } + + @Override + public synchronized void clear() { + delegate = Collections.emptyMap(); + } + + @Override + public Set<K> keySet() { + return delegate.keySet(); + } + + @Override + public Collection<V> values() { + return delegate.values(); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return delegate.entrySet(); + } + + //must be called under lock + private V putInternal(final K key, final V value) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + V existing = delegate.put(key, value); + this.delegate = delegate; + return existing; + } + + public V removeInternal(final Object key) { + final Map<K, V> delegate = new WeakHashMap<>(this.delegate); + V existing = delegate.remove(key); + this.delegate = delegate; + return existing; + } +}
core/src/main/java/io/undertow/websockets/core/WebSocketChannel.java+3 −3 modified@@ -214,7 +214,7 @@ protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException { } catch (WebSocketException e) { //the data was corrupt //send a close message - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); markReadsBroken(e); if (WebSocketLogger.REQUEST_LOGGER.isDebugEnabled()) { WebSocketLogger.REQUEST_LOGGER.debugf(e, "receive failed due to Exception"); @@ -278,10 +278,10 @@ protected void handleBrokenSourceChannel(Throwable e) { getFramePriority().immediateCloseFrame(); WebSockets.sendClose(new CloseMessage(CloseMessage.MSG_CONTAINS_INVALID_DATA, e.getMessage()).toByteBuffer(), this, null); } else if (e instanceof WebSocketInvalidCloseCodeException) { - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); } else if (e instanceof WebSocketFrameCorruptedException) { getFramePriority().immediateCloseFrame(); - WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + WebSockets.sendClose(new CloseMessage(CloseMessage.PROTOCOL_ERROR, e.getMessage()).toByteBuffer(), this, null); } }
core/src/test/java/io/undertow/server/ConnectionTerminationTestCase.java+22 −8 modified@@ -18,21 +18,24 @@ package io.undertow.server; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpOneOnly; +import io.undertow.testutils.ProxyIgnore; +import io.undertow.util.FileUtils; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xnio.IoUtils; +import org.xnio.OptionMap; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.xnio.IoUtils; -import io.undertow.testutils.DefaultServer; -import io.undertow.testutils.HttpOneOnly; -import io.undertow.testutils.ProxyIgnore; -import io.undertow.util.FileUtils; +import static io.undertow.UndertowOptions.MAX_ENTITY_SIZE; /** * Tests abnormal connection termination @@ -96,4 +99,15 @@ public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener IoUtils.safeClose(socket); } } + + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + }
core/src/test/java/io/undertow/server/ExactLengthReadTimeoutTestCase.java+143 −0 added@@ -0,0 +1,143 @@ +/* + * 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 java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import io.undertow.server.handlers.BlockingHandler; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpOneOnly; +import io.undertow.testutils.TestHttpClient; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; +import org.apache.http.HttpResponse; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xnio.OptionMap; +import org.xnio.Options; + +/** + * + * Tests to ensure no read timeout after an exact read of the content-length + * + * @author Stuart Douglas + * @author Flavia Rainone + * @author Aaron Ogburn + */ +@RunWith(DefaultServer.class) +@HttpOneOnly +public class ExactLengthReadTimeoutTestCase { + + private static volatile String message; + + private static final String DATA = "1234567890ABCDEF"; + + private static final int DATA_MULTIPLE = 2048; + + @BeforeClass + public static void setup() { + final BlockingHandler blockingHandler = new BlockingHandler(); + DefaultServer.setRootHandler(blockingHandler); + blockingHandler.setRootHandler(new HttpHandler() { + @Override + public void handleRequest(final HttpServerExchange exchange) { + try { + final OutputStream outputStream = exchange.getOutputStream(); + final InputStream inputStream = exchange.getInputStream(); + + long length = exchange.getRequestContentLength(); + byte[] b = new byte[DATA_MULTIPLE * DATA.length()]; + int i = 1; + StringBuilder builder = new StringBuilder(); + // read exact content length + while (i > 0 && length > 0) { + i = inputStream.read(b); + if (i > 0) { + length -=i; + builder.append(new String(b, 0, i)); + } + } + + // this shouldn't cause timeout after complete read + try { + Thread.sleep(200); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + Assert.assertEquals(message, builder.toString()); + inputStream.close(); + outputStream.close(); + } catch (IOException e) { + exchange.getResponseHeaders().put(Headers.CONNECTION, "close"); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); + throw new RuntimeException(e); + } + } + }); + } + + @DefaultServer.BeforeServerStarts + public static void beforeClass() { + DefaultServer.setServerOptions(OptionMap.create(Options.READ_TIMEOUT, 100)); + } + + @DefaultServer.AfterServerStops + public static void afterClass() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + + @Test + public void testExactLengthReadTimeout() throws InterruptedException, IOException { + StringBuilder builder = new StringBuilder(1000 * DATA.length()); + + for (int i = 0; i < DATA_MULTIPLE; ++i) { + try { + builder.append(DATA); + } catch (Throwable e) { + throw new RuntimeException("test failed with i equal to " + i, e); + } + } + + message = builder.toString(); + final TestHttpClient client = new TestHttpClient(); + try { + HttpPost post = new HttpPost(DefaultServer.getDefaultServerURL() + "/path"); + post.setEntity(new StringEntity(message)); + post.addHeader(Headers.CONNECTION_STRING, "close"); + boolean socketFailure = false; + try { + // Request should succeed. + HttpResponse result = client.execute(post); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + } catch (NoHttpResponseException e) { + Assert.fail("No response was received, this was presumably caused by read-timeout closing the connection."); + } + } finally { + client.getConnectionManager().shutdown(); + } + } +}
core/src/test/java/io/undertow/server/handlers/blocking/SimpleBlockingServerTestCase.java+13 −0 modified@@ -23,6 +23,7 @@ import java.io.InputStream; import java.io.OutputStream; +import io.undertow.UndertowOptions; import io.undertow.io.IoCallback; import io.undertow.io.Sender; import io.undertow.server.HttpHandler; @@ -44,6 +45,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; /** * @author Stuart Douglas @@ -57,6 +59,7 @@ public class SimpleBlockingServerTestCase { public static void setup() { final BlockingHandler blockingHandler = new BlockingHandler(); DefaultServer.setRootHandler(blockingHandler); + blockingHandler.setRootHandler(new HttpHandler() { @Override public void handleRequest(final HttpServerExchange exchange) { @@ -112,6 +115,16 @@ public void onException(final HttpServerExchange exchange, final Sender sender, }); } + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(UndertowOptions.MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + @Test public void sendHttpRequest() throws IOException { message = "My HTTP Request!";
core/src/test/java/io/undertow/server/handlers/ForwardedHandlerTestCase.java+17 −0 modified@@ -1,3 +1,20 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; import io.undertow.server.HttpHandler;
core/src/test/java/io/undertow/server/handlers/HostHandlerTestCase.java+282 −0 added@@ -0,0 +1,282 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 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.handlers; + +import java.io.IOException; + +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.conn.params.ConnRoutePNames; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.ProxyIgnore; +import io.undertow.testutils.TestHttpClient; +import io.undertow.util.Headers; + +@RunWith(DefaultServer.class) +@ProxyIgnore +public class HostHandlerTestCase { + + @BeforeClass + public static void setup() { + DefaultServer.setRootHandler(Handlers.hostHeaderHandler(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseSender().send("OK"); + } + })); + } + + @Test + @Ignore // ignore, since client will add one if there no present + public void testNoHostHeader() throws Exception { + test(new String[] {}, null, 400, null); + } + + @Test + public void testTooManyHostHeader() throws Exception { + test(new String[] { "212.138.1.1", "data.com" }, null, 400, null); + } + + @Test + public void testIPv4HostHeader() throws Exception { + // dont test bad IPv4, as this will be valid... reg-name + test(new String[] { "212.138.1.1" }, null, 200, null); + } + + @Test + public void testIPv4AndPortHostHeader() throws Exception { + test(new String[] { "212.138.1.1:80" }, null, 200, null); + } + + @Test + public void testIPv6HostHeader() throws Exception { + test(new String[] { "[1:2:3:4::]" }, null, 200, null); + } + + @Test + public void testIPv6AndPortHostHeader() throws Exception { + test(new String[] { "[1:2:3:4::]:80" }, null, 200, null); + } + + @Test + public void testIPv6HostHeader2() throws Exception { + test(new String[] { "[1:2:3:4:::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader2() throws Exception { + test(new String[] { "[1:2:3:4::]:80000" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_PORT); + } + + @Test + public void testIPv6HostHeader3() throws Exception { + test(new String[] { "[1:2:3:4::" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader3() throws Exception { + test(new String[] { "[1:2:3:4:::80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6HostHeader4() throws Exception { + test(new String[] { "1:2:3:4::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6HostHeader5() throws Exception { + test(new String[] { "1:2:3:4::" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_PORT); + } + + @Test + public void testIPv6HostHeader6() throws Exception { + //this will just fall into reg-name + test(new String[] { "1:2:3:4:5:6:7:8" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testIPv6AndPortHostHeader4() throws Exception { + test(new String[] { "1:2:3:4::]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6AndPortHostHeader6() throws Exception { + test(new String[] { "1:2:3:4:5:6:7:8]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader() throws Exception { + test(new String[] { "[1:2:3:4::192.168.32.1]" }, null, 200, null); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader2() throws Exception { + test(new String[] { "[1:2:3:4::192.168.32.1]:80" }, null, 200, null); + } + + @Test + public void testIPv6EmbeddedIPv4HostHeader3() throws Exception { + test(new String[] { "[1:2:3:4::192.355.32.1]:80" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader() throws Exception { + test(new String[] { "[vAF.1:2:3:4::]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader2() throws Exception { + test(new String[] { "[vG.1:2:3:4::]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testIPvFutureHostHeader3() throws Exception { + test(new String[] { "[vAF.abdc:abcd_-~AF]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader4() throws Exception { + test(new String[] { "[vAF.ImVal1.com-._~!$&'()*+,;=:]" }, null, 200, null); + } + + @Test + public void testIPvFutureHostHeader5() throws Exception { + test(new String[] { "[vAF.]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader6() throws Exception { + test(new String[] { "[vAF]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPvFutureHostHeader7() throws Exception { + test(new String[] { "[v.abcd]" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPRegNameHostHeader() throws Exception { + test(new String[] { "366.66.12.12" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader2() throws Exception { + test(new String[] { "domain.com%20" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader3() throws Exception { + test(new String[] { "domain.com%2" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL); + } + + @Test + public void testIPRegNameHostHeader4() throws Exception { + test(new String[] { "doma&n.com%20" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader5() throws Exception { + test(new String[] { "ImVal1.com-._~!$&'()*+,;=" }, null, 200, null); + } + + @Test + public void testIPRegNameHostHeader6() throws Exception { + // test userinfo presence? + test(new String[] { "juicyUserInfo@ImVal1.com-._~!$&'()*+,;=" }, null, 400, HostHeaderHandler.STATUS_MALFORMED_IP_LITERAL_BAD_CHARS); + } + + @Test + public void testAbsoluteURLBad() throws Exception { + // test userinfo presence? + test(new String[] { "wrong.com:8080" }, new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 400, HostHeaderHandler.STATUS_HOST_NO_MATCH); + } + + @Test + public void testAbsoluteURLGood() throws Exception { + // test userinfo presence? + test(new String[] { DefaultServer.getHostAddress() + ":" + DefaultServer.getHostPort() }, + new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 200, null); + } + + @Test + public void testEmptyHost() throws Exception { + // test userinfo presence? + test(new String[] { "" }, + null, 200, null); + } + + @Test + public void testEmptyHost2() throws Exception { + // test userinfo presence? + test(new String[] { "" }, + new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()), 400, HostHeaderHandler.STATUS_HOST_NO_MATCH); + } + + public void test(final String[] headers, final HttpHost proxy, final int resultCode, final String statusMessage) + throws ClientProtocolException, IOException { + TestHttpClient client = new TestHttpClient(); + if (proxy != null) { + client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + } + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL()); + for (String i : headers) { + get.addHeader(Headers.HOST_STRING, i); + } + HttpResponse result = client.execute(get, HttpClientContext.create()); + Assert.assertEquals(result.getStatusLine().getReasonPhrase(), resultCode, result.getStatusLine().getStatusCode()); + if (statusMessage != null) { + Assert.assertEquals(statusMessage, result.getStatusLine().getReasonPhrase()); + } + + } finally { + client.getConnectionManager().shutdown(); + } + } + + public void testProxyMode(final String[] headers, final int resultCode, final String statusMessage) throws ClientProtocolException, IOException { + // this has to be done this way in order to trick apache to use absolute form... + HttpHost proxy = new HttpHost(DefaultServer.getHostAddress(), DefaultServer.getHostPort()); + TestHttpClient client = new TestHttpClient(); + client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL()); + for (String i : headers) { + get.addHeader(Headers.HOST_STRING, i); + } + HttpResponse result = client.execute(get, HttpClientContext.create()); + Assert.assertEquals(result.getStatusLine().getReasonPhrase(), resultCode, result.getStatusLine().getStatusCode()); + + } finally { + client.getConnectionManager().shutdown(); + } + } + +}
core/src/test/java/io/undertow/server/handlers/QueryParametersWithAllowUnescapedCharactersTestCase.java+11 −6 modified@@ -59,16 +59,21 @@ public static void clearProxyOptions() { public static void setQueryStringsArray() { // format is: {queryString, expected result} queryStrings = new String[][] { new String[] { "/path?unicode=Iñtërnâtiônàližætiøn", - "unicode=Iñtërnâtiônàližætiøn{unicode=>Iñtërnâtiônàližætiøn}" }, - new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb bb{a=>b,value=>bb bb}" }, + //"unicode=Iñtërnâtiônàližætiøn{unicode=>Iñtërnâtiônàližætiøn}" }, + "unicode=I%C3%B1t%C3%ABrn%C3%A2ti%C3%B4n%C3%A0li%C5%BE%C3%A6ti%C3%B8n{unicode=>Iñtërnâtiônàližætiøn}" }, + //new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb bb{a=>b,value=>bb bb}" }, + new String[] { "/path?a=b&value=bb%20bb", "a=b&value=bb%20bb{a=>b,value=>bb bb}" }, new String[] { "/path?a=b&value=bb&value=cc", "a=b&value=bb&value=cc{a=>b,value=>[bb,cc]}" }, new String[] { "/path?&a=b&value=bb&&value=cc", "&a=b&value=bb&&value=cc{a=>b,value=>[bb,cc]}" }, - // Specifing some query parameters with empty by intentional for the test purpose. These should be ignored. + // Specifying some query parameters with empty by intentional for the test purpose. These should be ignored. new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20", - "a=b&value=bb&value=cc&s &t {a=>b,s =>,t =>,value=>[bb,cc]}" }, + //"a=b&value=bb&value=cc&s &t {a=>b,s =>,t =>,value=>[bb,cc]}" }, + "a=b&value=bb&value=cc&s%20&t%20{a=>b,s =>,t =>,value=>[bb,cc]}" }, new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20&", - "a=b&value=bb&value=cc&s &t &{a=>b,s =>,t =>,value=>[bb,cc]}" }, + //"a=b&value=bb&value=cc&s &t &{a=>b,s =>,t =>,value=>[bb,cc]}" }, + "a=b&value=bb&value=cc&s%20&t%20&{a=>b,s =>,t =>,value=>[bb,cc]}" }, new String[] { "/path?a=b&value=bb&value=cc&s%20&t%20&u", - "a=b&value=bb&value=cc&s &t &u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; + //"a=b&value=bb&value=cc&s &t &u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; + "a=b&value=bb&value=cc&s%20&t%20&u{a=>b,s =>,t =>,u=>,value=>[bb,cc]}" } }; } }
core/src/test/java/io/undertow/server/handlers/ReceiverTestCase.java+14 −0 modified@@ -35,6 +35,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; import java.io.IOException; import java.io.OutputStream; @@ -44,6 +45,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import static io.undertow.UndertowOptions.MAX_ENTITY_SIZE; + /** * @author Stuart Douglas */ @@ -61,6 +64,17 @@ public void error(HttpServerExchange exchange, IOException e) { } }; + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + + @BeforeClass public static void setup() { HttpHandler testFullString = new HttpHandler() {
core/src/test/java/io/undertow/server/protocol/ajp/AjpParsingUnitTestCase.java+2 −1 modified@@ -126,7 +126,8 @@ public void testCharsetHandling() throws Exception { Assert.assertFalse(state.badRequest); Assert.assertEquals("/한글이름", result.getRequestPath()); Assert.assertEquals("/한글이름", result.getRequestURI()); - Assert.assertEquals("param=한글이름", result.getQueryString()); + Assert.assertEquals("param=한글이름", result.getDecodedQueryString()); + Assert.assertEquals("param=í\u0095\u009Cê¸\u0080ì\u009D´ë¦\u0084", result.getQueryString()); } @Test
.github/workflows/ci.yml+1 −1 modified@@ -59,7 +59,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - module: [core] + module: [core, servlet, websockets-jsr] jdk: [11, 17, 21] openjdk_impl: [ temurin ] openssl: [false, true]
servlet/src/main/java/io/undertow/servlet/attribute/ServletRelativePathAttribute.java+2 −3 modified@@ -22,7 +22,6 @@ import io.undertow.attribute.ExchangeAttributeBuilder; import io.undertow.attribute.ReadOnlyAttributeException; import io.undertow.attribute.RelativePathAttribute; -import io.undertow.attribute.RequestURLAttribute; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.handlers.ServletRequestContext; @@ -48,12 +47,12 @@ private ServletRelativePathAttribute() { public String readAttribute(final HttpServerExchange exchange) { ServletRequestContext src = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); if(src == null) { - return RequestURLAttribute.INSTANCE.readAttribute(exchange); + return RelativePathAttribute.INSTANCE.readAttribute(exchange); } String path = (String) src.getServletRequest().getAttribute(RequestDispatcher.FORWARD_PATH_INFO); String sp = (String) src.getServletRequest().getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH); if(path == null && sp == null) { - return RequestURLAttribute.INSTANCE.readAttribute(exchange); + return RelativePathAttribute.INSTANCE.readAttribute(exchange); } if(sp == null) { return path;
servlet/src/main/java/io/undertow/servlet/attribute/ServletRequestLineAttribute.java+2 −2 modified@@ -57,9 +57,9 @@ public String readAttribute(final HttpServerExchange exchange) { if (query != null && !query.isEmpty()) { sb.append('?'); sb.append(query); - } else if (!exchange.getQueryString().isEmpty()) { + } else if (!exchange.getDecodedQueryString().isEmpty()) { sb.append('?'); - sb.append(exchange.getQueryString()); + sb.append(exchange.getDecodedQueryString()); } sb.append(' ') .append(exchange.getProtocol().toString()).toString();
servlet/src/main/java/io/undertow/servlet/spec/HttpServletRequestImpl.java+1 −1 modified@@ -312,7 +312,7 @@ public String getContextPath() { @Override public String getQueryString() { - return exchange.getQueryString().isEmpty() ? null : exchange.getQueryString(); + return exchange.getDecodedQueryString().isEmpty() ? null : exchange.getDecodedQueryString(); } @Override
servlet/src/main/java/io/undertow/servlet/util/DispatchUtils.java+4 −4 modified@@ -230,18 +230,18 @@ private static String assignRequestPath(final String path, final HttpServletRequ if (include) { // include does not modify exchange paths, just add the query string and request uri // the rest of attributes are added via the match later - requestImpl.setAttribute(INCLUDE_QUERY_STRING, fake.getQueryString()); + requestImpl.setAttribute(INCLUDE_QUERY_STRING, fake.getDecodedQueryString()); requestImpl.setAttribute(INCLUDE_REQUEST_URI, fake.getRequestURI()); } else { exchange.setRelativePath(newRequestPath); exchange.setRequestPath(fake.getRequestPath()); exchange.setRequestURI(fake.getRequestURI()); - if (!fake.getQueryString().isEmpty()) { - exchange.setQueryString(fake.getQueryString()); + if (!fake.getDecodedQueryString().isEmpty()) { + exchange.setDecodedQueryString(fake.getDecodedQueryString()); } } // both forward and include merge parameters by spec - if (!fake.getQueryString().isEmpty()) { + if (!fake.getDecodedQueryString().isEmpty()) { final Map<String, Deque<String>> merged = QueryParameterUtils.mergeQueryParameters(fake.getQueryParameters(), exchange.getQueryParameters()); requestImpl.setQueryParameters(null); exchange.getQueryParameters().clear();
servlet/src/test/java/io/undertow/servlet/test/multipart/MultiPartTestCase.java+22 −10 modified@@ -18,23 +18,17 @@ package io.undertow.servlet.test.multipart; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; - import io.undertow.servlet.ServletExtension; import io.undertow.servlet.Servlets; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.LoggingExceptionHandler; import io.undertow.servlet.test.util.DeploymentUtils; import io.undertow.testutils.DefaultServer; import io.undertow.testutils.HttpClientUtils; -import io.undertow.testutils.ProxyIgnore; import io.undertow.testutils.TestHttpClient; import io.undertow.util.StatusCodes; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; @@ -44,18 +38,24 @@ import org.apache.http.entity.mime.content.StringBody; import org.jboss.logging.Logger; import org.junit.Assert; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static io.undertow.UndertowOptions.MULTIPART_MAX_ENTITY_SIZE; import static io.undertow.servlet.Servlets.multipartConfig; import static io.undertow.servlet.Servlets.servlet; /** * @author Stuart Douglas */ @RunWith(DefaultServer.class) -@ProxyIgnore public class MultiPartTestCase { @@ -82,6 +82,16 @@ public void handleDeployment(DeploymentInfo deploymentInfo, ServletContext servl .setMultipartConfig(multipartConfig(null, 3, 0, 0))); } + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(MULTIPART_MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + @Test public void testMultiPartRequestWithNoMultipartConfig() throws IOException { TestHttpClient client = new TestHttpClient(); @@ -180,7 +190,9 @@ public void testMultiPartRequestWithAddedServlet() throws IOException { } @Test - public void testMultiPartRequestToLarge() throws IOException { + public void testMultiPartRequestTooLarge() throws IOException { + // FIXME UNDERTOW-2572 + Assume.assumeFalse(DefaultServer.isH2()); TestHttpClient client = new TestHttpClient(); try { String uri = DefaultServer.getDefaultServerURL() + "/servletContext/2";
servlet/src/test/java/io/undertow/servlet/test/streams/ServletInputStreamEarlyCloseClientSideTestCase.java+16 −7 modified@@ -18,31 +18,31 @@ package io.undertow.servlet.test.streams; +import io.undertow.UndertowOptions; import io.undertow.servlet.api.ServletInfo; import io.undertow.servlet.test.util.DeploymentUtils; import io.undertow.testutils.DefaultServer; -import io.undertow.testutils.HttpOneOnly; import io.undertow.testutils.TestHttpClient; +import jakarta.servlet.ServletException; import org.junit.Assert; -import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; -import jakarta.servlet.ServletException; import java.io.OutputStream; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; /** * Tests the behaviour of the input stream when the connection is closed on the client side * <p> - * https://issues.jboss.org/browse/WFLY-4827 + * (see WFLY-4827) * * @author Stuart Douglas */ @RunWith(DefaultServer.class) -@HttpOneOnly public class ServletInputStreamEarlyCloseClientSideTestCase { public static final String SERVLET = "servlet"; @@ -54,9 +54,18 @@ public static void setup() throws ServletException { .addMapping("/" + SERVLET)); } + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(UndertowOptions.MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + @Test public void testServletInputStreamEarlyClose() throws Exception { - Assume.assumeFalse(DefaultServer.isH2()); TestHttpClient client = new TestHttpClient(); EarlyCloseClientServlet.reset(); try (Socket socket = new Socket()) { @@ -70,7 +79,7 @@ public void testServletInputStreamEarlyClose() throws Exception { String request = "POST /servletContext/" + SERVLET + " HTTP/1.1\r\nHost:localhost\r\nContent-Length:" + sb.length() + 100 + "\r\n\r\n" + sb.toString(); OutputStream outputStream = socket.getOutputStream(); - outputStream.write(request.getBytes("US-ASCII")); + outputStream.write(request.getBytes(StandardCharsets.US_ASCII)); outputStream.flush(); socket.close();
websockets-jsr/src/main/java/io/undertow/websockets/jsr/FrameHandler.java+4 −1 modified@@ -269,8 +269,9 @@ private void invokeTextHandler(final BufferedTextMessage data, final HandlerWrap @Override public void run() { MessageHandler mHandler = handler.getHandler(); + final ClassLoader oldCL = Thread.currentThread().getContextClassLoader(); try { - + Thread.currentThread().setContextClassLoader(mHandler.getClass().getClassLoader()); if (mHandler instanceof MessageHandler.Partial) { if (handler.decodingNeeded) { Object object = getSession().getEncoding().decodeText(handler.getMessageType(), message); @@ -292,6 +293,8 @@ public void run() { } } catch (Exception e) { invokeOnError(e); + } finally { + Thread.currentThread().setContextClassLoader(oldCL); } } });
websockets-jsr/src/main/java/io/undertow/websockets/jsr/ServerWebSocketContainer.java+2 −2 modified@@ -28,7 +28,7 @@ import io.undertow.servlet.util.ConstructorInstanceFactory; import io.undertow.servlet.util.ImmediateInstanceHandle; import io.undertow.servlet.websockets.ServletWebSocketHttpExchange; -import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.WeakCopyOnWriteMap; import io.undertow.util.PathTemplate; import io.undertow.util.StatusCodes; import io.undertow.websockets.WebSocketExtension; @@ -104,7 +104,7 @@ public class ServerWebSocketContainer implements ServerContainer, Closeable { private final ClassIntrospecter classIntrospecter; - private final Map<Class<?>, ConfiguredClientEndpoint> clientEndpoints = new CopyOnWriteMap<>(); + private final Map<Class<?>, ConfiguredClientEndpoint> clientEndpoints = new WeakCopyOnWriteMap<>(); private final List<ConfiguredServerEndpoint> configuredServerEndpoints = new ArrayList<>(); private final Set<Class<?>> annotatedEndpointClasses = new HashSet<>();
websockets-jsr/src/main/java/io/undertow/websockets/jsr/UndertowContainerProvider.java+28 −16 modified@@ -85,26 +85,38 @@ static ServerWebSocketContainer getDefaultContainer() { //this is not great, as we have no way to control the lifecycle //but there is not much we can do //todo: what options should we use here? - ByteBufferPool buffers = new DefaultByteBufferPool(directBuffers, 1024, 100, 12); - defaultContainer = new ServerWebSocketContainer(defaultIntrospector, UndertowContainerProvider.class.getClassLoader(), new Supplier<XnioWorker>() { - volatile XnioWorker worker; - - @Override - public XnioWorker get() { - if(worker == null) { - synchronized (this) { - if(worker == null) { - try { - worker = Xnio.getInstance().createWorker(OptionMap.create(Options.THREAD_DAEMON, true)); - } catch (IOException e) { - throw new RuntimeException(e); + //final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + try { + //Thread.currentThread().setContextClassLoader(null); + ByteBufferPool buffers = new DefaultByteBufferPool(directBuffers, 1024, 100, 12); + defaultContainer = new ServerWebSocketContainer(defaultIntrospector, UndertowContainerProvider.class.getClassLoader(), new Supplier<XnioWorker>() { + volatile XnioWorker worker; + + @Override + public XnioWorker get() { + final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(null); + if (worker == null) { + synchronized (this) { + if (worker == null) { + try { + worker = Xnio.getInstance().createWorker(OptionMap.create(Options.THREAD_DAEMON, true)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } + } finally { + Thread.currentThread().setContextClassLoader(tccl); } + return worker; } - return worker; - } - }, buffers, Collections.EMPTY_LIST, !invokeInIoThread); + }, buffers, Collections.EMPTY_LIST, !invokeInIoThread); + } finally { + //Thread.currentThread().setContextClassLoader(tccl); + } } return defaultContainer; }
6b7c18481ce6[UNDERTOW-2377] CVE-2024-3884 CVE-2024-4027 Define 2MB default upload/entity size
3 files changed · +21 −3
core/src/main/java/io/undertow/server/handlers/form/MultiPartParserDefinition.java+1 −1 modified@@ -109,7 +109,7 @@ public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener.proceed(); } }); - Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE); + Long sizeLimit = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MULTIPART_MAX_ENTITY_SIZE ); if(sizeLimit != null && sizeLimit > 0) { // do not overwrite the entity size with sizeLimit that is <= 0 exchange.setMaxEntitySize(sizeLimit); }
core/src/main/java/io/undertow/UndertowOptions.java+7 −2 modified@@ -51,9 +51,14 @@ public class UndertowOptions { public static final Option<Long> MULTIPART_MAX_ENTITY_SIZE = Option.simple(UndertowOptions.class, "MULTIPART_MAX_ENTITY_SIZE", Long.class); /** - * We do not have a default upload limit + * Default maximum upload size 2MB */ - public static final long DEFAULT_MAX_ENTITY_SIZE = -1; + public static final long DEFAULT_MAX_ENTITY_SIZE = 2097152; + + /** + * Default maximum multipart upload size 2MB + */ + public static final long DEFAULT_MULTIPART_MAX_ENTITY_SIZE = 2097152; /** * If we should buffer pipelined requests. Defaults to false.
core/src/test/java/io/undertow/server/handlers/blocking/SimpleBlockingServerTestCase.java+13 −0 modified@@ -23,6 +23,7 @@ import java.io.InputStream; import java.io.OutputStream; +import io.undertow.UndertowOptions; import io.undertow.io.IoCallback; import io.undertow.io.Sender; import io.undertow.server.HttpHandler; @@ -44,6 +45,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.xnio.OptionMap; /** * @author Stuart Douglas @@ -57,6 +59,7 @@ public class SimpleBlockingServerTestCase { public static void setup() { final BlockingHandler blockingHandler = new BlockingHandler(); DefaultServer.setRootHandler(blockingHandler); + blockingHandler.setRootHandler(new HttpHandler() { @Override public void handleRequest(final HttpServerExchange exchange) { @@ -112,6 +115,16 @@ public void onException(final HttpServerExchange exchange, final Sender sender, }); } + @DefaultServer.BeforeServerStarts + public static void setupServer() { + DefaultServer.setServerOptions(OptionMap.create(UndertowOptions.MAX_ENTITY_SIZE, -1L, UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, -1L)); + } + + @DefaultServer.AfterServerStops + public static void cleanup() { + DefaultServer.setServerOptions(OptionMap.EMPTY); + } + @Test public void sendHttpRequest() throws IOException { message = "My HTTP Request!";
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
15- github.com/advisories/GHSA-33hj-rcmx-86mvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-4027ghsaADVISORY
- access.redhat.com/security/cve/CVE-2024-4027nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/undertow-io/undertow/commit/6b7c18481ce65ae4012d92fe2b7f17a21ef4d70bghsaWEB
- github.com/undertow-io/undertow/commit/8318dd36fdc2c9842baf10c5f1bfbb3bc23f75e7ghsaWEB
- github.com/undertow-io/undertow/commit/cb854c779b9e2368c3c274ebd7217c8e75d505beghsaWEB
- github.com/undertow-io/undertow/commit/fb14baa51b611a4a9f755f1d8b07d6e12eac68e3ghsaWEB
- github.com/undertow-io/undertow/pull/1860ghsaWEB
- github.com/undertow-io/undertow/pull/1882ghsaWEB
- github.com/undertow-io/undertow/pull/1894ghsaWEB
- github.com/undertow-io/undertow/releases/tag/2.2.39.FinalghsaWEB
- github.com/undertow-io/undertow/releases/tag/2.3.21.FinalghsaWEB
- github.com/undertow-io/undertow/releases/tag/2.4.0.Beta1ghsaWEB
- issues.redhat.com/browse/UNDERTOW-2377ghsaWEB
News mentions
0No linked articles in our index yet.