diff --git a/LICENSE-binary b/LICENSE-binary index 0e9bc06209de3a..47d2bef368ae7c 100644 --- a/LICENSE-binary +++ b/LICENSE-binary @@ -411,20 +411,29 @@ org.apache.yetus:audience-annotations:0.5.0 org.apache.zookeeper:zookeeper:3.8.6 org.codehaus.jettison:jettison:1.5.4 org.conscrypt:conscrypt-openjdk-uber:2.5.2 -org.eclipse.jetty:jetty-annotations:9.4.58.v20250814 -org.eclipse.jetty:jetty-http:9.4.58.v20250814 -org.eclipse.jetty:jetty-io:9.4.58.v20250814 -org.eclipse.jetty:jetty-jndi:9.4.58.v20250814 -org.eclipse.jetty:jetty-plus:9.4.58.v20250814 -org.eclipse.jetty:jetty-security:9.4.58.v20250814 -org.eclipse.jetty:jetty-server:9.4.58.v20250814 -org.eclipse.jetty:jetty-servlet:9.4.58.v20250814 -org.eclipse.jetty:jetty-util:9.4.58.v20250814 -org.eclipse.jetty:jetty-util-ajax:9.4.58.v20250814 -org.eclipse.jetty:jetty-webapp:9.4.58.v20250814 -org.eclipse.jetty:jetty-xml:9.4.58.v20250814 -org.eclipse.jetty.websocket:javax-websocket-client-impl:9.4.58.v20250814 -org.eclipse.jetty.websocket:javax-websocket-server-impl:9.4.58.v20250814 +org.eclipse.jetty:jetty-alpn-client:12.1.12 +org.eclipse.jetty:jetty-client:12.1.12 +org.eclipse.jetty:jetty-ee:12.1.12 +org.eclipse.jetty:jetty-http:12.1.12 +org.eclipse.jetty:jetty-io:12.1.12 +org.eclipse.jetty:jetty-security:12.1.12 +org.eclipse.jetty:jetty-server:12.1.12 +org.eclipse.jetty:jetty-session:12.1.12 +org.eclipse.jetty:jetty-util:12.1.12 +org.eclipse.jetty:jetty-xml:12.1.12 +org.eclipse.jetty.ee8:jetty-ee8-nested:12.1.12 +org.eclipse.jetty.ee8:jetty-ee8-security:12.1.12 +org.eclipse.jetty.ee8:jetty-ee8-servlet:12.1.12 +org.eclipse.jetty.ee8:jetty-ee8-webapp:12.1.12 +org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-api:12.1.12 +org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-client:12.1.12 +org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-common:12.1.12 +org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-server:12.1.12 +org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-servlet:12.1.12 +org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.9 +org.eclipse.jetty.websocket:jetty-websocket-core-client:12.1.12 +org.eclipse.jetty.websocket:jetty-websocket-core-common:12.1.12 +org.eclipse.jetty.websocket:jetty-websocket-core-server:12.1.12 org.ehcache:ehcache:3.8.2 org.ini4j:ini4j:0.5.4 org.objenesis:objenesis:2.6 @@ -625,9 +634,7 @@ CDDL 1.1 + GPLv2 with classpath exception com.sun.xml.bind:jaxb-impl:2.2.3-1 javax.annotation:javax.annotation-api:1.3.2 javax.cache:cache-api:1.1.1 -javax.servlet:javax.servlet-api:3.1.0 javax.servlet.jsp:jsp-api:2.1 -javax.websocket:javax.websocket-api:1.0 Eclipse Distribution License (EDL) 1.0 -------------------------- @@ -645,8 +652,6 @@ Eclipse Public License (EPL) 2.0 -------------------------- jakarta.ws.rs-api:jakarta.ws.rs-api:2.1.6 -jakarta.servlet.jsp:jakarta.servlet.jsp-api:2.3.6 -jakarta.servlet:jakarta.servlet-api:4.0.4 Eclipse Public License (EPL) 2.0 with some parts being GNU General Public License (GPL), Version 2, With Classpath Exception, @@ -660,9 +665,10 @@ org.glassfish.jersey.core:jersey-server:2.46 org.glassfish.jersey.inject:jersey-hk2:2.46 org.glassfish.jersey.core:jersey-client:2.46 org.glassfish.jersey.test-framework:jersey-test-framework-core:2.46 -org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-jetty:2.46 +org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-jdk-http:2.46 org.glassfish.jersey.containers:jersey-container-servlet:2.46 org.glassfish.jersey.containers:jersey-container-servlet-core:2.46 +org.glassfish.jersey.containers:jersey-container-jdk-http:2.46 org.glassfish.jersey.media:jersey-media-json-jettison:2.46 org.glassfish.jersey.media:jersey-media-jaxb:2.46 diff --git a/NOTICE-binary b/NOTICE-binary index 407f6c25d997a4..6ac4e1918ef97d 100644 --- a/NOTICE-binary +++ b/NOTICE-binary @@ -463,10 +463,8 @@ Oracle The following artifacts are CDDL + GPLv2 with classpath exception. https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html - * javax.servlet:javax.servlet-api * javax.annotation:javax.annotation-api * javax.transaction:javax.transaction-api - * javax.websocket:javax.websocket-api ------ Oracle OpenJDK diff --git a/hadoop-client-modules/hadoop-client-minicluster/pom.xml b/hadoop-client-modules/hadoop-client-minicluster/pom.xml index 7042f2ce07880a..9ddc124fb1fd0e 100644 --- a/hadoop-client-modules/hadoop-client-minicluster/pom.xml +++ b/hadoop-client-modules/hadoop-client-minicluster/pom.xml @@ -124,8 +124,12 @@ javax.servlet-api - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api + + + org.eclipse.jetty.toolchain + jetty-servlet-api jakarta.ws.rs @@ -419,7 +423,7 @@ - + @@ -464,7 +468,7 @@ - + org.apache.hadoop @@ -476,8 +480,12 @@ javax.servlet-api - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api + + + org.eclipse.jetty.toolchain + jetty-servlet-api jakarta.ws.rs @@ -551,8 +559,12 @@ javax.servlet-api - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api + + + org.eclipse.jetty.toolchain + jetty-servlet-api jakarta.ws.rs @@ -623,6 +635,10 @@ jakarta.servlet jakarta.servlet-api + + org.eclipse.jetty.toolchain + jetty-servlet-api + junit-jupiter org.junit.jupiter @@ -632,8 +648,17 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http + + + jakarta.servlet + jakarta.servlet-api + + + org.eclipse.jetty.toolchain + jetty-servlet-api + jakarta.ws.rs jakarta.ws.rs-api @@ -707,8 +732,14 @@ junit:junit com.google.code.findbugs:jsr305 ch.qos.reload4j:reload4j - org.eclipse.jetty.websocket:websocket-common - org.eclipse.jetty.websocket:websocket-api + + org.eclipse.jetty.websocket:jetty-websocket-core-common + org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-api + org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-common org.bouncycastle:* @@ -940,11 +971,13 @@ com/sun/security/* com/sun/jndi/* com/sun/management/* + com/sun/net/httpserver/* com/sun/tools/**/* com/sun/javadoc/**/* com/sun/security/**/* com/sun/jndi/**/* com/sun/management/**/* + com/sun/net/httpserver/**/* com/ibm/security/* com/ibm/security/**/* diff --git a/hadoop-client-modules/hadoop-client-runtime/pom.xml b/hadoop-client-modules/hadoop-client-runtime/pom.xml index de626ec9b18f1a..4414d3c4d95184 100644 --- a/hadoop-client-modules/hadoop-client-runtime/pom.xml +++ b/hadoop-client-modules/hadoop-client-runtime/pom.xml @@ -161,16 +161,35 @@ com.google.code.findbugs:jsr305 io.netty:* io.dropwizard.metrics:metrics-core - org.eclipse.jetty:jetty-servlet org.eclipse.jetty:jetty-security - org.eclipse.jetty:jetty-util - org.eclipse.jetty:jetty-util-ajax org.eclipse.jetty:jetty-server - org.eclipse.jetty:jetty-continuation + org.eclipse.jetty:jetty-session + org.eclipse.jetty.ee8:jetty-ee8-servlet + org.eclipse.jetty.ee8:jetty-ee8-nested + org.eclipse.jetty.ee8:jetty-ee8-security org.ow2.asm:* org.bouncycastle:* diff --git a/hadoop-common-project/hadoop-auth-examples/pom.xml b/hadoop-common-project/hadoop-auth-examples/pom.xml index 331cffc7815597..f442839fda2aeb 100644 --- a/hadoop-common-project/hadoop-auth-examples/pom.xml +++ b/hadoop-common-project/hadoop-auth-examples/pom.xml @@ -32,8 +32,8 @@ - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api provided diff --git a/hadoop-common-project/hadoop-auth/pom.xml b/hadoop-common-project/hadoop-auth/pom.xml index 4b8854887c1966..9bab9991929123 100644 --- a/hadoop-common-project/hadoop-auth/pom.xml +++ b/hadoop-common-project/hadoop-auth/pom.xml @@ -57,13 +57,13 @@ test - org.eclipse.jetty - jetty-servlet + org.eclipse.jetty.ee8 + jetty-ee8-servlet test - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api provided diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/AuthenticatedURL.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/AuthenticatedURL.java index cb7d36368aa393..b6742c9e541a43 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/AuthenticatedURL.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/AuthenticatedURL.java @@ -19,11 +19,13 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.net.CookieHandler; import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -75,6 +77,16 @@ public class AuthenticatedURL { */ public static final String AUTH_COOKIE = "hadoop.auth"; + /** How much of a failed response body is worth quoting back. */ + private static final int MAX_RESPONSE_DETAIL_BYTES = 4096; + + /** + * Content type of the JSON error envelope. Spelled out rather than taken + * from HttpExceptionUtils, which lives in hadoop-common and so is below this + * module rather than above it. + */ + private static final String APPLICATION_JSON_MIME = "application/json"; + // a lightweight cookie handler that will be attached to url connections. // client code is not required to extract or inject auth cookies. private static class AuthCookieHandler extends CookieHandler { @@ -403,7 +415,90 @@ public static void extractToken(HttpURLConnection conn, Token token) throws IOEx throw new AuthenticationException("Authentication failed" + ", URL: " + conn.getURL() + ", status: " + conn.getResponseCode() + - ", message: " + conn.getResponseMessage()); + ", message: " + responseDetail(conn)); + } + } + + /** + * Why the server turned the request down. + *

+ * {@link AuthenticationFilter} reports its reason through + * {@link javax.servlet.http.HttpServletResponse#sendError}, which used to + * reach the caller in the HTTP reason phrase. Jetty 12 does not put a reason + * phrase on the wire, so {@link HttpURLConnection#getResponseMessage()} now + * only ever returns the canonical text for the status code and the reason is + * in the body. Read the body, and fall back to the phrase when it is empty. + *

+ * A JSON body is left alone. That is the envelope + * HttpExceptionUtils#createServletExceptionResponse writes, and a refusal + * that carries it never carried a reason phrase of its own - it is sent with + * setStatus, not sendError, so the phrase was the canonical text for the + * status code before Jetty 12 and still is. Quoting the envelope back as + * free text would replace a readable "Forbidden" with a line of JSON, and + * the callers that want what is inside it parse it with + * HttpExceptionUtils#validateResponse instead. + */ + private static String responseDetail(HttpURLConnection conn) { + if (isJson(conn.getContentType())) { + return responsePhrase(conn); + } + try (InputStream es = conn.getErrorStream()) { + if (es != null) { + byte[] body = new byte[MAX_RESPONSE_DETAIL_BYTES]; + int read = 0, n; + while (read < body.length + && (n = es.read(body, read, body.length - read)) != -1) { + read += n; + } + String text = toPlainText( + new String(body, 0, read, StandardCharsets.UTF_8)); + if (!text.isEmpty()) { + return text; + } + } + } catch (IOException ex) { + // nothing to add: fall through to the reason phrase + } + return responsePhrase(conn); + } + + /** + * Whether the content type names the JSON error envelope. The header can + * carry parameters - "application/json; charset=utf-8" - so this matches a + * prefix rather than the whole value. + */ + private static boolean isJson(String contentType) { + return contentType != null + && contentType.trim().toLowerCase().startsWith(APPLICATION_JSON_MIME); + } + + /** + * Reduces a response body to something readable in a one-line message. A + * container renders sendError as an HTML page, so the reason arrives buried + * in markup. + *

+ * Kept in step with {@code HttpExceptionUtils.toPlainText}, which does the + * same job for the same bodies one module up. The duplication is forced: + * hadoop-common depends on this module, not the other way round. + */ + private static String toPlainText(String body) { + String text = body; + if (text.indexOf('<') >= 0) { + text = text.replaceAll("(?s)<(script|style)\\b.*?", " ") + .replaceAll("(?s)<[^>]*>", " "); + } + text = text.replace("<", "<").replace(">", ">") + .replace(""", "\"").replace("'", "'") + .replace("&", "&"); + return text.replaceAll("\\s+", " ").trim(); + } + + /** The reason phrase, or null if it cannot be read. */ + private static String responsePhrase(HttpURLConnection conn) { + try { + return conn.getResponseMessage(); + } catch (IOException ex) { + return null; } } diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java index 7cc70c493c0f66..3549ef1ed9c42a 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationFilter.java @@ -621,8 +621,11 @@ && getMaxInactiveInterval() > 0) { KerberosAuthenticator.WWW_AUTHENTICATE.toLowerCase()))) { errCode = HttpServletResponse.SC_FORBIDDEN; } - // After Jetty 9.4.21, sendError() no longer allows a custom message. - // use setStatus() to set a custom message. + // The reason phrase is not a place to put this any more. Jetty 12 + // stores what ee8's Response.setStatusWithReason is given and never + // writes it to the wire, and setStatus(int, String) has ignored its + // message since Servlet 3.0 deprecated it. sendError puts the detail + // in the response body, which is where a client can still read it. String reason; if (authenticationEx == null) { reason = "Authentication required"; @@ -630,7 +633,6 @@ && getMaxInactiveInterval() > 0) { reason = authenticationEx.getMessage(); } - httpResponse.setStatus(errCode, reason); httpResponse.sendError(errCode, reason); } } diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java index 8295fe173f4b1a..68da16580ec944 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/AuthenticationToken.java @@ -18,12 +18,10 @@ import java.security.Principal; -import javax.servlet.http.HttpServletRequest; - /** * The {@link AuthenticationToken} contains information about an authenticated * HTTP client and doubles as the {@link Principal} to be returned by - * authenticated {@link HttpServletRequest}s + * authenticated HTTP requests. *

* The token can be serialized/deserialized to and from a string as it is sent * and received in HTTP client responses and requests as a HTTP cookie (this is diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/JWTRedirectAuthenticationHandler.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/JWTRedirectAuthenticationHandler.java index 2dcb60836b5e43..5fba0af1bf2a58 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/JWTRedirectAuthenticationHandler.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/server/JWTRedirectAuthenticationHandler.java @@ -26,6 +26,7 @@ import java.util.Properties; import java.text.ParseException; +import java.security.cert.CertificateException; import java.security.interfaces.RSAPublicKey; import org.apache.hadoop.classification.VisibleForTesting; @@ -125,7 +126,11 @@ public void init(Properties config) throws ServletException { throw new ServletException( "Public key for signature validation must be provisioned."); } - publicKey = CertificateUtil.parseRSAPublicKey(pemPublicKey); + try { + publicKey = CertificateUtil.toRSAPublicKey(pemPublicKey); + } catch (CertificateException ce) { + throw new ServletException(ce.getMessage(), ce); + } } // setup the list of valid audiences for token validation String auds = config.getProperty(EXPECTED_JWT_AUDIENCES); diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java index f25602c67d4a31..13840948ab349f 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/CertificateUtil.java @@ -38,8 +38,37 @@ public class CertificateUtil { * - the pem encoding from config without the header and footer * @return RSAPublicKey the RSA public key * @throws ServletException thrown if a processing error occurred + * @deprecated use {@link #toRSAPublicKey(String)}, which reports the + * {@link CertificateException} it is really raising instead of + * wrapping it in a servlet type. This method is kept so that + * existing callers still compile and behave as before, and will + * be removed with the move to the jakarta servlet namespace. */ - public static RSAPublicKey parseRSAPublicKey(String pem) throws ServletException { + @Deprecated + public static RSAPublicKey parseRSAPublicKey(String pem) + throws ServletException { + try { + return toRSAPublicKey(pem); + } catch (CertificateException ce) { + // Report the exception toRSAPublicKey wrapped, not the wrapper, so that + // the cause a caller sees is the one this method has always reported: + // the CertificateException the parse itself raised. Wrapping the wrapper + // would add a level to the chain that was not there before. + Throwable cause = ce.getCause() == null ? ce : ce.getCause(); + throw new ServletException(ce.getMessage(), cause); + } + } + + /** + * Gets an RSAPublicKey from the provided PEM encoding. + * + * @param pem + * - the pem encoding from config without the header and footer + * @return RSAPublicKey the RSA public key + * @throws CertificateException thrown if the PEM could not be parsed + */ + public static RSAPublicKey toRSAPublicKey(String pem) + throws CertificateException { String fullPem = PEM_HEADER + pem + PEM_FOOTER; PublicKey key = null; try { @@ -57,7 +86,7 @@ public static RSAPublicKey parseRSAPublicKey(String pem) throws ServletException } else { message = "CertificateException - PEM may be corrupt"; } - throw new ServletException(message, ce); + throw new CertificateException(message, ce); } return (RSAPublicKey) key; } diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/FileSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/FileSignerSecretProvider.java index 2a8a712b595bac..66bd181771a9a1 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/FileSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/FileSignerSecretProvider.java @@ -17,7 +17,6 @@ import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.security.authentication.server.AuthenticationFilter; -import javax.servlet.ServletContext; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -37,7 +36,7 @@ public class FileSignerSecretProvider extends SignerSecretProvider { public FileSignerSecretProvider() {} @Override - public void init(Properties config, ServletContext servletContext, + public void initialize(Properties config, SecretProviderContext context, long tokenValidity) throws Exception { String signatureSecretFile = config.getProperty( diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RolloverSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RolloverSignerSecretProvider.java index ca95272cf9fd62..9edc3145c4a02c 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RolloverSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/RolloverSignerSecretProvider.java @@ -17,7 +17,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletContext; import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -61,12 +60,12 @@ public RolloverSignerSecretProvider() { * and starts the scheduler for the rollover to run at an interval of * tokenValidity. * @param config configuration properties - * @param servletContext servlet context + * @param context the attribute store to initialize against * @param tokenValidity The amount of time a token is valid for * @throws Exception thrown if an error occurred */ @Override - public void init(Properties config, ServletContext servletContext, + public void initialize(Properties config, SecretProviderContext context, long tokenValidity) throws Exception { initSecrets(generateNewSecret(), null); startScheduler(tokenValidity, tokenValidity); diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SecretProviderContext.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SecretProviderContext.java new file mode 100644 index 00000000000000..b002ab0c047303 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SecretProviderContext.java @@ -0,0 +1,52 @@ +/** + * 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. See accompanying LICENSE file. + */ +package org.apache.hadoop.security.authentication.util; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * The attribute store a {@link SignerSecretProvider} is initialized against. + *

+ * A SignerSecretProvider that has to share an object with the rest of the + * application it is embedded in - as {@link ZKSignerSecretProvider} shares its + * CuratorFramework client - reads and writes that object here. When the + * provider is running inside a servlet container the attributes are those of + * the ServletContext, so the sharing is unchanged; see + * {@link org.apache.hadoop.security.authentication.server.AuthenticationFilter}. + *

+ * This interface exists so that a SignerSecretProvider need not name a servlet + * type. Implementations of it that are backed by a ServletContext do, but the + * providers themselves stay independent of the servlet API and so of which + * servlet namespace - javax or jakarta - the container provides. + */ +@InterfaceStability.Unstable +@InterfaceAudience.Private +public interface SecretProviderContext { + + /** + * Returns the attribute stored under the given name, or null if there is + * none. + * @param name the attribute name + * @return the attribute value, or null + */ + Object getAttribute(String name); + + /** + * Stores an attribute under the given name, replacing any previous value. + * @param name the attribute name + * @param value the attribute value + */ + void setAttribute(String name, Object value); +} diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ServletSecretProviderContext.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ServletSecretProviderContext.java new file mode 100644 index 00000000000000..317eeb541453e9 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ServletSecretProviderContext.java @@ -0,0 +1,117 @@ +/** + * 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. See accompanying LICENSE file. + */ +package org.apache.hadoop.security.authentication.util; + +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletContext; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.classification.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Presents a ServletContext to a {@link SignerSecretProvider} as a + * {@link SecretProviderContext}. + *

+ * Reads and writes go straight through to the ServletContext, so an object a + * provider shares this way - the CuratorFramework client of + * {@link ZKSignerSecretProvider} - is still a ServletContext attribute under + * the same name, and is still found there by everything that looks for it, + * including DelegationTokenAuthenticationFilter in hadoop-common. + *

+ * This class is the one place in the provider hierarchy that names a servlet + * type, and it goes away with the deprecated + * {@link SignerSecretProvider#init(java.util.Properties, ServletContext, long)} + * it exists to support. + */ +@InterfaceStability.Unstable +@InterfaceAudience.Private +final class ServletSecretProviderContext implements SecretProviderContext { + + private static final Logger LOG = + LoggerFactory.getLogger(ServletSecretProviderContext.class); + + private final ServletContext servletContext; + + private ServletSecretProviderContext(ServletContext servletContext) { + this.servletContext = servletContext; + } + + /** + * Returns a context backed by the given ServletContext, or one backed by a + * map of its own when there is no ServletContext. + *

+ * Most callers of the deprecated init pass null - only AuthenticationFilter + * ever passes a real context - and a provider that does not use the store is + * unaffected either way. Handing back a store rather than null keeps a + * provider that does use it from having to check. + * + * @param servletContext the servlet context, or null + * @return an attribute store, never null + */ + static SecretProviderContext of(ServletContext servletContext) { + return servletContext == null + ? new MapSecretProviderContext() + : new ServletSecretProviderContext(servletContext); + } + + @Override + public Object getAttribute(String name) { + return servletContext.getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + servletContext.setAttribute(name, value); + } + + /** + * The store used when there is no ServletContext to write through to. + * Attributes live as long as the provider does and are seen by nothing else, + * which is what passing a null ServletContext already meant. + */ + @VisibleForTesting + static final class MapSecretProviderContext + implements SecretProviderContext { + + private final Map attributes = new HashMap<>(); + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + /** + * Stores the attribute, and says so. + *

+ * A provider only writes here to share an object with the rest of the + * application - the CuratorFramework client of + * {@link ZKSignerSecretProvider} is the one case in this tree. There is + * nothing to share it with: this store is private to the provider, so a + * second provider in the same JVM builds a second client rather than + * finding this one. Before there was a store at all a null ServletContext + * threw, which was at least loud; this keeps it visible. + */ + @Override + public void setAttribute(String name, Object value) { + LOG.warn("SignerSecretProvider was initialized without a ServletContext," + + " so the attribute {} is being stored in a map private to this" + + " provider. Anything that expects to share it - including another" + + " provider in this JVM - will not find it there.", name); + attributes.put(name, value); + } + } +} diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SignerSecretProvider.java index e937862458e0d0..ff5acc210f7805 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SignerSecretProvider.java @@ -24,20 +24,77 @@ * do more complicated things in the backend. * See the RolloverSignerSecretProvider class for an implementation that * supports rolling over the secret at a regular interval. + *

+ * Implementations should override + * {@link #initialize(Properties, SecretProviderContext, long)}, which names no + * servlet type. The older {@link #init(Properties, ServletContext, long)} is + * still the entry point callers use and is still honoured when an + * implementation overrides it, so providers written against it keep working + * unchanged; it is deprecated and will be removed with the move to the jakarta + * servlet namespace. + *

+ * One consequence for code that reflects over these classes: the providers + * shipped here override + * {@link #initialize(Properties, SecretProviderContext, long)} rather than + * {@link #init(Properties, ServletContext, long)}, so init is no longer among + * their declared methods. Calls and overrides are unaffected - init is + * inherited from this class and resolves as it always did - but + * Class#getDeclaredMethod("init", ...) on a subclass such as + * {@link FileSignerSecretProvider} now raises NoSuchMethodException where it + * used to succeed. Class#getMethod, which searches superclasses, still finds + * it. */ @InterfaceStability.Unstable @InterfaceAudience.Private public abstract class SignerSecretProvider { /** - * Initialize the SignerSecretProvider + * Initialize the SignerSecretProvider. + *

+ * The default implementation adapts the ServletContext to a + * {@link SecretProviderContext} and calls + * {@link #initialize(Properties, SecretProviderContext, long)}, so a provider + * that overrides only that method is initialized correctly through this + * entry point. A provider that overrides this method instead is called + * directly, as before. + * * @param config configuration properties * @param servletContext servlet context * @param tokenValidity The amount of time a token is valid for * @throws Exception thrown if an error occurred + * @deprecated override + * {@link #initialize(Properties, SecretProviderContext, long)}, + * which does not name a servlet type and so is unaffected by + * which servlet namespace the container provides. */ - public abstract void init(Properties config, ServletContext servletContext, - long tokenValidity) throws Exception; + @Deprecated + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { + initialize(config, ServletSecretProviderContext.of(servletContext), + tokenValidity); + } + + /** + * Initialize the SignerSecretProvider against an attribute store. + *

+ * The default implementation throws, because a provider has to implement one + * of the two initialization methods. It is never reached by a provider that + * overrides the deprecated + * {@link #init(Properties, ServletContext, long)}, since callers go through + * that method and its override does not delegate here. + * + * @param config configuration properties + * @param context the attribute store to initialize against + * @param tokenValidity The amount of time a token is valid for + * @throws Exception thrown if an error occurred + */ + public void initialize(Properties config, SecretProviderContext context, + long tokenValidity) throws Exception { + throw new UnsupportedOperationException(getClass().getName() + + " implements neither initialize(Properties, SecretProviderContext," + + " long) nor the deprecated init(Properties, ServletContext, long)"); + } + /** * Will be called on shutdown; subclasses should perform any cleanup here. */ diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ZKSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ZKSignerSecretProvider.java index b0604c85c39f42..853cffbfa9e412 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ZKSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/ZKSignerSecretProvider.java @@ -18,7 +18,6 @@ import java.security.SecureRandom; import java.util.Properties; import java.util.Random; -import javax.servlet.ServletContext; import org.apache.curator.framework.CuratorFramework; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -100,9 +99,11 @@ public class ZKSignerSecretProvider extends RolloverSignerSecretProvider { CONFIG_PREFIX + "disconnect.on.shutdown"; /** - * Constant for the ServletContext attribute that can be used for providing a - * custom CuratorFramework client. If set ZKSignerSecretProvider will use this - * Curator client instead of creating a new one. The providing class is + * Constant for the {@link SecretProviderContext} attribute that can be used + * for providing a custom CuratorFramework client. In a servlet container that + * is the ServletContext attribute of the same name, so the way it is set is + * unchanged. If set ZKSignerSecretProvider will use this Curator client + * instead of creating a new one. The providing class is * responsible for creating and configuring the Curator client (including * security and ACLs) in this case. */ @@ -159,16 +160,16 @@ public ZKSignerSecretProvider(long seed) { } @Override - public void init(Properties config, ServletContext servletContext, + public void initialize(Properties config, SecretProviderContext context, long tokenValidity) throws Exception { - Object curatorClientObj = servletContext.getAttribute( + Object curatorClientObj = context.getAttribute( ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE); if (curatorClientObj != null && curatorClientObj instanceof CuratorFramework) { client = (CuratorFramework) curatorClientObj; } else { client = createCuratorClient(config); - servletContext.setAttribute( + context.setAttribute( ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE, client); } this.tokenValidity = tokenValidity; diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/client/AuthenticatorTestCase.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/client/AuthenticatorTestCase.java index 14538b873c803c..c6137b4ee08cb9 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/client/AuthenticatorTestCase.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/client/AuthenticatorTestCase.java @@ -35,9 +35,9 @@ import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.FilterHolder; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import javax.servlet.DispatcherType; import javax.servlet.FilterConfig; diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/client/TestPseudoAuthenticator.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/client/TestPseudoAuthenticator.java index 43f38f2594e263..b944857656c872 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/client/TestPseudoAuthenticator.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/client/TestPseudoAuthenticator.java @@ -20,9 +20,12 @@ import org.apache.hadoop.security.authentication.server.PseudoAuthenticationHandler; import org.junit.jupiter.api.Test; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Properties; +import java.util.Scanner; public class TestPseudoAuthenticator { @@ -67,7 +70,9 @@ public void testAnonymousDisallowed() throws Exception { conn.connect(); assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); assertTrue(conn.getHeaderFields().containsKey("WWW-Authenticate")); - assertEquals("Authentication required", conn.getResponseMessage()); + // Jetty 12 sends the canonical reason phrase whatever the server sets, + // so the detail is read out of the body instead. + assertTrue(readBody(conn).contains("Authentication required")); } finally { auth.stop(); } @@ -105,4 +110,17 @@ public void testAuthenticationAnonymousDisallowedWithPost() throws Exception { auth._testAuthentication(new PseudoAuthenticator(), true); } + private static String readBody(HttpURLConnection conn) { + InputStream in = conn.getErrorStream() != null + ? conn.getErrorStream() : null; + if (in == null) { + return ""; + } + try (Scanner scanner = new Scanner(in, StandardCharsets.UTF_8.name())) { + // \A matches only at the start of input, so the whole body is one token. + scanner.useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next() : ""; + } + } + } diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java index 9d857640bcf482..ed5587d7a699d4 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/StringSignerSecretProvider.java @@ -15,7 +15,6 @@ import java.nio.charset.StandardCharsets; import java.util.Properties; -import javax.servlet.ServletContext; import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.classification.InterfaceStability; @@ -34,7 +33,7 @@ class StringSignerSecretProvider extends SignerSecretProvider { public StringSignerSecretProvider() {} @Override - public void init(Properties config, ServletContext servletContext, + public void initialize(Properties config, SecretProviderContext context, long tokenValidity) throws Exception { String signatureSecret = config.getProperty( AuthenticationFilter.SIGNATURE_SECRET, null); diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestCertificateUtil.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestCertificateUtil.java index 0580bac9053be3..92851654e4e041 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestCertificateUtil.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestCertificateUtil.java @@ -18,10 +18,12 @@ package org.apache.hadoop.security.authentication.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.security.cert.CertificateException; import java.security.interfaces.RSAPublicKey; import javax.servlet.ServletException; @@ -45,9 +47,9 @@ public void testInvalidPEMWithHeaderAndFooter() throws Exception { + "9aTyR+HGHCfvwoCegc9rAVw/DLaRriSO/jnEXzYK6XLVKH+hx5UXrJ7Oyc7JjZUc3g9kCWORThCX" + "Mzc1xA==" + "\n-----END CERTIFICATE-----"; try { - CertificateUtil.parseRSAPublicKey(pem); - fail("Should not have thrown ServletException"); - } catch (ServletException se) { + CertificateUtil.toRSAPublicKey(pem); + fail("Should not have thrown CertificateException"); + } catch (CertificateException se) { assertTrue(se.getMessage().contains("PEM header")); } } @@ -66,9 +68,9 @@ public void testCorruptPEM() throws Exception { + "9aTyR+HGHCfvwoCegc9rAVw/DLaRriSO/jnEXzYK6XLVKH+hx5UXrJ7Oyc7JjZUc3g9kCWORThCX" + "Mzc1xA++"; try { - CertificateUtil.parseRSAPublicKey(pem); - fail("Should not have thrown ServletException"); - } catch (ServletException se) { + CertificateUtil.toRSAPublicKey(pem); + fail("Should not have thrown CertificateException"); + } catch (CertificateException se) { assertTrue(se.getMessage().contains("corrupt")); } } @@ -87,12 +89,62 @@ public void testValidPEM() throws Exception { + "9aTyR+HGHCfvwoCegc9rAVw/DLaRriSO/jnEXzYK6XLVKH+hx5UXrJ7Oyc7JjZUc3g9kCWORThCX" + "Mzc1xA=="; try { - RSAPublicKey pk = CertificateUtil.parseRSAPublicKey(pem); + RSAPublicKey pk = CertificateUtil.toRSAPublicKey(pem); assertNotNull(pk); assertEquals("RSA", pk.getAlgorithm()); + } catch (CertificateException se) { + fail("Should not have thrown CertificateException"); + } + } + + /** + * The deprecated entry point still reports a ServletException, so callers + * written against it keep compiling and keep catching what they caught + * before. + */ + @Test + @SuppressWarnings("deprecation") + public void testDeprecatedEntryPointStillThrowsServletException() { + String pem = "not a certificate"; + try { + CertificateUtil.parseRSAPublicKey(pem); + fail("Should not have thrown ServletException"); } catch (ServletException se) { + assertTrue(se.getMessage().contains("corrupt")); + assertTrue(se.getCause() instanceof CertificateException); + } + } + + /** + * The deprecated entry point reports the CertificateException the parse + * raised, not the one toRSAPublicKey wraps it in. Routing through the new + * method must not lengthen the chain a caller walks. + */ + @Test + @SuppressWarnings("deprecation") + public void testDeprecatedEntryPointDoesNotDeepenTheCauseChain() { + try { + CertificateUtil.parseRSAPublicKey("not a certificate"); fail("Should not have thrown ServletException"); + } catch (ServletException se) { + Throwable cause = se.getCause(); + assertNotNull(cause, "the ServletException should carry a cause"); + assertTrue(cause instanceof CertificateException, + "expected a CertificateException, got " + cause.getClass()); + assertFalse(cause.getCause() instanceof CertificateException, + "the wrapper toRSAPublicKey adds must not appear in the chain;" + + " chain was " + chainOf(se)); } } + private static String chainOf(Throwable t) { + StringBuilder sb = new StringBuilder(); + for (Throwable c = t; c != null; c = c.getCause()) { + if (sb.length() > 0) { + sb.append(" -> "); + } + sb.append(c.getClass().getSimpleName()); + } + return sb.toString(); + } } diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSignerSecretProviderCompatibility.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSignerSecretProviderCompatibility.java new file mode 100644 index 00000000000000..2a515889da46b0 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSignerSecretProviderCompatibility.java @@ -0,0 +1,228 @@ +/** + * 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. See accompanying LICENSE file. + */ +package org.apache.hadoop.security.authentication.util; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.ServletContext; + +import org.junit.jupiter.api.Test; + +/** + * A SignerSecretProvider written against the deprecated + * {@link SignerSecretProvider#init(Properties, ServletContext, long)} must keep + * working without being recompiled, because implementations of it live outside + * this tree - signer.secret.provider takes a classname. + *

+ * These tests pin that contract. They are the reason + * {@link SignerSecretProvider#init(Properties, ServletContext, long)} is a + * concrete bridge rather than a changed abstract method, and they go when it + * does. + */ +@SuppressWarnings("deprecation") +public class TestSignerSecretProviderCompatibility { + + private static final byte[] SECRET = + "secret".getBytes(StandardCharsets.UTF_8); + + /** + * A provider as it would have been written before SecretProviderContext + * existed: it overrides init and takes a ServletContext. + */ + private static final class LegacyProvider extends SignerSecretProvider { + private ServletContext seen; + private boolean initCalled; + + @Override + public void init(Properties config, ServletContext servletContext, + long tokenValidity) { + this.seen = servletContext; + this.initCalled = true; + } + + @Override + public byte[] getCurrentSecret() { + return SECRET; + } + + @Override + public byte[][] getAllSecrets() { + return new byte[][]{SECRET}; + } + } + + /** + * The same, extending RolloverSignerSecretProvider and chaining to super as + * such a provider is expected to. + */ + private static final class LegacyRolloverProvider + extends RolloverSignerSecretProvider { + private boolean initCalled; + + @Override + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { + this.initCalled = true; + super.init(config, servletContext, tokenValidity); + } + + @Override + protected byte[] generateNewSecret() { + return SECRET; + } + } + + /** A provider written against the servlet-free method. */ + private static final class ContextProvider extends SignerSecretProvider { + private SecretProviderContext seen; + + @Override + public void initialize(Properties config, SecretProviderContext context, + long tokenValidity) { + this.seen = context; + } + + @Override + public byte[] getCurrentSecret() { + return SECRET; + } + + @Override + public byte[][] getAllSecrets() { + return new byte[][]{SECRET}; + } + } + + /** A provider that implements neither method. */ + private static final class UninitializableProvider extends SignerSecretProvider { + @Override + public byte[] getCurrentSecret() { + return SECRET; + } + + @Override + public byte[][] getAllSecrets() { + return new byte[][]{SECRET}; + } + } + + @Test + public void testLegacyProviderStillReceivesTheServletContext() + throws Exception { + ServletContext servletContext = mock(ServletContext.class); + LegacyProvider provider = new LegacyProvider(); + + provider.init(new Properties(), servletContext, 1000); + + assertTrue(provider.initCalled, "the legacy override should have run"); + assertSame(servletContext, provider.seen); + } + + @Test + public void testLegacyRolloverProviderIsStillRolledOver() throws Exception { + LegacyRolloverProvider provider = new LegacyRolloverProvider(); + try { + provider.init(new Properties(), mock(ServletContext.class), 100000); + + // The override ran, and chaining to super still reached + // RolloverSignerSecretProvider.initialize, which seeds the secret. Were + // the bridge dispatched statically, this would silently be null. + assertTrue(provider.initCalled, "the legacy override should have run"); + assertArrayEquals(SECRET, provider.getCurrentSecret()); + } finally { + provider.destroy(); + } + } + + @Test + public void testContextProviderIsReachedThroughTheDeprecatedEntryPoint() + throws Exception { + Map attributes = new HashMap<>(); + ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getAttribute("a")) + .thenAnswer(invocation -> attributes.get("a")); + + ContextProvider provider = new ContextProvider(); + provider.init(new Properties(), servletContext, 1000); + + assertNotNull(provider.seen, "initialize should have been called"); + + // Writes reach the real ServletContext, so an object a provider shares + // this way is still found there by everything that looks for it. + provider.seen.setAttribute("a", "value"); + org.mockito.Mockito.verify(servletContext).setAttribute("a", "value"); + } + + @Test + public void testNullServletContextYieldsAUsableStore() throws Exception { + ContextProvider provider = new ContextProvider(); + provider.init(new Properties(), null, 1000); + + assertNotNull(provider.seen, + "a null ServletContext should still yield a store"); + provider.seen.setAttribute("a", "value"); + assertSame("value", provider.seen.getAttribute("a")); + assertFalse(provider.seen instanceof ServletSecretProviderContext); + } + + /** + * A provider that shares an object through the store - which in this tree + * means ZKSignerSecretProvider and its Curator client - gets a store private + * to itself when there is no ServletContext, so a second provider does not + * find what the first one put there. That is a real change from the + * NullPointerException a null ServletContext used to raise, so it is pinned + * here rather than left to be discovered. + */ + @Test + public void testNullServletContextDoesNotShareBetweenProviders() + throws Exception { + ContextProvider first = new ContextProvider(); + ContextProvider second = new ContextProvider(); + first.init(new Properties(), null, 1000); + second.init(new Properties(), null, 1000); + + first.seen.setAttribute("shared", "from-first"); + + assertNull(second.seen.getAttribute("shared"), + "a store backed by no ServletContext is private to its provider"); + assertTrue( + first.seen instanceof ServletSecretProviderContext + .MapSecretProviderContext, + "expected the private map store, got " + + first.seen.getClass().getName()); + } + + @Test + public void testProviderImplementingNeitherMethodFailsLoudly() { + UninitializableProvider provider = new UninitializableProvider(); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, + () -> provider.init(new Properties(), null, 1000)); + assertTrue(e.getMessage().contains(UninitializableProvider.class.getName())); + } +} diff --git a/hadoop-common-project/hadoop-common/pom.xml b/hadoop-common-project/hadoop-common/pom.xml index f10ed425370661..23d5db114d2d87 100644 --- a/hadoop-common-project/hadoop-common/pom.xml +++ b/hadoop-common-project/hadoop-common/pom.xml @@ -92,11 +92,6 @@ commons-collections4 compile - - jakarta.servlet.jsp - jakarta.servlet.jsp-api - compile - jakarta.ws.rs jakarta.ws.rs-api @@ -126,6 +121,17 @@ jakarta.activation-api runtime + + + org.eclipse.jetty.toolchain + jetty-servlet-api + compile + org.eclipse.jetty jetty-server @@ -135,21 +141,15 @@ org.eclipse.jetty jetty-util compile - - - javax.servlet-api - javax.servlet - - - org.eclipse.jetty - jetty-servlet + org.eclipse.jetty.ee8 + jetty-ee8-servlet compile - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee8 + jetty-ee8-webapp compile diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/KMSClientProvider.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/KMSClientProvider.java index a3293620ab9e4b..04ec63ff3adf34 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/KMSClientProvider.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/crypto/key/kms/KMSClientProvider.java @@ -18,7 +18,6 @@ package org.apache.hadoop.crypto.key.kms; import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.crypto.key.KeyProvider; @@ -564,9 +563,7 @@ private T call(HttpURLConnection conn, Object jsonOutput, } if ((conn.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN - && (!StringUtils.isEmpty(conn.getResponseMessage()) - && (conn.getResponseMessage().equals(ANONYMOUS_REQUESTS_DISALLOWED) - || conn.getResponseMessage().contains(INVALID_SIGNATURE)))) + && isAuthenticationFailure(conn)) || conn.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { // Ideally, this should happen only when there is an Authentication // failure. Unfortunately, the AuthenticationFilter returns 403 when it @@ -607,6 +604,35 @@ private T call(HttpURLConnection conn, Object jsonOutput, return ret; } + /** + * Whether a 403 is the AuthenticationFilter refusing to authenticate the + * caller, rather than the KMS refusing to authorize an authenticated one. + * Only the former is worth resetting the token and retrying for. + *

+ * The filter reports why through + * {@link javax.servlet.http.HttpServletResponse#sendError}. That detail used + * to arrive in the HTTP reason phrase; since Jetty 12 the phrase is always + * the canonical text for the status code and the detail is in the body, so + * read it from there. An authorization denial arrives instead as the JSON + * envelope {@link HttpExceptionUtils} writes, which + * {@link HttpExceptionUtils#getResponseDetail} leaves untouched - so a + * denial neither matches here nor has its body consumed before + * {@link HttpExceptionUtils#validateResponse} below rebuilds the exception + * from it. + *

+ * Matching {@link #ANONYMOUS_REQUESTS_DISALLOWED} on a substring rather than + * on the whole value is forced by the move into the body: the container + * wraps the reason in an error page. + * + * @param conn a connection whose response status has been read + * @return true when the response names an authentication failure + */ + private static boolean isAuthenticationFailure(HttpURLConnection conn) { + String detail = HttpExceptionUtils.getResponseDetail(conn); + return detail.contains(ANONYMOUS_REQUESTS_DISALLOWED) + || detail.contains(INVALID_SIGNATURE); + } + public static class KMSKeyVersion extends KeyVersion { public KMSKeyVersion(String keyName, String versionName, byte[] material) { super(keyName, versionName, material); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/AdminAuthorizedServlet.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/AdminAuthorizedServlet.java index a4b05a1d534b85..8b17db7558f462 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/AdminAuthorizedServlet.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/AdminAuthorizedServlet.java @@ -23,7 +23,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.ee8.servlet.DefaultServlet; /** * General servlet which is admin-authorized. diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2.java index d17874d78aca82..3ff58b1b06eb1a 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2.java @@ -28,6 +28,7 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.nio.channels.ServerSocketChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -89,7 +90,17 @@ import org.apache.hadoop.util.ReflectionUtils; import org.apache.hadoop.util.Shell; import org.apache.hadoop.util.StringUtils; +import org.eclipse.jetty.ee8.nested.SessionHandler; +import org.eclipse.jetty.ee8.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.ee8.servlet.FilterHolder; +import org.eclipse.jetty.ee8.servlet.FilterMapping; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.ServletMapping; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.UriCompliance; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; @@ -102,21 +113,11 @@ import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker; import org.eclipse.jetty.server.handler.ContextHandlerCollection; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.StatisticsHandler; -import org.eclipse.jetty.server.session.SessionHandler; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.FilterMapping; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlet.ServletMapping; import org.eclipse.jetty.util.ArrayUtil; -import org.eclipse.jetty.util.MultiException; +import org.eclipse.jetty.util.ExceptionUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.webapp.WebAppContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -173,6 +174,19 @@ public final class HttpServer2 implements FilterContainer { = "hadoop.http.sni.host.check.enabled"; public static final boolean HTTP_SNI_HOST_CHECK_ENABLED_DEFAULT = false; + /** + * Prefix under which a context init parameter reaches Jetty's DefaultServlet. + *

+ * This is not the package the servlet lives in: ee8 moved the class to + * org.eclipse.jetty.ee8.servlet but DefaultServlet#getInitParameter still + * reads the 9.4 name. Spelling it the other way costs nothing at startup and + * silently drops the setting - dirAllowed then falls back to its default of + * true and /static starts listing directories - so the prefix is written + * once here rather than at each call site. + */ + private static final String DEFAULT_SERVLET_INIT_PREFIX = + "org.eclipse.jetty.servlet.Default."; + // The ServletContext attribute where the daemon Configuration // gets stored. public static final String CONF_CONTEXT_ATTRIBUTE = "hadoop.conf"; @@ -186,7 +200,7 @@ public final class HttpServer2 implements FilterContainer { protected final Server webServer; - private final HandlerCollection handlers; + private final Handler.Sequence handlers; private final List listeners = Lists.newArrayList(); @@ -538,6 +552,31 @@ public HttpServer2 build() throws IOException { httpConfig.setRequestHeaderSize(requestHeaderSize); httpConfig.setResponseHeaderSize(responseHeaderSize); httpConfig.setSendServerVersion(false); + // Jetty 12 rejects two kinds of path at the connector that 9.4 handed to + // the servlet, with a bare 400 and no body, so the request never reaches + // the code that knows what a Hadoop path is: + // + // - an empty segment, //tmp//file. WebHDFS takes those and answers with + // its own JSON RemoteException, which is what its clients parse. + // - an encoded percent, %25, which is how a file whose name contains a + // '%' is written on the wire. Names like that are ordinary in HDFS + // and YARN routes carry them too. + // - an encoded backslash, %5C. Jetty treats it as suspicious because + // it separates paths on Windows; on HDFS it is just a character in a + // name, and TestWebHdfsUrl creates files containing one. Measured + // rather than assumed: of every character in that test's filename, + // %5C is the only one this violation gates. + // + // All three are allowed back so that Hadoop keeps deciding what a path + // means. The ambiguities that let a request read as one path to a filter + // and another to a servlet stay rejected: an encoded separator (a%2Fb) + // and an encoded dot-segment (a%2E%2E%2Fb) are still refused, .. still + // cannot climb out of the context, and a%252Fb still decodes once, to + // the literal a%2Fb rather than to a separator. + httpConfig.setUriCompliance(UriCompliance.DEFAULT.with("hadoop", + UriCompliance.Violation.AMBIGUOUS_EMPTY_SEGMENT, + UriCompliance.Violation.AMBIGUOUS_PATH_ENCODING, + UriCompliance.Violation.SUSPICIOUS_PATH_CHARACTERS)); int backlogSize = conf.getInt(HTTP_SOCKET_BACKLOG_SIZE_KEY, HTTP_SOCKET_BACKLOG_SIZE_DEFAULT); @@ -599,7 +638,7 @@ HttpServer2 addConnectors( private ServerConnector createHttpChannelConnector( Server server, HttpConfiguration httpConfig) { - ServerConnector conn = new ServerConnector(server, + ServerConnector conn = new ReopenableServerConnector(server, conf.getInt(HTTP_ACCEPTOR_COUNT_KEY, HTTP_ACCEPTOR_COUNT_DEFAULT), conf.getInt(HTTP_SELECTOR_COUNT_KEY, HTTP_SELECTOR_COUNT_DEFAULT)); ConnectionFactory connFactory = new HttpConnectionFactory(httpConfig); @@ -739,7 +778,7 @@ private HttpServer2(final Builder b) throws IOException { final String appDir = getWebAppsPath(b.name); this.webServer = new Server(); this.adminsAcl = b.adminsAcl; - this.handlers = new HandlerCollection(); + this.handlers = new Handler.Sequence(); this.webAppContext = createWebAppContext(b, adminsAcl, appDir); this.xFrameOptionIsEnabled = b.xFrameEnabled; this.xFrameOption = b.xFrameOption; @@ -767,7 +806,22 @@ private void initializeWebServer(String name, String hostName, throws IOException { Preconditions.checkNotNull(webAppContext); - webAppContext.getErrorHandler().setShowStacks(LOG.isTraceEnabled()); + // Jetty only builds an error page for GET, HEAD and POST, so an error on a + // PUT or a DELETE goes back with no body at all. That did not show on 9.4 + // because the detail also travelled in the reason phrase, which Jetty 12 + // no longer puts on the wire, and losing both leaves a client with nothing + // but the status code. + // Subclasses the handler a WebAppContext installs for itself, so that + // and mappings from a webapp's web.xml keep + // working; a plain ErrorHandler here would silently drop them. + ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler() { + @Override + public boolean errorPageForMethod(String method) { + return true; + } + }; + errorHandler.setShowStacks(LOG.isTraceEnabled()); + webAppContext.setErrorHandler(errorHandler); int maxThreads = conf.getInt(HTTP_MAX_THREADS_KEY, -1); // If HTTP_MAX_THREADS is not configured, QueueThreadPool() will use the @@ -788,10 +842,13 @@ private void initializeWebServer(String name, String hostName, handlers.addHandler(contexts); if (requestLog != null) { - RequestLogHandler requestLogHandler = new RequestLogHandler(); - requestLogHandler.setRequestLog(requestLog); - handlers.addHandler(requestLogHandler); + // Jetty 12 removed RequestLogHandler. A request log is now a property of + // the server and covers every request it handles, which is what wrapping + // the whole handler list amounted to before. + webServer.setRequestLog(requestLog); } + // An ee8 context is not a core Handler; it supplies one. Handler.Collection + // takes the Supplier directly and adds the core handler behind it. handlers.addHandler(webAppContext); final String appDir = getWebAppsPath(name); addDefaultApps(contexts, appDir, conf); @@ -804,9 +861,8 @@ private void initializeWebServer(String name, String hostName, // The tree might look like this: // // - StatisticsHandler (for all requests) - // - HandlerList + // - Handler.Sequence // - ContextHandlerCollection - // - RequestLogHandler (if enabled) // - WebAppContext // - SessionHandler // - Servlets @@ -971,6 +1027,16 @@ protected void addDefaultApps(ContextHandlerCollection parent, boolean logsEnabled = conf.getBoolean( CommonConfigurationKeys.HADOOP_HTTP_LOGS_ENABLED, CommonConfigurationKeys.HADOOP_HTTP_LOGS_ENABLED_DEFAULT); + // Jetty 9.4 let a context start on a base resource that was not there and + // served 404s from it. Jetty 12 rejects it in ContextHandler#doStart, which + // turns a missing log directory into a server that will not come up at all, + // so the context is left out rather than allowed to fail the daemon. The + // endpoint answers 404 either way. + if (logDir != null && logsEnabled && !new File(logDir).isDirectory()) { + LOG.warn("Not adding the /logs context: hadoop.log.dir is set to {}," + + " which is not a directory.", logDir); + logsEnabled = false; + } if (logDir != null && logsEnabled) { ServletContextHandler logContext = new ServletContextHandler(parent, "/logs"); @@ -981,14 +1047,15 @@ protected void addDefaultApps(ContextHandlerCollection parent, CommonConfigurationKeys.DEFAULT_HADOOP_JETTY_LOGS_SERVE_ALIASES)) { @SuppressWarnings("unchecked") Map params = logContext.getInitParams(); - params.put("org.eclipse.jetty.servlet.Default.aliases", "true"); + params.put(DEFAULT_SERVLET_INIT_PREFIX + "aliases", "true"); } logContext.setDisplayName("logs"); SessionHandler handler = new SessionHandler(); handler.setHttpOnly(true); handler.getSessionCookieConfig().setSecure(true); logContext.setSessionHandler(handler); - logContext.addAliasCheck(new SymlinkAllowedResourceAliasChecker(logContext)); + logContext.addAliasCheck( + new SymlinkAllowedResourceAliasChecker(logContext.getCoreContextHandler())); setContextAttributes(logContext, conf); addNoCacheFilter(logContext); defaultContexts.put(logContext, true); @@ -1001,13 +1068,14 @@ protected void addDefaultApps(ContextHandlerCollection parent, staticContext.setDisplayName("static"); @SuppressWarnings("unchecked") Map params = staticContext.getInitParams(); - params.put("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); - params.put("org.eclipse.jetty.servlet.Default.gzip", "true"); + params.put(DEFAULT_SERVLET_INIT_PREFIX + "dirAllowed", "false"); + params.put(DEFAULT_SERVLET_INIT_PREFIX + "gzip", "true"); SessionHandler handler = new SessionHandler(); handler.setHttpOnly(true); handler.getSessionCookieConfig().setSecure(true); staticContext.setSessionHandler(handler); - staticContext.addAliasCheck(new SymlinkAllowedResourceAliasChecker(staticContext)); + staticContext.addAliasCheck(new SymlinkAllowedResourceAliasChecker( + staticContext.getCoreContextHandler())); setContextAttributes(staticContext, conf); defaultContexts.put(staticContext, true); } @@ -1208,8 +1276,8 @@ public void addInternalServlet(String name, String pathSpec, * @param handler The handler to add */ public void addHandlerAtFront(Handler handler) { - Handler[] h = ArrayUtil.prependToArray( - handler, this.handlers.getHandlers(), Handler.class); + List h = new ArrayList<>(this.handlers.getHandlers()); + h.add(0, handler); handlers.setHandlers(h); } @@ -1464,13 +1532,9 @@ public void start() throws IOException { } catch (IOException ex) { LOG.info("HttpServer.start() threw a non Bind IOException", ex); throw ex; - } catch (MultiException ex) { - LOG.info("HttpServer.start() threw a MultiException", ex); - throw ex; } // Make sure there is no handler failures. - Handler[] hs = webServer.getHandlers(); - for (Handler handler : hs) { + for (Handler handler : webServer.getHandlers()) { if (handler.isFailed()) { throw new IOException( "Problem in starting http server. Server handlers failed"); @@ -1511,9 +1575,75 @@ private static void bindListener(ServerConnector listener) throws Exception { // failed to open w/o issuing a close first, even if the port is changed listener.close(); listener.open(); + if (listener.getLocalPort() < 0) { + // open() came back without an exception and without a socket. That is + // not a port conflict, so it must not be retried on the next port: the + // retry would no-op in exactly the same way, forever. See + // ReopenableServerConnector for the one case that used to get here. + throw new IllegalStateException( + "Jetty reported no error but did not bind " + listener); + } LOG.info("Jetty bound to port " + listener.getLocalPort()); } + /** + * A ServerConnector whose listen socket can be reopened after it is closed. + *

+ * Jetty 9.4's ServerConnector#close released the accept channel as well as + * closing it, so a close/open pair rebound the same port. Jetty 12 closes + * the channel but keeps the reference, and ServerConnector#open does nothing + * at all while that reference is set - it neither rebinds nor complains, and + * getLocalPort stays at -2. Only doStop clears it. + *

+ * That is enough for a server that is started, because stopping it clears + * the channel on the way down. It is not enough here: openListeners binds + * before the Server is started, so a caller that opens listeners, stops, and + * opens them again gets a connector that silently never comes back. + *

+ * The public open(ServerSocketChannel) overload assigns the channel + * unconditionally, so handing it a fresh one restores the 9.4 behaviour + * without reaching into Jetty's internals. That is done only after Jetty's + * own open() has been given the first go and turned out to be a no-op, so + * the ordinary first bind runs the stock path unchanged; the replacement + * path does not fire the connector's open callbacks, which is a step up + * from the nothing it does today and the reason to keep it to that one + * case. + */ + private static final class ReopenableServerConnector extends ServerConnector { + + private ReopenableServerConnector(Server server, int acceptors, + int selectors) { + super(server, acceptors, selectors); + } + + @Override + public void open() throws IOException { + // Always give Jetty the first go, so a connector that has never been + // opened takes the stock path and nothing - the open callbacks it fires + // among them - is skipped. + super.open(); + if (isStarted() || getLocalPort() != -2 || isOpen()) { + return; + } + // super.open() returned having bound nothing: the connector is holding + // a channel it closed. isOpen() being false is what says the channel is + // spent - Jetty only closes it from close() when the connector has + // acceptors, and binding a second channel to a port the first one still + // holds would fail and leak the first. + ServerSocketChannel channel = openAcceptChannel(); + try { + open(channel); + } catch (Throwable t) { + try { + channel.close(); + } catch (IOException suppressed) { + t.addSuppressed(suppressed); + } + throw t; + } + } + } + /** * Create bind exception by wrapping the bind exception thrown. * @param listener @@ -1619,7 +1749,7 @@ void openListeners() throws Exception { * @throws Exception exception. */ public void stop() throws Exception { - MultiException exception = null; + ExceptionUtil.MultiException exception = null; if (this.configurationChangeMonitor.isPresent()) { try { this.configurationChangeMonitor.get().cancel(); @@ -1670,9 +1800,10 @@ public void stop() throws Exception { } - private MultiException addMultiException(MultiException exception, Exception e) { + private ExceptionUtil.MultiException addMultiException( + ExceptionUtil.MultiException exception, Exception e) { if(exception == null){ - exception = new MultiException(); + exception = new ExceptionUtil.MultiException(); } exception.add(e); return exception; @@ -1960,9 +2091,7 @@ public void doFilter(ServletRequest request, */ private String inferMimeType(ServletRequest request) { String path = ((HttpServletRequest)request).getRequestURI(); - ServletContextHandler.Context sContext = - (ServletContextHandler.Context)config.getServletContext(); - String mime = sContext.getMimeType(path); + String mime = config.getServletContext().getMimeType(path); return (mime == null) ? null : mime; } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2Metrics.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2Metrics.java index f99476fc4d148d..6cdb3d5b0f0737 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2Metrics.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/HttpServer2Metrics.java @@ -30,6 +30,14 @@ /** * This class collects all the metrics of Jetty's StatisticsHandler * and expose them as Hadoop Metrics. + * + * Jetty 12 rebuilt StatisticsHandler around the core request lifecycle. The + * dispatch counters it used to publish are now handle counters measuring the + * same thing under a different name, and are read as such here so the metric + * names Hadoop emits do not move. Its four async counters and its count of + * expired async requests have no counterpart, because the core no longer sees + * servlet async activity, so those five metrics are no longer emitted rather + * than reported as a constant. */ @InterfaceAudience.Private @InterfaceStability.Unstable @@ -42,53 +50,33 @@ public class HttpServer2Metrics { private final int acceptorThreads; private final int selectorThreads; - @Metric("number of requested that have been asynchronously dispatched") - public int asyncDispatches() { - return handler.getAsyncDispatches(); - } - @Metric("total number of async requests") - public int asyncRequests() { - return handler.getAsyncRequests(); - } - @Metric("currently waiting async requests") - public int asyncRequestsWaiting() { - return handler.getAsyncRequestsWaiting(); - } - @Metric("maximum number of waiting async requests") - public int asyncRequestsWaitingMax() { - return handler.getAsyncRequestsWaitingMax(); - } @Metric("number of dispatches") public int dispatched() { - return handler.getDispatched(); + return handler.getHandleTotal(); } @Metric("number of dispatches currently active") public int dispatchedActive() { - return handler.getDispatchedActive(); + return handler.getHandleActive(); } @Metric("maximum number of active dispatches being handled") public int dispatchedActiveMax() { - return handler.getDispatchedActiveMax(); + return handler.getHandleActiveMax(); } @Metric("maximum time spend in dispatch handling (in ms)") public long dispatchedTimeMax() { - return handler.getDispatchedTimeMax(); + return handler.getHandleTimeMax(); } @Metric("mean time spent in dispatch handling (in ms)") public double dispatchedTimeMean() { - return handler.getDispatchedTimeMean(); + return handler.getHandleTimeMean(); } @Metric("standard deviation for dispatch handling (in ms)") public double dispatchedTimeStdDev() { - return handler.getDispatchedTimeStdDev(); + return handler.getHandleTimeStdDev(); } @Metric("total time spent in dispatch handling (in ms)") public long dispatchedTimeTotal() { - return handler.getDispatchedTimeTotal(); - } - @Metric("number of async requests requests that have expired") - public int expires() { - return handler.getExpires(); + return handler.getHandleTimeTotal(); } @Metric("number of requests") public int requests() { @@ -140,11 +128,11 @@ public int responses5xx() { } @Metric("total number of bytes across all responses") public long responsesBytesTotal() { - return handler.getResponsesBytesTotal(); + return handler.getBytesWritten(); } @Metric("time in milliseconds stats have been collected for") public long statsOnMs() { - return handler.getStatsOnMs(); + return handler.getStatisticsDuration().toMillis(); } @Metric("maximum number of threads in the pool") public int maxThreads() { diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/JettyUtils.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/JettyUtils.java index 29c0930968ede7..09b08d411725ea 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/JettyUtils.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/JettyUtils.java @@ -18,6 +18,8 @@ package org.apache.hadoop.http; +import javax.servlet.http.HttpServletResponse; + import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -30,6 +32,26 @@ public final class JettyUtils { public static final String UTF_8 = "charset=utf-8"; public static final int HEADER_SIZE = 1024 * 64; + /** + * Clears the content type a response is carrying, charset included. + *

+ * Every server built by {@link HttpServer2} runs + * {@code QuotingInputFilter}, which sets {@code text/plain; charset=utf-8} + * before the request reaches the resource. A JAX-RS resource that picks its + * own content type has to undo that first, and {@code setContentType(null)} + * alone is not enough on Jetty 12: it drops the charset but remembers that + * one had been set explicitly, so the next content type gets that memory + * appended to it - literally {@code ;charset=null} for a type that carries + * no charset of its own, such as {@code application/octet-stream} or + * {@code application/xml}. Clearing the encoding as well resets that state. + * + * @param response the response to clear + */ + public static void clearContentType(HttpServletResponse response) { + response.setContentType(null); + response.setCharacterEncoding(null); + } + private JettyUtils() { } } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/ProfileOutputServlet.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/ProfileOutputServlet.java index 1ecc21f3753ceb..3c69eca6e6ae2f 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/ProfileOutputServlet.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/ProfileOutputServlet.java @@ -25,7 +25,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.ee8.servlet.DefaultServlet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/WebServlet.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/WebServlet.java index 2eb6c2beb16a62..765709625e5110 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/WebServlet.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/WebServlet.java @@ -22,7 +22,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.ee8.servlet.DefaultServlet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/RestCsrfPreventionFilter.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/RestCsrfPreventionFilter.java index 7363ca0ba64505..7c5da6fbd268ff 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/RestCsrfPreventionFilter.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/http/RestCsrfPreventionFilter.java @@ -36,8 +36,8 @@ import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.util.HttpExceptionUtils; -import org.eclipse.jetty.server.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -272,11 +272,21 @@ public void proceed() throws IOException, ServletException { @Override public void sendError(int code, String message) throws IOException { - if (httpResponse instanceof Response) { - ((Response)httpResponse).setStatusWithReason(code, message); - } - - httpResponse.sendError(code, message); + // Reported as the JSON envelope HttpExceptionUtils writes, rather than + // handed to sendError for the container to render. + // + // The message used to travel in the reason phrase. Jetty 12 does not + // send one, and what sendError leaves in its place is the container's + // HTML error page - which WebHdfsFileSystem refuses on its content type + // and reports as a bare "Bad Request", losing the one thing the caller + // needed to know. The envelope is what the rest of Hadoop's HTTP + // surface already answers refusals with, and what its clients parse. + // + // Only the servlet side moves. The two Netty implementations of + // HttpInteraction that serve the DataNode build their own responses and + // never went through a reason phrase. + HttpExceptionUtils.createServletExceptionResponse(httpResponse, code, + new IOException(message)); } } } diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/DelegationTokenAuthenticationHandler.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/DelegationTokenAuthenticationHandler.java index f4ede6f35edb0c..29e311739e18f4 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/DelegationTokenAuthenticationHandler.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/token/delegation/web/DelegationTokenAuthenticationHandler.java @@ -302,7 +302,15 @@ public boolean managementOperation(AuthenticationToken token, requestUgi.getShortUserName()); map = Collections.singletonMap("long", expirationTime); } catch (IOException ex) { - throw new AuthenticationException(ex.toString(), ex); + // Reported the same way as the authorization failure above, + // rather than thrown for AuthenticationFilter to turn into a + // bare status. Letting it out used to be survivable because + // the reason phrase carried the detail; Jetty 12 does not + // send one, and the container's error page is HTML, which is + // not what the client on the other end parses. + HttpExceptionUtils.createServletExceptionResponse(response, + HttpServletResponse.SC_FORBIDDEN, ex); + requestContinues = false; } } break; diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HttpExceptionUtils.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HttpExceptionUtils.java index 43441a5560a338..b4e4fd6de30060 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HttpExceptionUtils.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/HttpExceptionUtils.java @@ -23,13 +23,17 @@ import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.io.BufferedInputStream; +import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Writer; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -134,6 +138,11 @@ private static void throwException(Throwable ex) *

* NOTE: this method will throw the deserialized exception even if not * declared in the throws of the method signature. + *

+ * When the response does not carry the JSON envelope - a container error + * page, say - the detail is taken from the body via + * {@link #getResponseDetail}, because that is where a servlet's reason now + * is: Jetty 12 no longer puts one in the HTTP reason phrase. * * @param conn the HttpURLConnection. * @param expectedStatus the expected HTTP status code. @@ -147,8 +156,12 @@ public static void validateResponse(HttpURLConnection conn, Exception toThrow; InputStream es = null; try { - es = conn.getErrorStream(); - Map json = JsonSerialization.mapReader().readValue(es); + InputStream raw = conn.getErrorStream(); + if (raw != null) { + es = new BufferedInputStream(raw); + es.mark(ERROR_BODY_REWIND_LIMIT); + } + Map json = JsonSerialization.mapReader().readValue(shielded(es)); json = (Map) json.get(ERROR_JSON); String exClass = (String) json.get(ERROR_CLASSNAME_JSON); String exMsg = (String) json.get(ERROR_MESSAGE_JSON); @@ -175,7 +188,7 @@ public static void validateResponse(HttpURLConnection conn, } catch (Exception ex) { toThrow = new IOException(String.format( "HTTP status [%d], message [%s], URL [%s], exception [%s]", - conn.getResponseCode(), conn.getResponseMessage(), conn.getURL(), + conn.getResponseCode(), rewoundDetail(es, conn), conn.getURL(), ex.toString()), ex); } finally { if (es != null) { @@ -190,4 +203,157 @@ public static void validateResponse(HttpURLConnection conn, } } + /** How much of a failed response body is worth quoting back. */ + private static final int MAX_RESPONSE_DETAIL_CHARS = 4096; + + /** + * How much of an error body {@link #validateResponse} keeps buffered so it + * can be rewound and read as text once the JSON parse has failed. Sized to + * hold {@link #MAX_RESPONSE_DETAIL_CHARS} characters of any UTF-8 body. A + * body longer than this still parses - the reader runs straight through it - + * it just cannot be rewound, which leaves the reason phrase as the fallback, + * as it was before. + */ + private static final int ERROR_BODY_REWIND_LIMIT = + 4 * MAX_RESPONSE_DETAIL_CHARS; + + /** + * Describes why a request failed, preferring the response body over the HTTP + * reason phrase. + *

+ * A servlet reports its reason through + * {@link HttpServletResponse#sendError}, and that detail used to reach the + * caller in the reason phrase, which + * {@link HttpURLConnection#getResponseMessage()} returns. Jetty 12 never + * puts a reason phrase on the wire: the phrase is now always the canonical + * text for the status code - "Forbidden", "Gone" - and the detail is in the + * body instead. Read the body, and fall back to the phrase when there is + * none. + *

+ * For a response that carries the JSON envelope this class writes, prefer + * {@link #validateResponse}, which rebuilds the original exception. This is + * for everything else: a container's error page, or a plain-text reason. + * A JSON body is therefore left alone here and the phrase reported instead: + * the envelope is sent with {@code setStatus} rather than sendError, so its + * phrase was the canonical text for the status code before Jetty 12 and + * still is, and quoting the envelope back as free text would replace a + * readable "Forbidden" with a line of JSON. + * + * @param conn a connection whose response status has been read + * @return a description of the failure, never null + */ + public static String getResponseDetail(HttpURLConnection conn) { + String body = ""; + if (!isJson(conn.getContentType())) { + try (InputStream es = conn.getErrorStream()) { + if (es != null) { + body = toPlainText(readCapped(es)); + } + } catch (IOException ex) { + // nothing to add: fall through to the reason phrase + } + } + if (!body.isEmpty()) { + return body; + } + return responsePhrase(conn); + } + + /** + * Describes a failure whose body has already been read - and failed - as the + * JSON envelope. The body is the only place a servlet's reason can be now, + * so rewind and read it as text. The envelope guard {@link + * #getResponseDetail} applies does not belong here: nothing that parsed as + * the envelope reaches this point, so there is no envelope to protect. + * + * @param es the buffered error stream, marked at its start, or null + * @param conn the connection it came from + * @return a description of the failure, never null + */ + private static String rewoundDetail(InputStream es, HttpURLConnection conn) { + if (es != null) { + try { + es.reset(); + String body = toPlainText(readCapped(es)); + if (!body.isEmpty()) { + return body; + } + } catch (IOException ex) { + // read too far to rewind: fall through to the reason phrase + } + } + return responsePhrase(conn); + } + + /** + * The HTTP reason phrase, or "" when there is none. Since Jetty 12 this is + * always the canonical text for the status code. + */ + private static String responsePhrase(HttpURLConnection conn) { + try { + String phrase = conn.getResponseMessage(); + return phrase == null ? "" : phrase; + } catch (IOException ex) { + return ""; + } + } + + /** + * Hides {@link InputStream#close()} from a reader that would otherwise close + * the stream on its way out. The JSON reader closes its source even when the + * parse failed, and a closed stream can no longer be rewound and read as + * text. The caller keeps ownership and closes the real stream itself. + */ + private static InputStream shielded(InputStream in) { + if (in == null) { + return null; + } + return new FilterInputStream(in) { + @Override + public void close() { + // the caller owns the stream + } + }; + } + + /** + * Whether the content type names the JSON error envelope. The header can + * carry parameters - "application/json; charset=utf-8" - so this matches a + * prefix rather than the whole value. + */ + private static boolean isJson(String contentType) { + return contentType != null + && contentType.trim().toLowerCase().startsWith(APPLICATION_JSON_MIME); + } + + private static String readCapped(InputStream in) throws IOException { + InputStreamReader reader = + new InputStreamReader(in, StandardCharsets.UTF_8); + StringBuilder sb = new StringBuilder(); + char[] buf = new char[1024]; + int n; + while (sb.length() < MAX_RESPONSE_DETAIL_CHARS + && (n = reader.read(buf)) != -1) { + sb.append(buf, 0, Math.min(n, MAX_RESPONSE_DETAIL_CHARS - sb.length())); + } + return sb.toString(); + } + + /** + * Reduces a response body to something readable in a log line. A container + * that renders sendError as an HTML page buries the message in markup; strip + * it out rather than quoting the page. + */ + private static String toPlainText(String body) { + String text = body; + if (text.indexOf('<') >= 0) { + text = text.replaceAll("(?s)<(script|style)\\b.*?", " ") + .replaceAll("(?s)<[^>]*>", " "); + } + text = text.replace("<", "<").replace(">", ">") + .replace(""", "\"").replace("'", "'") + .replace("&", "&"); + return text.replaceAll("\\s+", " ").trim(); + } + } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/FSMainOperationsBaseTest.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/FSMainOperationsBaseTest.java index be1baa97c88109..a1b67a697078bb 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/FSMainOperationsBaseTest.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/FSMainOperationsBaseTest.java @@ -36,7 +36,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.eclipse.jetty.util.log.Log; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

@@ -58,6 +59,9 @@ *

*/ public abstract class FSMainOperationsBaseTest extends FileSystemTestHelper { + private static final Logger LOG = + LoggerFactory.getLogger(FSMainOperationsBaseTest.class); + private static String TEST_DIR_AAA2 = "test/hadoop2/aaa"; private static String TEST_DIR_AAA = "test/hadoop/aaa"; @@ -803,7 +807,7 @@ public void testRenameNonExistentPath() throws Exception { rename(src, dst, false, false, false, Rename.NONE); fail("Should throw FileNotFoundException"); } catch (IOException e) { - Log.getLog().info("XXX", e); + LOG.info("XXX", e); assertTrue(unwrapException(e) instanceof FileNotFoundException); } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFileSystemTestSetup.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFileSystemTestSetup.java index 5713f532be7e81..6482dc5936a6bd 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFileSystemTestSetup.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFileSystemTestSetup.java @@ -26,7 +26,8 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.viewfs.ConfigUtil; import org.apache.hadoop.util.Shell; -import org.eclipse.jetty.util.log.Log; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** @@ -46,6 +47,9 @@ * We also set the view file system's wd to point to the wd. */ public class ViewFileSystemTestSetup { + private static final Logger LOG = + LoggerFactory.getLogger(ViewFileSystemTestSetup.class); + static public String ViewFSTestDir = "/testDir"; @@ -83,7 +87,7 @@ static public FileSystem setupForViewFileSystem(Configuration conf, FileSystemTe FileSystem fsView = FileSystem.get(FsConstants.VIEWFS_URI, conf); fsView.setWorkingDirectory(new Path(wdDir)); // in case testdir relative to wd. - Log.getLog().info("Working dir is: " + fsView.getWorkingDirectory()); + LOG.info("Working dir is: " + fsView.getWorkingDirectory()); return fsView; } @@ -117,12 +121,12 @@ static void setUpHomeDir(Configuration conf, FileSystem fsTarget) { } else { // home dir is at root. Just link the home dir itse URI linkTarget = fsTarget.makeQualified(new Path(homeDir)).toUri(); ConfigUtil.addLink(conf, homeDir, linkTarget); - Log.getLog().info("Added link for home dir " + homeDir + "->" + linkTarget); + LOG.info("Added link for home dir " + homeDir + "->" + linkTarget); } // Now set the root of the home dir for viewfs String homeDirRoot = fsTarget.getHomeDirectory().getParent().toUri().getPath(); ConfigUtil.setHomeDirConf(conf, homeDirRoot); - Log.getLog().info("Home dir base for viewfs" + homeDirRoot); + LOG.info("Home dir base for viewfs" + homeDirRoot); } /* @@ -137,7 +141,7 @@ static void linkUpFirstComponents(Configuration conf, String path, FileSystem fs String firstComponent = path.substring(0, indexOfEnd); URI linkTarget = fsTarget.makeQualified(new Path(firstComponent)).toUri(); ConfigUtil.addLink(conf, firstComponent, linkTarget); - Log.getLog().info("Added link for " + info + " " + LOG.info("Added link for " + info + " " + firstComponent + "->" + linkTarget); } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFsTestSetup.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFsTestSetup.java index 8f9ac895946e0d..688825ac89ce67 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFsTestSetup.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/viewfs/ViewFsTestSetup.java @@ -32,7 +32,8 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.viewfs.ViewFileSystemOverloadScheme.ChildFsGetter; import org.apache.hadoop.util.Shell; -import org.eclipse.jetty.util.log.Log; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** @@ -53,6 +54,9 @@ */ public class ViewFsTestSetup { + private static final Logger LOG = + LoggerFactory.getLogger(ViewFsTestSetup.class); + static public String ViewFSTestDir = "/testDir"; @@ -88,7 +92,7 @@ static public FileContext setupForViewFsLocalFs(FileContextTestHelper helper) th FileContext fc = FileContext.getFileContext(FsConstants.VIEWFS_URI, conf); fc.setWorkingDirectory(new Path(wdDir)); // in case testdir relative to wd. - Log.getLog().info("Working dir is: " + fc.getWorkingDirectory()); + LOG.info("Working dir is: " + fc.getWorkingDirectory()); //System.out.println("SRCOfTests = "+ getTestRootPath(fc, "test")); //System.out.println("TargetOfTests = "+ targetOfTests.toUri()); return fc; @@ -113,12 +117,12 @@ static void setUpHomeDir(Configuration conf, FileContext fsTarget) { } else { // home dir is at root. Just link the home dir itse URI linkTarget = fsTarget.makeQualified(new Path(homeDir)).toUri(); ConfigUtil.addLink(conf, homeDir, linkTarget); - Log.getLog().info("Added link for home dir " + homeDir + "->" + linkTarget); + LOG.info("Added link for home dir " + homeDir + "->" + linkTarget); } // Now set the root of the home dir for viewfs String homeDirRoot = fsTarget.getHomeDirectory().getParent().toUri().getPath(); ConfigUtil.setHomeDirConf(conf, homeDirRoot); - Log.getLog().info("Home dir base for viewfs" + homeDirRoot); + LOG.info("Home dir base for viewfs" + homeDirRoot); } /* @@ -134,7 +138,7 @@ static void linkUpFirstComponents(Configuration conf, String path, String firstComponent = path.substring(0, indexOfEnd); URI linkTarget = fsTarget.makeQualified(new Path(firstComponent)).toUri(); ConfigUtil.addLink(conf, firstComponent, linkTarget); - Log.getLog().info("Added link for " + info + " " + LOG.info("Added link for " + info + " " + firstComponent + "->" + linkTarget); } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestAuthenticationSessionCookie.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestAuthenticationSessionCookie.java index 545b273701fcf0..79137f25ab9e24 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestAuthenticationSessionCookie.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestAuthenticationSessionCookie.java @@ -21,7 +21,6 @@ import org.apache.hadoop.test.GenericTestUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.eclipse.jetty.util.log.Log; import javax.servlet.*; import javax.servlet.http.HttpServletResponse; @@ -37,8 +36,13 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class TestAuthenticationSessionCookie { + private static final Logger LOG = + LoggerFactory.getLogger(TestAuthenticationSessionCookie.class); + private static final String BASEDIR = GenericTestUtils.getTempPath(TestHttpCookieFlag.class.getSimpleName()); private static boolean isCookiePersistent; @@ -152,7 +156,7 @@ public void testSessionCookie() throws IOException { String header = conn.getHeaderField("Set-Cookie"); List cookies = HttpCookie.parse(header); assertTrue(!cookies.isEmpty()); - Log.getLog().info(header); + LOG.info(header); assertFalse(header.contains("; Expires=")); assertTrue("token".equals(cookies.get(0).getValue())); } @@ -174,7 +178,7 @@ public void testPersistentCookie() throws IOException { String header = conn.getHeaderField("Set-Cookie"); List cookies = HttpCookie.parse(header); assertTrue(!cookies.isEmpty()); - Log.getLog().info(header); + LOG.info(header); assertTrue(header.contains("; Expires=")); assertTrue("token".equals(cookies.get(0).getValue())); } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestDownstreamServletCompatibility.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestDownstreamServletCompatibility.java new file mode 100644 index 00000000000000..ea8e238d3f5ba1 --- /dev/null +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestDownstreamServletCompatibility.java @@ -0,0 +1,422 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.hadoop.http; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Properties; +import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.security.authentication.server.AuthenticationHandler; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Guards the contract Hadoop offers the projects that embed it - HBase, Hive, + * Spark, Ozone, Knox and the rest - across a Jetty upgrade. + * + * Those projects implement Hadoop's servlet-facing extension points and embed + * HttpServer2, and they do it against javax.servlet. Two things have to stay + * true for them, and neither is checked by anything else in the tree: + * + * - the extension points keep speaking javax.servlet, never jakarta.servlet, + * - Jetty stays out of the signatures, so nobody downstream has to compile + * against the container Hadoop happens to embed, or care which one it is. + * + * The reflective half of this test would have failed had the Jetty 12 port let + * an org.eclipse.jetty type into a public signature, and will fail on the day + * the jakarta rename reaches one, which is the point: that day belongs to a + * major release and should not arrive by accident. + */ +public class TestDownstreamServletCompatibility + extends HttpServerFunctionalTest { + + /** + * The servlet-facing surface a downstream project compiles against. Types + * are named rather than discovered so that dropping one from the list is a + * deliberate edit and shows up in review. + */ + private static final Class[] PUBLIC_SURFACE = { + org.apache.hadoop.security.authentication.server.AuthenticationHandler.class, + org.apache.hadoop.security.authentication.server.AuthenticationFilter.class, + org.apache.hadoop.security.authentication.server.AuthenticationToken.class, + org.apache.hadoop.security.authentication.client.Authenticator.class, + org.apache.hadoop.security.http.RestCsrfPreventionFilter.class, + org.apache.hadoop.security.http.XFrameOptionsFilter.class, + org.apache.hadoop.http.FilterContainer.class, + org.apache.hadoop.http.FilterInitializer.class, + org.apache.hadoop.http.HttpServer2.class, + }; + + private static final String JAKARTA_SERVLET = "jakarta.servlet"; + private static final String JETTY = "org.eclipse.jetty"; + + /** + * How many of PUBLIC_SURFACE are not InterfaceAudience.Private, and so are + * held to the no-Jetty rule. Asserted as an exact count so that marking one + * of them Private has to be a deliberate edit here too. + */ + private static final int EXPECTED_JETTY_FREE_CLASSES = 7; + + /** + * Nothing on that surface may name a jakarta.servlet type. This is the + * promise the ee8 environment exists to keep, and the one that would break + * every downstream implementation at once. + */ + @Test + public void testPublicSurfaceNeverNamesJakartaServlet() { + List offences = new ArrayList<>(); + int javaxReferences = 0; + for (Class clazz : PUBLIC_SURFACE) { + for (String type : referencedTypes(clazz)) { + if (type.startsWith(JAKARTA_SERVLET)) { + offences.add(clazz.getName() + " exposes " + type + + "; downstream code is written against javax.servlet"); + } + if (type.startsWith("javax.servlet")) { + javaxReferences++; + } + } + } + // Without this the test would also pass if referencedTypes stopped + // reporting servlet types at all, which is the failure it exists to catch. + assertTrue(javaxReferences > 0, + "no javax.servlet type was found anywhere on the surface; the scan is" + + " not reading signatures and this test proves nothing"); + assertTrue(offences.isEmpty(), + "public API has moved to the jakarta namespace:\n " + + String.join("\n ", offences)); + } + + /** + * The classes downstream projects are expected to reach for on that surface + * - everything not marked InterfaceAudience.Private - may not name a Jetty + * type, so that nobody downstream has to compile against the container + * Hadoop embeds, or track which one it is. + *

+ * Absence of an annotation is not a licence to leak. Several of these are + * the oldest classes in hadoop-auth and carry no audience marker at all, + * which does not make them less public in practice; only an explicit + * Private marker excuses a class, and only two here have one - + * AuthenticationFilter, and HttpServer2, which names Jetty deliberately + * (addHandlerAtFront takes a Handler) and is documented as internal. + */ + @Test + public void testPublicApiNeverNamesJetty() { + List offences = new ArrayList<>(); + int checked = 0; + for (Class clazz : PUBLIC_SURFACE) { + if (clazz.getAnnotation(InterfaceAudience.Private.class) != null) { + continue; + } + checked++; + for (String type : referencedTypes(clazz)) { + if (type.startsWith(JETTY)) { + offences.add(clazz.getName() + " exposes " + type + + "; the embedded container must not reach a public signature"); + } + } + } + // A count, not a floor: marking one of these Private would shrink the + // guarded set silently, and that is a decision for review, not a + // side effect. + assertEquals(EXPECTED_JETTY_FREE_CLASSES, checked, + "the set of classes guarded against naming Jetty has changed; if that" + + " is deliberate, update EXPECTED_JETTY_FREE_CLASSES to match"); + assertTrue(offences.isEmpty(), + "public API leaks the servlet container:\n " + + String.join("\n ", offences)); + } + + /** + * Every type named by a public or protected member of the class, and of the + * nested classes it publishes. HttpServer2.Builder is the reason for the + * second half: it is how a downstream project actually configures an + * embedded server, and scanning only the outer class would miss it. + */ + private static Set referencedTypes(Class clazz) { + Set types = declaredTypes(clazz); + for (Class nested : clazz.getDeclaredClasses()) { + if (isVisibleDownstream(nested.getModifiers())) { + types.addAll(declaredTypes(nested)); + } + } + return types; + } + + /** Every type named by a public or protected member declared on the class. */ + private static Set declaredTypes(Class clazz) { + Set types = new LinkedHashSet<>(); + for (Method m : clazz.getDeclaredMethods()) { + if (!isVisibleDownstream(m.getModifiers())) { + continue; + } + collect(types, m.getGenericReturnType()); + for (Type t : m.getGenericParameterTypes()) { + collect(types, t); + } + for (Class t : m.getExceptionTypes()) { + collect(types, t); + } + } + for (Constructor c : clazz.getDeclaredConstructors()) { + if (!isVisibleDownstream(c.getModifiers())) { + continue; + } + for (Type t : c.getGenericParameterTypes()) { + collect(types, t); + } + } + for (Field f : clazz.getDeclaredFields()) { + if (isVisibleDownstream(f.getModifiers())) { + collect(types, f.getGenericType()); + } + } + return types; + } + + private static boolean isVisibleDownstream(int modifiers) { + return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers); + } + + private static void collect(Set into, Type type) { + if (type == null) { + return; + } + if (type instanceof Class) { + Class c = (Class) type; + while (c.isArray()) { + c = c.getComponentType(); + } + if (!c.isPrimitive()) { + into.add(c.getName()); + } + return; + } + if (type instanceof java.lang.reflect.ParameterizedType) { + java.lang.reflect.ParameterizedType p = + (java.lang.reflect.ParameterizedType) type; + collect(into, p.getRawType()); + for (Type arg : p.getActualTypeArguments()) { + collect(into, arg); + } + return; + } + if (type instanceof java.lang.reflect.GenericArrayType) { + collect(into, + ((java.lang.reflect.GenericArrayType) type).getGenericComponentType()); + } + } + + /** + * An AuthenticationHandler written the way a downstream project writes one: + * javax.servlet types, no Hadoop-internal or Jetty types anywhere. That this + * class compiles at all is most of the assertion. + */ + public static class DownstreamAuthenticationHandler + implements AuthenticationHandler { + + static final String TYPE = "downstream"; + private boolean initialised; + + @Override + public String getType() { + return TYPE; + } + + @Override + public void init(Properties config) throws ServletException { + initialised = true; + } + + @Override + public void destroy() { + initialised = false; + } + + @Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, HttpServletResponse response) { + return true; + } + + @Override + public AuthenticationToken authenticate(HttpServletRequest request, + HttpServletResponse response) { + if (!initialised) { + return null; + } + return new AuthenticationToken(request.getParameter("user"), "p", TYPE); + } + } + + /** + * The extension point still takes and returns javax.servlet types at run + * time, not only at compile time. + */ + @Test + public void testAuthenticationHandlerStillTakesJavaxServlet() + throws Exception { + AuthenticationHandler handler = new DownstreamAuthenticationHandler(); + handler.init(new Properties()); + try { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + when(request.getParameter("user")).thenReturn("alice"); + + AuthenticationToken token = handler.authenticate(request, response); + assertNotNull(token, "handler returned no token"); + assertEquals("alice", token.getUserName()); + assertEquals(DownstreamAuthenticationHandler.TYPE, token.getType()); + assertTrue(handler.managementOperation(token, request, response)); + } finally { + handler.destroy(); + } + } + + /** A servlet a downstream project would write: javax.servlet and nothing else. */ + public static class DownstreamServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws IOException { + response.setContentType("text/plain; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().print("downstream-ok"); + } + } + + /** Likewise a plain javax.servlet Filter. */ + public static class DownstreamFilter implements Filter { + static final AtomicBoolean RAN = new AtomicBoolean(false); + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + RAN.set(true); + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } + } + + /** + * A downstream servlet and filter, registered through the public HttpServer2 + * API, still serve a request on the embedded container. + */ + @Test + public void testDownstreamServletAndFilterStillServeRequests() + throws Exception { + DownstreamFilter.RAN.set(false); + HttpServer2 server = createTestServer(); + try { + server.addServlet("downstream", "/downstream", DownstreamServlet.class); + server.addFilter("downstream-filter", + DownstreamFilter.class.getName(), null); + server.start(); + + URL url = new URL(getServerURL(server), "/downstream"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.connect(); + + assertEquals(HttpServletResponse.SC_OK, conn.getResponseCode()); + assertEquals("downstream-ok", body(conn)); + assertTrue(DownstreamFilter.RAN.get(), + "a downstream filter registered through addFilter did not run"); + } finally { + stop(server); + } + } + + private static String body(HttpURLConnection conn) throws IOException { + try (InputStream in = conn.getInputStream(); + Scanner scanner = new Scanner(in, StandardCharsets.UTF_8.name())) { + scanner.useDelimiter("\\A"); + return scanner.hasNext() ? scanner.next().trim() : ""; + } + } + + /** + * The servlet API a downstream project resolves alongside Hadoop is the + * javax one, and it is loadable from Hadoop's own classpath. Guards against + * an upgrade that quietly swaps the namespace out from under an embedder. + */ + @Test + public void testServletApiOnTheClasspathIsJavax() throws Exception { + Class servlet = Class.forName("javax.servlet.Servlet"); + assertEquals("javax.servlet.Servlet", servlet.getName()); + assertTrue(servlet.isInterface()); + + // HttpServer2 is the embedding entry point; what it hands a downstream + // servlet has to be the same javax.servlet.Servlet loaded here. + assertTrue(servlet.isAssignableFrom(DownstreamServlet.class), + "HttpServlet no longer implements the javax.servlet.Servlet on" + + " the classpath"); + + List jakarta = new ArrayList<>(); + for (String name : Arrays.asList("jakarta.servlet.Servlet", + "jakarta.servlet.http.HttpServlet")) { + try { + Class.forName(name); + jakarta.add(name); + } catch (ClassNotFoundException expected) { + // the jakarta namespace must not be resolvable beside the javax one + } + } + assertTrue(jakarta.isEmpty(), + "both servlet namespaces are on the classpath: " + jakarta); + } +} diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServer.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServer.java index d8cf9ff0bdec3f..86264e3f2f6995 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServer.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestHttpServer.java @@ -34,6 +34,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.ServerConnector; +import org.apache.hadoop.test.GenericTestUtils; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -109,6 +111,20 @@ public void doGet(HttpServletRequest request, } } + /** Writes back the path info exactly as the container parsed it. */ + @SuppressWarnings("serial") + public static class PathInfoServlet extends HttpServlet { + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException { + response.setContentType("text/plain; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + try (PrintWriter out = response.getWriter()) { + out.println(request.getPathInfo()); + } + } + } + @SuppressWarnings("serial") public static class EchoServlet extends HttpServlet { @SuppressWarnings("unchecked") @@ -146,6 +162,23 @@ public void doGet(HttpServletRequest request, } } + /** + * Stands in for a JAX-RS resource that picks its own content type: it drops + * whatever QuotingInputFilter left on the response, then sets the type the + * way Jersey does, through a header. + */ + @SuppressWarnings("serial") + public static class OwnContentTypeServlet extends HttpServlet { + @Override + public void doGet(HttpServletRequest request, + HttpServletResponse response + ) throws ServletException, IOException { + JettyUtils.clearContentType(response); + response.addHeader("Content-Type", request.getParameter("type")); + response.setStatus(HttpServletResponse.SC_OK); + } + } + @BeforeAll public static void setup() throws Exception { Configuration conf = new Configuration(); @@ -154,9 +187,12 @@ public static void setup() throws Exception { CommonConfigurationKeysPublic.HADOOP_HTTP_METRICS_ENABLED, true); server = createTestServer(conf); server.addServlet("echo", "/echo", EchoServlet.class); + server.addServlet("pathinfo", "/pathinfo/*", PathInfoServlet.class); server.addServlet("echomap", "/echomap", EchoMapServlet.class); server.addServlet("htmlcontent", "/htmlcontent", HtmlContentServlet.class); server.addServlet("longheader", "/longheader", LongHeaderServlet.class); + server.addServlet("owncontenttype", "/owncontenttype", + OwnContentTypeServlet.class); server.addJerseyResourcePackage( JerseyResource.class.getPackage().getName(), "/jersey/*"); server.start(); @@ -277,6 +313,102 @@ public void testAcceptorSelectorConfigurability() throws Exception { conn.getContentType()); } + /** + * A resource that clears the content type QuotingInputFilter set, then picks + * its own, must not be given a charset back. + *

+ * Jetty 12 remembers that a charset had been set explicitly even after the + * content type carrying it is cleared, and appends that memory to the next + * content type - as ";charset=null" for a type with no charset of its own. + * Only types that assume a charset, application/json among them, escape it, + * which is why octet-stream and xml are the ones asserted here. + */ + @Test + public void testClearedContentTypeCarriesNoCharset() throws Exception { + for (String type : new String[] {MediaType.APPLICATION_OCTET_STREAM, + MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) { + URL url = new URL(baseUrl, "/owncontenttype?type=" + type); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.connect(); + assertEquals(HttpServletResponse.SC_OK, conn.getResponseCode()); + assertEquals(type, conn.getContentType(), + "the cleared charset came back for " + type); + conn.disconnect(); + } + } + + /** + * /static must never list what is in it. The setting that stops it is a + * context init parameter, which the DefaultServlet reads under a prefix of + * its own choosing: get the prefix wrong and the parameter is not rejected, + * it is ignored, and dirAllowed falls back to its default of true. That is + * silent, so the endpoint is asserted rather than the setting. + */ + @Test + public void testStaticContextDoesNotListDirectories() throws Exception { + URL staticUrl = new URL(baseUrl, "/static/"); + HttpURLConnection conn = (HttpURLConnection) staticUrl.openConnection(); + conn.connect(); + assertEquals(HttpServletResponse.SC_FORBIDDEN, conn.getResponseCode(), + "/static served a directory listing"); + + // The context is otherwise working, so the 403 above is dirAllowed doing + // its job and not the whole context being broken. + URL cssUrl = new URL(baseUrl, "/static/test.css"); + conn = (HttpURLConnection) cssUrl.openConnection(); + conn.connect(); + assertEquals(HttpServletResponse.SC_OK, conn.getResponseCode()); + } + + /** + * A path with an empty segment has to reach the servlet, which is where + * Hadoop decides what it means. Jetty 12 rejects one at the connector by + * default, with a bare 400 and no body - WebHDFS clients parse a JSON + * RemoteException out of that response, and there is nothing there to parse. + * The ambiguities that actually matter stay rejected, so an encoded + * separator is asserted here too. + */ + @Test + public void testHadoopPathsReachTheServlet() throws Exception { + assertPathInfo("//tmp//file", "/pathinfo//tmp//file"); + // A file whose name contains a '%' arrives as %25. + assertPathInfo("/a%b", "/pathinfo/a%25b"); + assertPathInfo("/@;%$", "/pathinfo/%40%3B%25%24"); + // A '\' is a path separator on Windows and just a character on HDFS. + assertPathInfo("/a\\b", "/pathinfo/a%5Cb"); + } + + /** + * The other half of the same setting: a path that would read as one thing + * to a filter and another to a servlet still has to be refused. + */ + @Test + public void testAmbiguousPathsAreStillRejected() throws Exception { + assertNotServed("an encoded path separator", "/pathinfo/tmp%2Ffile"); + assertNotServed("an encoded dot-segment", "/pathinfo/a%2E%2E%2Fb"); + } + + private static void assertPathInfo(String expected, String path) + throws Exception { + URL url = new URL(baseUrl, path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.connect(); + assertEquals(HttpServletResponse.SC_OK, conn.getResponseCode(), + path + " was rejected before the servlet ran"); + assertEquals(expected, readOutput(url).trim(), + path + " did not reach the servlet as sent"); + } + + private static void assertNotServed(String what, String path) + throws Exception { + URL url = new URL(baseUrl, path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.connect(); + assertThat(conn.getResponseCode()) + .as(what + " must not be served") + .isNotEqualTo(HttpServletResponse.SC_OK); + } + @Test public void testHttpServer2Metrics() throws Exception { final HttpServer2Metrics metrics = server.getMetrics(); @@ -286,8 +418,10 @@ public void testHttpServer2Metrics() throws Exception { (HttpURLConnection)servletUrl.openConnection(); conn.connect(); assertThat(conn.getResponseCode()).isEqualTo(200); - final int after = metrics.responses2xx(); - assertThat(after).isGreaterThan(before); + // Jetty 12 books the response when the exchange completes on the server, + // which can be after the client has read the status line, so the counter + // is given a moment rather than read straight away. + GenericTestUtils.waitFor(() -> metrics.responses2xx() > before, 50, 10000); } @Test @@ -323,9 +457,12 @@ public void testHttpServer2ThreadPoolMetrics() throws Exception { } /** - * Jetty StatisticsHandler must be inserted via Server#insertHandler - * instead of Server#setHandler. The server fails to start if - * the handler is added by setHandler. + * Jetty StatisticsHandler must be inserted via Server#insertHandler instead + * of Server#setHandler. On 9.4 the difference showed up as a server that + * refused to start, so the test could assert the failure; Jetty 12 starts a + * childless handler quite happily and serves 404s from it, which is worse. + * So the assertion is that the server still serves after the handler goes + * in - the reason to prefer insertHandler in the first place. */ @Test public void testSetStatisticsHandler() throws Exception { @@ -334,11 +471,30 @@ public void testSetStatisticsHandler() throws Exception { conf.setBoolean( CommonConfigurationKeysPublic.HADOOP_HTTP_METRICS_ENABLED, false); final HttpServer2 testServer = createTestServer(conf); - testServer.webServer.setHandler(new StatisticsHandler()); + testServer.addServlet("echo", "/echo", EchoServlet.class); + + final Handler tree = testServer.webServer.getHandler(); + assertThat(tree).isNotNull(); + final StatisticsHandler statistics = new StatisticsHandler(); + testServer.webServer.insertHandler(statistics); + assertThat(statistics.getHandler()) + .as("insertHandler keeps the handler tree underneath") + .isSameAs(tree); + try { testServer.start(); - fail("IOException should be thrown."); - } catch (IOException ignore) { + final URL echoUrl = new URL(getServerURL(testServer), "/echo?a=b"); + final HttpURLConnection conn = + (HttpURLConnection) echoUrl.openConnection(); + conn.connect(); + assertThat(conn.getResponseCode()) + .as("the webapp stopped serving once StatisticsHandler was inserted") + .isEqualTo(HttpServletResponse.SC_OK); + // Booked when the exchange completes on the server, which can be after + // the client has read the status line - see testHttpServer2Metrics. + GenericTestUtils.waitFor(() -> statistics.getRequests() > 0, 50, 10000); + } finally { + testServer.stop(); } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/http/TestRestCsrfPreventionFilter.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/http/TestRestCsrfPreventionFilter.java index f39dd1a103b452..a5e30fe0ef437c 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/http/TestRestCsrfPreventionFilter.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/http/TestRestCsrfPreventionFilter.java @@ -18,10 +18,13 @@ package org.apache.hadoop.security.http; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -67,7 +70,8 @@ public void testNoHeaderDefaultConfigBadRequest() thenReturn(BROWSER_AGENT); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -75,8 +79,7 @@ public void testNoHeaderDefaultConfigBadRequest() filter.init(filterConfig); filter.doFilter(mockReq, mockRes, mockChain); - verify(mockRes, atLeastOnce()).sendError( - HttpServletResponse.SC_BAD_REQUEST, EXPECTED_MESSAGE); + assertRejected(mockRes, writer); verifyZeroInteractions(mockChain); } @@ -102,7 +105,8 @@ public void testNoHeaderCustomAgentConfigBadRequest() thenReturn("curl"); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -110,8 +114,7 @@ public void testNoHeaderCustomAgentConfigBadRequest() filter.init(filterConfig); filter.doFilter(mockReq, mockRes, mockChain); - verify(mockRes, atLeastOnce()).sendError( - HttpServletResponse.SC_BAD_REQUEST, EXPECTED_MESSAGE); + assertRejected(mockRes, writer); verifyZeroInteractions(mockChain); } @@ -134,7 +137,8 @@ public void testNoHeaderDefaultConfigNonBrowserGoodRequest() thenReturn(NON_BROWSER); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -162,7 +166,8 @@ public void testHeaderPresentDefaultConfigGoodRequest() thenReturn("valueUnimportant"); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -191,7 +196,8 @@ public void testHeaderPresentCustomHeaderConfigGoodRequest() thenReturn("valueUnimportant"); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -222,7 +228,8 @@ public void testMissingHeaderWithCustomHeaderConfigBadRequest() thenReturn(null); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -254,7 +261,8 @@ public void testMissingHeaderNoMethodsToIgnoreConfigBadRequest() thenReturn("GET"); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -286,7 +294,8 @@ public void testMissingHeaderIgnoreGETMethodConfigGoodRequest() thenReturn("GET"); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -318,7 +327,8 @@ public void testMissingHeaderMultipleIgnoreMethodsConfigGoodRequest() thenReturn("OPTIONS"); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -350,7 +360,8 @@ public void testMissingHeaderMultipleIgnoreMethodsConfigBadRequest() thenReturn("PUT"); // Objects to verify interactions based on request - HttpServletResponse mockRes = Mockito.mock(HttpServletResponse.class); + StringWriter writer = new StringWriter(); + HttpServletResponse mockRes = mockResponse(writer); FilterChain mockChain = Mockito.mock(FilterChain.class); // Object under test @@ -360,4 +371,34 @@ public void testMissingHeaderMultipleIgnoreMethodsConfigBadRequest() verifyZeroInteractions(mockChain); } + + /** + * A refusal is reported as the JSON envelope HttpExceptionUtils writes, not + * handed to sendError, so what is asserted is the status and the message the + * caller will actually be able to read. + * + * @param res the mocked response the filter wrote to + * @param body what the filter wrote into it + */ + private static void assertRejected(HttpServletResponse res, StringWriter body) { + verify(res, atLeastOnce()).setStatus(HttpServletResponse.SC_BAD_REQUEST); + assertTrue(body.toString().contains(EXPECTED_MESSAGE), + "refusal did not carry the reason: " + body); + } + + /** + * A mock response the filter can write a refusal into. Every caller gets a + * writer whether or not it looks at one, because the filter now reports a + * refusal by writing the error rather than by calling sendError. + * + * @param body collects whatever the filter writes + * @return the mock response + * @throws IOException never, but getWriter declares it + */ + private static HttpServletResponse mockResponse(StringWriter body) + throws IOException { + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + Mockito.when(res.getWriter()).thenReturn(new PrintWriter(body)); + return res; + } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/token/delegation/web/TestWebDelegationToken.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/token/delegation/web/TestWebDelegationToken.java index 25756dd4277944..ce3867afd483b1 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/token/delegation/web/TestWebDelegationToken.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/token/delegation/web/TestWebDelegationToken.java @@ -21,6 +21,7 @@ import org.apache.commons.io.IOUtils; import org.apache.hadoop.io.Text; import org.apache.hadoop.minikdc.MiniKdc; +import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authentication.KerberosTestUtils; import org.apache.hadoop.security.authentication.client.AuthenticationException; @@ -35,12 +36,12 @@ import org.apache.hadoop.test.GenericTestUtils; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.FilterHolder; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import org.slf4j.event.Level; import javax.security.auth.Subject; @@ -78,6 +79,7 @@ import java.util.concurrent.Callable; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -344,6 +346,28 @@ public void testDelegationTokenAuthenticatorCallsWithQueryString() testDelegationTokenAuthenticatorCalls(true); } + /** + * A renewal refused by the server has to reach the caller as the exception + * the server raised, carrying the reason it gave. + *

+ * The handler reports this through HttpExceptionUtils, which writes the + * error as JSON and lets the client rebuild the original type. These calls + * used to assert that the message contained "403" instead, which only held + * while the body was unparseable: the client fell back to a generic + * IOException that quoted the status line. That fallback is what the + * assertion was really pinning, so it went on passing while the detail the + * caller needed was being thrown away. + * + * @param ex the exception the client raised + */ + private static void assertRenewRefused(Exception ex) { + assertInstanceOf(AccessControlException.class, ex, + "renewal refusal did not survive the round trip as the server's own" + + " exception; got " + ex.getClass().getName() + ": " + + ex.getMessage()); + assertTrue(ex.getMessage().contains("renew"), + "refusal lost the server's reason: " + ex.getMessage()); + } private void testDelegationTokenAuthenticatorCalls(final boolean useQS) throws Exception { @@ -394,7 +418,7 @@ private void testDelegationTokenAuthenticatorCalls(final boolean useQS) aUrl.renewDelegationToken(authURL2, token); fail(); } catch (Exception ex) { - assertTrue(ex.getMessage().contains("403")); + assertRenewRefused(ex); } aUrl.getDelegationToken(authURL, token, FOO_USER); @@ -817,7 +841,7 @@ public Void call() throws Exception { aUrl.renewDelegationToken(url, token, doAsUser); fail(); } catch (Exception ex) { - assertTrue(ex.getMessage().contains("403")); + assertRenewRefused(ex); } aUrl.getDelegationToken(url, token, FOO_USER, doAsUser); diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestHttpExceptionUtils.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestHttpExceptionUtils.java index f3f0e43b394194..d501866fc106a3 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestHttpExceptionUtils.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestHttpExceptionUtils.java @@ -37,6 +37,8 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -109,12 +111,83 @@ public void testValidateResponseNonJsonErrorMessage() throws Exception { when(conn.getErrorStream()).thenReturn(is); when(conn.getResponseMessage()).thenReturn("msg"); when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST); + // The body wins over the reason phrase: a servlet's reason travels in the + // body now, so "stream" is the detail and "msg" is only the canonical + // text for the status code. LambdaTestUtils.interceptAndValidateMessageContains(IOException.class, - Arrays.asList(Integer.toString(HttpURLConnection.HTTP_BAD_REQUEST), "msg", + Arrays.asList(Integer.toString(HttpURLConnection.HTTP_BAD_REQUEST), "stream", "com.fasterxml.jackson.core.JsonParseException"), () -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED)); } + @Test + public void testValidateResponseHtmlErrorPageReportsTheReason() + throws Exception { + // What AuthenticationFilter's sendError looks like on the wire. + String page = "Error 403 Invalid signature" + + "" + + "

HTTP ERROR 403

Reason: Invalid signature

" + + ""; + HttpURLConnection conn = connectionReturning(page, "Forbidden", "text/html"); + when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); + LambdaTestUtils.interceptAndValidateMessageContains(IOException.class, + Arrays.asList("Invalid signature"), + () -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK)); + } + + @Test + public void testValidateResponseFallsBackToThePhraseWithNoBody() + throws Exception { + HttpURLConnection conn = connectionReturning(null, "Forbidden", "text/html"); + when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); + LambdaTestUtils.interceptAndValidateMessageContains(IOException.class, + Arrays.asList("Forbidden"), + () -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK)); + } + + @Test + public void testValidateResponseStillRebuildsTheEnvelopeException() + throws Exception { + // The rewind must not disturb the envelope path: a JSON body still + // reconstructs its exception rather than being quoted back as text. + Map json = new HashMap(); + json.put(HttpExceptionUtils.ERROR_EXCEPTION_JSON, + IllegalStateException.class.getSimpleName()); + json.put(HttpExceptionUtils.ERROR_CLASSNAME_JSON, + IllegalStateException.class.getName()); + json.put(HttpExceptionUtils.ERROR_MESSAGE_JSON, "EX"); + Map response = new HashMap(); + response.put(HttpExceptionUtils.ERROR_JSON, json); + String body = new ObjectMapper().writeValueAsString(response); + HttpURLConnection conn = + connectionReturning(body, "Forbidden", "application/json"); + when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); + LambdaTestUtils.intercept(IllegalStateException.class, "EX", + () -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK)); + } + + @Test + public void testValidateResponseParsesAnEnvelopeTooLargeToRewind() + throws Exception { + // Larger than the rewind buffer: the parser reads straight through, so the + // exception is still rebuilt - only the text fallback is given up. + Map json = new HashMap(); + json.put(HttpExceptionUtils.ERROR_EXCEPTION_JSON, + IllegalStateException.class.getSimpleName()); + json.put(HttpExceptionUtils.ERROR_CLASSNAME_JSON, + IllegalStateException.class.getName()); + json.put(HttpExceptionUtils.ERROR_MESSAGE_JSON, + "x".repeat(64 * 1024)); + Map response = new HashMap(); + response.put(HttpExceptionUtils.ERROR_JSON, json); + String body = new ObjectMapper().writeValueAsString(response); + HttpURLConnection conn = + connectionReturning(body, "Forbidden", "application/json"); + when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); + LambdaTestUtils.intercept(IllegalStateException.class, + () -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_OK)); + } + @Test public void testValidateResponseJsonErrorKnownException() throws Exception { Map json = new HashMap(); @@ -178,4 +251,86 @@ public void testValidateResponseJsonErrorNonException() throws Exception { "java.lang.String", "EX"), () -> HttpExceptionUtils.validateResponse(conn, HttpURLConnection.HTTP_CREATED)); } + + private static HttpURLConnection connectionReturning(String body, + String phrase) throws IOException { + return connectionReturning(body, phrase, null); + } + + private static HttpURLConnection connectionReturning(String body, + String phrase, String contentType) throws IOException { + HttpURLConnection conn = mock(HttpURLConnection.class); + when(conn.getErrorStream()).thenReturn(body == null ? null + : new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + when(conn.getResponseMessage()).thenReturn(phrase); + when(conn.getContentType()).thenReturn(contentType); + return conn; + } + + @Test + public void testResponseDetailPrefersTheBody() throws Exception { + assertEquals("the real reason", HttpExceptionUtils.getResponseDetail( + connectionReturning("the real reason", "Forbidden"))); + } + + @Test + public void testResponseDetailStripsAnErrorPage() throws Exception { + // what a container renders for sendError(403, "the real reason") + String page = "\n\nError 403 the real reason\n" + + "\n

HTTP ERROR 403

\n" + + "
MESSAGE:the real reason
\n" + + "\n\n"; + String detail = HttpExceptionUtils.getResponseDetail( + connectionReturning(page, "Forbidden")); + assertTrue(detail.contains("the real reason"), detail); + assertFalse(detail.contains("<"), "markup survived: " + detail); + } + + @Test + public void testResponseDetailFallsBackToThePhrase() throws Exception { + assertEquals("Forbidden", HttpExceptionUtils.getResponseDetail( + connectionReturning(null, "Forbidden"))); + assertEquals("Forbidden", HttpExceptionUtils.getResponseDetail( + connectionReturning(" \n ", "Forbidden"))); + } + + @Test + public void testResponseDetailIsNeverNull() throws Exception { + assertEquals("", HttpExceptionUtils.getResponseDetail( + connectionReturning(null, null))); + } + + /** + * A refusal carrying the JSON envelope is reported by its reason phrase, not + * by the envelope. Such a response is sent with setStatus rather than + * sendError - see {@link HttpExceptionUtils#createServletExceptionResponse} + * - so it never had a reason of its own in the phrase, and quoting the JSON + * back would replace a readable "Forbidden" with a line of markup. Callers + * that want what is inside it use validateResponse. + */ + @Test + public void testResponseDetailLeavesTheJsonEnvelopeAlone() throws Exception { + String envelope = "{\"RemoteException\":{\"message\":\"User: client is not" + + " allowed to impersonate foo1\",\"exception\":\"AuthorizationException\"," + + "\"javaClassName\":\"org.apache.hadoop.security.authorize." + + "AuthorizationException\"}}"; + + assertEquals("Forbidden", HttpExceptionUtils.getResponseDetail( + connectionReturning(envelope, "Forbidden", "application/json"))); + // the header may carry parameters + assertEquals("Forbidden", HttpExceptionUtils.getResponseDetail( + connectionReturning(envelope, "Forbidden", + "application/json; charset=utf-8"))); + } + + /** + * A body that is not JSON is still preferred, which is the case the reader + * exists for: Jetty 12 puts what sendError was given in the body and leaves + * the phrase canonical. + */ + @Test + public void testResponseDetailStillPrefersANonJsonBody() throws Exception { + assertEquals("the real reason", HttpExceptionUtils.getResponseDetail( + connectionReturning("the real reason", "Forbidden", "text/plain"))); + } } diff --git a/hadoop-common-project/hadoop-kms/pom.xml b/hadoop-common-project/hadoop-kms/pom.xml index e2874eb3a8033a..42c3654367ab6d 100644 --- a/hadoop-common-project/hadoop-kms/pom.xml +++ b/hadoop-common-project/hadoop-kms/pom.xml @@ -69,8 +69,8 @@ compile
- jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api org.eclipse.jetty @@ -82,8 +82,8 @@ runtime - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee8 + jetty-ee8-webapp org.apache.hadoop diff --git a/hadoop-common-project/hadoop-kms/src/main/java/org/apache/hadoop/crypto/key/kms/server/KMSAuthenticationFilter.java b/hadoop-common-project/hadoop-kms/src/main/java/org/apache/hadoop/crypto/key/kms/server/KMSAuthenticationFilter.java index 30f55737c62f00..dc7a5a3cb56453 100644 --- a/hadoop-common-project/hadoop-kms/src/main/java/org/apache/hadoop/crypto/key/kms/server/KMSAuthenticationFilter.java +++ b/hadoop-common-project/hadoop-kms/src/main/java/org/apache/hadoop/crypto/key/kms/server/KMSAuthenticationFilter.java @@ -28,7 +28,6 @@ import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticationHandler; import org.apache.hadoop.security.token.delegation.web.KerberosDelegationTokenAuthenticationHandler; import org.apache.hadoop.security.token.delegation.web.PseudoDelegationTokenAuthenticationHandler; -import org.eclipse.jetty.server.Response; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -115,17 +114,8 @@ public void sendError(int sc, String msg) throws IOException { statusCode = sc; this.msg = msg; - ServletResponse response = getResponse(); - - // After Jetty 9.4.21, sendError() no longer allows a custom message. - // use setStatusWithReason() to set a custom message. - if (response instanceof Response) { - ((Response) response).setStatusWithReason(sc, msg); - } else { - KMS.LOG.warn("The wrapped response object is instance of {}" + - ", not org.eclipse.jetty.server.Response. Can't set custom error " + - "message", response.getClass()); - } + // Jetty 12 never puts a reason phrase on the wire, so the detail is + // left to sendError, which writes it into the response body. super.sendError(sc, HtmlQuoting.quoteHtmlChars(msg)); } diff --git a/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMS.java b/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMS.java index 84bcbbde4fbed6..89da53b005d888 100644 --- a/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMS.java +++ b/hadoop-common-project/hadoop-kms/src/test/java/org/apache/hadoop/crypto/key/kms/server/TestKMS.java @@ -33,6 +33,10 @@ import org.apache.hadoop.crypto.key.kms.KMSDelegationToken; import org.apache.hadoop.crypto.key.kms.LoadBalancingKMSClientProvider; import org.apache.hadoop.crypto.key.kms.ValueQueue; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; +import org.apache.hadoop.security.authentication.util.SignerException; +import org.apache.hadoop.security.token.delegation.web.PseudoDelegationTokenAuthenticationHandler; import org.apache.hadoop.fs.CommonConfigurationKeysPublic; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.Text; @@ -61,6 +65,8 @@ import javax.net.ssl.HttpsURLConnection; import javax.security.auth.login.AppConfigurationEntry; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; import java.io.DataInputStream; @@ -91,6 +97,8 @@ import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1433,6 +1441,74 @@ public Void run() throws Exception { }); } + /** + * Refuses one request, once armed, with the reason an invalid signature + * produces, so that a test can drive the branch of KMSClientProvider's retry + * that reads it. + *

+ * The refusal is thrown from managementOperation rather than from + * authenticate because authenticate is reached only by a request that + * carries no valid token, and such a request is answered 401, not 403. An + * AuthenticationException out of managementOperation is what makes + * AuthenticationFilter answer 403 - with the reason in the body only. + */ + public static class FailWhenArmedAuthenticationHandler + extends PseudoDelegationTokenAuthenticationHandler { + + private static final AtomicBoolean ARMED = new AtomicBoolean(); + private static final AtomicInteger REFUSALS = new AtomicInteger(); + + @Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException { + if (ARMED.compareAndSet(true, false)) { + REFUSALS.incrementAndGet(); + throw new AuthenticationException( + new SignerException("Invalid signature")); + } + return super.managementOperation(token, request, response); + } + } + + @Test + public void testKMSAuthFailureRetryOn403() throws Exception { + // The reason for a refusal reaches the client in the response body, not in + // the HTTP reason phrase: Jetty 12 never puts a phrase on the wire. A + // client that reads only the phrase sees "Forbidden", fails to recognise + // an authentication failure, and gives up without re-authenticating. + Configuration conf = new Configuration(); + File confDir = getTestDir(); + conf = createBaseKMSConf(confDir, conf); + conf.set("hadoop.kms.authentication.type", + FailWhenArmedAuthenticationHandler.class.getName()); + conf.set(KeyAuthorizationKeyProvider.KEY_ACL + "k1.ALL", "*"); + conf.set(KeyAuthorizationKeyProvider.KEY_ACL + "k2.ALL", "*"); + writeConf(confDir, conf); + FailWhenArmedAuthenticationHandler.ARMED.set(false); + FailWhenArmedAuthenticationHandler.REFUSALS.set(0); + + runServer(null, null, confDir, new KMSCallable() { + @Override + public Void call() throws Exception { + final Configuration conf = new Configuration(); + conf.setInt(KeyProvider.DEFAULT_BITLENGTH_NAME, 128); + final URI uri = createKMSUri(getKMSUrl()); + KeyProvider kp = createProvider(uri, conf); + // Establish a token first, so the refusal below is the only thing + // standing between the client and a successful call. + kp.createKey("k1", new KeyProvider.Options(conf)); + + FailWhenArmedAuthenticationHandler.ARMED.set(true); + // Succeeds only if the 403 was recognised and the call retried. + kp.createKey("k2", new KeyProvider.Options(conf)); + assertEquals(1, FailWhenArmedAuthenticationHandler.REFUSALS.get(), + "the armed request should have been refused exactly once"); + return null; + } + }); + } + @Test @SuppressWarnings("checkstyle:methodlength") public void testACLs() throws Exception { diff --git a/hadoop-common-project/hadoop-nfs/pom.xml b/hadoop-common-project/hadoop-nfs/pom.xml index 685397e131d427..f942f04718d27d 100644 --- a/hadoop-common-project/hadoop-nfs/pom.xml +++ b/hadoop-common-project/hadoop-nfs/pom.xml @@ -59,8 +59,8 @@ test - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api provided diff --git a/hadoop-hdfs-project/hadoop-hdfs-client/src/main/java/org/apache/hadoop/hdfs/web/WebHdfsFileSystem.java b/hadoop-hdfs-project/hadoop-hdfs-client/src/main/java/org/apache/hadoop/hdfs/web/WebHdfsFileSystem.java index a92cea31363c4a..e15a01145d49bb 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-client/src/main/java/org/apache/hadoop/hdfs/web/WebHdfsFileSystem.java +++ b/hadoop-hdfs-project/hadoop-hdfs-client/src/main/java/org/apache/hadoop/hdfs/web/WebHdfsFileSystem.java @@ -130,6 +130,7 @@ import org.apache.hadoop.thirdparty.com.google.common.net.HttpHeaders; import org.apache.hadoop.util.JsonSerialization; import org.apache.hadoop.util.KMSUtil; +import org.apache.hadoop.util.HttpExceptionUtils; import org.apache.hadoop.util.Lists; import org.apache.hadoop.util.Progressable; import org.apache.hadoop.util.StringUtils; @@ -485,6 +486,18 @@ private Path makeAbsolute(Path f) { if (c.getContentLength() == 0) { return null; } + // Checked before the stream is opened: a body this method will not read + // is left intact for the caller to report, and getInputStream is not + // called for a response that has no input stream to give. + final String contentType = c.getContentType(); + if (contentType != null) { + final MediaType parsed = MediaType.valueOf(contentType); + if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(parsed)) { + throw new IOException("Content-Type \"" + contentType + + "\" is incompatible with \"" + MediaType.APPLICATION_JSON + + "\" (parsed=\"" + parsed + "\")"); + } + } final InputStream in = useErrorStream ? c.getErrorStream() : c.getInputStream(); if (in == null) { @@ -492,15 +505,6 @@ private Path makeAbsolute(Path f) { " stream is null."); } try { - final String contentType = c.getContentType(); - if (contentType != null) { - final MediaType parsed = MediaType.valueOf(contentType); - if (!MediaType.APPLICATION_JSON_TYPE.isCompatible(parsed)) { - throw new IOException("Content-Type \"" + contentType - + "\" is incompatible with \"" + MediaType.APPLICATION_JSON - + "\" (parsed=\"" + parsed + "\")"); - } - } return JsonSerialization.mapReader().readValue(in); } finally { in.close(); @@ -514,7 +518,12 @@ private Path makeAbsolute(Path f) { // server is demanding an authentication we don't support if (code == HttpURLConnection.HTTP_UNAUTHORIZED) { // match hdfs/rpc exception - throw new AccessControlException(conn.getResponseMessage()); + // + // The reason comes from the body, not the reason phrase: what + // AuthenticationFilter passes to sendError no longer reaches the wire as + // a phrase, and the phrase is now always "Unauthorized". + throw new AccessControlException( + HttpExceptionUtils.getResponseDetail(conn)); } if (code != op.getExpectedHttpResponseCode()) { final Map m; @@ -523,13 +532,13 @@ private Path makeAbsolute(Path f) { } catch(Exception e) { throw new IOException("Unexpected HTTP response: code=" + code + " != " + op.getExpectedHttpResponseCode() + ", " + op.toQueryString() - + ", message=" + conn.getResponseMessage(), e); + + ", message=" + HttpExceptionUtils.getResponseDetail(conn), e); } if (m == null) { throw new IOException("Unexpected HTTP response: code=" + code + " != " + op.getExpectedHttpResponseCode() + ", " + op.toQueryString() - + ", message=" + conn.getResponseMessage()); + + ", message=" + HttpExceptionUtils.getResponseDetail(conn)); } else if (m.get(RemoteException.class.getSimpleName()) == null) { return m; } diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/pom.xml b/hadoop-hdfs-project/hadoop-hdfs-httpfs/pom.xml index f0c0e21ac12e50..539f29d7a99836 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/pom.xml +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/pom.xml @@ -63,8 +63,8 @@ compile - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api compile @@ -87,8 +87,8 @@ runtime - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee8 + jetty-ee8-webapp org.apache.hadoop diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/client/BaseTestHttpFSWith.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/client/BaseTestHttpFSWith.java index 41ba89d9517cc0..a414c975e87539 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/client/BaseTestHttpFSWith.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/client/BaseTestHttpFSWith.java @@ -82,7 +82,7 @@ import org.json.simple.parser.ContainerFactory; import org.json.simple.parser.JSONParser; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSAccessControlled.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSAccessControlled.java index 99dfb3e281d597..e7334e88809f4d 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSAccessControlled.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSAccessControlled.java @@ -31,7 +31,7 @@ import org.apache.hadoop.test.TestJettyHelper; import org.junit.jupiter.api.Test; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import java.io.File; import java.io.FileOutputStream; diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServer.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServer.java index d9c1b8813f49fe..d5a82a4613fd25 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServer.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServer.java @@ -111,7 +111,7 @@ import org.json.simple.parser.JSONParser; import org.junit.jupiter.api.Test; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import org.apache.hadoop.thirdparty.com.google.common.collect.Maps; import java.util.Properties; diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServerNoACLs.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServerNoACLs.java index ff78afabe1bdd7..3cb15048d522ed 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServerNoACLs.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServerNoACLs.java @@ -32,7 +32,7 @@ import org.apache.hadoop.test.TestJettyHelper; import org.junit.jupiter.api.Test; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import java.io.BufferedReader; import java.io.File; diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServerNoXAttrs.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServerNoXAttrs.java index a7dcffe06fbd1c..bd7c9dff6f00bc 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServerNoXAttrs.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSServerNoXAttrs.java @@ -33,7 +33,7 @@ import org.apache.hadoop.test.TestJettyHelper; import org.junit.jupiter.api.Test; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import java.io.BufferedReader; import java.io.File; diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSWithKerberos.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSWithKerberos.java index 44da554a849e15..349664ab108e84 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSWithKerberos.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/fs/http/server/TestHttpFSWithKerberos.java @@ -41,7 +41,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import java.io.File; import java.io.FileOutputStream; diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/test/TestHFSTestCase.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/test/TestHFSTestCase.java index 737940908cf3e8..ceae8b88ffced6 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/test/TestHFSTestCase.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/test/TestHFSTestCase.java @@ -35,7 +35,7 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.util.Time; -import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; import org.junit.jupiter.api.Test; import org.eclipse.jetty.server.Server; diff --git a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/test/TestHTestCase.java b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/test/TestHTestCase.java index 7354def8b05bca..1b60324658e855 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/test/TestHTestCase.java +++ b/hadoop-hdfs-project/hadoop-hdfs-httpfs/src/test/java/org/apache/hadoop/test/TestHTestCase.java @@ -30,7 +30,7 @@ import javax.servlet.http.HttpServletResponse; import org.apache.hadoop.util.Time; -import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; import org.junit.jupiter.api.Test; import org.eclipse.jetty.server.Server; diff --git a/hadoop-hdfs-project/hadoop-hdfs-nfs/pom.xml b/hadoop-hdfs-project/hadoop-hdfs-nfs/pom.xml index 38f293c1c34cc6..149a075b949362 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-nfs/pom.xml +++ b/hadoop-hdfs-project/hadoop-hdfs-nfs/pom.xml @@ -149,8 +149,8 @@ https://maven.apache.org/xsd/maven-4.0.0.xsd"> ${transient.protobuf2.scope} - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api compile diff --git a/hadoop-hdfs-project/hadoop-hdfs-rbf/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterNetworkTopologyServlet.java b/hadoop-hdfs-project/hadoop-hdfs-rbf/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterNetworkTopologyServlet.java index 850f281b2bb120..c4388c3d099e01 100644 --- a/hadoop-hdfs-project/hadoop-hdfs-rbf/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterNetworkTopologyServlet.java +++ b/hadoop-hdfs-project/hadoop-hdfs-rbf/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterNetworkTopologyServlet.java @@ -21,13 +21,11 @@ import org.apache.hadoop.hdfs.protocol.HdfsConstants; import org.apache.hadoop.hdfs.server.namenode.NetworkTopologyServlet; import org.apache.hadoop.net.Node; -import org.apache.hadoop.util.StringUtils; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.PrintStream; import java.util.Arrays; import java.util.List; @@ -43,13 +41,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final ServletContext context = getServletContext(); - String format = parseAcceptHeader(request); - if (FORMAT_TEXT.equals(format)) { - response.setContentType("text/plain; charset=UTF-8"); - } else if (FORMAT_JSON.equals(format)) { - response.setContentType("application/json; charset=UTF-8"); - } - Router router = RouterHttpServer.getRouterFromContext(context); DatanodeInfo[] datanodeReport = null; if (router.getRpcServer().isAsync()) { @@ -66,16 +57,6 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) } List datanodeInfos = Arrays.asList(datanodeReport); - try (PrintStream out = new PrintStream( - response.getOutputStream(), false, "UTF-8")) { - printTopology(out, datanodeInfos, format); - } catch (Throwable t) { - String errMsg = "Print network topology failed. " - + StringUtils.stringifyException(t); - response.sendError(HttpServletResponse.SC_GONE, errMsg); - throw new IOException(errMsg); - } finally { - response.getOutputStream().close(); - } + sendTopology(response, datanodeInfos, parseAcceptHeader(request)); } } \ No newline at end of file diff --git a/hadoop-hdfs-project/hadoop-hdfs/pom.xml b/hadoop-hdfs-project/hadoop-hdfs/pom.xml index 07154ccdf7f872..1c2ff021e9aa47 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/pom.xml +++ b/hadoop-hdfs-project/hadoop-hdfs/pom.xml @@ -142,8 +142,8 @@ https://maven.apache.org/xsd/maven-4.0.0.xsd"> ${transient.protobuf2.scope} - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api compile diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/common/Util.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/common/Util.java index 5039db6ceb4882..cf3013b94daaca 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/common/Util.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/common/Util.java @@ -49,6 +49,7 @@ import org.apache.hadoop.net.NetUtils; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.util.HttpExceptionUtils; import org.apache.hadoop.util.Lists; import org.apache.hadoop.util.StringUtils; import org.apache.hadoop.util.Time; @@ -170,7 +171,8 @@ public static MD5Hash doGetUrl(URL url, List localPaths, if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throw new HttpGetFailedException("Image transfer servlet at " + url + " failed with status code " + connection.getResponseCode() + - "\nResponse message:\n" + connection.getResponseMessage(), + "\nResponse message:\n" + + HttpExceptionUtils.getResponseDetail(connection), connection); } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EditLogFileInputStream.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EditLogFileInputStream.java index ba4f32fd2154dd..247339225db782 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EditLogFileInputStream.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/EditLogFileInputStream.java @@ -46,6 +46,7 @@ import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.apache.hadoop.classification.VisibleForTesting; +import org.apache.hadoop.util.HttpExceptionUtils; import org.apache.hadoop.util.Preconditions; import org.apache.hadoop.thirdparty.com.google.common.base.Throwables; @@ -494,7 +495,8 @@ public InputStream run() throws IOException { throw new HttpGetFailedException( "Fetch of " + url + " failed with status code " + connection.getResponseCode() + - "\nResponse message:\n" + connection.getResponseMessage(), + "\nResponse message:\n" + + HttpExceptionUtils.getResponseDetail(connection), connection); } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/ImageServlet.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/ImageServlet.java index 442c1aba95b1cf..02dc270548058f 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/ImageServlet.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/ImageServlet.java @@ -28,6 +28,7 @@ import static org.apache.hadoop.util.Time.monotonicNow; import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; import java.security.PrivilegedExceptionAction; import java.util.*; import java.io.*; @@ -42,7 +43,6 @@ import org.apache.hadoop.hdfs.DFSConfigKeys; import org.apache.hadoop.hdfs.DFSUtilClient; import org.apache.hadoop.security.SecurityUtil; -import org.eclipse.jetty.server.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hadoop.classification.InterfaceAudience; @@ -703,11 +703,25 @@ public Void run() throws Exception { private void sendError(HttpServletResponse response, int code, String message) throws IOException { - if (response instanceof Response) { - ((Response)response).setStatusWithReason(code, message); - } - - response.sendError(code, message); + // Write the reason into the body rather than leaving it to sendError. + // + // Jetty 12 no longer puts a reason phrase on the wire, so sendError's + // message survives only in the error page the container renders. This + // servlet closes its output stream on the way out of every request, which + // commits an empty response before that page is ever produced - the + // client then sees a bare status code and no reason at all. Writing the + // body here keeps the detail, and does not depend on how the container + // renders errors. + if (response.isCommitted()) { + LOG.warn("Could not report \"{}\": the response is already committed.", + message); + return; + } + byte[] body = message.getBytes(StandardCharsets.UTF_8); + response.setStatus(code); + response.setContentType("text/plain; charset=utf-8"); + response.setContentLength(body.length); + response.getOutputStream().write(body); } /* diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/NetworkTopologyServlet.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/NetworkTopologyServlet.java index a6460280835d32..aacdf2f6bc97b0 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/NetworkTopologyServlet.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/NetworkTopologyServlet.java @@ -31,7 +31,9 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; @@ -57,28 +59,48 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final ServletContext context = getServletContext(); - String format = parseAcceptHeader(request); - if (FORMAT_TEXT.equals(format)) { - response.setContentType("text/plain; charset=UTF-8"); - } else if (FORMAT_JSON.equals(format)) { - response.setContentType("application/json; charset=UTF-8"); - } - NameNode nn = NameNodeHttpServer.getNameNodeFromContext(context); BlockManager bm = nn.getNamesystem().getBlockManager(); List leaves = bm.getDatanodeManager().getNetworkTopology() .getLeaves(NodeBase.ROOT); - try (PrintStream out = new PrintStream( - response.getOutputStream(), false, "UTF-8")) { + sendTopology(response, leaves, parseAcceptHeader(request)); + } + + /** + * Renders the topology and sends it, or reports why it could not be sent. + *

+ * The rendering happens before the response is touched. Writing it straight + * to the response stream commits the response as soon as that stream is + * closed, and once the status line is on the wire a later failure cannot be + * reported at all: sendError has nothing left to set, so a topology that + * failed half way through goes out as 200 OK with a truncated body. + * + * @param response the response to send on + * @param leaves the nodes to render + * @param format the response format, from {@link #parseAcceptHeader} + * @throws IOException if the topology could not be rendered or sent + */ + protected void sendTopology(HttpServletResponse response, List leaves, + String format) throws IOException { + ByteArrayOutputStream rendered = new ByteArrayOutputStream(); + try (PrintStream out = new PrintStream(rendered, false, "UTF-8")) { printTopology(out, leaves, format); } catch (Throwable t) { String errMsg = "Print network topology failed. " + StringUtils.stringifyException(t); response.sendError(HttpServletResponse.SC_GONE, errMsg); throw new IOException(errMsg); - } finally { - response.getOutputStream().close(); + } + + if (FORMAT_TEXT.equals(format)) { + response.setContentType("text/plain; charset=UTF-8"); + } else if (FORMAT_JSON.equals(format)) { + response.setContentType("application/json; charset=UTF-8"); + } + response.setContentLength(rendered.size()); + try (OutputStream out = response.getOutputStream()) { + rendered.writeTo(out); } } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/TransferFsImage.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/TransferFsImage.java index ac31c16c91720f..9cee56e13e81eb 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/TransferFsImage.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/TransferFsImage.java @@ -54,6 +54,7 @@ import org.apache.hadoop.io.MD5Hash; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.util.HttpExceptionUtils; import org.apache.hadoop.util.Lists; import org.apache.hadoop.util.Time; import org.apache.http.client.utils.URIBuilder; @@ -317,7 +318,8 @@ private static void uploadImage(URL url, Configuration conf, if (responseCode != HttpURLConnection.HTTP_OK) { throw new HttpPutFailedException(String.format( "Image uploading failed, status: %d, url: %s, message: %s", - responseCode, urlWithParams, connection.getResponseMessage()), + responseCode, urlWithParams, + HttpExceptionUtils.getResponseDetail(connection)), responseCode); } } catch (AuthenticationException | URISyntaxException e) { diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/web/resources/NamenodeWebHdfsMethods.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/web/resources/NamenodeWebHdfsMethods.java index 443c1836351ed3..b1f924123bfc72 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/web/resources/NamenodeWebHdfsMethods.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/web/resources/NamenodeWebHdfsMethods.java @@ -178,8 +178,7 @@ protected void init(final UserGroupInformation ugi, + Param.toSortedString(", ", parameters)); } - //clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); } private static NamenodeProtocols getRPCServer(NameNode namenode) diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/web/resources/ExceptionHandler.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/web/resources/ExceptionHandler.java index 038f9275f57c88..e911755099ac89 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/web/resources/ExceptionHandler.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/web/resources/ExceptionHandler.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hadoop.hdfs.web.JsonUtil; +import org.apache.hadoop.http.JettyUtils; import org.apache.hadoop.ipc.RemoteException; import org.apache.hadoop.ipc.StandbyException; import org.apache.hadoop.security.authorize.AuthorizationException; @@ -75,8 +76,7 @@ public Response toResponse(Exception e) { LOG.trace("GOT EXCEPITION", e); } - //clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); //Convert exception if (e instanceof ParamException) { diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestCheckpoint.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestCheckpoint.java index 8be6cbef5106f5..d62fed5a7cbf27 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestCheckpoint.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestCheckpoint.java @@ -87,7 +87,6 @@ import org.apache.hadoop.util.ExitUtil; import org.apache.hadoop.util.ExitUtil.ExitException; import org.apache.hadoop.util.Lists; -import org.apache.hadoop.util.Shell; import org.apache.hadoop.util.StringUtils; import org.slf4j.event.Level; import org.junit.jupiter.api.AfterEach; @@ -654,9 +653,16 @@ public void testNameNodeImageSendFailWrongSize() Mockito.doReturn(true).when(faultInjector) .shouldSendShortFile(filePathContaining("fsimage")); - String expectedText = Shell.isJavaVersionAtLeast(24) ? "Premature EOF" - : "is not of the advertised size"; - doSendFailTest(expectedText); + // Where a short send is caught decides how it reads. Jetty 12 fails the + // servlet's own write as soon as it delivers fewer bytes than the + // Content-Length it announced, so the namenode reports the shortfall + // ("written 200 < 399 content-length") rather than putting a truncated + // body on the wire for the secondary to notice. Under Jetty 9.4 the + // truncated body did go out, and the secondary reported it - as "is not + // of the advertised size", or "Premature EOF" on JDK 24 and later. The + // checkpoint fails either way without corrupting the namenode, which is + // what this test guards. + doSendFailTest("content-length"); } /** diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNetworkTopologyServlet.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNetworkTopologyServlet.java index 3a9119d350dfd0..2a52f1eb869459 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNetworkTopologyServlet.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestNetworkTopologyServlet.java @@ -23,19 +23,32 @@ import org.apache.hadoop.hdfs.HdfsConfiguration; import org.apache.hadoop.hdfs.MiniDFSCluster; import org.apache.hadoop.io.IOUtils; +import org.apache.hadoop.net.Node; import org.apache.hadoop.net.StaticMapping; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; public class TestNetworkTopologyServlet { @@ -202,4 +215,55 @@ public void testPrintTopologyNoDatanodesJsonFormat() throws IOException { assertTrue(topology.contains("No DataNodes")); } } + + /** + * A dump that fails half way through must not go out as a success. + *

+ * Rendering straight to the response stream committed the response before + * the failure was known - sendError then had nothing left to set, and a + * truncated topology was answered 200 OK. The response must stay untouched + * until the whole dump is in hand. + */ + @Test + public void testFailedDumpIsNotAnsweredAsSuccess() throws Exception { + NetworkTopologyServlet servlet = new NetworkTopologyServlet() { + @Override + protected void printTopology(PrintStream stream, List leaves, + String format) throws BadFormatException { + stream.print("half a topology"); + throw new BadFormatException("boom"); + } + }; + // a response that really accepts writes, so the old streaming shape would + // get as far as committing one + ByteArrayOutputStream written = new ByteArrayOutputStream(); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(response.getOutputStream()) + .thenReturn(new ServletOutputStream() { + @Override + public void write(int b) { + written.write(b); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener listener) { + } + }); + + IOException thrown = assertThrows(IOException.class, () -> + servlet.sendTopology(response, Collections.emptyList(), "text")); + + assertTrue(thrown.getMessage().contains("boom"), thrown.getMessage()); + verify(response).sendError(eq(HttpServletResponse.SC_GONE), + contains("boom")); + // the half-written dump never reached the wire, so nothing was committed + verify(response, never()).getOutputStream(); + verify(response, never()).setContentType(Mockito.anyString()); + assertEquals(0, written.size(), "a half-written dump reached the wire"); + } } diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/pom.xml b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/pom.xml index 4184166ebd3394..9583523ce553c5 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/pom.xml +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/pom.xml @@ -131,7 +131,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapreduce/v2/app/JobEndNotifier.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapreduce/v2/app/JobEndNotifier.java index ed49f82506bc6a..b98451223a664d 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapreduce/v2/app/JobEndNotifier.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapreduce/v2/app/JobEndNotifier.java @@ -32,7 +32,8 @@ import org.apache.hadoop.mapreduce.CustomJobEndNotifier; import org.apache.hadoop.mapreduce.MRJobConfig; import org.apache.hadoop.mapreduce.v2.api.records.JobReport; -import org.eclipse.jetty.util.log.Log; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

This class handles job end notification. Submitters of jobs can choose to @@ -48,6 +49,9 @@ * (eg. SUCCEEDED/KILLED/FAILED) */ public class JobEndNotifier implements Configurable { + private static final Logger LOG = + LoggerFactory.getLogger(JobEndNotifier.class); + private static final String JOB_ID = "$jobId"; private static final String JOB_STATUS = "$jobStatus"; @@ -109,11 +113,11 @@ public void setConf(Configuration conf) { int port = Integer.parseInt(portConf); proxyToUse = new Proxy(proxyType, new InetSocketAddress(hostname, port)); - Log.getLog().info("Job end notification using proxy type \"" + LOG.info("Job end notification using proxy type \"" + proxyType + "\" hostname \"" + hostname + "\" and port \"" + port + "\""); } catch(NumberFormatException nfe) { - Log.getLog().warn("Job end notification couldn't parse configured" + LOG.warn("Job end notification couldn't parse configured" + "proxy's port " + portConf + ". Not going to use a proxy"); } } @@ -141,24 +145,24 @@ protected boolean notifyURLOnce() { private boolean notifyViaBuiltInNotifier() { boolean success = false; try { - Log.getLog().info("Job end notification trying " + urlToNotify); + LOG.info("Job end notification trying " + urlToNotify); HttpURLConnection conn = (HttpURLConnection) urlToNotify.openConnection(proxyToUse); conn.setConnectTimeout(timeout); conn.setReadTimeout(timeout); conn.setAllowUserInteraction(false); if(conn.getResponseCode() != HttpURLConnection.HTTP_OK) { - Log.getLog().warn("Job end notification to " + urlToNotify + LOG.warn("Job end notification to " + urlToNotify + " failed with code: " + conn.getResponseCode() + " and message \"" + conn.getResponseMessage() + "\""); } else { success = true; - Log.getLog().info("Job end notification to " + urlToNotify + LOG.info("Job end notification to " + urlToNotify + " succeeded"); } } catch(IOException ioe) { - Log.getLog().warn("Job end notification to " + urlToNotify + " failed", + LOG.warn("Job end notification to " + urlToNotify + " failed", ioe); } return success; @@ -169,7 +173,7 @@ private boolean notifyViaBuiltInNotifier() { */ private boolean notifyViaCustomNotifier() { try { - Log.getLog().info("Will be using " + customJobEndNotifierClassName + LOG.info("Will be using " + customJobEndNotifierClassName + " for Job end notification"); final Class customJobEndNotifierClass = @@ -180,15 +184,15 @@ private boolean notifyViaCustomNotifier() { boolean success = customJobEndNotifier.notifyOnce(urlToNotify, conf); if (success) { - Log.getLog().info("Job end notification to " + urlToNotify + LOG.info("Job end notification to " + urlToNotify + " succeeded"); } else { - Log.getLog().warn("Job end notification to " + urlToNotify + LOG.warn("Job end notification to " + urlToNotify + " failed"); } return success; } catch (Exception e) { - Log.getLog().warn("Job end notification to " + urlToNotify + LOG.warn("Job end notification to " + urlToNotify + " failed", e); return false; } @@ -215,24 +219,24 @@ public void notify(JobReport jobReport) try { urlToNotify = new URL(userUrl); } catch (MalformedURLException mue) { - Log.getLog().warn("Job end notification couldn't parse " + userUrl, mue); + LOG.warn("Job end notification couldn't parse " + userUrl, mue); return; } // Send notification boolean success = false; while (numTries-- > 0 && !success) { - Log.getLog().info("Job end notification attempts left " + numTries); + LOG.info("Job end notification attempts left " + numTries); success = notifyURLOnce(); if (!success) { Thread.sleep(waitInterval); } } if (!success) { - Log.getLog().warn("Job end notification failed to notify : " + LOG.warn("Job end notification failed to notify : " + urlToNotify); } else { - Log.getLog().info("Job end notification succeeded for " + LOG.info("Job end notification succeeded for " + jobReport.getJobId()); } } diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapreduce/v2/app/webapp/AMWebServices.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapreduce/v2/app/webapp/AMWebServices.java index 2aa3dcb6441eff..549726a4537260 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapreduce/v2/app/webapp/AMWebServices.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-app/src/main/java/org/apache/hadoop/mapreduce/v2/app/webapp/AMWebServices.java @@ -115,8 +115,7 @@ Boolean hasAccess(Job job, HttpServletRequest request) { } private void init() { - //clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); } public static Job getJobFromContainerIdString(String cid, AppContext appCtx) diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-hs/src/main/java/org/apache/hadoop/mapreduce/v2/hs/webapp/HsWebServices.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-hs/src/main/java/org/apache/hadoop/mapreduce/v2/hs/webapp/HsWebServices.java index 3efa09ec0d2e51..fcb70552f01fd3 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-hs/src/main/java/org/apache/hadoop/mapreduce/v2/hs/webapp/HsWebServices.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-hs/src/main/java/org/apache/hadoop/mapreduce/v2/hs/webapp/HsWebServices.java @@ -150,8 +150,7 @@ private boolean isMRJobContainer(String containerIdStr) { } private void init() { - //clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); } @VisibleForTesting diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-jobclient/src/test/java/org/apache/hadoop/mapred/NotificationTestCase.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-jobclient/src/test/java/org/apache/hadoop/mapred/NotificationTestCase.java index 26feba37a0672f..5e044c8003bd1a 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-jobclient/src/test/java/org/apache/hadoop/mapred/NotificationTestCase.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-jobclient/src/test/java/org/apache/hadoop/mapred/NotificationTestCase.java @@ -20,8 +20,8 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.io.Text; diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-shuffle/src/main/java/org/apache/hadoop/mapred/ShuffleChannelHandler.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-shuffle/src/main/java/org/apache/hadoop/mapred/ShuffleChannelHandler.java index d0d0e74c9970b0..41f3e906b104e2 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-shuffle/src/main/java/org/apache/hadoop/mapred/ShuffleChannelHandler.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-shuffle/src/main/java/org/apache/hadoop/mapred/ShuffleChannelHandler.java @@ -61,7 +61,6 @@ import org.apache.hadoop.io.SecureIOUtils; import org.apache.hadoop.mapreduce.security.SecureShuffleUtils; import org.apache.hadoop.mapreduce.task.reduce.ShuffleHeader; -import org.eclipse.jetty.http.HttpHeader; import static io.netty.buffer.Unpooled.wrappedBuffer; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; @@ -128,6 +127,15 @@ * */ public class ShuffleChannelHandler extends SimpleChannelInboundHandler { + /** + * Spelled out rather than taken from a library so that the bytes on the wire + * do not depend on which library supplies the constant: Netty's + * HttpHeaderNames and HttpHeaderValues are lowercase AsciiStrings, and these + * headers have been sent capitalised since this handler was written. + */ + static final String CONNECTION = "Connection"; + static final String KEEP_ALIVE = "Keep-Alive"; + private final ShuffleChannelHandlerContext handlerCtx; ShuffleChannelHandler(ShuffleChannelHandlerContext ctx) { @@ -420,11 +428,10 @@ protected void populateHeaders(List mapIds, String jobId, protected void setResponseHeaders(HttpResponse response, boolean keepAliveParam, long contentLength) { if (!handlerCtx.connectionKeepAliveEnabled && !keepAliveParam) { - response.headers().set(HttpHeader.CONNECTION.asString(), CONNECTION_CLOSE); + response.headers().set(CONNECTION, CONNECTION_CLOSE); } else { - response.headers().set(HttpHeader.CONNECTION.asString(), - HttpHeader.KEEP_ALIVE.asString()); - response.headers().set(HttpHeader.KEEP_ALIVE.asString(), + response.headers().set(CONNECTION, KEEP_ALIVE); + response.headers().set(KEEP_ALIVE, "timeout=" + handlerCtx.connectionKeepAliveTimeOut); } diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-shuffle/src/test/java/org/apache/hadoop/mapred/TestShuffleChannelHandler.java b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-shuffle/src/test/java/org/apache/hadoop/mapred/TestShuffleChannelHandler.java index 07ec12a42b8868..e8260337073037 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-shuffle/src/test/java/org/apache/hadoop/mapred/TestShuffleChannelHandler.java +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoop-mapreduce-client-shuffle/src/test/java/org/apache/hadoop/mapred/TestShuffleChannelHandler.java @@ -81,7 +81,6 @@ import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem; import org.apache.hadoop.security.token.SecretManager; import org.apache.hadoop.security.token.Token; -import org.eclipse.jetty.http.HttpHeader; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; @@ -371,10 +370,11 @@ public DefaultHttpResponse getExpectedHttpResponse( headers.set(ShuffleHeader.HTTP_HEADER_NAME, ShuffleHeader.DEFAULT_HTTP_HEADER_NAME); headers.set(ShuffleHeader.HTTP_HEADER_VERSION, ShuffleHeader.DEFAULT_HTTP_HEADER_VERSION); if (keepAlive) { - headers.set(HttpHeader.CONNECTION.asString(), HttpHeader.KEEP_ALIVE.asString()); - headers.set(HttpHeader.KEEP_ALIVE.asString(), "timeout=" + ctx.connectionKeepAliveTimeOut); + headers.set(ShuffleChannelHandler.CONNECTION, + ShuffleChannelHandler.KEEP_ALIVE); + headers.set(ShuffleChannelHandler.KEEP_ALIVE, "timeout=" + ctx.connectionKeepAliveTimeOut); } else { - response.headers().set(HttpHeader.CONNECTION.asString(), CONNECTION_CLOSE); + response.headers().set(ShuffleChannelHandler.CONNECTION, CONNECTION_CLOSE); } HttpUtil.setContentLength(response, contentLength); return response; diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-client/pom.xml b/hadoop-mapreduce-project/hadoop-mapreduce-client/pom.xml index acca311eb53c03..0ec8950a4234e8 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-client/pom.xml +++ b/hadoop-mapreduce-project/hadoop-mapreduce-client/pom.xml @@ -161,7 +161,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-mapreduce-project/hadoop-mapreduce-examples/pom.xml b/hadoop-mapreduce-project/hadoop-mapreduce-examples/pom.xml index 1e02c05fe75b32..45041b3668c74a 100644 --- a/hadoop-mapreduce-project/hadoop-mapreduce-examples/pom.xml +++ b/hadoop-mapreduce-project/hadoop-mapreduce-examples/pom.xml @@ -101,7 +101,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-project/pom.xml b/hadoop-project/pom.xml index 79f9b8093daa48..02e29bda6e2a7c 100644 --- a/hadoop-project/pom.xml +++ b/hadoop-project/pom.xml @@ -36,7 +36,20 @@ true true - 9.4.58.v20250814 + + 12.1.12 + + 4.0.9 _ _ @@ -234,6 +247,8 @@ 3.9.0 4.11.0 8.11.2 + + 9.4.44.v20210927 2.2.5.Final 1.0.2 5.4.0 @@ -904,57 +919,89 @@ org.eclipse.jetty jetty-server ${jetty.version} - - - org.eclipse.jetty - javax.servlet-api - - org.eclipse.jetty - jetty-util + jetty-http ${jetty.version} org.eclipse.jetty - jetty-servlet + jetty-util + ${jetty.version} + + + + org.eclipse.jetty.ee8 + jetty-ee8-nested ${jetty.version} - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee8 + jetty-ee8-servlet ${jetty.version} - org.eclipse.jetty.websocket - javax-websocket-server-impl + org.eclipse.jetty.ee8 + jetty-ee8-webapp + ${jetty.version} + + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-server ${jetty.version} - org.ow2.asm - asm - - - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee8 + jetty-ee8-annotations - org.eclipse.jetty.websocket - websocket-client + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-client ${jetty.version} - jakarta.servlet.jsp - jakarta.servlet.jsp-api - 2.3.6 - - - jakarta.servlet - jakarta.servlet-api - 4.0.4 + org.eclipse.jetty.toolchain + jetty-servlet-api + ${jetty-servlet-api.version} @@ -1069,11 +1116,6 @@ ${commons-io.version} - - org.eclipse.jetty - jetty-servlet-tester - ${jetty.version} - commons-logging commons-logging @@ -2174,15 +2216,36 @@ org.glassfish.jersey.test-framework jersey-test-framework-core ${jersey2.version} + + + + jakarta.servlet + jakarta.servlet-api + + + org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http ${jersey2.version} + - javax.servlet - javax.servlet-api + jakarta.servlet + jakarta.servlet-api diff --git a/hadoop-tools/hadoop-resourceestimator/pom.xml b/hadoop-tools/hadoop-resourceestimator/pom.xml index 5bd5db3d5422df..ce05ed3c722f1c 100644 --- a/hadoop-tools/hadoop-resourceestimator/pom.xml +++ b/hadoop-tools/hadoop-resourceestimator/pom.xml @@ -67,12 +67,12 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api org.glassfish.jersey.core diff --git a/hadoop-tools/hadoop-sls/pom.xml b/hadoop-tools/hadoop-sls/pom.xml index af9a37b3cd61a9..2e9be81530d371 100644 --- a/hadoop-tools/hadoop-sls/pom.xml +++ b/hadoop-tools/hadoop-sls/pom.xml @@ -52,12 +52,16 @@ org.eclipse.jetty jetty-server provided - - - org.eclipse.jetty - javax.servlet-api - - + + + + org.eclipse.jetty.ee8 + jetty-ee8-nested + provided org.eclipse.jetty diff --git a/hadoop-tools/hadoop-sls/src/main/java/org/apache/hadoop/yarn/sls/web/SLSWebApp.java b/hadoop-tools/hadoop-sls/src/main/java/org/apache/hadoop/yarn/sls/web/SLSWebApp.java index ce36854bca2fde..ada2f83be51790 100644 --- a/hadoop-tools/hadoop-sls/src/main/java/org/apache/hadoop/yarn/sls/web/SLSWebApp.java +++ b/hadoop-tools/hadoop-sls/src/main/java/org/apache/hadoop/yarn/sls/web/SLSWebApp.java @@ -41,12 +41,13 @@ import org.apache.hadoop.yarn.sls.scheduler.SchedulerWrapper; import org.apache.hadoop.yarn.sls.utils.NodeUsageRanges; +import org.eclipse.jetty.ee8.nested.AbstractHandler; +import org.eclipse.jetty.ee8.nested.ContextHandler; +import org.eclipse.jetty.ee8.nested.Handler; +import org.eclipse.jetty.ee8.nested.Request; +import org.eclipse.jetty.ee8.nested.ResourceHandler; import org.eclipse.jetty.http.MimeTypes; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.server.handler.ResourceHandler; import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; @@ -179,8 +180,11 @@ public void handle(String target, Request baseRequest, } }; + ContextHandler context = new ContextHandler("/"); + context.setHandler(handler); + server = new Server(port); - server.setHandler(handler); + server.setHandler(context); server.start(); } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp/pom.xml index 724a91431c188c..94c26316e7c77b 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-applications-catalog/hadoop-yarn-applications-catalog-webapp/pom.xml @@ -39,6 +39,45 @@ *Spec + + + + + org.eclipse.jetty + jetty-server + ${solr.jetty.version} + + + org.eclipse.jetty + jetty-http + ${solr.jetty.version} + + + org.eclipse.jetty + jetty-util + ${solr.jetty.version} + + + org.eclipse.jetty + jetty-io + ${solr.jetty.version} + + + org.eclipse.jetty + jetty-client + ${solr.jetty.version} + + + + io.swagger @@ -87,25 +126,32 @@ test + org.eclipse.jetty.http2 http2-hpack - ${jetty.version} + ${solr.jetty.version} org.eclipse.jetty.http2 http2-client - ${jetty.version} + ${solr.jetty.version} org.eclipse.jetty.http2 http2-common - ${jetty.version} + ${solr.jetty.version} org.eclipse.jetty.http2 http2-http-client-transport - ${jetty.version} + ${solr.jetty.version} @@ -281,6 +327,36 @@ org.apache.hadoop hadoop-common + + + + org.eclipse.jetty.ee8 + jetty-ee8-servlet + + + org.eclipse.jetty.ee8 + jetty-ee8-webapp + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-server + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-client + + @@ -291,16 +367,70 @@ org.apache.hadoop hadoop-yarn-common + + + org.eclipse.jetty.ee8 + jetty-ee8-servlet + + + org.eclipse.jetty.ee8 + jetty-ee8-webapp + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-server + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-client + + org.apache.hadoop hadoop-yarn-services-core + + + org.eclipse.jetty.ee8 + jetty-ee8-servlet + + + org.eclipse.jetty.ee8 + jetty-ee8-webapp + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-server + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-client + + org.apache.hadoop hadoop-yarn-services-api + + + org.eclipse.jetty.ee8 + jetty-ee8-servlet + + + org.eclipse.jetty.ee8 + jetty-ee8-webapp + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-server + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-client + + diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/pom.xml index 4e5f602cf387ce..e27e7eadd670e8 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/pom.xml @@ -124,8 +124,8 @@ slf4j-api - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee8 + jetty-ee8-webapp com.google.inject @@ -136,8 +136,8 @@ jakarta.ws.rs-api - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api commons-codec @@ -173,8 +173,8 @@ jetty-util - org.eclipse.jetty - jetty-servlet + org.eclipse.jetty.ee8 + jetty-ee8-servlet org.mockito diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/client/ApiServiceClient.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/client/ApiServiceClient.java index 57d14efcc5a9c8..b85169b6460165 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/client/ApiServiceClient.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/client/ApiServiceClient.java @@ -21,6 +21,8 @@ import java.io.File; import java.io.IOException; import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.List; import java.util.Map; @@ -62,7 +64,6 @@ import org.apache.hadoop.yarn.service.conf.RestApiConstants; import org.apache.hadoop.yarn.service.utils.ServiceApiUtil; import org.apache.hadoop.yarn.util.RMHAUtils; -import org.eclipse.jetty.util.UrlEncoded; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -226,8 +227,13 @@ private void appendUserNameIfRequired(StringBuilder builder) .equalsIgnoreCase("simple")) { String username = UserGroupInformation.getCurrentUser() .getShortUserName(); - builder.append("?user.name=").append(UrlEncoded - .encodeString(username)); + // Not byte-for-byte what Jetty's UrlEncoded produced: it left '~' + // literal and escaped '*', and URLEncoder does the opposite. Both forms + // decode to the same user name, so what the server reads is unchanged; + // every other character a short user name can hold, '@' and non-ASCII + // among them, encodes identically. + builder.append("?user.name=").append( + URLEncoder.encode(username, StandardCharsets.UTF_8)); } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/webapp/ApiServerWebApp.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/webapp/ApiServerWebApp.java index f4acd942cc9fb0..a9c1bdfeadbcc4 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/webapp/ApiServerWebApp.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/main/java/org/apache/hadoop/yarn/service/webapp/ApiServerWebApp.java @@ -27,7 +27,7 @@ import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.webapp.GenericExceptionHandler; import org.apache.hadoop.yarn.webapp.YarnJacksonJaxbJsonProvider; -import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.ee8.webapp.Configuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestApiServiceClient.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestApiServiceClient.java index fe9c081ed64a51..e62bdc8e16ec2e 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestApiServiceClient.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestApiServiceClient.java @@ -32,8 +32,8 @@ import org.apache.hadoop.yarn.exceptions.YarnException; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestSecureApiServiceClient.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestSecureApiServiceClient.java index 60c06e9aa75f73..fb0b2a84cbedb7 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestSecureApiServiceClient.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-applications/hadoop-yarn-services/hadoop-yarn-services-api/src/test/java/org/apache/hadoop/yarn/service/client/TestSecureApiServiceClient.java @@ -46,8 +46,8 @@ import org.apache.log4j.Logger; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/pom.xml index 04a78a94a54e96..1ac9f6cf165fa2 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/pom.xml @@ -70,8 +70,8 @@ reload4j - org.eclipse.jetty.websocket - websocket-client + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-client @@ -91,7 +91,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/ContainerShellWebSocket.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/ContainerShellWebSocket.java index 5656484fca126b..7b83d72099e276 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/ContainerShellWebSocket.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/ContainerShellWebSocket.java @@ -20,15 +20,17 @@ import java.io.IOException; import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.ee8.websocket.api.Session; +import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.ee8.websocket.api.annotations.WebSocket; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; import org.jline.reader.LineReader; @@ -69,16 +71,16 @@ public void onText(Session session, String message) throws IOException { @OnWebSocketConnect public void onConnect(Session s) { initTerminal(s); - LOG.info("{} connected!", s.getRemoteAddress().getHostString()); + LOG.info("{} connected!", remoteHost(s)); } @OnWebSocketClose public void onClose(Session session, int status, String reason) { if (status==1000) { - LOG.info("{} closed, status: {}", session.getRemoteAddress().getHostString(), status); + LOG.info("{} closed, status: {}", remoteHost(session), status); } else { LOG.warn("{} closed, status:" + - " {} Reason: {}.", session.getRemoteAddress().getHostString(), status, reason); + " {} Reason: {}.", remoteHost(session), status, reason); } } @@ -101,11 +103,8 @@ public void run() { } inputThread.join(); } catch (IOException | InterruptedException e) { - try { - mySession.disconnect(); - } catch (IOException e1) { - LOG.error("Error closing connection: ", e1); - } + // Session.disconnect() no longer declares IOException as of Jetty 12. + mySession.disconnect(); } } @@ -159,4 +158,19 @@ public void run() { } } } + + /** + * Jetty 12 widened Session.getRemoteAddress() from InetSocketAddress to + * SocketAddress, so the host is read back out here for logging. + * + * @param session the WebSocket session + * @return the remote host, or the address as written if it is not an IP socket + */ + private static String remoteHost(Session session) { + SocketAddress remote = session.getRemoteAddress(); + return remote instanceof InetSocketAddress + ? ((InetSocketAddress) remote).getHostString() + : String.valueOf(remote); + } + } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/impl/YarnClientImpl.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/impl/YarnClientImpl.java index ca30ff865bf08e..538d0a8a9a226a 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/impl/YarnClientImpl.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/impl/YarnClientImpl.java @@ -143,10 +143,10 @@ import org.apache.hadoop.yarn.util.Records; import org.apache.hadoop.yarn.util.resource.ResourceUtils; import org.apache.hadoop.yarn.util.timeline.TimelineUtils; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WebSocketException; -import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; -import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.ee8.websocket.api.Session; +import org.eclipse.jetty.ee8.websocket.api.exceptions.WebSocketException; +import org.eclipse.jetty.ee8.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.ee8.websocket.client.WebSocketClient; import org.apache.hadoop.classification.VisibleForTesting; import org.slf4j.Logger; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/api/impl/TestAMRMClient.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/api/impl/TestAMRMClient.java index 2da782ea6db1c0..524f5240d7579a 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/api/impl/TestAMRMClient.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/api/impl/TestAMRMClient.java @@ -87,14 +87,18 @@ import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import org.eclipse.jetty.util.log.Log; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Test application master client class to resource manager. */ public class TestAMRMClient extends BaseAMRMClientTest{ + private static final Logger LOG = + LoggerFactory.getLogger(TestAMRMClient.class); + private final static int DEFAULT_ITERATION = 15; @@ -555,7 +559,7 @@ public void testAMRMClientMatchStorage(String pSchedulerName, int iterationsLeft = 15; while (allocatedContainerCount < 2 && iterationsLeft-- > 0) { - Log.getLog().info("Allocated " + allocatedContainerCount + " containers" + LOG.info("Allocated " + allocatedContainerCount + " containers" + " with " + iterationsLeft + " iterations left"); AllocateResponse allocResponse = amClient.allocate(0.1f); assertEquals(0, amClient.ask.size()); @@ -752,7 +756,7 @@ private int getAllocatedContainersNumber( throws YarnException, IOException { int allocatedContainerCount = 0; while (iterationsLeft-- > 0) { - Log.getLog().info("Allocated " + allocatedContainerCount + " containers" + LOG.info("Allocated " + allocatedContainerCount + " containers" + " with " + iterationsLeft + " iterations left"); AllocateResponse allocResponse = amClient.allocate(0.1f); assertEquals(0, amClient.ask.size()); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java index 1848a921222e0b..a984c80bb69534 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java @@ -106,7 +106,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.eclipse.jetty.util.log.Log; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -385,11 +384,11 @@ public void testGetContainers() throws Exception { "http://host:2345", ""); pw.close(); String appReportStr = baos.toString("UTF-8"); - Log.getLog().info("ExpectedOutput"); - Log.getLog().info("["+appReportStr+"]"); - Log.getLog().info("OutputFrom command"); + LOG.info("ExpectedOutput"); + LOG.info("["+appReportStr+"]"); + LOG.info("OutputFrom command"); String actualOutput = sysOutStream.toString("UTF-8"); - Log.getLog().info("["+actualOutput+"]"); + LOG.info("["+actualOutput+"]"); assertEquals(appReportStr, actualOutput); } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/pom.xml index fdfec1d7d1ca6a..943f5e49cf8c8e 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/pom.xml @@ -65,8 +65,8 @@ commons-compress - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api commons-codec @@ -148,7 +148,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/GenericExceptionHandler.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/GenericExceptionHandler.java index 00b725e06cf571..acc6ffad4ee2ff 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/GenericExceptionHandler.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/GenericExceptionHandler.java @@ -32,6 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.http.JettyUtils; import org.apache.hadoop.ipc.RemoteException; import org.apache.hadoop.security.authorize.AuthorizationException; @@ -61,8 +62,7 @@ public Response toResponse(Exception e) { if (e instanceof javax.ws.rs.NotFoundException) { return ((javax.ws.rs.NotFoundException) e).getResponse(); } - // clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); // Convert exception if (e instanceof RemoteException) { diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java index 27600f24211de5..b25751d3cec708 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-common/src/main/java/org/apache/hadoop/yarn/webapp/WebApps.java @@ -29,6 +29,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import javax.servlet.http.HttpServlet; @@ -44,7 +45,7 @@ import org.apache.hadoop.yarn.api.ApplicationClientProtocol; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.webapp.util.WebAppUtils; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletProperties; import org.slf4j.Logger; @@ -108,6 +109,8 @@ static class ServletStruct { private final HashMap attributes = new HashMap<>(); private ApplicationClientProtocol appClientProtocol; private ResourceConfig config; + private final List> serverConfigurers = + new ArrayList<>(); Builder(String name, Class api, T application, String wsName) { this.name = name; this.api = api; @@ -223,6 +226,24 @@ public Builder withCSRFProtection(String prefix) { return this; } + /** + * Register work to run against the built server before it is started. + *

+ * Some servlets have to be set up while their context is still stopped - + * a Jetty WebSocket servlet needs the WebSocket components installed on + * the context, and installing them once the context has started throws. + * Builder gives no other window between build and start, and the work is + * specific enough to a single webapp that pulling its dependencies into + * this module would be the wrong trade. + * + * @param configurer work to run on the server before it starts + * @return this builder + */ + public Builder withServerConfigurer(Consumer configurer) { + this.serverConfigurers.add(configurer); + return this; + } + /** * Enable the XFS filter. * @param prefix The config prefix that identifies the @@ -492,11 +513,15 @@ public WebApp start(WebApp webapp, WebAppContext... additionalContexts) { for (WebAppContext context : additionalContexts) { if (context != null) { addFiltersForNewContext(context); - httpServer.addHandlerAtFront(context); + httpServer.addHandlerAtFront(context.getCoreContextHandler()); } } } + for (Consumer configurer : serverConfigurers) { + configurer.accept(httpServer); + } + try { httpServer.start(); LOG.info("Web app {} started at {}.", name, httpServer.getConnectorAddress(0).getPort()); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/pom.xml index b35bb52121684e..adfef58d0f285d 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/pom.xml @@ -38,8 +38,8 @@ - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api @@ -53,6 +53,21 @@ org.apache.hadoop hadoop-annotations + + + + org.eclipse.jetty.ee8 + jetty-ee8-webapp + provided + + + + org.eclipse.jetty.ee8 + jetty-ee8-servlet + provided + org.mockito mockito-inline @@ -92,7 +107,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/main/java/org/apache/hadoop/yarn/server/applicationhistoryservice/ApplicationHistoryServer.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/main/java/org/apache/hadoop/yarn/server/applicationhistoryservice/ApplicationHistoryServer.java index 06f21f4ae747ba..e2a75b32788d49 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/main/java/org/apache/hadoop/yarn/server/applicationhistoryservice/ApplicationHistoryServer.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/main/java/org/apache/hadoop/yarn/server/applicationhistoryservice/ApplicationHistoryServer.java @@ -64,8 +64,8 @@ import org.apache.hadoop.yarn.webapp.WebApps; import org.apache.hadoop.yarn.webapp.YarnJacksonJaxbJsonProvider; import org.apache.hadoop.yarn.webapp.util.WebAppUtils; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.servlet.FilterHolder; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import org.apache.hadoop.classification.VisibleForTesting; import org.glassfish.jersey.internal.inject.AbstractBinder; @@ -323,7 +323,7 @@ private void startWebApp() { } } LOG.info("Hosting {} from {} at {}.", name, onDiskPath, webPath); - httpServer.addHandlerAtFront(uiWebAppContext); + httpServer.addHandlerAtFront(uiWebAppContext.getCoreContextHandler()); } httpServer.start(); conf.updateConnectAddr(YarnConfiguration.TIMELINE_SERVICE_BIND_HOST, diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/main/java/org/apache/hadoop/yarn/server/timeline/webapp/TimelineWebServices.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/main/java/org/apache/hadoop/yarn/server/timeline/webapp/TimelineWebServices.java index 7a38c59b002e0a..bcd49c501a77cc 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/main/java/org/apache/hadoop/yarn/server/timeline/webapp/TimelineWebServices.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/main/java/org/apache/hadoop/yarn/server/timeline/webapp/TimelineWebServices.java @@ -360,7 +360,7 @@ public TimelineDomains getDomains( } private void init(HttpServletResponse response) { - response.setContentType(null); + JettyUtils.clearContentType(response); } private static UserGroupInformation getUser(HttpServletRequest req) { diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/test/java/org/apache/hadoop/yarn/server/timeline/TestRollingLevelDBTimelineStore.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/test/java/org/apache/hadoop/yarn/server/timeline/TestRollingLevelDBTimelineStore.java index 6d37a9730e1d51..71cc77d4bf5ce9 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/test/java/org/apache/hadoop/yarn/server/timeline/TestRollingLevelDBTimelineStore.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-applicationhistoryservice/src/test/java/org/apache/hadoop/yarn/server/timeline/TestRollingLevelDBTimelineStore.java @@ -21,7 +21,6 @@ import java.io.FilenameFilter; import java.io.IOException; -import org.eclipse.jetty.util.log.Log; import org.fusesource.leveldbjni.JniDBFactory; import org.iq80.leveldb.Options; import org.junit.jupiter.api.AfterEach; @@ -50,11 +49,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Test class to verify RollingLevelDBTimelineStore. */ @InterfaceAudience.Private @InterfaceStability.Unstable public class TestRollingLevelDBTimelineStore extends TimelineStoreTestUtils { + private static final Logger LOG = + LoggerFactory.getLogger(TestRollingLevelDBTimelineStore.class); + private FileContext fsContext; private File fsPath; private Configuration config = new YarnConfiguration(); @@ -375,7 +379,7 @@ public void testStorePerformance() throws IOException { long start = System.currentTimeMillis(); int num = 1000000; - Log.getLog().info("Start test for " + num); + LOG.info("Start test for " + num); final String tezTaskAttemptId = "TEZ_TA"; final String tezEntityId = "attempt_1429158534256_0001_1_00_000000_"; @@ -421,7 +425,7 @@ public void testStorePerformance() throws IOException { } long duration = System.currentTimeMillis() - start; - Log.getLog().info("Duration for " + num + ": " + duration); + LOG.info("Duration for " + num + ": " + duration); } /** diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/pom.xml index fe166cd1c1abbb..1c1c243579735e 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/pom.xml @@ -119,8 +119,8 @@ leveldbjni-all - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api javax.cache diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/LogWebService.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/LogWebService.java index c537df226a0248..e8783866b49072 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/LogWebService.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/LogWebService.java @@ -122,8 +122,7 @@ private Client createTimelineWebClient() { } private void initForReadableEndpoints(HttpServletResponse response) { - // clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); } /** diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/WebServices.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/WebServices.java index f62a490dd2d0b6..cbf9031f725946 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/WebServices.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/WebServices.java @@ -32,6 +32,7 @@ import javax.ws.rs.WebApplicationException; import org.apache.commons.lang3.Range; +import org.apache.hadoop.http.JettyUtils; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authorize.AuthorizationException; import org.apache.hadoop.util.StringUtils; @@ -418,8 +419,7 @@ public ContainerInfo getContainer(HttpServletRequest req, } protected void initForReadableEndpoints(HttpServletResponse response) { - // clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); } public static Set diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-globalpolicygenerator/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-globalpolicygenerator/pom.xml index 05913c2832733b..e4aea97967047a 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-globalpolicygenerator/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-globalpolicygenerator/pom.xml @@ -119,13 +119,13 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/pom.xml index afde1fa415a6bc..be795b3e228bee 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/pom.xml @@ -70,8 +70,8 @@ jettison - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api commons-codec @@ -82,8 +82,18 @@ jetty-util - org.eclipse.jetty.websocket - javax-websocket-server-impl + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-server + + + + org.eclipse.jetty.ee8.websocket + jetty-ee8-websocket-jetty-client + test org.apache.hadoop.thirdparty @@ -203,7 +213,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/amrmproxy/FederationInterceptor.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/amrmproxy/FederationInterceptor.java index 86d78f2fc351ae..18376bc939549e 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/amrmproxy/FederationInterceptor.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/amrmproxy/FederationInterceptor.java @@ -99,7 +99,6 @@ import org.apache.hadoop.yarn.util.ConverterUtils; import org.apache.hadoop.yarn.util.MonotonicClock; import org.apache.hadoop.yarn.util.resource.Resources; -import org.eclipse.jetty.util.ConcurrentHashSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -290,7 +289,7 @@ public FederationInterceptor() { this.finishAMCalled = false; this.lastSCResponseTime = new ConcurrentHashMap<>(); this.lastAMHeartbeatTime = this.clock.getTime(); - this.nmTokenMapFromRegisterSecondaryCluster = new ConcurrentHashSet<>(); + this.nmTokenMapFromRegisterSecondaryCluster = ConcurrentHashMap.newKeySet(); } /** diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellWebSocket.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellWebSocket.java index 175ee09f8db889..461e85eebffe65 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellWebSocket.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellWebSocket.java @@ -20,6 +20,8 @@ import java.io.IOException; import java.net.URI; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -33,11 +35,11 @@ import org.apache.hadoop.yarn.server.nodemanager.ContainerExecutor; import org.apache.hadoop.yarn.server.nodemanager.containermanager.container.Container; import org.apache.hadoop.yarn.server.nodemanager.executor.ContainerExecContext; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.ee8.websocket.api.Session; +import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.ee8.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.ee8.websocket.api.annotations.WebSocket; import org.apache.hadoop.hdfs.protocol.datatransfer.IOStreamPair; import org.apache.hadoop.security.HadoopKerberosName; import org.apache.hadoop.security.UserGroupInformation; @@ -120,7 +122,7 @@ public void onConnect(Session session) { session.close(1003, "Nonsecure mode is unsupported."); return; } - LOG.info(session.getRemoteAddress().getHostString() + " connected!"); + LOG.info(remoteHost(session) + " connected!"); LOG.info( "Making interactive connection to running docker container with ID: " + cId); @@ -140,7 +142,7 @@ public void onConnect(Session session) { @OnWebSocketClose public void onClose(Session session, int status, String reason) { try { - LOG.info(session.getRemoteAddress().getHostString() + " closed!"); + LOG.info(remoteHost(session) + " closed!"); String exit = "exit\r\n"; pair.out.write(exit.getBytes(StandardCharsets.UTF_8)); pair.out.flush(); @@ -195,4 +197,19 @@ private boolean checkInsecureSetup() { } return limitUsers; } + + /** + * Jetty 12 widened Session.getRemoteAddress() from InetSocketAddress to + * SocketAddress, so the host is read back out here for logging. + * + * @param session the WebSocket session + * @return the remote host, or the address as written if it is not an IP socket + */ + private static String remoteHost(Session session) { + SocketAddress remote = session.getRemoteAddress(); + return remote instanceof InetSocketAddress + ? ((InetSocketAddress) remote).getHostString() + : String.valueOf(remote); + } + } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellWebSocketServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellWebSocketServlet.java index 8a8d6d102abf70..d82454b15249e0 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellWebSocketServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellWebSocketServlet.java @@ -20,17 +20,17 @@ import javax.servlet.annotation.WebServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServletFactory; /** * Container shell web socket interface. */ @WebServlet(urlPatterns="/container/container/*") -public class ContainerShellWebSocketServlet extends WebSocketServlet{ +public class ContainerShellWebSocketServlet extends JettyWebSocketServlet { @Override - public void configure(WebSocketServletFactory factory) { + protected void configure(JettyWebSocketServletFactory factory) { factory.register(ContainerShellWebSocket.class); } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/NMWebServices.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/NMWebServices.java index 25a7c91a020882..5e5227d5ba20c7 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/NMWebServices.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/NMWebServices.java @@ -141,8 +141,7 @@ public NMWebServices(final Context nm, final ResourceView view, } private void init() { - //clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); } @GET diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/TerminalServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/TerminalServlet.java index ec1f9be13fb005..ac6be36d4b8913 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/TerminalServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/TerminalServlet.java @@ -24,7 +24,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.ee8.servlet.DefaultServlet; /** * TerminalServlet host the static html and javascript for diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/WebServer.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/WebServer.java index eb7436b6a109d7..b920d42c8464bb 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/WebServer.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/java/org/apache/hadoop/yarn/server/nodemanager/webapp/WebServer.java @@ -40,6 +40,7 @@ import org.apache.hadoop.yarn.webapp.WebApps; import org.apache.hadoop.yarn.webapp.YarnWebParams; import org.apache.hadoop.yarn.webapp.util.WebAppUtils; +import org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer; import javax.servlet.Filter; import org.glassfish.jersey.internal.inject.AbstractBinder; @@ -138,6 +139,16 @@ protected void serviceStart() throws Exception { .at(bindAddress) .withServlet("ContainerShellWebSocket", "/container/*", ContainerShellWebSocketServlet.class, params, false) + // Jetty 9.4 let a WebSocketServlet bootstrap itself from init(). + // Jetty 12 keeps the WebSocket components on the context and expects + // a ServletContainerInitializer to have put them there, which only + // runs if jetty-ee8-annotations is on the classpath - it is excluded + // here because it drags in a banned asm. Installing them directly is + // the same work without the scan, and has to happen while the + // context is still stopped. + .withServerConfigurer(server -> + JettyWebSocketServletContainerInitializer.configure( + server.getWebAppContext(), null)) .withServlet("Terminal", "/terminal/*", TerminalServlet.class, terminalParams, false) .with(conf) diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/docker/TestDockerCommandExecutor.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/docker/TestDockerCommandExecutor.java index 5c2ba9c0942a46..20913b208c5def 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/docker/TestDockerCommandExecutor.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/linux/runtime/docker/TestDockerCommandExecutor.java @@ -46,7 +46,6 @@ import static org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.LinuxContainerRuntimeConstants.CONTAINER_ID_STR; import static org.apache.hadoop.yarn.server.nodemanager.containermanager.linux.runtime.docker.DockerCommandExecutor.DockerContainerStatus; -import static org.eclipse.jetty.server.handler.gzip.GzipHttpOutputInterceptor.LOG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -55,11 +54,16 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Test common docker commands. */ public class TestDockerCommandExecutor { + private static final Logger LOG = + LoggerFactory.getLogger(TestDockerCommandExecutor.class); + private static final String MOCK_CONTAINER_ID = "container_e11_1861047502093_13763105_01_000001"; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/logaggregation/TestLogAggregationService.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/logaggregation/TestLogAggregationService.java index a90172a096007c..a77d2346c59c90 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/logaggregation/TestLogAggregationService.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/containermanager/logaggregation/TestLogAggregationService.java @@ -156,7 +156,7 @@ import org.junit.jupiter.api.Timeout; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; -import org.eclipse.jetty.util.MultiException; +import org.eclipse.jetty.util.ExceptionUtil; import java.util.function.Supplier; import org.slf4j.LoggerFactory; @@ -1456,7 +1456,7 @@ void checkEvents(EventHandler eventHandler, List actualEvents = eventCaptor.getAllValues(); // batch up exceptions so junit presents them as one - MultiException failures = new MultiException(); + ExceptionUtil.MultiException failures = new ExceptionUtil.MultiException(); try { assertEquals(expectedEvents.length, actualEvents.size(), "expected events"); } catch (Throwable e) { diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellClientSocketTest.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellClientSocketTest.java index e059de90b7fdd3..9e4484e69e39cd 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellClientSocketTest.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/webapp/ContainerShellClientSocketTest.java @@ -18,9 +18,9 @@ package org.apache.hadoop.yarn.server.nodemanager.webapp; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WebSocketAdapter; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.ee8.websocket.api.Session; +import org.eclipse.jetty.ee8.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.ee8.websocket.api.annotations.WebSocket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/webapp/TestNMContainerWebSocket.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/webapp/TestNMContainerWebSocket.java index 87c1f782112b08..31a1bb21ba6c38 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/webapp/TestNMContainerWebSocket.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/test/java/org/apache/hadoop/yarn/server/nodemanager/webapp/TestNMContainerWebSocket.java @@ -28,14 +28,15 @@ import org.apache.hadoop.yarn.server.nodemanager.containermanager.container.Container; import org.apache.hadoop.yarn.server.nodemanager.health.NodeHealthCheckerService; import org.apache.hadoop.yarn.server.security.ApplicationACLsManager; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.UpgradeRequest; -import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.ee8.websocket.api.Session; +import org.eclipse.jetty.ee8.websocket.api.UpgradeRequest; +import org.eclipse.jetty.ee8.websocket.client.WebSocketClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; @@ -48,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; /** * Test class for Node Manager Container Web Socket. @@ -125,31 +127,36 @@ private NodeHealthCheckerService createNodeHealthCheckerService() { return new NodeHealthCheckerService(dirsHandler); } + /** + * The container shell endpoint completes a WebSocket upgrade. + *

+ * What is asserted is the handshake, not a working shell: there is no + * container "abc" here, so ContainerShellWebSocket closes the session + * shortly after it opens. Getting a Session at all is the interesting part, + * because it is the whole Jetty upgrade path - and it is what breaks, with a + * 500, when the WebSocket components are missing from the context. + */ @Test - public void testWebServerWithServlet() { + public void testWebServerWithServlet() throws Exception { int port = startNMWebAppServer("0.0.0.0"); LOG.info("bind to port: " + port); - StringBuilder sb = new StringBuilder(); - sb.append("ws://localhost:").append(port).append("/container/abc/"); - String dest = sb.toString(); + String dest = "ws://localhost:" + port + "/container/abc/"; WebSocketClient client = new WebSocketClient(); try { - ContainerShellClientSocketTest socket = new ContainerShellClientSocketTest(); + ContainerShellClientSocketTest socket = + new ContainerShellClientSocketTest(); client.start(); - URI echoUri = new URI(dest); - Future future = client.connect(socket, echoUri); - Session session = future.get(); - session.getRemote().sendString("hello world"); + Future future = client.connect(socket, new URI(dest)); + Session session = future.get(30, TimeUnit.SECONDS); + assertNotNull(session, "WebSocket upgrade produced no session"); + assertTrue(socket.getLatch().await(30, TimeUnit.SECONDS), + "the client was never told the WebSocket had connected"); session.close(); - client.stop(); - } catch (Throwable t) { - LOG.error("Failed to connect WebSocket and send message to server", t); } finally { try { client.stop(); + } finally { server.close(); - } catch (Exception e) { - LOG.error("Failed to close client", e); } } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/pom.xml index 40dcf5d390b558..65e406d132ad72 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/pom.xml @@ -33,8 +33,8 @@ - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api @@ -244,7 +244,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ResourceManager.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ResourceManager.java index 7b1042d101ff5d..c6d7d366c2767f 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ResourceManager.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/ResourceManager.java @@ -139,7 +139,7 @@ import org.apache.hadoop.yarn.webapp.WebApps.Builder; import org.apache.hadoop.yarn.webapp.util.WebAppUtils; import org.apache.zookeeper.server.auth.DigestAuthenticationProvider; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import java.io.IOException; import java.io.InputStream; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWebServices.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWebServices.java index b6ea2d438c0b41..8a6c52bd170dfc 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWebServices.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/RMWebServices.java @@ -328,8 +328,7 @@ protected Boolean hasAccess(RMApp app, HttpServletRequest hsr) { * initForReadableEndpoints does the init for all readable REST end points. */ private void initForReadableEndpoints() { - // clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); } /** @@ -345,8 +344,7 @@ private void initForReadableEndpoints() { */ private void initForWritableEndpoints(UserGroupInformation callerUGI, boolean doAdminACLsCheck) throws AuthorizationException { - // clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); if (callerUGI == null) { String msg = "Unable to obtain user name, user not authenticated"; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/MockNM.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/MockNM.java index d433753701b25e..7b3b3e51daff2d 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/MockNM.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/MockNM.java @@ -55,9 +55,13 @@ import org.apache.hadoop.yarn.util.Records; import org.apache.hadoop.yarn.util.YarnVersionInfo; import org.apache.hadoop.yarn.util.resource.Resources; -import org.eclipse.jetty.util.log.Log; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class MockNM { + private static final Logger LOG = + LoggerFactory.getLogger(MockNM.class); + private int responseId; private NodeId nodeId; @@ -238,7 +242,7 @@ public NodeHeartbeatResponse nodeHeartbeat(ApplicationAttemptId attemptId, ArrayList containerStatusList = new ArrayList(1); containerStatusList.add(containerStatus); - Log.getLog().info("ContainerStatus: " + containerStatus); + LOG.info("ContainerStatus: " + containerStatus); return nodeHeartbeat(containerStatusList, Collections.emptyList(), true, responseId); } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/pom.xml index 67fc6348f7edc0..67efbc2a97d1e4 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/pom.xml @@ -60,6 +60,19 @@ hadoop-yarn-server-common + + + org.eclipse.jetty.ee8 + jetty-ee8-webapp + + org.apache.hadoop hadoop-yarn-server-common diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/Router.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/Router.java index 8f3c4d0fe577e0..ffeaca543a6bc2 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/Router.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/Router.java @@ -67,7 +67,7 @@ import org.apache.hadoop.yarn.webapp.WebApps.Builder; import org.apache.hadoop.yarn.webapp.util.WebAppUtils; import org.apache.hadoop.yarn.webapp.util.WebServiceClient; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/webapp/RouterWebServices.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/webapp/RouterWebServices.java index 1266598ff0e27e..e53c92d1cc2d82 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/webapp/RouterWebServices.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/main/java/org/apache/hadoop/yarn/server/router/webapp/RouterWebServices.java @@ -138,8 +138,7 @@ public RouterWebServices(final @Named("router") Router router, } private void init() { - // clear content type - response.setContentType(null); + JettyUtils.clearContentType(response); } @VisibleForTesting diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/TestRouter.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/TestRouter.java index 10ef06c8125f56..6da068f9dcb695 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/TestRouter.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/TestRouter.java @@ -33,9 +33,9 @@ import org.apache.hadoop.test.LambdaTestUtils; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.webapp.WebApp; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee8.servlet.FilterHolder; +import org.eclipse.jetty.ee8.servlet.ServletHandler; +import org.eclipse.jetty.ee8.webapp.WebAppContext; import org.junit.jupiter.api.Test; import javax.servlet.FilterChain; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/webapp/TestRouterWebAppProxy.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/webapp/TestRouterWebAppProxy.java index d6b87225065ee0..f319b0f0942cab 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/webapp/TestRouterWebAppProxy.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-router/src/test/java/org/apache/hadoop/yarn/server/router/webapp/TestRouterWebAppProxy.java @@ -41,8 +41,8 @@ import org.apache.hadoop.yarn.server.webproxy.FedAppReportFetcher; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice-hbase-tests/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice-hbase-tests/pom.xml index 9b6f8f4a79e967..cbd9a0909c4302 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice-hbase-tests/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice-hbase-tests/pom.xml @@ -222,7 +222,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http org.glassfish.jersey.test-framework @@ -460,13 +460,13 @@ dependency --> - org.eclipse.jetty - jetty-servlet + org.eclipse.jetty.ee8 + jetty-ee8-servlet test - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee8 + jetty-ee8-webapp test diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/pom.xml index 180b15713b4268..2206ed41874d35 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/pom.xml @@ -88,8 +88,8 @@ - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/src/main/java/org/apache/hadoop/yarn/server/timelineservice/collector/TimelineCollectorWebService.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/src/main/java/org/apache/hadoop/yarn/server/timelineservice/collector/TimelineCollectorWebService.java index 6f95f3ed9edf3f..1fcfc46a9f1c32 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/src/main/java/org/apache/hadoop/yarn/server/timelineservice/collector/TimelineCollectorWebService.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/src/main/java/org/apache/hadoop/yarn/server/timelineservice/collector/TimelineCollectorWebService.java @@ -279,7 +279,7 @@ private static ApplicationId parseApplicationId(String appId) { } private static void init(HttpServletResponse response) { - response.setContentType(null); + JettyUtils.clearContentType(response); } private static UserGroupInformation getUser(HttpServletRequest req) { diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/src/main/java/org/apache/hadoop/yarn/server/timelineservice/reader/TimelineReaderWebServices.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/src/main/java/org/apache/hadoop/yarn/server/timelineservice/reader/TimelineReaderWebServices.java index a1c2e194216a1b..7f5d334a0c8320 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/src/main/java/org/apache/hadoop/yarn/server/timelineservice/reader/TimelineReaderWebServices.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-timelineservice/src/main/java/org/apache/hadoop/yarn/server/timelineservice/reader/TimelineReaderWebServices.java @@ -98,7 +98,7 @@ protected DateFormat initialValue() { }; private void init(HttpServletResponse response) { - response.setContentType(null); + JettyUtils.clearContentType(response); } private static final class DateRange { diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/pom.xml b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/pom.xml index 6c64dce18ebbee..e48c2d965761c3 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/pom.xml +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/pom.xml @@ -33,8 +33,8 @@ - jakarta.servlet - jakarta.servlet-api + org.eclipse.jetty.toolchain + jetty-servlet-api compile @@ -123,7 +123,7 @@ org.glassfish.jersey.test-framework.providers - jersey-test-framework-provider-jetty + jersey-test-framework-provider-jdk-http test diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java index b1349df26de2b9..90086d0ae57c71 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServlet.java @@ -45,8 +45,8 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServletFed.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServletFed.java index d3cc76951527ef..3906b7581b1d6a 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServletFed.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/TestWebAppProxyServletFed.java @@ -54,8 +54,8 @@ import org.apache.hadoop.yarn.webapp.util.WebAppUtils; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/amfilter/TestAmFilter.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/amfilter/TestAmFilter.java index 1b3545c767d966..ad9d2a74551c58 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/amfilter/TestAmFilter.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/amfilter/TestAmFilter.java @@ -46,8 +46,8 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.eclipse.jetty.ee8.servlet.ServletHolder; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout;