diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 21bf060b016e..306fa2b8c6f8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,7 +16,7 @@ updates: # Provided by Jetty and should be aligned with the version provided by the # version of Jetty we deliver. See: # https://github.com/jenkinsci/jenkins/pull/5211 - - dependency-name: "javax.servlet:javax.servlet-api" + - dependency-name: "jakarta.servlet:jakarta.servlet-api" # Jetty Maven Plugin and Winstone should be upgraded in lockstep in order # to keep their corresponding Jetty versions aligned. diff --git a/bom/pom.xml b/bom/pom.xml index 6b8d1b3c6246..3b8e92c10a76 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -40,7 +40,7 @@ THE SOFTWARE. 9.3 1.7.36 - 1685.v3b_5035c4ce05 + 1711.v5b_1b_03f0fcf2 2.4.21 diff --git a/core/pom.xml b/core/pom.xml index fe1415c82182..1f98d7b2ff42 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -477,9 +477,9 @@ THE SOFTWARE. 1.1.4c - javax.servlet - javax.servlet-api - 3.1.0 + jakarta.servlet + jakarta.servlet-api + 4.0.4 provided diff --git a/pom.xml b/pom.xml index ee1dafc4a6de..ec9e2ffaa468 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,7 @@ THE SOFTWARE. bom websocket/spi websocket/jetty9 + websocket/jetty10 core war test diff --git a/src/spotbugs/spotbugs-excludes.xml b/src/spotbugs/spotbugs-excludes.xml index 50f646752f88..4d568e0320b8 100644 --- a/src/spotbugs/spotbugs-excludes.xml +++ b/src/spotbugs/spotbugs-excludes.xml @@ -658,6 +658,7 @@ + diff --git a/war/pom.xml b/war/pom.xml index 1db05dc7b875..62f312b03443 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -86,8 +86,8 @@ THE SOFTWARE. jars that are not needed in war. most of the exclusions should happen in the core, to make IDEs happy, not here. --> - javax.servlet.jsp - javax.servlet.jsp-api + jakarta.servlet.jsp + jakarta.servlet.jsp-api @@ -96,6 +96,11 @@ THE SOFTWARE. + + org.jenkins-ci.main + websocket-jetty10 + ${project.version} + org.jenkins-ci.main websocket-jetty9 @@ -137,7 +142,7 @@ THE SOFTWARE. org.jenkins-ci winstone - 5.25 + 6.1 test @@ -177,10 +182,15 @@ THE SOFTWARE. 1.8 + org.jenkins-ci:commons-jelly org.jenkins-ci.main:cli org.jenkins-ci.main:jenkins-core - org.jenkins-ci.main:websocket-jetty9 org.jenkins-ci.main:websocket-spi + org.jenkins-ci.main:websocket-jetty9 + org.jenkins-ci.main:websocket-jetty10 + org.kohsuke.stapler:stapler + org.kohsuke.stapler:stapler-groovy + org.kohsuke.stapler:stapler-jelly @@ -567,7 +577,7 @@ THE SOFTWARE. org.eclipse.jetty jetty-maven-plugin - 9.4.46.v20220331 + 10.0.11 + + 4.0.0 + + + org.jenkins-ci.main + jenkins-parent + ${revision}${changelist} + ../.. + + + websocket-jetty10 + Jetty 10 implementation for WebSocket + An implementation of the WebSocket handler that works with Jetty 10. + + + + + org.jenkins-ci.main + jenkins-bom + ${project.version} + pom + import + + + + + + + org.jenkins-ci + winstone + 6.0 + true + + + org.jenkins-ci.main + websocket-spi + ${project.version} + + + org.kohsuke + access-modifier-annotation + + + org.kohsuke.metainf-services + metainf-services + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + + + + + diff --git a/websocket/jetty10/src/main/java/jenkins/websocket/Jetty10Provider.java b/websocket/jetty10/src/main/java/jenkins/websocket/Jetty10Provider.java new file mode 100644 index 000000000000..a6b98ead54a7 --- /dev/null +++ b/websocket/jetty10/src/main/java/jenkins/websocket/Jetty10Provider.java @@ -0,0 +1,162 @@ +/* + * The MIT License + * + * Copyright 2022 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketListener; +import org.eclipse.jetty.websocket.api.WriteCallback; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; +import org.kohsuke.MetaInfServices; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +@MetaInfServices(Provider.class) +public class Jetty10Provider implements Provider { + + private static final String ATTR_LISTENER = Jetty10Provider.class.getName() + ".listener"; + + // TODO does not seem possible to use HttpServletRequest.get/setAttribute for this + private static final Map sessions = new WeakHashMap<>(); + + public Jetty10Provider() { + JettyWebSocketServerContainer.class.hashCode(); + } + + @Override + public Handler handle(HttpServletRequest req, HttpServletResponse rsp, Listener listener) throws Exception { + req.setAttribute(ATTR_LISTENER, listener); + // TODO Jetty 10 has no obvious equivalent to WebSocketServerFactory.isUpgradeRequest; RFC6455Negotiation? + if (!"websocket".equalsIgnoreCase(req.getHeader("Upgrade"))) { + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "only WS connections accepted here"); + return null; + } + if (!JettyWebSocketServerContainer.getContainer(req.getServletContext()).upgrade(Jetty10Provider::createWebSocket, req, rsp)) { + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "did not manage to upgrade"); + return null; + } + return new Handler() { + @Override + public Future sendBinary(ByteBuffer data) throws IOException { + CompletableFuture f = new CompletableFuture<>(); + session().getRemote().sendBytes(data, new WriteCallbackImpl(f)); + return f; + } + + @Override + public void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException { + session().getRemote().sendPartialBytes(partialByte, isLast); + } + + @Override + public Future sendText(String text) throws IOException { + CompletableFuture f = new CompletableFuture<>(); + session().getRemote().sendString(text, new WriteCallbackImpl(f)); + return f; + } + + @Override + public void sendPing(ByteBuffer applicationData) throws IOException { + session().getRemote().sendPing(applicationData); + } + + @Override + public void close() throws IOException { + session().close(); + } + + private Session session() { + Session session = sessions.get(listener); + if (session == null) { + throw new IllegalStateException("missing session"); + } + return session; + } + }; + } + + private static final class WriteCallbackImpl implements WriteCallback { + private final CompletableFuture f; + + WriteCallbackImpl(CompletableFuture f) { + this.f = f; + } + + @Override + public void writeSuccess() { + f.complete(null); + } + + @Override + public void writeFailed(Throwable x) { + f.completeExceptionally(x); + } + } + + private static Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) { + Listener listener = (Listener) req.getHttpServletRequest().getAttribute(ATTR_LISTENER); + if (listener == null) { + throw new IllegalStateException("missing listener attribute"); + } + return new WebSocketListener() { + @Override + public void onWebSocketBinary(byte[] payload, int offset, int length) { + listener.onWebSocketBinary(payload, offset, length); + } + + @Override + public void onWebSocketText(String message) { + listener.onWebSocketText(message); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + listener.onWebSocketClose(statusCode, reason); + } + + @Override + public void onWebSocketConnect(Session session) { + sessions.put(listener, session); + listener.onWebSocketConnect(); + } + + @Override + public void onWebSocketError(Throwable cause) { + listener.onWebSocketError(cause); + } + }; + } + +}