diff --git a/.gitignore b/.gitignore index a3cbb20fda1..847f4383974 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ bin/ *.backup *.debug *.dump +.attach_pid* # vim .*.sw[a-p] diff --git a/Jenkinsfile b/Jenkinsfile index 79402a71340..bc82c8dcebd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -40,16 +40,6 @@ pipeline { } } - stage("Build / Test - JDK11") { - agent { node { label 'linux' } } - options { timeout(time: 120, unit: 'MINUTES') } - steps { - mavenBuild("jdk11", "-Pmongodb install", "maven3", false) - warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']] - maven_invoker reportsFilenamePattern: "**/target/invoker-reports/BUILD*.xml", invokerBuildDir: "**/target/it" - } - } - stage("Build / Test - JDK12") { agent { node { label 'linux' } } options { timeout(time: 120, unit: 'MINUTES') } @@ -71,8 +61,37 @@ pipeline { } } } + /* + post { + failure { + slackNotif() + } + unstable { + slackNotif() + } + fixed { + slackNotif() + } + } + */ } +/* +def slackNotif() { + script { + if (env.BRANCH_NAME=='jetty-10.0.x' || + env.BRANCH_NAME=='jetty-9.4.x') { + //BUILD_USER = currentBuild.rawBuild.getCause(Cause.UserIdCause).getUserId() + // by ${BUILD_USER} + COLOR_MAP = ['SUCCESS': 'good', 'FAILURE': 'danger', 'UNSTABLE': 'danger', 'ABORTED': 'danger'] + slackSend channel: '#jenkins', + color: COLOR_MAP[currentBuild.currentResult], + message: "*${currentBuild.currentResult}:* Job ${env.JOB_NAME} build ${env.BUILD_NUMBER} - ${env.BUILD_URL}" + } + } +} +*/ + /** * To other developers, if you are using this method above, please use the following syntax. * @@ -97,7 +116,7 @@ def mavenBuild(jdk, cmdline, mvnName, junitPublishDisabled) { mavenOpts: mavenOpts, mavenLocalRepo: localRepo) { // Some common Maven command line + provided command line - sh "mvn -V -B -T3 -e -fae -Dmaven.test.failure.ignore=true -Djetty.testtracker.log=true $cmdline -Dunix.socket.tmp=" + env.JENKINS_HOME + sh "mvn -Pci -V -B -T3 -e -fae -Dmaven.test.failure.ignore=true -Djetty.testtracker.log=true $cmdline -Dunix.socket.tmp=" + env.JENKINS_HOME } } diff --git a/Jmh_Jenkinsfile b/Jmh_Jenkinsfile index 220ee0fb677..c9df27e6b37 100644 --- a/Jmh_Jenkinsfile +++ b/Jmh_Jenkinsfile @@ -2,53 +2,49 @@ def branch = params.get("JETTY_BRANCH" ,"jetty-10.0.x") def owner = params.get("REPO_OWNER", "eclipse") +def jdk = params.get("JDK", "jdk11") +def jmhJarPath = params.get("jmhJarPath","tests/jetty-jmh/target/benchmarks.jar") +currentBuild.description = "Build branch $branch with jdk $jdk" node("linux") { // System Dependent Locations - def mvntool = tool name: 'maven3.5', type: 'hudson.tasks.Maven$MavenInstallation' - def jdktool = tool name: "jdk8", type: 'hudson.model.JDK' def mvnName = 'maven3.5' def localRepo = "${env.JENKINS_HOME}/${env.EXECUTOR_NUMBER}" def settingsName = 'oss-settings.xml' def mavenOpts = '-Xms1g -Xmx4g -Djava.awt.headless=true' - // Environment - List mvnEnv = ["PATH+MVN=${mvntool}/bin", "PATH+JDK=${jdktool}/bin", "JAVA_HOME=${jdktool}/", "MAVEN_HOME=${mvntool}"] - mvnEnv.add("MAVEN_OPTS=$mavenOpts") - stage("Checkout") { git url: "https://github.com/$owner/jetty.project.git", branch: "$branch" + } stage("Compile") { - withEnv(mvnEnv) { - timeout(time: 15, unit: 'MINUTES') { - withMaven( - maven: mvnName, - jdk: "jdk11", - publisherStrategy: 'EXPLICIT', - globalMavenSettingsConfig: settingsName, - mavenOpts: mavenOpts, - mavenLocalRepo: localRepo) { - sh "mvn -V -B clean install -DskipTests -T6 -e" - } - + timeout(time: 15, unit: 'MINUTES') { + withMaven( + maven: mvnName, + jdk: jdk, + publisherStrategy: 'EXPLICIT', + globalMavenSettingsConfig: settingsName, + mavenOpts: mavenOpts, + mavenLocalRepo: localRepo) { + sh "mvn -V -B clean install -DskipTests -T6 -e -pl :jetty-jmh -am" } - stash name: 'perf-tests', includes: 'jetty-jmh/target/benchmarks.jar' } + stash name: 'perf-tests', includes: jmhJarPath } } // jmh run - -stage("jmh-run") { - node( 'jmh-build-node' ) { - timeout( time: 120, unit: 'MINUTES' ) { - withEnv( ["JAVA_HOME=${tool "jdk8"}"] ) { +node( 'jmh-build-node' ) { + stage("jmh-run") { + timeout( time: 180, unit: 'MINUTES' ) { + withEnv( ["JAVA_HOME=${tool "$jdk"}"] ) { unstash name: 'perf-tests' - sh "${env.JAVA_HOME}/bin/java -jar jetty-jmh/target/benchmarks.jar -rff jetty-jmh/target/jmh_result.json -rf json" - jmhReport 'jetty-jmh/target/jmh_result.json' + sh "rm -rf jmh_results" + sh "mkdir jmh_results" + sh "${env.JAVA_HOME}/bin/java -jar $jmhJarPath -rff jmh_results/jmh_result.json -rf json -foe true" + jmhReport 'jmh_results/jmh_result.json' } } } diff --git a/LICENSE b/LICENSE index 46f4f252464..6acfaf43962 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,8 @@ This program and the accompanying materials are made available under the -terms of the Eclipse Public License 2.0 which is available at -http://www.eclipse.org/legal/epl-2.0, or the Apache Software License -2.0 which is available at https://www.apache.org/licenses/LICENSE-2.0. +terms of the Eclipse Public License 1.0 which is available at +https://www.eclipse.org/org/documents/epl-1.0/EPL-1.0.txt +or the Apache Software License 2.0 which is available at +https://www.apache.org/licenses/LICENSE-2.0 diff --git a/VERSION.txt b/VERSION.txt index 043c4bf32e7..455877d02b5 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1,5 +1,49 @@ jetty-10.0.0-SNAPSHOT +jetty-9.4.18.v20190429 - 29 April 2019 + + 3476 IllegalStateException in WebSocket ConnectionState + + 3550 Server becomes unresponsive after sitting idle from a load spike + + 3563 Update to apache jasper 8.5.40 + + 3573 Update jetty-bom for new infinispan artifacts. + + 3582 HeapByteBuffer cleared unexpected + + 3597 Session persistence broken from 9.4.13+ + + 3609 Fix infinispan start module dependencies + +jetty-9.4.17.v20190418 - 18 April 2019 + + 3464 Split SslContextFactory into Client and Server + + 3549 Directory Listing on Windows reveals Resource Base path + + 3555 DefaultHandler Reveals Base Resource Path of each Context + +jetty-9.4.16.v20190411 - 11 April 2019 + + 1861 Limit total bytes pooled by ByteBufferPools + + 3133 Logging of `key.readyOps()` can throw unchecked `CancelledKeyException` + + 3159 WebSocket permessage-deflate RSV1 validity check + + 3274 OSGi versions of java.base classes in + org.apache.felix:org.osgi.foundation:jar conflicts with new rules on Java 9+ + + 3319 Modernize Directory Listing: HTML5 and Sorting + + 3361 HandlerCollection.addHandler is lacking synchronization + + 3373 OutOfMemoryError: Java heap space in GZIPContentDecoder + + 3389 Websockets jsr356 willDecode not invoked during decoding + + 3394 java.security.acl.Group is deprecated and marked for removal + + 3404 Cleanup QuotedQualityCSV internal use of Double + + 3411 HttpClient does not timeout during multiple redirection + + 3421 Duplicate JSESSIONID sent when invalidating new session + + 3422 CLOSE_WAIT socket status forever after terminating websocket client + side + + 3425 Upgrade conscrypt version to 2.0.0 and remove usage of reflection + + 3429 JMX Operation to trigger manual deployment scan in WebAppProvider + + 3440 Stop server if Unavailable thrown + + 3444 org.eclipse.jetty.http.Http1FieldPreEncoder generates an invalid header + byte-array if header is null + + 3456 Allow multiple programmatic login/logout in same request + + 3464 Split SslContextFactory into Client and Server + + 3481 TLS close_notify() is not guaranteed + + 3489 Using setExtraClasspath("lib/extra/*") does not work on Microsoft + Windows + + 3526 HTTP Request Locale not retained in WebsocketUpgrade Request + + 3540 Use configured Provider in SslContextFactory consistently + + 3545 NullPointerException on ServletOutputStream.print(""); + jetty-9.4.15.v20190215 - 15 February 2019 + 113 Add support for NCSA Extended Log File Format + 150 extraClasspath() method on WebAppContext dont support dir path @@ -43,6 +87,25 @@ jetty-9.4.15.v20190215 - 15 February 2019 + 3350 Do not expect to be able to connect to https URLs with the HttpClient created from a parameterless constructor +jetty-9.3.27.v20190418 - 18 April 2019 + + 3549 Directory Listing on Windows reveals Resource Base path + + 3555 DefaultHandler Reveals Base Resource Path of each Context + +jetty-9.3.26.v20190403 - 03 April 2019 + + 2954 Improve cause reporting for HttpClient failures + + 3274 OSGi versions of java.base classes in + org.apache.felix:org.osgi.foundation:jar conflicts with new rules on Java 9+ + + 3302 Support host:port in X-Forwarded-For header in + ForwardedRequestCustomizer + + 3319 Allow reverse sort for directory listed files + +jetty-9.2.28.v20190418 - 18 April 2019 + + 3549 Directory Listing on Windows reveals Resource Base path + + 3555 DefaultHandler Reveals Base Resource Path of each Context + +jetty-9.2.27.v20190403 - 03 April 2019 + + 3319 Refactored Directory Listing to modernize and avoid XSS + jetty-9.4.14.v20181114 - 14 November 2018 + 3097 Duplicated programmatic Servlet Listeners causing duplicate calls + 3103 HttpClientLoadTest reports a leak in byte buffer @@ -90,6 +153,13 @@ jetty-9.4.13.v20181111 - 11 November 2018 + 3090 MBeanContainer throws NPE for arrays + 3092 Wrong classloader used to load *MBean classes +jetty-9.3.25.v20180904 - 04 September 2018 + + 2135 Android 8.1 needs direct buffers for SSL/TLS to work + + 2777 Workaround for Conscrypt's ssl == null + + 2787 BadMessageException wrapped as ServletException not handled + + 2860 Leakage of HttpDestinations in HttpClient + + 2871 Server reads -1 after client resets HTTP/2 stream + jetty-9.4.12.v20180830 - 30 August 2018 + 300 Implement Deflater / Inflater Object Pool + 307 Monitor contention in AbstractNCSARequestLog @@ -182,13 +252,6 @@ jetty-9.4.12.v20180830 - 30 August 2018 + 2860 Leakage of HttpDestinations in HttpClient + 2871 Server reads -1 after client resets HTTP/2 stream -jetty-9.3.25.v20180904 - 04 September 2018 - + 2135 Android 8.1 needs direct buffers for SSL/TLS to work - + 2777 Workaround for Conscrypt's ssl == null - + 2787 BadMessageException wrapped as ServletException not handled - + 2860 Leakage of HttpDestinations in HttpClient - + 2871 Server reads -1 after client resets HTTP/2 stream - jetty-9.2.26.v20180806 - 06 August 2018 + 2777 Workaround for Conscrypt's ssl == null diff --git a/aggregates/jetty-all/pom.xml b/aggregates/jetty-all/pom.xml index 2cf059197cf..9ffbd300a9f 100644 --- a/aggregates/jetty-all/pom.xml +++ b/aggregates/jetty-all/pom.xml @@ -209,8 +209,8 @@ - javax.websocket - javax.websocket-api + org.eclipse.jetty.toolchain + jetty-javax-websocket-api org.eclipse.jetty.toolchain diff --git a/aggregates/jetty-websocket-all/pom.xml b/aggregates/jetty-websocket-all/pom.xml index 8ab1dc55680..e402bec529c 100644 --- a/aggregates/jetty-websocket-all/pom.xml +++ b/aggregates/jetty-websocket-all/pom.xml @@ -144,8 +144,8 @@ - javax.websocket - javax.websocket-api + org.eclipse.jetty.toolchain + jetty-javax-websocket-api compile diff --git a/apache-jsp/src/main/java/org/eclipse/jetty/jsp/JettyJspServlet.java b/apache-jsp/src/main/java/org/eclipse/jetty/jsp/JettyJspServlet.java index d448b909366..fd2b9f63307 100644 --- a/apache-jsp/src/main/java/org/eclipse/jetty/jsp/JettyJspServlet.java +++ b/apache-jsp/src/main/java/org/eclipse/jetty/jsp/JettyJspServlet.java @@ -119,7 +119,7 @@ public class JettyJspServlet extends JspServlet */ private String addPaths(String servletPath, String pathInfo) { - if (servletPath.length()==0) + if (servletPath.isEmpty()) return pathInfo; if (pathInfo==null) diff --git a/examples/async-rest/async-rest-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/examples/async-rest/async-rest-webapp/src/main/webapp/WEB-INF/jetty-web.xml index c15dc38f119..6cf5df47eff 100644 --- a/examples/async-rest/async-rest-webapp/src/main/webapp/WEB-INF/jetty-web.xml +++ b/examples/async-rest/async-rest-webapp/src/main/webapp/WEB-INF/jetty-web.xml @@ -1,5 +1,5 @@ - + - - async-rest webapp is deployed. DO NOT USE IN PRODUCTION! + + The async-rest webapp is deployed. DO NOT USE IN PRODUCTION! diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/FastFileServer.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/FastFileServer.java index 169ae96ec0a..5f23ad4fda2 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/FastFileServer.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/FastFileServer.java @@ -25,7 +25,6 @@ import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; import java.nio.file.StandardOpenOption; - import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -116,7 +115,8 @@ public class FastFileServer } String listing = Resource.newResource(file).getListHTML( request.getRequestURI(), - request.getPathInfo().lastIndexOf("/") > 0); + request.getPathInfo().lastIndexOf("/") > 0, + request.getQueryString()); response.setContentType("text/html; charset=utf-8"); response.getWriter().println(listing); return; diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/FileServerXml.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/FileServerXml.java index c05fe5c4cd1..641612e0e0d 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/FileServerXml.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/FileServerXml.java @@ -36,9 +36,8 @@ public class FileServerXml { public static void main( String[] args ) throws Exception { - Resource fileserverXml = Resource.newSystemResource("fileserver.xml"); - XmlConfiguration configuration = new XmlConfiguration( - fileserverXml.getInputStream()); + Resource fileServerXml = Resource.newSystemResource("fileserver.xml"); + XmlConfiguration configuration = new XmlConfiguration(fileServerXml); Server server = (Server) configuration.configure(); server.start(); server.join(); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/Http2Server.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/Http2Server.java index 12ddbf4f6b0..a72c660879a 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/Http2Server.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/Http2Server.java @@ -60,10 +60,6 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlets.PushCacheFilter; import org.eclipse.jetty.util.ssl.SslContextFactory; - -/* ------------------------------------------------------------ */ -/** - */ public class Http2Server { public static void main(String... args) throws Exception @@ -102,7 +98,7 @@ public class Http2Server String jetty_distro = System.getProperty("jetty.distro","../../jetty-distribution/target/distribution"); if (!new File(jetty_distro).exists()) jetty_distro = "jetty-distribution/target/distribution"; - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(jetty_distro + "/demo-base/etc/keystore"); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java index adef2d6cd94..1cd09c4cb36 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/LikeJettyXml.java @@ -99,7 +99,7 @@ public class LikeJettyXml Server server = new Server(threadPool); // Scheduler - server.addBean(new ScheduledExecutorScheduler()); + server.addBean(new ScheduledExecutorScheduler(null,false)); // HTTP Configuration HttpConfiguration http_config = new HttpConfiguration(); @@ -139,7 +139,7 @@ public class LikeJettyXml // === jetty-https.xml === // SSL Context Factory - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(jetty_home + "/../../../jetty-server/src/test/config/etc/keystore"); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyConnectors.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyConnectors.java index 851d584c63c..960563ab340 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyConnectors.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyConnectors.java @@ -20,9 +20,7 @@ package org.eclipse.jetty.embedded; import java.io.File; import java.io.FileNotFoundException; -import java.security.Security; -import org.conscrypt.OpenSSLProvider; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; @@ -89,7 +87,7 @@ public class ManyConnectors // including things like choosing the particular certificate out of a // keystore to be used. - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath()); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyHandlers.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyHandlers.java index 6db2d2af434..09e342fb2c3 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyHandlers.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyHandlers.java @@ -79,7 +79,7 @@ public class ManyHandlers ServletException { Map params = request.getParameterMap(); - if (params.size() > 0) + if (!params.isEmpty()) { response.setContentType("text/plain"); response.getWriter().println(JSON.toString(params)); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneWebApp.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneWebApp.java index 7aec6fe9b73..b2ad76f919a 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneWebApp.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneWebApp.java @@ -23,7 +23,6 @@ import java.lang.management.ManagementFactory; import org.eclipse.jetty.jmx.MBeanContainer; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker; import org.eclipse.jetty.webapp.Configurations; import org.eclipse.jetty.webapp.WebAppContext; @@ -65,7 +64,7 @@ public class OneWebApp Configurations.setServerDefault(server); - // Start things up! + // Start things up! server.start(); server.dumpStdErr(); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketServer.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketServer.java index e34d3407edd..4f5a51439b6 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketServer.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketServer.java @@ -24,10 +24,8 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.core.FrameHandler; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; /** * Example of setting up a Jetty WebSocket server @@ -53,10 +51,10 @@ public class WebSocketServer * Servlet layer */ @SuppressWarnings("serial") - public static class EchoServlet extends WebSocketServlet + public static class EchoServlet extends JettyWebSocketServlet { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { factory.addMapping(factory.parsePathSpec("/"), (req,res)->new EchoSocket()); } diff --git a/examples/embedded/src/main/resources/exampleserver.xml b/examples/embedded/src/main/resources/exampleserver.xml index deae1ddec15..6dbbd6a07f5 100644 --- a/examples/embedded/src/main/resources/exampleserver.xml +++ b/examples/embedded/src/main/resources/exampleserver.xml @@ -1,5 +1,5 @@ - + diff --git a/examples/embedded/src/main/resources/fileserver.xml b/examples/embedded/src/main/resources/fileserver.xml index 126f0987c8e..8562cb00071 100644 --- a/examples/embedded/src/main/resources/fileserver.xml +++ b/examples/embedded/src/main/resources/fileserver.xml @@ -1,5 +1,5 @@ - + diff --git a/examples/embedded/src/main/resources/jetty-otherserver.xml b/examples/embedded/src/main/resources/jetty-otherserver.xml index 21272bb3a91..89596307069 100644 --- a/examples/embedded/src/main/resources/jetty-otherserver.xml +++ b/examples/embedded/src/main/resources/jetty-otherserver.xml @@ -1,5 +1,5 @@ - + diff --git a/examples/pom.xml b/examples/pom.xml index 6cec9ca0e9e..9bad588e2b6 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -19,6 +19,14 @@ + + org.apache.maven.plugins + maven-deploy-plugin + + + true + + org.codehaus.mojo findbugs-maven-plugin diff --git a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnection.java b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnection.java index 4b6c25573b8..981e702285c 100644 --- a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnection.java +++ b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnection.java @@ -48,6 +48,6 @@ public class ALPNClientConnection extends NegotiatingClientConnection if (protocol == null || !protocols.contains(protocol)) close(); else - completed(); + completed(protocol); } } diff --git a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java index 5922be7e4f9..83d7885e4cf 100644 --- a/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java +++ b/jetty-alpn/jetty-alpn-client/src/main/java/org/eclipse/jetty/alpn/client/ALPNClientConnectionFactory.java @@ -111,4 +111,12 @@ public class ALPNClientConnectionFactory extends NegotiatingClientConnectionFact } throw new IllegalStateException("No ALPNProcessor for " + engine); } + + public static class ALPN extends Info + { + public ALPN(Executor executor, ClientConnectionFactory factory, List protocols) + { + super(List.of("alpn"), new ALPNClientConnectionFactory(executor, factory, protocols)); + } + } } diff --git a/jetty-alpn/jetty-alpn-conscrypt-client/src/main/java/org/eclipse/jetty/alpn/conscrypt/client/ConscryptClientALPNProcessor.java b/jetty-alpn/jetty-alpn-conscrypt-client/src/main/java/org/eclipse/jetty/alpn/conscrypt/client/ConscryptClientALPNProcessor.java index 03d83a5d1c4..5fd47d2f2f6 100644 --- a/jetty-alpn/jetty-alpn-conscrypt-client/src/main/java/org/eclipse/jetty/alpn/conscrypt/client/ConscryptClientALPNProcessor.java +++ b/jetty-alpn/jetty-alpn-conscrypt-client/src/main/java/org/eclipse/jetty/alpn/conscrypt/client/ConscryptClientALPNProcessor.java @@ -18,12 +18,10 @@ package org.eclipse.jetty.alpn.conscrypt.client; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; import java.security.Security; - import javax.net.ssl.SSLEngine; +import org.conscrypt.Conscrypt; import org.conscrypt.OpenSSLProvider; import org.eclipse.jetty.alpn.client.ALPNClientConnection; import org.eclipse.jetty.io.Connection; @@ -40,7 +38,7 @@ public class ConscryptClientALPNProcessor implements ALPNProcessor.Client @Override public void init() { - if (Security.getProvider("Conscrypt")==null) + if (Security.getProvider("Conscrypt") == null) { Security.addProvider(new OpenSSLProvider()); if (LOG.isDebugEnabled()) @@ -59,11 +57,9 @@ public class ConscryptClientALPNProcessor implements ALPNProcessor.Client { try { - Method setAlpnProtocols = sslEngine.getClass().getDeclaredMethod("setApplicationProtocols", String[].class); - setAlpnProtocols.setAccessible(true); ALPNClientConnection alpn = (ALPNClientConnection)connection; String[] protocols = alpn.getProtocols().toArray(new String[0]); - setAlpnProtocols.invoke(sslEngine, (Object)protocols); + Conscrypt.setApplicationProtocols(sslEngine, protocols); ((SslConnection.DecryptedEndPoint)connection.getEndPoint()).getSslConnection() .addHandshakeListener(new ALPNListener(alpn)); } @@ -92,9 +88,9 @@ public class ConscryptClientALPNProcessor implements ALPNProcessor.Client try { SSLEngine sslEngine = alpnConnection.getSSLEngine(); - Method method = sslEngine.getClass().getDeclaredMethod("getApplicationProtocol"); - method.setAccessible(true); - String protocol = (String)method.invoke(sslEngine); + String protocol = Conscrypt.getApplicationProtocol(sslEngine); + if (LOG.isDebugEnabled()) + LOG.debug("Selected {} for {}", protocol, alpnConnection); alpnConnection.selected(protocol); } catch (Throwable e) diff --git a/jetty-alpn/jetty-alpn-conscrypt-client/src/test/java/org/eclipse/jetty/alpn/java/client/ConscryptHTTP2Client.java b/jetty-alpn/jetty-alpn-conscrypt-client/src/test/java/org/eclipse/jetty/alpn/java/client/ConscryptHTTP2Client.java deleted file mode 100644 index d1d714dfce6..00000000000 --- a/jetty-alpn/jetty-alpn-conscrypt-client/src/test/java/org/eclipse/jetty/alpn/java/client/ConscryptHTTP2Client.java +++ /dev/null @@ -1,89 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.alpn.java.client; - -import java.net.InetSocketAddress; -import java.security.Security; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.conscrypt.OpenSSLProvider; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpURI; -import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.http.MetaData; -import org.eclipse.jetty.http2.api.Session; -import org.eclipse.jetty.http2.api.Stream; -import org.eclipse.jetty.http2.client.HTTP2Client; -import org.eclipse.jetty.http2.frames.DataFrame; -import org.eclipse.jetty.http2.frames.HeadersFrame; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.FuturePromise; -import org.eclipse.jetty.util.Jetty; -import org.eclipse.jetty.util.Promise; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -public class ConscryptHTTP2Client -{ - public static void main(String[] args) throws Exception - { - Security.addProvider(new OpenSSLProvider()); - SslContextFactory sslContextFactory = new SslContextFactory(); - sslContextFactory.setProvider("Conscrypt"); - HTTP2Client client = new HTTP2Client(); - client.addBean(sslContextFactory); - client.start(); - - String host = "webtide.com"; - int port = 443; - - FuturePromise sessionPromise = new FuturePromise<>(); - client.connect(sslContextFactory, new InetSocketAddress(host, port), new Session.Listener.Adapter(), sessionPromise); - Session session = sessionPromise.get(5, TimeUnit.SECONDS); - - HttpFields requestFields = new HttpFields(); - requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION); - MetaData.Request metaData = new MetaData.Request("GET", new HttpURI("https://" + host + ":" + port + "/"), HttpVersion.HTTP_2, requestFields); - HeadersFrame headersFrame = new HeadersFrame(metaData, null, true); - CountDownLatch latch = new CountDownLatch(1); - session.newStream(headersFrame, new Promise.Adapter<>(), new Stream.Listener.Adapter() - { - @Override - public void onHeaders(Stream stream, HeadersFrame frame) - { - System.err.println(frame); - if (frame.isEndStream()) - latch.countDown(); - } - - @Override - public void onData(Stream stream, DataFrame frame, Callback callback) - { - System.err.println(frame); - callback.succeeded(); - if (frame.isEndStream()) - latch.countDown(); - } - }); - - latch.await(5, TimeUnit.SECONDS); - - client.stop(); - } -} diff --git a/jetty-alpn/jetty-alpn-conscrypt-client/src/test/java/org/eclipse/jetty/alpn/java/client/ConscryptHTTP2ClientTest.java b/jetty-alpn/jetty-alpn-conscrypt-client/src/test/java/org/eclipse/jetty/alpn/java/client/ConscryptHTTP2ClientTest.java new file mode 100644 index 00000000000..d89b4ba101e --- /dev/null +++ b/jetty-alpn/jetty-alpn-conscrypt-client/src/test/java/org/eclipse/jetty/alpn/java/client/ConscryptHTTP2ClientTest.java @@ -0,0 +1,122 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.alpn.java.client; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.Security; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.conscrypt.Conscrypt; +import org.conscrypt.OpenSSLProvider; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.Jetty; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ConscryptHTTP2ClientTest +{ + @Tag("external") + @Test + @Disabled("issue google/conscrypt#667") + public void testConscryptHTTP2Client() throws Exception + { + String host = "webtide.com"; + int port = 443; + + Assumptions.assumeTrue(canConnectTo(host, port)); + + Security.insertProviderAt(new OpenSSLProvider(), 1); + SslContextFactory sslContextFactory = new SslContextFactory.Client(); + sslContextFactory.setProvider("Conscrypt"); + Conscrypt.setDefaultHostnameVerifier((hostname, session) -> true); + + HTTP2Client client = new HTTP2Client(); + try + { + client.addBean(sslContextFactory); + client.start(); + + FuturePromise sessionPromise = new FuturePromise<>(); + client.connect(sslContextFactory, new InetSocketAddress(host, port), new Session.Listener.Adapter(), sessionPromise); + Session session = sessionPromise.get(15, TimeUnit.SECONDS); + + HttpFields requestFields = new HttpFields(); + requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION); + MetaData.Request metaData = new MetaData.Request("GET", new HttpURI("https://" + host + ":" + port + "/"), HttpVersion.HTTP_2, requestFields); + HeadersFrame headersFrame = new HeadersFrame(metaData, null, true); + CountDownLatch latch = new CountDownLatch(1); + session.newStream(headersFrame, new Promise.Adapter<>(), new Stream.Listener.Adapter() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + System.err.println(frame); + if (frame.isEndStream()) + latch.countDown(); + } + + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + System.err.println(frame); + callback.succeeded(); + if (frame.isEndStream()) + latch.countDown(); + } + }); + + assertTrue(latch.await(15, TimeUnit.SECONDS)); + } + finally + { + client.stop(); + } + } + + private boolean canConnectTo(String host, int port) + { + try + { + new Socket(host, port).close(); + return true; + } + catch (Throwable x) + { + return false; + } + } +} diff --git a/jetty-alpn/jetty-alpn-conscrypt-server/pom.xml b/jetty-alpn/jetty-alpn-conscrypt-server/pom.xml index 4a28ced33cc..346eae08d92 100644 --- a/jetty-alpn/jetty-alpn-conscrypt-server/pom.xml +++ b/jetty-alpn/jetty-alpn-conscrypt-server/pom.xml @@ -38,23 +38,57 @@ ${project.version} test + + org.eclipse.jetty + jetty-alpn-conscrypt-client + ${project.version} + test + + + org.eclipse.jetty + jetty-client + ${project.version} + test + + + org.eclipse.jetty.http2 + http2-client + ${project.version} + test + + + org.eclipse.jetty.http2 + http2-http-client-transport + ${project.version} + test + + - - org.apache.felix - maven-bundle-plugin - true - - - Conscrypt ALPN - org.conscrypt;version="${conscrypt.version}",* - osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional - osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server - <_nouses>true - - - + + org.apache.felix + maven-bundle-plugin + true + + + Conscrypt ALPN + org.conscrypt;version="${conscrypt.version}",* + osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)";resolution:=optional + osgi.serviceloader;osgi.serviceloader=org.eclipse.jetty.io.ssl.ALPNProcessor$Server + <_nouses>true + + + + + maven-surefire-plugin + + + @{argLine} ${jetty.surefire.argLine} + --add-reads org.eclipse.jetty.alpn.conscrypt.server=org.eclipse.jetty.server + + + diff --git a/jetty-alpn/jetty-alpn-conscrypt-server/src/main/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptServerALPNProcessor.java b/jetty-alpn/jetty-alpn-conscrypt-server/src/main/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptServerALPNProcessor.java index 9444683b58b..6f050e82fe5 100644 --- a/jetty-alpn/jetty-alpn-conscrypt-server/src/main/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptServerALPNProcessor.java +++ b/jetty-alpn/jetty-alpn-conscrypt-server/src/main/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptServerALPNProcessor.java @@ -18,13 +18,13 @@ package org.eclipse.jetty.alpn.conscrypt.server; -import java.lang.reflect.Method; import java.security.Security; import java.util.List; -import java.util.function.BiFunction; - import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSocket; +import org.conscrypt.ApplicationProtocolSelector; +import org.conscrypt.Conscrypt; import org.conscrypt.OpenSSLProvider; import org.eclipse.jetty.alpn.server.ALPNServerConnection; import org.eclipse.jetty.io.Connection; @@ -41,7 +41,7 @@ public class ConscryptServerALPNProcessor implements ALPNProcessor.Server @Override public void init() { - if (Security.getProvider("Conscrypt")==null) + if (Security.getProvider("Conscrypt") == null) { Security.addProvider(new OpenSSLProvider()); if (LOG.isDebugEnabled()) @@ -56,13 +56,11 @@ public class ConscryptServerALPNProcessor implements ALPNProcessor.Server } @Override - public void configure(SSLEngine sslEngine,Connection connection) + public void configure(SSLEngine sslEngine, Connection connection) { try { - Method method = sslEngine.getClass().getMethod("setHandshakeApplicationProtocolSelector", BiFunction.class); - method.setAccessible(true); - method.invoke(sslEngine,new ALPNCallback((ALPNServerConnection)connection)); + Conscrypt.setApplicationProtocolSelector(sslEngine, new ALPNCallback((ALPNServerConnection)connection)); } catch (RuntimeException x) { @@ -74,23 +72,31 @@ public class ConscryptServerALPNProcessor implements ALPNProcessor.Server } } - private final class ALPNCallback implements BiFunction,String>, SslHandshakeListener + private final class ALPNCallback extends ApplicationProtocolSelector implements SslHandshakeListener { private final ALPNServerConnection alpnConnection; + private ALPNCallback(ALPNServerConnection connection) { - alpnConnection = connection; + alpnConnection = connection; ((DecryptedEndPoint)alpnConnection.getEndPoint()).getSslConnection().addHandshakeListener(this); } @Override - public String apply(SSLEngine engine, List protocols) + public String selectApplicationProtocol(SSLEngine engine, List protocols) { - if (LOG.isDebugEnabled()) - LOG.debug("apply {} {}", alpnConnection, protocols); alpnConnection.select(protocols); - return alpnConnection.getProtocol(); + String protocol = alpnConnection.getProtocol(); + if (LOG.isDebugEnabled()) + LOG.debug("Selected {} among {} for {}", protocol, protocols, alpnConnection); + return protocol; + } + + @Override + public String selectApplicationProtocol(SSLSocket socket, List protocols) + { + throw new UnsupportedOperationException(); } @Override @@ -99,7 +105,7 @@ public class ConscryptServerALPNProcessor implements ALPNProcessor.Server String protocol = alpnConnection.getProtocol(); if (LOG.isDebugEnabled()) LOG.debug("TLS handshake succeeded, protocol={} for {}", protocol, alpnConnection); - if (protocol ==null) + if (protocol == null) alpnConnection.unsupported(); } diff --git a/jetty-alpn/jetty-alpn-conscrypt-server/src/test/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptHTTP2Server.java b/jetty-alpn/jetty-alpn-conscrypt-server/src/test/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptHTTP2Server.java deleted file mode 100644 index 26f6ec1e92f..00000000000 --- a/jetty-alpn/jetty-alpn-conscrypt-server/src/test/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptHTTP2Server.java +++ /dev/null @@ -1,72 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.alpn.conscrypt.server; - -import java.security.Security; - -import org.conscrypt.OpenSSLProvider; -import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; -import org.eclipse.jetty.http2.HTTP2Cipher; -import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.SecureRequestCustomizer; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -/** - * Test server that verifies that the Conscrypt ALPN mechanism works. - */ -public class ConscryptHTTP2Server -{ - public static void main(String[] args) throws Exception - { - Security.addProvider(new OpenSSLProvider()); - - Server server = new Server(); - - HttpConfiguration httpsConfig = new HttpConfiguration(); - httpsConfig.setSecureScheme("https"); - httpsConfig.setSecurePort(8443); - httpsConfig.setSendXPoweredBy(true); - httpsConfig.setSendServerVersion(true); - httpsConfig.addCustomizer(new SecureRequestCustomizer()); - - SslContextFactory sslContextFactory = new SslContextFactory(); - sslContextFactory.setProvider("Conscrypt"); - sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); - sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); - sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); - sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); - - HttpConnectionFactory http = new HttpConnectionFactory(httpsConfig); - HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); - ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); - alpn.setDefaultProtocol(http.getProtocol()); - SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); - - ServerConnector http2Connector = new ServerConnector(server, ssl, alpn, h2, http); - http2Connector.setPort(8443); - server.addConnector(http2Connector); - - server.start(); - } -} diff --git a/jetty-alpn/jetty-alpn-conscrypt-server/src/test/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptHTTP2ServerTest.java b/jetty-alpn/jetty-alpn-conscrypt-server/src/test/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptHTTP2ServerTest.java new file mode 100644 index 00000000000..f17c3283fd8 --- /dev/null +++ b/jetty-alpn/jetty-alpn-conscrypt-server/src/test/java/org/eclipse/jetty/alpn/conscrypt/server/ConscryptHTTP2ServerTest.java @@ -0,0 +1,154 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.alpn.conscrypt.server; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.Security; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.conscrypt.OpenSSLProvider; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.JavaVersion; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test server that verifies that the Conscrypt ALPN mechanism works for both server and client side + */ +public class ConscryptHTTP2ServerTest +{ + static + { + Security.addProvider(new OpenSSLProvider()); + } + + private Server server = new Server(); + + private SslContextFactory.Server newServerSslContextFactory() + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + configureSslContextFactory(sslContextFactory); + return sslContextFactory; + } + + private SslContextFactory.Client newClientSslContextFactory() + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + configureSslContextFactory(sslContextFactory); + sslContextFactory.setEndpointIdentificationAlgorithm(null); + return sslContextFactory; + } + + private void configureSslContextFactory(SslContextFactory sslContextFactory) + { + Path path = Paths.get("src", "test", "resources"); + File keys = path.resolve("keystore").toFile(); + sslContextFactory.setKeyStorePath(keys.getAbsolutePath()); + sslContextFactory.setKeyManagerPassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); + sslContextFactory.setTrustStorePath(keys.getAbsolutePath()); + sslContextFactory.setTrustStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); + sslContextFactory.setProvider("Conscrypt"); + if (JavaVersion.VERSION.getPlatform() < 9) + { + // Conscrypt enables TLSv1.3 by default but it's not supported in Java 8. + sslContextFactory.addExcludeProtocols("TLSv1.3"); + } + } + + @BeforeEach + public void startServer() throws Exception + { + HttpConfiguration httpsConfig = new HttpConfiguration(); + httpsConfig.setSecureScheme("https"); + + httpsConfig.setSendXPoweredBy(true); + httpsConfig.setSendServerVersion(true); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + + HttpConnectionFactory http = new HttpConnectionFactory(httpsConfig); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(http.getProtocol()); + SslConnectionFactory ssl = new SslConnectionFactory(newServerSslContextFactory(), alpn.getProtocol()); + + ServerConnector http2Connector = new ServerConnector(server, ssl, alpn, h2, http); + http2Connector.setPort(0); + server.addConnector(http2Connector); + + server.setHandler(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + { + response.setStatus(200); + baseRequest.setHandled(true); + } + }); + + server.start(); + } + + @AfterEach + public void stopServer() throws Exception + { + if (server != null) + server.stop(); + } + + @Test + public void testSimpleRequest() throws Exception + { + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(newClientSslContextFactory()); + HTTP2Client h2Client = new HTTP2Client(clientConnector); + HttpClient client = new HttpClient(new HttpClientTransportOverHTTP2(h2Client)); + client.start(); + try + { + int port = ((ServerConnector)server.getConnectors()[0]).getLocalPort(); + ContentResponse contentResponse = client.GET("https://localhost:" + port); + assertEquals(200, contentResponse.getStatus()); + } + finally + { + client.stop(); + } + } +} diff --git a/jetty-alpn/jetty-alpn-conscrypt-server/src/test/resources/keystore b/jetty-alpn/jetty-alpn-conscrypt-server/src/test/resources/keystore new file mode 100644 index 00000000000..428ba54776e Binary files /dev/null and b/jetty-alpn/jetty-alpn-conscrypt-server/src/test/resources/keystore differ diff --git a/jetty-alpn/jetty-alpn-conscrypt-server/src/test/resources/keystore.jks b/jetty-alpn/jetty-alpn-conscrypt-server/src/test/resources/keystore.jks deleted file mode 100644 index d6592f95ee9..00000000000 Binary files a/jetty-alpn/jetty-alpn-conscrypt-server/src/test/resources/keystore.jks and /dev/null differ diff --git a/jetty-alpn/jetty-alpn-java-client/src/test/java/org/eclipse/jetty/alpn/java/client/JDK9HTTP2Client.java b/jetty-alpn/jetty-alpn-java-client/src/test/java/org/eclipse/jetty/alpn/java/client/JDK9HTTP2Client.java deleted file mode 100644 index aa772b826e3..00000000000 --- a/jetty-alpn/jetty-alpn-java-client/src/test/java/org/eclipse/jetty/alpn/java/client/JDK9HTTP2Client.java +++ /dev/null @@ -1,82 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.alpn.java.client; - -import java.net.InetSocketAddress; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpURI; -import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.http.MetaData; -import org.eclipse.jetty.http2.api.Session; -import org.eclipse.jetty.http2.api.Stream; -import org.eclipse.jetty.http2.client.HTTP2Client; -import org.eclipse.jetty.http2.frames.DataFrame; -import org.eclipse.jetty.http2.frames.HeadersFrame; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.FuturePromise; -import org.eclipse.jetty.util.Jetty; -import org.eclipse.jetty.util.Promise; - -public class JDK9HTTP2Client -{ - public static void main(String[] args) throws Exception - { - HTTP2Client client = new HTTP2Client(); - client.start(); - - String host = "webtide.com"; - int port = 443; - - FuturePromise sessionPromise = new FuturePromise<>(); - client.connect(client.getClientConnector().getSslContextFactory(), new InetSocketAddress(host, port), new Session.Listener.Adapter(), sessionPromise); - Session session = sessionPromise.get(5, TimeUnit.SECONDS); - - HttpFields requestFields = new HttpFields(); - requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION); - MetaData.Request metaData = new MetaData.Request("GET", new HttpURI("https://" + host + ":" + port + "/"), HttpVersion.HTTP_2, requestFields); - HeadersFrame headersFrame = new HeadersFrame(metaData, null, true); - CountDownLatch latch = new CountDownLatch(1); - session.newStream(headersFrame, new Promise.Adapter<>(), new Stream.Listener.Adapter() - { - @Override - public void onHeaders(Stream stream, HeadersFrame frame) - { - System.err.println(frame); - if (frame.isEndStream()) - latch.countDown(); - } - - @Override - public void onData(Stream stream, DataFrame frame, Callback callback) - { - System.err.println(frame); - callback.succeeded(); - if (frame.isEndStream()) - latch.countDown(); - } - }); - - latch.await(5, TimeUnit.SECONDS); - - client.stop(); - } -} diff --git a/jetty-alpn/jetty-alpn-java-client/src/test/java/org/eclipse/jetty/alpn/java/client/JDK9HTTP2ClientTest.java b/jetty-alpn/jetty-alpn-java-client/src/test/java/org/eclipse/jetty/alpn/java/client/JDK9HTTP2ClientTest.java new file mode 100644 index 00000000000..6d6f73f79e8 --- /dev/null +++ b/jetty-alpn/jetty-alpn-java-client/src/test/java/org/eclipse/jetty/alpn/java/client/JDK9HTTP2ClientTest.java @@ -0,0 +1,111 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.alpn.java.client; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.Jetty; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +public class JDK9HTTP2ClientTest +{ + @Tag("external") + @Test + public void testJDK9HTTP2Client() throws Exception + { + String host = "webtide.com"; + int port = 443; + + Assumptions.assumeTrue(canConnectTo(host, port)); + + HTTP2Client client = new HTTP2Client(); + try + { + SslContextFactory sslContextFactory = new SslContextFactory.Client(); + client.addBean(sslContextFactory); + client.start(); + + FuturePromise sessionPromise = new FuturePromise<>(); + client.connect(sslContextFactory, new InetSocketAddress(host, port), new Session.Listener.Adapter(), sessionPromise); + Session session = sessionPromise.get(15, TimeUnit.SECONDS); + + HttpFields requestFields = new HttpFields(); + requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION); + MetaData.Request metaData = new MetaData.Request("GET", new HttpURI("https://" + host + ":" + port + "/"), HttpVersion.HTTP_2, requestFields); + HeadersFrame headersFrame = new HeadersFrame(metaData, null, true); + CountDownLatch latch = new CountDownLatch(1); + session.newStream(headersFrame, new Promise.Adapter<>(), new Stream.Listener.Adapter() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + System.err.println(frame); + if (frame.isEndStream()) + latch.countDown(); + } + + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) + { + System.err.println(frame); + callback.succeeded(); + if (frame.isEndStream()) + latch.countDown(); + } + }); + + latch.await(15, TimeUnit.SECONDS); + } + finally + { + client.stop(); + } + } + + private boolean canConnectTo(String host, int port) + { + try + { + new Socket(host, port).close(); + return true; + } + catch (Throwable x) + { + return false; + } + } +} diff --git a/jetty-alpn/jetty-alpn-java-server/src/test/java/org/eclipse/jetty/alpn/java/server/JDK9ALPNTest.java b/jetty-alpn/jetty-alpn-java-server/src/test/java/org/eclipse/jetty/alpn/java/server/JDK9ALPNTest.java index 72a5aef503a..5fcc2feca09 100644 --- a/jetty-alpn/jetty-alpn-java-server/src/test/java/org/eclipse/jetty/alpn/java/server/JDK9ALPNTest.java +++ b/jetty-alpn/jetty-alpn-java-server/src/test/java/org/eclipse/jetty/alpn/java/server/JDK9ALPNTest.java @@ -59,7 +59,7 @@ public class JDK9ALPNTest HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfiguration); ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); alpn.setDefaultProtocol(h1.getProtocol()); - connector = new ServerConnector(server, newSslContextFactory(), alpn, h1, h2); + connector = new ServerConnector(server, newServerSslContextFactory(), alpn, h1, h2); server.addConnector(connector); server.setHandler(handler); server.start(); @@ -72,13 +72,12 @@ public class JDK9ALPNTest server.stop(); } - private SslContextFactory newSslContextFactory() + private SslContextFactory.Server newServerSslContextFactory() { - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); - sslContextFactory.setIncludeProtocols("TLSv1.2"); // The mandatory HTTP/2 cipher. sslContextFactory.setIncludeCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); return sslContextFactory; @@ -96,7 +95,7 @@ public class JDK9ALPNTest } }); - SslContextFactory sslContextFactory = new SslContextFactory(true); + SslContextFactory sslContextFactory = new SslContextFactory.Client(true); sslContextFactory.start(); SSLContext sslContext = sslContextFactory.getSslContext(); try (SSLSocket client = (SSLSocket)sslContext.getSocketFactory().createSocket("localhost", connector.getLocalPort())) @@ -138,7 +137,7 @@ public class JDK9ALPNTest } }); - SslContextFactory sslContextFactory = new SslContextFactory(true); + SslContextFactory sslContextFactory = new SslContextFactory.Client(true); sslContextFactory.start(); SSLContext sslContext = sslContextFactory.getSslContext(); try (SSLSocket client = (SSLSocket)sslContext.getSocketFactory().createSocket("localhost", connector.getLocalPort())) diff --git a/jetty-alpn/jetty-alpn-java-server/src/test/java/org/eclipse/jetty/alpn/java/server/JDK9HTTP2Server.java b/jetty-alpn/jetty-alpn-java-server/src/test/java/org/eclipse/jetty/alpn/java/server/JDK9HTTP2Server.java index d2d2ce12536..763488f5750 100644 --- a/jetty-alpn/jetty-alpn-java-server/src/test/java/org/eclipse/jetty/alpn/java/server/JDK9HTTP2Server.java +++ b/jetty-alpn/jetty-alpn-java-server/src/test/java/org/eclipse/jetty/alpn/java/server/JDK9HTTP2Server.java @@ -45,7 +45,7 @@ public class JDK9HTTP2Server httpsConfig.setSendServerVersion(true); httpsConfig.addCustomizer(new SecureRequestCustomizer()); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); diff --git a/jetty-alpn/jetty-alpn-server/src/main/config/etc/jetty-alpn.xml b/jetty-alpn/jetty-alpn-server/src/main/config/etc/jetty-alpn.xml index e577c2baa44..785b21f8237 100644 --- a/jetty-alpn/jetty-alpn-server/src/main/config/etc/jetty-alpn.xml +++ b/jetty-alpn/jetty-alpn-server/src/main/config/etc/jetty-alpn.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestAnnotationParser.java b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestAnnotationParser.java index c5449858c47..0d7cf14ddb8 100644 --- a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestAnnotationParser.java +++ b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestAnnotationParser.java @@ -18,14 +18,6 @@ package org.eclipse.jetty.annotations; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.isIn; -import static org.hamcrest.Matchers.notNullValue; -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 java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -55,6 +47,15 @@ import org.eclipse.jetty.util.resource.Resource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + @ExtendWith(WorkDirExtension.class) public class TestAnnotationParser { @@ -142,7 +143,7 @@ public class TestAnnotationParser if (annotation == null || !"org.eclipse.jetty.annotations.Sample".equals(annotation)) return; assertEquals("org.eclipse.jetty.annotations.ClassA",info.getClassInfo().getClassName()); - assertThat(info.getMethodName(), isIn(methods)); + assertThat(info.getMethodName(), is(in(methods))); assertEquals("org.eclipse.jetty.annotations.Sample",annotation); } } diff --git a/jetty-ant/src/main/java/org/eclipse/jetty/ant/AntWebAppContext.java b/jetty-ant/src/main/java/org/eclipse/jetty/ant/AntWebAppContext.java index a76d390922f..34e740e6eac 100644 --- a/jetty-ant/src/main/java/org/eclipse/jetty/ant/AntWebAppContext.java +++ b/jetty-ant/src/main/java/org/eclipse/jetty/ant/AntWebAppContext.java @@ -52,6 +52,7 @@ import org.eclipse.jetty.servlet.ServletMapping; import org.eclipse.jetty.servlet.Source; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.webapp.MetaInfConfiguration; import org.eclipse.jetty.webapp.WebAppClassLoader; @@ -592,7 +593,7 @@ public class AntWebAppContext extends WebAppContext TaskLog.logWithTimestamp("Starting web application "+this.getDescriptor()); if (jettyEnvXml != null && jettyEnvXml.exists()) - getConfiguration(EnvConfiguration.class).setJettyEnvXml(Resource.toURL(jettyEnvXml)); + getConfiguration(EnvConfiguration.class).setJettyEnvResource(new PathResource(jettyEnvXml)); ClassLoader parentLoader = this.getClass().getClassLoader(); if (parentLoader instanceof AntClassLoader) @@ -608,7 +609,7 @@ public class AntWebAppContext extends WebAppContext //apply a context xml file if one was supplied if (contextXml != null) { - XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(contextXml)); + XmlConfiguration xmlConfiguration = new XmlConfiguration(new PathResource(contextXml)); TaskLog.log("Applying context xml file "+contextXml); xmlConfiguration.configure(this); } diff --git a/jetty-ant/src/main/java/org/eclipse/jetty/ant/ServerProxyImpl.java b/jetty-ant/src/main/java/org/eclipse/jetty/ant/ServerProxyImpl.java index ac62bfc280a..9cc5b05c36c 100644 --- a/jetty-ant/src/main/java/org/eclipse/jetty/ant/ServerProxyImpl.java +++ b/jetty-ant/src/main/java/org/eclipse/jetty/ant/ServerProxyImpl.java @@ -41,6 +41,7 @@ import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.util.Scanner; +import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlConfiguration; import org.xml.sax.SAXException; @@ -451,7 +452,7 @@ public class ServerProxyImpl implements ServerProxy XmlConfiguration configuration; try { - configuration = new XmlConfiguration(Resource.toURL(jettyXml)); + configuration = new XmlConfiguration(new PathResource(jettyXml)); configuration.configure(server); } catch (MalformedURLException e) diff --git a/jetty-bom/pom.xml b/jetty-bom/pom.xml index 31df62c78dc..64844bddf11 100644 --- a/jetty-bom/pom.xml +++ b/jetty-bom/pom.xml @@ -186,7 +186,17 @@ org.eclipse.jetty - jetty-infinispan + infinispan-common + 10.0.0-SNAPSHOT + + + org.eclipse.jetty + infinispan-remote-query + 10.0.0-SNAPSHOT + + + org.eclipse.jetty + infinispan-embedded-query 10.0.0-SNAPSHOT diff --git a/jetty-cdi/cdi-2/src/main/config/etc/cdi2/jetty-cdi2.xml b/jetty-cdi/cdi-2/src/main/config/etc/cdi2/jetty-cdi2.xml index 3af8f23f2b2..82fcecb6a94 100644 --- a/jetty-cdi/cdi-2/src/main/config/etc/cdi2/jetty-cdi2.xml +++ b/jetty-cdi/cdi-2/src/main/config/etc/cdi2/jetty-cdi2.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-cdi/cdi-2/src/main/config/etc/cdi2/jetty-web-cdi2.xml b/jetty-cdi/cdi-2/src/main/config/etc/cdi2/jetty-web-cdi2.xml index b45e52e2608..94c413b18f5 100644 --- a/jetty-cdi/cdi-2/src/main/config/etc/cdi2/jetty-web-cdi2.xml +++ b/jetty-cdi/cdi-2/src/main/config/etc/cdi2/jetty-web-cdi2.xml @@ -1,22 +1,22 @@ - + - - - -org.eclipse.jetty.util.Decorator - - - -org.eclipse.jetty.util.DecoratedObjectFactory - - - -org.eclipse.jetty.server.handler.ContextHandler. - - - -org.eclipse.jetty.server.handler.ContextHandler - - - -org.eclipse.jetty.servlet.ServletContextHandler - - + + + -org.eclipse.jetty.util.Decorator + + + -org.eclipse.jetty.util.DecoratedObjectFactory + + + -org.eclipse.jetty.server.handler.ContextHandler. + + + -org.eclipse.jetty.server.handler.ContextHandler + + + -org.eclipse.jetty.servlet.ServletContextHandler + + diff --git a/jetty-cdi/cdi-core/src/test/java/org/eclipse/jetty/cdi/core/NamedLiteralTest.java b/jetty-cdi/cdi-core/src/test/java/org/eclipse/jetty/cdi/core/NamedLiteralTest.java new file mode 100644 index 00000000000..82f09de8d47 --- /dev/null +++ b/jetty-cdi/cdi-core/src/test/java/org/eclipse/jetty/cdi/core/NamedLiteralTest.java @@ -0,0 +1,45 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.cdi.core; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for class {@link NamedLiteral}. + * + * @see NamedLiteral + */ +public class NamedLiteralTest +{ + + @Test + public void testCreatesNamedLiteralWithNull() + { + assertEquals("", new NamedLiteral(null).value()); + } + + @Test + public void testGetValue() + { + assertEquals("a b", new NamedLiteral("a b").value()); + } + +} diff --git a/jetty-client/pom.xml b/jetty-client/pom.xml index b078c636b3e..2ce321e6e30 100644 --- a/jetty-client/pom.xml +++ b/jetty-client/pom.xml @@ -112,6 +112,12 @@ jetty-io ${project.version} + + org.eclipse.jetty + jetty-alpn-client + ${project.version} + true + org.eclipse.jetty jetty-jmx diff --git a/jetty-client/src/main/java/module-info.java b/jetty-client/src/main/java/module-info.java index 92ca51083ca..fcb2b0c2d7f 100644 --- a/jetty-client/src/main/java/module-info.java +++ b/jetty-client/src/main/java/module-info.java @@ -20,8 +20,10 @@ module org.eclipse.jetty.client { exports org.eclipse.jetty.client; exports org.eclipse.jetty.client.api; + exports org.eclipse.jetty.client.dynamic; exports org.eclipse.jetty.client.http; exports org.eclipse.jetty.client.jmx to org.eclipse.jetty.jmx; + exports org.eclipse.jetty.client.proxy; exports org.eclipse.jetty.client.util; requires org.eclipse.jetty.http; @@ -30,6 +32,8 @@ module org.eclipse.jetty.client // Only required if using SPNEGO. requires static java.security.jgss; + // Only required if using the dynamic transport. + requires static org.eclipse.jetty.alpn.client; // Only required if using JMX. requires static org.eclipse.jetty.jmx; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java index 78d2ee3aa5d..55dff0d7c89 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java @@ -119,14 +119,14 @@ public abstract class AbstractConnectionPool implements ConnectionPool, Dumpable if (LOG.isDebugEnabled()) LOG.debug("newConnection {}/{} connections {}/{} pending", total+1, maxConnections, pending+1, maxPending); - destination.newConnection(new Promise() + destination.newConnection(new Promise<>() { @Override public void succeeded(Connection connection) { if (LOG.isDebugEnabled()) - LOG.debug("Connection {}/{} creation succeeded {}", total+1, maxConnections, connection); - connections.add(-1,0); + LOG.debug("Connection {}/{} creation succeeded {}", total + 1, maxConnections, connection); + connections.add(-1, 0); onCreated(connection); proceed(); } @@ -135,8 +135,8 @@ public abstract class AbstractConnectionPool implements ConnectionPool, Dumpable public void failed(Throwable x) { if (LOG.isDebugEnabled()) - LOG.debug("Connection " + (total+1) + "/" + maxConnections + " creation failed", x); - connections.add(-1,-1); + LOG.debug("Connection " + (total + 1) + "/" + maxConnections + " creation failed", x); + connections.add(-1, -1); requester.failed(x); } }); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index 1388931e233..a51aea42b9b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -217,6 +218,8 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler path = request.getPath(); } Request newRequest = client.copyRequest(request, requestURI); + // Disable the timeout so that only the one from the initial request applies. + newRequest.timeout(0, TimeUnit.MILLISECONDS); if (path != null) newRequest.path(path); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/DuplexHttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/DuplexHttpDestination.java new file mode 100644 index 00000000000..1fe94996a33 --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/DuplexHttpDestination.java @@ -0,0 +1,37 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +/** + *

A destination for those network transports that are duplex (e.g. HTTP/1.1 and FastCGI).

+ * + * @see MultiplexHttpDestination + */ +public class DuplexHttpDestination extends HttpDestination +{ + public DuplexHttpDestination(HttpClient client, Origin origin) + { + this(client, new Key(origin, null)); + } + + public DuplexHttpDestination(HttpClient client, Key key) + { + super(client, key); + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 24a30100718..4b82e97e490 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -28,6 +28,7 @@ import java.net.SocketAddress; import java.net.URI; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -61,6 +62,7 @@ import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; import org.eclipse.jetty.util.Fields; @@ -122,19 +124,16 @@ public class HttpClient extends ContainerLifeCycle public static final String USER_AGENT = "Jetty/" + Jetty.VERSION; private static final Logger LOG = Log.getLogger(HttpClient.class); - private final ConcurrentMap destinations = new ConcurrentHashMap<>(); + private final ConcurrentMap destinations = new ConcurrentHashMap<>(); private final ProtocolHandlers handlers = new ProtocolHandlers(); private final List requestListeners = new ArrayList<>(); private final Set decoderFactories = new ContentDecoderFactorySet(); private final ProxyConfiguration proxyConfig = new ProxyConfiguration(); private final HttpClientTransport transport; - private final SslContextFactory sslContextFactory; + private final ClientConnector connector; private AuthenticationStore authenticationStore = new HttpAuthenticationStore(); private CookieManager cookieManager; private CookieStore cookieStore; - private Executor executor; - private ByteBufferPool byteBufferPool; - private Scheduler scheduler; private SocketAddressResolver resolver; private HttpField agentField = new HttpField(HttpHeader.USER_AGENT, USER_AGENT); private boolean followRedirects = true; @@ -143,48 +142,28 @@ public class HttpClient extends ContainerLifeCycle private int requestBufferSize = 4096; private int responseBufferSize = 16384; private int maxRedirects = 8; - private SocketAddress bindAddress; - private long connectTimeout = 15000; private long addressResolutionTimeout = 15000; - private long idleTimeout; private boolean tcpNoDelay = true; private boolean strictEventOrdering = false; private HttpField encodingField; private boolean removeIdleDestinations = false; - private boolean connectBlocking = false; private String name = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); private HttpCompliance httpCompliance = HttpCompliance.RFC7230; private String defaultRequestContentType = "application/octet-stream"; /** - * Creates a HttpClient instance that can perform requests to non-TLS destinations only - * (that is, requests with the "http" scheme only, and not "https"). - * - * @see #HttpClient(SslContextFactory) to perform requests to TLS destinations. + * Creates a HttpClient instance that can perform HTTP/1.1 requests to non-TLS and TLS destinations. */ public HttpClient() { - this(null); + this(new HttpClientTransportOverHTTP()); } - /** - * Creates a HttpClient instance that can perform requests to non-TLS and TLS destinations - * (that is, both requests with the "http" scheme and with the "https" scheme). - * - * @param sslContextFactory the {@link SslContextFactory} that manages TLS encryption - * @see #getSslContextFactory() - */ - public HttpClient(SslContextFactory sslContextFactory) + public HttpClient(HttpClientTransport transport) { - this(new HttpClientTransportOverHTTP(), sslContextFactory); - } - - public HttpClient(HttpClientTransport transport, SslContextFactory sslContextFactory) - { - this.transport = transport; + this.transport = Objects.requireNonNull(transport); addBean(transport); - this.sslContextFactory = sslContextFactory; - addBean(sslContextFactory); + this.connector = ((AbstractHttpClientTransport)transport).getBean(ClientConnector.class); addBean(handlers); addBean(decoderFactories); } @@ -202,34 +181,34 @@ public class HttpClient extends ContainerLifeCycle /** * @return the {@link SslContextFactory} that manages TLS encryption - * @see #HttpClient(SslContextFactory) */ - public SslContextFactory getSslContextFactory() + public SslContextFactory.Client getSslContextFactory() { - return sslContextFactory; + return connector.getSslContextFactory(); } @Override protected void doStart() throws Exception { + Executor executor = getExecutor(); if (executor == null) { QueuedThreadPool threadPool = new QueuedThreadPool(); threadPool.setName(name); setExecutor(threadPool); } - + ByteBufferPool byteBufferPool = getByteBufferPool(); if (byteBufferPool == null) setByteBufferPool(new MappedByteBufferPool(2048, executor instanceof ThreadPool.SizedThreadPool ? ((ThreadPool.SizedThreadPool)executor).getMaxThreads() / 2 : ProcessorUtils.availableProcessors() * 2)); - + Scheduler scheduler = getScheduler(); if (scheduler == null) setScheduler(new ScheduledExecutorScheduler(name + "-scheduler", false)); if (resolver == null) - setSocketAddressResolver(new SocketAddressResolver.Async(executor, scheduler, getAddressResolutionTimeout())); + setSocketAddressResolver(new SocketAddressResolver.Async(getExecutor(), getScheduler(), getAddressResolutionTimeout())); handlers.put(new ContinueProtocolHandler()); handlers.put(new RedirectProtocolHandler(this)); @@ -291,6 +270,8 @@ public class HttpClient extends ContainerLifeCycle */ public void setCookieStore(CookieStore cookieStore) { + if (isStarted()) + throw new IllegalStateException(); this.cookieStore = Objects.requireNonNull(cookieStore); this.cookieManager = newCookieManager(); } @@ -319,6 +300,8 @@ public class HttpClient extends ContainerLifeCycle */ public void setAuthenticationStore(AuthenticationStore authenticationStore) { + if (isStarted()) + throw new IllegalStateException(); this.authenticationStore = authenticationStore; } @@ -523,10 +506,11 @@ public class HttpClient extends ContainerLifeCycle */ public Destination getDestination(String scheme, String host, int port) { - return destinationFor(scheme, host, port); + Origin origin = createOrigin(scheme, host, port); + return resolveDestination(new HttpDestination.Key(origin, null)); } - protected HttpDestination destinationFor(String scheme, String host, int port) + private Origin createOrigin(String scheme, String host, int port) { if (!HttpScheme.HTTP.is(scheme) && !HttpScheme.HTTPS.is(scheme) && !HttpScheme.WS.is(scheme) && !HttpScheme.WSS.is(scheme)) @@ -536,13 +520,18 @@ public class HttpClient extends ContainerLifeCycle host = host.toLowerCase(Locale.ENGLISH); port = normalizePort(scheme, port); - Origin origin = new Origin(scheme, host, port); - HttpDestination destination = destinations.get(origin); + return new Origin(scheme, host, port); + } + + private HttpDestination resolveDestination(HttpDestination.Key key) + { + HttpDestination destination = destinations.get(key); if (destination == null) { - destination = transport.newHttpDestination(origin); + destination = getTransport().newHttpDestination(key); + // Start the destination before it's published to other threads. addManaged(destination); - HttpDestination existing = destinations.putIfAbsent(origin, destination); + HttpDestination existing = destinations.putIfAbsent(key, destination); if (existing != null) { removeBean(destination); @@ -560,7 +549,7 @@ public class HttpClient extends ContainerLifeCycle protected boolean removeDestination(HttpDestination destination) { removeBean(destination); - return destinations.remove(destination.getOrigin(), destination); + return destinations.remove(destination.getKey(), destination); } /** @@ -573,7 +562,16 @@ public class HttpClient extends ContainerLifeCycle protected void send(final HttpRequest request, List listeners) { - HttpDestination destination = destinationFor(request.getScheme(), request.getHost(), request.getPort()); + Origin origin = createOrigin(request.getScheme(), request.getHost(), request.getPort()); + HttpClientTransport transport = getTransport(); + HttpDestination.Key destinationKey = null; + if (transport instanceof HttpClientTransport.Dynamic) + destinationKey = ((HttpClientTransport.Dynamic)transport).newDestinationKey(request, origin); + if (destinationKey == null) + destinationKey = new HttpDestination.Key(origin, null); + if (LOG.isDebugEnabled()) + LOG.debug("Selected {} for {}", destinationKey, request); + HttpDestination destination = resolveDestination(destinationKey); destination.send(request, listeners); } @@ -636,7 +634,7 @@ public class HttpClient extends ContainerLifeCycle */ public ByteBufferPool getByteBufferPool() { - return byteBufferPool; + return connector.getByteBufferPool(); } /** @@ -644,10 +642,7 @@ public class HttpClient extends ContainerLifeCycle */ public void setByteBufferPool(ByteBufferPool byteBufferPool) { - if (isStarted()) - LOG.warn("Calling setByteBufferPool() while started is deprecated"); - updateBean(this.byteBufferPool, byteBufferPool); - this.byteBufferPool = byteBufferPool; + connector.setByteBufferPool(byteBufferPool); } /** @@ -677,7 +672,7 @@ public class HttpClient extends ContainerLifeCycle @ManagedAttribute("The timeout, in milliseconds, for connect() operations") public long getConnectTimeout() { - return connectTimeout; + return connector.getConnectTimeout().toMillis(); } /** @@ -686,7 +681,7 @@ public class HttpClient extends ContainerLifeCycle */ public void setConnectTimeout(long connectTimeout) { - this.connectTimeout = connectTimeout; + connector.setConnectTimeout(Duration.ofMillis(connectTimeout)); } /** @@ -718,7 +713,7 @@ public class HttpClient extends ContainerLifeCycle @ManagedAttribute("The timeout, in milliseconds, to close idle connections") public long getIdleTimeout() { - return idleTimeout; + return connector.getIdleTimeout().toMillis(); } /** @@ -726,7 +721,7 @@ public class HttpClient extends ContainerLifeCycle */ public void setIdleTimeout(long idleTimeout) { - this.idleTimeout = idleTimeout; + connector.setIdleTimeout(Duration.ofMillis(idleTimeout)); } /** @@ -735,7 +730,7 @@ public class HttpClient extends ContainerLifeCycle */ public SocketAddress getBindAddress() { - return bindAddress; + return connector.getBindAddress(); } /** @@ -745,7 +740,7 @@ public class HttpClient extends ContainerLifeCycle */ public void setBindAddress(SocketAddress bindAddress) { - this.bindAddress = bindAddress; + connector.setBindAddress(bindAddress); } /** @@ -790,7 +785,7 @@ public class HttpClient extends ContainerLifeCycle */ public Executor getExecutor() { - return executor; + return connector.getExecutor(); } /** @@ -798,10 +793,7 @@ public class HttpClient extends ContainerLifeCycle */ public void setExecutor(Executor executor) { - if (isStarted()) - LOG.warn("Calling setExecutor() while started is deprecated"); - updateBean(this.executor, executor); - this.executor = executor; + connector.setExecutor(executor); } /** @@ -809,7 +801,7 @@ public class HttpClient extends ContainerLifeCycle */ public Scheduler getScheduler() { - return scheduler; + return connector.getScheduler(); } /** @@ -817,10 +809,7 @@ public class HttpClient extends ContainerLifeCycle */ public void setScheduler(Scheduler scheduler) { - if (isStarted()) - LOG.warn("Calling setScheduler() while started is deprecated"); - updateBean(this.scheduler, scheduler); - this.scheduler = scheduler; + connector.setScheduler(scheduler); } /** @@ -837,7 +826,7 @@ public class HttpClient extends ContainerLifeCycle public void setSocketAddressResolver(SocketAddressResolver resolver) { if (isStarted()) - LOG.warn("Calling setSocketAddressResolver() while started is deprecated"); + throw new IllegalStateException(); updateBean(this.resolver, resolver); this.resolver = resolver; } @@ -929,7 +918,7 @@ public class HttpClient extends ContainerLifeCycle } /** - * @return the max number of HTTP redirects that are followed + * @return the max number of HTTP redirects that are followed in a conversation * @see #setMaxRedirects(int) */ public int getMaxRedirects() @@ -938,7 +927,7 @@ public class HttpClient extends ContainerLifeCycle } /** - * @param maxRedirects the max number of HTTP redirects that are followed + * @param maxRedirects the max number of HTTP redirects that are followed in a conversation, or -1 for unlimited redirects * @see #setFollowRedirects(boolean) */ public void setMaxRedirects(int maxRedirects) @@ -1059,7 +1048,7 @@ public class HttpClient extends ContainerLifeCycle @ManagedAttribute("Whether the connect() operation is blocking") public boolean isConnectBlocking() { - return connectBlocking; + return connector.isConnectBlocking(); } /** @@ -1074,7 +1063,7 @@ public class HttpClient extends ContainerLifeCycle */ public void setConnectBlocking(boolean connectBlocking) { - this.connectBlocking = connectBlocking; + connector.setConnectBlocking(connectBlocking); } /** @@ -1109,7 +1098,7 @@ public class HttpClient extends ContainerLifeCycle protected String normalizeHost(String host) { - if (host != null && host.matches("\\[.*\\]")) + if (host != null && host.matches("\\[.*]")) return host.substring(1, host.length() - 1); return host; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java index 7d2366a19fd..68aa5772fdc 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java @@ -43,7 +43,7 @@ public interface HttpClientTransport extends ClientConnectionFactory * Sets the {@link HttpClient} instance on this transport. *

* This is needed because of a chicken-egg problem: in order to create the {@link HttpClient} - * a {@link HttpClientTransport} is needed, that therefore cannot have a reference yet to the + * a HttpClientTransport is needed, that therefore cannot have a reference yet to the * {@link HttpClient}. * * @param client the {@link HttpClient} that uses this transport. @@ -56,15 +56,15 @@ public interface HttpClientTransport extends ClientConnectionFactory * {@link HttpDestination} controls the destination-connection cardinality: protocols like * HTTP have 1-N cardinality, while multiplexed protocols like HTTP/2 have a 1-1 cardinality. * - * @param origin the destination origin + * @param key the destination key * @return a new, transport-specific, {@link HttpDestination} object */ - public HttpDestination newHttpDestination(Origin origin); + public HttpDestination newHttpDestination(HttpDestination.Key key); /** * Establishes a physical connection to the given {@code address}. * - * @param address the address to connect to + * @param address the address to connect to * @param context the context information to establish the connection */ public void connect(InetSocketAddress address, Map context); @@ -78,4 +78,20 @@ public interface HttpClientTransport extends ClientConnectionFactory * @param factory the factory for ConnectionPool instances */ public void setConnectionPoolFactory(ConnectionPool.Factory factory); + + /** + * Specifies whether a {@link HttpClientTransport} is dynamic. + */ + @FunctionalInterface + public interface Dynamic + { + /** + * Creates a new Key with the given request and origin. + * + * @param request the request that triggers the creation of the Key + * @param origin the origin of the server for the request + * @return a Key that identifies a destination + */ + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin); + } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java index de3c4576da4..a4a264e8342 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -27,7 +27,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.eclipse.jetty.client.api.Authentication; -import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; @@ -38,7 +37,7 @@ import org.eclipse.jetty.util.HttpCookieStore; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -public abstract class HttpConnection implements Connection +public abstract class HttpConnection implements IConnection { private static final Logger LOG = Log.getLogger(HttpConnection.class); @@ -80,8 +79,6 @@ public abstract class HttpConnection implements Connection httpRequest.abort(result.failure); } - protected abstract SendFailure send(HttpExchange exchange); - protected void normalizeRequest(Request request) { HttpVersion version = request.getVersion(); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java index a555d700965..3f57de2b9fc 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java @@ -25,9 +25,13 @@ import java.util.concurrent.ConcurrentLinkedDeque; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.util.AttributesMap; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; public class HttpConversation extends AttributesMap { + private static final Logger LOG = Log.getLogger(HttpConversation.class); + private final Deque exchanges = new ConcurrentLinkedDeque<>(); private volatile List listeners; @@ -118,6 +122,7 @@ public class HttpConversation extends AttributesMap HttpExchange lastExchange = exchanges.peekLast(); if (firstExchange == lastExchange) { + // We don't have a conversation, just a single request. if (overrideListener != null) listeners.add(overrideListener); else @@ -125,13 +130,16 @@ public class HttpConversation extends AttributesMap } else { - // Order is important, we want to notify the last exchange first + // We have a conversation (e.g. redirect, authentication). + // Order is important, we want to notify the last exchange first. listeners.addAll(lastExchange.getResponseListeners()); if (overrideListener != null) listeners.add(overrideListener); else listeners.addAll(firstExchange.getResponseListeners()); } + if (LOG.isDebugEnabled()) + LOG.debug("Exchanges in conversation {}, override={}, listeners={}", exchanges.size(), overrideListener, listeners); this.listeners = listeners; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java index 58fde88a489..14022463f73 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java @@ -23,20 +23,25 @@ import java.io.IOException; import java.nio.channels.AsynchronousCloseException; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Queue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.CyclicTimeout; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.HostPort; @@ -52,12 +57,12 @@ import org.eclipse.jetty.util.thread.Scheduler; import org.eclipse.jetty.util.thread.Sweeper; @ManagedObject -public abstract class HttpDestination extends ContainerLifeCycle implements Destination, Closeable, Callback, Dumpable +public class HttpDestination extends ContainerLifeCycle implements Destination, Closeable, Callback, Dumpable { protected static final Logger LOG = Log.getLogger(HttpDestination.class); private final HttpClient client; - private final Origin origin; + private final Key key; private final Queue exchanges; private final RequestNotifier requestNotifier; private final ResponseNotifier responseNotifier; @@ -67,21 +72,38 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest private final TimeoutTask timeout; private ConnectionPool connectionPool; - public HttpDestination(HttpClient client, Origin origin) + public HttpDestination(HttpClient client, Key key) + { + this(client, key, Function.identity()); + } + + public HttpDestination(HttpClient client, Key key, Function factoryFn) { this.client = client; - this.origin = origin; + this.key = key; this.exchanges = newExchangeQueue(client); this.requestNotifier = new RequestNotifier(client); this.responseNotifier = new ResponseNotifier(); - + this.timeout = new TimeoutTask(client.getScheduler()); + String host = HostPort.normalizeHost(getHost()); + if (!client.isDefaultPort(getScheme(), getPort())) + host += ":" + getPort(); + hostField = new HttpField(HttpHeader.HOST, host); + ProxyConfiguration proxyConfig = client.getProxyConfiguration(); - proxy = proxyConfig.match(origin); - ClientConnectionFactory connectionFactory = client.getTransport(); + this.proxy = proxyConfig.match(getOrigin()); + + this.connectionFactory = factoryFn.apply(createClientConnectionFactory()); + } + + private ClientConnectionFactory createClientConnectionFactory() + { + ProxyConfiguration.Proxy proxy = getProxy(); + ClientConnectionFactory connectionFactory = getHttpClient().getTransport(); if (proxy != null) { connectionFactory = proxy.newClientConnectionFactory(connectionFactory); @@ -93,12 +115,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest if (isSecure()) connectionFactory = newSslClientConnectionFactory(connectionFactory); } - this.connectionFactory = connectionFactory; - - String host = HostPort.normalizeHost(getHost()); - if (!client.isDefaultPort(getScheme(), getPort())) - host += ":" + getPort(); - hostField = new HttpField(HttpHeader.HOST, host); + return connectionFactory; } @Override @@ -147,9 +164,14 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest return client; } + public Key getKey() + { + return key; + } + public Origin getOrigin() { - return origin; + return key.origin; } public Queue getHttpExchanges() @@ -181,7 +203,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest @ManagedAttribute(value = "The destination scheme", readonly = true) public String getScheme() { - return origin.getScheme(); + return getOrigin().getScheme(); } @Override @@ -190,14 +212,14 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest { // InetSocketAddress.getHostString() transforms the host string // in case of IPv6 addresses, so we return the original host string - return origin.getAddress().getHost(); + return getOrigin().getAddress().getHost(); } @Override @ManagedAttribute(value = "The destination port", readonly = true) public int getPort() { - return origin.getAddress().getPort(); + return getOrigin().getAddress().getPort(); } @ManagedAttribute(value = "The number of queued requests", readonly = true) @@ -208,7 +230,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest public Origin.Address getConnectAddress() { - return proxy == null ? origin.getAddress() : proxy.getAddress(); + return proxy == null ? getOrigin().getAddress() : proxy.getAddress(); } public HttpField getHostField() @@ -235,7 +257,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest } protected void send(HttpRequest request, List listeners) - { + { if (!getScheme().equalsIgnoreCase(request.getScheme())) throw new IllegalArgumentException("Invalid request scheme " + request.getScheme() + " for destination " + this); if (!getHost().equalsIgnoreCase(request.getHost())) @@ -343,7 +365,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest } else { - SendFailure result = send(connection, exchange); + SendFailure result = ((IConnection)connection).send(exchange); if (result != null) { if (LOG.isDebugEnabled()) @@ -358,8 +380,6 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest } } - protected abstract SendFailure send(Connection connection, HttpExchange exchange); - @Override public void newConnection(Promise promise) { @@ -476,7 +496,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest public String asString() { - return origin.asString(); + return getKey().asString(); } @Override @@ -490,8 +510,178 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest exchanges.size(), connectionPool); } - - // The TimeoutTask that expires when the next check of expiry is needed + + @FunctionalInterface + public interface Multiplexed + { + void setMaxRequestsPerConnection(int maxRequestsPerConnection); + } + + /** + *

Class that groups the elements that uniquely identify a destination.

+ *

The elements are an {@link Origin}, a {@link Protocol} and an opaque + * string that further distinguishes destinations that have the same origin + * and protocol.

+ *

In general it is possible that, for the same origin, the server can + * speak different protocols (for example, clear-text HTTP/1.1 and clear-text + * HTTP/2), so the {@link Protocol} makes that distinction.

+ *

Furthermore, it may be desirable to have different destinations for + * the same origin and protocol (for example, when using the PROXY protocol + * in a reverse proxy server, you want to be able to map the client ip:port + * to the destination {@code kind}, so that all the connections to the server + * associated to that destination can specify the PROXY protocol bytes for + * that particular client connection.

+ */ + public static class Key + { + private final Origin origin; + private final Protocol protocol; + private final String kind; + + /** + * Creates a Key with the given origin and protocol and a {@code null} kind. + * + * @param origin the origin + * @param protocol the protocol + */ + public Key(Origin origin, Protocol protocol) + { + this(origin, protocol, null); + } + + /** + * Creates a Key with the given origin and protocol and kind. + * + * @param origin the origin + * @param protocol the protocol + * @param kind the opaque kind + */ + public Key(Origin origin, Protocol protocol, String kind) + { + this.origin = origin; + this.protocol = protocol; + this.kind = kind; + } + + public Origin getOrigin() + { + return origin; + } + + public Protocol getProtocol() + { + return protocol; + } + + public String getKind() + { + return kind; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + Key that = (Key)obj; + return origin.equals(that.origin) && + Objects.equals(protocol, that.protocol) && + Objects.equals(kind, that.kind); + } + + @Override + public int hashCode() + { + return Objects.hash(origin, protocol, kind); + } + + public String asString() + { + return String.format("%s|%s,kind=%s", + origin.asString(), + protocol == null ? "null" : protocol.asString(), + kind); + } + + @Override + public String toString() + { + return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), asString()); + } + } + + /** + *

The representation of a network protocol.

+ *

A network protocol may have multiple protocol names + * associated to it, for example {@code ["h2", "h2-17", "h2-16"]}.

+ *

A Protocol is then rendered into a {@link ClientConnectionFactory} + * chain, for example in + * {@link HttpClientTransportDynamic#newConnection(EndPoint, Map)}.

+ */ + public static class Protocol + { + private final List protocols; + private final boolean negotiate; + + /** + * Creates a Protocol with the given list of protocol names + * and whether it should negotiate the protocol. + * + * @param protocols the protocol names + * @param negotiate whether the protocol should be negotiated + */ + public Protocol(List protocols, boolean negotiate) + { + this.protocols = protocols; + this.negotiate = negotiate; + } + + public List getProtocols() + { + return protocols; + } + + public boolean isNegotiate() + { + return negotiate; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + Protocol that = (Protocol)obj; + return protocols.equals(that.protocols) && negotiate == that.negotiate; + } + + @Override + public int hashCode() + { + return Objects.hash(protocols, negotiate); + } + + public String asString() + { + return String.format("proto=%s,nego=%b", protocols, negotiate); + } + + @Override + public String toString() + { + return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), asString()); + } + } + + /** + * This class enforces the total timeout for exchanges that are still in the queue. + * The total timeout for exchanges that are not in the destination queue is enforced + * by {@link HttpChannel}. + */ private class TimeoutTask extends CyclicTimeout { private final AtomicLong nextTimeout = new AtomicLong(Long.MAX_VALUE); @@ -504,10 +694,13 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest @Override public void onTimeoutExpired() { + if (LOG.isDebugEnabled()) + LOG.debug("{} timeout expired", this); + nextTimeout.set(Long.MAX_VALUE); long now = System.nanoTime(); long nextExpiresAt = Long.MAX_VALUE; - + // Check all queued exchanges for those that have expired // and to determine when the next check must be. for (HttpExchange exchange : exchanges) @@ -521,7 +714,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest else if (expiresAt < nextExpiresAt) nextExpiresAt = expiresAt; } - + if (nextExpiresAt < Long.MAX_VALUE && client.isRunning()) schedule(nextExpiresAt); } @@ -536,12 +729,16 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest if (timeoutAt != expiresAt) { long delay = expiresAt - System.nanoTime(); - if (LOG.isDebugEnabled()) - LOG.debug("Scheduled timeout in {} ms", TimeUnit.NANOSECONDS.toMillis(delay)); if (delay <= 0) + { onTimeoutExpired(); + } else + { schedule(delay, TimeUnit.NANOSECONDS); + if (LOG.isDebugEnabled()) + LOG.debug("{} scheduled timeout in {} ms", this, TimeUnit.NANOSECONDS.toMillis(delay)); + } } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRedirector.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRedirector.java index 24bdc74d8c6..47292818600 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRedirector.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRedirector.java @@ -23,6 +23,7 @@ import java.net.URISyntaxException; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -61,10 +62,10 @@ public class HttpRedirector { private static final Logger LOG = Log.getLogger(HttpRedirector.class); private static final String SCHEME_REGEXP = "(^https?)"; - private static final String AUTHORITY_REGEXP = "([^/\\?#]+)"; + private static final String AUTHORITY_REGEXP = "([^/?#]+)"; // The location may be relative so the scheme://authority part may be missing private static final String DESTINATION_REGEXP = "(" + SCHEME_REGEXP + "://" + AUTHORITY_REGEXP + ")?"; - private static final String PATH_REGEXP = "([^\\?#]*)"; + private static final String PATH_REGEXP = "([^?#]*)"; private static final String QUERY_REGEXP = "([^#]*)"; private static final String FRAGMENT_REGEXP = "(.*)"; private static final Pattern URI_PATTERN = Pattern.compile(DESTINATION_REGEXP + PATH_REGEXP + QUERY_REGEXP + FRAGMENT_REGEXP); @@ -101,11 +102,11 @@ public class HttpRedirector /** * Redirects the given {@code response}, blocking until the redirect is complete. * - * @param request the original request that triggered the redirect + * @param request the original request that triggered the redirect * @param response the response to the original request * @return a {@link Result} object containing the request to the redirected location and its response * @throws InterruptedException if the thread is interrupted while waiting for the redirect to complete - * @throws ExecutionException if the redirect failed + * @throws ExecutionException if the redirect failed * @see #redirect(Request, Response, Response.CompleteListener) */ public Result redirect(Request request, Response response) throws InterruptedException, ExecutionException @@ -144,7 +145,7 @@ public class HttpRedirector /** * Redirects the given {@code response} asynchronously. * - * @param request the original request that triggered the redirect + * @param request the original request that triggered the redirect * @param response the response to the original request * @param listener the listener that receives response events * @return the request to the redirected location @@ -292,7 +293,8 @@ public class HttpRedirector Integer redirects = (Integer)conversation.getAttribute(ATTRIBUTE); if (redirects == null) redirects = 0; - if (redirects < client.getMaxRedirects()) + int maxRedirects = client.getMaxRedirects(); + if (maxRedirects < 0 || redirects < maxRedirects) { ++redirects; conversation.setAttribute(ATTRIBUTE, redirects); @@ -310,19 +312,17 @@ public class HttpRedirector try { Request redirect = client.copyRequest(httpRequest, location); + // Disable the timeout so that only the one from the initial request applies. + redirect.timeout(0, TimeUnit.MILLISECONDS); // Use given method redirect.method(method); - redirect.onRequestBegin(new Request.BeginListener() + redirect.onRequestBegin(request -> { - @Override - public void onBegin(Request redirect) - { - Throwable cause = httpRequest.getAbortCause(); - if (cause != null) - redirect.abort(cause); - } + Throwable cause = httpRequest.getAbortCause(); + if (cause != null) + request.abort(cause); }); redirect.send(listener); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/PoolingHttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/IConnection.java similarity index 82% rename from jetty-client/src/main/java/org/eclipse/jetty/client/PoolingHttpDestination.java rename to jetty-client/src/main/java/org/eclipse/jetty/client/IConnection.java index 1da6f1e4859..0e935aba1e1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/PoolingHttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/IConnection.java @@ -18,10 +18,9 @@ package org.eclipse.jetty.client; -public abstract class PoolingHttpDestination extends HttpDestination +import org.eclipse.jetty.client.api.Connection; + +public interface IConnection extends Connection { - public PoolingHttpDestination(HttpClient client, Origin origin) - { - super(client, origin); - } + public SendFailure send(HttpExchange exchange); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexConnectionPool.java index 05179ffd421..cfdfbd530a9 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexConnectionPool.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexConnectionPool.java @@ -22,11 +22,10 @@ import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; -import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.eclipse.jetty.client.api.Connection; @@ -41,11 +40,9 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C { private static final Logger LOG = Log.getLogger(MultiplexConnectionPool.class); - private final ReentrantLock lock = new ReentrantLock(); private final HttpDestination destination; private final Deque idleConnections; - private final Map muxedConnections; - private final Map busyConnections; + private final Map activeConnections; private int maxMultiplex; public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplex) @@ -53,8 +50,7 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C super(destination, maxConnections, requester); this.destination = destination; this.idleConnections = new ArrayDeque<>(maxConnections); - this.muxedConnections = new HashMap<>(maxConnections); - this.busyConnections = new HashMap<>(maxConnections); + this.activeConnections = new LinkedHashMap<>(maxConnections); this.maxMultiplex = maxMultiplex; } @@ -69,120 +65,73 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C connection = activate(); } return connection; - } - - protected void lock() - { - lock.lock(); - } - - protected void unlock() - { - lock.unlock(); } @Override public int getMaxMultiplex() { - lock(); - try + synchronized (this) { return maxMultiplex; } - finally - { - unlock(); - } } @Override public void setMaxMultiplex(int maxMultiplex) { - lock(); - try + synchronized (this) { this.maxMultiplex = maxMultiplex; } - finally - { - unlock(); - } } @Override public boolean isActive(Connection connection) { - lock(); - try + synchronized (this) { - if (muxedConnections.containsKey(connection)) - return true; - if (busyConnections.containsKey(connection)) - return true; - return false; - } - finally - { - unlock(); + return activeConnections.containsKey(connection); } } @Override protected void onCreated(Connection connection) { - lock(); - try + synchronized (this) { // Use "cold" connections as last. idleConnections.offer(new Holder(connection)); } - finally - { - unlock(); - } - idle(connection, false); } @Override protected Connection activate() { - Holder holder; - lock(); - try + Holder result = null; + synchronized (this) { - while (true) + for (Holder holder : activeConnections.values()) { - if (muxedConnections.isEmpty()) - { - holder = idleConnections.poll(); - if (holder == null) - return null; - muxedConnections.put(holder.connection, holder); - } - else - { - holder = muxedConnections.values().iterator().next(); - } - if (holder.count < maxMultiplex) { - ++holder.count; + result = holder; break; } - else - { - muxedConnections.remove(holder.connection); - busyConnections.put(holder.connection, holder); - } } - } - finally - { - unlock(); - } - return active(holder.connection); + if (result == null) + { + Holder holder = idleConnections.poll(); + if (holder == null) + return null; + activeConnections.put(holder.connection, holder); + result = holder; + } + + ++result.count; + } + return active(result.connection); } @Override @@ -191,16 +140,15 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C boolean closed = isClosed(); boolean idle = false; Holder holder; - lock(); - try + synchronized (this) { - holder = muxedConnections.get(connection); + holder = activeConnections.get(connection); if (holder != null) { int count = --holder.count; if (count == 0) { - muxedConnections.remove(connection); + activeConnections.remove(connection); if (!closed) { idleConnections.offerFirst(holder); @@ -208,32 +156,7 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C } } } - else - { - holder = busyConnections.remove(connection); - if (holder != null) - { - int count = --holder.count; - if (!closed) - { - if (count == 0) - { - idleConnections.offerFirst(holder); - idle = true; - } - else - { - muxedConnections.put(connection, holder); - } - } - } - } } - finally - { - unlock(); - } - if (holder == null) return false; @@ -253,16 +176,13 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C { boolean activeRemoved = true; boolean idleRemoved = false; - lock(); - try + synchronized (this) { - Holder holder = muxedConnections.remove(connection); - if (holder == null) - holder = busyConnections.remove(connection); + Holder holder = activeConnections.remove(connection); if (holder == null) { activeRemoved = false; - for (Iterator iterator = idleConnections.iterator(); iterator.hasNext();) + for (Iterator iterator = idleConnections.iterator(); iterator.hasNext(); ) { holder = iterator.next(); if (holder.connection == connection) @@ -274,11 +194,6 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C } } } - finally - { - unlock(); - } - if (activeRemoved || force) released(connection); boolean removed = activeRemoved || idleRemoved || force; @@ -291,65 +206,39 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C public void close() { super.close(); - List connections; - lock(); - try + synchronized (this) { connections = idleConnections.stream().map(holder -> holder.connection).collect(Collectors.toList()); - connections.addAll(muxedConnections.keySet()); - connections.addAll(busyConnections.keySet()); + connections.addAll(activeConnections.keySet()); } - finally - { - unlock(); - } - close(connections); } @Override public void dump(Appendable out, String indent) throws IOException { - DumpableCollection busy; - DumpableCollection muxed; + DumpableCollection active; DumpableCollection idle; - lock(); - try + synchronized (this) { - busy = new DumpableCollection("busy", new ArrayList<>(busyConnections.values())); - muxed = new DumpableCollection("muxed", new ArrayList<>(muxedConnections.values())); + active = new DumpableCollection("active", new ArrayList<>(activeConnections.values())); idle = new DumpableCollection("idle", new ArrayList<>(idleConnections)); } - finally - { - unlock(); - } - - Dumpable.dumpObjects(out, indent, this, busy, muxed, idle); + Dumpable.dumpObjects(out, indent, this, active, idle); } @Override public boolean sweep() { List toSweep = new ArrayList<>(); - lock(); - try + synchronized (this) { - busyConnections.values().stream() - .map(holder -> holder.connection) - .filter(connection -> connection instanceof Sweeper.Sweepable) - .collect(Collectors.toCollection(() -> toSweep)); - muxedConnections.values().stream() + activeConnections.values().stream() .map(holder -> holder.connection) .filter(connection -> connection instanceof Sweeper.Sweepable) .collect(Collectors.toCollection(() -> toSweep)); } - finally - { - unlock(); - } - for (Connection connection : toSweep) { if (((Sweeper.Sweepable)connection).sweep()) @@ -363,34 +252,26 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C dump()); } } - return false; } @Override public String toString() { - int busySize; - int muxedSize; + int activeSize; int idleSize; - lock(); - try + synchronized (this) { - busySize = busyConnections.size(); - muxedSize = muxedConnections.size(); + activeSize = activeConnections.size(); idleSize = idleConnections.size(); } - finally - { - unlock(); - } - return String.format("%s@%x[c=%d/%d,b=%d,m=%d,i=%d]", + return String.format("%s@%x[connections=%d/%d,multiplex=%d,active=%d,idle=%d]", getClass().getSimpleName(), hashCode(), getConnectionCount(), getMaxConnectionCount(), - busySize, - muxedSize, + getMaxMultiplex(), + activeSize, idleSize); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexHttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexHttpDestination.java index 544422c3dc9..693db851102 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexHttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexHttpDestination.java @@ -18,13 +18,33 @@ package org.eclipse.jetty.client; -public abstract class MultiplexHttpDestination extends HttpDestination +import java.util.function.Function; + +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.util.annotation.ManagedAttribute; + +/** + *

A destination for those transports that are multiplex (e.g. HTTP/2).

+ *

Transports that negotiate the protocol, and therefore do not know in advance + * whether they are duplex or multiplex, should use this class and when the + * cardinality is known call {@link #setMaxRequestsPerConnection(int)} with + * the proper cardinality.

+ *

If the cardinality is {@code 1}, the behavior of this class is similar + * to that of {@link DuplexHttpDestination}.

+ */ +public class MultiplexHttpDestination extends HttpDestination implements HttpDestination.Multiplexed { - protected MultiplexHttpDestination(HttpClient client, Origin origin) + public MultiplexHttpDestination(HttpClient client, Key key) { - super(client, origin); + this(client, key, Function.identity()); } + public MultiplexHttpDestination(HttpClient client, Key key, Function factoryFn) + { + super(client, key, factoryFn); + } + + @ManagedAttribute(value = "The maximum number of concurrent requests per connection") public int getMaxRequestsPerConnection() { ConnectionPool connectionPool = getConnectionPool(); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/Origin.java b/jetty-client/src/main/java/org/eclipse/jetty/client/Origin.java index 344b1c16edb..8c364dba568 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/Origin.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/Origin.java @@ -75,7 +75,7 @@ public class Origin @Override public String toString() { - return asString(); + return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), asString()); } public static class Address diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java index 207a0ac020c..f55ab074ae9 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ProxyAuthenticationProtocolHandler.java @@ -73,7 +73,7 @@ public class ProxyAuthenticationProtocolHandler extends AuthenticationProtocolHa @Override protected URI getAuthenticationURI(Request request) { - HttpDestination destination = getHttpClient().destinationFor(request.getScheme(), request.getHost(), request.getPort()); + HttpDestination destination = (HttpDestination)getHttpClient().getDestination(request.getScheme(), request.getHost(), request.getPort()); ProxyConfiguration.Proxy proxy = destination.getProxy(); return proxy != null ? proxy.getURI() : request.getURI(); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/TimeoutCompleteListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/TimeoutCompleteListener.java index ced60b57239..7a3121a755b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/TimeoutCompleteListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/TimeoutCompleteListener.java @@ -26,7 +26,6 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.io.CyclicTimeout; -import org.eclipse.jetty.util.component.Destroyable; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Scheduler; @@ -47,7 +46,7 @@ public class TimeoutCompleteListener extends CyclicTimeout implements Response.C { Request request = this.request.getAndSet(null); if (LOG.isDebugEnabled()) - LOG.debug("Total timeout {} ms elapsed for {}", request.getTimeout(), request); + LOG.debug("Total timeout {} ms elapsed for {} on {}", request.getTimeout(), request, this); if (request != null) request.abort(new TimeoutException("Total timeout " + request.getTimeout() + " ms elapsed")); } @@ -60,7 +59,7 @@ public class TimeoutCompleteListener extends CyclicTimeout implements Response.C { boolean cancelled = cancel(); if (LOG.isDebugEnabled()) - LOG.debug("Cancelled ({}) timeout for {}", cancelled, request); + LOG.debug("Cancelled ({}) timeout for {} on {}", cancelled, request, this); } } @@ -69,12 +68,16 @@ public class TimeoutCompleteListener extends CyclicTimeout implements Response.C if (this.request.compareAndSet(null, request)) { long delay = timeoutAt - System.nanoTime(); - if (LOG.isDebugEnabled()) - LOG.debug("Scheduled timeout in {} ms for {}", TimeUnit.NANOSECONDS.toMillis(delay), request); if (delay <= 0) + { onTimeoutExpired(); + } else + { schedule(delay, TimeUnit.NANOSECONDS); + if (LOG.isDebugEnabled()) + LOG.debug("Scheduled timeout in {} ms for {} on {}", TimeUnit.NANOSECONDS.toMillis(delay), request, this); + } } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java index 97bcf4c9014..09f35d9ec12 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -261,12 +261,14 @@ public interface Request Request idleTimeout(long timeout, TimeUnit unit); /** - * @return the total timeout for this request, in milliseconds + * @return the total timeout for this request, in milliseconds; + * zero or negative if the timeout is disabled */ long getTimeout(); /** - * @param timeout the total timeout for the request/response conversation + * @param timeout the total timeout for the request/response conversation; + * use zero or a negative value to disable the timeout * @param unit the timeout unit * @return this request object */ diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java new file mode 100644 index 00000000000..33820ecf1ff --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/dynamic/HttpClientTransportDynamic.java @@ -0,0 +1,201 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.dynamic; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.jetty.alpn.client.ALPNClientConnection; +import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory; +import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.MultiplexConnectionPool; +import org.eclipse.jetty.client.MultiplexHttpDestination; +import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; + +/** + *

A {@link HttpClientTransport} that can dynamically switch among different application protocols.

+ *

Applications create HttpClientTransportDynamic instances specifying all the application protocols + * it supports, in order of preference. The typical case is when the server supports both HTTP/1.1 and + * HTTP/2, but the client does not know that. In this case, the application will create a + * HttpClientTransportDynamic in this way:

+ *
+ * ClientConnector clientConnector = new ClientConnector();
+ * // Configure the clientConnector.
+ *
+ * // Prepare the application protocols.
+ * HttpClientConnectionFactory.Key h1 = HttpClientConnectionFactory.HTTP;
+ * HTTP2Client http2Client = new HTTP2Client(clientConnector);
+ * ClientConnectionFactory.Key h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client);
+ *
+ * // Create the HttpClientTransportDynamic, preferring h2 over h1.
+ * HttpClientTransport transport = new HttpClientTransportDynamic(clientConnector, h2, h1);
+ *
+ * // Create the HttpClient.
+ * client = new HttpClient(transport);
+ * 
+ *

Note how in the code above the HttpClientTransportDynamic has been created with the application + * protocols {@code h2} and {@code h1}, without the need to specify TLS (which is implied by the request + * scheme) or ALPN (which is implied by HTTP/2 over TLS).

+ *

When a request is first sent, a destination needs to be created, and the {@link org.eclipse.jetty.client.Origin} + * {@code (scheme, host, port)} is not enough to identify the destination because the same origin may speak + * different protocols. + * For example, the Jetty server supports speaking clear-text {@code http/1.1} and {@code h2c} on the same port. + * Imagine a client sending a {@code h2c} request to that port; this will create a destination and connections + * that speak {@code h2c}; it won't be possible to use the connections from that destination to send + * {@code http/1.1} requests. + * Therefore a destination is identified by a {@link org.eclipse.jetty.client.HttpDestination.Key} and + * applications can customize the creation of the destination key (for example depending on request protocol + * version, or request headers, or request attributes, or even request path) by overriding + * {@link #newDestinationKey(HttpRequest, Origin)}.

+ */ +public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTransport implements HttpClientTransport.Dynamic +{ + private final List factoryInfos; + private final List protocols; + + /** + * Creates a transport that speaks only HTTP/1.1. + */ + public HttpClientTransportDynamic() + { + this(new ClientConnector(), HttpClientConnectionFactory.HTTP11); + } + + /** + * Creates a transport with the given {@link ClientConnector} and the given application protocols. + * + * @param connector the ClientConnector used by this transport + * @param factoryInfos the application protocols that this transport can speak + */ + public HttpClientTransportDynamic(ClientConnector connector, ClientConnectionFactory.Info... factoryInfos) + { + super(connector); + addBean(connector); + if (factoryInfos.length == 0) + throw new IllegalArgumentException("Missing ClientConnectionFactory"); + this.factoryInfos = Arrays.asList(factoryInfos); + this.protocols = Arrays.stream(factoryInfos) + .flatMap(info -> info.getProtocols().stream()) + .distinct() + .collect(Collectors.toList()); + for (ClientConnectionFactory.Info factoryInfo : factoryInfos) + addBean(factoryInfo); + setConnectionPoolFactory(destination -> + new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination, 1)); + } + + @Override + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) + { + boolean ssl = HttpScheme.HTTPS.is(request.getScheme()); + String http2 = ssl ? "h2" : "h2c"; + List protocols = List.of(); + if (request.getVersion() == HttpVersion.HTTP_2) + { + // The application is explicitly asking for HTTP/2, so exclude HTTP/1.1. + if (this.protocols.contains(http2)) + protocols = List.of(http2); + } + else + { + // Preserve the order of protocols chosen by the application. + protocols = this.protocols.stream() + .filter(p -> p.equals("http/1.1") || p.equals(http2)) + .collect(Collectors.toList()); + } + if (protocols.isEmpty()) + return new HttpDestination.Key(origin, null); + return new HttpDestination.Key(origin, new HttpDestination.Protocol(protocols, ssl)); + } + + @Override + public HttpDestination newHttpDestination(HttpDestination.Key key) + { + return new MultiplexHttpDestination(getHttpClient(), key); + } + + @Override + public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException + { + HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); + HttpDestination.Protocol protocol = destination.getKey().getProtocol(); + ClientConnectionFactory.Info factoryInfo; + if (protocol == null) + { + // Use the default ClientConnectionFactory. + factoryInfo = factoryInfos.get(0); + } + else + { + if (destination.isSecure() && protocol.isNegotiate()) + { + factoryInfo = new ALPNClientConnectionFactory.ALPN(getClientConnector().getExecutor(), this::newNegotiatedConnection, protocol.getProtocols()); + } + else + { + factoryInfo = findClientConnectionFactoryInfo(protocol.getProtocols()) + .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for " + protocol)); + } + } + return factoryInfo.getClientConnectionFactory().newConnection(endPoint, context); + } + + protected Connection newNegotiatedConnection(EndPoint endPoint, Map context) throws IOException + { + try + { + ALPNClientConnection alpnConnection = (ALPNClientConnection)endPoint.getConnection(); + String protocol = alpnConnection.getProtocol(); + if (LOG.isDebugEnabled()) + LOG.debug("ALPN negotiated {} among {}", protocol, alpnConnection.getProtocols()); + if (protocol == null) + throw new IOException("Could not negotiate protocol among " + alpnConnection.getProtocols()); + List protocols = List.of(protocol); + Info factoryInfo = findClientConnectionFactoryInfo(protocols) + .orElseThrow(() -> new IOException("Cannot find " + ClientConnectionFactory.class.getSimpleName() + " for negotiated protocol " + protocol)); + return factoryInfo.getClientConnectionFactory().newConnection(endPoint, context); + } + catch (Throwable failure) + { + this.connectFailed(context, failure); + throw failure; + } + } + + private Optional findClientConnectionFactoryInfo(List protocols) + { + return factoryInfos.stream() + .filter(info -> info.matches(protocols)) + .findFirst(); + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientConnectionFactory.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientConnectionFactory.java new file mode 100644 index 00000000000..fca30689e9e --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientConnectionFactory.java @@ -0,0 +1,43 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.http; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.Promise; + +public class HttpClientConnectionFactory implements ClientConnectionFactory +{ + public static final Info HTTP11 = new Info(List.of("http/1.1"), new HttpClientConnectionFactory()); + + @Override + public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) + { + HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); + @SuppressWarnings("unchecked") + Promise promise = (Promise)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY); + return customize(new HttpConnectionOverHTTP(endPoint, destination, promise), context); + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java index ddfacc5f67a..da1aaf23d83 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java @@ -23,8 +23,8 @@ import java.util.Map; import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport; import org.eclipse.jetty.client.DuplexConnectionPool; +import org.eclipse.jetty.client.DuplexHttpDestination; import org.eclipse.jetty.client.HttpDestination; -import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; @@ -53,9 +53,9 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran } @Override - public HttpDestination newHttpDestination(Origin origin) + public HttpDestination newHttpDestination(HttpDestination.Key key) { - return new HttpDestinationOverHTTP(getHttpClient(), origin); + return new DuplexHttpDestination(getHttpClient(), key); } @Override diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java index 746e54ec2b0..fcc2bb2b67f 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java @@ -28,6 +28,7 @@ import java.util.concurrent.atomic.LongAdder; import org.eclipse.jetty.client.HttpConnection; import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.client.IConnection; import org.eclipse.jetty.client.SendFailure; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Request; @@ -39,7 +40,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Sweeper; -public class HttpConnectionOverHTTP extends AbstractConnection implements Connection, org.eclipse.jetty.io.Connection.UpgradeFrom, Sweeper.Sweepable +public class HttpConnectionOverHTTP extends AbstractConnection implements IConnection, org.eclipse.jetty.io.Connection.UpgradeFrom, Sweeper.Sweepable { private static final Logger LOG = Log.getLogger(HttpConnectionOverHTTP.class); @@ -71,9 +72,9 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements Connec return channel; } - public HttpDestinationOverHTTP getHttpDestination() + public HttpDestination getHttpDestination() { - return (HttpDestinationOverHTTP)delegate.getHttpDestination(); + return delegate.getHttpDestination(); } @Override @@ -116,7 +117,8 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements Connec delegate.send(request, listener); } - protected SendFailure send(HttpExchange exchange) + @Override + public SendFailure send(HttpExchange exchange) { return delegate.send(exchange); } @@ -238,7 +240,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements Connec } @Override - protected SendFailure send(HttpExchange exchange) + public SendFailure send(HttpExchange exchange) { Request request = exchange.getRequest(); normalizeRequest(request); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 81697151435..cdfbad98ec6 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -221,6 +221,13 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res return 4096; } + @Override + public boolean isHeaderCacheCaseSensitive() + { + // TODO get from configuration + return false; + } + @Override public boolean startResponse(HttpVersion version, int status, String reason) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/proxy/ProxyProtocolClientConnectionFactory.java b/jetty-client/src/main/java/org/eclipse/jetty/client/proxy/ProxyProtocolClientConnectionFactory.java new file mode 100644 index 00000000000..87350b6ef8d --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/proxy/ProxyProtocolClientConnectionFactory.java @@ -0,0 +1,140 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.proxy; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +public class ProxyProtocolClientConnectionFactory implements ClientConnectionFactory +{ + private final ClientConnectionFactory connectionFactory; + private final Supplier proxiedAddressSupplier; + + public ProxyProtocolClientConnectionFactory(ClientConnectionFactory connectionFactory, Supplier proxiedAddressSupplier) + { + this.connectionFactory = connectionFactory; + this.proxiedAddressSupplier = proxiedAddressSupplier; + } + + @Override + public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) + { + HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); + Executor executor = destination.getHttpClient().getExecutor(); + ProxyProtocolConnection connection = new ProxyProtocolConnection(endPoint, executor, context); + return customize(connection, context); + } + + private class ProxyProtocolConnection extends AbstractConnection implements Callback + { + private final Logger LOG = Log.getLogger(ProxyProtocolConnection.class); + private final Map context; + + public ProxyProtocolConnection(EndPoint endPoint, Executor executor, Map context) + { + super(endPoint, executor); + this.context = context; + } + + @Override + public void onOpen() + { + super.onOpen(); + writePROXYLine(); + } + + protected void writePROXYLine() + { + Origin.Address proxiedAddress = proxiedAddressSupplier.get(); + if (proxiedAddress == null) + { + failed(new IllegalArgumentException("Missing proxied socket address")); + return; + } + String proxiedIP = proxiedAddress.getHost(); + int proxiedPort = proxiedAddress.getPort(); + InetSocketAddress serverSocketAddress = getEndPoint().getRemoteAddress(); + InetAddress serverAddress = serverSocketAddress.getAddress(); + String serverIP = serverAddress.getHostAddress(); + int serverPort = serverSocketAddress.getPort(); + + boolean ipv6 = serverAddress instanceof Inet6Address; + String line = String.format("PROXY %s %s %s %d %d\r\n", ipv6 ? "TCP6" : "TCP4" , proxiedIP, serverIP, proxiedPort, serverPort); + if (LOG.isDebugEnabled()) + LOG.debug("Writing PROXY line: {}", line.trim()); + ByteBuffer buffer = ByteBuffer.wrap(line.getBytes(StandardCharsets.US_ASCII)); + getEndPoint().write(this, buffer); + } + + @Override + public void succeeded() + { + try + { + EndPoint endPoint = getEndPoint(); + org.eclipse.jetty.io.Connection connection = connectionFactory.newConnection(endPoint, context); + if (LOG.isDebugEnabled()) + LOG.debug("Written PROXY line, upgrading to {}", connection); + endPoint.upgrade(connection); + } + catch (Throwable x) + { + failed(x); + } + } + + @Override + public void failed(Throwable x) + { + close(); + @SuppressWarnings("unchecked") + Promise promise = (Promise)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY); + promise.failed(x); + } + + @Override + public InvocationType getInvocationType() + { + return InvocationType.NON_BLOCKING; + } + + @Override + public void onFillable() + { + } + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java index 50a2bdc344b..be22484dd0c 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.client.util; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Base64; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.AuthenticationStore; @@ -27,7 +28,6 @@ import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.Attributes; -import org.eclipse.jetty.util.B64Code; /** * Implementation of the HTTP "Basic" authentication defined in RFC 2617. @@ -91,7 +91,8 @@ public class BasicAuthentication extends AbstractAuthentication { this.uri = uri; this.header = header; - this.value = "Basic " + B64Code.encode(user + ":" + password, StandardCharsets.ISO_8859_1); + byte[] authBytes = (user + ":" + password).getBytes(StandardCharsets.ISO_8859_1); + this.value = "Basic " + Base64.getEncoder().encodeToString(authBytes); } @Override diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java index dc47b51eebd..d5c7218362f 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java @@ -24,6 +24,7 @@ import java.util.stream.Stream; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -58,7 +59,7 @@ public abstract class AbstractHttpClientServerTest serverThreads.setName("server"); server = new Server(serverThreads); } - connector = new ServerConnector(server, scenario.newSslContextFactory()); + connector = new ServerConnector(server, scenario.newServerSslContextFactory()); connector.setPort(0); server.addConnector(connector); server.setHandler(handler); @@ -67,30 +68,30 @@ public abstract class AbstractHttpClientServerTest protected void startClient(final Scenario scenario) throws Exception { - startClient(scenario, null,null); + startClient(scenario, null); } - protected void startClient(final Scenario scenario, HttpClientTransport transport, Consumer config) throws Exception + protected void startClient(final Scenario scenario, Consumer config) throws Exception { - if (transport==null) - transport = new HttpClientTransportOverHTTP(1); - + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(scenario.newClientSslContextFactory()); + HttpClientTransport transport = new HttpClientTransportOverHTTP(clientConnector); QueuedThreadPool executor = new QueuedThreadPool(); executor.setName("client"); + clientConnector.setExecutor(executor); Scheduler scheduler = new ScheduledExecutorScheduler("client-scheduler", false); - client = newHttpClient(scenario, transport); - client.setExecutor(executor); - client.setScheduler(scheduler); + clientConnector.setScheduler(scheduler); + client = newHttpClient(transport); client.setSocketAddressResolver(new SocketAddressResolver.Sync()); - if (config!=null) + if (config != null) config.accept(client); - client.start(); } - public HttpClient newHttpClient(Scenario scenario, HttpClientTransport transport) + public HttpClient newHttpClient(HttpClientTransport transport) { - return new HttpClient(transport, scenario.newSslContextFactory()); + return new HttpClient(transport); } @AfterEach @@ -113,9 +114,10 @@ public abstract class AbstractHttpClientServerTest } } - public static class ScenarioProvider implements ArgumentsProvider { + public static class ScenarioProvider implements ArgumentsProvider + { @Override - public Stream provideArguments(ExtensionContext context) throws Exception + public Stream provideArguments(ExtensionContext context) { return Stream.of( new NormalScenario(), @@ -125,9 +127,10 @@ public abstract class AbstractHttpClientServerTest } } - public static class NonSslScenarioProvider implements ArgumentsProvider { + public static class NonSslScenarioProvider implements ArgumentsProvider + { @Override - public Stream provideArguments(ExtensionContext context) throws Exception + public Stream provideArguments(ExtensionContext context) { return Stream.of( new NormalScenario() @@ -138,12 +141,27 @@ public abstract class AbstractHttpClientServerTest public interface Scenario { - default SslContextFactory newSslContextFactory() { return null; } + SslContextFactory.Client newClientSslContextFactory(); + + SslContextFactory.Server newServerSslContextFactory(); + String getScheme(); } public static class NormalScenario implements Scenario { + @Override + public SslContextFactory.Client newClientSslContextFactory() + { + return null; + } + + @Override + public SslContextFactory.Server newServerSslContextFactory() + { + return null; + } + @Override public String getScheme() { @@ -160,15 +178,27 @@ public abstract class AbstractHttpClientServerTest public static class SslScenario implements Scenario { @Override - public SslContextFactory newSslContextFactory() + public SslContextFactory.Client newClientSslContextFactory() + { + SslContextFactory.Client result = new SslContextFactory.Client(); + result.setEndpointIdentificationAlgorithm(null); + configure(result); + return result; + } + + @Override + public SslContextFactory.Server newServerSslContextFactory() + { + SslContextFactory.Server result = new SslContextFactory.Server(); + configure(result); + return result; + } + + private void configure(SslContextFactory ssl) { Path keystorePath = MavenTestingUtils.getTestResourcePath("keystore.jks"); - - SslContextFactory ssl = new SslContextFactory(); - ssl.setEndpointIdentificationAlgorithm(""); ssl.setKeyStorePath(keystorePath.toString()); ssl.setKeyStorePassword("storepwd"); - return ssl; } @Override diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java index 6e81bcf45f6..0ed5dcf2d7d 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.client; -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 java.io.IOException; import java.io.InterruptedIOException; import java.nio.ByteBuffer; @@ -36,7 +31,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; -import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.DeferredContentProvider; import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; @@ -47,6 +41,11 @@ import org.eclipse.jetty.server.handler.AbstractHandler; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +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; + public class ClientConnectionCloseTest extends AbstractHttpClientServerTest { @ParameterizedTest @@ -87,7 +86,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); ContentResponse response = client.newRequest(host, port) @@ -124,7 +123,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); CountDownLatch resultLatch = new CountDownLatch(1); @@ -185,7 +184,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); DeferredContentProvider content = new DeferredContentProvider(ByteBuffer.allocate(8)); @@ -240,7 +239,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); ContentResponse response = client.newRequest(host, port) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java index 6dd31ce1bf7..7dece557831 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.client; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -31,7 +28,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import java.util.stream.Stream; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -53,6 +49,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + @Disabled // Disabled by @gregw on issue #2540 - commit 621b946b10884e7308eacca241dcf8b5d6f6cff2 public class ConnectionPoolTest { @@ -83,7 +82,7 @@ public class ConnectionPoolTest transport.setConnectionPoolFactory(factory); server.start(); - client = new HttpClient(transport, null); + client = new HttpClient(transport); client.start(); } @@ -115,7 +114,7 @@ public class ConnectionPoolTest start(factory, new EmptyServerHandler() { @Override - protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { switch (HttpMethod.fromString(request.getMethod())) { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ExternalSiteTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ExternalSiteTest.java index 50238673069..aa72bcd3538 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ExternalSiteTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ExternalSiteTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.client; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - import java.net.Socket; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -30,12 +26,16 @@ import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.http.HttpScheme; -import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + @Disabled public class ExternalSiteTest { @@ -44,7 +44,7 @@ public class ExternalSiteTest @BeforeEach public void prepare() throws Exception { - client = new HttpClient(new SslContextFactory()); + client = new HttpClient(); client.start(); } @@ -54,6 +54,7 @@ public class ExternalSiteTest client.stop(); } + @Tag("external") @Test public void testExternalSite() throws Exception { @@ -64,39 +65,29 @@ public class ExternalSiteTest assumeCanConnectTo(host, port); final CountDownLatch latch1 = new CountDownLatch(1); - client.newRequest(host, port).send(new Response.CompleteListener() + client.newRequest(host, port).send(result -> { - @Override - public void onComplete(Result result) - { - if (!result.isFailed() && result.getResponse().getStatus() == 200) - latch1.countDown(); - } + assertTrue(result.isSucceeded()); + assertEquals(200, result.getResponse().getStatus()); + latch1.countDown(); }); - assertTrue(latch1.await(10, TimeUnit.SECONDS)); + assertTrue(latch1.await(15, TimeUnit.SECONDS)); // Try again the same URI, but without specifying the port final CountDownLatch latch2 = new CountDownLatch(1); - client.newRequest("http://" + host).send(new Response.CompleteListener() + client.newRequest("http://" + host).send(result -> { - @Override - public void onComplete(Result result) - { - assertTrue(result.isSucceeded()); - assertEquals(200, result.getResponse().getStatus()); - latch2.countDown(); - } + assertTrue(result.isSucceeded()); + assertEquals(200, result.getResponse().getStatus()); + latch2.countDown(); }); - assertTrue(latch2.await(10, TimeUnit.SECONDS)); + assertTrue(latch2.await(15, TimeUnit.SECONDS)); } + @Tag("external") @Test public void testExternalSSLSite() throws Exception { - client.stop(); - client = new HttpClient(new SslContextFactory()); - client.start(); - String host = "api-3t.paypal.com"; int port = 443; @@ -104,18 +95,16 @@ public class ExternalSiteTest assumeCanConnectTo(host, port); final CountDownLatch latch = new CountDownLatch(1); - client.newRequest(host, port).scheme("https").path("/nvp").send(new Response.CompleteListener() + client.newRequest(host, port).scheme("https").path("/nvp").send(result -> { - @Override - public void onComplete(Result result) - { - if (result.isSucceeded() && result.getResponse().getStatus() == 200) - latch.countDown(); - } + assertTrue(result.isSucceeded()); + assertEquals(200, result.getResponse().getStatus()); + latch.countDown(); }); - assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertTrue(latch.await(15, TimeUnit.SECONDS)); } + @Tag("external") @Test public void testExternalSiteWrongProtocol() throws Exception { @@ -129,14 +118,7 @@ public class ExternalSiteTest { final CountDownLatch latch = new CountDownLatch(3); client.newRequest(host, port) - .onResponseFailure(new Response.FailureListener() - { - @Override - public void onFailure(Response response, Throwable failure) - { - latch.countDown(); - } - }) + .onResponseFailure((response, failure) -> latch.countDown()) .send(new Response.Listener.Adapter() { @Override @@ -152,10 +134,11 @@ public class ExternalSiteTest latch.countDown(); } }); - assertTrue(latch.await(10, TimeUnit.SECONDS)); + assertTrue(latch.await(15, TimeUnit.SECONDS)); } } + @Tag("external") @Test public void testExternalSiteRedirect() throws Exception { @@ -168,6 +151,7 @@ public class ExternalSiteTest ContentResponse response = client.newRequest(host, port) .scheme(HttpScheme.HTTPS.asString()) .path("/twitter") + .timeout(15, TimeUnit.SECONDS) .send(); assertEquals(200, response.getStatus()); } @@ -180,7 +164,7 @@ public class ExternalSiteTest } catch (Throwable x) { - assumeTrue(x == null, "Unable to connect"); + assumeTrue(false, "Unable to connect"); } } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HostnameVerificationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HostnameVerificationTest.java index 706471023fd..c8e1d5feb3a 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HostnameVerificationTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HostnameVerificationTest.java @@ -18,19 +18,16 @@ package org.eclipse.jetty.client; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; - import java.io.IOException; import java.security.cert.CertificateException; import java.util.concurrent.ExecutionException; import javax.net.ssl.SSLHandshakeException; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; @@ -40,11 +37,14 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + /** * This test class runs tests to make sure that hostname verification (http://www.ietf.org/rfc/rfc2818.txt * section 3.1) is configurable in SslContextFactory and works as expected. @@ -52,7 +52,7 @@ import org.junit.jupiter.api.Test; @Disabled public class HostnameVerificationTest { - private SslContextFactory clientSslContextFactory = new SslContextFactory(); + private SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(); private Server server; private HttpClient client; private NetworkConnector connector; @@ -64,7 +64,7 @@ public class HostnameVerificationTest serverThreads.setName("server"); server = new Server(serverThreads); - SslContextFactory serverSslContextFactory = new SslContextFactory(); + SslContextFactory.Server serverSslContextFactory = new SslContextFactory.Server(); serverSslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); serverSslContextFactory.setKeyStorePassword("storepwd"); connector = new ServerConnector(server, serverSslContextFactory); @@ -72,7 +72,7 @@ public class HostnameVerificationTest server.setHandler(new DefaultHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); response.getWriter().write("foobar"); @@ -80,14 +80,19 @@ public class HostnameVerificationTest }); server.start(); - // keystore contains a hostname which doesn't match localhost + // The keystore contains a hostname which doesn't match localhost clientSslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); clientSslContextFactory.setKeyStorePassword("storepwd"); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(clientSslContextFactory); + QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - client = new HttpClient(clientSslContextFactory); - client.setExecutor(clientThreads); + clientConnector.setExecutor(clientThreads); + + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)); client.start(); } @@ -112,9 +117,7 @@ public class HostnameVerificationTest clientSslContextFactory.setEndpointIdentificationAlgorithm("HTTPS"); String uri = "https://localhost:" + connector.getLocalPort() + "/"; - ExecutionException x = assertThrows(ExecutionException.class, ()->{ - client.GET(uri); - }); + ExecutionException x = assertThrows(ExecutionException.class, ()-> client.GET(uri)); Throwable cause = x.getCause(); assertThat(cause, Matchers.instanceOf(SSLHandshakeException.class)); Throwable root = cause.getCause().getCause(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java index 5e3307746a2..672b6272501 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java @@ -44,9 +44,11 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response.Listener; import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.AbstractAuthentication; import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.client.util.DeferredContentProvider; import org.eclipse.jetty.client.util.DigestAuthentication; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.ConstraintMapping; @@ -57,7 +59,6 @@ import org.eclipse.jetty.security.authentication.BasicAuthenticator; import org.eclipse.jetty.security.authentication.DigestAuthenticator; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.IO; @@ -67,6 +68,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import static org.eclipse.jetty.client.api.Authentication.ANY_REALM; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -137,7 +139,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest { startBasic(scenario, new EmptyServerHandler()); URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort()); - test_Authentication(scenario, new BasicAuthentication(uri, Authentication.ANY_REALM, "basic", "basic")); + test_Authentication(scenario, new BasicAuthentication(uri, ANY_REALM, "basic", "basic")); } @ParameterizedTest @@ -155,7 +157,7 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest { startDigest(scenario, new EmptyServerHandler()); URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort()); - test_Authentication(scenario, new DigestAuthentication(uri, Authentication.ANY_REALM, "digest", "digest")); + test_Authentication(scenario, new DigestAuthentication(uri, ANY_REALM, "digest", "digest")); } private void test_Authentication(final Scenario scenario, Authentication authentication) throws Exception @@ -227,16 +229,19 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest @ArgumentsSource(ScenarioProvider.class) public void test_BasicAuthentication_ThenRedirect(Scenario scenario) throws Exception { - startBasic(scenario, new AbstractHandler() + startBasic(scenario, new EmptyServerHandler() { private final AtomicInteger requests = new AtomicInteger(); @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException + protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - baseRequest.setHandled(true); - if (requests.incrementAndGet() == 1) - response.sendRedirect(URIUtil.newURI(scenario.getScheme(), request.getServerName(), request.getServerPort(), request.getRequestURI(), null)); + int r = requests.incrementAndGet(); + if (r == 1) + { + String path = request.getRequestURI() + "/" + r; + response.sendRedirect(URIUtil.newURI(scenario.getScheme(), request.getServerName(), request.getServerPort(), path, null)); + } } }); @@ -269,12 +274,11 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest @ArgumentsSource(ScenarioProvider.class) public void test_Redirect_ThenBasicAuthentication(Scenario scenario) throws Exception { - startBasic(scenario, new AbstractHandler() + startBasic(scenario, new EmptyServerHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException + protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - baseRequest.setHandled(true); if (request.getRequestURI().endsWith("/redirect")) response.sendRedirect(URIUtil.newURI(scenario.getScheme(), request.getServerName(), request.getServerPort(), "/secure", null)); } @@ -571,61 +575,57 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest assertTrue(resultLatch.await(5, TimeUnit.SECONDS)); } - private static class GeneratingContentProvider implements ContentProvider + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void test_InfiniteAuthentication(Scenario scenario) throws Exception { - private static final ByteBuffer DONE = ByteBuffer.allocate(0); - - private final IntFunction generator; - - private GeneratingContentProvider(IntFunction generator) + String authType = "Authenticate"; + start(scenario, new EmptyServerHandler() { - this.generator = generator; - } - - @Override - public long getLength() - { - return -1; - } - - @Override - public boolean isReproducible() - { - return true; - } - - @Override - public Iterator iterator() - { - return new Iterator() + @Override + protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) { - private int index; - public ByteBuffer current; + // Always reply with a 401 to see if the client + // can handle an infinite authentication loop. + response.setStatus(HttpStatus.UNAUTHORIZED_401); + response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), authType); + } + }); - @Override - @SuppressWarnings("ReferenceEquality") - public boolean hasNext() + AuthenticationStore authenticationStore = client.getAuthenticationStore(); + URI uri = URI.create(scenario.getScheme() + "://localhost:" + connector.getLocalPort()); + authenticationStore.addAuthentication(new AbstractAuthentication(uri, Authentication.ANY_REALM) + { + @Override + public String getType() + { + return authType; + } + + @Override + public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) + { + return new Result() { - if (current == null) + @Override + public URI getURI() { - current = generator.apply(index++); - if (current == null) - current = DONE; + return uri; } - return current != DONE; - } - @Override - public ByteBuffer next() - { - ByteBuffer result = current; - current = null; - if (result == null) - throw new NoSuchElementException(); - return result; - } - }; - } + @Override + public void apply(Request request) + { + } + }; + } + }); + + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scenario.getScheme()) + .send(); + + assertEquals(HttpStatus.UNAUTHORIZED_401, response.getStatus()); } @Test @@ -801,4 +801,61 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest assertEquals("thermostat", headerInfo.getParameter("realm")); assertEquals(headerInfo.getParameter("nonce"), "1523430383="); } + + private static class GeneratingContentProvider implements ContentProvider + { + private static final ByteBuffer DONE = ByteBuffer.allocate(0); + + private final IntFunction generator; + + private GeneratingContentProvider(IntFunction generator) + { + this.generator = generator; + } + + @Override + public long getLength() + { + return -1; + } + + @Override + public boolean isReproducible() + { + return true; + } + + @Override + public Iterator iterator() + { + return new Iterator() + { + private int index; + public ByteBuffer current; + + @Override + @SuppressWarnings("ReferenceEquality") + public boolean hasNext() + { + if (current == null) + { + current = generator.apply(index++); + if (current == null) + current = DONE; + } + return current != DONE; + } + + @Override + public ByteBuffer next() + { + ByteBuffer result = current; + current = null; + if (result == null) + throw new NoSuchElementException(); + return result; + } + }; + } + } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java index a4c27d2f6ed..ee3559261f2 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.client; -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 java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -31,13 +26,17 @@ import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; -import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.FutureResponseListener; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.util.FuturePromise; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +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; + public class HttpClientExplicitConnectionTest extends AbstractHttpClientServerTest { @ParameterizedTest @@ -59,7 +58,7 @@ public class HttpClientExplicitConnectionTest extends AbstractHttpClientServerTe assertNotNull(response); assertEquals(200, response.getStatus()); - HttpDestinationOverHTTP httpDestination = (HttpDestinationOverHTTP)destination; + HttpDestination httpDestination = (HttpDestination)destination; DuplexConnectionPool connectionPool = (DuplexConnectionPool)httpDestination.getConnectionPool(); assertTrue(connectionPool.getActiveConnections().isEmpty()); assertTrue(connectionPool.getIdleConnections().isEmpty()); @@ -94,7 +93,7 @@ public class HttpClientExplicitConnectionTest extends AbstractHttpClientServerTe HttpConnectionOverHTTP httpConnection = (HttpConnectionOverHTTP)connection; assertFalse(httpConnection.getEndPoint().isOpen()); - HttpDestinationOverHTTP httpDestination = (HttpDestinationOverHTTP)destination; + HttpDestination httpDestination = (HttpDestination)destination; DuplexConnectionPool connectionPool = (DuplexConnectionPool)httpDestination.getConnectionPool(); assertTrue(connectionPool.getActiveConnections().isEmpty()); assertTrue(connectionPool.getIdleConnections().isEmpty()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientFailureTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientFailureTest.java index 5192550d743..eada0ae3a5b 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientFailureTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientFailureTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.client; -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 java.nio.ByteBuffer; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -40,9 +36,12 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpClientFailureTest { private Server server; @@ -75,7 +74,7 @@ public class HttpClientFailureTest startServer(new EmptyServerHandler()); final AtomicReference connectionRef = new AtomicReference<>(); - client = new HttpClient(new HttpClientTransportOverHTTP() + client = new HttpClient(new HttpClientTransportOverHTTP(1) { @Override protected HttpConnectionOverHTTP newHttpConnection(EndPoint endPoint, HttpDestination destination, Promise promise) @@ -84,15 +83,14 @@ public class HttpClientFailureTest connectionRef.set(connection); return connection; } - }, null); + }); client.start(); - assertThrows(ExecutionException.class, ()->{ - client.newRequest("localhost", connector.getLocalPort()) - .onRequestHeaders(request -> connectionRef.get().getEndPoint().close()) - .timeout(5, TimeUnit.SECONDS) - .send(); - }); + assertThrows(ExecutionException.class, () -> + client.newRequest("localhost", connector.getLocalPort()) + .onRequestHeaders(request -> connectionRef.get().getEndPoint().close()) + .timeout(5, TimeUnit.SECONDS) + .send()); DuplexConnectionPool connectionPool = (DuplexConnectionPool)connectionRef.get().getHttpDestination().getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); @@ -106,7 +104,7 @@ public class HttpClientFailureTest startServer(new EmptyServerHandler()); final AtomicReference connectionRef = new AtomicReference<>(); - client = new HttpClient(new HttpClientTransportOverHTTP() + client = new HttpClient(new HttpClientTransportOverHTTP(1) { @Override protected HttpConnectionOverHTTP newHttpConnection(EndPoint endPoint, HttpDestination destination, Promise promise) @@ -115,7 +113,7 @@ public class HttpClientFailureTest connectionRef.set(connection); return connection; } - }, null); + }); client.start(); final CountDownLatch commitLatch = new CountDownLatch(1); @@ -155,98 +153,4 @@ public class HttpClientFailureTest assertEquals(0, connectionPool.getActiveConnections().size()); assertEquals(0, connectionPool.getIdleConnections().size()); } -/* - @Test - public void test_ExchangeIsComplete_WhenRequestFailsMidway_WithResponse() throws Exception - { - start(new AbstractHandler() - { - @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - // Echo back - IO.copy(request.getInputStream(), response.getOutputStream()); - } - }); - - final CountDownLatch latch = new CountDownLatch(1); - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scheme) - // The second ByteBuffer set to null will throw an exception - .content(new ContentProvider() - { - @Override - public long getLength() - { - return -1; - } - - @Override - public Iterator iterator() - { - return new Iterator() - { - @Override - public boolean hasNext() - { - return true; - } - - @Override - public ByteBuffer next() - { - throw new NoSuchElementException("explicitly_thrown_by_test"); - } - - @Override - public void remove() - { - throw new UnsupportedOperationException(); - } - }; - } - }) - .send(new Response.Listener.Adapter() - { - @Override - public void onComplete(Result result) - { - latch.countDown(); - } - }); - - assertTrue(latch.await(5, TimeUnit.SECONDS)); - } - - @Test - public void test_ExchangeIsComplete_WhenRequestFails_WithNoResponse() throws Exception - { - start(new EmptyServerHandler()); - - final CountDownLatch latch = new CountDownLatch(1); - final String host = "localhost"; - final int port = connector.getLocalPort(); - client.newRequest(host, port) - .scheme(scheme) - .onRequestBegin(new Request.BeginListener() - { - @Override - public void onBegin(Request request) - { - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - destination.getConnectionPool().getActiveConnections().peek().close(); - } - }) - .send(new Response.Listener.Adapter() - { - @Override - public void onComplete(Result result) - { - latch.countDown(); - } - }); - - assertTrue(latch.await(5, TimeUnit.SECONDS)); - } -*/ } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java index f28893ca4e8..781e1414554 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java @@ -21,9 +21,9 @@ package org.eclipse.jetty.client; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -35,7 +35,6 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.util.B64Code; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -82,7 +81,7 @@ public class HttpClientProxyTest extends AbstractHttpClientServerTest { final String user = "foo"; final String password = "bar"; - final String credentials = B64Code.encode(user + ":" + password, StandardCharsets.ISO_8859_1); + final String credentials = Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.ISO_8859_1)); final String serverHost = "server"; final String realm = "test_realm"; final int status = HttpStatus.NO_CONTENT_204; @@ -162,7 +161,7 @@ public class HttpClientProxyTest extends AbstractHttpClientServerTest { String user = "foo"; String password = "bar"; - String credentials = B64Code.encode(user + ":" + password, StandardCharsets.ISO_8859_1); + String credentials = Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.ISO_8859_1)); String proxyHost = "localhost"; String serverHost = "server"; int serverPort = HttpScheme.HTTP.is(scenario.getScheme()) ? 80 : 443; diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientRedirectTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientRedirectTest.java index 9038ec52f95..8da2a30390b 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientRedirectTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientRedirectTest.java @@ -18,14 +18,6 @@ package org.eclipse.jetty.client; -import static org.hamcrest.MatcherAssert.assertThat; -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.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.IOException; import java.net.URLDecoder; import java.nio.ByteBuffer; @@ -34,7 +26,9 @@ import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -48,13 +42,20 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.IO; import org.hamcrest.Matchers; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import static org.hamcrest.MatcherAssert.assertThat; +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpClientRedirectTest extends AbstractHttpClientServerTest { @ParameterizedTest @@ -128,14 +129,13 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest { start(scenario, new RedirectHandler()); - ExecutionException x = assertThrows(ExecutionException.class, ()->{ - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .method(HttpMethod.DELETE) - .path("/301/localhost/done") - .timeout(5, TimeUnit.SECONDS) - .send(); - }); + ExecutionException x = assertThrows(ExecutionException.class, () -> + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scenario.getScheme()) + .method(HttpMethod.DELETE) + .path("/301/localhost/done") + .timeout(5, TimeUnit.SECONDS) + .send()); HttpResponseException xx = (HttpResponseException)x.getCause(); Response response = xx.getResponse(); assertNotNull(response); @@ -170,13 +170,12 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest start(scenario, new RedirectHandler()); client.setMaxRedirects(1); - ExecutionException x = assertThrows(ExecutionException.class, ()->{ - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scenario.getScheme()) - .path("/303/localhost/302/localhost/done") - .timeout(5, TimeUnit.SECONDS) - .send(); - }); + ExecutionException x = assertThrows(ExecutionException.class, () -> + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scenario.getScheme()) + .path("/303/localhost/302/localhost/done") + .timeout(5, TimeUnit.SECONDS) + .send()); HttpResponseException xx = (HttpResponseException)x.getCause(); Response response = xx.getResponse(); assertNotNull(response); @@ -269,12 +268,11 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest @ArgumentsSource(ScenarioProvider.class) public void testRedirectWithWrongScheme(Scenario scenario) throws Exception { - start(scenario, new AbstractHandler() + start(scenario, new EmptyServerHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) { - baseRequest.setHandled(true); response.setStatus(303); response.setHeader("Location", "ssh://localhost:" + connector.getLocalPort() + "/path"); } @@ -439,12 +437,11 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest public void testRedirectWithCorruptedBody(Scenario scenario) throws Exception { byte[] bytes = "ok".getBytes(StandardCharsets.UTF_8); - start(scenario, new AbstractHandler() + start(scenario, new EmptyServerHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - baseRequest.setHandled(true); if (target.startsWith("/redirect")) { response.setStatus(HttpStatus.SEE_OTHER_303); @@ -471,6 +468,60 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest assertArrayEquals(bytes, response.getContent()); } + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testRedirectToSameURL(Scenario scenario) throws Exception + { + start(scenario, new EmptyServerHandler() + { + @Override + protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) + { + response.setStatus(HttpStatus.SEE_OTHER_303); + response.setHeader(HttpHeader.LOCATION.asString(), request.getRequestURI()); + } + }); + + ExecutionException x = assertThrows(ExecutionException.class, () -> + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scenario.getScheme()) + .send()); + assertThat(x.getCause(), Matchers.instanceOf(HttpResponseException.class)); + } + + @ParameterizedTest + @ArgumentsSource(ScenarioProvider.class) + public void testInfiniteRedirectLoopMustTimeout(Scenario scenario) throws Exception + { + AtomicLong counter = new AtomicLong(); + start(scenario, new EmptyServerHandler() + { + @Override + protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) + { + try + { + Thread.sleep(200); + response.setStatus(HttpStatus.SEE_OTHER_303); + response.setHeader(HttpHeader.LOCATION.asString(), "/" + counter.getAndIncrement()); + } + catch (InterruptedException x) + { + throw new RuntimeException(x); + } + } + }); + + assertThrows(TimeoutException.class, () -> + { + client.setMaxRedirects(-1); + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scenario.getScheme()) + .timeout(1, TimeUnit.SECONDS) + .send(); + }); + } + private void testSameMethodRedirect(final Scenario scenario, final HttpMethod method, int redirectCode) throws Exception { testMethodRedirect(scenario, method, method, redirectCode); @@ -519,10 +570,10 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest assertEquals(200, response.getStatus()); } - private class RedirectHandler extends AbstractHandler + private class RedirectHandler extends EmptyServerHandler { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { try { @@ -551,10 +602,6 @@ public class HttpClientRedirectTest extends AbstractHttpClientServerTest // Echo content back IO.copy(request.getInputStream(), response.getOutputStream()); } - finally - { - baseRequest.setHandled(true); - } } } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java index 13c204f012a..c37ba125586 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java @@ -35,15 +35,18 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; import org.eclipse.jetty.io.ssl.SslHandshakeListener; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.ExecutorThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -66,7 +69,7 @@ public class HttpClientTLSTest private ServerConnector connector; private HttpClient client; - private void startServer(SslContextFactory sslContextFactory, Handler handler) throws Exception + private void startServer(SslContextFactory.Server sslContextFactory, Handler handler) throws Exception { ExecutorThreadPool serverThreads = new ExecutorThreadPool(); serverThreads.setName("server"); @@ -79,22 +82,37 @@ public class HttpClientTLSTest server.start(); } - private void startClient(SslContextFactory sslContextFactory) throws Exception + private void startClient(SslContextFactory.Client sslContextFactory) throws Exception { + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(sslContextFactory); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - client = new HttpClient(sslContextFactory); - client.setExecutor(clientThreads); + clientConnector.setExecutor(clientThreads); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)); client.start(); } - private SslContextFactory createSslContextFactory() + private SslContextFactory.Server createServerSslContextFactory() + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + configureSslContextFactory(sslContextFactory); + return sslContextFactory; + } + + private SslContextFactory.Client createClientSslContextFactory() + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + configureSslContextFactory(sslContextFactory); + sslContextFactory.setEndpointIdentificationAlgorithm(null); + return sslContextFactory; + } + + private void configureSslContextFactory(SslContextFactory sslContextFactory) { - SslContextFactory sslContextFactory = new SslContextFactory(); - sslContextFactory.setEndpointIdentificationAlgorithm(""); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); - return sslContextFactory; } @AfterEach @@ -109,7 +127,7 @@ public class HttpClientTLSTest @Test public void testNoCommonTLSProtocol() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); serverTLSFactory.setIncludeProtocols("TLSv1.3"); startServer(serverTLSFactory, new EmptyServerHandler()); @@ -123,7 +141,7 @@ public class HttpClientTLSTest } }); - SslContextFactory clientTLSFactory = createSslContextFactory(); + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); clientTLSFactory.setIncludeProtocols("TLSv1.2"); startClient(clientTLSFactory); @@ -150,7 +168,7 @@ public class HttpClientTLSTest @Test public void testNoCommonTLSCiphers() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); serverTLSFactory.setIncludeCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA"); startServer(serverTLSFactory, new EmptyServerHandler()); @@ -164,7 +182,7 @@ public class HttpClientTLSTest } }); - SslContextFactory clientTLSFactory = createSslContextFactory(); + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); clientTLSFactory.setExcludeCipherSuites(".*_SHA$"); startClient(clientTLSFactory); @@ -191,7 +209,7 @@ public class HttpClientTLSTest @Test public void testMismatchBetweenTLSProtocolAndTLSCiphersOnServer() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); // TLS 1.1 protocol, but only TLS 1.2 ciphers. serverTLSFactory.setIncludeProtocols("TLSv1.1"); serverTLSFactory.setIncludeCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); @@ -207,7 +225,7 @@ public class HttpClientTLSTest } }); - SslContextFactory clientTLSFactory = createSslContextFactory(); + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); startClient(clientTLSFactory); CountDownLatch clientLatch = new CountDownLatch(1); @@ -236,7 +254,7 @@ public class HttpClientTLSTest @Test public void testMismatchBetweenTLSProtocolAndTLSCiphersOnClient() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); startServer(serverTLSFactory, new EmptyServerHandler()); CountDownLatch serverLatch = new CountDownLatch(1); @@ -249,7 +267,7 @@ public class HttpClientTLSTest } }); - SslContextFactory clientTLSFactory = createSslContextFactory(); + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); // TLS 1.1 protocol, but only TLS 1.2 ciphers. clientTLSFactory.setIncludeProtocols("TLSv1.1"); clientTLSFactory.setIncludeCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); @@ -278,7 +296,7 @@ public class HttpClientTLSTest @Test public void testHandshakeSucceeded() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); startServer(serverTLSFactory, new EmptyServerHandler()); CountDownLatch serverLatch = new CountDownLatch(1); @@ -291,7 +309,7 @@ public class HttpClientTLSTest } }); - SslContextFactory clientTLSFactory = createSslContextFactory(); + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); startClient(clientTLSFactory); CountDownLatch clientLatch = new CountDownLatch(1); @@ -317,7 +335,7 @@ public class HttpClientTLSTest @Test public void testHandshakeSucceededWithSessionResumption() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); startServer(serverTLSFactory, new EmptyServerHandler()); AtomicReference serverSession = new AtomicReference<>(); @@ -330,7 +348,7 @@ public class HttpClientTLSTest } }); - SslContextFactory clientTLSFactory = createSslContextFactory(); + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); startClient(clientTLSFactory); AtomicReference clientSession = new AtomicReference<>(); @@ -397,10 +415,10 @@ public class HttpClientTLSTest @Test public void testClientRawCloseDoesNotInvalidateSession() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); startServer(serverTLSFactory, new EmptyServerHandler()); - SslContextFactory clientTLSFactory = createSslContextFactory(); + SslContextFactory clientTLSFactory = createClientSslContextFactory(); clientTLSFactory.start(); String host = "localhost"; @@ -452,13 +470,17 @@ public class HttpClientTLSTest @Test public void testServerRawCloseDetectedByClient() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory serverTLSFactory = createServerSslContextFactory(); serverTLSFactory.start(); try (ServerSocket server = new ServerSocket(0)) { + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - client = new HttpClient(createSslContextFactory()) + clientConnector.setExecutor(clientThreads); + clientConnector.setSslContextFactory(createClientSslContextFactory()); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)) { @Override protected ClientConnectionFactory newSslClientConnectionFactory(ClientConnectionFactory connectionFactory) @@ -468,7 +490,6 @@ public class HttpClientTLSTest return ssl; } }; - client.setExecutor(clientThreads); client.start(); CountDownLatch latch = new CountDownLatch(1); @@ -488,7 +509,7 @@ public class HttpClientTLSTest while (true) { String line = reader.readLine(); - if (line == null || line.isEmpty()) + if (StringUtil.isEmpty(line)) break; } @@ -522,10 +543,10 @@ public class HttpClientTLSTest @Test public void testHostNameVerificationFailure() throws Exception { - SslContextFactory serverTLSFactory = createSslContextFactory(); + SslContextFactory.Server serverTLSFactory = createServerSslContextFactory(); startServer(serverTLSFactory, new EmptyServerHandler()); - SslContextFactory clientTLSFactory = createSslContextFactory(); + SslContextFactory.Client clientTLSFactory = createClientSslContextFactory(); // Make sure the host name is not verified at the TLS level. clientTLSFactory.setEndpointIdentificationAlgorithm(null); // Add host name verification after the TLS handshake. diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index 9fba73442ca..2e665d669ff 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -50,7 +50,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; - import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; @@ -65,7 +64,6 @@ import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; -import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.client.util.BytesContentProvider; import org.eclipse.jetty.client.util.DeferredContentProvider; @@ -79,8 +77,10 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -111,7 +111,6 @@ public class HttpClientTest extends AbstractHttpClientServerTest { public WorkDir testdir; - @ParameterizedTest @ArgumentsSource(ScenarioProvider.class) public void testStoppingClosesConnections(Scenario scenario) throws Exception @@ -124,7 +123,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest Response response = client.GET(scenario.getScheme() + "://" + host + ":" + port + path); assertEquals(200, response.getStatus()); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); long start = System.nanoTime(); @@ -187,7 +186,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { response.getOutputStream().write(data); baseRequest.setHandled(true); @@ -212,7 +211,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { response.setCharacterEncoding("UTF-8"); ServletOutputStream output = response.getOutputStream(); @@ -245,7 +244,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { response.setCharacterEncoding("UTF-8"); ServletOutputStream output = response.getOutputStream(); @@ -282,7 +281,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); String value = request.getParameter(paramName); @@ -315,7 +314,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); String value = request.getParameter(paramName); @@ -349,7 +348,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); consume(request.getInputStream(), true); @@ -381,7 +380,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); consume(request.getInputStream(), true); @@ -412,7 +411,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); consume(request.getInputStream(), true); @@ -493,7 +492,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { if (target.endsWith("/one")) baseRequest.getHttpChannel().getEndPoint().close(); @@ -612,7 +611,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { // Echo back IO.copy(request.getInputStream(), response.getOutputStream()); @@ -634,7 +633,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest @Override public Iterator iterator() { - return new Iterator() + return new Iterator<>() { @Override public boolean hasNext() @@ -681,7 +680,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest .scheme(scenario.getScheme()) .onRequestBegin(request -> { - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); connectionPool.getActiveConnections().iterator().next().close(); }) @@ -706,7 +705,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException { try { @@ -722,13 +721,12 @@ public class HttpClientTest extends AbstractHttpClientServerTest final String host = "localhost"; final int port = connector.getLocalPort(); - assertThrows(TimeoutException.class, ()->{ - client.newRequest(host, port) - .scheme(scenario.getScheme()) - .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) - .timeout(3 * idleTimeout, TimeUnit.MILLISECONDS) - .send(); - }); + assertThrows(TimeoutException.class, () -> + client.newRequest(host, port) + .scheme(scenario.getScheme()) + .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) + .timeout(3 * idleTimeout, TimeUnit.MILLISECONDS) + .send()); // Make another request without specifying the idle timeout, should not fail ContentResponse response = client.newRequest(host, port) @@ -744,6 +742,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest @ArgumentsSource(ScenarioProvider.class) public void testSendToIPv6Address(Scenario scenario) throws Exception { + Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); start(scenario, new EmptyServerHandler()); ContentResponse response = client.newRequest("[::1]", connector.getLocalPort()) @@ -763,7 +762,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); response.setHeader(headerName, "X"); @@ -821,7 +820,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); response.getOutputStream().write(new byte[length]); @@ -882,14 +881,14 @@ public class HttpClientTest extends AbstractHttpClientServerTest public void testConnectHostWithMultipleAddresses(Scenario scenario) throws Exception { startServer(scenario, new EmptyServerHandler()); - startClient(scenario, null, client -> + startClient(scenario, client -> { client.setSocketAddressResolver(new SocketAddressResolver.Async(client.getExecutor(), client.getScheduler(), 5000) { @Override public void resolve(String host, int port, Promise> promise) { - super.resolve(host, port, new Promise>() + super.resolve(host, port, new Promise<>() { @Override public void succeeded(List result) @@ -925,7 +924,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); ArrayList userAgents = Collections.list(request.getHeaders("User-Agent")); @@ -959,7 +958,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); ArrayList userAgents = Collections.list(request.getHeaders("User-Agent")); @@ -1161,7 +1160,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); response.getOutputStream().write(content); @@ -1205,7 +1204,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); assertEquals(host, request.getServerName()); @@ -1227,7 +1226,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { // Send the headers at this point, then write the content byte[] content = "TEST".getBytes("UTF-8"); @@ -1255,7 +1254,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { // Send the headers at this point, then write the content response.flushBuffer(); @@ -1313,7 +1312,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); request.startAsync(); @@ -1344,9 +1343,8 @@ public class HttpClientTest extends AbstractHttpClientServerTest { Assumptions.assumeTrue(HttpScheme.HTTP.is(scenario.getScheme())); - ExecutionException e = assertThrows(ExecutionException.class, ()->{ - testContentDelimitedByEOFWithSlowRequest(scenario, HttpVersion.HTTP_1_0, 1024); - }); + ExecutionException e = assertThrows(ExecutionException.class, () -> + testContentDelimitedByEOFWithSlowRequest(scenario, HttpVersion.HTTP_1_0, 1024)); assertThat(e.getCause(), instanceOf(BadMessageException.class)); assertThat(e.getCause().getMessage(), containsString("Unknown content")); @@ -1356,9 +1354,8 @@ public class HttpClientTest extends AbstractHttpClientServerTest @ArgumentsSource(NonSslScenarioProvider.class) public void testBigContentDelimitedByEOFWithSlowRequestHTTP10(Scenario scenario) throws Exception { - ExecutionException e = assertThrows(ExecutionException.class, ()->{ - testContentDelimitedByEOFWithSlowRequest(scenario, HttpVersion.HTTP_1_0, 128 * 1024); - }); + ExecutionException e = assertThrows(ExecutionException.class, () -> + testContentDelimitedByEOFWithSlowRequest(scenario, HttpVersion.HTTP_1_0, 128 * 1024)); assertThat(e.getCause(), instanceOf(BadMessageException.class)); assertThat(e.getCause().getMessage(), containsString("Unknown content")); @@ -1392,7 +1389,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); // Send Connection: close to avoid that the server chunks the content with HTTP 1.1. @@ -1428,7 +1425,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { int count = requests.incrementAndGet(); if (count == maxRetries) @@ -1457,7 +1454,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); ServletOutputStream output = response.getOutputStream(); @@ -1507,14 +1504,16 @@ public class HttpClientTest extends AbstractHttpClientServerTest startServer(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); } }); final AtomicBoolean open = new AtomicBoolean(); - client = new HttpClient(new HttpClientTransportOverHTTP() + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(scenario.newClientSslContextFactory()); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector) { @Override protected HttpConnectionOverHTTP newHttpConnection(EndPoint endPoint, HttpDestination destination, Promise promise) @@ -1529,7 +1528,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest } }; } - }, scenario.newSslContextFactory()); + }); client.start(); final CountDownLatch latch = new CountDownLatch(2); @@ -1613,10 +1612,11 @@ public class HttpClientTest extends AbstractHttpClientServerTest @ArgumentsSource(ScenarioProvider.class) public void test_IPv6_Host(Scenario scenario) throws Exception { + Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); response.setContentType("text/plain"); @@ -1691,7 +1691,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); assertThat(request.getHeader("Host"), Matchers.notNullValue()); @@ -1717,7 +1717,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest try (ServerSocket server = new ServerSocket(0)) { int idleTimeout = 2000; - startClient(scenario, null, httpClient -> + startClient(scenario, httpClient -> { httpClient.setMaxConnectionsPerDestination(1); httpClient.setIdleTimeout(idleTimeout); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java index 8523d0b2fc6..0863fcc770d 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java @@ -18,13 +18,6 @@ package org.eclipse.jetty.client; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -38,7 +31,6 @@ import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -50,17 +42,28 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpClientURITest extends AbstractHttpClientServerTest { @ParameterizedTest @ArgumentsSource(ScenarioProvider.class) public void testIPv6Host(Scenario scenario) throws Exception { + Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); start(scenario, new EmptyServerHandler()); String host = "::1"; @@ -607,7 +610,7 @@ public class HttpClientURITest extends AbstractHttpClientServerTest while (true) { String line = reader.readLine(); - if (line == null || line.isEmpty()) + if (StringUtil.isEmpty(line)) break; } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java index 09c43659315..148ecd6156b 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.client; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.IOException; import java.io.InputStream; import java.util.Random; @@ -29,7 +26,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,7 +33,6 @@ import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.http.HttpChannelOverHTTP; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; -import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.BytesContentProvider; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Request; @@ -46,9 +41,11 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.thread.QueuedThreadPool; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpClientUploadDuringServerShutdown { /** @@ -67,7 +64,7 @@ public class HttpClientUploadDuringServerShutdown server.setHandler(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); byte[] buffer = new byte[1024]; @@ -105,7 +102,7 @@ public class HttpClientUploadDuringServerShutdown { QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - HttpClient client = new HttpClient(new HttpClientTransportOverHTTP(2), null); + HttpClient client = new HttpClient(new HttpClientTransportOverHTTP(2)); client.setMaxConnectionsPerDestination(2); client.setIdleTimeout(10000); client.setExecutor(clientThreads); @@ -144,7 +141,7 @@ public class HttpClientUploadDuringServerShutdown server.setHandler(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); endPointRef.set(baseRequest.getHttpChannel().getEndPoint()); @@ -212,7 +209,7 @@ public class HttpClientUploadDuringServerShutdown } }; } - }, null); + }); client.setIdleTimeout(10000); client.setExecutor(clientThreads); client.start(); @@ -252,7 +249,7 @@ public class HttpClientUploadDuringServerShutdown assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", connector.getLocalPort()); DuplexConnectionPool pool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, pool.getConnectionCount()); assertEquals(0, pool.getIdleConnections().size()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java index 9dfc0cde09d..f4b2fbdf2a1 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.client; -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 java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -39,7 +35,6 @@ import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; -import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.ByteBufferContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; @@ -51,12 +46,16 @@ import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest { @Override - public HttpClient newHttpClient(Scenario scenario, HttpClientTransport transport) + public HttpClient newHttpClient(HttpClientTransport transport) { - HttpClient client = super.newHttpClient(scenario, transport); + HttpClient client = super.newHttpClient(transport); client.setStrictEventOrdering(false); return client; } @@ -69,7 +68,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Collection idleConnections = connectionPool.getIdleConnections(); @@ -120,7 +119,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Collection idleConnections = connectionPool.getIdleConnections(); @@ -172,7 +171,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Queue idleConnections = connectionPool.getIdleConnections(); @@ -234,7 +233,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Collection idleConnections = connectionPool.getIdleConnections(); @@ -308,7 +307,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Collection idleConnections = connectionPool.getIdleConnections(); @@ -351,7 +350,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Collection idleConnections = connectionPool.getIdleConnections(); @@ -400,7 +399,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Collection idleConnections = connectionPool.getIdleConnections(); @@ -448,7 +447,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Collection idleConnections = connectionPool.getIdleConnections(); @@ -481,7 +480,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), host, port); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), host, port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Collection idleConnections = connectionPool.getIdleConnections(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java index c739d8eae07..b9c4c82f5f3 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java @@ -18,12 +18,6 @@ package org.eclipse.jetty.client; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -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 java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.CountDownLatch; @@ -39,7 +33,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Result; -import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.ByteBufferContentProvider; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.IO; @@ -47,6 +40,12 @@ import org.eclipse.jetty.util.log.StacklessLogging; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpRequestAbortTest extends AbstractHttpClientServerTest { @ParameterizedTest @@ -112,7 +111,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest assertSame(cause, x.getCause()); assertFalse(begin.get()); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -156,7 +155,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest assertSame(cause, x.getCause()); assertFalse(committed.await(1, TimeUnit.SECONDS)); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -200,7 +199,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest assertSame(cause, x.getCause()); assertFalse(committed.await(1, TimeUnit.SECONDS)); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -235,7 +234,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest if (aborted.get()) assertSame(cause, x.getCause()); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -293,7 +292,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest if (aborted.get()) assertSame(cause, x.getCause()); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -353,7 +352,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest assertTrue(serverLatch.await(5, TimeUnit.SECONDS)); - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); @@ -460,7 +459,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest assertSame(cause, x.getCause()); } - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination(scenario.getScheme(), "localhost", connector.getLocalPort()); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getActiveConnections().size()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/InsufficientThreadsDetectionTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/InsufficientThreadsDetectionTest.java index 86051d6f8d7..2d396121628 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/InsufficientThreadsDetectionTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/InsufficientThreadsDetectionTest.java @@ -30,7 +30,7 @@ public class InsufficientThreadsDetectionTest public void testInsufficientThreads() { QueuedThreadPool clientThreads = new QueuedThreadPool(1); - HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP(1), null); + HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP(1)); httpClient.setExecutor(clientThreads); assertThrows(IllegalStateException.class, httpClient::start); } @@ -39,14 +39,14 @@ public class InsufficientThreadsDetectionTest public void testInsufficientThreadsForMultipleHttpClients() throws Exception { QueuedThreadPool clientThreads = new QueuedThreadPool(3); - HttpClient httpClient1 = new HttpClient(new HttpClientTransportOverHTTP(1), null); + HttpClient httpClient1 = new HttpClient(new HttpClientTransportOverHTTP(1)); httpClient1.setExecutor(clientThreads); httpClient1.start(); assertThrows(IllegalStateException.class, () -> { // Share the same thread pool with another instance. - HttpClient httpClient2 = new HttpClient(new HttpClientTransportOverHTTP(1), null); + HttpClient httpClient2 = new HttpClient(new HttpClientTransportOverHTTP(1)); httpClient2.setExecutor(clientThreads); httpClient2.start(); }); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/LivelockTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/LivelockTest.java index 172c9fdd007..1440e975427 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/LivelockTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/LivelockTest.java @@ -93,7 +93,7 @@ public class LivelockTest int count = 5; HttpClientTransport transport = new HttpClientTransportOverHTTP(1); - client = new HttpClient(transport, null); + client = new HttpClient(transport); client.setMaxConnectionsPerDestination(2 * count); client.setMaxRequestsQueuedPerDestination(2 * count); client.setSocketAddressResolver(new SocketAddressResolver.Sync()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ProxyConfigurationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ProxyConfigurationTest.java index 2b112bcd9f9..87632761d7b 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ProxyConfigurationTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ProxyConfigurationTest.java @@ -18,12 +18,13 @@ package org.eclipse.jetty.client; +import org.eclipse.jetty.toolchain.test.Net; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - public class ProxyConfigurationTest { @Test @@ -70,6 +71,7 @@ public class ProxyConfigurationTest @Test public void testProxyMatchesWithIncludesAndExcludesIPv6() throws Exception { + Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); HttpProxy proxy = new HttpProxy("host", 0); proxy.getIncludedAddresses().add("[1::2:3:4]"); proxy.getExcludedAddresses().add("[1::2:3:4]:5"); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java index c47a69db097..2869e72b293 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.client; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -30,14 +28,14 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; -import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.FutureResponseListener; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class ServerConnectionCloseTest { private HttpClient client; @@ -46,7 +44,7 @@ public class ServerConnectionCloseTest { QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - client = new HttpClient(new HttpClientTransportOverHTTP(1), null); + client = new HttpClient(new HttpClientTransportOverHTTP(1)); client.setExecutor(clientThreads); client.start(); } @@ -149,7 +147,7 @@ public class ServerConnectionCloseTest Thread.sleep(1000); // Connection should have been removed from pool. - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", port); + HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getIdleConnectionCount()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java index cdd6603dca1..9568b69eb28 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.client; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -33,30 +31,37 @@ import javax.net.ssl.SSLSocket; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; -import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.FutureResponseListener; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class TLSServerConnectionCloseTest { private HttpClient client; private void startClient() throws Exception { - SslContextFactory sslContextFactory = new SslContextFactory(); - sslContextFactory.setEndpointIdentificationAlgorithm(""); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + sslContextFactory.setEndpointIdentificationAlgorithm(null); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); + clientConnector.setSslContextFactory(sslContextFactory); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - client = new HttpClient(new HttpClientTransportOverHTTP(1), sslContextFactory); - client.setExecutor(clientThreads); + clientConnector.setExecutor(clientThreads); + + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)); client.start(); } @@ -168,7 +173,7 @@ public class TLSServerConnectionCloseTest Thread.sleep(1000); // Connection should have been removed from pool. - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", port); + HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", port); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); assertEquals(0, connectionPool.getConnectionCount()); assertEquals(0, connectionPool.getIdleConnectionCount()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ValidatingConnectionPoolTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ValidatingConnectionPoolTest.java index 27b12989c41..8df67a63849 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ValidatingConnectionPoolTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ValidatingConnectionPoolTest.java @@ -42,13 +42,13 @@ import org.junit.jupiter.params.provider.ArgumentsSource; public class ValidatingConnectionPoolTest extends AbstractHttpClientServerTest { @Override - public HttpClient newHttpClient(Scenario scenario, HttpClientTransport transport) + public HttpClient newHttpClient(HttpClientTransport transport) { long timeout = 1000; transport.setConnectionPoolFactory(destination -> new ValidatingConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination, destination.getHttpClient().getScheduler(), timeout)); - return super.newHttpClient(scenario, transport); + return super.newHttpClient(transport); } @ParameterizedTest diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java index 7dff8d953a7..d2ecbe95d57 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java @@ -26,8 +26,10 @@ import java.util.function.Supplier; import org.eclipse.jetty.client.AbstractHttpClientServerTest; import org.eclipse.jetty.client.ConnectionPool; import org.eclipse.jetty.client.DuplexConnectionPool; +import org.eclipse.jetty.client.DuplexHttpDestination; import org.eclipse.jetty.client.EmptyServerHandler; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.ContentResponse; @@ -56,7 +58,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest { start(scenario, new EmptyServerHandler()); - try(HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()))) + try(HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()))) { destination.start(); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); @@ -76,7 +78,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest { start(scenario, new EmptyServerHandler()); - try(HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()))) + try(HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()))) { destination.start(); @@ -102,7 +104,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest final CountDownLatch idleLatch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1); - HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort())) + HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort())) { @Override protected ConnectionPool newConnectionPool(HttpClient client) @@ -126,30 +128,29 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest }; } }; - { - destination.start(); - DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - Connection connection1 = connectionPool.acquire(); - // Make sure we entered idleCreated(). - assertTrue(idleLatch.await(5, TimeUnit.SECONDS)); + destination.start(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + Connection connection1 = connectionPool.acquire(); - // There are no available existing connections, so acquire() - // returns null because we delayed idleCreated() above - assertNull(connection1); + // Make sure we entered idleCreated(). + assertTrue(idleLatch.await(5, TimeUnit.SECONDS)); - // Second attempt also returns null because we delayed idleCreated() above. - Connection connection2 = connectionPool.acquire(); - assertNull(connection2); + // There are no available existing connections, so acquire() + // returns null because we delayed idleCreated() above + assertNull(connection1); - latch.countDown(); + // Second attempt also returns null because we delayed idleCreated() above. + Connection connection2 = connectionPool.acquire(); + assertNull(connection2); - // There must be 2 idle connections. - Connection connection = pollIdleConnection(connectionPool, 5, TimeUnit.SECONDS); - assertNotNull(connection); - connection = pollIdleConnection(connectionPool, 5, TimeUnit.SECONDS); - assertNotNull(connection); - } + latch.countDown(); + + // There must be 2 idle connections. + Connection connection = pollIdleConnection(connectionPool, 5, TimeUnit.SECONDS); + assertNotNull(connection); + connection = pollIdleConnection(connectionPool, 5, TimeUnit.SECONDS); + assertNotNull(connection); } @ParameterizedTest @@ -158,7 +159,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest { start(scenario, new EmptyServerHandler()); - try(HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()))) + try(HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()))) { destination.start(); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); @@ -185,9 +186,9 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest { startServer(scenario, new EmptyServerHandler()); long idleTimeout = 1000; - startClient(scenario, null, httpClient -> httpClient.setIdleTimeout(idleTimeout)); + startClient(scenario, httpClient -> httpClient.setIdleTimeout(idleTimeout)); - try (HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort()))) + try(HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()))) { destination.start(); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java index c70cc68181d..739e1bde451 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java @@ -18,15 +18,6 @@ package org.eclipse.jetty.client.http; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.jupiter.api.Assertions.assertEquals; -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 java.io.EOFException; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -35,15 +26,17 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Stream; +import org.eclipse.jetty.client.DuplexHttpDestination; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpResponseException; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.util.FutureResponseListener; -import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; @@ -54,10 +47,19 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +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; + public class HttpReceiverOverHTTPTest { private HttpClient client; - private HttpDestinationOverHTTP destination; + private HttpDestination destination; private ByteArrayEndPoint endPoint; private HttpConnectionOverHTTP connection; @@ -77,7 +79,7 @@ public class HttpReceiverOverHTTPTest client = new HttpClient(); client.setHttpCompliance(compliance); client.start(); - destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination = new DuplexHttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); endPoint = new ByteArrayEndPoint(); connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<>()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java index 5130cfd23eb..0e79a5820af 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.client.http; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -28,7 +25,9 @@ import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.client.DuplexHttpDestination; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Request; @@ -39,11 +38,13 @@ import org.eclipse.jetty.io.ByteArrayEndPoint; import org.eclipse.jetty.util.Promise; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpSenderOverHTTPTest { private HttpClient client; @@ -65,7 +66,7 @@ public class HttpSenderOverHTTPTest public void test_Send_NoRequestContent() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); - HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -99,7 +100,7 @@ public class HttpSenderOverHTTPTest public void test_Send_NoRequestContent_IncompleteFlush() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16); - HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -129,7 +130,7 @@ public class HttpSenderOverHTTPTest ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); // Shutdown output to trigger the exception on write endPoint.shutdownOutput(); - HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -159,7 +160,7 @@ public class HttpSenderOverHTTPTest public void test_Send_NoRequestContent_IncompleteFlush_Exception() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16); - HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -195,7 +196,7 @@ public class HttpSenderOverHTTPTest public void test_Send_SmallRequestContent_InOneBuffer() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); - HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -230,7 +231,7 @@ public class HttpSenderOverHTTPTest public void test_Send_SmallRequestContent_InTwoBuffers() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); - HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); @@ -266,7 +267,7 @@ public class HttpSenderOverHTTPTest public void test_Send_SmallRequestContent_Chunked_InTwoChunks() throws Exception { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); - HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", 8080)); destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter()); Request request = client.newRequest(URI.create("http://localhost/")); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java index 59fd76f42cd..ab32beb6b9f 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/NeedWantClientAuthTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.client.ssl; -import static org.hamcrest.MatcherAssert.assertThat; -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 java.security.cert.Certificate; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -34,7 +29,9 @@ import javax.net.ssl.SSLSession; import org.eclipse.jetty.client.EmptyServerHandler; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.ssl.SslHandshakeListener; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -43,9 +40,13 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; - import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * In order to work, client authentication needs a certificate * signed by a CA that also signed the server certificate. @@ -59,7 +60,7 @@ public class NeedWantClientAuthTest private ServerConnector connector; private HttpClient client; - private void startServer(SslContextFactory sslContextFactory, Handler handler) throws Exception + private void startServer(SslContextFactory.Server sslContextFactory, Handler handler) throws Exception { QueuedThreadPool serverThreads = new QueuedThreadPool(); serverThreads.setName("server"); @@ -72,19 +73,21 @@ public class NeedWantClientAuthTest server.start(); } - private void startClient(SslContextFactory sslContextFactory) throws Exception + private void startClient(SslContextFactory.Client sslContextFactory) throws Exception { + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - client = new HttpClient(sslContextFactory); - client.setExecutor(clientThreads); + clientConnector.setExecutor(clientThreads); + clientConnector.setSslContextFactory(sslContextFactory); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)); client.start(); } - private SslContextFactory createSslContextFactory() + private SslContextFactory.Server createServerSslContextFactory() { - SslContextFactory sslContextFactory = new SslContextFactory(); - sslContextFactory.setEndpointIdentificationAlgorithm(""); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); return sslContextFactory; @@ -102,11 +105,11 @@ public class NeedWantClientAuthTest @Test public void testWantClientAuthWithoutAuth() throws Exception { - SslContextFactory serverSSL = createSslContextFactory(); + SslContextFactory.Server serverSSL = createServerSslContextFactory(); serverSSL.setWantClientAuth(true); startServer(serverSSL, new EmptyServerHandler()); - SslContextFactory clientSSL = new SslContextFactory(true); + SslContextFactory.Client clientSSL = new SslContextFactory.Client(true); startClient(clientSSL); ContentResponse response = client.newRequest("https://localhost:" + connector.getLocalPort()) @@ -119,7 +122,7 @@ public class NeedWantClientAuthTest @Test public void testWantClientAuthWithAuth() throws Exception { - SslContextFactory serverSSL = createSslContextFactory(); + SslContextFactory.Server serverSSL = createServerSslContextFactory(); serverSSL.setWantClientAuth(true); startServer(serverSSL, new EmptyServerHandler()); CountDownLatch handshakeLatch = new CountDownLatch(1); @@ -143,7 +146,7 @@ public class NeedWantClientAuthTest } }); - SslContextFactory clientSSL = new SslContextFactory(true); + SslContextFactory.Client clientSSL = new SslContextFactory.Client(true); clientSSL.setKeyStorePath("src/test/resources/client_keystore.jks"); clientSSL.setKeyStorePassword("storepwd"); startClient(clientSSL); @@ -166,11 +169,11 @@ public class NeedWantClientAuthTest // The server still sends bad_certificate to the client, but the client handshake has already // completed successfully its TLS handshake. - SslContextFactory serverSSL = createSslContextFactory(); + SslContextFactory.Server serverSSL = createServerSslContextFactory(); serverSSL.setNeedClientAuth(true); startServer(serverSSL, new EmptyServerHandler()); - SslContextFactory clientSSL = new SslContextFactory(true); + SslContextFactory.Client clientSSL = new SslContextFactory.Client(true); startClient(clientSSL); CountDownLatch handshakeLatch = new CountDownLatch(1); client.addBean(new SslHandshakeListener() @@ -210,7 +213,7 @@ public class NeedWantClientAuthTest @Test public void testNeedClientAuthWithAuth() throws Exception { - SslContextFactory serverSSL = createSslContextFactory(); + SslContextFactory.Server serverSSL = createServerSslContextFactory(); serverSSL.setNeedClientAuth(true); startServer(serverSSL, new EmptyServerHandler()); CountDownLatch handshakeLatch = new CountDownLatch(1); @@ -234,7 +237,7 @@ public class NeedWantClientAuthTest } }); - SslContextFactory clientSSL = new SslContextFactory(true); + SslContextFactory.Client clientSSL = new SslContextFactory.Client(true); clientSSL.setKeyStorePath("src/test/resources/client_keystore.jks"); clientSSL.setKeyStorePassword("storepwd"); startClient(clientSSL); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java index 836c935c002..9bcdedbd375 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesClientTest.java @@ -38,9 +38,11 @@ import javax.net.ssl.SSLSocket; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.util.FutureResponseListener; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.AfterEach; @@ -61,7 +63,7 @@ public class SslBytesClientTest extends SslBytesTest { private ExecutorService threadPool; private HttpClient client; - private SslContextFactory sslContextFactory; + private SslContextFactory.Client sslContextFactory; private SSLServerSocket acceptor; private SimpleProxy proxy; @@ -70,8 +72,11 @@ public class SslBytesClientTest extends SslBytesTest { threadPool = Executors.newCachedThreadPool(); - sslContextFactory = new SslContextFactory(true); - client = new HttpClient(sslContextFactory); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + sslContextFactory = new SslContextFactory.Client(true); + clientConnector.setSslContextFactory(sslContextFactory); + client = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)); client.setMaxConnectionsPerDestination(1); File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks"); sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath()); @@ -243,7 +248,7 @@ public class SslBytesClientTest extends SslBytesTest // Trigger a read to have the server write the final renegotiation steps server.setSoTimeout(100); - assertThrows(SocketTimeoutException.class, ()->serverInput.read()); + assertThrows(SocketTimeoutException.class, () -> serverInput.read()); // Renegotiation Handshake record = proxy.readFromServer(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java index 1b16f72befd..874718cb8c9 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java @@ -100,7 +100,7 @@ public class SslBytesServerTest extends SslBytesTest private final int idleTimeout = 2000; private ExecutorService threadPool; private Server server; - private SslContextFactory sslContextFactory; + private SslContextFactory.Server sslContextFactory; private int serverPort; private SSLContext sslContext; private SimpleProxy proxy; @@ -119,7 +119,7 @@ public class SslBytesServerTest extends SslBytesTest serverEndPoint.set(null); File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks"); - sslContextFactory = new SslContextFactory(); + sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslConnectionTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslConnectionTest.java index cb657274128..c0952cebf60 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslConnectionTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslConnectionTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.client.ssl; -import static org.junit.jupiter.api.Assertions.assertThrows; - import java.io.File; import java.nio.ByteBuffer; @@ -36,16 +34,17 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class SslConnectionTest { @Test public void testSslConnectionClosedBeforeFill() throws Exception { File keyStore = MavenTestingUtils.getTestResourceFile("keystore.jks"); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keyStore.getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.start(); diff --git a/jetty-deploy/pom.xml b/jetty-deploy/pom.xml index ef6e71f0326..0bc5232e344 100644 --- a/jetty-deploy/pom.xml +++ b/jetty-deploy/pom.xml @@ -23,7 +23,7 @@ @{argLine} ${jetty.surefire.argLine} --add-modules org.eclipse.jetty.jmx - --add-reads org.eclipse.jetty.deploy=org.eclipse.jetty.http + --add-reads org.eclipse.jetty.deploy=org.eclipse.jetty.http,jetty.servlet.api diff --git a/jetty-deploy/src/main/config/etc/jetty-deploy.xml b/jetty-deploy/src/main/config/etc/jetty-deploy.xml index 5372d29b2cd..ebbd9d3cb75 100644 --- a/jetty-deploy/src/main/config/etc/jetty-deploy.xml +++ b/jetty-deploy/src/main/config/etc/jetty-deploy.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-deploy/src/main/config/modules/global-webapp-common.d/global-webapp-common.xml b/jetty-deploy/src/main/config/modules/global-webapp-common.d/global-webapp-common.xml index aa3d308bbf7..3eec9436c37 100644 --- a/jetty-deploy/src/main/config/modules/global-webapp-common.d/global-webapp-common.xml +++ b/jetty-deploy/src/main/config/modules/global-webapp-common.d/global-webapp-common.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-deploy/src/main/config/modules/global-webapp-common.d/webapp-common.xml b/jetty-deploy/src/main/config/modules/global-webapp-common.d/webapp-common.xml index 3a056f63569..958378ffc5e 100644 --- a/jetty-deploy/src/main/config/modules/global-webapp-common.d/webapp-common.xml +++ b/jetty-deploy/src/main/config/modules/global-webapp-common.d/webapp-common.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/App.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/App.java index ab66563d054..365f5e8ecf5 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/App.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/App.java @@ -69,7 +69,6 @@ public class App _context = context; } - /* ------------------------------------------------------------ */ /** * @return The deployment manager */ @@ -78,7 +77,6 @@ public class App return _manager; } - /* ------------------------------------------------------------ */ /** * @return The AppProvider */ @@ -87,7 +85,6 @@ public class App return _provider; } - /* ------------------------------------------------------------ */ /** * Get ContextHandler for the App. * @@ -149,7 +146,6 @@ public class App return this._context.getContextPath(); } - /** * The origin of this {@link App} as specified by the {@link AppProvider} * diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/AppProvider.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/AppProvider.java index 8d7fd9adf08..1daa58c5a2c 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/AppProvider.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/AppProvider.java @@ -36,8 +36,7 @@ public interface AppProvider extends LifeCycle * if the provider {@link #isRunning()}. */ void setDeploymentManager(DeploymentManager deploymentManager); - - /* ------------------------------------------------------------ */ + /** Create a ContextHandler for an App * @param app The App * @return A ContextHandler diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/DeploymentManager.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/DeploymentManager.java index bed2c5b93d1..e15eccaf561 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/DeploymentManager.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/DeploymentManager.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.deploy.graph.Path; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.util.AttributesMap; +import org.eclipse.jetty.util.MultiException; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedOperation; @@ -68,6 +69,7 @@ import org.eclipse.jetty.xml.XmlConfiguration; public class DeploymentManager extends ContainerLifeCycle { private static final Logger LOG = Log.getLogger(DeploymentManager.class); + private MultiException onStartupErrors; /** * Represents a single tracked app within the deployment manager. @@ -152,7 +154,6 @@ public class DeploymentManager extends ContainerLifeCycle } } - /* ------------------------------------------------------------ */ /** Set the AppProviders. * The providers passed are added via {@link #addBean(Object)} so that * their lifecycles may be managed as a {@link ContainerLifeCycle}. @@ -170,7 +171,6 @@ public class DeploymentManager extends ContainerLifeCycle addBean(provider); } - @ManagedAttribute("Application Providers") public Collection getAppProviders() { return Collections.unmodifiableList(_providers); @@ -181,7 +181,7 @@ public class DeploymentManager extends ContainerLifeCycle if (isRunning()) throw new IllegalStateException(); _providers.add(provider); - addBean(provider); + addBean(provider); } public void setLifeCycleBindings(Collection bindings) @@ -239,6 +239,12 @@ public class DeploymentManager extends ContainerLifeCycle { startAppProvider(provider); } + + if (onStartupErrors != null) + { + onStartupErrors.ifExceptionThrow(); + } + super.doStart(); } @@ -292,7 +298,6 @@ public class DeploymentManager extends ContainerLifeCycle return Collections.unmodifiableCollection(_apps); } - @ManagedAttribute("Deployed Apps") public Collection getApps() { List ret = new ArrayList< >(); @@ -522,9 +527,23 @@ public class DeploymentManager extends ContainerLifeCycle // The runBindings failed for 'failed' node LOG.ignore(ignore); } + + if (isStarting()) + { + addOnStartupError(t); + } } } + private synchronized void addOnStartupError(Throwable cause) + { + if(onStartupErrors == null) + { + onStartupErrors = new MultiException(); + } + onStartupErrors.add(cause); + } + /** * Move an {@link App} through the {@link AppLifeCycle} to the desired {@link Node}, executing each lifecycle step * in the process to reach the desired state. diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/DebugListenerBinding.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/DebugListenerBinding.java index 93065927492..2ffc02aa9e7 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/DebugListenerBinding.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/DebugListenerBinding.java @@ -22,7 +22,6 @@ import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.graph.Node; import org.eclipse.jetty.server.DebugListener; - /** A Deployment binding that installs a DebugListener in all deployed contexts */ public class DebugListenerBinding extends DebugBinding diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/GlobalWebappConfigBinding.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/GlobalWebappConfigBinding.java index cf8e53cb52c..5b80468eba2 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/GlobalWebappConfigBinding.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/GlobalWebappConfigBinding.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.deploy.bindings; -import java.io.File; - import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.AppLifeCycle; import org.eclipse.jetty.deploy.graph.Node; @@ -49,7 +47,6 @@ public class GlobalWebappConfigBinding implements AppLifeCycle.Binding { private static final Logger LOG = Log.getLogger(GlobalWebappConfigBinding.class); - private String _jettyXml; public String getJettyXml() @@ -95,7 +92,7 @@ public class GlobalWebappConfigBinding implements AppLifeCycle.Binding if (globalContextSettings.exists()) { - XmlConfiguration jettyXmlConfig = new XmlConfiguration(globalContextSettings.getInputStream()); + XmlConfiguration jettyXmlConfig = new XmlConfiguration(globalContextSettings); Resource resource = Resource.newResource(app.getOriginId()); app.getDeploymentManager().scope(jettyXmlConfig,resource); WebAppClassLoader.runWithServerClassAccess(()->{jettyXmlConfig.configure(context);return null;}); diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/StandardDeployer.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/StandardDeployer.java index dd58166c7be..5fa840a794a 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/StandardDeployer.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/StandardDeployer.java @@ -22,6 +22,7 @@ import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.AppLifeCycle; import org.eclipse.jetty.deploy.graph.Node; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.Callback; public class StandardDeployer implements AppLifeCycle.Binding { @@ -37,9 +38,10 @@ public class StandardDeployer implements AppLifeCycle.Binding { ContextHandler handler = app.getContextHandler(); if (handler == null) - { throw new NullPointerException("No Handler created for App: " + app); - } - app.getDeploymentManager().getContexts().addHandler(handler); + + Callback.Completable blocker = new Callback.Completable(); + app.getDeploymentManager().getContexts().deployHandler(handler, blocker); + blocker.get(); } } diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/StandardUndeployer.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/StandardUndeployer.java index ad5b0377799..6502381cbe0 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/StandardUndeployer.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/bindings/StandardUndeployer.java @@ -21,17 +21,12 @@ package org.eclipse.jetty.deploy.bindings; import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.AppLifeCycle; import org.eclipse.jetty.deploy.graph.Node; -import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.Callback; public class StandardUndeployer implements AppLifeCycle.Binding { - private static final Logger LOG = Log.getLogger(StandardUndeployer.class); - @Override public String[] getBindingTargets() { @@ -42,33 +37,11 @@ public class StandardUndeployer implements AppLifeCycle.Binding @Override public void processBinding(Node node, App app) throws Exception { - ContextHandler handler = app.getContextHandler(); - ContextHandlerCollection chcoll = app.getDeploymentManager().getContexts(); - - recursiveRemoveContext(chcoll,handler); - } - - private void recursiveRemoveContext(HandlerCollection coll, ContextHandler context) - { - Handler children[] = coll.getHandlers(); - int originalCount = children.length; - - for (int i = 0, n = children.length; i < n; i++) - { - Handler child = children[i]; - LOG.debug("Child handler {}",child); - if (child.equals(context)) - { - LOG.debug("Removing handler {}",child); - coll.removeHandler(child); - child.destroy(); - if (LOG.isDebugEnabled()) - LOG.debug("After removal: {} (originally {})",coll.getHandlers().length,originalCount); - } - else if (child instanceof HandlerCollection) - { - recursiveRemoveContext((HandlerCollection)child,context); - } - } + ContextHandlerCollection contexts = app.getDeploymentManager().getContexts(); + ContextHandler context = app.getContextHandler(); + Callback.Completable blocker = new Callback.Completable(); + contexts.undeployHandler(context, blocker); + blocker.get(); + context.destroy(); } } diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/graph/Graph.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/graph/Graph.java index ec2bb0a0c51..bfa30f68ef1 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/graph/Graph.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/graph/Graph.java @@ -203,7 +203,6 @@ public class Graph Path path = breadthFirst(from,to,new CopyOnWriteArrayList(),new HashSet()); return path; } - private Path breadthFirst(Node from, Node destination, CopyOnWriteArrayList paths, Set seen) { @@ -246,7 +245,6 @@ public class Graph return null; } - public Set getEdges() { return _edges; diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/jmx/DeploymentManagerMBean.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/jmx/DeploymentManagerMBean.java index af8fe6db6fa..fa532548737 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/jmx/DeploymentManagerMBean.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/jmx/DeploymentManagerMBean.java @@ -24,11 +24,11 @@ import java.util.List; import java.util.stream.Collectors; import org.eclipse.jetty.deploy.App; -import org.eclipse.jetty.deploy.AppProvider; import org.eclipse.jetty.deploy.DeploymentManager; import org.eclipse.jetty.deploy.graph.Node; import org.eclipse.jetty.jmx.ObjectMBean; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedOperation; import org.eclipse.jetty.util.annotation.Name; @@ -45,7 +45,7 @@ public class DeploymentManagerMBean extends ObjectMBean _manager = (DeploymentManager) managedObject; } - @ManagedOperation(value = "list apps being tracked", impact = "INFO") + @ManagedAttribute(value = "list apps being tracked") public Collection getApps() { List ret = new ArrayList<>(); @@ -95,9 +95,10 @@ public class DeploymentManagerMBean extends ObjectMBean return apps; } - public Collection getAppProviders() + @ManagedAttribute("Registered AppProviders") + public List getAppProviders() { - return _manager.getAppProviders(); + return _manager.getAppProviders().stream().map(String::valueOf).collect(Collectors.toList()); } public void requestAppGoal(String appId, String nodeName) diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java index 3ce17135cc7..85e39a3d9f3 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.AppProvider; @@ -34,6 +35,7 @@ import org.eclipse.jetty.deploy.DeploymentManager; import org.eclipse.jetty.util.Scanner; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.annotation.ManagedOperation; import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -55,7 +57,6 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A private int _scanInterval = 10; private Scanner _scanner; - /* ------------------------------------------------------------ */ private final Scanner.DiscreteListener _scannerListener = new Scanner.DiscreteListener() { @Override @@ -77,26 +78,22 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A } }; - /* ------------------------------------------------------------ */ protected ScanningAppProvider() { } - - /* ------------------------------------------------------------ */ + protected ScanningAppProvider(FilenameFilter filter) { _filenameFilter = filter; } - /* ------------------------------------------------------------ */ protected void setFilenameFilter(FilenameFilter filter) { if (isRunning()) throw new IllegalStateException(); _filenameFilter = filter; } - - /* ------------------------------------------------------------ */ + /** * @return The index of currently deployed applications. */ @@ -105,7 +102,6 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A return _appMap; } - /* ------------------------------------------------------------ */ /** * Called by the Scanner.DiscreteListener to create a new App object. * Isolated in a method so that it is possible to override the default App @@ -121,7 +117,6 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A return new App(_deploymentManager,this,filename); } - /* ------------------------------------------------------------ */ @Override protected void doStart() throws Exception { @@ -150,7 +145,6 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A _scanner.start(); } - /* ------------------------------------------------------------ */ @Override protected void doStop() throws Exception { @@ -161,14 +155,12 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A _scanner = null; } } - - /* ------------------------------------------------------------ */ + protected boolean exists(String path) { return _scanner.exists(path); } - /* ------------------------------------------------------------ */ protected void fileAdded(String filename) throws Exception { if (LOG.isDebugEnabled()) @@ -181,7 +173,6 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A } } - /* ------------------------------------------------------------ */ protected void fileChanged(String filename) throws Exception { if (LOG.isDebugEnabled()) @@ -198,8 +189,7 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A _deploymentManager.addApp(app); } } - - /* ------------------------------------------------------------ */ + protected void fileRemoved(String filename) throws Exception { if (LOG.isDebugEnabled()) @@ -208,8 +198,7 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A if (app != null) _deploymentManager.removeApp(app); } - - /* ------------------------------------------------------------ */ + /** * Get the deploymentManager. * @@ -220,8 +209,6 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A return _deploymentManager; } - - /* ------------------------------------------------------------ */ public Resource getMonitoredDirResource() { if (_monitored.size()==0) @@ -231,60 +218,51 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A return _monitored.get(0); } - /* ------------------------------------------------------------ */ public String getMonitoredDirName() { Resource resource=getMonitoredDirResource(); return resource==null?null:resource.toString(); } - /* ------------------------------------------------------------ */ @ManagedAttribute("scanning interval to detect changes which need reloaded") public int getScanInterval() { return _scanInterval; } - /* ------------------------------------------------------------ */ @ManagedAttribute("recursive scanning supported") public boolean isRecursive() { return _recursive; } - /* ------------------------------------------------------------ */ @Override public void setDeploymentManager(DeploymentManager deploymentManager) { _deploymentManager = deploymentManager; } - - /* ------------------------------------------------------------ */ + public void setMonitoredResources(List resources) { _monitored.clear(); _monitored.addAll(resources); } - - /* ------------------------------------------------------------ */ + public List getMonitoredResources() { return Collections.unmodifiableList(_monitored); } - - /* ------------------------------------------------------------ */ + public void setMonitoredDirResource(Resource resource) { setMonitoredResources(Collections.singletonList(resource)); } - /* ------------------------------------------------------------ */ public void addScannerListener(Scanner.Listener listener) { _scanner.addListener(listener); } - - /* ------------------------------------------------------------ */ + /** * @param dir * Directory to scan for context descriptors or war files @@ -294,7 +272,6 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A setMonitoredDirectories(Collections.singletonList(dir)); } - /* ------------------------------------------------------------ */ public void setMonitoredDirectories(Collection directories) { try @@ -309,16 +286,24 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A throw new IllegalArgumentException(e); } } - - /* ------------------------------------------------------------ */ + protected void setRecursive(boolean recursive) { _recursive = recursive; } - /* ------------------------------------------------------------ */ public void setScanInterval(int scanInterval) { _scanInterval = scanInterval; } + + @ManagedOperation(value = "Scan the monitored directories", impact = "ACTION") + public void scan() + { + LOG.info("Performing scan of monitored directories: {}", + getMonitoredResources().stream().map((r) -> r.getURI().toASCIIString()) + .collect(Collectors.joining(", ", "[", "]")) + ); + _scanner.scan(); + } } diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java index b0f38166443..7805d2d6bad 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java @@ -124,7 +124,6 @@ public class WebAppProvider extends ScanningAppProvider } } - /* ------------------------------------------------------------ */ public WebAppProvider() { super(); @@ -132,7 +131,6 @@ public class WebAppProvider extends ScanningAppProvider setScanInterval(0); } - /* ------------------------------------------------------------ */ /** Get the extractWars. * @return the extractWars */ @@ -142,7 +140,6 @@ public class WebAppProvider extends ScanningAppProvider return _extractWars; } - /* ------------------------------------------------------------ */ /** Set the extractWars. * @param extractWars the extractWars to set */ @@ -151,7 +148,6 @@ public class WebAppProvider extends ScanningAppProvider _extractWars = extractWars; } - /* ------------------------------------------------------------ */ /** Get the parentLoaderPriority. * @return the parentLoaderPriority */ @@ -161,7 +157,6 @@ public class WebAppProvider extends ScanningAppProvider return _parentLoaderPriority; } - /* ------------------------------------------------------------ */ /** Set the parentLoaderPriority. * @param parentLoaderPriority the parentLoaderPriority to set */ @@ -169,8 +164,7 @@ public class WebAppProvider extends ScanningAppProvider { _parentLoaderPriority = parentLoaderPriority; } - - /* ------------------------------------------------------------ */ + /** Get the defaultsDescriptor. * @return the defaultsDescriptor */ @@ -180,7 +174,6 @@ public class WebAppProvider extends ScanningAppProvider return _defaultsDescriptor; } - /* ------------------------------------------------------------ */ /** Set the defaultsDescriptor. * @param defaultsDescriptor the defaultsDescriptor to set */ @@ -189,13 +182,11 @@ public class WebAppProvider extends ScanningAppProvider _defaultsDescriptor = defaultsDescriptor; } - /* ------------------------------------------------------------ */ public ConfigurationManager getConfigurationManager() { return _configurationManager; } - - /* ------------------------------------------------------------ */ + /** Set the configurationManager. * @param configurationManager the configurationManager to set */ @@ -203,8 +194,7 @@ public class WebAppProvider extends ScanningAppProvider { _configurationManager = configurationManager; } - - /* ------------------------------------------------------------ */ + /** * @param configurations The configuration class names. */ @@ -212,8 +202,7 @@ public class WebAppProvider extends ScanningAppProvider { _configurationClasses = configurations==null?null:(String[])configurations.clone(); } - - /* ------------------------------------------------------------ */ + @ManagedAttribute("configuration classes for webapps to be processed through") public String[] getConfigurationClasses() { @@ -231,8 +220,7 @@ public class WebAppProvider extends ScanningAppProvider { _tempDirectory = directory; } - - /* ------------------------------------------------------------ */ + /** * Get the user supplied Work Directory. * @@ -244,7 +232,6 @@ public class WebAppProvider extends ScanningAppProvider return _tempDirectory; } - /* ------------------------------------------------------------ */ protected void initializeWebAppContextDefaults(WebAppContext webapp) { if (_defaultsDescriptor != null) @@ -265,8 +252,7 @@ public class WebAppProvider extends ScanningAppProvider webapp.setAttribute(WebAppContext.BASETEMPDIR, _tempDirectory); } } - - /* ------------------------------------------------------------ */ + @Override public ContextHandler createContextHandler(final App app) throws Exception { @@ -279,7 +265,7 @@ public class WebAppProvider extends ScanningAppProvider if (resource.exists() && FileID.isXmlFile(file)) { - XmlConfiguration xmlc = new XmlConfiguration(resource.getURI().toURL()) + XmlConfiguration xmlc = new XmlConfiguration(resource) { @Override public void initializeDefaults(Object context) @@ -349,8 +335,7 @@ public class WebAppProvider extends ScanningAppProvider return webAppContext; } - - /* ------------------------------------------------------------ */ + @Override protected void fileChanged(String filename) throws Exception { @@ -410,7 +395,6 @@ public class WebAppProvider extends ScanningAppProvider super.fileChanged(filename); } - /* ------------------------------------------------------------ */ @Override protected void fileAdded(String filename) throws Exception { @@ -433,7 +417,6 @@ public class WebAppProvider extends ScanningAppProvider return; } - //is the file that was added a .war file? String lowname = file.getName().toLowerCase(Locale.ENGLISH); if (lowname.endsWith(".war")) @@ -453,8 +436,6 @@ public class WebAppProvider extends ScanningAppProvider super.fileAdded(filename); } - - /* ------------------------------------------------------------ */ @Override protected void fileRemoved(String filename) throws Exception { diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/jmx/WebAppProviderMBean.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/jmx/WebAppProviderMBean.java new file mode 100644 index 00000000000..c771c2b64af --- /dev/null +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/jmx/WebAppProviderMBean.java @@ -0,0 +1,44 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.deploy.providers.jmx; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jetty.deploy.providers.WebAppProvider; +import org.eclipse.jetty.jmx.ObjectMBean; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; + +@ManagedObject("WebAppProvider mbean wrapper") +public class WebAppProviderMBean extends ObjectMBean +{ + public WebAppProviderMBean(Object managedObject) + { + super(managedObject); + } + + @ManagedAttribute("List of monitored resources") + public List getMonitoredResources() + { + return ((WebAppProvider) _managed).getMonitoredResources().stream() + .map((r) -> r.getURI().toASCIIString()) + .collect(Collectors.toList()); + } +} diff --git a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/BadAppDeployTest.java b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/BadAppDeployTest.java new file mode 100644 index 00000000000..132b5cbe5f1 --- /dev/null +++ b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/BadAppDeployTest.java @@ -0,0 +1,174 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.deploy; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import javax.servlet.ServletException; + +import org.eclipse.jetty.deploy.providers.WebAppProvider; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.server.handler.DefaultHandler; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.StacklessLogging; +import org.eclipse.jetty.webapp.WebAppContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static java.time.Duration.ofSeconds; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(WorkDirExtension.class) +public class BadAppDeployTest +{ + public WorkDir workDir; + private Server server; + + @AfterEach + public void stopServer() throws Exception + { + if (server != null) + { + server.stop(); + } + } + + @Test + public void testBadApp_ThrowOnUnavailableTrue_XmlOrder() throws Exception + { + /* Non-working Bean Order as reported in Issue #3620 + It is important that this Order be maintained for an accurate test case. + ### BEAN: QueuedThreadPool[qtp1327763628]@4f2410ac{STOPPED,8<=0<=200,i=0,r=-1,q=0}[NO_TRY] + ### BEAN: ServerConnector@16f65612{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} + ### BEAN: HandlerCollection@5f150435{STOPPED} + ### BEAN: DeploymentManager@1c53fd30{STOPPED} + */ + + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(0); + server.addConnector(connector); + + ContextHandlerCollection contexts = new ContextHandlerCollection(); + HandlerCollection handlers = new HandlerCollection(); + handlers.addHandler(contexts); + handlers.addHandler(new DefaultHandler()); + server.setHandler(handlers); // this should be done before addBean(deploymentManager) + + DeploymentManager deploymentManager = new DeploymentManager(); + deploymentManager.setContexts(contexts); + WebAppProvider webAppProvider = new WebAppProvider(); + deploymentManager.addAppProvider(webAppProvider); + + Path webappsDir = workDir.getEmptyPathDir().resolve("webapps").toAbsolutePath(); + + FS.ensureDirExists(webappsDir); + + copyTestResource("webapps/badapp/badapp.war", webappsDir.resolve("badapp.war")); + copyTestResource("webapps/badapp/badapp.xml", webappsDir.resolve("badapp.xml")); + + webAppProvider.setMonitoredDirName(webappsDir.toString()); + webAppProvider.setScanInterval(1); + + server.addBean(deploymentManager); // this should be done after setHandler(handlers) + + assertTimeoutPreemptively(ofSeconds(10), () -> { + + try (StacklessLogging ignore = new StacklessLogging(Log.getLogger(WebAppContext.class), + Log.getLogger(DeploymentManager.class), + Log.getLogger("org.eclipse.jetty.server.handler.ContextHandler.badapp"))) + { + ServletException cause = assertThrows(ServletException.class, () -> server.start()); + assertThat(cause.getMessage(), containsString("intentionally")); + assertTrue(server.isFailed(), "Server should be in failed state"); + } + }); + } + + @Test + public void testBadApp_ThrowOnUnavailableTrue_EmbeddedOrder() throws Exception + { + /* Working Bean Order + ### BEAN: QueuedThreadPool[qtp1530388690]@5b37e0d2{STOPPED,8<=0<=200,i=0,r=-1,q=0}[NO_TRY] + ### BEAN: ServerConnector@5e265ba4{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} + ### BEAN: DeploymentManager@3419866c{STOPPED} + ### BEAN: HandlerCollection@63e31ee{STOPPED} + */ + + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(0); + server.addConnector(connector); + + ContextHandlerCollection contexts = new ContextHandlerCollection(); + + DeploymentManager deploymentManager = new DeploymentManager(); + deploymentManager.setContexts(contexts); + WebAppProvider webAppProvider = new WebAppProvider(); + deploymentManager.addAppProvider(webAppProvider); + + Path webappsDir = workDir.getEmptyPathDir().resolve("webapps").toAbsolutePath(); + + FS.ensureDirExists(webappsDir); + + copyTestResource("webapps/badapp/badapp.war", webappsDir.resolve("badapp.war")); + copyTestResource("webapps/badapp/badapp.xml", webappsDir.resolve("badapp.xml")); + + webAppProvider.setMonitoredDirName(webappsDir.toString()); + webAppProvider.setScanInterval(1); + + server.addBean(deploymentManager); // this should be done before setHandler(handlers) + + HandlerCollection handlers = new HandlerCollection(); + handlers.addHandler(contexts); + handlers.addHandler(new DefaultHandler()); + server.setHandler(handlers); // this should be done after addBean(deploymentManager) + + assertTimeoutPreemptively(ofSeconds(10), () -> { + + try (StacklessLogging ignore = new StacklessLogging(Log.getLogger(WebAppContext.class), + Log.getLogger(DeploymentManager.class), + Log.getLogger("org.eclipse.jetty.server.handler.ContextHandler.badapp"))) + { + ServletException cause = assertThrows(ServletException.class, () -> server.start()); + assertThat(cause.getMessage(), containsString("intentionally")); + assertTrue(server.isFailed(), "Server should be in failed state"); + } + }); + } + + private void copyTestResource(String testResourceFile, Path webappsFile) throws IOException + { + Path srcFile = MavenTestingUtils.getTestResourcePathFile(testResourceFile); + Files.copy(srcFile, webappsFile); + } +} diff --git a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/JmxServiceConnection.java b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/JmxServiceConnection.java index 98daaa738c4..d112ed0f834 100644 --- a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/JmxServiceConnection.java +++ b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/JmxServiceConnection.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.deploy; import java.io.IOException; import java.lang.management.ManagementFactory; - import javax.management.MBeanServer; import javax.management.MBeanServerConnection; import javax.management.remote.JMXConnector; @@ -74,7 +73,6 @@ public class JmxServiceConnection return serviceUrl; } - /* ------------------------------------------------------------ */ /** * Retrieve a connection to MBean server * diff --git a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/bindings/GlobalWebappConfigBindingTest.java b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/bindings/GlobalWebappConfigBindingTest.java index eb97633305d..3e385dfdb7f 100644 --- a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/bindings/GlobalWebappConfigBindingTest.java +++ b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/bindings/GlobalWebappConfigBindingTest.java @@ -18,13 +18,6 @@ package org.eclipse.jetty.deploy.bindings; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.isIn; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.assertNotNull; - import java.io.File; import java.util.List; @@ -41,6 +34,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; + /** * Tests {@link ScanningAppProvider} as it starts up for the first time. */ @@ -92,7 +92,7 @@ public class GlobalWebappConfigBindingTest String currentClasses[] = context.getServerClasses(); String addedClass = "org.eclipse.foo."; // What was added by the binding - assertThat("Current Server Classes",addedClass,isIn(currentClasses)); + assertThat("Current Server Classes",addedClass,is(in(currentClasses))); // boolean jndiPackage = false; diff --git a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/test/XmlConfiguredJetty.java b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/test/XmlConfiguredJetty.java index db579f09088..e4f582a6046 100644 --- a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/test/XmlConfiguredJetty.java +++ b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/test/XmlConfiguredJetty.java @@ -18,13 +18,6 @@ package org.eclipse.jetty.deploy.test; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -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 java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -53,17 +46,25 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.PathAssert; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.xml.XmlConfiguration; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * Allows for setting up a Jetty server for testing based on XML configuration files. */ public class XmlConfiguredJetty { - private List _xmlConfigurations; + private List _xmlConfigurations; private Map _properties = new HashMap<>(); private Server _server; private int _serverPort; @@ -137,9 +138,9 @@ public class XmlConfiguredJetty setProperty(String.valueOf(key),String.valueOf(properties.get(key))); } - public void addConfiguration(File xmlConfigFile) throws MalformedURLException + public void addConfiguration(File xmlConfigFile) { - addConfiguration(Resource.toURL(xmlConfigFile)); + addConfiguration(new PathResource(xmlConfigFile)); } public void addConfiguration(String testConfigName) throws MalformedURLException @@ -147,7 +148,7 @@ public class XmlConfiguredJetty addConfiguration(MavenTestingUtils.getTestResourceFile(testConfigName)); } - public void addConfiguration(URL xmlConfig) + public void addConfiguration(Resource xmlConfig) { _xmlConfigurations.add(xmlConfig); } @@ -330,8 +331,8 @@ public class XmlConfiguredJetty // Configure everything for (int i = 0; i < this._xmlConfigurations.size(); i++) { - URL configURL = this._xmlConfigurations.get(i); - XmlConfiguration configuration = new XmlConfiguration(configURL); + Resource configResource = this._xmlConfigurations.get(i); + XmlConfiguration configuration = new XmlConfiguration(configResource); if (last != null) configuration.getIdMap().putAll(last.getIdMap()); configuration.getProperties().putAll(_properties); diff --git a/jetty-deploy/src/test/resources/binding-test-contexts-1.xml b/jetty-deploy/src/test/resources/binding-test-contexts-1.xml index 7464c7c9dc4..6714b3b2316 100644 --- a/jetty-deploy/src/test/resources/binding-test-contexts-1.xml +++ b/jetty-deploy/src/test/resources/binding-test-contexts-1.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-deploy/src/test/resources/jetty-deploy-wars.xml b/jetty-deploy/src/test/resources/jetty-deploy-wars.xml index 1760a674f51..50c192bdb07 100644 --- a/jetty-deploy/src/test/resources/jetty-deploy-wars.xml +++ b/jetty-deploy/src/test/resources/jetty-deploy-wars.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-deploy/src/test/resources/jetty-deploymgr-contexts.xml b/jetty-deploy/src/test/resources/jetty-deploymgr-contexts.xml index c5d3cd156d1..bde43d34299 100644 --- a/jetty-deploy/src/test/resources/jetty-deploymgr-contexts.xml +++ b/jetty-deploy/src/test/resources/jetty-deploymgr-contexts.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-deploy/src/test/resources/jetty-http.xml b/jetty-deploy/src/test/resources/jetty-http.xml index 7fa6def071a..9526b56890a 100644 --- a/jetty-deploy/src/test/resources/jetty-http.xml +++ b/jetty-deploy/src/test/resources/jetty-http.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-deploy/src/test/resources/jetty.xml b/jetty-deploy/src/test/resources/jetty.xml index 00fcd1a9329..35810226482 100644 --- a/jetty-deploy/src/test/resources/jetty.xml +++ b/jetty-deploy/src/test/resources/jetty.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-deploy/src/test/resources/webapps/badapp/badapp.war b/jetty-deploy/src/test/resources/webapps/badapp/badapp.war new file mode 100644 index 00000000000..3fc1a60d5fe Binary files /dev/null and b/jetty-deploy/src/test/resources/webapps/badapp/badapp.war differ diff --git a/jetty-deploy/src/test/resources/webapps/badapp/badapp.xml b/jetty-deploy/src/test/resources/webapps/badapp/badapp.xml new file mode 100644 index 00000000000..952aab492d1 --- /dev/null +++ b/jetty-deploy/src/test/resources/webapps/badapp/badapp.xml @@ -0,0 +1,8 @@ + + + + + /badapp + /badapp.war + true + diff --git a/jetty-deploy/src/test/resources/webapps/foo.xml b/jetty-deploy/src/test/resources/webapps/foo.xml index f0a96119a3f..8b600c3bdc6 100644 --- a/jetty-deploy/src/test/resources/webapps/foo.xml +++ b/jetty-deploy/src/test/resources/webapps/foo.xml @@ -1,5 +1,5 @@ - + /foo diff --git a/jetty-distribution/pom.xml b/jetty-distribution/pom.xml index e8b40e2d90c..7952ef35fd6 100644 --- a/jetty-distribution/pom.xml +++ b/jetty-distribution/pom.xml @@ -350,7 +350,8 @@ jetty.home=${assembly-directory} jetty.base=${assembly-directory}/demo-base - --add-to-startd=server,deploy,websocket,ext,resources,client,annotations,jndi,servlets,jsp,jstl,http,https,test-keystore + --create-startd + --add-to-start=server,requestlog,deploy,websocket,ext,resources,client,annotations,jndi,servlets,jsp,jstl,http,https,demo @@ -430,6 +431,27 @@ war true
+ + org.eclipse.jetty.tests + test-jaas-webapp + ${project.version} + war + true + + + org.eclipse.jetty.tests + test-jndi-webapp + ${project.version} + war + true + + + org.eclipse.jetty.tests + test-spec-webapp + ${project.version} + war + true + org.eclipse.jetty jetty-documentation diff --git a/jetty-distribution/src/main/resources/demo-base/webapps/example-moved.xml b/jetty-distribution/src/main/resources/demo-base/webapps/example-moved.xml index e9ad12443bc..d2532f2aa49 100644 --- a/jetty-distribution/src/main/resources/demo-base/webapps/example-moved.xml +++ b/jetty-distribution/src/main/resources/demo-base/webapps/example-moved.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-documentation/src/main/asciidoc/administration/extras/moved-context-handler.adoc b/jetty-documentation/src/main/asciidoc/administration/extras/moved-context-handler.adoc index da5bf832b7e..76252848f88 100644 --- a/jetty-documentation/src/main/asciidoc/administration/extras/moved-context-handler.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/extras/moved-context-handler.adoc @@ -48,7 +48,7 @@ This is a permanent redirection, which also preserves `pathinfo` and query strin [source, xml, subs="{sub-order}"] ---- - + /foo diff --git a/jetty-documentation/src/main/asciidoc/administration/fastcgi/configuring-fastcgi.adoc b/jetty-documentation/src/main/asciidoc/administration/fastcgi/configuring-fastcgi.adoc index 3a7a47cd1a5..7409d9b91e3 100644 --- a/jetty-documentation/src/main/asciidoc/administration/fastcgi/configuring-fastcgi.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/fastcgi/configuring-fastcgi.adoc @@ -49,7 +49,7 @@ Copy and paste the following content as `$JETTY_BASE/webapps/jetty-wordpress.xml [source, xml, subs="{sub-order}"] ---- - + diff --git a/jetty-documentation/src/main/asciidoc/administration/jmx/using-jmx.adoc b/jetty-documentation/src/main/asciidoc/administration/jmx/using-jmx.adoc index e650ecb0f8b..3a6756935a9 100644 --- a/jetty-documentation/src/main/asciidoc/administration/jmx/using-jmx.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/jmx/using-jmx.adoc @@ -290,8 +290,8 @@ Similarly, in code: [source, java, subs="{sub-order}"] ---- -SslContextFactory sslContextFactory = new SslContextFactory(); -sslContextFactory.setKeyStorePath(); +SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); +sslContextFactory.setKeyStorePath("/path/to/keystore"); sslContextFactory.setKeyStorePassword("secret"); JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1099, "/jndi/rmi:///jmxrmi"); diff --git a/jetty-documentation/src/main/asciidoc/administration/logging/example-logback-centralized-logging.adoc b/jetty-documentation/src/main/asciidoc/administration/logging/example-logback-centralized-logging.adoc index 1598d03eb75..648fe58c827 100644 --- a/jetty-documentation/src/main/asciidoc/administration/logging/example-logback-centralized-logging.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/logging/example-logback-centralized-logging.adoc @@ -32,7 +32,8 @@ ____ This configuration is essentially the multiple logger configuration with added configuration to the deployers to force a `WebAppClassLoader` change to use the server classpath over the webapps classpath for the logger specific classes. -The technique used by this configuration is to provide an link:{JDURL}org/eclipse/jetty/deploy/AppLifeCycle.Binding.html[AppLifeCycle.Binding] against the link:{JDURL}/org/eclipse/jetty/deploy/AppLifeCycle.html[`"deploying"`node] that modifies the link:{JDURL}/org/eclipse/jetty/webapp/WebAppContext.html#addSystemClass(java.lang.String)[WebAppContext.addSystemClass(String)] for the common logging classes. +The technique used by this configuration is to provide an link:{JDURL}org/eclipse/jetty/deploy/AppLifeCycle.Binding.html[AppLifeCycle.Binding] against the link:{JDURL}/org/eclipse/jetty/deploy/AppLifeCycle.html[`"deploying"`node] that modifies the +link:{JDURL}/org/eclipse/jetty/webapp/WebAppContext.html#getSystemClasspathPattern()[WebAppContext.getSystemClasspathPattern().add(String)] for the common logging classes. See https://github.com/jetty-project/jetty-webapp-logging/blob/master/src/main/java/org/eclipse/jetty/webapp/logging/CentralizedWebAppLoggingBinding.java[org.eclipse.jetty.logging.CentralizedWebAppLoggingBinding] for actual implementation. A convenient replacement `logging` module has been created to bootstrap your `${jetty.base}` directory for capturing all Jetty server logging from multiple logging frameworks into a single logging output file managed by Logback. diff --git a/jetty-documentation/src/main/asciidoc/administration/sessions/legacy/session-clustering-infinispan.adoc b/jetty-documentation/src/main/asciidoc/administration/sessions/legacy/session-clustering-infinispan.adoc index 02c1e49ca3c..3e9fe70d94f 100644 --- a/jetty-documentation/src/main/asciidoc/administration/sessions/legacy/session-clustering-infinispan.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/sessions/legacy/session-clustering-infinispan.adoc @@ -94,9 +94,11 @@ From a context xml file, you reference the Server instance as a Ref: [source, xml, subs="{sub-order}"] ---- - - -org.eclipse.jetty.session.infinispan. - + + + -org.eclipse.jetty.session.infinispan. + + @@ -132,10 +134,12 @@ From a `WEB-INF/jetty-web.xml` file, you can reference the Server instance direc [source, xml, subs="{sub-order}"] ---- - - + + + -org.eclipse.jetty.session.infinispan. + @@ -143,25 +147,25 @@ From a `WEB-INF/jetty-web.xml` file, you can reference the Server instance direc - - - + + + - - - - - - - - - - 600 - - + + + + + + + + + + 600 + + ---- @@ -178,7 +182,7 @@ staleIntervalSec:: ===== Using HotRod If you're using the hotrod client - where serialization will be required - you will need to ensure that the hotrod marshalling software works with Jetty classloading. -To do this, firstly ensure that you have included the lines containing the `prependServerClass` to your context xml file as shown above. +To do this, firstly ensure that you have included the lines containing the `getServerClasspathPattern().add(...)` to your context xml file as shown above. Then, create the file `${jetty.base}/resources/hotrod-client.properties`. Add the following line to this file: diff --git a/jetty-documentation/src/main/asciidoc/administration/sessions/session-configuration-hazelcast.adoc b/jetty-documentation/src/main/asciidoc/administration/sessions/session-configuration-hazelcast.adoc index 11eaf71790e..ba8bfd985e5 100644 --- a/jetty-documentation/src/main/asciidoc/administration/sessions/session-configuration-hazelcast.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/sessions/session-configuration-hazelcast.adoc @@ -84,6 +84,7 @@ Opening the `start.d/session-store-hazelcast-remote.ini` will show a list of all #jetty.session.hazelcast.mapName=jetty_sessions #jetty.session.hazelcast.onlyClient=true #jetty.session.hazelcast.configurationLocation= +jetty.session.hazelcast.scavengeZombies=false #jetty.session.gracePeriod.seconds=3600 #jetty.session.savePeriod.seconds=0 ---- @@ -94,6 +95,8 @@ jetty.session.hazelcast.onlyClient:: Hazelcast instance will be configured in client mode jetty.session.hazelcast.configurationLocation:: Path to an an Hazelcast xml configuration file +jetty.session.hazelcast.scavengeZombies:: +True/False. `False` by default. If `true`, jetty will use hazelcast queries to find sessions that are no longer being used on any jetty node and whose expiry time has passed. If you enable this option, and your session stores attributes that reference classes from inside your webapp, or jetty classes, you will need to ensure that these classes are available on each of your hazelcast instances. jetty.session.gracePeriod.seconds:: Amount of time, in seconds, to wait for other nodes to be checked to verify an expired session is in fact expired throughout the cluster before closing it. jetty.session.savePeriod.seconds=0:: @@ -106,6 +109,8 @@ Configuring `savePeriod` is useful if your persistence technology is very slow/c In a clustered environment, there is a risk of the last access time of the session being out-of-date in the shared store for up to `savePeriod` seconds. This allows the possibility that a node may prematurely expire the session, even though it is in use by another node. Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - there is no point in setting a `savePeriod` that is larger than the `maxIdleTime`. + +Be aware using the `scavengeZombies` option that if your session attributes contain classes from inside your webapp (or jetty classes) then you will need to put these classes onto the classpath of all of your hazelcast instances. ____ ==== Configuring Embedded Hazelcast Clustering @@ -165,15 +170,18 @@ Opening the `start.d/start.d/session-store-hazelcast-embedded.ini` will show a l #jetty.session.hazelcast.mapName=jetty_sessions #jetty.session.hazelcast.configurationLocation= +jetty.session.hazelcast.scavengeZombies=false #jetty.session.gracePeriod.seconds=3600 #jetty.session.savePeriod.seconds=0 ---- jetty.session.hazelcast.mapName:: Name of the Map in Hazelcast where sessions will be stored. -jetty.session.gracePeriod.seconds:: -Amount of time, in seconds, to wait for other nodes to be checked to verify an expired session is in fact expired throughout the cluster before closing it. jetty.session.hazelcast.configurationLocation:: Path to an an Hazelcast xml configuration file +jetty.session.hazelcast.scavengeZombies:: +True/False. `False` by default. If `true`, jetty will use hazelcast queries to find sessions that are no longer being used on any jetty node and whose expiry time has passed. If you enable this option, and your sessions contain attributes that reference classes from inside your webapp (or jetty classes) you will need to ensure that these classes are available on each of your hazelcast instances. +jetty.session.gracePeriod.seconds:: +Amount of time, in seconds, to wait for other nodes to be checked to verify an expired session is in fact expired throughout the cluster before closing it. jetty.session.savePeriod.seconds=0:: By default whenever the last concurrent request leaves a session, that session is always persisted via the `SessionDataStore`, even if the only thing that changed on the session is its updated last access time. A non-zero value means that the `SessionDataStore` will skip persisting the session if only the access time changed, and it has been less than `savePeriod` seconds since the last time the session was written. @@ -184,4 +192,6 @@ Configuring `savePeriod` is useful if your persistence technology is very slow/c In a clustered environment, there is a risk of the last access time of the session being out-of-date in the shared store for up to `savePeriod` seconds. This allows the possibility that a node may prematurely expire the session, even though it is in use by another node. Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - there is no point in setting a `savePeriod` that is larger than the `maxIdleTime`. + +Be aware using the `scavengeZombies` option that if your session attributes contain classes from inside your webapp (or jetty classes) then you will need to put these classes onto the classpath of all of your hazelcast instances. In the cast of embedded hazelcast, as it is started before your webapp, it will NOT have access to your webapp's classes - you will need to extract these classes and put them onto the jetty server's classpath. ____ diff --git a/jetty-documentation/src/main/asciidoc/administration/sessions/session-configuration-infinispan.adoc b/jetty-documentation/src/main/asciidoc/administration/sessions/session-configuration-infinispan.adoc index 31b0e1f35fa..18e617b1849 100644 --- a/jetty-documentation/src/main/asciidoc/administration/sessions/session-configuration-infinispan.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/sessions/session-configuration-infinispan.adoc @@ -24,10 +24,6 @@ When using the Jetty distribution, you will first need to enable the `session-store-infinispan-remote` link:#startup-modules[module] for your link:#startup-base-and-home[Jetty base] using the `--add-to-start` argument on the command line. -____ -[IMPORTANT] -If you are running Jetty with JDK 9 or greater, enable `session-store-infinispan-remote-910.mod` instead. -____ [source, screen, subs="{sub-order}"] ---- @@ -52,7 +48,7 @@ INFO : server transitively enabled, ini template available with --add- INFO : sessions transitively enabled, ini template available with --add-to-start=sessions INFO : session-store-infinispan-remote initialized in ${jetty.base}/start.d/session-store-infinispan-remote.ini MKDIR : ${jetty.base}/lib/infinispan -DOWNLD: https://repo1.maven.org/maven2/org/infinispan/infinispan-remote/7.1.1.Final/infinispan-remote-7.1.1.Final.jar to ${jetty.base}/lib/infinispan/infinispan-remote-7.1.1.Final.jar +DOWNLD: https://repo1.maven.org/maven2/org/infinispan/infinispan-remote-it/9.4.8.Final/infinispan-remote-it-9.4.8.Final.jar to ${jetty.base}/lib/infinispan/infinispan-remote-it-9.4.8.Final.jar MKDIR : ${jetty.base}/resources COPY : ${jetty.home}/modules/session-store-infinispan-remote/resources/hotrod-client.properties to ${jetty.base}/resources/hotrod-client.properties INFO : Base directory was modified @@ -93,13 +89,17 @@ Opening the `start.d/session-store-infinispan-remote.ini` will show a list of al jetty.session.infinispan.remoteCacheName:: Name of the cache in Infinispan where sessions will be stored. jetty.session.infinispan.idleTimeout.seconds:: -Amount of time, in seconds, that the system allows the connector to remain idle before closing the connection. +Amount of time, in seconds, that a session entry in infinispan can be idle (ie not read or written) before infinispan will delete its entry. +Usually, you do *not* want to set a value for this, as you want jetty to handle all session expiration (and call any SessionListeners). +However, if there is the possibility that sessions can be left in infinispan but no longer referenced by any jetty node (so called "zombie" or "orphan" sessions), then you might want to use this feature. +You should make sure that the number of seconds you specify is sufficiently large to avoid the situation where a session is still being referenced by jetty, but is rarely accessed and thus deleted by infinispan. +Alternatively, you can enable the `infinispan-remote-query` module, which will allow jetty to search the infinispan session cache to proactively find and properly (ie calling any SessionListeners) scavenge defunct sessions. jetty.session.gracePeriod.seconds:: Amount of time, in seconds, to wait for other nodes to be checked to verify an expired session is in fact expired throughout the cluster before closing it. jetty.session.savePeriod.seconds=0:: By default whenever the last concurrent request leaves a session, that session is always persisted via the `SessionDataStore`, even if the only thing that changed on the session is its updated last access time. A non-zero value means that the `SessionDataStore` will skip persisting the session if only the access time changed, and it has been less than `savePeriod` seconds since the last time the session was written. -+ + ____ [NOTE] Configuring `savePeriod` is useful if your persistence technology is very slow/costly for writes. @@ -108,6 +108,19 @@ This allows the possibility that a node may prematurely expire the session, even Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - there is no point in setting a `savePeriod` that is larger than the `maxIdleTime`. ____ +==== Configuring the Remote Infinispan Query Module + +Enabling this module allows jetty to search infinispan for expired sessions that are no longer being referenced by any jetty node. +Note that this is an *additional* module, to be used in conjuction with the `session-store-infinispan-remote` module. + +[source, screen, subs="{sub-order}"] +---- +java -jar ../start.jar --add-to-start=infinispan-remote-query +---- + +There are no configuration properties associated with this module. + + ==== Configuring Embedded Inifinspan Clustering During testing, it can be helpful to run an in-process instance of Infinispan. @@ -137,7 +150,7 @@ Proceed (y/N)? y INFO : server initialised (transitively) in ${jetty.base}/start.d/server.ini INFO : sessions initialised (transitively) in ${jetty.base}/start.d/sessions.ini INFO : session-store-infinispan-embedded initialised in ${jetty.base}/start.d/session-store-infinispan-embedded.ini -DOWNLOAD: https://repo1.maven.org/maven2/org/infinispan/infinispan-embedded/7.1.1.Final/infinispan-embedded-7.1.1.Final.jar to ${jetty.base}/lib/infinispan/infinispan-embedded-7.1.1.Final.jar +DOWNLOAD: https://repo1.maven.org/maven2/org/infinispan/infinispan-embedded-it/9.4.8.Final/infinispan-embedded-it-9.4.8.Final.jar to ${jetty.base}/lib/infinispan/infinispan-embedded-it-9.4.8.Final.jar INFO : Base directory was modified ---- @@ -180,6 +193,19 @@ This allows the possibility that a node may prematurely expire the session, even Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - there is no point in setting a `savePeriod` that is larger than the `maxIdleTime`. ____ + +==== Configuring Inifinspan Embedded Query + +Similarly to the `session-store-infinispan-remote` module, the `session-store-infinispan-embedded` module has an adjunct module `infinispan-embedded-query`, which when enabled, will allow jetty to detect and properly scavenge defunct sessions stranded in infinispan. + +[source, screen, subs="{sub-order}"] +---- +java -jar ../start.jar --add-to-start=infinispan-embedded-query +---- + +There are no configuration properties associated with this module. + + ==== Converting session format for jetty-9.4.13 From jetty-9.4.13 onwards, we have changed the format of the serialized session when using a remote cache (ie using hotrod). diff --git a/jetty-documentation/src/main/asciidoc/administration/startup/startup-windows-service.adoc b/jetty-documentation/src/main/asciidoc/administration/startup/startup-windows-service.adoc index 086b216a267..2bc40090b31 100644 --- a/jetty-documentation/src/main/asciidoc/administration/startup/startup-windows-service.adoc +++ b/jetty-documentation/src/main/asciidoc/administration/startup/startup-windows-service.adoc @@ -147,7 +147,7 @@ We'll start by specifying which modules we want to use (this will create a start [source, screen, subs="{sub-order}"] .... -C:\opt\myappbase>java -jar ..\jetty\start.jar --add-to-start=deploy,http,logging +C:\opt\myappbase>java -jar ..\jetty\start.jar --add-to-start=deploy,http,console-capture WARNING: deploy initialised in ${jetty.base}\start.ini (appended) WARNING: deploy enabled in ${jetty.base}\start.ini @@ -260,7 +260,7 @@ set PR_STOPPARAMS=--stop;STOP.KEY="%STOPKEY%";STOP.PORT=%STOPPORT%;STOP.WAIT=10 --JvmMs="%PR_JVMMS%" ^ --JvmMx="%PR_JVMMX%" ^ --JvmSs="%PR_JVMSS%" ^ - --JvmOptions="%PR_JVMOPTIONS%" ^ + --JvmOptions=%PR_JVMOPTIONS% ^ --Classpath="%PR_CLASSPATH%" ^ --StartMode="%PR_STARTMODE%" ^ --StartClass="%JETTY_START_CLASS%" ^ diff --git a/jetty-documentation/src/main/asciidoc/configuring/connectors/configuring-connectors.adoc b/jetty-documentation/src/main/asciidoc/configuring/connectors/configuring-connectors.adoc index 8b2a1864a28..f0adcb24ecd 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/connectors/configuring-connectors.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/connectors/configuring-connectors.adoc @@ -472,7 +472,7 @@ This adds a `SecureRequestCustomizer` which adds SSL Session IDs and certificate ==== SSL Context Configuration The SSL/TLS connectors for HTTPS and HTTP/2 require a certificate to establish a secure connection. -Jetty holds certificates in standard JVM keystores and are configured as keystore and truststores on a link:{JDURL}/org/eclipse/jetty/util/ssl/SslContextFactory.html[`SslContextFactory`] instance that is injected into an link:{JDURL}/org/eclipse/jetty/server/SslConnectionFactory.html[`SslConnectionFactory`] instance. +Jetty holds certificates in standard JVM keystores and are configured as keystore and truststores on a link:{JDURL}/org/eclipse/jetty/util/ssl/SslContextFactory.Server.html[`SslContextFactory.Server`] instance that is injected into an link:{JDURL}/org/eclipse/jetty/server/SslConnectionFactory.html[`SslConnectionFactory`] instance. An example using the keystore distributed with Jetty (containing a self signed test certificate) is in link:{GITBROWSEURL}/jetty-server/src/main/config/etc/jetty-https.xml[`jetty-https.xml`]. Read more about SSL keystores in link:#configuring-ssl[Configuring SSL]. diff --git a/jetty-documentation/src/main/asciidoc/configuring/connectors/configuring-ssl.adoc b/jetty-documentation/src/main/asciidoc/configuring/connectors/configuring-ssl.adoc index eff950eb2ef..29c8635c215 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/connectors/configuring-ssl.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/connectors/configuring-ssl.adoc @@ -55,9 +55,8 @@ You can re-enable these by re-declaring the ciphers you want excluded in code: [source, java, subs="{sub-order}"] ---- -SslContextFactory sslContextFactory = new SslContextFactory(); -sslContextFactory.setExcludeCipherSuites( - "^.*_(MD5|SHA|SHA1)$"); +SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); +sslContextFactory.setExcludeCipherSuites("^.*_(MD5|SHA|SHA1)$"); ---- If, after making these changes, you still have issues using these ciphers they are likely being blocked at the JVM level. @@ -664,7 +663,7 @@ the other is `$JETTY/etc/truststore` which contains intermediary CA and root CA. [[configuring-sslcontextfactory]] ==== Configuring the Jetty SslContextFactory -The generated SSL certificates from above are held in the key store are configured in an instance of link:{JDURL}/org/eclipse/jetty/util/ssl/SslContextFactory.html[SslContextFactory] object. +The generated SSL certificates from above are held in the key store are configured in an instance of link:{JDURL}/org/eclipse/jetty/util/ssl/SslContextFactory.Server.html[SslContextFactory.Server] object. The `SslContextFactory` is responsible for: @@ -679,9 +678,9 @@ The `SslContextFactory` is responsible for: * https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol[OCSP] Support * Client Authentication Support -For Jetty Connectors, the configured `SslContextFactory` is injected into a specific ServerConnector `SslConnectionFactory`. +For Jetty Connectors, the configured `SslContextFactory.Server` is injected into a specific ServerConnector `SslConnectionFactory`. -For Jetty Clients, the various constructors support using a configured `SslContextFactory`. +For Jetty Clients, the various constructors support using a configured `SslContextFactory.Client`. While the `SslContextFactory` can operate without a keystore (this mode is most suitable for the various Jetty Clients) it is best practice to at least configure the keystore being used. @@ -729,7 +728,7 @@ Implementing Conscrypt for the link:{GITBROWSEURL}/jetty-alpn/jetty-alpn-conscry ... Security.addProvider(new OpenSSLProvider()); ... -SslContextFactory sslContextFactory = new SslContextFactory(); +SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath("path/to/keystore"); sslContextFactory.setKeyStorePassword("CleverKeyStorePassword"); sslContextFactory.setKeyManagerPassword("OBF:VerySecretManagerPassword"); @@ -739,6 +738,9 @@ sslContextFactory.setProvider("Conscrypt"); If you are using the Jetty Distribution, please see the section on enabling the link:#jetty-conscrypt-distribution[Conscrypt SSL module.] +If you are using Conscrypt with Java 8, you must exclude `TLSv1.3` protocol as it is now enabled per default with Conscrypt 2.0.0 but not supported by Java 8. + + ==== Configuring SNI From Java 8, the JVM contains support for the http://en.wikipedia.org/wiki/Server_Name_Indication[Server Name Indicator (SNI)] extension, which allows a SSL connection handshake to indicate one or more DNS names that it applies to. @@ -785,9 +787,9 @@ To do this, first create a new `${jetty.base}/etc/tweak-ssl.xml` file (this can [source, xml, subs="{sub-order}"] ---- + "https://www.eclipse.org/jetty/configure_9_3.dtd"> - + @@ -822,7 +824,7 @@ $ ____ [NOTE] The default `SslContextFactory` implementation applies the latest SSL/TLS recommendations surrounding vulnerabilities in SSL/TLS. -Check the release notes (the `VERSION.txt` found in the root of the Jetty Distribution, or the http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.eclipse.jetty%22%20AND%20a%3A%22jetty-project%22[alternate (classified 'version') artifacts for the `jetty-project` component]on Maven Central) for updates. +Check the release notes (the `VERSION.txt` found in the root of the Jetty Distribution, or the http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.eclipse.jetty%22%20AND%20a%3A%22jetty-project%22[alternate (classified 'version') artifacts for the `jetty-project` component] on Maven Central) for updates. The Java JVM also applies exclusions at the JVM level and, as such, if you have a need to enable something that is generally accepted by the industry as being insecure or vulnerable you will likely have to enable it in *both* the Java JVM and your Jetty configuration. ____ diff --git a/jetty-documentation/src/main/asciidoc/configuring/contexts/configuring-virtual-hosts.adoc b/jetty-documentation/src/main/asciidoc/configuring/contexts/configuring-virtual-hosts.adoc index 451c589f046..99ddb9dd20b 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/contexts/configuring-virtual-hosts.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/contexts/configuring-virtual-hosts.adoc @@ -65,7 +65,7 @@ Here's how you would configure the virtual hosts with a link:#deployable-descrip [source, xml, subs="{sub-order}"] ---- - + /blah @@ -105,7 +105,7 @@ For `blah` webapp: [source, xml, subs="{sub-order}"] ---- - + /blah @@ -131,7 +131,7 @@ For `other` webapp: [source, xml, subs="{sub-order}"] ---- - + /other @@ -180,7 +180,7 @@ For foo webapp: [source, xml, subs="{sub-order}"] ---- - + / @@ -200,7 +200,7 @@ For bar webapp: [source, xml, subs="{sub-order}"] ---- - + / diff --git a/jetty-documentation/src/main/asciidoc/configuring/contexts/custom-error-pages.adoc b/jetty-documentation/src/main/asciidoc/configuring/contexts/custom-error-pages.adoc index 78a0c17c354..95234c4a06d 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/contexts/custom-error-pages.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/contexts/custom-error-pages.adoc @@ -77,7 +77,7 @@ Context files are normally located in `${jetty.base}/webapps/` (see `DeployerMan [source, xml, subs="{sub-order}"] ---- - + /test diff --git a/jetty-documentation/src/main/asciidoc/configuring/deploying/configuring-specific-webapp-deployment.adoc b/jetty-documentation/src/main/asciidoc/configuring/deploying/configuring-specific-webapp-deployment.adoc index 66370f0a4bb..ff95e52693a 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/deploying/configuring-specific-webapp-deployment.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/deploying/configuring-specific-webapp-deployment.adoc @@ -46,7 +46,7 @@ For example, here is a descriptor file that deploys the file `/opt/myapp/myapp.w [source, xml, subs="{sub-order}"] ---- - + /wiki @@ -60,7 +60,7 @@ For example, if the system property is set to `myapp.home=/opt/myapp`, the previ [source, xml, subs="{sub-order}"] ---- - + /wiki @@ -88,7 +88,7 @@ This can help make it clear that users should not make changes to the temporary [source, xml, subs="{sub-order}"] ---- - + /wiki @@ -104,7 +104,7 @@ However, since the `web.xml` for the web application is processed after the depl [source, xml, subs="{sub-order}"] ---- - + /wiki @@ -125,7 +125,7 @@ This feature is useful when adding parameters or additional Servlet mappings wit [source, xml, subs="{sub-order}"] ---- - + /wiki @@ -140,7 +140,7 @@ If the `web.xml` does not include a reference to this data source, an override d [source, xml, subs="{sub-order}"] ---- - + /wiki diff --git a/jetty-documentation/src/main/asciidoc/configuring/deploying/deployment-architecture.adoc b/jetty-documentation/src/main/asciidoc/configuring/deploying/deployment-architecture.adoc index b7a203c5ab1..e2bef1a55a0 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/deploying/deployment-architecture.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/deploying/deployment-architecture.adoc @@ -104,7 +104,7 @@ In the standard Jetty Distribution, this is configured in the `${jetty.home}/etc [source, xml, subs="{sub-order}"] ---- - + diff --git a/jetty-documentation/src/main/asciidoc/configuring/deploying/deployment-processing-webapps.adoc b/jetty-documentation/src/main/asciidoc/configuring/deploying/deployment-processing-webapps.adoc index 2f5d52f2a94..52045e1d819 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/deploying/deployment-processing-webapps.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/deploying/deployment-processing-webapps.adoc @@ -135,7 +135,7 @@ Let's see an example of how we would add in the Configurations for both JNDI _an [source, xml, subs="{sub-order}"] ---- - + @@ -167,7 +167,7 @@ They will then be applied to each `WebAppContext` deployed by the deployer: [source, xml, subs="{sub-order}"] ---- - + @@ -211,7 +211,7 @@ This example uses an xml file, in fact it is the `$JETTY_HOME/etc/jetty-plus.xml [source, xml, subs="{sub-order}"] ---- - + @@ -261,7 +261,7 @@ Here's an example from a context xml file (although as always, you could have ac [source, xml, subs="{sub-order}"] ---- - + @@ -287,7 +287,7 @@ Here's an example in a xml file of a pattern that matches any jar that starts wi [source, xml, subs="{sub-order}"] ---- - + diff --git a/jetty-documentation/src/main/asciidoc/configuring/deploying/overlay-deployer.adoc b/jetty-documentation/src/main/asciidoc/configuring/deploying/overlay-deployer.adoc index 739c7b3b0e1..5395382821d 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/deploying/overlay-deployer.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/deploying/overlay-deployer.adoc @@ -170,7 +170,7 @@ WEB-INF/overlay.xml:: [source, xml, subs="{sub-order}"] ---- - + / @@ -183,7 +183,7 @@ WEB-INF/template.xml:: [source, xml, subs="{sub-order}"] ---- - + true @@ -254,7 +254,7 @@ WEB-INF/overlay.xml:: [source, xml, subs="{sub-order}"] ---- - + diff --git a/jetty-documentation/src/main/asciidoc/configuring/deploying/quickstart-webapp.adoc b/jetty-documentation/src/main/asciidoc/configuring/deploying/quickstart-webapp.adoc index cbbc71e40f3..ece12be99c1 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/deploying/quickstart-webapp.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/deploying/quickstart-webapp.adoc @@ -105,7 +105,7 @@ Otherwise, create a context xml file with the following information (in addition [source, xml, subs="{sub-order}"] ---- - + true diff --git a/jetty-documentation/src/main/asciidoc/configuring/deploying/static-content-deployment.adoc b/jetty-documentation/src/main/asciidoc/configuring/deploying/static-content-deployment.adoc index a2cfe410bb5..a9f566c10fe 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/deploying/static-content-deployment.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/deploying/static-content-deployment.adoc @@ -25,7 +25,7 @@ Create a file called `scratch.xml` in the `${jetty.base}/webapps` directory and [source, xml, subs="{sub-order}"] ---- - + /scratch diff --git a/jetty-documentation/src/main/asciidoc/configuring/security/jaas-support.adoc b/jetty-documentation/src/main/asciidoc/configuring/security/jaas-support.adoc index d6e486c86e4..37f8f7f95ca 100644 --- a/jetty-documentation/src/main/asciidoc/configuring/security/jaas-support.adoc +++ b/jetty-documentation/src/main/asciidoc/configuring/security/jaas-support.adoc @@ -52,13 +52,13 @@ Let's look at an example. ===== Step 1 -Configure a Jetty `org.eclipse.jetty.jaas.JAASLoginService` to match the `` in your `web.xml` file. For example, if the `web.xml` contains a realm called "xyz" like so: +Configure a Jetty `org.eclipse.jetty.jaas.JAASLoginService` to match the `` in your `web.xml` file. For example, if the `web.xml` contains a realm called "Test JAAS Realm" like so: [source, xml, subs="{sub-order}"] ---- FORM - xyz + Test JAAS Realm /login/login /login/error @@ -66,7 +66,7 @@ Configure a Jetty `org.eclipse.jetty.jaas.JAASLoginService` to match the ` ---- -Then you need to create a `JAASLoginService` with the matching name of "xyz": +then you need to create a `JAASLoginService` with the matching realm name of "Test JAAS Realm": [source, xml, subs="{sub-order}"] ---- @@ -76,9 +76,10 @@ Then you need to create a `JAASLoginService` with the matching name of "xyz": ---- +The `LoginModuleName` must match the name of your LoginModule as declared in your login module configuration file (see <>). ____ [CAUTION] -The name of the realm-name that you declare in `web.xml` must match exactly the name of your `JAASLoginService`. +The name of the realm-name that you declare in `web.xml` must match *exactly* the `Name` field of your `JAASLoginService`. ____ You can declare your `JAASLoginService` in a couple of different ways: @@ -135,7 +136,7 @@ xyz { ____ [CAUTION] -It is imperative that the application name on the first line is exactly the same as the `LoginModuleName` of your `JAASLoginService`. +It is imperative that the application name on the first line is *exactly* the same as the `LoginModuleName` of your `JAASLoginService`. ____ You may find it convenient to name this configuration file as `etc/login.conf` because, as we will see below, some of the wiring up for JAAS has been done for you. diff --git a/jetty-documentation/src/main/asciidoc/development/clients/http/http-client-intro.adoc b/jetty-documentation/src/main/asciidoc/development/clients/http/http-client-intro.adoc index de8d0567f31..429f0444763 100644 --- a/jetty-documentation/src/main/asciidoc/development/clients/http/http-client-intro.adoc +++ b/jetty-documentation/src/main/asciidoc/development/clients/http/http-client-intro.adoc @@ -75,13 +75,13 @@ There are several reasons for having multiple `HttpClient` instances including, When you create a `HttpClient` instance using the parameterless constructor, you will only be able to perform plain HTTP requests and you will not be able to perform HTTPS requests. -In order to perform HTTPS requests, you should create first a link:{JDURL}/org/eclipse/jetty/util/ssl/SslContextFactory.html[`SslContextFactory`], configure it, and pass it to the `HttpClient` constructor. +In order to perform HTTPS requests, you should create first a link:{JDURL}/org/eclipse/jetty/util/ssl/SslContextFactory.Client.html[`SslContextFactory.Client`], configure it, and pass it to the `HttpClient` constructor. When created with a `SslContextFactory`, the `HttpClient` will be able to perform both HTTP and HTTPS requests to any domain. [source, java, subs="{sub-order}"] ---- // Instantiate and configure the SslContextFactory -SslContextFactory sslContextFactory = new SslContextFactory(); +SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); // Instantiate HttpClient with the SslContextFactory HttpClient httpClient = new HttpClient(sslContextFactory); diff --git a/jetty-documentation/src/main/asciidoc/development/frameworks/osgi.adoc b/jetty-documentation/src/main/asciidoc/development/frameworks/osgi.adoc index 80ddb4e1298..8b5d7b0812e 100644 --- a/jetty-documentation/src/main/asciidoc/development/frameworks/osgi.adoc +++ b/jetty-documentation/src/main/asciidoc/development/frameworks/osgi.adoc @@ -360,7 +360,7 @@ Here's an example of the contents of a `META-INF/jetty-webapp-context.xml` file: ---- - + META-INF/webdefault.xml @@ -801,7 +801,7 @@ To set the pattern, you will need to provide your own etc files - see the sectio [source, xml, subs="{sub-order}"] ---- - + diff --git a/jetty-documentation/src/main/asciidoc/development/websockets/jetty/jetty-websocket-server-api.adoc b/jetty-documentation/src/main/asciidoc/development/websockets/jetty/jetty-websocket-server-api.adoc index e258b4bd6d4..d18910ae134 100644 --- a/jetty-documentation/src/main/asciidoc/development/websockets/jetty/jetty-websocket-server-api.adoc +++ b/jetty-documentation/src/main/asciidoc/development/websockets/jetty/jetty-websocket-server-api.adoc @@ -31,7 +31,7 @@ To wire up your WebSocket to a specific path via the WebSocketServlet, you will [source, java, subs="{sub-order}"] ---- -include::{SRCDIR}/jetty-websocket/websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyEchoServlet.java[] +include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyEchoServlet.java[] ---- This example will create a Servlet mapped via the http://docs.oracle.com/javaee/6/api/javax/servlet/annotation/WebServlet.html[@WebServlet] annotation to the Servlet path spec of `"/echo"` (or you can do this manually in the `WEB-INF/web.xml` of your web application) which will create MyEchoSocket instances when encountering HTTP Upgrade requests. @@ -57,7 +57,7 @@ If you have a more complicated creation scenario, you might want to provide your [source, java, subs="{sub-order}"] ---- -include::{SRCDIR}/jetty-websocket/websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java[] +include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java[] ---- Here we show a WebSocketCreator that will utilize the http://tools.ietf.org/html/rfc6455#section-1.9[WebSocket subprotocol] information from request to determine what WebSocket type should be @@ -65,7 +65,7 @@ created. [source, java, subs="{sub-order}"] ---- -include::{SRCDIR}/jetty-websocket/websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoServlet.java[] +include::{SRCDIR}/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoServlet.java[] ---- When you want a custom WebSocketCreator, use link:{JDURL}/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.html#setCreator(org.eclipse.jetty.websocket.servlet.WebSocketCreator)[`WebSocketServletFactory.setCreator(WebSocketCreator creator)`] and the WebSocketServletFactory will use your creator for all incoming Upgrade requests on this servlet. diff --git a/jetty-documentation/src/main/asciidoc/quick-start/configuring/how-to-configure.adoc b/jetty-documentation/src/main/asciidoc/quick-start/configuring/how-to-configure.adoc index 2ca4cf98321..b75c2b9f50c 100644 --- a/jetty-documentation/src/main/asciidoc/quick-start/configuring/how-to-configure.adoc +++ b/jetty-documentation/src/main/asciidoc/quick-start/configuring/how-to-configure.adoc @@ -225,7 +225,7 @@ Now that we know what XML file these properties relate to, we can navigate to it ---- $ cat $JETTY_HOME/etc/jetty-http.xml - + diff --git a/jetty-documentation/src/main/asciidoc/quick-start/configuring/what-to-configure.adoc b/jetty-documentation/src/main/asciidoc/quick-start/configuring/what-to-configure.adoc index ad125fb47c3..02ed4f97682 100644 --- a/jetty-documentation/src/main/asciidoc/quick-start/configuring/what-to-configure.adoc +++ b/jetty-documentation/src/main/asciidoc/quick-start/configuring/what-to-configure.adoc @@ -145,9 +145,7 @@ The deployer discovers and hot deploys context IoC descriptors like the followin [source, xml, subs="{sub-order}"] ---- - + diff --git a/jetty-home/src/main/resources/etc/jetty-started.xml b/jetty-home/src/main/resources/etc/jetty-started.xml index faa28395ac5..b8d6007fbcd 100644 --- a/jetty-home/src/main/resources/etc/jetty-started.xml +++ b/jetty-home/src/main/resources/etc/jetty-started.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-home/src/main/resources/etc/jetty-stop.xml b/jetty-home/src/main/resources/etc/jetty-stop.xml index d63e52f23cf..d1a39642862 100644 --- a/jetty-home/src/main/resources/etc/jetty-stop.xml +++ b/jetty-home/src/main/resources/etc/jetty-stop.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-home/src/main/resources/modules/conscrypt.mod b/jetty-home/src/main/resources/modules/conscrypt.mod index 66bcf8d06fc..9a886357be8 100644 --- a/jetty-home/src/main/resources/modules/conscrypt.mod +++ b/jetty-home/src/main/resources/modules/conscrypt.mod @@ -29,6 +29,6 @@ Conscrypt is distributed under the Apache Licence 2.0 https://github.com/google/conscrypt/blob/master/LICENSE [ini] -conscrypt.version?=1.1.4 +conscrypt.version?=2.0.0 jetty.sslContext.provider?=Conscrypt diff --git a/jetty-home/src/main/resources/modules/conscrypt/conscrypt.xml b/jetty-home/src/main/resources/modules/conscrypt/conscrypt.xml index f3c69bd0953..df41e444bcf 100644 --- a/jetty-home/src/main/resources/modules/conscrypt/conscrypt.xml +++ b/jetty-home/src/main/resources/modules/conscrypt/conscrypt.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-home/src/main/resources/modules/hawtio/hawtio.xml b/jetty-home/src/main/resources/modules/hawtio/hawtio.xml index cc87fce9879..922bcac7f46 100644 --- a/jetty-home/src/main/resources/modules/hawtio/hawtio.xml +++ b/jetty-home/src/main/resources/modules/hawtio/hawtio.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-home/src/main/resources/modules/jamon/jamon.xml b/jetty-home/src/main/resources/modules/jamon/jamon.xml index 2ec2d89db4b..14e2e6ea1e4 100644 --- a/jetty-home/src/main/resources/modules/jamon/jamon.xml +++ b/jetty-home/src/main/resources/modules/jamon/jamon.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-home/src/main/resources/modules/jminix/jminix.xml b/jetty-home/src/main/resources/modules/jminix/jminix.xml index 15c699ea4b4..933de82b697 100644 --- a/jetty-home/src/main/resources/modules/jminix/jminix.xml +++ b/jetty-home/src/main/resources/modules/jminix/jminix.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-home/src/main/resources/modules/jolokia/jolokia.xml b/jetty-home/src/main/resources/modules/jolokia/jolokia.xml index b6e240497a3..d8f511fbca1 100644 --- a/jetty-home/src/main/resources/modules/jolokia/jolokia.xml +++ b/jetty-home/src/main/resources/modules/jolokia/jolokia.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-home/start.ini b/jetty-home/start.ini new file mode 100644 index 00000000000..215582944fe --- /dev/null +++ b/jetty-home/start.ini @@ -0,0 +1,9 @@ +# --------------------------------------- +# Module: session-store-infinispan-embedded +# Enables session data store in a local Infinispan cache +# --------------------------------------- +--module=session-store-infinispan-embedded + +#jetty.session.gracePeriod.seconds=3600 +#jetty.session.savePeriod.seconds=0 + diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/ComplianceViolation.java b/jetty-http/src/main/java/org/eclipse/jetty/http/ComplianceViolation.java new file mode 100644 index 00000000000..3972c2171bc --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/ComplianceViolation.java @@ -0,0 +1,48 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.util.Set; + +public interface ComplianceViolation +{ + String getName(); + String getURL(); + String getDescription(); + + default boolean isAllowedBy(Mode mode) + { + return mode.allows(this); + } + + interface Mode + { + String getName(); + boolean allows(ComplianceViolation violation); + Set getKnown(); + Set getAllowed(); + } + + interface Listener + { + default void onComplianceViolation(Mode mode, ComplianceViolation violation, String details) + { + } + } +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java index b2d339cd885..aa852a90d3b 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCompliance.java @@ -18,8 +18,100 @@ package org.eclipse.jetty.http; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static java.util.Collections.unmodifiableSet; +import static java.util.EnumSet.allOf; +import static java.util.EnumSet.copyOf; +import static java.util.EnumSet.noneOf; + /** * The compliance for Cookie handling. * */ -public enum CookieCompliance { RFC6265, RFC2965 } +public class CookieCompliance implements ComplianceViolation.Mode +{ + enum Violation implements ComplianceViolation + { + COMMA_NOT_VALID_OCTET("https://tools.ietf.org/html/rfc6265#section-4.1.1", "Comma not valid as cookie-octet or separator"), + RESERVED_NAMES_NOT_DOLLAR_PREFIXED("https://tools.ietf.org/html/rfc6265#section-4.1.1","Reserved names no longer use '$' prefix") + ; + + private final String url; + private final String description; + + Violation(String url, String description) + { + this.url = url; + this.description = description; + } + + @Override + public String getName() + { + return name(); + } + @Override + public String getURL() + { + return null; + } + + @Override + public String getDescription() + { + return null; + } + } + + public static final CookieCompliance RFC6265 = new CookieCompliance("RFC6265", noneOf(Violation.class)); + public static final CookieCompliance RFC2965 = new CookieCompliance("RFC2965", allOf(Violation.class)); + + private final static List KNOWN_MODES = Arrays.asList(RFC6265,RFC2965); + + public static CookieCompliance valueOf(String name) + { + for (CookieCompliance compliance : KNOWN_MODES) + if (compliance.getName().equals(name)) + return compliance; + return null; + } + + private final String _name; + private final Set _violations; + + private CookieCompliance(String name, Set violations) + { + Objects.nonNull(violations); + _name = name; + _violations = unmodifiableSet(copyOf(violations)); + } + + @Override + public boolean allows(ComplianceViolation violation) + { + return _violations.contains(violation); + } + + @Override + public String getName() + { + return _name; + } + + @Override + public Set getKnown() + { + return EnumSet.allOf(Violation.class); + } + + @Override + public Set getAllowed() + { + return _violations; + } +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java index 15249e61b10..cee4bcaa1fd 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java @@ -24,17 +24,22 @@ import java.util.Locale; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import static org.eclipse.jetty.http.CookieCompliance.Violation.COMMA_NOT_VALID_OCTET; +import static org.eclipse.jetty.http.CookieCompliance.Violation.RESERVED_NAMES_NOT_DOLLAR_PREFIXED; + /** Cookie parser */ public abstract class CookieCutter { protected static final Logger LOG = Log.getLogger(CookieCutter.class); - protected final CookieCompliance _compliance; + protected final CookieCompliance _complianceMode; + private final ComplianceViolation.Listener _complianceListener; - protected CookieCutter(CookieCompliance compliance) + protected CookieCutter(CookieCompliance compliance, ComplianceViolation.Listener complianceListener) { - _compliance = compliance; + _complianceMode = compliance; + _complianceListener = complianceListener; } protected void parseFields(List rawFields) @@ -121,7 +126,9 @@ public abstract class CookieCutter break; case ',': - if (_compliance!=CookieCompliance.RFC2965) + if (COMMA_NOT_VALID_OCTET.isAllowedBy(_complianceMode)) + reportComplianceViolation(COMMA_NOT_VALID_OCTET, "Cookie "+cookieName); + else { if (quoted) { @@ -157,8 +164,9 @@ public abstract class CookieCutter { if (name.startsWith("$")) { - if (_compliance==CookieCompliance.RFC2965) + if (RESERVED_NAMES_NOT_DOLLAR_PREFIXED.isAllowedBy(_complianceMode)) { + reportComplianceViolation(RESERVED_NAMES_NOT_DOLLAR_PREFIXED, "Cookie "+cookieName+" field "+name); String lowercaseName = name.toLowerCase(Locale.ENGLISH); switch(lowercaseName) { @@ -277,7 +285,13 @@ public abstract class CookieCutter } } + protected void reportComplianceViolation(CookieCompliance.Violation violation, String reason) + { + if (_complianceListener != null) + { + _complianceListener.onComplianceViolation(_complianceMode, violation, reason); + } + } + protected abstract void addCookie(String cookieName, String cookieValue, String cookieDomain, String cookiePath, int cookieVersion, String cookieComment); - - } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java index 215c353b4b9..6c9f3b921e8 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java @@ -22,9 +22,9 @@ package org.eclipse.jetty.http; import org.eclipse.jetty.util.HostPort; - -/* ------------------------------------------------------------ */ /** + * A HttpField holding a preparsed Host and port number + * @see HostPort */ public class HostPortHttpField extends HttpField { @@ -35,7 +35,6 @@ public class HostPortHttpField extends HttpField this(HttpHeader.HOST,HttpHeader.HOST.asString(),authority); } - /* ------------------------------------------------------------ */ protected HostPortHttpField(HttpHeader header, String name, String authority) { super(header,name,authority); @@ -49,7 +48,17 @@ public class HostPortHttpField extends HttpField } } - /* ------------------------------------------------------------ */ + public HostPortHttpField(String host, int port) + { + this(new HostPort(host, port)); + } + + public HostPortHttpField(HostPort hostport) + { + super(HttpHeader.HOST,HttpHeader.HOST.asString(),hostport.toString()); + _hostPort = hostport; + } + /** Get the host. * @return the host */ @@ -58,7 +67,6 @@ public class HostPortHttpField extends HttpField return _hostPort.getHost(); } - /* ------------------------------------------------------------ */ /** Get the port. * @return the port */ @@ -67,7 +75,6 @@ public class HostPortHttpField extends HttpField return _hostPort.getPort(); } - /* ------------------------------------------------------------ */ /** Get the port. * @param defaultPort The default port to return if no port set * @return the port @@ -76,4 +83,9 @@ public class HostPortHttpField extends HttpField { return _hostPort.getPort(defaultPort); } + + public HostPort getHostPort() + { + return _hostPort; + } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java index c94ebffdbf5..17da781202e 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java @@ -19,10 +19,10 @@ package org.eclipse.jetty.http; -import static java.nio.charset.StandardCharsets.ISO_8859_1; - import java.util.Arrays; +import static java.nio.charset.StandardCharsets.ISO_8859_1; + /* ------------------------------------------------------------ */ /** @@ -60,7 +60,8 @@ public class Http1FieldPreEncoder implements HttpFieldPreEncoder byte[] v=value.getBytes(ISO_8859_1); byte[] bytes=Arrays.copyOf(n,n.length+2+v.length+2); bytes[n.length]=(byte)':'; - bytes[n.length]=(byte)' '; + bytes[n.length+1]=(byte)' '; + System.arraycopy(v, 0, bytes, n.length+2, v.length); bytes[bytes.length-2]=(byte)'\r'; bytes[bytes.length-1]=(byte)'\n'; diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java index 0664b6f0795..be2f6d70a5d 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java @@ -18,110 +18,148 @@ package org.eclipse.jetty.http; +import java.util.Arrays; import java.util.EnumSet; -import java.util.HashMap; -import java.util.Map; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableSet; +import static java.util.EnumSet.allOf; +import static java.util.EnumSet.complementOf; +import static java.util.EnumSet.noneOf; +import static java.util.EnumSet.of; + /** * HTTP compliance modes for Jetty HTTP parsing and handling. - * A Compliance mode consists of a set of {@link HttpComplianceSection}s which are applied + * A Compliance mode consists of a set of {@link Violation}s which are applied * when the mode is enabled. - *

- * Currently the set of modes is an enum and cannot be dynamically extended, but future major releases may convert this - * to a class. To modify modes there are four custom modes that can be modified by setting the property - * org.eclipse.jetty.http.HttpCompliance.CUSTOMn (where 'n' is '0', '1', '2' or '3'), to a comma separated - * list of sections. The list should start with one of the following strings:

- *
0
No {@link HttpComplianceSection}s
- *
*
All {@link HttpComplianceSection}s
- *
RFC2616
The set of {@link HttpComplianceSection}s application to https://tools.ietf.org/html/rfc2616, - * but not https://tools.ietf.org/html/rfc7230
- *
RFC7230
The set of {@link HttpComplianceSection}s application to https://tools.ietf.org/html/rfc7230
- *
- * The remainder of the list can contain then names of {@link HttpComplianceSection}s to include them in the mode, or prefixed - * with a '-' to exclude thm from the mode. Note that Jetty's modes may have some historic minor differences from the strict - * RFC compliance, for example the RFC2616_LEGACY HttpCompliance is defined as - * RFC2616,-FIELD_COLON,-METHOD_CASE_SENSITIVE. - *

- * Note also that the {@link EnumSet} return by {@link HttpCompliance#sections()} is mutable, so that modes may - * be altered in code and will affect all usages of the mode. */ -public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so that extra custom modes can be defined dynamically +public final class HttpCompliance implements ComplianceViolation.Mode { - /** A Legacy compliance mode to match jetty's behavior prior to RFC2616 and RFC7230. - */ - LEGACY(sectionsBySpec("0,METHOD_CASE_SENSITIVE")), - - /** The legacy RFC2616 support, which incorrectly excludes - * {@link HttpComplianceSection#METHOD_CASE_SENSITIVE}, - * {@link HttpComplianceSection#FIELD_COLON}, - * {@link HttpComplianceSection#TRANSFER_ENCODING_WITH_CONTENT_LENGTH}, - * {@link HttpComplianceSection#MULTIPLE_CONTENT_LENGTHS}, - */ - RFC2616_LEGACY(sectionsBySpec("RFC2616,-FIELD_COLON,-METHOD_CASE_SENSITIVE,-TRANSFER_ENCODING_WITH_CONTENT_LENGTH,-MULTIPLE_CONTENT_LENGTHS")), - - /** The strict RFC2616 support mode */ - RFC2616(sectionsBySpec("RFC2616")), - - /** Jetty's current RFC7230 support, which incorrectly excludes {@link HttpComplianceSection#METHOD_CASE_SENSITIVE} */ - RFC7230_LEGACY(sectionsBySpec("RFC7230,-METHOD_CASE_SENSITIVE")), - /** The RFC7230 support mode */ - RFC7230(sectionsBySpec("RFC7230")); - - public static final String VIOLATIONS_ATTR = "org.eclipse.jetty.http.compliance.violations"; - - private static final Logger LOG = Log.getLogger(HttpParser.class); - private static EnumSet sectionsByProperty(String property) + // These are compliance violations, which may optionally be allowed by the compliance mode, which mean that + // the relevant section of the RFC is not strictly adhered to. + public enum Violation implements ComplianceViolation { - String s = System.getProperty(HttpCompliance.class.getName()+property); - return sectionsBySpec(s==null?"*":s); + CASE_SENSITIVE_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Field name is case-insensitive"), + CASE_INSENSITIVE_METHOD("https://tools.ietf.org/html/rfc7230#section-3.1.1", "Method is case-sensitive"), + HTTP_0_9("https://tools.ietf.org/html/rfc7230#appendix-A.2", "HTTP/0.9 not supported"), + MULTILINE_FIELD_VALUE("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Line Folding not supported"), + MULTIPLE_CONTENT_LENGTHS("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Multiple Content-Lengths"), + TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Transfer-Encoding and Content-Length"), + WHITESPACE_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Whitespace not allowed after field name"), + NO_COLON_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon"); + + private final String url; + private final String description; + + Violation(String url, String description) + { + this.url = url; + this.description = description; + } + + @Override + public String getName() + { + return name(); + } + + @Override + public String getURL() + { + return url; + } + + @Override + public String getDescription() + { + return description; + } } - static EnumSet sectionsBySpec(String spec) + private static final Logger LOG = Log.getLogger(HttpParser.class); + public static final String VIOLATIONS_ATTR = "org.eclipse.jetty.http.compliance.violations"; + + public final static HttpCompliance RFC7230 = new HttpCompliance("RFC7230", noneOf(Violation.class)); + public final static HttpCompliance RFC2616 = new HttpCompliance("RFC2616", of(Violation.HTTP_0_9, Violation.MULTILINE_FIELD_VALUE)); + public final static HttpCompliance LEGACY = new HttpCompliance("LEGACY", complementOf(of(Violation.CASE_INSENSITIVE_METHOD))); + public final static HttpCompliance RFC2616_LEGACY = RFC2616.with( "RFC2616_LEGACY", + Violation.CASE_INSENSITIVE_METHOD, + Violation.NO_COLON_AFTER_FIELD_NAME, + Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH, + Violation.MULTIPLE_CONTENT_LENGTHS); + public final static HttpCompliance RFC7230_LEGACY = RFC7230.with("RFC7230_LEGACY", Violation.CASE_INSENSITIVE_METHOD); + + + private final static List KNOWN_MODES = Arrays.asList(RFC7230,RFC2616,LEGACY,RFC2616_LEGACY,RFC7230_LEGACY); + private final static AtomicInteger __custom = new AtomicInteger(); + + public static HttpCompliance valueOf(String name) { - EnumSet sections; + for (HttpCompliance compliance : KNOWN_MODES) + if (compliance.getName().equals(name)) + return compliance; + return null; + } + + /** + * Create compliance set from string. + *

+ * Format: + *

+ *
+ *
0
No {@link Violation}s
+ *
*
All {@link Violation}s
+ *
RFC2616
The set of {@link Violation}s application to https://tools.ietf.org/html/rfc2616, + * but not https://tools.ietf.org/html/rfc7230
+ *
RFC7230
The set of {@link Violation}s application to https://tools.ietf.org/html/rfc7230
+ *
name
Any of the known modes defined in {@link HttpCompliance#KNOWN_MODES}
+ *
+ *

+ * The remainder of the list can contain then names of {@link Violation}s to include them in the mode, or prefixed + * with a '-' to exclude thm from the mode. + *

+ * @param spec A string in the format of a comma separated list starting with one of the following strings: + * @return the compliance from the string spec + */ + public static HttpCompliance from(String spec) + { + Set sections; String[] elements = spec.split("\\s*,\\s*"); - int i=0; - - switch(elements[i]) - { + switch(elements[0]) + { case "0": - sections = EnumSet.noneOf(HttpComplianceSection.class); - i++; + sections = noneOf(Violation.class); break; - + case "*": - i++; - sections = EnumSet.allOf(HttpComplianceSection.class); - break; - - case "RFC2616": - sections = EnumSet.complementOf(EnumSet.of( - HttpComplianceSection.NO_FIELD_FOLDING, - HttpComplianceSection.NO_HTTP_0_9)); - i++; - break; - - case "RFC7230": - i++; - sections = EnumSet.allOf(HttpComplianceSection.class); + sections = allOf(Violation.class); break; default: - sections = EnumSet.noneOf(HttpComplianceSection.class); - break; + { + HttpCompliance mode = HttpCompliance.valueOf(elements[0]); + if (mode==null) + sections = noneOf(Violation.class); + else + sections = copyOf(mode.getAllowed()); + } } - while(i __required = new HashMap<>(); - static + + + private final String _name; + private final Set _violations; + + private HttpCompliance(String name, Set violations) { - for (HttpComplianceSection section : HttpComplianceSection.values()) - { - for (HttpCompliance compliance : HttpCompliance.values()) - { - if (compliance.sections().contains(section)) - { - __required.put(section,compliance); - break; - } - } - } + Objects.nonNull(violations); + _name = name; + _violations = unmodifiableSet(violations.isEmpty()?noneOf(Violation.class):copyOf(violations)); } - + + @Override + public boolean allows(ComplianceViolation violation) + { + return _violations.contains(violation); + } + + @Override + public String getName() + { + return _name; + } + /** - * @param section The section to query - * @return The minimum compliance required to enable the section. + * Get the set of {@link Violation}s allowed by this compliance mode. + * @return The immutable set of {@link Violation}s allowed by this compliance mode. */ - public static HttpCompliance requiredCompliance(HttpComplianceSection section) + @Override + public Set getAllowed() { - return __required.get(section); + return _violations; } - - private final EnumSet _sections; - - private HttpCompliance(EnumSet sections) + + @Override + public Set getKnown() { - _sections = sections; + return EnumSet.allOf(Violation.class); } - + /** - * Get the set of {@link HttpComplianceSection}s supported by this compliance mode. This set - * is mutable, so it can be modified. Any modification will affect all usages of the mode - * within the same {@link ClassLoader}. - * @return The set of {@link HttpComplianceSection}s supported by this compliance mode. + * Create a new HttpCompliance mode that includes the passed {@link Violation}s. + * @param name The name of the new mode + * @param violations The violations to include + * @return A new {@link HttpCompliance} mode. */ - public EnumSet sections() + public HttpCompliance with(String name, Violation... violations) { - return _sections; + Set union = _violations.isEmpty()?EnumSet.noneOf(Violation.class):copyOf(_violations); + union.addAll(copyOf(violations)); + return new HttpCompliance(name, union); + } + + /** + * Create a new HttpCompliance mode that excludes the passed {@link Violation}s. + * @param name The name of the new mode + * @param violations The violations to exclude + * @return A new {@link HttpCompliance} mode. + */ + public HttpCompliance without(String name, Violation... violations) + { + Set remainder = _violations.isEmpty()?EnumSet.noneOf(Violation.class):copyOf(_violations); + remainder.removeAll(copyOf(violations)); + return new HttpCompliance(name, remainder); + } + + @Override + public String toString() + { + return String.format("%s%s",_name,_violations); + } + + + private static Set copyOf(Violation[] violations) + { + if (violations==null || violations.length==0) + return EnumSet.noneOf(Violation.class); + return EnumSet.copyOf(asList(violations)); + } + + private static Set copyOf(Set violations) + { + if (violations==null || violations.isEmpty()) + return EnumSet.noneOf(Violation.class); + return EnumSet.copyOf(violations); } - } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpComplianceSection.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpComplianceSection.java deleted file mode 100644 index 4aadc830306..00000000000 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpComplianceSection.java +++ /dev/null @@ -1,54 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.http; - -/** - */ -public enum HttpComplianceSection -{ - CASE_INSENSITIVE_FIELD_VALUE_CACHE("","Use case insensitive field value cache"), - METHOD_CASE_SENSITIVE("https://tools.ietf.org/html/rfc7230#section-3.1.1","Method is case-sensitive"), - FIELD_COLON("https://tools.ietf.org/html/rfc7230#section-3.2","Fields must have a Colon"), - FIELD_NAME_CASE_INSENSITIVE("https://tools.ietf.org/html/rfc7230#section-3.2","Field name is case-insensitive"), - NO_WS_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2.4","Whitespace not allowed after field name"), - NO_FIELD_FOLDING("https://tools.ietf.org/html/rfc7230#section-3.2.4","No line Folding"), - NO_HTTP_0_9("https://tools.ietf.org/html/rfc7230#appendix-A.2","No HTTP/0.9"), - TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://tools.ietf.org/html/rfc7230#section-3.3.1","Transfer-Encoding and Content-Length"), - MULTIPLE_CONTENT_LENGTHS("https://tools.ietf.org/html/rfc7230#section-3.3.1","Multiple Content-Lengths"); - - final String url; - final String description; - - HttpComplianceSection(String url,String description) - { - this.url = url; - this.description = description; - } - - public String getURL() - { - return url; - } - - public String getDescription() - { - return description; - } - -} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java index 62ab47e9c55..a4c63baa007 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java @@ -18,10 +18,17 @@ package org.eclipse.jetty.http; +import java.util.List; import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.util.QuotedStringTokenizer; + +// TODO consider replacing this with java.net.HttpCookie public class HttpCookie { + private static final String __COOKIE_DELIM="\",;\\ \t"; + private static final String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim(); + private final String _name; private final String _value; private final String _comment; @@ -67,6 +74,27 @@ public class HttpCookie _expiration = maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(maxAge); } + public HttpCookie(String setCookie) + { + List cookies = java.net.HttpCookie.parse(setCookie); + if (cookies.size()!=1) + throw new IllegalStateException(); + + java.net.HttpCookie cookie = cookies.get(0); + + _name = cookie.getName(); + _value = cookie.getValue(); + _domain = cookie.getDomain(); + _path = cookie.getPath(); + _maxAge = cookie.getMaxAge(); + _httpOnly = cookie.isHttpOnly(); + _secure = cookie.getSecure(); + _comment = cookie.getComment(); + _version = cookie.getVersion(); + _expiration = _maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(_maxAge); + } + + /** * @return the cookie name */ @@ -161,4 +189,197 @@ public class HttpCookie builder.append(";$Path=").append(getPath()); return builder.toString(); } + + + private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote) + { + if (quote) + QuotedStringTokenizer.quoteOnly(buf,s); + else + buf.append(s); + } + + /** Does a cookie value need to be quoted? + * @param s value string + * @return true if quoted; + * @throws IllegalArgumentException If there a control characters in the string + */ + private static boolean isQuoteNeededForCookie(String s) + { + if (s==null || s.length()==0) + return true; + + if (QuotedStringTokenizer.isQuoted(s)) + return false; + + for (int i=0;i=0) + return true; + + if (c<0x20 || c>=0x7f) + throw new IllegalArgumentException("Illegal character in cookie value"); + } + + return false; + } + + public String getSetCookie(CookieCompliance compliance) + { + if (compliance == CookieCompliance.RFC6265) + return getRFC6265SetCookie(); + if (compliance == CookieCompliance.RFC2965) + return getRFC2965SetCookie(); + + throw new IllegalStateException(); + } + + public String getRFC2965SetCookie() + { + // Check arguments + if (_name == null || _name.length() == 0) + throw new IllegalArgumentException("Bad cookie name"); + + // Format value and params + StringBuilder buf = new StringBuilder(); + + // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting + boolean quote_name=isQuoteNeededForCookie(_name); + quoteOnlyOrAppend(buf,_name,quote_name); + + buf.append('='); + + // Append the value + boolean quote_value=isQuoteNeededForCookie(_value); + quoteOnlyOrAppend(buf,_value,quote_value); + + // Look for domain and path fields and check if they need to be quoted + boolean has_domain = _domain!=null && _domain.length()>0; + boolean quote_domain = has_domain && isQuoteNeededForCookie(_domain); + boolean has_path = _path!=null && _path.length()>0; + boolean quote_path = has_path && isQuoteNeededForCookie(_path); + + // Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted + int version = _version; + if (version==0 && ( _comment!=null || quote_name || quote_value || quote_domain || quote_path || + QuotedStringTokenizer.isQuoted(_name) || QuotedStringTokenizer.isQuoted(_value) || + QuotedStringTokenizer.isQuoted(_path) || QuotedStringTokenizer.isQuoted(_domain))) + version=1; + + // Append version + if (version==1) + buf.append (";Version=1"); + else if (version>1) + buf.append (";Version=").append(version); + + // Append path + if (has_path) + { + buf.append(";Path="); + quoteOnlyOrAppend(buf,_path,quote_path); + } + + // Append domain + if (has_domain) + { + buf.append(";Domain="); + quoteOnlyOrAppend(buf,_domain,quote_domain); + } + + // Handle max-age and/or expires + if (_maxAge >= 0) + { + // Always use expires + // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies + buf.append(";Expires="); + if (_maxAge == 0) + buf.append(__01Jan1970_COOKIE); + else + DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * _maxAge); + + // for v1 cookies, also send max-age + if (version>=1) + { + buf.append(";Max-Age="); + buf.append(_maxAge); + } + } + + // add the other fields + if (_secure) + buf.append(";Secure"); + if (_httpOnly) + buf.append(";HttpOnly"); + if (_comment != null) + { + buf.append(";Comment="); + quoteOnlyOrAppend(buf,_comment,isQuoteNeededForCookie(_comment)); + } + return buf.toString(); + } + + public String getRFC6265SetCookie() + { + // Check arguments + if (_name == null || _name.length() == 0) + throw new IllegalArgumentException("Bad cookie name"); + + // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting + // Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules + Syntax.requireValidRFC2616Token(_name, "RFC6265 Cookie name"); + // Ensure that Per RFC6265, Cookie.value follows syntax rules + Syntax.requireValidRFC6265CookieValue(_value); + + // Format value and params + StringBuilder buf = new StringBuilder(); + buf.append(_name).append('=').append(_value==null?"":_value); + + // Append path + if (_path!=null && _path.length()>0) + buf.append("; Path=").append(_path); + + // Append domain + if (_domain!=null && _domain.length()>0) + buf.append("; Domain=").append(_domain); + + // Handle max-age and/or expires + if (_maxAge >= 0) + { + // Always use expires + // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies + buf.append("; Expires="); + if (_maxAge == 0) + buf.append(__01Jan1970_COOKIE); + else + DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * _maxAge); + + buf.append("; Max-Age="); + buf.append(_maxAge); + } + + // add the other fields + if (_secure) + buf.append("; Secure"); + if (_httpOnly) + buf.append("; HttpOnly"); + return buf.toString(); + } + + + public static class SetCookieHttpField extends HttpField + { + final HttpCookie _cookie; + + public SetCookieHttpField(HttpCookie cookie, CookieCompliance compliance) + { + super(HttpHeader.SET_COOKIE, cookie.getSetCookie(compliance)); + this._cookie = cookie; + } + + public HttpCookie getHttpCookie() + { + return _cookie; + } + } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java index 12fdde7f412..dbbc048d839 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java @@ -100,7 +100,7 @@ public class HttpField { if (search==null) return _value==null; - if (search.length()==0) + if (search.isEmpty()) return false; if (_value==null) return false; @@ -276,12 +276,9 @@ public class HttpField public boolean isSameName(HttpField field) { - @SuppressWarnings("ReferenceEquality") - boolean sameObject = (field==this); - if (field==null) return false; - if (sameObject) + if (field==this) return true; if (_header!=null && _header==field.getHeader()) return true; diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java index 9387cd7206f..32dda0fd27c 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java @@ -32,7 +32,6 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.function.ToIntFunction; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.log.Log; @@ -44,7 +43,7 @@ import org.eclipse.jetty.util.log.Logger; * *

This class is not synchronized as it is expected that modifications will only be performed by a * single thread. - * + * *

The cookie handling provided by this class is guided by the Servlet specification and RFC6265. * */ @@ -54,33 +53,33 @@ public class HttpFields implements Iterable private HttpField[] _fields; private int _size; - + /** * Initialize an empty HttpFields. */ public HttpFields() { - _fields=new HttpField[20]; + this(16); // Based on small sample of Chrome requests. } - + /** * Initialize an empty HttpFields. - * + * * @param capacity the capacity of the http fields */ public HttpFields(int capacity) { _fields=new HttpField[capacity]; } - + /** * Initialize HttpFields from copy. - * + * * @param fields the fields to copy data from */ public HttpFields(HttpFields fields) { - _fields=Arrays.copyOf(fields._fields,fields._fields.length+10); + _fields=Arrays.copyOf(fields._fields,fields._fields.length); _size=fields._size; } @@ -88,22 +87,21 @@ public class HttpFields implements Iterable { return _size; } - + @Override public Iterator iterator() { - return new Itr(); + return listIterator(); } public ListIterator listIterator() { - return new Itr(); + return new ListItr(); } - - + public Stream stream() { - return StreamSupport.stream(Arrays.spliterator(_fields,0,_size),false); + return Arrays.stream(_fields).limit(_size); } /** @@ -112,13 +110,15 @@ public class HttpFields implements Iterable */ public Set getFieldNamesCollection() { - final Set set = new HashSet<>(_size); - for (HttpField f : this) + Set set = null; + for (int i=0;i<_size;i++) { - if (f!=null) - set.add(f.getName()); + HttpField f=_fields[i]; + if (set==null) + set = new HashSet<>(); + set.add(f.getName()); } - return set; + return set==null?Collections.emptySet():set; } /** @@ -133,7 +133,7 @@ public class HttpFields implements Iterable /** * Get a Field by index. - * @param index the field index + * @param index the field index * @return A Field value or null if the Field value has not been set */ public HttpField getField(int index) @@ -165,6 +165,22 @@ public class HttpFields implements Iterable return null; } + public List getFields(HttpHeader header) + { + List fields = null; + for (int i=0;i<_size;i++) + { + HttpField f=_fields[i]; + if (f.getHeader()==header) + { + if (fields==null) + fields = new ArrayList<>(); + fields.add(f); + } + } + return fields==null?Collections.emptyList():fields; + } + public boolean contains(HttpField field) { for (int i=_size;i-->0;) @@ -186,7 +202,7 @@ public class HttpFields implements Iterable } return false; } - + public boolean contains(String name, String value) { for (int i=_size;i-->0;) @@ -208,7 +224,7 @@ public class HttpFields implements Iterable } return false; } - + public boolean containsKey(String name) { for (int i=_size;i-->0;) @@ -251,24 +267,30 @@ public class HttpFields implements Iterable public List getValuesList(HttpHeader header) { final List list = new ArrayList<>(); - for (HttpField f : this) - if (f.getHeader()==header) + for (int i=0;i<_size;i++) + { + HttpField f = _fields[i]; + if (f.getHeader() == header) list.add(f.getValue()); + } return list; } - + /** * Get multiple header of the same name - * + * * @return List the header values * @param name the case-insensitive field name */ public List getValuesList(String name) { final List list = new ArrayList<>(); - for (HttpField f : this) + for (int i=0;i<_size;i++) + { + HttpField f = _fields[i]; if (f.getName().equalsIgnoreCase(name)) list.add(f.getValue()); + } return list; } @@ -283,8 +305,9 @@ public class HttpFields implements Iterable public boolean addCSV(HttpHeader header,String... values) { QuotedCSV existing = null; - for (HttpField f : this) + for (int i=0;i<_size;i++) { + HttpField f = _fields[i]; if (f.getHeader()==header) { if (existing==null) @@ -292,7 +315,7 @@ public class HttpFields implements Iterable existing.addValue(f.getValue()); } } - + String value = addCSV(existing,values); if (value!=null) { @@ -301,7 +324,7 @@ public class HttpFields implements Iterable } return false; } - + /** * Add comma separated values, but only if not already * present. @@ -312,8 +335,9 @@ public class HttpFields implements Iterable public boolean addCSV(String name,String... values) { QuotedCSV existing = null; - for (HttpField f : this) + for (int i=0;i<_size;i++) { + HttpField f = _fields[i]; if (f.getName().equalsIgnoreCase(name)) { if (existing==null) @@ -330,14 +354,14 @@ public class HttpFields implements Iterable return false; } - protected String addCSV(QuotedCSV existing,String... values) + protected String addCSV(QuotedCSV existing, String... values) { // remove any existing values from the new values boolean add = true; if (existing!=null && !existing.isEmpty()) { add = false; - + for (int i=values.length;i-->0;) { String unquoted = QuotedCSV.unquote(values[i]); @@ -347,7 +371,7 @@ public class HttpFields implements Iterable add = true; } } - + if (add) { StringBuilder value = new StringBuilder(); @@ -362,12 +386,12 @@ public class HttpFields implements Iterable if (value.length()>0) return value.toString(); } - + return null; } - + /** - * Get multiple field values of the same name, split + * Get multiple field values of the same name, split * as a {@link QuotedCSV} * * @return List the values with OWS stripped @@ -481,7 +505,7 @@ public class HttpFields implements Iterable for (int i=0;i<_size;i++) { final HttpField f = _fields[i]; - + if (f.getName().equalsIgnoreCase(name) && f.getValue()!=null) { final int first=i; @@ -495,7 +519,7 @@ public class HttpFields implements Iterable { if (field==null) { - while (i<_size) + while (i<_size) { field=_fields[i++]; if (field.getName().equalsIgnoreCase(name) && field.getValue()!=null) @@ -548,7 +572,7 @@ public class HttpFields implements Iterable if (!put) add(field); } - + /** * Set a field. * @@ -840,7 +864,7 @@ public class HttpFields implements Iterable { _size=0; } - + public void add(HttpField field) { if (field!=null) @@ -938,36 +962,36 @@ public class HttpFields implements Iterable return value.substring(0, i).trim(); } - private class Itr implements ListIterator + private class ListItr implements ListIterator { int _cursor; // index of next element to return - int _last=-1; + int _current =-1; @Override - public boolean hasNext() + public boolean hasNext() { return _cursor != _size; } @Override - public HttpField next() + public HttpField next() { - int i = _cursor; - if (i >= _size) + if (_cursor == _size) throw new NoSuchElementException(); - _cursor = i + 1; - return _fields[_last=i]; + _current = _cursor++; + return _fields[_current]; } @Override - public void remove() + public void remove() { - if (_last<0) + if (_current <0) throw new IllegalStateException(); - - System.arraycopy(_fields,_last+1,_fields,_last,--_size-_last); - _cursor=_last; - _last=-1; + _size--; + System.arraycopy(_fields, _current +1,_fields, _current,_size- _current); + _fields[_size]=null; + _cursor= _current; + _current =-1; } @Override @@ -981,7 +1005,8 @@ public class HttpFields implements Iterable { if (_cursor == 0) throw new NoSuchElementException(); - return _fields[_last=--_cursor]; + _current = --_cursor; + return _fields[_current]; } @Override @@ -998,10 +1023,10 @@ public class HttpFields implements Iterable @Override public void set(HttpField field) - { - if (_last<0) + { + if (_current <0) throw new IllegalStateException(); - _fields[_last] = field; + _fields[_current] = field; } @Override @@ -1010,8 +1035,7 @@ public class HttpFields implements Iterable _fields = Arrays.copyOf(_fields,_fields.length+1); System.arraycopy(_fields,_cursor,_fields,_cursor+1,_size++); _fields[_cursor++] = field; - _last=-1; + _current =-1; } } - } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java index f39c0f7df98..ac6bf2be8da 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpHeader.java @@ -82,6 +82,7 @@ public enum HttpHeader TE("TE"), USER_AGENT("User-Agent"), X_FORWARDED_FOR("X-Forwarded-For"), + X_FORWARDED_PORT("X-Forwarded-Port"), X_FORWARDED_PROTO("X-Forwarded-Proto"), X_FORWARDED_SERVER("X-Forwarded-Server"), X_FORWARDED_HOST("X-Forwarded-Host"), diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index a2178b97a92..17e60d78e6f 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -24,17 +24,22 @@ import java.util.EnumSet; import java.util.List; import java.util.Locale; +import org.eclipse.jetty.http.HttpCompliance.Violation; import org.eclipse.jetty.http.HttpTokens.EndOfContent; import org.eclipse.jetty.util.ArrayTernaryTrie; import org.eclipse.jetty.util.ArrayTrie; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.Trie; import org.eclipse.jetty.util.Utf8StringBuilder; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import static org.eclipse.jetty.http.HttpComplianceSection.MULTIPLE_CONTENT_LENGTHS; -import static org.eclipse.jetty.http.HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH; +import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME; +import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS; +import static org.eclipse.jetty.http.HttpCompliance.Violation.NO_COLON_AFTER_FIELD_NAME; +import static org.eclipse.jetty.http.HttpCompliance.Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH; +import static org.eclipse.jetty.http.HttpCompliance.Violation.WHITESPACE_AFTER_FIELD_NAME; /* ------------------------------------------------------------ */ @@ -144,10 +149,9 @@ public class HttpParser private final HttpHandler _handler; private final RequestHandler _requestHandler; private final ResponseHandler _responseHandler; - private final ComplianceHandler _complianceHandler; + private final ComplianceViolation.Listener _complianceListener; private final int _maxHeaderBytes; - private final HttpCompliance _compliance; - private final EnumSet _compliances; + private final HttpCompliance _complianceMode; private HttpField _field; private HttpHeader _header; private String _headerString; @@ -282,9 +286,8 @@ public class HttpParser _requestHandler=requestHandler; _responseHandler=responseHandler; _maxHeaderBytes=maxHeaderBytes; - _compliance=compliance; - _compliances=compliance.sections(); - _complianceHandler=(ComplianceHandler)(_handler instanceof ComplianceHandler?_handler:null); + _complianceMode =compliance; + _complianceListener =(ComplianceViolation.Listener)(_handler instanceof ComplianceViolation.Listener?_handler:null); } /* ------------------------------------------------------------------------------- */ @@ -293,48 +296,34 @@ public class HttpParser return _handler; } - /* ------------------------------------------------------------------------------- */ - /** Check RFC compliance violation - * @param violation The compliance section violation - * @return True if the current compliance level is set so as to Not allow this violation - */ - protected boolean complianceViolation(HttpComplianceSection violation) + protected void checkViolation(Violation violation) throws BadMessageException { - return complianceViolation(violation,null); - } - - /* ------------------------------------------------------------------------------- */ - /** Check RFC compliance violation - * @param violation The compliance section violation - * @param reason The reason for the violation - * @return True if the current compliance level is set so as to Not allow this violation - */ - protected boolean complianceViolation(HttpComplianceSection violation, String reason) - { - if (_compliances.contains(violation)) - return true; - if (reason==null) - reason=violation.description; - if (_complianceHandler!=null) - _complianceHandler.onComplianceViolation(_compliance,violation,reason); - - return false; + if (violation.isAllowedBy(_complianceMode)) + reportComplianceViolation(violation, violation.getDescription()); + else + throw new BadMessageException(HttpStatus.BAD_REQUEST_400,violation.getDescription()); } /* ------------------------------------------------------------------------------- */ - protected void handleViolation(HttpComplianceSection section,String reason) + protected void reportComplianceViolation(Violation violation) { - if (_complianceHandler!=null) - _complianceHandler.onComplianceViolation(_compliance,section,reason); + reportComplianceViolation(violation,violation.getDescription()); + } + + /* ------------------------------------------------------------------------------- */ + protected void reportComplianceViolation(Violation violation, String reason) + { + if (_complianceListener !=null) + _complianceListener.onComplianceViolation(_complianceMode,violation,reason); } /* ------------------------------------------------------------------------------- */ protected String caseInsensitiveHeader(String orig, String normative) - { - if (_compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE)) + { + if (CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode)) return normative; if (!orig.equals(normative)) - handleViolation(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE,orig); + reportComplianceViolation(CASE_SENSITIVE_FIELD_NAME,orig); return orig; } @@ -619,23 +608,22 @@ public class HttpParser _length=_string.length(); _methodString=takeString(); - if (_compliances.contains(HttpComplianceSection.METHOD_CASE_SENSITIVE)) + if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode)) + { + HttpMethod method=HttpMethod.INSENSITIVE_CACHE.get(_methodString); + if (method!=null) + { + if (!method.asString().equals(_methodString)) + reportComplianceViolation(Violation.CASE_INSENSITIVE_METHOD,_methodString); + _methodString = method.asString(); + } + } + else { HttpMethod method=HttpMethod.CACHE.get(_methodString); if (method!=null) _methodString = method.asString(); } - else - { - HttpMethod method=HttpMethod.INSENSITIVE_CACHE.get(_methodString); - - if (method!=null) - { - if (!method.asString().equals(_methodString)) - handleViolation(HttpComplianceSection.METHOD_CASE_SENSITIVE,_methodString); - _methodString = method.asString(); - } - } setState(State.SPACE1); break; @@ -762,8 +750,7 @@ public class HttpParser case LF: // HTTP/0.9 - if (complianceViolation(HttpComplianceSection.NO_HTTP_0_9,"No request version")) - throw new BadMessageException("HTTP/0.9 not supported"); + checkViolation(Violation.HTTP_0_9); handle=_requestHandler.startRequest(_methodString,_uri.toString(), HttpVersion.HTTP_0_9); setState(State.END); BufferUtil.clear(buffer); @@ -848,9 +835,7 @@ public class HttpParser else { // HTTP/0.9 - if (complianceViolation(HttpComplianceSection.NO_HTTP_0_9,"No request version")) - throw new BadMessageException("HTTP/0.9 not supported"); - + checkViolation(Violation.HTTP_0_9); handle=_requestHandler.startRequest(_methodString,_uri.toString(), HttpVersion.HTTP_0_9); setState(State.END); BufferUtil.clear(buffer); @@ -959,15 +944,14 @@ public class HttpParser case CONTENT_LENGTH: if (_hasContentLength) { - if(complianceViolation(MULTIPLE_CONTENT_LENGTHS)) - throw new BadMessageException(HttpStatus.BAD_REQUEST_400,MULTIPLE_CONTENT_LENGTHS.description); + checkViolation(MULTIPLE_CONTENT_LENGTHS); if (convertContentLength(_valueString)!=_contentLength) - throw new BadMessageException(HttpStatus.BAD_REQUEST_400,MULTIPLE_CONTENT_LENGTHS.description); + throw new BadMessageException(HttpStatus.BAD_REQUEST_400,MULTIPLE_CONTENT_LENGTHS.getDescription()); } _hasContentLength = true; - if (_endOfContent == EndOfContent.CHUNKED_CONTENT && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)) - throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad Content-Length"); + if (_endOfContent == EndOfContent.CHUNKED_CONTENT) + checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH); if (_endOfContent != EndOfContent.CHUNKED_CONTENT) { @@ -980,8 +964,8 @@ public class HttpParser break; case TRANSFER_ENCODING: - if (_hasContentLength && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)) - throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Transfer-Encoding and Content-Length"); + if (_hasContentLength) + checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH); if (HttpHeaderValue.CHUNKED.is(_valueString)) { @@ -991,7 +975,7 @@ public class HttpParser else { List values = new QuotedCSV(_valueString).getValues(); - if (values.size()>0 && HttpHeaderValue.CHUNKED.is(values.get(values.size()-1))) + if (!values.isEmpty() && HttpHeaderValue.CHUNKED.is(values.get(values.size()-1))) { _endOfContent=EndOfContent.CHUNKED_CONTENT; _contentLength=-1; @@ -1008,7 +992,7 @@ public class HttpParser if (!(_field instanceof HostPortHttpField) && _valueString!=null && !_valueString.isEmpty()) { _field=new HostPortHttpField(_header, - _compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE)?_header.asString():_headerString, + CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode)?_headerString:_header.asString(), _valueString); add_to_connection_trie=_fieldCache!=null; } @@ -1106,11 +1090,10 @@ public class HttpParser case SPACE: case HTAB: { - if (complianceViolation(HttpComplianceSection.NO_FIELD_FOLDING,_headerString)) - throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Header Folding"); + checkViolation(Violation.MULTILINE_FIELD_VALUE); // header value without name - continuation? - if (_valueString==null || _valueString.isEmpty()) + if ( StringUtil.isEmpty(_valueString)) { _string.setLength(0); _length=0; @@ -1223,24 +1206,23 @@ public class HttpParser String n = cached_field.getName(); String v = cached_field.getValue(); - if (!_compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE)) + if (CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode)) { // Have to get the fields exactly from the buffer to match case String en = BufferUtil.toString(buffer,buffer.position()-1,n.length(),StandardCharsets.US_ASCII); if (!n.equals(en)) { - handleViolation(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE,en); + reportComplianceViolation(CASE_SENSITIVE_FIELD_NAME,en); n = en; cached_field = new HttpField(cached_field.getHeader(),n,v); } } - - if (v!=null && !_compliances.contains(HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE)) + + if (v!=null && _handler.isHeaderCacheCaseSensitive()) { String ev = BufferUtil.toString(buffer,buffer.position()+n.length()+1,v.length(),StandardCharsets.ISO_8859_1); if (!v.equals(ev)) { - handleViolation(HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE,ev+"!="+v); v = ev; cached_field = new HttpField(cached_field.getHeader(),n,v); } @@ -1303,9 +1285,10 @@ public class HttpParser case SPACE: case HTAB: //Ignore trailing whitespaces ? - if (!complianceViolation(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME,null)) + if (WHITESPACE_AFTER_FIELD_NAME.isAllowedBy(_complianceMode)) { _headerString=takeString(); + reportComplianceViolation(WHITESPACE_AFTER_FIELD_NAME,"Space after "+_headerString); _header=HttpHeader.CACHE.get(_headerString); _length=-1; setState(FieldState.WS_AFTER_NAME); @@ -1327,11 +1310,12 @@ public class HttpParser _valueString=""; _length=-1; - if (!complianceViolation(HttpComplianceSection.FIELD_COLON,_headerString)) - { + if (NO_COLON_AFTER_FIELD_NAME.isAllowedBy(_complianceMode)) + { + reportComplianceViolation(NO_COLON_AFTER_FIELD_NAME,"Field "+_headerString); setState(FieldState.FIELD); break; - } + } throw new IllegalCharacterException(_state,t,buffer); case ALPHA: @@ -1359,11 +1343,12 @@ public class HttpParser break; case LF: - if (!complianceViolation(HttpComplianceSection.FIELD_COLON,_headerString)) - { + if (NO_COLON_AFTER_FIELD_NAME.isAllowedBy(_complianceMode)) + { + reportComplianceViolation(NO_COLON_AFTER_FIELD_NAME,"Field "+_headerString); setState(FieldState.FIELD); break; - } + } throw new IllegalCharacterException(_state,t,buffer); default: @@ -1877,44 +1862,46 @@ public class HttpParser */ public interface HttpHandler { - public boolean content(ByteBuffer item); + boolean content(ByteBuffer item); - public boolean headerComplete(); + boolean headerComplete(); - public boolean contentComplete(); - - public boolean messageComplete(); + boolean contentComplete(); + + boolean messageComplete(); /** * This is the method called by parser when a HTTP Header name and value is found * @param field The field parsed */ - public void parsedHeader(HttpField field); - + void parsedHeader(HttpField field); + /** * This is the method called by parser when a HTTP Trailer name and value is found * @param field The field parsed */ - public default void parsedTrailer(HttpField field) {} + default void parsedTrailer(HttpField field) {} /* ------------------------------------------------------------ */ /** Called to signal that an EOF was received unexpectedly * during the parsing of a HTTP message */ - public void earlyEOF(); + void earlyEOF(); /* ------------------------------------------------------------ */ /** Called to signal that a bad HTTP message has been received. * @param failure the failure with the bad message information */ - public default void badMessage(BadMessageException failure) + default void badMessage(BadMessageException failure) { } /* ------------------------------------------------------------ */ /** @return the size in bytes of the per parser header cache */ - public int getHeaderCacheSize(); + int getHeaderCacheSize(); + + boolean isHeaderCacheCaseSensitive(); } /* ------------------------------------------------------------------------------- */ @@ -1929,7 +1916,7 @@ public class HttpParser * @param version the http version in use * @return true if handling parsing should return. */ - public boolean startRequest(String method, String uri, HttpVersion version); + boolean startRequest(String method, String uri, HttpVersion version); } @@ -1945,18 +1932,7 @@ public class HttpParser * @param reason the response reason phrase * @return true if handling parsing should return */ - public boolean startResponse(HttpVersion version, int status, String reason); - } - - /* ------------------------------------------------------------------------------- */ - /* ------------------------------------------------------------------------------- */ - /* ------------------------------------------------------------------------------- */ - public interface ComplianceHandler extends HttpHandler - { - public default void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String details) - { - - } + boolean startResponse(HttpVersion version, int status, String reason); } /* ------------------------------------------------------------------------------- */ diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java index 3eead93f09e..0a7a6f6ec3d 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java @@ -568,7 +568,7 @@ public class HttpURI public String getHost() { // Return null for empty host to retain compatibility with java.net.URI - if (_host!=null && _host.length()==0) + if (_host!=null && _host.isEmpty()) return null; return _host; } @@ -613,7 +613,7 @@ public class HttpURI /* ------------------------------------------------------------ */ public boolean hasQuery() { - return _query!=null && _query.length()>0; + return _query!=null && !_query.isEmpty(); } /* ------------------------------------------------------------ */ @@ -667,7 +667,7 @@ public class HttpURI /* ------------------------------------------------------------ */ public boolean isAbsolute() { - return _scheme!=null && _scheme.length()>0; + return _scheme!=null && !_scheme.isEmpty(); } /* ------------------------------------------------------------ */ diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 8a12b10f37f..3baf9a80996 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -223,7 +223,7 @@ public class MimeTypes .forEach(x-> __dftMimeMap.put(StringUtil.asciiToLowerCase(x), normalizeMimeType(props.getProperty(x)))); - if (__dftMimeMap.size()==0) + if (__dftMimeMap.isEmpty()) { LOG.warn("Empty mime types at {}", resourceName); } @@ -268,7 +268,7 @@ public class MimeTypes __inferredEncodings.put(t, props.getProperty(t)); }); - if (__inferredEncodings.size()==0) + if (__inferredEncodings.isEmpty()) { LOG.warn("Empty encodings at {}", resourceName); } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java index 1ea2f229418..10386218023 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartFormInputStream.java @@ -106,7 +106,7 @@ public class MultiPartFormInputStream // We will either be writing to a file, if it has a filename on the content-disposition // and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we // will need to change to write to a file. - if (isWriteFilesWithFilenames() && _filename != null && _filename.trim().length() > 0) + if (isWriteFilesWithFilenames() && _filename != null && !_filename.trim().isEmpty()) { createFile(); } @@ -298,9 +298,8 @@ public class MultiPartFormInputStream */ public void cleanUp() throws IOException { - if (_temporary && _file != null && _file.exists()) - if (!_file.delete()) - throw new IOException("Could Not Delete File"); + if (_temporary) + delete(); } /** @@ -364,7 +363,7 @@ public class MultiPartFormInputStream Collection> values = _parts.values(); for (List partList : values) { - if (partList.size() != 0) + if (!partList.isEmpty()) return false; } @@ -376,31 +375,21 @@ public class MultiPartFormInputStream */ public void deleteParts() { - if (!_parsed) - return; - - Collection parts; - try - { - parts = getParts(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - MultiException err = null; - for (Part p : parts) + for (List parts : _parts.values()) { - try + for (Part p : parts) { - ((MultiPart)p).cleanUp(); - } - catch (Exception e) - { - if (err == null) - err = new MultiException(); - err.add(e); + try + { + ((MultiPart)p).cleanUp(); + } + catch (Exception e) + { + if (err == null) + err = new MultiException(); + err.add(e); + } } } _parts.clear(); @@ -455,6 +444,9 @@ public class MultiPartFormInputStream { if (_err != null) { + if (LOG.isDebugEnabled()) + LOG.debug("MultiPart parsing failure ", _err); + _err.addSuppressed(new Throwable()); if (_err instanceof IOException) throw (IOException)_err; @@ -473,7 +465,9 @@ public class MultiPartFormInputStream if (_parsed) return; _parsed = true; - + + MultiPartParser parser = null; + Handler handler = new Handler(); try { // initialize @@ -509,9 +503,7 @@ public class MultiPartFormInputStream contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart, bend)).trim()); } - Handler handler = new Handler(); - MultiPartParser parser = new MultiPartParser(handler, contentTypeBoundary); - + parser = new MultiPartParser(handler, contentTypeBoundary); byte[] data = new byte[_bufferSize]; int len; long total = 0; @@ -523,7 +515,6 @@ public class MultiPartFormInputStream if (len > 0) { - // keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize total += len; if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize()) @@ -573,6 +564,10 @@ public class MultiPartFormInputStream catch (Throwable e) { _err = e; + + // Notify parser if failure occurs + if (parser != null) + parser.parse(BufferUtil.EMPTY_BUFFER, true); } } @@ -720,6 +715,16 @@ public class MultiPartFormInputStream { if (LOG.isDebugEnabled()) LOG.debug("Early EOF {}", MultiPartFormInputStream.this); + + try + { + if (_part != null) + _part.close(); + } + catch (IOException e) + { + LOG.warn("part could not be closed", e); + } } public void reset() diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java index 0832e0f8641..7e4b12da9d8 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java @@ -131,12 +131,8 @@ public class MultiPartParser private HttpTokens.Token next(ByteBuffer buffer) { byte ch = buffer.get(); - HttpTokens.Token t = HttpTokens.TOKENS[0xff & ch]; - if (DEBUG) - LOG.debug("token={}",t); - switch(t.getType()) { case CNTL: @@ -271,6 +267,9 @@ public class MultiPartParser /* ------------------------------------------------------------------------------- */ private void parsePreamble(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) + LOG.debug("parsePreamble({})", BufferUtil.toDetailString(buffer)); + if (_partialBoundary > 0) { int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary); @@ -307,6 +306,9 @@ public class MultiPartParser /* ------------------------------------------------------------------------------- */ private void parseDelimiter(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) + LOG.debug("parseDelimiter({})", BufferUtil.toDetailString(buffer)); + while (__delimiterStates.contains(_state) && hasNextByte(buffer)) { HttpTokens.Token t = next(buffer); @@ -354,6 +356,9 @@ public class MultiPartParser */ protected boolean parseMimePartHeaders(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) + LOG.debug("parseMimePartHeaders({})", BufferUtil.toDetailString(buffer)); + // Process headers while (_state == State.BODY_PART && hasNextByte(buffer)) { @@ -575,6 +580,8 @@ public class MultiPartParser protected boolean parseOctetContent(ByteBuffer buffer) { + if (LOG.isDebugEnabled()) + LOG.debug("parseOctetContent({})", BufferUtil.toDetailString(buffer)); // Starts With if (_partialBoundary > 0) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java index 11ad88821c0..7b5c0c47d88 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/PreEncodedHttpField.java @@ -21,7 +21,6 @@ package org.eclipse.jetty.http; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; @@ -73,7 +72,7 @@ public class PreEncodedHttpField extends HttpField else LOG.warn("multiple PreEncoders for "+e.getHttpVersion()); } - + // Always support HTTP1 if (__encoders[0]==null) __encoders[0] = new Http1FieldPreEncoder(); diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java index d5403a476b1..538bfc51c8f 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/QuotedCSV.java @@ -33,7 +33,7 @@ import org.eclipse.jetty.util.QuotedStringTokenizer; * @see "https://tools.ietf.org/html/rfc7230#section-3.2.6" * @see "https://tools.ietf.org/html/rfc7230#section-7" */ -public class QuotedCSV implements Iterable +public class QuotedCSV extends QuotedCSVParser implements Iterable { /** * ABNF from RFC 2616, RFC 822, and RFC 6455 specified characters requiring quoting. @@ -110,225 +110,24 @@ public class QuotedCSV implements Iterable } } - - private enum State { VALUE, PARAM_NAME, PARAM_VALUE}; - protected final List _values = new ArrayList<>(); - protected final boolean _keepQuotes; - - /* ------------------------------------------------------------ */ + public QuotedCSV(String... values) { this(true,values); } - /* ------------------------------------------------------------ */ public QuotedCSV(boolean keepQuotes,String... values) { - _keepQuotes=keepQuotes; + super(keepQuotes); for (String v:values) addValue(v); } - - /* ------------------------------------------------------------ */ - /** Add and parse a value string(s) - * @param value A value that may contain one or more Quoted CSV items. - */ - public void addValue(String value) - { - if (value == null) - return; - - StringBuffer buffer = new StringBuffer(); - - int l=value.length(); - State state=State.VALUE; - boolean quoted=false; - boolean sloshed=false; - int nws_length=0; - int last_length=0; - int value_length=-1; - int param_name=-1; - int param_value=-1; - - for (int i=0;i<=l;i++) - { - char c=i==l?0:value.charAt(i); - - // Handle quoting https://tools.ietf.org/html/rfc7230#section-3.2.6 - if (quoted && c!=0) - { - if (sloshed) - sloshed=false; - else - { - switch(c) - { - case '\\': - sloshed=true; - if (!_keepQuotes) - continue; - break; - case '"': - quoted=false; - if (!_keepQuotes) - continue; - break; - } - } - - buffer.append(c); - nws_length=buffer.length(); - continue; - } - - // Handle common cases - switch(c) - { - case ' ': - case '\t': - if (buffer.length()>last_length) // not leading OWS - buffer.append(c); - continue; - - case '"': - quoted=true; - if (_keepQuotes) - { - if (state==State.PARAM_VALUE && param_value<0) - param_value=nws_length; - buffer.append(c); - } - else if (state==State.PARAM_VALUE && param_value<0) - param_value=nws_length; - nws_length=buffer.length(); - continue; - - case ';': - buffer.setLength(nws_length); // trim following OWS - if (state==State.VALUE) - { - parsedValue(buffer); - value_length=buffer.length(); - } - else - parsedParam(buffer,value_length,param_name,param_value); - nws_length=buffer.length(); - param_name=param_value=-1; - buffer.append(c); - last_length=++nws_length; - state=State.PARAM_NAME; - continue; - - case ',': - case 0: - if (nws_length>0) - { - buffer.setLength(nws_length); // trim following OWS - switch(state) - { - case VALUE: - parsedValue(buffer); - value_length=buffer.length(); - break; - case PARAM_NAME: - case PARAM_VALUE: - parsedParam(buffer,value_length,param_name,param_value); - break; - } - _values.add(buffer.toString()); - } - buffer.setLength(0); - last_length=0; - nws_length=0; - value_length=param_name=param_value=-1; - state=State.VALUE; - continue; - - case '=': - switch (state) - { - case VALUE: - // It wasn't really a value, it was a param name - value_length=param_name=0; - buffer.setLength(nws_length); // trim following OWS - String param = buffer.toString(); - buffer.setLength(0); - parsedValue(buffer); - value_length=buffer.length(); - buffer.append(param); - buffer.append(c); - last_length=++nws_length; - state=State.PARAM_VALUE; - continue; - - case PARAM_NAME: - buffer.setLength(nws_length); // trim following OWS - buffer.append(c); - last_length=++nws_length; - state=State.PARAM_VALUE; - continue; - - case PARAM_VALUE: - if (param_value<0) - param_value=nws_length; - buffer.append(c); - nws_length=buffer.length(); - continue; - } - continue; - - default: - { - switch (state) - { - case VALUE: - { - buffer.append(c); - nws_length=buffer.length(); - continue; - } - - case PARAM_NAME: - { - if (param_name<0) - param_name=nws_length; - buffer.append(c); - nws_length=buffer.length(); - continue; - } - - case PARAM_VALUE: - { - if (param_value<0) - param_value=nws_length; - buffer.append(c); - nws_length=buffer.length(); - continue; - } - } - } - } - } - } - - /** - * Called when a value has been parsed - * @param buffer Containing the trimmed value, which may be mutated - */ - protected void parsedValue(StringBuffer buffer) - { - } - - /** - * Called when a parameter has been parsed - * @param buffer Containing the trimmed value and all parameters, which may be mutated - * @param valueLength The length of the value - * @param paramName The index of the start of the parameter just parsed - * @param paramValue The index of the start of the parameter value just parsed, or -1 - */ - protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) + + @Override + protected void parsedValueAndParams(StringBuffer buffer) { + _values.add(buffer.toString()); } public int size() @@ -351,55 +150,7 @@ public class QuotedCSV implements Iterable { return _values.iterator(); } - - public static String unquote(String s) - { - // handle trivial cases - int l=s.length(); - if (s==null || l==0) - return s; - - // Look for any quotes - int i=0; - for (;ilast_length) // not leading OWS + buffer.append(c); + continue; + + case '"': + quoted=true; + if (_keepQuotes) + { + if (state==State.PARAM_VALUE && param_value<0) + param_value=nws_length; + buffer.append(c); + } + else if (state==State.PARAM_VALUE && param_value<0) + param_value=nws_length; + nws_length=buffer.length(); + continue; + + case ';': + buffer.setLength(nws_length); // trim following OWS + if (state==State.VALUE) + { + parsedValue(buffer); + value_length=buffer.length(); + } + else + parsedParam(buffer,value_length,param_name,param_value); + nws_length=buffer.length(); + param_name=param_value=-1; + buffer.append(c); + last_length=++nws_length; + state=State.PARAM_NAME; + continue; + + case ',': + case 0: + if (nws_length>0) + { + buffer.setLength(nws_length); // trim following OWS + switch(state) + { + case VALUE: + parsedValue(buffer); + value_length=buffer.length(); + break; + case PARAM_NAME: + case PARAM_VALUE: + parsedParam(buffer,value_length,param_name,param_value); + break; + } + parsedValueAndParams(buffer); + } + buffer.setLength(0); + last_length=0; + nws_length=0; + value_length=param_name=param_value=-1; + state=State.VALUE; + continue; + + case '=': + switch (state) + { + case VALUE: + // It wasn't really a value, it was a param name + value_length=param_name=0; + buffer.setLength(nws_length); // trim following OWS + String param = buffer.toString(); + buffer.setLength(0); + parsedValue(buffer); + value_length=buffer.length(); + buffer.append(param); + buffer.append(c); + last_length=++nws_length; + state=State.PARAM_VALUE; + continue; + + case PARAM_NAME: + buffer.setLength(nws_length); // trim following OWS + buffer.append(c); + last_length=++nws_length; + state=State.PARAM_VALUE; + continue; + + case PARAM_VALUE: + if (param_value<0) + param_value=nws_length; + buffer.append(c); + nws_length=buffer.length(); + continue; + } + continue; + + default: + { + switch (state) + { + case VALUE: + { + buffer.append(c); + nws_length=buffer.length(); + continue; + } + + case PARAM_NAME: + { + if (param_name<0) + param_name=nws_length; + buffer.append(c); + nws_length=buffer.length(); + continue; + } + + case PARAM_VALUE: + { + if (param_value<0) + param_value=nws_length; + buffer.append(c); + nws_length=buffer.length(); + continue; + } + } + } + } + } + } + + /** + * Called when a value and it's parameters has been parsed + * @param buffer Containing the trimmed value and parameters + */ + protected void parsedValueAndParams(StringBuffer buffer) + { + } + + /** + * Called when a value has been parsed (prior to any parameters) + * @param buffer Containing the trimmed value, which may be mutated + */ + protected void parsedValue(StringBuffer buffer) + { + } + + /** + * Called when a parameter has been parsed + * @param buffer Containing the trimmed value and all parameters, which may be mutated + * @param valueLength The length of the value + * @param paramName The index of the start of the parameter just parsed + * @param paramValue The index of the start of the parameter value just parsed, or -1 + */ + protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) + { + } + +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java b/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java index beab73124a7..704a33ecbd2 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java @@ -186,7 +186,7 @@ public class ServletPathSpec extends PathSpec assertValidServletPathSpec(servletPathSpec); // The Root Path Spec - if (servletPathSpec.length() == 0) + if (servletPathSpec.isEmpty()) { super.pathSpec = ""; super.pathDepth = -1; // force this to be at the end of the sort order diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterLenientTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterLenientTest.java index 2ec26f772ff..ff5b239545f 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterLenientTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterLenientTest.java @@ -18,15 +18,15 @@ package org.eclipse.jetty.http; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -179,7 +179,7 @@ public class CookieCutterLenientTest protected TestCutter() { - super(CookieCompliance.RFC6265); + super(CookieCompliance.RFC6265, null); } @Override diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterTest.java index befce25cf5b..de68a91f493 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterTest.java @@ -18,13 +18,12 @@ package org.eclipse.jetty.http; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -32,7 +31,7 @@ public class CookieCutterTest { private Cookie[] parseCookieHeaders(CookieCompliance compliance,String... headers) { - TestCutter cutter = new TestCutter(compliance); + TestCutter cutter = new TestCutter(compliance, null); for (String header : headers) { cutter.parseFields(header); @@ -269,14 +268,9 @@ public class CookieCutterTest { List cookies = new ArrayList<>(); - protected TestCutter() + public TestCutter(CookieCompliance compliance, ComplianceViolation.Listener complianceListener) { - this(CookieCompliance.RFC6265); - } - - public TestCutter(CookieCompliance compliance) - { - super(compliance); + super(compliance, complianceListener); } @Override diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/DateParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/DateParserTest.java new file mode 100644 index 00000000000..f219fc0250e --- /dev/null +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/DateParserTest.java @@ -0,0 +1,45 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for class {@link DateParser}. + * + * @see DateParser + */ +public class DateParserTest +{ + + @Test + public void testParseDateWithNonDateString() + { + assertEquals((-1L), DateParser.parseDate("3%~")); + } + + @Test + public void testParseDateWithNonDateStringEndingWithGMT() + { + assertEquals((-1L), DateParser.parseDate("3%~ GMT")); + } + +} diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java new file mode 100644 index 00000000000..c1ac8685311 --- /dev/null +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java @@ -0,0 +1,184 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class HttpCookieTest +{ + + @Test + public void testConstructFromSetCookie() + { + HttpCookie cookie = new HttpCookie("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly"); + } + + @Test + public void testSetRFC2965Cookie() throws Exception + { + HttpCookie httpCookie; + + httpCookie = new HttpCookie("null", null, null, null, -1, false, false, null, -1); + assertEquals("null=",httpCookie.getRFC2965SetCookie()); + + + httpCookie = new HttpCookie("minimal", "value", null, null, -1, false, false, null, -1); + assertEquals("minimal=value",httpCookie.getRFC2965SetCookie()); + + httpCookie = new HttpCookie("everything", "something", "domain", "path", 0, true, true, "noncomment", 0); + assertEquals("everything=something;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=noncomment",httpCookie.getRFC2965SetCookie()); + + httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, "comment", 0); + assertEquals("everything=value;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",httpCookie.getRFC2965SetCookie()); + + + httpCookie = new HttpCookie("ev erything", "va lue", "do main", "pa th", 1, true, true, "co mment", 1); + String setCookie=httpCookie.getRFC2965SetCookie(); + assertThat(setCookie, Matchers.startsWith("\"ev erything\"=\"va lue\";Version=1;Path=\"pa th\";Domain=\"do main\";Expires=")); + assertThat(setCookie,Matchers.endsWith(" GMT;Max-Age=1;Secure;HttpOnly;Comment=\"co mment\"")); + + httpCookie = new HttpCookie("name", "value", null, null, -1, false, false, null, 0); + setCookie=httpCookie.getRFC2965SetCookie(); + assertEquals(-1,setCookie.indexOf("Version=")); + httpCookie = new HttpCookie("name", "v a l u e", null, null, -1, false, false, null, 0); + setCookie=httpCookie.getRFC2965SetCookie(); + + httpCookie = new HttpCookie("json","{\"services\":[\"cwa\", \"aa\"]}", null, null, -1, false, false, null, -1); + assertEquals("json=\"{\\\"services\\\":[\\\"cwa\\\", \\\"aa\\\"]}\"",httpCookie.getRFC2965SetCookie()); + + httpCookie = new HttpCookie("name", "value%=", null, null, -1, false, false, null, 0); + setCookie=httpCookie.getRFC2965SetCookie(); + assertEquals("name=value%=",setCookie); + } + + @Test + public void testSetRFC6265Cookie() throws Exception + { + HttpCookie httpCookie; + + httpCookie = new HttpCookie("null",null,null,null,-1,false,false, null, -1); + assertEquals("null=",httpCookie.getRFC6265SetCookie()); + + httpCookie = new HttpCookie("minimal","value",null,null,-1,false,false, null, -1); + assertEquals("minimal=value",httpCookie.getRFC6265SetCookie()); + + //test cookies with same name, domain and path + httpCookie = new HttpCookie("everything","something","domain","path",0,true,true, null, -1); + assertEquals("everything=something; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",httpCookie.getRFC6265SetCookie()); + + httpCookie = new HttpCookie("everything","value","domain","path",0,true,true, null, -1); + assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",httpCookie.getRFC6265SetCookie()); + + String badNameExamples[] = { + "\"name\"", + "name\t", + "na me", + "name\u0082", + "na\tme", + "na;me", + "{name}", + "[name]", + "\"" + }; + + for (String badNameExample : badNameExamples) + { + try + { + httpCookie = new HttpCookie(badNameExample, "value", null, "/", 1, true, true, null, -1); + httpCookie.getRFC6265SetCookie(); + fail(badNameExample); + } + catch (IllegalArgumentException ex) + { + // System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage()); + assertThat("Testing bad name: [" + badNameExample + "]", ex.getMessage(), + allOf(containsString("RFC6265"), containsString("RFC2616"))); + } + } + + String badValueExamples[] = { + "va\tlue", + "\t", + "value\u0000", + "val\u0082ue", + "va lue", + "va;lue", + "\"value", + "value\"", + "val\\ue", + "val\"ue", + "\"" + }; + + for (String badValueExample : badValueExamples) + { + try + { + httpCookie = new HttpCookie("name", badValueExample, null, "/", 1, true, true, null, -1); + httpCookie.getRFC6265SetCookie(); + fail(); + } + catch (IllegalArgumentException ex) + { + // System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage()); + assertThat("Testing bad value [" + badValueExample + "]", ex.getMessage(), Matchers.containsString("RFC6265")); + } + } + + String goodNameExamples[] = { + "name", + "n.a.m.e", + "na-me", + "+name", + "na*me", + "na$me", + "#name" + }; + + for (String goodNameExample : goodNameExamples) + { + httpCookie = new HttpCookie(goodNameExample, "value", null, "/", 1, true, true, null, -1); + // should not throw an exception + } + + String goodValueExamples[] = { + "value", + "", + null, + "val=ue", + "val-ue", + "val/ue", + "v.a.l.u.e" + }; + + for (String goodValueExample : goodValueExamples) + { + httpCookie = new HttpCookie("name", goodValueExample, null, "/", 1, true, true, null, -1); + // should not throw an exception + } + } +} diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldTest.java index 1b0e9697b11..f4135803ffd 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldTest.java @@ -18,18 +18,18 @@ package org.eclipse.jetty.http; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertNull; - import java.nio.ByteBuffer; import org.eclipse.jetty.util.BufferUtil; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpFieldTest { diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java index 3d15791cba8..11e369009e5 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java @@ -18,15 +18,6 @@ package org.eclipse.jetty.http; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.nio.ByteBuffer; import java.util.Collections; import java.util.Enumeration; @@ -40,6 +31,15 @@ import org.eclipse.jetty.util.BufferUtil; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpFieldsTest { @Test @@ -299,6 +299,46 @@ public class HttpFieldsTest assertEquals(false, e.hasMoreElements()); } + @Test + public void testPreEncodedField() + { + ByteBuffer buffer = BufferUtil.allocate(1024); + + PreEncodedHttpField known = new PreEncodedHttpField(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); + BufferUtil.clearToFill(buffer); + known.putTo(buffer,HttpVersion.HTTP_1_1); + BufferUtil.flipToFlush(buffer,0); + assertThat(BufferUtil.toString(buffer),is("Connection: close\r\n")); + + PreEncodedHttpField unknown = new PreEncodedHttpField(null, "Header", "Value"); + BufferUtil.clearToFill(buffer); + unknown.putTo(buffer,HttpVersion.HTTP_1_1); + BufferUtil.flipToFlush(buffer,0); + assertThat(BufferUtil.toString(buffer),is("Header: Value\r\n")); + } + + @Test + public void testAddPreEncodedField() + { + final PreEncodedHttpField X_XSS_PROTECTION_FIELD = new PreEncodedHttpField("X-XSS-Protection", "1; mode=block"); + + HttpFields fields = new HttpFields(); + fields.add(X_XSS_PROTECTION_FIELD); + + assertThat("Fields output", fields.toString(), containsString("X-XSS-Protection: 1; mode=block")); + } + + @Test + public void testAddFinalHttpField() + { + final HttpField X_XSS_PROTECTION_FIELD = new HttpField("X-XSS-Protection", "1; mode=block"); + + HttpFields fields = new HttpFields(); + fields.add(X_XSS_PROTECTION_FIELD); + + assertThat("Fields output", fields.toString(), containsString("X-XSS-Protection: 1; mode=block")); + } + @Test public void testGetValues() throws Exception { diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerHTTPTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerHTTPTest.java index 9f9ac3bd20f..07d6d744fe5 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerHTTPTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorServerHTTPTest.java @@ -266,6 +266,12 @@ public class HttpGeneratorServerHTTPTest { return 4096; } + + @Override + public boolean isHeaderCacheCaseSensitive() + { + return false; + } } public final static String CONTENT = "The quick brown fox jumped over the lazy dog.\nNow is the time for all good men to come to the aid of the party\nThe moon is blue to a fish in love.\n"; diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java index f124233356d..5cee781fec7 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java @@ -24,13 +24,18 @@ import java.util.ArrayList; import java.util.List; import org.eclipse.jetty.http.HttpParser.State; +import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.log.StacklessLogging; import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.eclipse.jetty.http.HttpComplianceSection.NO_FIELD_FOLDING; +import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_INSENSITIVE_METHOD; +import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME; +import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTILINE_FIELD_VALUE; +import static org.eclipse.jetty.http.HttpCompliance.Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; @@ -130,7 +135,7 @@ public class HttpParserTest assertEquals("/999", _uriOrStatus); assertEquals("HTTP/0.9", _versionOrReason); assertEquals(-1, _headers); - assertThat(_complianceViolation, contains(HttpComplianceSection.NO_HTTP_0_9)); + assertThat(_complianceViolation, contains(HttpCompliance.Violation.HTTP_0_9)); } @Test @@ -159,7 +164,7 @@ public class HttpParserTest assertEquals("/222", _uriOrStatus); assertEquals("HTTP/0.9", _versionOrReason); assertEquals(-1, _headers); - assertThat(_complianceViolation, contains(HttpComplianceSection.NO_HTTP_0_9)); + assertThat(_complianceViolation, contains(HttpCompliance.Violation.HTTP_0_9)); } @Test @@ -304,7 +309,7 @@ public class HttpParserTest assertEquals("value extra", _val[1]); assertEquals("Name2", _hdr[2]); assertEquals("value2", _val[2]); - assertThat(_complianceViolation, contains(NO_FIELD_FOLDING,NO_FIELD_FOLDING)); + assertThat(_complianceViolation, contains(MULTILINE_FIELD_VALUE,MULTILINE_FIELD_VALUE)); } @Test @@ -322,7 +327,7 @@ public class HttpParserTest parseAll(parser, buffer); assertThat(_bad, Matchers.notNullValue()); - assertThat(_bad, containsString("Header Folding")); + assertThat(_bad, containsString("Line Folding not supported")); assertThat(_complianceViolation,Matchers.empty()); } @@ -807,7 +812,7 @@ public class HttpParserTest parseAll(parser, buffer); assertNull(_bad); assertEquals("GET", _methodOrVersion); - assertThat(_complianceViolation, contains(HttpComplianceSection.METHOD_CASE_SENSITIVE)); + assertThat(_complianceViolation, contains(CASE_INSENSITIVE_METHOD)); } @Test @@ -857,7 +862,7 @@ public class HttpParserTest "HOST: localhost\r\n" + "cOnNeCtIoN: ClOsE\r\n" + "\r\n"); - HttpParser.RequestHandler handler = new Handler(); + HttpParser.RequestHandler handler = new Handler(true); HttpParser parser = new HttpParser(handler, -1, HttpCompliance.LEGACY); parseAll(parser, buffer); assertNull(_bad); @@ -869,7 +874,7 @@ public class HttpParserTest assertEquals("cOnNeCtIoN", _hdr[1]); assertEquals("ClOsE", _val[1]); assertEquals(1, _headers); - assertThat(_complianceViolation, contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE,HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE,HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE)); + assertThat(_complianceViolation, contains(CASE_SENSITIVE_FIELD_NAME, CASE_SENSITIVE_FIELD_NAME)); } @Test @@ -1862,7 +1867,7 @@ public class HttpParserTest assertTrue(_headerCompleted); assertTrue(_messageCompleted); - assertThat(_complianceViolation, contains(HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); + assertThat(_complianceViolation, contains(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); } @Test @@ -1891,7 +1896,7 @@ public class HttpParserTest assertTrue(_headerCompleted); assertTrue(_messageCompleted); - assertThat(_complianceViolation, contains(HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); + assertThat(_complianceViolation, contains(TRANSFER_ENCODING_WITH_CONTENT_LENGTH)); } @Test @@ -1974,6 +1979,7 @@ public class HttpParserTest @Test public void testIPv6Host() throws Exception { + Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); ByteBuffer buffer = BufferUtil.toBuffer( "GET / HTTP/1.1\r\n" + "Host: [::1]\r\n" @@ -2055,6 +2061,7 @@ public class HttpParserTest @Test public void testIPv6HostPort() throws Exception { + Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); ByteBuffer buffer = BufferUtil.toBuffer( "GET / HTTP/1.1\r\n" + "Host: [::1]:8888\r\n" @@ -2185,10 +2192,22 @@ public class HttpParserTest private boolean _early; private boolean _headerCompleted; private boolean _messageCompleted; - private final List _complianceViolation = new ArrayList<>(); + private final List _complianceViolation = new ArrayList<>(); - private class Handler implements HttpParser.RequestHandler, HttpParser.ResponseHandler, HttpParser.ComplianceHandler + private class Handler implements HttpParser.RequestHandler, HttpParser.ResponseHandler, ComplianceViolation.Listener { + private boolean _headerCacheCaseSensitive; + + public Handler() + { + this(false); + } + + public Handler(boolean headerCacheCaseSensitive) + { + _headerCacheCaseSensitive = headerCacheCaseSensitive; + } + @Override public boolean content(ByteBuffer ref) { @@ -2295,7 +2314,13 @@ public class HttpParserTest } @Override - public void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String reason) + public boolean isHeaderCacheCaseSensitive() + { + return _headerCacheCaseSensitive; + } + + @Override + public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String reason) { _complianceViolation.add(violation); } diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpSchemeTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpSchemeTest.java new file mode 100644 index 00000000000..513c2a3f17a --- /dev/null +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpSchemeTest.java @@ -0,0 +1,81 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for class {@link HttpScheme}. + * + * @see HttpScheme + */ +public class HttpSchemeTest +{ + + @Test + public void testIsReturningTrue() + { + HttpScheme httpScheme = HttpScheme.HTTPS; + + assertTrue(httpScheme.is("https")); + assertEquals("https", httpScheme.asString()); + assertEquals("https", httpScheme.toString()); + } + + @Test + public void testIsReturningFalse() + { + HttpScheme httpScheme = HttpScheme.HTTP; + + assertFalse(httpScheme.is(",CPL@@4'U4p")); + } + + @Test + public void testIsWithNull() + { + HttpScheme httpScheme = HttpScheme.HTTPS; + + assertFalse(httpScheme.is(null)); + } + + @Test + public void testAsByteBuffer() + { + HttpScheme httpScheme = HttpScheme.WS; + ByteBuffer byteBuffer = httpScheme.asByteBuffer(); + + assertEquals("ws", httpScheme.asString()); + assertEquals("ws", httpScheme.toString()); + assertEquals(2, byteBuffer.capacity()); + assertEquals(2, byteBuffer.remaining()); + assertEquals(2, byteBuffer.limit()); + assertFalse(byteBuffer.hasArray()); + assertEquals(0, byteBuffer.position()); + assertTrue(byteBuffer.isReadOnly()); + assertFalse(byteBuffer.isDirect()); + assertTrue(byteBuffer.hasRemaining()); + } + +} diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java index 0d84e4e8c89..3ada26f378a 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/MimeTypesTest.java @@ -18,13 +18,13 @@ package org.eclipse.jetty.http; -import static org.hamcrest.CoreMatchers.is; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.hamcrest.MatcherAssert.assertThat; - -import org.junit.jupiter.api.Test; public class MimeTypesTest { diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormInputStreamTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormInputStreamTest.java index 6501c57ed2d..7fcfdc5a62b 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormInputStreamTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartFormInputStreamTest.java @@ -18,6 +18,24 @@ package org.eclipse.jetty.http; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import javax.servlet.MultipartConfigElement; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.Part; + +import org.eclipse.jetty.http.MultiPartFormInputStream.MultiPart; +import org.eclipse.jetty.util.IO; +import org.junit.jupiter.api.Test; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -33,24 +51,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collection; -import java.util.concurrent.TimeUnit; - -import javax.servlet.MultipartConfigElement; -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; -import javax.servlet.http.Part; - -import org.eclipse.jetty.http.MultiPartFormInputStream.MultiPart; -import org.eclipse.jetty.util.B64Code; -import org.eclipse.jetty.util.IO; -import org.junit.jupiter.api.Test; - /** * MultiPartInputStreamTest */ @@ -217,6 +217,7 @@ public class MultiPartFormInputStreamTest @Test public void testNoBody() + throws Exception { String body = ""; @@ -277,6 +278,7 @@ public class MultiPartFormInputStreamTest @Test public void testWhitespaceBodyWithCRLF() + throws Exception { String whitespace = " \n\n\n\r\n\r\n\r\n\r\n"; @@ -292,6 +294,7 @@ public class MultiPartFormInputStreamTest @Test public void testWhitespaceBody() + throws Exception { String whitespace = " "; @@ -400,6 +403,7 @@ public class MultiPartFormInputStreamTest @Test public void testRequestTooBig () + throws Exception { MultipartConfigElement config = new MultipartConfigElement(_dirname, 60, 100, 50); MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()), @@ -415,6 +419,7 @@ public class MultiPartFormInputStreamTest @Test public void testRequestTooBigThrowsErrorOnGetParts () + throws Exception { MultipartConfigElement config = new MultipartConfigElement(_dirname, 60, 100, 50); MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()), @@ -434,6 +439,7 @@ public class MultiPartFormInputStreamTest @Test public void testFileTooBig() + throws Exception { MultipartConfigElement config = new MultipartConfigElement(_dirname, 40, 1024, 30); MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()), @@ -449,6 +455,7 @@ public class MultiPartFormInputStreamTest @Test public void testFileTooBigThrowsErrorOnGetParts() + throws Exception { MultipartConfigElement config = new MultipartConfigElement(_dirname, 40, 1024, 30); MultiPartFormInputStream mpis = new MultiPartFormInputStream(new ByteArrayInputStream(_multi.getBytes()), @@ -550,6 +557,7 @@ public class MultiPartFormInputStreamTest @Test public void testCROnlyRequest() + throws Exception { String str = "--AaB03x\r" + "content-disposition: form-data; name=\"field1\"\r" + @@ -576,6 +584,7 @@ public class MultiPartFormInputStreamTest @Test public void testCRandLFMixRequest() + throws Exception { String str = "--AaB03x\r" + "content-disposition: form-data; name=\"field1\"\r" + @@ -862,7 +871,7 @@ public class MultiPartFormInputStreamTest "Content-Transfer-Encoding: base64\r\n" + "Content-Type: application/octet-stream\r\n" + "\r\n" + - B64Code.encode("hello jetty") + "\r\n" + + Base64.getEncoder().encodeToString("hello jetty".getBytes(ISO_8859_1)) + "\r\n" + "--AaB03x\r\n" + "Content-disposition: form-data; name=\"final\"\r\n" + "Content-Type: text/plain\r\n" + @@ -889,7 +898,7 @@ public class MultiPartFormInputStreamTest assertNotNull(p2); baos = new ByteArrayOutputStream(); IO.copy(p2.getInputStream(), baos); - assertEquals(B64Code.encode("hello jetty"), baos.toString("US-ASCII")); + assertEquals(Base64.getEncoder().encodeToString("hello jetty".getBytes(ISO_8859_1)), baos.toString("US-ASCII")); Part p3 = mpis.getPart("final"); assertNotNull(p3); diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedCSVTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedCSVTest.java index 3fe87fb1165..529c54b86c1 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedCSVTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/QuotedCSVTest.java @@ -18,17 +18,15 @@ package org.eclipse.jetty.http; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.isEmptyOrNullString; -import static org.hamcrest.Matchers.isEmptyString; -import static org.hamcrest.Matchers.nullValue; +import java.util.Collections; import org.hamcrest.Matchers; - import org.junit.jupiter.api.Test; -import java.util.Collections; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; public class QuotedCSVTest { @@ -161,7 +159,7 @@ public class QuotedCSVTest public void testJoin() { assertThat(QuotedCSV.join((String)null),nullValue()); - assertThat(QuotedCSV.join(Collections.emptyList()),isEmptyString()); + assertThat(QuotedCSV.join(Collections.emptyList()),is(emptyString())); assertThat(QuotedCSV.join(Collections.singletonList("hi")),is("hi")); assertThat(QuotedCSV.join("hi","ho"),is("hi, ho")); assertThat(QuotedCSV.join("h i","h,o"),is("\"h i\", \"h,o\"")); diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java index 5f4725061fb..cf2fed2dd33 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/SyntaxTest.java @@ -18,13 +18,13 @@ package org.eclipse.jetty.http; -import static org.hamcrest.CoreMatchers.allOf; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.fail; - import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.fail; + public class SyntaxTest { @Test diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ProxyProtocolTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ProxyProtocolTest.java index 684619f2e3a..c06a555aa21 100644 --- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ProxyProtocolTest.java +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ProxyProtocolTest.java @@ -18,13 +18,6 @@ package org.eclipse.jetty.http2.client; -import static org.hamcrest.CoreMatchers.instanceOf; -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.hamcrest.MatcherAssert.assertThat; - import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; @@ -32,7 +25,6 @@ import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -58,9 +50,14 @@ import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.TypeUtil; import org.junit.jupiter.api.AfterEach; - import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class ProxyProtocolTest { private Server server; diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java index c06bb884a71..490614e8394 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java @@ -39,7 +39,7 @@ public class MetaDataBuilder private HostPortHttpField _authority; private String _path; private long _contentLength=Long.MIN_VALUE; - private HttpFields _fields = new HttpFields(10); + private HttpFields _fields = new HttpFields(); private HpackException.StreamException _streamException; private boolean _request; private boolean _response; @@ -255,7 +255,7 @@ public class MetaDataBuilder } finally { - _fields = new HttpFields(Math.max(10, fields.size() + 5)); + _fields = new HttpFields(Math.max(16, fields.size() + 5)); _request = false; _response = false; _status = null; diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java index 141f871dfc4..6654b285835 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java @@ -18,12 +18,6 @@ package org.eclipse.jetty.http2.hpack; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; @@ -38,6 +32,12 @@ import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.util.BufferUtil; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + public class HpackTest { diff --git a/jetty-http2/http2-http-client-transport/pom.xml b/jetty-http2/http2-http-client-transport/pom.xml index 19e4a1b506f..59677412682 100644 --- a/jetty-http2/http2-http-client-transport/pom.xml +++ b/jetty-http2/http2-http-client-transport/pom.xml @@ -46,7 +46,6 @@ jetty-alpn-java-client ${project.version} - org.eclipse.jetty jetty-alpn-java-server @@ -70,11 +69,6 @@ ${project.version} test - - junit - junit - test - diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientConnectionFactoryOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientConnectionFactoryOverHTTP2.java new file mode 100644 index 00000000000..8ca10232392 --- /dev/null +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/ClientConnectionFactoryOverHTTP2.java @@ -0,0 +1,66 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2.client.http; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.HTTP2ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; + +public class ClientConnectionFactoryOverHTTP2 implements ClientConnectionFactory +{ + private final ClientConnectionFactory factory = new HTTP2ClientConnectionFactory(); + private final HTTP2Client client; + + public ClientConnectionFactoryOverHTTP2(HTTP2Client client) + { + this.client = client; + } + + @Override + public Connection newConnection(EndPoint endPoint, Map context) throws IOException + { + HTTPSessionListenerPromise listenerPromise = new HTTPSessionListenerPromise(context); + context.put(HTTP2ClientConnectionFactory.CLIENT_CONTEXT_KEY, client); + context.put(HTTP2ClientConnectionFactory.SESSION_LISTENER_CONTEXT_KEY, listenerPromise); + context.put(HTTP2ClientConnectionFactory.SESSION_PROMISE_CONTEXT_KEY, listenerPromise); + return factory.newConnection(endPoint, context); + } + + public static class H2 extends Info + { + public H2(HTTP2Client client) + { + super(List.of("h2"), new ClientConnectionFactoryOverHTTP2(client)); + } + } + + public static class H2C extends Info + { + public H2C(HTTP2Client client) + { + super(List.of("h2c"), new ClientConnectionFactoryOverHTTP2(client)); + } + } +} diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HTTPSessionListenerPromise.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HTTPSessionListenerPromise.java new file mode 100644 index 00000000000..6db3aad9b70 --- /dev/null +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HTTPSessionListenerPromise.java @@ -0,0 +1,139 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http2.client.http; + +import java.nio.channels.ClosedChannelException; +import java.util.Map; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicMarkableReference; + +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.http2.HTTP2Session; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.frames.GoAwayFrame; +import org.eclipse.jetty.http2.frames.SettingsFrame; +import org.eclipse.jetty.util.Promise; + +class HTTPSessionListenerPromise extends Session.Listener.Adapter implements Promise +{ + private final AtomicMarkableReference connection = new AtomicMarkableReference<>(null, false); + private final Map context; + + HTTPSessionListenerPromise(Map context) + { + this.context = context; + } + + @Override + public void succeeded(Session session) + { + // This method is invoked when the client preface + // is sent, but we want to succeed the nested + // promise when the server preface is received. + } + + @Override + public void failed(Throwable failure) + { + failConnectionPromise(failure); + } + + private HttpDestination destination() + { + return (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); + } + + @SuppressWarnings("unchecked") + private Promise connectionPromise() + { + return (Promise)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY); + } + + @Override + public void onSettings(Session session, SettingsFrame frame) + { + Map settings = frame.getSettings(); + if (settings.containsKey(SettingsFrame.MAX_CONCURRENT_STREAMS)) + { + HttpDestination destination = destination(); + if (destination instanceof HttpDestination.Multiplexed) + ((HttpDestination.Multiplexed)destination).setMaxRequestsPerConnection(settings.get(SettingsFrame.MAX_CONCURRENT_STREAMS)); + } + if (!connection.isMarked()) + onServerPreface(session); + } + + private void onServerPreface(Session session) + { + HttpConnectionOverHTTP2 connection = newHttpConnection(destination(), session); + if (this.connection.compareAndSet(null, connection, false, true)) + connectionPromise().succeeded(connection); + } + + protected HttpConnectionOverHTTP2 newHttpConnection(HttpDestination destination, Session session) + { + return new HttpConnectionOverHTTP2(destination, session); + } + + @Override + public void onClose(Session session, GoAwayFrame frame) + { + if (failConnectionPromise(new ClosedChannelException())) + return; + HttpConnectionOverHTTP2 connection = this.connection.getReference(); + if (connection != null) + onClose(connection, frame); + } + + void onClose(HttpConnectionOverHTTP2 connection, GoAwayFrame frame) + { + } + + @Override + public boolean onIdleTimeout(Session session) + { + long idleTimeout = ((HTTP2Session)session).getEndPoint().getIdleTimeout(); + if (failConnectionPromise(new TimeoutException("Idle timeout expired: " + idleTimeout + " ms"))) + return true; + HttpConnectionOverHTTP2 connection = this.connection.getReference(); + if (connection != null) + return connection.onIdleTimeout(idleTimeout); + return true; + } + + @Override + public void onFailure(Session session, Throwable failure) + { + if (failConnectionPromise(failure)) + return; + HttpConnectionOverHTTP2 connection = this.connection.getReference(); + if (connection != null) + connection.close(failure); + } + + private boolean failConnectionPromise(Throwable failure) + { + boolean result = connection.compareAndSet(null, null, false, true); + if (result) + connectionPromise().failed(failure); + return result; + } +} diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2.java index cacb023db7d..7efe42ccdc6 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2.java @@ -20,26 +20,20 @@ package org.eclipse.jetty.http2.client.http; import java.io.IOException; import java.net.InetSocketAddress; -import java.nio.channels.ClosedChannelException; import java.util.Map; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicMarkableReference; import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory; import org.eclipse.jetty.client.AbstractHttpClientTransport; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.MultiplexConnectionPool; -import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.MultiplexHttpDestination; import org.eclipse.jetty.client.ProxyConfiguration; -import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.http.HttpScheme; -import org.eclipse.jetty.http2.HTTP2Session; import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.HTTP2ClientConnectionFactory; import org.eclipse.jetty.http2.frames.GoAwayFrame; -import org.eclipse.jetty.http2.frames.SettingsFrame; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.Promise; @@ -49,13 +43,14 @@ import org.eclipse.jetty.util.annotation.ManagedObject; @ManagedObject("The HTTP/2 client transport") public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport { + private final ClientConnectionFactory connectionFactory = new HTTP2ClientConnectionFactory(); private final HTTP2Client client; - private ClientConnectionFactory connectionFactory; private boolean useALPN = true; public HttpClientTransportOverHTTP2(HTTP2Client client) { this.client = client; + addBean(client.getClientConnector(), false); setConnectionPoolFactory(destination -> { HttpClient httpClient = getHttpClient(); @@ -100,7 +95,6 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport } addBean(client); super.doStart(); - connectionFactory = new HTTP2ClientConnectionFactory(); } @Override @@ -111,9 +105,9 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport } @Override - public HttpDestination newHttpDestination(Origin origin) + public HttpDestination newHttpDestination(HttpDestination.Key key) { - return new HttpDestinationOverHTTP2(getHttpClient(), origin); + return new MultiplexHttpDestination(getHttpClient(), key); } @Override @@ -126,7 +120,7 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport SessionListenerPromise listenerPromise = new SessionListenerPromise(context); - HttpDestinationOverHTTP2 destination = (HttpDestinationOverHTTP2)context.get(HTTP_DESTINATION_CONTEXT_KEY); + HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); connect(address, destination.getClientConnectionFactory(), listenerPromise, listenerPromise, context); } @@ -141,7 +135,7 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport endPoint.setIdleTimeout(getHttpClient().getIdleTimeout()); ClientConnectionFactory factory = connectionFactory; - HttpDestinationOverHTTP2 destination = (HttpDestinationOverHTTP2)context.get(HTTP_DESTINATION_CONTEXT_KEY); + HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY); ProxyConfiguration.Proxy proxy = destination.getProxy(); boolean ssl = proxy == null ? HttpScheme.HTTPS.is(destination.getScheme()) : proxy.isSecure(); if (ssl && isUseALPN()) @@ -159,96 +153,23 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport connection.close(); } - private class SessionListenerPromise extends Session.Listener.Adapter implements Promise + private class SessionListenerPromise extends HTTPSessionListenerPromise { - private final AtomicMarkableReference connection = new AtomicMarkableReference<>(null, false); - private final Map context; - private SessionListenerPromise(Map context) { - this.context = context; + super(context); } @Override - public void succeeded(Session session) + protected HttpConnectionOverHTTP2 newHttpConnection(HttpDestination destination, Session session) { - // This method is invoked when the client preface - // is sent, but we want to succeed the nested - // promise when the server preface is received. + return HttpClientTransportOverHTTP2.this.newHttpConnection(destination, session); } @Override - public void failed(Throwable failure) + void onClose(HttpConnectionOverHTTP2 connection, GoAwayFrame frame) { - failConnectionPromise(failure); - } - - private HttpDestinationOverHTTP2 destination() - { - return (HttpDestinationOverHTTP2)context.get(HTTP_DESTINATION_CONTEXT_KEY); - } - - @SuppressWarnings("unchecked") - private Promise connectionPromise() - { - return (Promise)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY); - } - - @Override - public void onSettings(Session session, SettingsFrame frame) - { - Map settings = frame.getSettings(); - if (settings.containsKey(SettingsFrame.MAX_CONCURRENT_STREAMS)) - destination().setMaxRequestsPerConnection(settings.get(SettingsFrame.MAX_CONCURRENT_STREAMS)); - if (!connection.isMarked()) - onServerPreface(session); - } - - private void onServerPreface(Session session) - { - HttpConnectionOverHTTP2 connection = newHttpConnection(destination(), session); - if (this.connection.compareAndSet(null, connection, false, true)) - connectionPromise().succeeded(connection); - } - - @Override - public void onClose(Session session, GoAwayFrame frame) - { - if (failConnectionPromise(new ClosedChannelException())) - return; - HttpConnectionOverHTTP2 connection = this.connection.getReference(); - if (connection != null) - HttpClientTransportOverHTTP2.this.onClose(connection, frame); - } - - @Override - public boolean onIdleTimeout(Session session) - { - long idleTimeout = ((HTTP2Session)session).getEndPoint().getIdleTimeout(); - if (failConnectionPromise(new TimeoutException("Idle timeout expired: " + idleTimeout + " ms"))) - return true; - HttpConnectionOverHTTP2 connection = this.connection.getReference(); - if (connection != null) - return connection.onIdleTimeout(idleTimeout); - return true; - } - - @Override - public void onFailure(Session session, Throwable failure) - { - if (failConnectionPromise(failure)) - return; - HttpConnectionOverHTTP2 connection = this.connection.getReference(); - if (connection != null) - connection.close(failure); - } - - private boolean failConnectionPromise(Throwable failure) - { - boolean result = connection.compareAndSet(null, null, false, true); - if (result) - connectionPromise().failed(failure); - return result; + HttpClientTransportOverHTTP2.this.onClose(connection, frame); } } } diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpConnectionOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpConnectionOverHTTP2.java index 31e415ebce7..19f8151c2c3 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpConnectionOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpConnectionOverHTTP2.java @@ -51,6 +51,7 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S private final AtomicBoolean closed = new AtomicBoolean(); private final AtomicInteger sweeps = new AtomicInteger(); private final Session session; + private boolean recycleHttpChannels; public HttpConnectionOverHTTP2(HttpDestination destination, Session session) { @@ -63,8 +64,18 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S return session; } + public boolean isRecycleHttpChannels() + { + return recycleHttpChannels; + } + + public void setRecycleHttpChannels(boolean recycleHttpChannels) + { + this.recycleHttpChannels = recycleHttpChannels; + } + @Override - protected SendFailure send(HttpExchange exchange) + public SendFailure send(HttpExchange exchange) { HttpRequest request = exchange.getRequest(); request.version(HttpVersion.HTTP_2); @@ -99,7 +110,7 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S // Recycle only non-failed channels. if (channel.isFailed()) channel.destroy(); - else + else if (isRecycleHttpChannels()) idleChannels.offer(channel); } else diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java index e4cde5c10c5..0d91d5ea4e3 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java @@ -213,21 +213,21 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen @Override protected Action process() { - DataInfo dataInfo; + if (dataInfo != null) + { + dataInfo.callback.succeeded(); + if (dataInfo.frame.isEndStream()) + return Action.SUCCEEDED; + } + synchronized (this) { dataInfo = queue.poll(); } if (dataInfo == null) - { - DataInfo prevDataInfo = this.dataInfo; - if (prevDataInfo != null && prevDataInfo.frame.isEndStream()) - return Action.SUCCEEDED; return Action.IDLE; - } - this.dataInfo = dataInfo; ByteBuffer buffer = dataInfo.frame.getData(); if (buffer.hasRemaining()) responseContent(dataInfo.exchange, buffer, this); @@ -244,13 +244,6 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen ((Retainable)callback).retain(); } - @Override - public void succeeded() - { - dataInfo.callback.succeeded(); - super.succeeded(); - } - @Override protected void onCompleteSuccess() { @@ -263,6 +256,14 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen dataInfo.callback.failed(failure); responseFailure(failure); } + + @Override + public boolean reset() + { + queue.clear(); + dataInfo = null; + return super.reset(); + } } private static class DataInfo diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/AbstractTest.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/AbstractTest.java index b6cd168abe3..75790060d48 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/AbstractTest.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/AbstractTest.java @@ -65,7 +65,7 @@ public class AbstractTest protected void prepareClient() throws Exception { - client = new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client()), null); + client = new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client())); QueuedThreadPool clientExecutor = new QueuedThreadPool(); clientExecutor.setName("client"); client.setExecutor(clientExecutor); diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/DirectHTTP2OverTLSTest.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/DirectHTTP2OverTLSTest.java index 53e0f530f5c..bb34c209727 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/DirectHTTP2OverTLSTest.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/DirectHTTP2OverTLSTest.java @@ -18,10 +18,8 @@ package org.eclipse.jetty.http2.client.http; -import java.io.IOException; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -32,6 +30,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http2.HTTP2Cipher; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; @@ -68,7 +67,7 @@ public class DirectHTTP2OverTLSTest HttpConfiguration httpsConfig = new HttpConfiguration(); httpsConfig.addCustomizer(new SecureRequestCustomizer()); ConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); - ConnectionFactory ssl = new SslConnectionFactory(newSslContextFactory(), h2.getProtocol()); + ConnectionFactory ssl = new SslConnectionFactory(newServerSslContextFactory(), h2.getProtocol()); connector = new ServerConnector(server, 1, 1, ssl, h2); server.addConnector(connector); server.setHandler(handler); @@ -77,14 +76,14 @@ public class DirectHTTP2OverTLSTest private void startClient() throws Exception { + ClientConnector clientConnector = new ClientConnector(); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); - HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(new HTTP2Client()); + clientConnector.setExecutor(clientThreads); + clientConnector.setSslContextFactory(newClientSslContextFactory()); + HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(new HTTP2Client(clientConnector)); transport.setUseALPN(false); - SslContextFactory sslContextFactory = newSslContextFactory(); - sslContextFactory.setEndpointIdentificationAlgorithm(null); - client = new HttpClient(transport, sslContextFactory); - client.setExecutor(clientThreads); + client = new HttpClient(transport); client.start(); } @@ -97,14 +96,27 @@ public class DirectHTTP2OverTLSTest server.stop(); } - private SslContextFactory newSslContextFactory() + private SslContextFactory.Server newServerSslContextFactory() + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + configureSslContextFactory(sslContextFactory); + return sslContextFactory; + } + + private SslContextFactory.Client newClientSslContextFactory() + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + configureSslContextFactory(sslContextFactory); + sslContextFactory.setEndpointIdentificationAlgorithm(null); + return sslContextFactory; + } + + private void configureSslContextFactory(SslContextFactory sslContextFactory) { - SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setUseCipherSuitesOrder(true); sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); - return sslContextFactory; } @Test @@ -115,7 +127,7 @@ public class DirectHTTP2OverTLSTest start(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); } diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java index 00b92695f73..34ee90517f8 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java @@ -18,16 +18,6 @@ package org.eclipse.jetty.http2.client.http; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -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 java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; @@ -48,7 +38,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -76,6 +65,7 @@ import org.eclipse.jetty.http2.generator.Generator; import org.eclipse.jetty.http2.parser.ServerParser; import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory; import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.Request; @@ -87,13 +77,22 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class HttpClientTransportOverHTTP2Test extends AbstractTest { @Test public void testPropertiesAreForwarded() throws Exception { HTTP2Client http2Client = new HTTP2Client(); - HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client), null); + HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client)); Executor executor = new QueuedThreadPool(); httpClient.setExecutor(executor); httpClient.setConnectTimeout(13); @@ -133,11 +132,10 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest } }); - assertThrows(ExecutionException.class, ()->{ - client.newRequest("localhost", connector.getLocalPort()) - .onRequestCommit(request -> request.abort(new Exception("explicitly_aborted_by_test"))) - .send(); - }); + assertThrows(ExecutionException.class, () -> + client.newRequest("localhost", connector.getLocalPort()) + .onRequestCommit(request -> request.abort(new Exception("explicitly_aborted_by_test"))) + .send()); assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); } @@ -172,11 +170,10 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest } }); - assertThrows(ExecutionException.class, ()->{ - client.newRequest("localhost", connector.getLocalPort()) - .onResponseContent((response, buffer) -> response.abort(new Exception("explicitly_aborted_by_test"))) - .send(); - }); + assertThrows(ExecutionException.class, () -> + client.newRequest("localhost", connector.getLocalPort()) + .onResponseContent((response, buffer) -> response.abort(new Exception("explicitly_aborted_by_test"))) + .send()); assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); } @@ -186,7 +183,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest start(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); HttpVersion version = HttpVersion.fromString(request.getProtocol()); @@ -274,7 +271,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest lastStream.set(frame.getLastStreamId()); latch.countDown(); } - }, null); + }); QueuedThreadPool clientExecutor = new QueuedThreadPool(); clientExecutor.setName("client"); client.setExecutor(clientExecutor); @@ -312,7 +309,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest start(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); assertEquals(path, request.getRequestURI()); @@ -336,7 +333,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest start(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); assertEquals(path, request.getRequestURI()); @@ -381,12 +378,11 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest client.setIdleTimeout(idleTimeout); client.start(); - assertThrows(TimeoutException.class, ()->{ - client.newRequest("localhost", connector.getLocalPort()) - // Make sure the connection idle times out, not the stream. - .idleTimeout(2 * idleTimeout, TimeUnit.MILLISECONDS) - .send(); - }); + assertThrows(TimeoutException.class, () -> + client.newRequest("localhost", connector.getLocalPort()) + // Make sure the connection idle times out, not the stream. + .idleTimeout(2 * idleTimeout, TimeUnit.MILLISECONDS) + .send()); assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); } @@ -436,7 +432,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest sessions.add(session); return super.newHttpConnection(destination, session); } - }, null); + }); QueuedThreadPool clientExecutor = new QueuedThreadPool(); clientExecutor.setName("client"); client.setExecutor(clientExecutor); @@ -600,12 +596,13 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest @Test public void testExternalServer() throws Exception { - HTTP2Client http2Client = new HTTP2Client(); - SslContextFactory sslContextFactory = new SslContextFactory(); - HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client), sslContextFactory); + ClientConnector clientConnector = new ClientConnector(); + HTTP2Client http2Client = new HTTP2Client(clientConnector); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + clientConnector.setSslContextFactory(sslContextFactory); + HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client)); Executor executor = new QueuedThreadPool(); - httpClient.setExecutor(executor); - + clientConnector.setExecutor(executor); httpClient.start(); // ContentResponse response = httpClient.GET("https://http2.akamai.com/"); diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/MaxConcurrentStreamsTest.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/MaxConcurrentStreamsTest.java index 21225c96a95..4a0c29691c7 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/MaxConcurrentStreamsTest.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/MaxConcurrentStreamsTest.java @@ -206,7 +206,7 @@ public class MaxConcurrentStreamsTest extends AbstractTest } }, promise, context); } - }, null); + }); QueuedThreadPool clientExecutor = new QueuedThreadPool(); clientExecutor.setName("client"); client.setExecutor(clientExecutor); @@ -264,7 +264,7 @@ public class MaxConcurrentStreamsTest extends AbstractTest }); // The last exchange should remain in the queue. - HttpDestinationOverHTTP2 destination = (HttpDestinationOverHTTP2)client.getDestination("http", "localhost", connector.getLocalPort()); + HttpDestination destination = (HttpDestination)client.getDestination("http", "localhost", connector.getLocalPort()); assertEquals(1, destination.getHttpExchanges().size()); assertEquals(path, destination.getHttpExchanges().peek().getRequest().getPath()); diff --git a/jetty-http2/http2-http-client-transport/src/test/resources/jetty-logging.properties b/jetty-http2/http2-http-client-transport/src/test/resources/jetty-logging.properties index 287d28319e0..34929219c9d 100644 --- a/jetty-http2/http2-http-client-transport/src/test/resources/jetty-logging.properties +++ b/jetty-http2/http2-http-client-transport/src/test/resources/jetty-logging.properties @@ -1,4 +1,5 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +#org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.client.LEVEL=DEBUG org.eclipse.jetty.http2.hpack.LEVEL=INFO #org.eclipse.jetty.http2.LEVEL=DEBUG diff --git a/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml b/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml index 3d1b92e054b..8e8ac30ada3 100644 --- a/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml +++ b/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml b/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml index d58991e5233..f2a5140ba40 100644 --- a/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml +++ b/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java index f6f32350e9f..ad9b1416b51 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Base64; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -55,7 +56,6 @@ import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.CountingCallback; @@ -333,7 +333,7 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection if (settingsField == null) throw new BadMessageException("Missing " + HttpHeader.HTTP2_SETTINGS + " header"); String value = settingsField.getValue(); - final byte[] settings = B64Code.decodeRFC4648URL(value == null ? "" : value); + final byte[] settings = Base64.getUrlDecoder().decode(value == null ? "" : value); if (LOG.isDebugEnabled()) LOG.debug("{} settings {}",this,TypeUtil.toHexString(settings)); diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java index 9ae23123000..f2add0f087f 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java @@ -38,6 +38,7 @@ import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.NegotiatingServerConnection.CipherDiscriminator; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.Name; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -116,7 +117,7 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF public void onClose(Session session, GoAwayFrame frame, Callback callback) { String reason = frame.tryConvertPayload(); - if (reason != null && !reason.isEmpty()) + if (!StringUtil.isEmpty(reason)) reason = " (" + reason + ")"; getConnection().onSessionFailure(new EofException(String.format("Close %s/%s", ErrorCode.toString(frame.getError(), null), reason)), callback); } diff --git a/jetty-infinispan/infinispan-common/pom.xml b/jetty-infinispan/infinispan-common/pom.xml new file mode 100644 index 00000000000..0dc1b5b82a6 --- /dev/null +++ b/jetty-infinispan/infinispan-common/pom.xml @@ -0,0 +1,70 @@ + + + org.eclipse.jetty + infinispan-parent + 10.0.0-SNAPSHOT + + 4.0.0 + infinispan-common + Jetty :: Infinispan Session Manager Common + http://www.eclipse.org/jetty + + ${project.groupId}.infinispan.common + + + + + install + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + config + + + + + + + + + + org.infinispan + infinispan-core + ${infinispan.version} + true + + + org.infinispan.protostream + protostream + 4.2.2.Final + true + provided + + + org.eclipse.jetty + jetty-server + ${project.version} + + + org.infinispan + infinispan-client-hotrod + ${infinispan.version} + provided + + + org.infinispan + infinispan-remote-query-client + ${infinispan.version} + provided + + + diff --git a/jetty-infinispan/infinispan-common/src/main/config/etc/sessions/infinispan/infinispan-common.xml b/jetty-infinispan/infinispan-common/src/main/config/etc/sessions/infinispan/infinispan-common.xml new file mode 100644 index 00000000000..f62605ccf16 --- /dev/null +++ b/jetty-infinispan/infinispan-common/src/main/config/etc/sessions/infinispan/infinispan-common.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-infinispan/infinispan-common/src/main/config/modules/sessions/infinispan/infinispan-common.mod b/jetty-infinispan/infinispan-common/src/main/config/modules/sessions/infinispan/infinispan-common.mod new file mode 100644 index 00000000000..3c588cc1b5c --- /dev/null +++ b/jetty-infinispan/infinispan-common/src/main/config/modules/sessions/infinispan/infinispan-common.mod @@ -0,0 +1,20 @@ +[description] +Common to all infinispan modules + +[tags] +session + +[depend] +sessions + +[lib] +lib/infinispan-common-${jetty.version}.jar +lib/infinispan/*.jar + +[ini] +infinispan.version?=9.4.8.Final + +[license] +Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. +http://infinispan.org/ +http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionData.java b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionData.java similarity index 100% rename from jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionData.java rename to jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionData.java diff --git a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStore.java b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStore.java similarity index 66% rename from jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStore.java rename to jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStore.java index fa21de58ade..d35b2c0e071 100644 --- a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStore.java +++ b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStore.java @@ -1,6 +1,6 @@ // // ======================================================================== -// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 @@ -34,6 +34,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.infinispan.commons.api.BasicCache; + /** * InfinispanSessionDataStore * @@ -48,10 +49,14 @@ public class InfinispanSessionDataStore extends AbstractSessionDataStore /** * Clustered cache of sessions */ - private BasicCache _cache; + private BasicCache _cache; private int _infinispanIdleTimeoutSec; + + + + private QueryManager _queryManager; private boolean _passivating; @@ -61,7 +66,7 @@ public class InfinispanSessionDataStore extends AbstractSessionDataStore * * @return the cache */ - public BasicCache getCache() + public BasicCache getCache() { return _cache; } @@ -73,13 +78,26 @@ public class InfinispanSessionDataStore extends AbstractSessionDataStore * * @param cache the cache */ - public void setCache (BasicCache cache) + public void setCache (BasicCache cache) { this._cache = cache; } + public QueryManager getQueryManager() + { + return _queryManager; + } + + public void setQueryManager (QueryManager queryManager) + { + _queryManager = queryManager; + } + + /** + * @see org.eclipse.jetty.server.session.SessionDataStore#load(String) + */ @Override protected void doStart() throws Exception { @@ -90,13 +108,14 @@ public class InfinispanSessionDataStore extends AbstractSessionDataStore try { _passivating = false; - Class remoteClass = Thread.currentThread().getContextClassLoader().loadClass("org.infinispan.client.hotrod.RemoteCache"); + Class remoteClass = InfinispanSessionDataStore.class.getClassLoader().loadClass("org.infinispan.client.hotrod.RemoteCache"); if (remoteClass.isAssignableFrom(_cache.getClass())) _passivating = true; } catch (ClassNotFoundException e) { //expected if not running with remote cache + LOG.info("Hotrod classes not found, assuming infinispan in embedded mode"); } } @@ -139,67 +158,92 @@ public class InfinispanSessionDataStore extends AbstractSessionDataStore @Override public Set doGetExpired(Set candidates) { - if (candidates == null || candidates.isEmpty()) - return candidates; - + long now = System.currentTimeMillis(); Set expired = new HashSet<>(); - //TODO if there is NOT an idle timeout set on entries in infinispan, need to check other sessions - //that are not currently in the SessionDataStore (eg they've been passivated) - for (String candidate:candidates) + /* + * 1. Select sessions managed by this node for our context that have expired + */ + if(candidates != null) { - if (LOG.isDebugEnabled()) - LOG.debug("Checking expiry for candidate {}", candidate); - try + for (String candidate:candidates) { - SessionData sd = load(candidate); + if (LOG.isDebugEnabled()) + LOG.debug("Checking expiry for candidate {}", candidate); + try + { + SessionData sd = load(candidate); - //if the session no longer exists - if (sd == null) - { - expired.add(candidate); - if (LOG.isDebugEnabled()) - LOG.debug("Session {} does not exist in infinispan", candidate); - } - else - { - if (_context.getWorkerName().equals(sd.getLastNode())) + //if the session no longer exists + if (sd == null) { - //we are its manager, add it to the expired set if it is expired now - if ((sd.getExpiry() > 0 ) && sd.getExpiry() <= now) - { - expired.add(candidate); - if (LOG.isDebugEnabled()) - LOG.debug("Session {} managed by {} is expired", candidate, _context.getWorkerName()); - } + expired.add(candidate); + if (LOG.isDebugEnabled()) + LOG.debug("Session {} does not exist in infinispan", candidate); } else { - //if we are not the session's manager, only expire it iff: - // this is our first expiryCheck and the session expired a long time ago - //or - //the session expired at least one graceperiod ago - if (_lastExpiryCheckTime <=0) + if (_context.getWorkerName().equals(sd.getLastNode())) { - if ((sd.getExpiry() > 0 ) && sd.getExpiry() < (now - (1000L * (3 * _gracePeriodSec)))) + //we are its manager, add it to the expired set if it is expired now + if ((sd.getExpiry() > 0 ) && sd.getExpiry() <= now) + { expired.add(candidate); + if (LOG.isDebugEnabled()) + LOG.debug("Session {} managed by {} is expired", candidate, _context.getWorkerName()); + } } else { - if ((sd.getExpiry() > 0 ) && sd.getExpiry() < (now - (1000L * _gracePeriodSec))) - expired.add(candidate); + //if we are not the session's manager, only expire it iff: + // this is our first expiryCheck and the session expired a long time ago + //or + //the session expired at least one graceperiod ago + if (_lastExpiryCheckTime <=0) + { + if ((sd.getExpiry() > 0 ) && sd.getExpiry() < (now - (1000L * (3 * _gracePeriodSec)))) + expired.add(candidate); + } + else + { + if ((sd.getExpiry() > 0 ) && sd.getExpiry() < (now - (1000L * _gracePeriodSec))) + expired.add(candidate); + } } } } - } - catch (Exception e) - { - LOG.warn("Error checking if candidate {} is expired", candidate, e); + catch (Exception e) + { + LOG.warn("Error checking if candidate {} is expired", candidate, e); + } } } + + /* + * 2. Select sessions for any node or context that have expired + * at least 1 graceperiod since the last expiry check. If we haven't done previous expiry checks, then check + * those that have expired at least 3 graceperiod ago. + */ + if(_queryManager != null) + { + long upperBound = now; + if (_lastExpiryCheckTime <= 0) + upperBound = (now - (3*(1000L * _gracePeriodSec))); + else + upperBound = _lastExpiryCheckTime - (1000L * _gracePeriodSec); + + if (LOG.isDebugEnabled()) LOG.debug("{}- Pass 2: Searching for sessions expired before {}", _context.getWorkerName(), upperBound); + + for (String sessionId : _queryManager.queryExpiredSessions(upperBound)) + { + expired.add(sessionId); + if (LOG.isDebugEnabled()) LOG.debug ("{}- Found expired sessionId=",_context.getWorkerName(), sessionId); + } + } + return expired; } @@ -233,8 +277,8 @@ public class InfinispanSessionDataStore extends AbstractSessionDataStore { return _passivating; } - - + + @Override public boolean exists(String id) throws Exception diff --git a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStoreFactory.java b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStoreFactory.java similarity index 83% rename from jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStoreFactory.java rename to jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStoreFactory.java index 291b80a6dc9..241678057d3 100644 --- a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStoreFactory.java +++ b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStoreFactory.java @@ -20,8 +20,9 @@ package org.eclipse.jetty.session.infinispan; import org.eclipse.jetty.server.session.AbstractSessionDataStoreFactory; -import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.server.session.SessionData; import org.eclipse.jetty.server.session.SessionDataStore; +import org.eclipse.jetty.server.session.SessionHandler; import org.infinispan.commons.api.BasicCache; /** @@ -32,8 +33,8 @@ import org.infinispan.commons.api.BasicCache; public class InfinispanSessionDataStoreFactory extends AbstractSessionDataStoreFactory { int _infinispanIdleTimeoutSec; - BasicCache _cache; - + BasicCache _cache; + protected QueryManager _queryManager; /** * @return the infinispanIdleTimeoutSec @@ -62,6 +63,7 @@ public class InfinispanSessionDataStoreFactory extends AbstractSessionDataStoreF store.setInfinispanIdleTimeoutSec(getInfinispanIdleTimeoutSec()); store.setCache(getCache()); store.setSavePeriodSec(getSavePeriodSec()); + store.setQueryManager(getQueryManager()); return store; } @@ -70,7 +72,7 @@ public class InfinispanSessionDataStoreFactory extends AbstractSessionDataStoreF * * @return the cache */ - public BasicCache getCache() + public BasicCache getCache() { return _cache; } @@ -82,10 +84,20 @@ public class InfinispanSessionDataStoreFactory extends AbstractSessionDataStoreF * * @param cache the cache */ - public void setCache (BasicCache cache) + public void setCache (BasicCache cache) { this._cache = cache; } + public QueryManager getQueryManager() + { + return _queryManager; + } + + public void setQueryManager(QueryManager queryManager) + { + _queryManager = queryManager; + } + } diff --git a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionLegacyConverter.java b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionLegacyConverter.java similarity index 100% rename from jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionLegacyConverter.java rename to jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionLegacyConverter.java diff --git a/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/NullQueryManagerFactory.java b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/NullQueryManagerFactory.java new file mode 100644 index 00000000000..764ac2405bf --- /dev/null +++ b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/NullQueryManagerFactory.java @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.session.infinispan; + +import org.eclipse.jetty.server.session.SessionData; +import org.infinispan.commons.api.BasicCache; + +/** + * NullQueryManagerFactory + * + * Trivial impl of the QueryManagerFactory that does not support doing queries. + */ +public class NullQueryManagerFactory implements QueryManagerFactory +{ + @Override + public QueryManager getQueryManager(BasicCache cache) + { + return null; + } +} diff --git a/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/QueryManager.java b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/QueryManager.java new file mode 100644 index 00000000000..33711c3a506 --- /dev/null +++ b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/QueryManager.java @@ -0,0 +1,27 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.session.infinispan; + +import java.util.Set; + +public interface QueryManager +{ + Set queryExpiredSessions(); + Set queryExpiredSessions(long currentTime); +} diff --git a/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/QueryManagerFactory.java b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/QueryManagerFactory.java new file mode 100644 index 00000000000..6475614a290 --- /dev/null +++ b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/QueryManagerFactory.java @@ -0,0 +1,27 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.session.infinispan; + +import org.eclipse.jetty.server.session.SessionData; +import org.infinispan.commons.api.BasicCache; + +public interface QueryManagerFactory +{ + public QueryManager getQueryManager(BasicCache cache); +} diff --git a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/SessionDataMarshaller.java b/jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/SessionDataMarshaller.java similarity index 100% rename from jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/SessionDataMarshaller.java rename to jetty-infinispan/infinispan-common/src/main/java/org/eclipse/jetty/session/infinispan/SessionDataMarshaller.java diff --git a/jetty-infinispan/src/main/resources/session.proto b/jetty-infinispan/infinispan-common/src/main/resources/session.proto similarity index 100% rename from jetty-infinispan/src/main/resources/session.proto rename to jetty-infinispan/infinispan-common/src/main/resources/session.proto diff --git a/jetty-infinispan/infinispan-embedded-query/pom.xml b/jetty-infinispan/infinispan-embedded-query/pom.xml new file mode 100644 index 00000000000..8ee44503830 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded-query/pom.xml @@ -0,0 +1,125 @@ + + + org.eclipse.jetty + infinispan-parent + 10.0.0-SNAPSHOT + + 4.0.0 + infinispan-embedded-query + Jetty :: Infinispan Session Manager Embedded with Querying + http://www.eclipse.org/jetty + + ${project.groupId}.infinispan.embedded.query + + + install + + + org.apache.maven.plugins + maven-dependency-plugin + + + build-deps-file + generate-resources + + list + + + false + ${project.build.directory}/deps.txt + true + org.eclipse.jetty,javax.servlet,org.slf4j + true + runtime + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + process-deps + process-resources + + run + + + + + + + + + + process-mod + process-resources + + run + + + + + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + src/main/assembly/config.xml + + + + + + + + + + org.eclipse.jetty + infinispan-common + ${project.version} + + + org.infinispan + infinispan-core + + + org.infinispan + infinispan-commons + + + + + org.infinispan + infinispan-query + ${infinispan.version} + + + org.eclipse.jetty.toolchain + jetty-test-helper + test + + + diff --git a/jetty-infinispan/infinispan-embedded-query/src/main/assembly/config.xml b/jetty-infinispan/infinispan-embedded-query/src/main/assembly/config.xml new file mode 100644 index 00000000000..d9db9a74ef2 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded-query/src/main/assembly/config.xml @@ -0,0 +1,27 @@ + + + config + false + + jar + + + + src/main/config-template + + + ** + + + **/infinispan-embedded-query-libs.mod + + + + target + modules/sessions/infinispan/embedded + + infinispan-embedded-query-libs.mod + + + + diff --git a/jetty-infinispan/infinispan-embedded-query/src/main/config-template/etc/sessions/infinispan/infinispan-embedded-query.xml b/jetty-infinispan/infinispan-embedded-query/src/main/config-template/etc/sessions/infinispan/infinispan-embedded-query.xml new file mode 100644 index 00000000000..662fbb8e14f --- /dev/null +++ b/jetty-infinispan/infinispan-embedded-query/src/main/config-template/etc/sessions/infinispan/infinispan-embedded-query.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + expiry + + + + + + + + + + + + + + + + /etc/infinispan.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jetty-query-sessions + + + + + + + + + + diff --git a/jetty-infinispan/infinispan-embedded-query/src/main/config-template/modules/infinispan-embedded-query.mod b/jetty-infinispan/infinispan-embedded-query/src/main/config-template/modules/infinispan-embedded-query.mod new file mode 100644 index 00000000000..60607ab4f5f --- /dev/null +++ b/jetty-infinispan/infinispan-embedded-query/src/main/config-template/modules/infinispan-embedded-query.mod @@ -0,0 +1,20 @@ +[description] +Enables querying with the Infinispan cache + +[tags] +session + +[provides] +infinispan-embedded + +[depends] +sessions/infinispan/embedded/infinispan-embedded-query-libs + +[lib] +lib/infinispan/*.jar +lib/infinispan-embedded-query-${jetty.version}.jar + +[xml] +etc/sessions/infinispan/infinispan-embedded-query.xml +etc/sessions/infinispan/infinispan-common.xml + diff --git a/jetty-infinispan/infinispan-embedded-query/src/main/config-template/modules/sessions/infinispan/embedded/infinispan-embedded-query-libs.mod b/jetty-infinispan/infinispan-embedded-query/src/main/config-template/modules/sessions/infinispan/embedded/infinispan-embedded-query-libs.mod new file mode 100644 index 00000000000..05f4038a0d2 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded-query/src/main/config-template/modules/sessions/infinispan/embedded/infinispan-embedded-query-libs.mod @@ -0,0 +1,14 @@ +[description] +The Infinispan query libraries + +[tags] +3rdparty +infinispan + + +[license] +Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. +http://infinispan.org/ +http://www.apache.org/licenses/LICENSE-2.0.html + + diff --git a/jetty-infinispan/infinispan-embedded-query/src/main/java/org/eclipse/jetty/session/infinispan/EmbeddedQueryManager.java b/jetty-infinispan/infinispan-embedded-query/src/main/java/org/eclipse/jetty/session/infinispan/EmbeddedQueryManager.java new file mode 100644 index 00000000000..4b5a23dfa27 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded-query/src/main/java/org/eclipse/jetty/session/infinispan/EmbeddedQueryManager.java @@ -0,0 +1,42 @@ +package org.eclipse.jetty.session.infinispan; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jetty.server.session.SessionData; +import org.infinispan.Cache; +import org.infinispan.query.Search; +import org.infinispan.query.dsl.Query; +import org.infinispan.query.dsl.QueryFactory; + +public class EmbeddedQueryManager implements QueryManager +{ + private Cache _cache; + + public EmbeddedQueryManager(Cache cache) + { + _cache = cache; + } + + @Override + public Set queryExpiredSessions(long time) + { + QueryFactory qf = Search.getQueryFactory(_cache); + Query q = qf.from(SessionData.class).select("id").having("expiry").lte(time).build(); + + List list = q.list(); + Set ids = new HashSet<>(); + for(Object[] sl : list) + ids.add((String)sl[0]); + return ids; + } + + + @Override + public Set queryExpiredSessions() + { + return queryExpiredSessions(System.currentTimeMillis()); + } + +} diff --git a/jetty-infinispan/infinispan-embedded-query/src/main/java/org/eclipse/jetty/session/infinispan/EmbeddedQueryManagerFactory.java b/jetty-infinispan/infinispan-embedded-query/src/main/java/org/eclipse/jetty/session/infinispan/EmbeddedQueryManagerFactory.java new file mode 100644 index 00000000000..f33f4dcd8c3 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded-query/src/main/java/org/eclipse/jetty/session/infinispan/EmbeddedQueryManagerFactory.java @@ -0,0 +1,19 @@ +package org.eclipse.jetty.session.infinispan; + +import org.eclipse.jetty.server.session.SessionData; +import org.infinispan.Cache; +import org.infinispan.commons.api.BasicCache; + +public class EmbeddedQueryManagerFactory implements QueryManagerFactory +{ + + @Override + public QueryManager getQueryManager(BasicCache cache) + { + if (!(cache instanceof Cache)) + throw new IllegalArgumentException("Argument was not of type Cache"); + + return new EmbeddedQueryManager((Cache)cache); + } + +} diff --git a/jetty-infinispan/infinispan-embedded-query/src/test/java/org/eclipse/jetty/server/session/infinispan/EmbeddedQueryManagerTest.java b/jetty-infinispan/infinispan-embedded-query/src/test/java/org/eclipse/jetty/server/session/infinispan/EmbeddedQueryManagerTest.java new file mode 100644 index 00000000000..107df7da347 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded-query/src/test/java/org/eclipse/jetty/server/session/infinispan/EmbeddedQueryManagerTest.java @@ -0,0 +1,111 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server.session.infinispan; + +import java.lang.annotation.ElementType; +import java.util.HashSet; +import java.util.Properties; +import java.util.Random; +import java.util.Set; + +import org.eclipse.jetty.server.session.SessionData; +import org.eclipse.jetty.session.infinispan.EmbeddedQueryManager; +import org.eclipse.jetty.session.infinispan.QueryManager; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.hibernate.search.cfg.Environment; +import org.hibernate.search.cfg.SearchMapping; +import org.infinispan.Cache; +import org.infinispan.configuration.cache.Configuration; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.cache.Index; +import org.infinispan.configuration.global.GlobalConfigurationBuilder; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class EmbeddedQueryManagerTest +{ + public static final String DEFAULT_CACHE_NAME = "session_test_cache"; + + + @Test + public void test() throws Exception + { + + String _name = DEFAULT_CACHE_NAME+System.currentTimeMillis(); + EmbeddedCacheManager _manager; + + _manager = new DefaultCacheManager(new GlobalConfigurationBuilder().globalJmxStatistics().allowDuplicateDomains(true).build()); + + //TODO verify that this is being indexed properly, if you change expiry to something that is not a valid field it still passes the tests + SearchMapping mapping = new SearchMapping(); + mapping.entity(SessionData.class).indexed().providedId().property("expiry", ElementType.FIELD).field(); + Properties properties = new Properties(); + properties.put(Environment.MODEL_MAPPING, mapping); + properties.put("hibernate.search.default.indexBase", MavenTestingUtils.getTargetTestingDir().getAbsolutePath()); + + Configuration dcc = _manager.getDefaultCacheConfiguration(); + ConfigurationBuilder b = new ConfigurationBuilder(); + if (dcc != null) + b = b.read(dcc); + + b.indexing().index(Index.ALL).addIndexedEntity(SessionData.class).withProperties(properties); + Configuration c = b.build(); + + _manager.defineConfiguration(_name, c); + Cache _cache = _manager.getCache(_name); + + //put some sessions into the cache + int numSessions = 10; + long currentTime = 500; + int maxExpiryTime = 1000; + Set expiredSessions = new HashSet<>(); + Random r = new Random(); + + for (int i=0; i queryResult = qm.queryExpiredSessions(currentTime); + + // Check that the result is correct + assertEquals(expiredSessions.size(), queryResult.size()); + for (String s : expiredSessions) + { + assertTrue(queryResult.contains(s)); + } + } +} diff --git a/jetty-infinispan/infinispan-embedded/pom.xml b/jetty-infinispan/infinispan-embedded/pom.xml new file mode 100644 index 00000000000..e20605c1461 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded/pom.xml @@ -0,0 +1,112 @@ + + + org.eclipse.jetty + infinispan-parent + 10.0.0-SNAPSHOT + + 4.0.0 + infinispan-embedded + pom + Jetty :: Infinispan Session Manager Embedded + http://www.eclipse.org/jetty + + ${project.groupId}.infinispan.embedded + + + install + + + org.apache.maven.plugins + maven-dependency-plugin + + + build-deps-file + generate-resources + + list + + + false + ${project.build.directory}/deps.txt + true + org.eclipse.jetty,javax.servlet,org.slf4j + true + runtime + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + process-deps + process-resources + + run + + + + + + + + + + process-mod + process-resources + + run + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + src/main/assembly/config.xml + + + + + + + + + + org.eclipse.jetty + infinispan-common + ${project.version} + + + org.infinispan + infinispan-core + ${infinispan.version} + + + diff --git a/jetty-infinispan/infinispan-embedded/src/main/assembly/config.xml b/jetty-infinispan/infinispan-embedded/src/main/assembly/config.xml new file mode 100644 index 00000000000..491f84e6116 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded/src/main/assembly/config.xml @@ -0,0 +1,27 @@ + + + config + false + + jar + + + + src/main/config-templates + + + ** + + + **/infinispan-embedded-libs.mod + + + + target + modules/sessions/infinispan/embedded + + infinispan-embedded-libs.mod + + + + diff --git a/jetty-infinispan/infinispan-embedded/src/main/config-templates/etc/sessions/infinispan/infinispan-embedded.xml b/jetty-infinispan/infinispan-embedded/src/main/config-templates/etc/sessions/infinispan/infinispan-embedded.xml new file mode 100644 index 00000000000..e2efc697e69 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded/src/main/config-templates/etc/sessions/infinispan/infinispan-embedded.xml @@ -0,0 +1,17 @@ + + + + + + + + + + /etc/infinispan.xml + + + + + + + diff --git a/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/infinispan-embedded.mod b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/infinispan-embedded.mod new file mode 100644 index 00000000000..8d5d0cfa25c --- /dev/null +++ b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/infinispan-embedded.mod @@ -0,0 +1,10 @@ +[description] +Setup infinispan embedded without querying + +[tags] +session + +[xml] +etc/sessions/infinispan/infinispan-embedded.xml +etc/sessions/infinispan/infinispan-common.xml + diff --git a/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/session-store-infinispan-embedded.mod b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/session-store-infinispan-embedded.mod new file mode 100644 index 00000000000..1d2e9da7b87 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/session-store-infinispan-embedded.mod @@ -0,0 +1,24 @@ +[description] +Enables session data store in a local Infinispan cache + +[tags] +session + +[provides] +session-store + +[depend] +sessions/infinispan/infinispan-common +infinispan-embedded +sessions/infinispan/embedded/infinispan-embedded-libs + +[files] +basehome:modules/sessions/infinispan/embedded/infinispan.xml|etc/infinispan.xml + +[ini] +infinispan.version?=9.4.8.Final + +[ini-template] +#jetty.session.infinispan.idleTimeout.seconds=0 +#jetty.session.gracePeriod.seconds=3600 +#jetty.session.savePeriod.seconds=0 diff --git a/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-libs.mod b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-libs.mod new file mode 100644 index 00000000000..c8c02f8cae2 --- /dev/null +++ b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-libs.mod @@ -0,0 +1,16 @@ +[description] +The Infinispan embedded libraries + +[tags] +3rdparty +infinispan + +[depends] +sessions/infinispan/embedded/infinispan-embedded-serverclasses + +[license] +Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. +http://infinispan.org/ +http://www.apache.org/licenses/LICENSE-2.0.html + + diff --git a/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-serverclasses.mod b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-serverclasses.mod new file mode 100644 index 00000000000..b36937e1c6c --- /dev/null +++ b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan-embedded-serverclasses.mod @@ -0,0 +1,12 @@ +[description] +Hides Infinispan classes from webapp. + +[tags] +session +3rdparty +infinispan + + +[ini] +## Hide the infinispan libraries from deployed webapps +jetty.webapp.addServerClasses+=,${jetty.base.uri}/lib/infinispan/ diff --git a/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan.xml b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan.xml new file mode 100644 index 00000000000..07d8ca214cb --- /dev/null +++ b/jetty-infinispan/infinispan-embedded/src/main/config-templates/modules/sessions/infinispan/embedded/infinispan.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/jetty-infinispan/infinispan-remote-query/pom.xml b/jetty-infinispan/infinispan-remote-query/pom.xml new file mode 100644 index 00000000000..151716c6f5b --- /dev/null +++ b/jetty-infinispan/infinispan-remote-query/pom.xml @@ -0,0 +1,164 @@ + + + org.eclipse.jetty + infinispan-parent + 10.0.0-SNAPSHOT + + 4.0.0 + infinispan-remote-query + Jetty :: Infinispan Session Manager Remote + http://www.eclipse.org/jetty + + ${project.groupId}.infinispan.remote.query + + + install + + + org.apache.maven.plugins + maven-dependency-plugin + + + build-deps-file + generate-resources + + list + + + false + ${project.build.directory}/deps.txt + true + org.eclipse.jetty,javax.servlet + true + runtime + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + process-deps + process-resources + + run + + + + + + + + + + + process-mod + process-resources + + run + + + + + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + src/main/assembly/config.xml + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + + org.eclipse.jetty + infinispan-common + ${project.version} + + + org.infinispan + infinispan-core + + + org.infinispan + infinispan-commons + + + + + org.infinispan + infinispan-query + ${infinispan.version} + + + org.infinispan + infinispan-client-hotrod + ${infinispan.version} + + + org.infinispan + infinispan-remote-query-client + ${infinispan.version} + + + + + remote + + + hotrod.enabled + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + + + + diff --git a/jetty-infinispan/infinispan-remote-query/src/main/assembly/config.xml b/jetty-infinispan/infinispan-remote-query/src/main/assembly/config.xml new file mode 100644 index 00000000000..270dcfad382 --- /dev/null +++ b/jetty-infinispan/infinispan-remote-query/src/main/assembly/config.xml @@ -0,0 +1,27 @@ + + + config + false + + jar + + + + src/main/config-template + + + ** + + + **/infinispan-remote-query-libs.mod + + + + target + modules/sessions/infinispan/remote + + infinispan-remote-query-libs.mod + + + + diff --git a/jetty-infinispan/infinispan-remote-query/src/main/config-template/etc/sessions/infinispan/infinispan-remote-query.xml b/jetty-infinispan/infinispan-remote-query/src/main/config-template/etc/sessions/infinispan/infinispan-remote-query.xml new file mode 100644 index 00000000000..c6ee114ebf4 --- /dev/null +++ b/jetty-infinispan/infinispan-remote-query/src/main/config-template/etc/sessions/infinispan/infinispan-remote-query.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + org.eclipse.jetty.server.session.SessionData + + + + expiry + + + + + + + + + + + + + /resources/hotrod-client.properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /session.proto + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/infinispan-remote-query.mod b/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/infinispan-remote-query.mod new file mode 100644 index 00000000000..7cb7c40ec30 --- /dev/null +++ b/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/infinispan-remote-query.mod @@ -0,0 +1,22 @@ +[description] +Enables querying with a remote Infinispan cache + +[tags] +session + +[provides] +infinispan-remote + +[depends] +sessions/infinispan/remote/infinispan-remote-query-libs + +[files] +basehome:modules/sessions/infinispan/remote/other_proto_marshallers.xml|etc/other_proto_marshallers.xml + +[lib] +lib/infinispan-remote-query-${jetty.version}.jar + +[xml] +etc/sessions/infinispan/infinispan-remote-query.xml +etc/other_proto_marshallers.xml +etc/sessions/infinispan/infinispan-common.xml diff --git a/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-query-libs.mod b/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-query-libs.mod new file mode 100644 index 00000000000..50cb76896f8 --- /dev/null +++ b/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-query-libs.mod @@ -0,0 +1,13 @@ +[description] +The Infinispan remote query libraries + +[tags] +3rdparty +infinispan + +[license] +Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. +http://infinispan.org/ +http://www.apache.org/licenses/LICENSE-2.0.html + + diff --git a/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/sessions/infinispan/remote/other_proto_marshallers.xml b/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/sessions/infinispan/remote/other_proto_marshallers.xml new file mode 100644 index 00000000000..389202eb511 --- /dev/null +++ b/jetty-infinispan/infinispan-remote-query/src/main/config-template/modules/sessions/infinispan/remote/other_proto_marshallers.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/jetty-infinispan/infinispan-remote-query/src/main/java/org/eclipse/jetty/session/infinispan/RemoteQueryManager.java b/jetty-infinispan/infinispan-remote-query/src/main/java/org/eclipse/jetty/session/infinispan/RemoteQueryManager.java new file mode 100644 index 00000000000..8a79328a72f --- /dev/null +++ b/jetty-infinispan/infinispan-remote-query/src/main/java/org/eclipse/jetty/session/infinispan/RemoteQueryManager.java @@ -0,0 +1,68 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.session.infinispan; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jetty.server.session.SessionData; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.Search; +import org.infinispan.query.dsl.Query; +import org.infinispan.query.dsl.QueryFactory; + +/** + * RemoteQueryManager + * + * A QueryManager impl that supports doing queries against remote infinispan server. + * + */ +public class RemoteQueryManager implements QueryManager +{ + private RemoteCache _cache; + + public RemoteQueryManager(RemoteCache cache) + { + _cache = cache; + } + + @Override + public Set queryExpiredSessions(long time) + { + // TODO can the QueryFactory be created only once + QueryFactory qf = Search.getQueryFactory(_cache); + Query q = qf.from(InfinispanSessionData.class).select("id").having("expiry").lte(time).build(); + + List list = q.list(); + Set ids = new HashSet<>(); + for(Object[] sl : list) + ids.add((String)sl[0]); + + return ids; + } + + + @Override + public Set queryExpiredSessions() + { + return queryExpiredSessions(System.currentTimeMillis()); + } + +} diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpDestinationOverFCGI.java b/jetty-infinispan/infinispan-remote-query/src/main/java/org/eclipse/jetty/session/infinispan/RemoteQueryManagerFactory.java similarity index 56% rename from jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpDestinationOverFCGI.java rename to jetty-infinispan/infinispan-remote-query/src/main/java/org/eclipse/jetty/session/infinispan/RemoteQueryManagerFactory.java index 9a296c1bf13..21bea54b821 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpDestinationOverFCGI.java +++ b/jetty-infinispan/infinispan-remote-query/src/main/java/org/eclipse/jetty/session/infinispan/RemoteQueryManagerFactory.java @@ -16,25 +16,23 @@ // ======================================================================== // -package org.eclipse.jetty.fcgi.client.http; +package org.eclipse.jetty.session.infinispan; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpExchange; -import org.eclipse.jetty.client.Origin; -import org.eclipse.jetty.client.PoolingHttpDestination; -import org.eclipse.jetty.client.SendFailure; -import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.server.session.SessionData; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.commons.api.BasicCache; -public class HttpDestinationOverFCGI extends PoolingHttpDestination +public class RemoteQueryManagerFactory implements QueryManagerFactory { - public HttpDestinationOverFCGI(HttpClient client, Origin origin) - { - super(client, origin); - } @Override - protected SendFailure send(Connection connection, HttpExchange exchange) + public QueryManager getQueryManager(BasicCache cache) { - return ((HttpConnectionOverFCGI)connection).send(exchange); + System.err.println(cache.getClass().getName()); + if (!RemoteCache.class.isAssignableFrom(cache.getClass())) + throw new IllegalArgumentException("Argument is not of type RemoteCache"); + + return new RemoteQueryManager((RemoteCache)cache); } + } diff --git a/jetty-infinispan/infinispan-remote-query/src/test/java/org/eclipse/jetty/server/session/infinispan/RemoteQueryManagerTest.java b/jetty-infinispan/infinispan-remote-query/src/test/java/org/eclipse/jetty/server/session/infinispan/RemoteQueryManagerTest.java new file mode 100644 index 00000000000..d3e4e08b62e --- /dev/null +++ b/jetty-infinispan/infinispan-remote-query/src/test/java/org/eclipse/jetty/server/session/infinispan/RemoteQueryManagerTest.java @@ -0,0 +1,126 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server.session.infinispan; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.lang.annotation.ElementType; +import java.util.HashSet; +import java.util.Properties; +import java.util.Random; +import java.util.Set; + +import org.eclipse.jetty.server.session.SessionData; +import org.eclipse.jetty.session.infinispan.QueryManager; +import org.eclipse.jetty.session.infinispan.RemoteQueryManager; +import org.eclipse.jetty.session.infinispan.SessionDataMarshaller; +import org.eclipse.jetty.util.IO; +import org.hibernate.search.cfg.Environment; +import org.hibernate.search.cfg.SearchMapping; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.RemoteCacheManager; +import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; +import org.infinispan.client.hotrod.marshall.ProtoStreamMarshaller; +import org.infinispan.protostream.FileDescriptorSource; +import org.infinispan.protostream.SerializationContext; +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; + +public class RemoteQueryManagerTest +{ + public static final String DEFAULT_CACHE_NAME = "remote-session-test"; + + @Test + public void test() throws Exception + { + SearchMapping mapping = new SearchMapping(); + mapping.entity(SessionData.class).indexed().providedId().property("expiry", ElementType.FIELD).field(); + + Properties properties = new Properties(); + properties.put(Environment.MODEL_MAPPING, mapping); + + ConfigurationBuilder clientBuilder = new ConfigurationBuilder(); + clientBuilder.withProperties(properties).addServer().host("127.0.0.1").marshaller(new ProtoStreamMarshaller()); + + RemoteCacheManager remoteCacheManager = new RemoteCacheManager(clientBuilder.build()); + + + FileDescriptorSource fds = new FileDescriptorSource(); + fds.addProtoFiles("/session.proto"); + + SerializationContext serCtx = ProtoStreamMarshaller.getSerializationContext(remoteCacheManager); + serCtx.registerProtoFiles(fds); + serCtx.registerMarshaller(new SessionDataMarshaller()); + + RemoteCache _cache = remoteCacheManager.getCache(DEFAULT_CACHE_NAME); + + + ByteArrayOutputStream baos; + try(InputStream is = RemoteQueryManagerTest.class.getClassLoader().getResourceAsStream("session.proto")) + { + if (is == null) + throw new IllegalStateException("inputstream is null"); + + baos = new ByteArrayOutputStream(); + IO.copy(is, baos); + } + + String content = baos.toString("UTF-8"); + remoteCacheManager.getCache("___protobuf_metadata").put("session.proto", content); + + //put some sessions into the remote cache + int numSessions = 10; + long currentTime = 500; + int maxExpiryTime = 1000; + Set expiredSessions = new HashSet<>(); + Random r = new Random(); + + for(int i=0; i queryResult = qm.queryExpiredSessions(currentTime); + + // Check that the result is correct + assertEquals(expiredSessions.size(), queryResult.size()); + for(String s : expiredSessions) + { + assertTrue(queryResult.contains(s)); + } + } +} diff --git a/jetty-infinispan/infinispan-remote/pom.xml b/jetty-infinispan/infinispan-remote/pom.xml new file mode 100644 index 00000000000..19917c3b74f --- /dev/null +++ b/jetty-infinispan/infinispan-remote/pom.xml @@ -0,0 +1,130 @@ + + + org.eclipse.jetty + infinispan-parent + 10.0.0-SNAPSHOT + + 4.0.0 + infinispan-remote + pom + Jetty :: Infinispan Session Manager Remote + http://www.eclipse.org/jetty + + ${project.groupId}.infinispan.remote + + + install + + + org.apache.maven.plugins + maven-dependency-plugin + + + build-deps-file + generate-resources + + list + + + false + ${project.build.directory}/deps.txt + true + org.eclipse.jetty,javax.servlet + true + provided + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + process-deps + process-resources + + run + + + + + + + + + + + process-mod + process-resources + + run + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + package + + single + + + + src/main/assembly/config.xml + + + + + + + + + + org.eclipse.jetty + infinispan-common + ${project.version} + + + org.infinispan + infinispan-client-hotrod + ${infinispan.version} + provided + + + org.infinispan + infinispan-remote-query-client + ${infinispan.version} + provided + + + org.infinispan.protostream + protostream + 4.2.2.Final + provided + + + diff --git a/jetty-infinispan/infinispan-remote/src/main/assembly/config.xml b/jetty-infinispan/infinispan-remote/src/main/assembly/config.xml new file mode 100644 index 00000000000..802f815d6aa --- /dev/null +++ b/jetty-infinispan/infinispan-remote/src/main/assembly/config.xml @@ -0,0 +1,27 @@ + + + config + false + + jar + + + + src/main/config-template + + + ** + + + **/infinispan-remote-libs.mod + + + + target + modules/sessions/infinispan/remote + + infinispan-remote-libs.mod + + + + diff --git a/jetty-infinispan/src/main/config/etc/sessions/infinispan/remote.xml b/jetty-infinispan/infinispan-remote/src/main/config-template/etc/sessions/infinispan/infinispan-remote.xml similarity index 60% rename from jetty-infinispan/src/main/config/etc/sessions/infinispan/remote.xml rename to jetty-infinispan/infinispan-remote/src/main/config-template/etc/sessions/infinispan/infinispan-remote.xml index 356cd48b29c..527a30c53f9 100644 --- a/jetty-infinispan/src/main/config/etc/sessions/infinispan/remote.xml +++ b/jetty-infinispan/infinispan-remote/src/main/config-template/etc/sessions/infinispan/infinispan-remote.xml @@ -1,25 +1,24 @@ - + - - - - + - - + - + /resources/hotrod-client.properties + + + @@ -32,14 +31,20 @@ - - + + + + + + + + - + @@ -61,28 +66,19 @@ + - - + + - - - + - - - - - - - - - - + + diff --git a/jetty-infinispan/infinispan-remote/src/main/config-template/modules/infinispan-remote.mod b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/infinispan-remote.mod new file mode 100644 index 00000000000..d55aa9c760a --- /dev/null +++ b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/infinispan-remote.mod @@ -0,0 +1,9 @@ +[description] +Default setup for the remote infinispan cache without queries + +[tags] +session + +[xml] +etc/sessions/infinispan/infinispan-remote.xml +etc/sessions/infinispan/infinispan-common.xml diff --git a/jetty-infinispan/src/main/config/modules/session-store-infinispan-remote-910.mod b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/session-store-infinispan-remote.mod similarity index 56% rename from jetty-infinispan/src/main/config/modules/session-store-infinispan-remote-910.mod rename to jetty-infinispan/infinispan-remote/src/main/config-template/modules/session-store-infinispan-remote.mod index 93903bd41a3..95e033d10c6 100644 --- a/jetty-infinispan/src/main/config/modules/session-store-infinispan-remote-910.mod +++ b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/session-store-infinispan-remote.mod @@ -1,5 +1,3 @@ -DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html - [description] Enables session data store in a remote Infinispan cache @@ -8,30 +6,27 @@ session [provides] session-store -session-store-infinispan-remote [depend] -sessions +sessions/infinispan/infinispan-common +infinispan-remote +sessions/infinispan/remote/infinispan-remote-libs [files] -maven://org.infinispan/infinispan-remote/9.1.0.Final|lib/infinispan/infinispan-remote-9.1.0.Final.jar -basehome:modules/session-store-infinispan-remote/ +basehome:modules/sessions/infinispan/remote/resources/hotrod-client.properties|resources/hotrod-client.properties -[xml] -etc/sessions/infinispan/remote.xml +[ini] +infinispan.version?=9.4.8.Final -[lib] -lib/jetty-infinispan-${jetty.version}.jar -lib/infinispan/*.jar [license] Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. http://infinispan.org/ http://www.apache.org/licenses/LICENSE-2.0.html - [ini-template] #jetty.session.infinispan.remoteCacheName=sessions #jetty.session.infinispan.idleTimeout.seconds=0 #jetty.session.gracePeriod.seconds=3600 #jetty.session.savePeriod.seconds=0 + diff --git a/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-libs.mod b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-libs.mod new file mode 100644 index 00000000000..28820b94fd4 --- /dev/null +++ b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-libs.mod @@ -0,0 +1,16 @@ +[description] +The Infinispan remote libs + +[tags] +3rdparty +infinispan + +[depends] +sessions/infinispan/remote/infinispan-remote-serverclasses + +[license] +Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. +http://infinispan.org/ +http://www.apache.org/licenses/LICENSE-2.0.html + + diff --git a/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-serverclasses.mod b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-serverclasses.mod new file mode 100644 index 00000000000..b36937e1c6c --- /dev/null +++ b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/infinispan-remote-serverclasses.mod @@ -0,0 +1,12 @@ +[description] +Hides Infinispan classes from webapp. + +[tags] +session +3rdparty +infinispan + + +[ini] +## Hide the infinispan libraries from deployed webapps +jetty.webapp.addServerClasses+=,${jetty.base.uri}/lib/infinispan/ diff --git a/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/resources/hotrod-client.properties b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/resources/hotrod-client.properties new file mode 100644 index 00000000000..bb774cd9c52 --- /dev/null +++ b/jetty-infinispan/infinispan-remote/src/main/config-template/modules/sessions/infinispan/remote/resources/hotrod-client.properties @@ -0,0 +1 @@ +#infinispan.client.hotrod.server_list diff --git a/jetty-infinispan/pom.xml b/jetty-infinispan/pom.xml index ca621e23af8..d693cbf4819 100644 --- a/jetty-infinispan/pom.xml +++ b/jetty-infinispan/pom.xml @@ -1,66 +1,26 @@ + org.eclipse.jetty jetty-project 10.0.0-SNAPSHOT + 4.0.0 - jetty-infinispan - Jetty :: Infinispan Session Managers - http://www.eclipse.org/jetty + org.eclipse.jetty + infinispan-parent + pom + Jetty :: Infinispan + ${project.groupId}.infinispan - 9.1.0.Final - - install - - - org.apache.maven.plugins - maven-assembly-plugin - - - package - - single - - - - config - - - - - - - - - - org.infinispan - infinispan-core - ${infinispan.version} - - - org.infinispan.protostream - protostream - 4.1.0.Final - - - org.eclipse.jetty - jetty-server - ${project.version} - - - org.infinispan - infinispan-client-hotrod - 9.1.0.Final - provided - - - org.infinispan - infinispan-remote-query-client - 9.1.0.Final - provided - - + + infinispan-common + infinispan-embedded + infinispan-remote + infinispan-embedded-query + infinispan-remote-query + + diff --git a/jetty-infinispan/src/main/config/etc/sessions/infinispan/default.xml b/jetty-infinispan/src/main/config/etc/sessions/infinispan/default.xml deleted file mode 100644 index efc565e2fad..00000000000 --- a/jetty-infinispan/src/main/config/etc/sessions/infinispan/default.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - /etc/infinispan-embedded.xml - - - - - - - - - - - - - - - - - - diff --git a/jetty-infinispan/src/main/config/modules/session-store-infinispan-embedded-910.mod b/jetty-infinispan/src/main/config/modules/session-store-infinispan-embedded-910.mod deleted file mode 100644 index e7c8f425f8a..00000000000 --- a/jetty-infinispan/src/main/config/modules/session-store-infinispan-embedded-910.mod +++ /dev/null @@ -1,35 +0,0 @@ -DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html - -[description] -Enables session data store in a local Infinispan cache - -[tags] -session - -[provides] -session-store -session-store-infinispan-embedded - -[depend] -sessions - -[files] -maven://org.infinispan/infinispan-embedded/9.1.0.Final|lib/infinispan/infinispan-embedded-9.1.0.Final.jar -basehome:modules/session-store-infinispan-embedded/infinispan-embedded.xml|etc/infinispan-embedded.xml - - -[xml] -etc/sessions/infinispan/default.xml - -[lib] -lib/jetty-infinispan-${jetty.version}.jar -lib/infinispan/*.jar - -[license] -Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. -http://infinispan.org/ -http://www.apache.org/licenses/LICENSE-2.0.html - -[ini-template] -#jetty.session.gracePeriod.seconds=3600 -#jetty.session.savePeriod.seconds=0 diff --git a/jetty-infinispan/src/main/config/modules/session-store-infinispan-embedded.mod b/jetty-infinispan/src/main/config/modules/session-store-infinispan-embedded.mod deleted file mode 100644 index 21dc1e65c9b..00000000000 --- a/jetty-infinispan/src/main/config/modules/session-store-infinispan-embedded.mod +++ /dev/null @@ -1,35 +0,0 @@ -DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html - -[description] -Enables session data store in a local Infinispan cache - -[tags] -session - -[provides] -session-store -session-store-infnispan-embedded - -[depend] -sessions - -[files] -maven://org.infinispan/infinispan-embedded/7.1.1.Final|lib/infinispan/infinispan-embedded-7.1.1.Final.jar -basehome:modules/session-store-infinispan-embedded/infinispan-embedded.xml|etc/infinispan-embedded.xml - - -[xml] -etc/sessions/infinispan/default.xml - -[lib] -lib/jetty-infinispan-${jetty.version}.jar -lib/infinispan/*.jar - -[license] -Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. -http://infinispan.org/ -http://www.apache.org/licenses/LICENSE-2.0.html - -[ini-template] -#jetty.session.gracePeriod.seconds=3600 -#jetty.session.savePeriod.seconds=0 diff --git a/jetty-infinispan/src/main/config/modules/session-store-infinispan-remote.mod b/jetty-infinispan/src/main/config/modules/session-store-infinispan-remote.mod deleted file mode 100644 index 844b47323cb..00000000000 --- a/jetty-infinispan/src/main/config/modules/session-store-infinispan-remote.mod +++ /dev/null @@ -1,36 +0,0 @@ -DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html - -[description] -Enables session data store in a remote Infinispan cache - -[tags] -session - -[provides] -session-store - -[depend] -sessions - -[files] -maven://org.infinispan/infinispan-remote/7.1.1.Final|lib/infinispan/infinispan-remote-7.1.1.Final.jar -basehome:modules/session-store-infinispan-remote/ - -[xml] -etc/sessions/infinispan/remote.xml - -[lib] -lib/jetty-infinispan-${jetty.version}.jar -lib/infinispan/*.jar - -[license] -Infinispan is an open source project hosted on Github and released under the Apache 2.0 license. -http://infinispan.org/ -http://www.apache.org/licenses/LICENSE-2.0.html - - -[ini-template] -#jetty.session.infinispan.remoteCacheName=sessions -#jetty.session.infinispan.idleTimeout.seconds=0 -#jetty.session.gracePeriod.seconds=3600 -#jetty.session.savePeriod.seconds=0 \ No newline at end of file diff --git a/jetty-infinispan/src/main/config/modules/session-store-infinispan-remote/resources/hotrod-client.properties b/jetty-infinispan/src/main/config/modules/session-store-infinispan-remote/resources/hotrod-client.properties deleted file mode 100644 index 5b9016fc1e9..00000000000 --- a/jetty-infinispan/src/main/config/modules/session-store-infinispan-remote/resources/hotrod-client.properties +++ /dev/null @@ -1 +0,0 @@ -infinispan.client.hotrod.marshaller=org.eclipse.jetty.session.infinispan.WebAppMarshaller \ No newline at end of file diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java index d935b3d1eb2..d6bcf20369b 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnectionFactory.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.io; import java.io.IOException; +import java.util.List; import java.util.Map; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -45,4 +46,42 @@ public interface ClientConnectionFactory client.getBeans(Connection.Listener.class).forEach(connection::addListener); return connection; } + + /** + *

A holder for a list of protocol strings identifying a network protocol + * (for example {@code ["h2", "h2-17", "h2-16"]}) and a {@link ClientConnectionFactory} + * that creates connections that speak that network protocol.

+ */ + public static class Info + { + private final List protocols; + private final ClientConnectionFactory factory; + + public Info(List protocols, ClientConnectionFactory factory) + { + this.protocols = protocols; + this.factory = factory; + } + + public List getProtocols() + { + return protocols; + } + + public ClientConnectionFactory getClientConnectionFactory() + { + return factory; + } + + /** + * Tests whether one of the protocols of this class is also present in the given candidates list. + * + * @param candidates the candidates to match against + * @return whether one of the protocols of this class is present in the candidates + */ + public boolean matches(List candidates) + { + return protocols.stream().anyMatch(candidates::contains); + } + } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index 78cd26cfaba..afe22f23e55 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -42,7 +42,7 @@ import org.eclipse.jetty.util.thread.Scheduler; public class ClientConnector extends ContainerLifeCycle { public static final String CLIENT_CONNECTOR_CONTEXT_KEY = "org.eclipse.jetty.client.connector"; - public static final String SOCKET_ADDRESS_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".socketAddress"; + public static final String REMOTE_SOCKET_ADDRESS_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".remoteSocketAddress"; public static final String CLIENT_CONNECTION_FACTORY_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".clientConnectionFactory"; public static final String CONNECTION_PROMISE_CONTEXT_KEY = CLIENT_CONNECTOR_CONTEXT_KEY + ".connectionPromise"; private static final Logger LOG = Log.getLogger(ClientConnector.class); @@ -50,7 +50,7 @@ public class ClientConnector extends ContainerLifeCycle private Executor executor; private Scheduler scheduler; private ByteBufferPool byteBufferPool; - private SslContextFactory sslContextFactory; + private SslContextFactory.Client sslContextFactory; private SelectorManager selectorManager; private int selectors = 1; private boolean connectBlocking; @@ -97,12 +97,12 @@ public class ClientConnector extends ContainerLifeCycle this.byteBufferPool = byteBufferPool; } - public SslContextFactory getSslContextFactory() + public SslContextFactory.Client getSslContextFactory() { return sslContextFactory; } - public void setSslContextFactory(SslContextFactory sslContextFactory) + public void setSslContextFactory(SslContextFactory.Client sslContextFactory) { if (isStarted()) throw new IllegalStateException(); @@ -192,9 +192,9 @@ public class ClientConnector extends ContainerLifeCycle removeBean(selectorManager); } - protected SslContextFactory newSslContextFactory() + protected SslContextFactory.Client newSslContextFactory() { - SslContextFactory sslContextFactory = new SslContextFactory(false); + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(false); sslContextFactory.setEndpointIdentificationAlgorithm("HTTPS"); return sslContextFactory; } @@ -212,7 +212,7 @@ public class ClientConnector extends ContainerLifeCycle if (context == null) context = new HashMap<>(); context.put(ClientConnector.CLIENT_CONNECTOR_CONTEXT_KEY, this); - context.putIfAbsent(SOCKET_ADDRESS_CONTEXT_KEY, address); + context.putIfAbsent(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address); channel = SocketChannel.open(); SocketAddress bindAddress = getBindAddress(); @@ -299,7 +299,7 @@ public class ClientConnector extends ContainerLifeCycle protected void connectFailed(Throwable failure, Map context) { if (LOG.isDebugEnabled()) - LOG.debug("Could not connect to {}", context.get(SOCKET_ADDRESS_CONTEXT_KEY)); + LOG.debug("Could not connect to {}", context.get(REMOTE_SOCKET_ADDRESS_CONTEXT_KEY)); Promise promise = (Promise)context.get(CONNECTION_PROMISE_CONTEXT_KEY); promise.failed(failure); } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/IdleTimeout.java b/jetty-io/src/main/java/org/eclipse/jetty/io/IdleTimeout.java index df6230bca2c..aae4b2c1449 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/IdleTimeout.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/IdleTimeout.java @@ -40,18 +40,7 @@ public abstract class IdleTimeout private final Scheduler _scheduler; private final AtomicReference _timeout = new AtomicReference<>(); private volatile long _idleTimeout; - private volatile long _idleTimestamp = System.currentTimeMillis(); - - private final Runnable _idleTask = new Runnable() - { - @Override - public void run() - { - long idleLeft = checkIdleTimeout(); - if (idleLeft >= 0) - scheduleIdleTimeout(idleLeft > 0 ? idleLeft : getIdleTimeout()); - } - }; + private volatile long _idleTimestamp = System.nanoTime(); /** * @param scheduler A scheduler used to schedule checks for the idle timeout. @@ -65,22 +54,31 @@ public abstract class IdleTimeout { return _scheduler; } - - public long getIdleTimestamp() - { - return _idleTimestamp; - } + /** + * @return the period of time, in milliseconds, that this object was idle + */ public long getIdleFor() { - return System.currentTimeMillis() - getIdleTimestamp(); + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - _idleTimestamp); } + /** + * @return the idle timeout in milliseconds + * @see #setIdleTimeout(long) + */ public long getIdleTimeout() { return _idleTimeout; } + /** + *

Sets the idle timeout in milliseconds.

+ *

A value that is less than or zero disables the idle timeout checks.

+ * + * @param idleTimeout the idle timeout in milliseconds + * @see #getIdleTimeout() + */ public void setIdleTimeout(long idleTimeout) { long old = _idleTimeout; @@ -107,14 +105,21 @@ public abstract class IdleTimeout */ public void notIdle() { - _idleTimestamp = System.currentTimeMillis(); + _idleTimestamp = System.nanoTime(); + } + + private void idleCheck() + { + long idleLeft = checkIdleTimeout(); + if (idleLeft >= 0) + scheduleIdleTimeout(idleLeft > 0 ? idleLeft : getIdleTimeout()); } private void scheduleIdleTimeout(long delay) { Scheduler.Task newTimeout = null; if (isOpen() && delay > 0 && _scheduler != null) - newTimeout = _scheduler.schedule(_idleTask, delay, TimeUnit.MILLISECONDS); + newTimeout = _scheduler.schedule(this::idleCheck, delay, TimeUnit.MILLISECONDS); Scheduler.Task oldTimeout = _timeout.getAndSet(newTimeout); if (oldTimeout != null) oldTimeout.cancel(); @@ -128,7 +133,7 @@ public abstract class IdleTimeout private void activate() { if (_idleTimeout > 0) - _idleTask.run(); + idleCheck(); } public void onClose() @@ -147,15 +152,15 @@ public abstract class IdleTimeout { if (isOpen()) { - long idleTimestamp = getIdleTimestamp(); + long idleTimestamp = _idleTimestamp; + long idleElapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - idleTimestamp); long idleTimeout = getIdleTimeout(); - long idleElapsed = System.currentTimeMillis() - idleTimestamp; long idleLeft = idleTimeout - idleElapsed; if (LOG.isDebugEnabled()) LOG.debug("{} idle timeout check, elapsed: {} ms, remaining: {} ms", this, idleElapsed, idleLeft); - if (idleTimestamp != 0 && idleTimeout > 0) + if (idleTimeout > 0) { if (idleLeft <= 0) { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java index 4594b5a9299..88a604f1130 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java @@ -295,8 +295,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { - if (LOG.isDebugEnabled()) - LOG.debug(x); + LOG.ignore(x); return -1; } } @@ -309,8 +308,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { - if (LOG.isDebugEnabled()) - LOG.debug(x); + LOG.ignore(x); return -1; } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnection.java index 867f88957aa..b7574f1363b 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/NegotiatingClientConnection.java @@ -35,6 +35,7 @@ public abstract class NegotiatingClientConnection extends AbstractConnection private final SSLEngine engine; private final ClientConnectionFactory connectionFactory; private final Map context; + private String protocol; private volatile boolean completed; protected NegotiatingClientConnection(EndPoint endPoint, Executor executor, SSLEngine sslEngine, ClientConnectionFactory connectionFactory, Map context) @@ -50,8 +51,14 @@ public abstract class NegotiatingClientConnection extends AbstractConnection return engine; } - protected void completed() + public String getProtocol() { + return protocol; + } + + protected void completed(String protocol) + { + this.protocol = protocol; completed = true; } @@ -70,6 +77,7 @@ public abstract class NegotiatingClientConnection extends AbstractConnection catch (Throwable x) { close(); + // TODO: should we not fail the promise in the context here? throw new RuntimeIOException(x); } } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java b/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java index 0468f3a15ca..85173d9352e 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java @@ -389,9 +389,9 @@ abstract public class WriteFlusher boolean progress = true; while (progress && buffers != null) { - long before = remaining(buffers); + long before = BufferUtil.remaining(buffers); boolean flushed = _endPoint.flush(buffers); - long after = remaining(buffers); + long after = BufferUtil.remaining(buffers); long written = before - after; if (LOG.isDebugEnabled()) @@ -441,16 +441,6 @@ abstract public class WriteFlusher return buffers == null ? EMPTY_BUFFERS : buffers; } - private long remaining(ByteBuffer[] buffers) - { - if (buffers == null) - return 0; - long result = 0; - for (ByteBuffer buffer : buffers) - result += buffer.remaining(); - return result; - } - /** * Notify the flusher of a failure * diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java index 1a6feaa2f6c..d75e29fbd4e 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslClientConnectionFactory.java @@ -90,7 +90,7 @@ public class SslClientConnectionFactory implements ClientConnectionFactory @Override public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map context) throws IOException { - InetSocketAddress address = (InetSocketAddress)context.get(ClientConnector.SOCKET_ADDRESS_CONTEXT_KEY); + InetSocketAddress address = (InetSocketAddress)context.get(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY); SSLEngine engine = sslContextFactory.newSSLEngine(address); engine.setUseClientMode(true); context.put(SSL_ENGINE_CONTEXT_KEY, engine); @@ -143,7 +143,7 @@ public class SslClientConnectionFactory implements ClientConnectionFactory HostnameVerifier verifier = sslContextFactory.getHostnameVerifier(); if (verifier != null) { - InetSocketAddress address = (InetSocketAddress)context.get(ClientConnector.SOCKET_ADDRESS_CONTEXT_KEY); + InetSocketAddress address = (InetSocketAddress)context.get(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY); String host = address.getHostString(); try { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java index 15518193881..3bfcf2e99f3 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java @@ -835,6 +835,12 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr LOG.debug("flush b[{}]={}", i++, BufferUtil.toDetailString(b)); } + // finish of any previous flushes + if (BufferUtil.hasContent(_encryptedOutput) && !getEndPoint().flush(_encryptedOutput)) + return false; + + boolean isEmpty = BufferUtil.isEmpty(appOuts); + Boolean result = null; try { @@ -866,7 +872,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr if (filled < 0) throw new IOException("Broken pipe"); } - return result = false; + return result = isEmpty; default: throw new IllegalStateException("Unexpected HandshakeStatus " + status); @@ -895,10 +901,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr _sslEngine.isOutboundDone()); // Was all the data consumed? - boolean allConsumed = true; - for (ByteBuffer b : appOuts) - if (BufferUtil.hasContent(b)) - allConsumed = false; + isEmpty = BufferUtil.isEmpty(appOuts); // if we have net bytes, let's try to flush them boolean flushed = true; @@ -906,7 +909,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr flushed = getEndPoint().flush(_encryptedOutput); if (LOG.isDebugEnabled()) - LOG.debug("net flushed={}, ac={}", flushed, allConsumed); + LOG.debug("net flushed={}, ac={}", flushed, isEmpty); // Now deal with the results returned from the wrap Status wrap = wrapResult.getStatus(); @@ -919,7 +922,7 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr if (!flushed) return result = false; getEndPoint().shutdownOutput(); - if (allConsumed) + if (isEmpty) return result = true; throw new IOException("Broken pipe"); } @@ -936,15 +939,20 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr if (isRenegotiating() && !allowRenegotiate()) { getEndPoint().shutdownOutput(); - if (allConsumed && BufferUtil.isEmpty(_encryptedOutput)) + if (isEmpty && BufferUtil.isEmpty(_encryptedOutput)) return result = true; throw new IOException("Broken pipe"); } if (!flushed) return result = false; - if (allConsumed) - return result = true; + + if (isEmpty) + { + if (wrapResult.getHandshakeStatus() != HandshakeStatus.NEED_WRAP || + wrapResult.bytesProduced() == 0) + return result = true; + } break; default: @@ -1073,14 +1081,15 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr @Override public void doShutdownOutput() { + final EndPoint endp = getEndPoint(); try { boolean close; boolean flush = false; synchronized (_decryptedEndPoint) { - boolean ishut = getEndPoint().isInputShutdown(); - boolean oshut = getEndPoint().isOutputShutdown(); + boolean ishut = endp.isInputShutdown(); + boolean oshut = endp.isOutputShutdown(); if (LOG.isDebugEnabled()) LOG.debug("shutdownOutput: {} oshut={}, ishut={} {}", SslConnection.this, oshut, ishut); @@ -1097,16 +1106,28 @@ public class SslConnection extends AbstractConnection implements Connection.Upgr } if (flush) - flush(BufferUtil.EMPTY_BUFFER); // Send the TLS close message. + { + if (!flush(BufferUtil.EMPTY_BUFFER) && !close) + { + Thread.yield(); + // if we still can't flush, but we are not closing the endpoint, + // let's just flush the encrypted output in the background. + // and continue as if we are closed. The assumption here is that + // the encrypted buffer will contain the entire close handshake + // and that a call to flush(EMPTY_BUFFER) is not needed. + endp.write(Callback.from(() -> {}, t -> endp.close()), _encryptedOutput); + } + } + if (close) - getEndPoint().close(); + endp.close(); else ensureFillInterested(); } catch (Throwable x) { LOG.ignore(x); - getEndPoint().close(); + endp.close(); } } diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java index 96230e2c675..a2292d3a298 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java @@ -625,24 +625,23 @@ public class SocketChannelEndPointTest public static class SslScenario implements Scenario { private final NormalScenario _normalScenario; - private final SslContextFactory __sslCtxFactory = new SslContextFactory(); - private final ByteBufferPool __byteBufferPool = new MappedByteBufferPool(); + private final SslContextFactory _sslCtxFactory = new SslContextFactory.Server(); + private final ByteBufferPool _byteBufferPool = new MappedByteBufferPool(); public SslScenario(NormalScenario normalScenario) throws Exception { _normalScenario = normalScenario; File keystore = MavenTestingUtils.getTestResourceFile("keystore"); - __sslCtxFactory.setKeyStorePath(keystore.getAbsolutePath()); - __sslCtxFactory.setKeyStorePassword("storepwd"); - __sslCtxFactory.setKeyManagerPassword("keypwd"); - __sslCtxFactory.setEndpointIdentificationAlgorithm(""); - __sslCtxFactory.start(); + _sslCtxFactory.setKeyStorePath(keystore.getAbsolutePath()); + _sslCtxFactory.setKeyStorePassword("storepwd"); + _sslCtxFactory.setKeyManagerPassword("keypwd"); + _sslCtxFactory.start(); } @Override public Socket newClient(ServerSocketChannel connector) throws IOException { - SSLSocket socket = __sslCtxFactory.newSslSocket(); + SSLSocket socket = _sslCtxFactory.newSslSocket(); socket.connect(connector.socket().getLocalSocketAddress()); return socket; } @@ -650,11 +649,11 @@ public class SocketChannelEndPointTest @Override public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Executor executor, AtomicInteger blockAt, AtomicInteger writeCount) { - SSLEngine engine = __sslCtxFactory.newSSLEngine(); + SSLEngine engine = _sslCtxFactory.newSSLEngine(); engine.setUseClientMode(false); - SslConnection sslConnection = new SslConnection(__byteBufferPool, executor, endpoint, engine); - sslConnection.setRenegotiationAllowed(__sslCtxFactory.isRenegotiationAllowed()); - sslConnection.setRenegotiationLimit(__sslCtxFactory.getRenegotiationLimit()); + SslConnection sslConnection = new SslConnection(_byteBufferPool, executor, endpoint, engine); + sslConnection.setRenegotiationAllowed(_sslCtxFactory.isRenegotiationAllowed()); + sslConnection.setRenegotiationLimit(_sslCtxFactory.getRenegotiationLimit()); Connection appConnection = _normalScenario.newConnection(channel, sslConnection.getDecryptedEndPoint(), executor, blockAt, writeCount); sslConnection.getDecryptedEndPoint().setConnection(appConnection); return sslConnection; diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java index dfbb26e0f0f..70634cac55a 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SslConnectionTest.java @@ -23,6 +23,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; +import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; @@ -31,6 +32,7 @@ import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLEngine; @@ -50,6 +52,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -60,9 +64,11 @@ public class SslConnectionTest private static final int TIMEOUT = 1000000; private static ByteBufferPool __byteBufferPool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged()); - private final SslContextFactory _sslCtxFactory =new SslContextFactory(); + private final SslContextFactory _sslCtxFactory = new SslContextFactory.Server(); protected volatile EndPoint _lastEndp; private volatile boolean _testFill=true; + private volatile boolean _onXWriteThenShutdown=false; + private volatile FutureCallback _writeCallback; protected ServerSocketChannel _connector; final AtomicInteger _dispatches = new AtomicInteger(); @@ -92,7 +98,6 @@ public class SslConnectionTest return sslConnection; } - @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) { @@ -105,6 +110,7 @@ public class SslConnectionTest static final AtomicInteger __startBlocking = new AtomicInteger(); static final AtomicInteger __blockFor = new AtomicInteger(); + static final AtomicBoolean __onIncompleteFlush = new AtomicBoolean(); private static class TestEP extends SocketChannelEndPoint { public TestEP(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) @@ -115,13 +121,14 @@ public class SslConnectionTest @Override protected void onIncompleteFlush() { - super.onIncompleteFlush(); + __onIncompleteFlush.set(true); } @Override public boolean flush(ByteBuffer... buffers) throws IOException { + __onIncompleteFlush.set(false); if (__startBlocking.get()==0 || __startBlocking.decrementAndGet()==0) { if (__blockFor.get()>0 && __blockFor.getAndDecrement()>0) @@ -133,7 +140,6 @@ public class SslConnectionTest } } - @BeforeEach public void initSSL() throws Exception { @@ -143,7 +149,6 @@ public class SslConnectionTest _sslCtxFactory.setKeyManagerPassword("keypwd"); _sslCtxFactory.setRenegotiationAllowed(true); _sslCtxFactory.setRenegotiationLimit(-1); - _sslCtxFactory.setEndpointIdentificationAlgorithm(null); startManager(); } @@ -227,20 +232,23 @@ public class SslConnectionTest filled=endp.fill(_in); } + boolean shutdown = _onXWriteThenShutdown && BufferUtil.toString(_in).contains("X"); + // Write everything int l=_in.remaining(); if (l>0) { FutureCallback blockingWrite= new FutureCallback(); + endp.write(blockingWrite,_in); blockingWrite.get(); + if (shutdown) + endp.shutdownOutput(); } // are we done? - if (endp.isInputShutdown()) - { + if (endp.isInputShutdown() || shutdown) endp.shutdownOutput(); - } } } catch(InterruptedException|EofException e) @@ -426,7 +434,7 @@ public class SslConnectionTest public void testBlockedWrite() throws Exception { startSSL(); - try (Socket client = newClient()) + try (SSLSocket client = newClient()) { client.setSoTimeout(5000); try (SocketChannel server = _connector.accept()) @@ -434,21 +442,78 @@ public class SslConnectionTest server.configureBlocking(false); _manager.accept(server); - __startBlocking.set(5); - __blockFor.set(3); - client.getOutputStream().write("Hello".getBytes(StandardCharsets.UTF_8)); byte[] buffer = new byte[1024]; int len = client.getInputStream().read(buffer); - assertEquals(5, len); assertEquals("Hello", new String(buffer, 0, len, StandardCharsets.UTF_8)); + __startBlocking.set(0); + __blockFor.set(2); _dispatches.set(0); client.getOutputStream().write("World".getBytes(StandardCharsets.UTF_8)); - len = 5; - while (len > 0) - len -= client.getInputStream().read(buffer); - assertEquals(0, len); + + try + { + client.setSoTimeout(500); + client.getInputStream().read(buffer); + throw new IllegalStateException(); + } + catch(SocketTimeoutException e) + { + } + + + assertTrue(__onIncompleteFlush.get()); + ((TestEP)_lastEndp).getWriteFlusher().completeWrite(); + + len = client.getInputStream().read(buffer); + assertEquals("World", new String(buffer, 0, len, StandardCharsets.UTF_8)); + } + } + } + + @Test + public void testBlockedClose() throws Exception + { + startSSL(); + try (SSLSocket client = newClient()) + { + client.setSoTimeout(5000); + try (SocketChannel server = _connector.accept()) + { + server.configureBlocking(false); + _manager.accept(server); + + //__startBlocking.set(5); + //__blockFor.set(3); + + client.getOutputStream().write("Short".getBytes(StandardCharsets.UTF_8)); + byte[] buffer = new byte[1024]; + int len = client.getInputStream().read(buffer); + assertEquals("Short", new String(buffer, 0, len, StandardCharsets.UTF_8)); + + _onXWriteThenShutdown=true; + __startBlocking.set(2); // block on the close handshake flush + __blockFor.set(Integer.MAX_VALUE); // > retry loops in SslConnection + 1 + client.getOutputStream().write("This is a much longer example with X".getBytes(StandardCharsets.UTF_8)); + len = client.getInputStream().read(buffer); + assertEquals("This is a much longer example with X", new String(buffer, 0, len, StandardCharsets.UTF_8)); + + try + { + client.setSoTimeout(500); + client.getInputStream().read(buffer); + throw new IllegalStateException(); + } + catch(SocketTimeoutException e) + { + } + + __blockFor.set(0); + assertTrue(__onIncompleteFlush.get()); + ((TestEP)_lastEndp).getWriteFlusher().completeWrite(); + len = client.getInputStream().read(buffer); + assertThat(len, is(len)); } } } @@ -478,7 +543,6 @@ public class SslConnectionTest String line = in.readLine(); if (line == null) break; - // System.err.println(line); count.countDown(); } } @@ -491,7 +555,6 @@ public class SslConnectionTest for (int i = 0; i < LINES; i++) { client.getOutputStream().write(("HelloWorld " + i + "\n").getBytes(StandardCharsets.UTF_8)); - // System.err.println("wrote"); if (i % 1000 == 0) { client.getOutputStream().flush(); diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SslEngineBehaviorTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SslEngineBehaviorTest.java index ac6a8ac1d3a..d8bee115735 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SslEngineBehaviorTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SslEngineBehaviorTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.io; -import static org.hamcrest.Matchers.greaterThan; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.File; import java.nio.ByteBuffer; @@ -32,12 +28,15 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.AfterAll; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.JRE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class SslEngineBehaviorTest { private static SslContextFactory sslCtxFactory; @@ -45,12 +44,11 @@ public class SslEngineBehaviorTest @BeforeAll public static void startSsl() throws Exception { - sslCtxFactory = new SslContextFactory(); + sslCtxFactory = new SslContextFactory.Server(); File keystore = MavenTestingUtils.getTestResourceFile("keystore"); sslCtxFactory.setKeyStorePath(keystore.getAbsolutePath()); sslCtxFactory.setKeyStorePassword("storepwd"); sslCtxFactory.setKeyManagerPassword("keypwd"); - sslCtxFactory.setEndpointIdentificationAlgorithm(""); sslCtxFactory.start(); } diff --git a/jetty-jaas/src/main/config/etc/jetty-jaas.xml b/jetty-jaas/src/main/config/etc/jetty-jaas.xml index 5ef2426cf54..db6bdfebcab 100644 --- a/jetty-jaas/src/main/config/etc/jetty-jaas.xml +++ b/jetty-jaas/src/main/config/etc/jetty-jaas.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/LdapLoginModule.java b/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/LdapLoginModule.java index af63c29e1c4..21dd9012461 100644 --- a/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/LdapLoginModule.java +++ b/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/LdapLoginModule.java @@ -20,12 +20,12 @@ package org.eclipse.jetty.jaas.spi; import java.io.IOException; import java.util.ArrayList; +import java.util.Base64; import java.util.Hashtable; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; - import javax.naming.AuthenticationException; import javax.naming.Context; import javax.naming.NamingEnumeration; @@ -45,7 +45,6 @@ import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import org.eclipse.jetty.jaas.callback.ObjectCallback; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -755,13 +754,13 @@ public class LdapLoginModule extends AbstractLoginModule private static String base64ToHex(String src) { - byte[] bytes = B64Code.decode(src); + byte[] bytes = Base64.getDecoder().decode(src); return TypeUtil.toString(bytes, 16); } private static String hexToBase64(String src) { byte[] bytes = TypeUtil.fromHexString(src); - return new String(B64Code.encode(bytes)); + return Base64.getEncoder().encodeToString(bytes); } } diff --git a/jetty-jaspi/src/main/java/org/eclipse/jetty/security/jaspi/modules/BaseAuthModule.java b/jetty-jaspi/src/main/java/org/eclipse/jetty/security/jaspi/modules/BaseAuthModule.java index e2c1ffb1a1f..81b03f31688 100644 --- a/jetty-jaspi/src/main/java/org/eclipse/jetty/security/jaspi/modules/BaseAuthModule.java +++ b/jetty-jaspi/src/main/java/org/eclipse/jetty/security/jaspi/modules/BaseAuthModule.java @@ -20,9 +20,9 @@ package org.eclipse.jetty.security.jaspi.modules; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Map; import java.util.Set; - import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -41,7 +41,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.security.authentication.LoginCallbackImpl; import org.eclipse.jetty.security.jaspi.JaspiMessageInfo; import org.eclipse.jetty.security.jaspi.callback.CredentialValidationCallback; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.util.security.Password; @@ -120,7 +119,7 @@ public class BaseAuthModule implements ServerAuthModule, ServerAuthContext throws IOException, UnsupportedCallbackException { credentials = credentials.substring(credentials.indexOf(' ')+1); - credentials = B64Code.decode(credentials, StandardCharsets.ISO_8859_1); + credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1); int i = credentials.indexOf(':'); String userName = credentials.substring(0,i); String password = credentials.substring(i+1); diff --git a/jetty-jaspi/src/test/java/org/eclipse/jetty/security/jaspi/JaspiTest.java b/jetty-jaspi/src/test/java/org/eclipse/jetty/security/jaspi/JaspiTest.java index 58b23e3ecc7..13309d2a73c 100644 --- a/jetty-jaspi/src/test/java/org/eclipse/jetty/security/jaspi/JaspiTest.java +++ b/jetty-jaspi/src/test/java/org/eclipse/jetty/security/jaspi/JaspiTest.java @@ -18,13 +18,10 @@ package org.eclipse.jetty.security.jaspi; -import static org.hamcrest.Matchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.IOException; +import java.util.Base64; import java.util.HashMap; import java.util.Map; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -38,7 +35,6 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.util.security.Password; @@ -47,6 +43,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.startsWith; + public class JaspiTest { Server _server; @@ -162,8 +162,9 @@ public class JaspiTest @Test public void testConstraintWrongAuth() throws Exception { - String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n"+ - "Authorization: Basic " + B64Code.encode("user:wrong") + "\n\n"); + String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n" + + "Authorization: Basic " + Base64.getEncoder().encodeToString("user:wrong".getBytes(ISO_8859_1)) + + "\n\n"); assertThat(response,startsWith("HTTP/1.1 401 Unauthorized")); assertThat(response,Matchers.containsString("WWW-Authenticate: basic realm=\"TestRealm\"")); } @@ -171,8 +172,9 @@ public class JaspiTest @Test public void testConstraintAuth() throws Exception { - String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n"+ - "Authorization: Basic " + B64Code.encode("user:password") + "\n\n"); + String response = _connector.getResponse("GET /ctx/jaspi/test HTTP/1.0\n" + + "Authorization: Basic " + Base64.getEncoder().encodeToString("user:password".getBytes(ISO_8859_1)) + + "\n\n"); assertThat(response,startsWith("HTTP/1.1 200 OK")); } diff --git a/jetty-jmx/src/main/config/etc/jetty-jmx-remote.xml b/jetty-jmx/src/main/config/etc/jetty-jmx-remote.xml index 74143533b78..ceed76d9f33 100644 --- a/jetty-jmx/src/main/config/etc/jetty-jmx-remote.xml +++ b/jetty-jmx/src/main/config/etc/jetty-jmx-remote.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-jmx/src/main/config/etc/jetty-jmx.xml b/jetty-jmx/src/main/config/etc/jetty-jmx.xml index ac906d8172f..dc1793be9e9 100644 --- a/jetty-jmx/src/main/config/etc/jetty-jmx.xml +++ b/jetty-jmx/src/main/config/etc/jetty-jmx.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java index 0fd9ad74dd1..661f34f696b 100644 --- a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java +++ b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.jmx; -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 java.net.ConnectException; import java.net.InetAddress; import java.net.ServerSocket; @@ -40,6 +36,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * Running the tests of this class in the same JVM results often in *
@@ -227,7 +227,7 @@ public class ConnectorServerTest
     @Test
     public void testJMXOverTLS() throws Exception
     {
-        SslContextFactory sslContextFactory = new SslContextFactory();
+        SslContextFactory sslContextFactory = new SslContextFactory.Server();
         String keyStorePath = MavenTestingUtils.getTestResourcePath("keystore.jks").toString();
         String keyStorePassword = "storepwd";
         sslContextFactory.setKeyStorePath(keyStorePath);
diff --git a/jetty-jmx/src/test/java/org/eclipse/jetty/util/log/jmx/LogMBeanTest.java b/jetty-jmx/src/test/java/org/eclipse/jetty/util/log/jmx/LogMBeanTest.java
index 6945d5e68ba..2c322e40aaf 100644
--- a/jetty-jmx/src/test/java/org/eclipse/jetty/util/log/jmx/LogMBeanTest.java
+++ b/jetty-jmx/src/test/java/org/eclipse/jetty/util/log/jmx/LogMBeanTest.java
@@ -18,16 +18,16 @@
 
 package org.eclipse.jetty.util.log.jmx;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.isIn;
-import static org.hamcrest.Matchers.not;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
 import com.acme.Managed;
-
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.in;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 public class LogMBeanTest
 {
 
@@ -48,13 +48,13 @@ public class LogMBeanTest
     public void testKeySet()
     {
         // given
-        assertThat("Managed is not registered with loggers", MANAGED_CLASS, not(isIn(logMBean.getLoggers())));
+        assertThat("Managed is not registered with loggers", MANAGED_CLASS, not(is(in(logMBean.getLoggers()))));
 
         // when
         logMBean.setDebugEnabled(MANAGED_CLASS,true);
 
         // then
-        assertThat("Managed must be registered with loggers", MANAGED_CLASS, isIn(logMBean.getLoggers()));
+        assertThat("Managed must be registered with loggers", MANAGED_CLASS, is(in(logMBean.getLoggers())));
         assertTrue(logMBean.isDebugEnabled(MANAGED_CLASS), "This must return true as debug is enabled for this class");
     }
 }
diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/ContextFactory.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/ContextFactory.java
index ab1601b274c..c480b10152f 100644
--- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/ContextFactory.java
+++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/ContextFactory.java
@@ -18,12 +18,11 @@
 
 package org.eclipse.jetty.jndi;
 
-
 import java.io.IOException;
+import java.util.Collections;
 import java.util.Hashtable;
 import java.util.Map;
 import java.util.WeakHashMap;
-
 import javax.naming.Context;
 import javax.naming.Name;
 import javax.naming.NameParser;
@@ -32,6 +31,8 @@ import javax.naming.StringRefAddr;
 import javax.naming.spi.ObjectFactory;
 
 import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
 
 
@@ -59,12 +60,12 @@ import org.eclipse.jetty.util.log.Logger;
  */
 public class ContextFactory implements ObjectFactory
 {
-    private static Logger __log = NamingUtil.__log;
+    private static final Logger LOG = Log.getLogger(ContextFactory.class);
 
     /**
      * Map of classloaders to contexts.
      */
-    private static final WeakHashMap __contextMap = new WeakHashMap();
+    private static final Map __contextMap = Collections.synchronizedMap(new WeakHashMap<>());
 
     /**
      * Threadlocal for injecting a context to use
@@ -77,8 +78,6 @@ public class ContextFactory implements ObjectFactory
      * when finding the comp context.
      */
     private static final ThreadLocal __threadClassLoader = new ThreadLocal();
-    
-
 
     /**
      * Find or create a context which pertains to a classloader.
@@ -106,7 +105,8 @@ public class ContextFactory implements ObjectFactory
         Context ctx = (Context)__threadContext.get();
         if (ctx != null)
         {
-            if(__log.isDebugEnabled()) __log.debug("Using the Context that is bound on the thread");
+            if(LOG.isDebugEnabled())
+                LOG.debug("Using the Context that is bound on the thread");
             return ctx;
         }
 
@@ -115,37 +115,47 @@ public class ContextFactory implements ObjectFactory
         ClassLoader loader = (ClassLoader)__threadClassLoader.get();
         if (loader != null)
         {
-            if (__log.isDebugEnabled() && loader != null) __log.debug("Using threadlocal classloader");
-            ctx = getContextForClassLoader(loader);
-            if (ctx == null)
+            if (LOG.isDebugEnabled())
+                LOG.debug("Using threadlocal classloader");
+            synchronized(__contextMap)
             {
-                ctx = newNamingContext(obj, loader, env, name, nameCtx);
-                __contextMap.put (loader, ctx);
-                if(__log.isDebugEnabled())__log.debug("Made context "+name.get(0)+" for classloader: "+loader);
+                ctx = getContextForClassLoader(loader);
+                if (ctx == null)
+                {
+                    ctx = newNamingContext(obj, loader, env, name, nameCtx);
+                    __contextMap.put (loader, ctx);
+                    if(LOG.isDebugEnabled())
+                        LOG.debug("Made context {} for classloader {}",name.get(0),loader);
+                }
+                return ctx;
             }
-            return ctx;
         }
-       
+
         //If the thread context classloader is set, then try its hierarchy to find a matching context
         ClassLoader tccl = Thread.currentThread().getContextClassLoader();
         loader = tccl;      
         if (loader != null)
         {
-            if (__log.isDebugEnabled() && loader != null) __log.debug("Trying thread context classloader");
-            while (ctx == null && loader != null)
+            if (LOG.isDebugEnabled())
+                LOG.debug("Trying thread context classloader");
+            synchronized(__contextMap)
             {
-                ctx = getContextForClassLoader(loader);
-                if (ctx == null && loader != null)
-                    loader = loader.getParent();
-            }
+                while (ctx == null && loader != null)
+                {
+                    ctx = getContextForClassLoader(loader);
+                    if (ctx == null && loader != null)
+                        loader = loader.getParent();
+                }
 
-            if (ctx == null)
-            {
-                ctx = newNamingContext(obj, tccl, env, name, nameCtx);
-                __contextMap.put (tccl, ctx);
-                if(__log.isDebugEnabled())__log.debug("Made context "+name.get(0)+" for classloader: "+tccl);
+                if (ctx == null)
+                {
+                    ctx = newNamingContext(obj, tccl, env, name, nameCtx);
+                    __contextMap.put (tccl, ctx);
+                    if(LOG.isDebugEnabled())
+                        LOG.debug("Made context {} for classloader {}", name.get(0), tccl);
+                }
+                return ctx;
             }
-            return ctx;
         }
 
 
@@ -153,19 +163,23 @@ public class ContextFactory implements ObjectFactory
         //classloader associated with the current context
         if (ContextHandler.getCurrentContext() != null)
         {
-            
-            if (__log.isDebugEnabled() && loader != null) __log.debug("Trying classloader of current org.eclipse.jetty.server.handler.ContextHandler");
-            loader = ContextHandler.getCurrentContext().getContextHandler().getClassLoader();
-            ctx = (Context)__contextMap.get(loader);    
-
-            if (ctx == null && loader != null)
+            if (LOG.isDebugEnabled() && loader != null)
+                LOG.debug("Trying classloader of current org.eclipse.jetty.server.handler.ContextHandler");
+            synchronized(__contextMap)
             {
-                ctx = newNamingContext(obj, loader, env, name, nameCtx);
-                __contextMap.put (loader, ctx);
-                if(__log.isDebugEnabled())__log.debug("Made context "+name.get(0)+" for classloader: "+loader);
-            }
+                loader = ContextHandler.getCurrentContext().getContextHandler().getClassLoader();
+                ctx = (Context)__contextMap.get(loader);    
 
-            return ctx;
+                if (ctx == null && loader != null)
+                {
+                    ctx = newNamingContext(obj, loader, env, name, nameCtx);
+                    __contextMap.put (loader, ctx);
+                    if(LOG.isDebugEnabled())
+                        LOG.debug("Made context {} for classloader {} ", name.get(0), loader);
+                }
+
+                return ctx;
+            }
         }
         return null;
     }
@@ -243,17 +257,9 @@ public class ContextFactory implements ObjectFactory
 
     public static void dump(Appendable out, String indent) throws IOException
     {
-        out.append("o.e.j.jndi.ContextFactory@").append(Long.toHexString(__contextMap.hashCode())).append("\n");
-        int size=__contextMap.size();
-        int i=0;
-        for (Map.Entry entry : ((Map)__contextMap).entrySet())
+        synchronized (__contextMap)
         {
-            boolean last=++i==size;
-            ClassLoader loader=entry.getKey();
-            out.append(indent).append(" +- ").append(loader.getClass().getSimpleName()).append("@").append(Long.toHexString(loader.hashCode())).append(": ");
-
-            NamingContext context = entry.getValue();
-            context.dump(out,indent+(last?"    ":" |  "));
+            Dumpable.dumpObjects(out, indent, String.format("o.e.j.jndi.ContextFactory@",__contextMap.hashCode()), __contextMap);
         }
     }
 }
diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/InitialContextFactory.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/InitialContextFactory.java
index ab90fe0c684..25355060333 100644
--- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/InitialContextFactory.java
+++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/InitialContextFactory.java
@@ -18,10 +18,8 @@
 
 package org.eclipse.jetty.jndi;
 
-
 import java.util.Hashtable;
 import java.util.Properties;
-
 import javax.naming.CompoundName;
 import javax.naming.Context;
 import javax.naming.Name;
@@ -29,6 +27,7 @@ import javax.naming.NameParser;
 import javax.naming.NamingException;
 
 import org.eclipse.jetty.jndi.local.localContextRoot;
+import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
 
 
@@ -44,7 +43,7 @@ import org.eclipse.jetty.util.log.Logger;
  */
 public class InitialContextFactory implements javax.naming.spi.InitialContextFactory
 {
-    private static Logger __log = NamingUtil.__log;
+    private static final Logger LOG = Log.getLogger(InitialContextFactory.class);
 
     public static class DefaultParser implements NameParser
     {
@@ -78,10 +77,12 @@ public class InitialContextFactory implements javax.naming.spi.InitialContextFac
     @Override
     public Context getInitialContext(Hashtable env)
     {
-        __log.debug("InitialContextFactory.getInitialContext()");
+        if(LOG.isDebugEnabled())
+            LOG.debug("InitialContextFactory.getInitialContext()");
 
         Context ctx = new localContextRoot(env);
-        if(__log.isDebugEnabled())__log.debug("Created initial context delegate for local namespace:"+ctx);
+        if(LOG.isDebugEnabled())
+            LOG.debug("Created initial context delegate for local namespace:"+ctx);
 
         return ctx;
     }
diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingContext.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingContext.java
index 49c85e06123..1d147752c12 100644
--- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingContext.java
+++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingContext.java
@@ -22,11 +22,10 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
-import java.util.Map;
-
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import javax.naming.Binding;
 import javax.naming.CompoundName;
 import javax.naming.Context;
@@ -45,6 +44,7 @@ import javax.naming.Referenceable;
 import javax.naming.spi.NamingManager;
 
 import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
 
 /** 
@@ -56,22 +56,36 @@ import org.eclipse.jetty.util.log.Logger;
  * All Names are expected to be Compound, not Composite.
  */
 @SuppressWarnings("unchecked")
-public class NamingContext implements Context, Cloneable, Dumpable
+public class NamingContext implements Context, Dumpable
 {
-    private final static Logger __log=NamingUtil.__log;
+    private static final Logger LOG = Log.getLogger(NamingContext.class);
     private final static List __empty = Collections.emptyList();
     public static final String DEEP_BINDING = "org.eclipse.jetty.jndi.deepBinding";
     public static final String LOCK_PROPERTY = "org.eclipse.jetty.jndi.lock";
     public static final String UNLOCK_PROPERTY = "org.eclipse.jetty.jndi.unlock";
 
-    protected final Hashtable _env = new Hashtable();
-    private boolean _supportDeepBinding = false;
-    protected Map _bindings = new HashMap();
+    /*
+     * The env is stored as a Hashtable to be compatible with the API.
+     * There is no need for concurrent protection (see Concurrent Access section
+     * of the {@link Context} javadoc), so multiple threads acting on the same
+     * Context env need to self - mutually exclude.
+     */
+    protected final Hashtable _env = new Hashtable<>();
+
+    /*
+     * This contexts bindings are stored as a ConcurrentHashMap.
+     * There is generally no need for concurrent protection (see Concurrent Access section
+     * of the {@link Context} javadoc), However, the NamingContext is used for root contexts and
+     * we do not mutually exclude when initializing, so instead we do make the bindings
+     * thread safe (unlike the env where we expect users to respect the Concurrent Access requirements).
+     */
+    protected final ConcurrentMap _bindings;
 
     protected NamingContext _parent = null;
     protected String _name = null;
     protected NameParser _parser = null;
     private Collection _listeners;
+    private Object _lock;
 
     /*------------------------------------------------*/
     /**
@@ -99,7 +113,7 @@ public class NamingContext implements Context, Cloneable, Dumpable
     /**
      * Constructor
      *
-     * @param env environment properties
+     * @param env environment properties which are copied into this Context's environment
      * @param name relative name of this context
      * @param parent immediate ancestor Context (can be null)
      * @param parser NameParser for this Context
@@ -108,53 +122,48 @@ public class NamingContext implements Context, Cloneable, Dumpable
                          String name,
                          NamingContext parent,
                          NameParser parser)
+    {
+        this(env, name, parent, parser, null);
+    }
+
+    private NamingContext(Hashtable env,
+                         String name,
+                         NamingContext parent,
+                         NameParser parser,
+                         ConcurrentMap bindings)
     {
         if (env != null)
-        {
             _env.putAll(env);
-            // look for deep binding support in _env
-            Object support = _env.get(DEEP_BINDING);
-            if (support == null)
-                _supportDeepBinding = false;
-            else
-                _supportDeepBinding = support != null?Boolean.parseBoolean(support.toString()):false;
-        }
-        else
-        {
-            // no env?  likely this is a root context (java or local) that
-            // was created without an _env.  Look for a system property.
-            String value = System.getProperty(DEEP_BINDING,"false");
-            _supportDeepBinding = Boolean.parseBoolean(value);
-            // put what we discovered into the _env for later sub-contexts
-            // to utilize.
-            _env.put(DEEP_BINDING,_supportDeepBinding);
-        }
         _name = name;
         _parent = parent;
         _parser = parser;
-        if(__log.isDebugEnabled())
-            __log.debug("supportDeepBinding={}",_supportDeepBinding);
+        _bindings = bindings==null ? new ConcurrentHashMap<>() : bindings;
+        if(LOG.isDebugEnabled())
+            LOG.debug("new {}", this);
     }
 
-
-    /*------------------------------------------------*/
     /**
-     * Clone this NamingContext
-     *
-     * @return copy of this NamingContext
-     * @exception CloneNotSupportedException if an error occurs
+     * @return A shallow copy of the Context with the same bindings, but a copy of the Env
      */
-    @Override
-    public Object clone ()
-        throws CloneNotSupportedException
+    public NamingContext shallowCopy()
     {
-        NamingContext ctx = (NamingContext)super.clone();
-        ctx._env.putAll(_env);
-        ctx._bindings.putAll(_bindings);
-        return ctx;
+        return new NamingContext(_env, _name, _parent, _parser, _bindings);
     }
 
 
+    public boolean isDeepBindingSupported()
+    {
+        // look for deep binding support in _env
+        Object support = _env.get(DEEP_BINDING);
+        if (support!=null)
+            return Boolean.parseBoolean(String.valueOf(support));
+
+        if (_parent!=null)
+            return _parent.isDeepBindingSupported();
+        // probably a root context
+        return Boolean.parseBoolean(System.getProperty(DEEP_BINDING,"false"));
+    }
+
     /*------------------------------------------------*/
     /**
      * Getter for _name
@@ -183,27 +192,69 @@ public class NamingContext implements Context, Cloneable, Dumpable
         _parser = parser;
     }
 
-
     public final void setEnv (Hashtable env)
     {
-        _env.clear();
-        if(env == null)
-            return;
-        _env.putAll(env);
-        _supportDeepBinding = _env.containsKey(DEEP_BINDING);
+        Object lock = _env.get(LOCK_PROPERTY);
+        try
+        {
+            _env.clear();
+            if (env == null)
+                return;
+            _env.putAll(env);
+        }
+        finally
+        {
+            if (lock!=null)
+                _env.put(LOCK_PROPERTY, lock);
+        }
     }
 
-
-    public Map getBindings ()
+    private Object dereference(Object ctx, String firstComponent) throws NamingException
     {
-        return _bindings;
+        if (ctx instanceof Reference)
+        {
+            //deference the object
+            try
+            {
+                return NamingManager.getObjectInstance(ctx, _parser.parse(firstComponent), this, _env);
+            }
+            catch (NamingException e)
+            {
+                throw e;
+            }
+            catch (Exception e)
+            {
+                LOG.warn("",e);
+                throw new NamingException (e.getMessage());
+            }
+        }
+        return ctx;
     }
 
-    public void setBindings(Map bindings)
+    private Context getContext(Name cname) throws NamingException
     {
-        _bindings = bindings;
+        String firstComponent = cname.get(0);
+
+        if (firstComponent.equals(""))
+            return this;
+
+        Binding binding = getBinding (firstComponent);
+        if (binding == null)
+        {
+            NameNotFoundException nnfe = new NameNotFoundException(firstComponent + " is not bound");
+            nnfe.setRemainingName(cname);
+            throw nnfe;
+        }
+
+        Object ctx = dereference(binding.getObject(), firstComponent);
+
+        if (!(ctx instanceof Context))
+            throw new NotContextException(firstComponent + " not a context in " + this.getNameInNamespace());
+
+        return (Context)ctx;
     }
 
+
     /*------------------------------------------------*/
     /**
      * Bind a name to an object
@@ -244,7 +295,8 @@ public class NamingContext implements Context, Cloneable, Dumpable
         }
         else
         {
-            if(__log.isDebugEnabled())__log.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
+            if(LOG.isDebugEnabled())
+                LOG.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
 
             //walk down the subcontext hierarchy
             //need to ignore trailing empty "" name components
@@ -256,13 +308,13 @@ public class NamingContext implements Context, Cloneable, Dumpable
                 ctx = this;
             else
             {
-
                 Binding  binding = getBinding (firstComponent);
-                if (binding == null) {
-                    if (_supportDeepBinding)
+                if (binding == null)
+                {
+                    if (isDeepBindingSupported())
                     {
                         Name subname = _parser.parse(firstComponent);
-                        Context subctx = new NamingContext((Hashtable)_env.clone(),firstComponent,this,_parser);
+                        Context subctx = new NamingContext(_env,firstComponent,this,_parser, null);
                         addBinding(subname,subctx);
                         binding = getBinding(subname);
                     }
@@ -272,28 +324,9 @@ public class NamingContext implements Context, Cloneable, Dumpable
                     }
                 }
 
-                ctx = binding.getObject();
-
-                if (ctx instanceof Reference)
-                {
-                    //deference the object
-                    try
-                    {
-                        ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
-                }
+                ctx = dereference(binding.getObject(),firstComponent);
             }
 
-
             if (ctx instanceof Context)
             {
                 ((Context)ctx).bind (cname.getSuffix(1), obj);
@@ -354,57 +387,14 @@ public class NamingContext implements Context, Cloneable, Dumpable
             if (binding != null)
                 throw new NameAlreadyBoundException (cname.toString());
 
-            Context ctx = new NamingContext ((Hashtable)_env.clone(), cname.get(0), this, _parser);
+            Context ctx = new NamingContext (_env, cname.get(0), this, _parser);
             addBinding (cname, ctx);
             return ctx;
         }
 
-
-        //If the name has multiple subcontexts, walk the hierarchy by
-        //fetching the first one. All intermediate subcontexts in the
-        //name must already exist.
-        String firstComponent = cname.get(0);
-        Object ctx = null;
-
-        if (firstComponent.equals(""))
-            ctx = this;
-        else
-        {
-            Binding binding = getBinding (firstComponent);
-            if (binding == null)
-                throw new NameNotFoundException (firstComponent + " is not bound");
-
-            ctx = binding.getObject();
-
-            if (ctx instanceof Reference)
-            {
-                //deference the object
-                if(__log.isDebugEnabled())__log.debug("Object bound at "+firstComponent +" is a Reference");
-                try
-                {
-                    ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                }
-                catch (NamingException e)
-                {
-                    throw e;
-                }
-                catch (Exception e)
-                {
-                    __log.warn("",e);
-                    throw new NamingException (e.getMessage());
-                }
-            }
-        }
-
-        if (ctx instanceof Context)
-        {
-            return ((Context)ctx).createSubcontext (cname.getSuffix(1));
-        }
-        else
-            throw new NotContextException (firstComponent +" is not a Context");
+        return getContext(cname).createSubcontext (cname.getSuffix(1));
     }
 
-
     /*------------------------------------------------*/
     /**
      * Create a Context as a child of this one
@@ -420,8 +410,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
         return createSubcontext(_parser.parse(name));
     }
 
-
-
     /*------------------------------------------------*/
     /**
      *
@@ -436,8 +424,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
         removeBinding(_parser.parse(name));
     }
 
-
-
     /*------------------------------------------------*/
     /**
      *
@@ -463,19 +449,17 @@ public class NamingContext implements Context, Cloneable, Dumpable
     public Object lookup(Name name)
         throws NamingException
     {
-        if(__log.isDebugEnabled())__log.debug("Looking up name=\""+name+"\"");
+        if(LOG.isDebugEnabled())
+            LOG.debug("Looking up name=\""+name+"\"");
         Name cname = toCanonicalName(name);
 
         if ((cname == null) || (cname.size() == 0))
         {
-            __log.debug("Null or empty name, returning copy of this context");
-            NamingContext ctx = new NamingContext (_env, _name, _parent, _parser);
-            ctx._bindings=_bindings;
-            return ctx;
+            if(LOG.isDebugEnabled())
+                LOG.debug("Null or empty name, returning shallowCopy of this context");
+            return shallowCopy();
         }
 
-
-
         if (cname.size() == 1)
         {
             Binding binding = getBinding (cname);
@@ -486,7 +470,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
                 throw nnfe;
             }
 
-
             Object o = binding.getObject();
 
             //handle links by looking up the link
@@ -505,7 +488,7 @@ public class NamingContext implements Context, Cloneable, Dumpable
             }
             else if (o instanceof Reference)
             {
-                //deference the object
+                // TODO use deference ??
                 try
                 {
                     return NamingManager.getObjectInstance(o, cname, this, _env);
@@ -516,7 +499,7 @@ public class NamingContext implements Context, Cloneable, Dumpable
                 }
                 catch (Exception e)
                 {
-                    __log.warn("",e);
+                    LOG.warn("",e);
                     throw new NamingException (e.getMessage());
                 }
             }
@@ -524,54 +507,9 @@ public class NamingContext implements Context, Cloneable, Dumpable
                 return o;
         }
 
-        //it is a multipart name, recurse to the first subcontext
-
-        String firstComponent = cname.get(0);
-        Object ctx = null;
-
-        if (firstComponent.equals(""))
-            ctx = this;
-        else
-        {
-
-            Binding binding = getBinding (firstComponent);
-            if (binding == null)
-            {
-                NameNotFoundException nnfe = new NameNotFoundException();
-                nnfe.setRemainingName(cname);
-                throw nnfe;
-            }
-
-            //as we have bound a reference to an object factory
-            //for the component specific contexts
-            //at "comp" we need to resolve the reference
-            ctx = binding.getObject();
-
-            if (ctx instanceof Reference)
-            {
-                //deference the object
-                try
-                {
-                    ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                }
-                catch (NamingException e)
-                {
-                    throw e;
-                }
-                catch (Exception e)
-                {
-                    __log.warn("",e);
-                    throw new NamingException (e.getMessage());
-                }
-            }
-        }
-        if (!(ctx instanceof Context))
-            throw new NotContextException();
-
-        return ((Context)ctx).lookup (cname.getSuffix(1));
+        return getContext(cname).lookup (cname.getSuffix(1));
     }
 
-
     /*------------------------------------------------*/
     /**
      * Lookup binding of an object by name
@@ -587,8 +525,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
         return lookup (_parser.parse(name));
     }
 
-
-
     /*------------------------------------------------*/
     /**
      * Lookup link bound to name
@@ -603,12 +539,11 @@ public class NamingContext implements Context, Cloneable, Dumpable
     {
         Name cname = toCanonicalName(name);
 
-        if (cname == null)
+        if (cname == null || name.isEmpty())
         {
-            NamingContext ctx = new NamingContext (_env, _name, _parent, _parser);
-            ctx._bindings=_bindings;
-            return ctx;
+            return shallowCopy();
         }
+
         if (cname.size() == 0)
             throw new NamingException ("Name is empty");
 
@@ -618,75 +553,12 @@ public class NamingContext implements Context, Cloneable, Dumpable
             if (binding == null)
                 throw new NameNotFoundException();
 
-            Object o = binding.getObject();
-
-            //handle links by looking up the link
-            if (o instanceof Reference)
-            {
-                //deference the object
-                try
-                {
-                    return NamingManager.getObjectInstance(o, cname.getPrefix(1), this, _env);
-                }
-                catch (NamingException e)
-                {
-                    throw e;
-                }
-                catch (Exception e)
-                {
-                    __log.warn("",e);
-                    throw new NamingException (e.getMessage());
-                }
-            }
-            else
-            {
-                //object is either a LinkRef which we don't dereference
-                //or a plain object in which case spec says we return it
-                return o;
-            }
+            return dereference(binding.getObject(), cname.getPrefix(1).toString());
         }
 
-
-        //it is a multipart name, recurse to the first subcontext
-        String firstComponent = cname.get(0);
-        Object ctx = null;
-
-        if (firstComponent.equals(""))
-            ctx = this;
-        else
-        {
-            Binding binding = getBinding (firstComponent);
-            if (binding == null)
-                throw new NameNotFoundException ();
-
-            ctx = binding.getObject();
-
-            if (ctx instanceof Reference)
-            {
-                //deference the object
-                try
-                {
-                    ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                }
-                catch (NamingException e)
-                {
-                    throw e;
-                }
-                catch (Exception e)
-                {
-                    __log.warn("",e);
-                    throw new NamingException (e.getMessage());
-                }
-            }
-        }
-
-        if (!(ctx instanceof Context))
-            throw new NotContextException();
-
-        return ((Context)ctx).lookup (cname.getSuffix(1));
+        return getContext(cname).lookup (cname.getSuffix(1));
     }
 
-
     /*------------------------------------------------*/
     /**
      * Lookup link bound to name
@@ -702,7 +574,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
         return lookupLink (_parser.parse(name));
     }
 
-
     /*------------------------------------------------*/
     /**
      * List all names bound at Context named by Name
@@ -715,65 +586,24 @@ public class NamingContext implements Context, Cloneable, Dumpable
     public NamingEnumeration list(Name name)
         throws NamingException
     {
-        if(__log.isDebugEnabled())__log.debug("list() on Context="+getName()+" for name="+name);
+        if(LOG.isDebugEnabled())
+            LOG.debug("list() on Context="+getName()+" for name="+name);
         Name cname = toCanonicalName(name);
 
-
-
         if (cname == null)
         {
             return new NameEnumeration(__empty.iterator());
         }
 
-
         if (cname.size() == 0)
         {
            return new NameEnumeration (_bindings.values().iterator());
         }
 
-
-
         //multipart name
-        String firstComponent = cname.get(0);
-        Object ctx = null;
-
-        if (firstComponent.equals(""))
-            ctx = this;
-        else
-        {
-            Binding binding = getBinding (firstComponent);
-            if (binding == null)
-                throw new NameNotFoundException ();
-
-            ctx = binding.getObject();
-
-            if (ctx instanceof Reference)
-            {
-                //deference the object
-                if(__log.isDebugEnabled())__log.debug("Dereferencing Reference for "+name.get(0));
-                try
-                {
-                    ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                }
-                catch (NamingException e)
-                {
-                    throw e;
-                }
-                catch (Exception e)
-                {
-                    __log.warn("",e);
-                    throw new NamingException (e.getMessage());
-                }
-            }
-        }
-
-        if (!(ctx instanceof Context))
-            throw new NotContextException();
-
-        return ((Context)ctx).list (cname.getSuffix(1));
+        return getContext(cname).list (cname.getSuffix(1));
     }
 
-
     /*------------------------------------------------*/
     /**
      * List all names bound at Context named by Name
@@ -789,8 +619,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
         return list(_parser.parse(name));
     }
 
-
-
     /*------------------------------------------------*/
     /**
      * List all Bindings present at Context named by Name
@@ -815,48 +643,7 @@ public class NamingContext implements Context, Cloneable, Dumpable
            return new BindingEnumeration (_bindings.values().iterator());
         }
 
-
-
-        //multipart name
-        String firstComponent = cname.get(0);
-        Object ctx = null;
-
-        //if a name has a leading "/" it is parsed as "" so ignore it by staying
-        //at this level in the tree
-        if (firstComponent.equals(""))
-            ctx = this;
-        else
-        {
-            //it is a non-empty name component
-            Binding binding = getBinding (firstComponent);
-            if (binding == null)
-                throw new NameNotFoundException ();
-
-            ctx = binding.getObject();
-
-            if (ctx instanceof Reference)
-            {
-                //deference the object
-                try
-                {
-                    ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                }
-                catch (NamingException e)
-                {
-                    throw e;
-                }
-                catch (Exception e)
-                {
-                    __log.warn("",e);
-                    throw new NamingException (e.getMessage());
-                }
-            }
-        }
-
-        if (!(ctx instanceof Context))
-            throw new NotContextException();
-
-        return ((Context)ctx).listBindings (cname.getSuffix(1));
+        return getContext(cname).listBindings (cname.getSuffix(1));
     }
 
 
@@ -917,49 +704,7 @@ public class NamingContext implements Context, Cloneable, Dumpable
         }
         else
         {
-            //walk down the subcontext hierarchy
-            if(__log.isDebugEnabled())__log.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
-
-            String firstComponent = cname.get(0);
-            Object ctx = null;
-
-
-            if (firstComponent.equals(""))
-                ctx = this;
-            else
-            {
-                Binding  binding = getBinding (name.get(0));
-                if (binding == null)
-                    throw new NameNotFoundException (name.get(0)+ " is not bound");
-
-                ctx = binding.getObject();
-
-
-                if (ctx instanceof Reference)
-                {
-                    //deference the object
-                    try
-                    {
-                        ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
-                }
-            }
-
-            if (ctx instanceof Context)
-            {
-                ((Context)ctx).rebind (cname.getSuffix(1), obj);
-            }
-            else
-                throw new NotContextException ("Object bound at "+firstComponent +" is not a Context");
+            getContext(cname).rebind (cname.getSuffix(1), obj);
         }
     }
 
@@ -1020,7 +765,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
         if (cname.size() == 0)
             throw new NamingException ("Name is empty");
 
-
         //if no subcontexts, just unbind it
         if (cname.size() == 1)
         {
@@ -1028,48 +772,7 @@ public class NamingContext implements Context, Cloneable, Dumpable
         }
         else
         {
-            //walk down the subcontext hierarchy
-            if(__log.isDebugEnabled())__log.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
-
-            String firstComponent = cname.get(0);
-            Object ctx = null;
-
-
-            if (firstComponent.equals(""))
-                ctx = this;
-            else
-            {
-                Binding  binding = getBinding (name.get(0));
-                if (binding == null)
-                    throw new NameNotFoundException (name.get(0)+ " is not bound");
-
-                ctx = binding.getObject();
-
-                if (ctx instanceof Reference)
-                {
-                    //deference the object
-                    try
-                    {
-                        ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
-                }
-            }
-
-            if (ctx instanceof Context)
-            {
-                ((Context)ctx).unbind (cname.getSuffix(1));
-            }
-            else
-                throw new NotContextException ("Object bound at "+firstComponent +" is not a Context");
+            getContext(cname).unbind (cname.getSuffix(1));
         }
     }
 
@@ -1198,7 +901,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
         return _parser;
     }
 
-
     /*------------------------------------------------*/
     /**
      * Get the full name of this Context node
@@ -1227,7 +929,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
         return name.toString();
     }
 
-
     /*------------------------------------------------*/
     /**
      * Add an environment setting to this Context
@@ -1242,13 +943,25 @@ public class NamingContext implements Context, Cloneable, Dumpable
                                    Object propVal)
         throws NamingException
     {
-        if (isLocked() && !(propName.equals(UNLOCK_PROPERTY)))
-            throw new NamingException ("This context is immutable");
+        switch(propName)
+        {
+            case LOCK_PROPERTY:
+                if (_lock == null)
+                    _lock = propVal;
+                return null;
 
-        return _env.put (propName, propVal);
+            case UNLOCK_PROPERTY:
+                if (propVal != null && propVal.equals(_lock))
+                    _lock = null;
+                return null;
+
+            default:
+                if (isLocked())
+                    throw new NamingException("This context is immutable");
+                return _env.put(propName, propVal);
+        }
     }
 
-
     /*------------------------------------------------*/
     /**
      * Remove a property from this Context's environment.
@@ -1277,7 +990,7 @@ public class NamingContext implements Context, Cloneable, Dumpable
     @Override
     public Hashtable getEnvironment ()
     {
-        return (Hashtable)_env.clone();
+        return _env;
     }
 
     /*------------------------------------------------*/
@@ -1302,13 +1015,15 @@ public class NamingContext implements Context, Cloneable, Dumpable
                 break;
         }
 
-        if(__log.isDebugEnabled())
-            __log.debug("Adding binding with key="+key+" obj="+obj+" for context="+_name+" as "+binding);
+        if(LOG.isDebugEnabled())
+            LOG.debug("Adding binding with key="+key+" obj="+obj+" for context="+_name+" as "+binding);
 
         if (binding!=null)
         {
-            if (_bindings.containsKey(key)) {
-                if(_supportDeepBinding) {
+            if (_bindings.putIfAbsent(key, binding)!=null)
+            {
+                if(isDeepBindingSupported())
+                {
                     // quietly return (no exception)
                     // this is jndi spec breaking, but is added to support broken
                     // jndi users like openejb.
@@ -1316,7 +1031,6 @@ public class NamingContext implements Context, Cloneable, Dumpable
                 }
                 throw new NameAlreadyBoundException(name.toString());
             }
-            _bindings.put(key,binding);
         }
     }
 
@@ -1329,10 +1043,9 @@ public class NamingContext implements Context, Cloneable, Dumpable
      */
     public Binding getBinding (Name name)
     {
-        return (Binding) _bindings.get(name.toString());
+        return _bindings.get(name.toString());
     }
 
-
     /*------------------------------------------------*/
     /**
      * Get a name to object binding from this Context
@@ -1342,15 +1055,15 @@ public class NamingContext implements Context, Cloneable, Dumpable
      */
     public Binding getBinding (String name)
     {
-        return (Binding) _bindings.get(name);
+        return _bindings.get(name);
     }
 
     /*------------------------------------------------*/
     public void removeBinding (Name name)
     {
         String key = name.toString();
-        if (__log.isDebugEnabled())
-            __log.debug("Removing binding with key="+key);
+        if (LOG.isDebugEnabled())
+            LOG.debug("Removing binding with key="+key);
         Binding binding = _bindings.remove(key);
         if (binding!=null)
         {
@@ -1390,65 +1103,22 @@ public class NamingContext implements Context, Cloneable, Dumpable
     /* ------------------------------------------------------------ */
     public boolean isLocked()
     {
-       if ((_env.get(LOCK_PROPERTY) == null) && (_env.get(UNLOCK_PROPERTY) == null))
-           return false;
-
-       Object lockKey = _env.get(LOCK_PROPERTY);
-       Object unlockKey = _env.get(UNLOCK_PROPERTY);
-
-       if ((lockKey != null) && (unlockKey != null) && (lockKey.equals(unlockKey)))
-           return false;
-       return true;
+        //TODO lock whole hierarchy?
+        return _lock != null;
     }
 
-
     /* ------------------------------------------------------------ */
     @Override
     public String dump()
     {
-        StringBuilder buf = new StringBuilder();
-        try
-        {
-            dump(buf,"");
-        }
-        catch(Exception e)
-        {
-            __log.warn(e);
-        }
-        return buf.toString();
+        return Dumpable.dump(this);
     }
 
-
     /* ------------------------------------------------------------ */
     @Override
     public void dump(Appendable out,String indent) throws IOException
     {
-        out.append(this.getClass().getSimpleName()).append("@").append(Long.toHexString(this.hashCode())).append("\n");
-        int size=_bindings.size();
-        int i=0;
-        for (Map.Entry entry : ((Map)_bindings).entrySet())
-        {
-            boolean last=++i==size;
-            out.append(indent).append(" +- ").append(entry.getKey()).append(": ");
-
-            Binding binding = entry.getValue();
-            Object value = binding.getObject();
-
-            if ("comp".equals(entry.getKey()) && value instanceof Reference && "org.eclipse.jetty.jndi.ContextFactory".equals(((Reference)value).getFactoryClassName()))
-            {
-                ContextFactory.dump(out,indent+(last?"    ":" |  "));
-            }
-            else if (value instanceof Dumpable)
-            {
-                ((Dumpable)value).dump(out,indent+(last?"    ":" |  "));
-            }
-            else
-            {
-                out.append(value.getClass().getSimpleName()).append("=");
-                out.append(String.valueOf(value).replace('\n','|').replace('\r','|'));
-                out.append("\n");
-            }
-        }
+        Dumpable.dumpObjects(out,indent,this, _bindings);
     }
 
     private Collection findListeners()
diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingUtil.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingUtil.java
index c0053dd6ab1..47b5a404b26 100644
--- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingUtil.java
+++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingUtil.java
@@ -20,7 +20,6 @@ package org.eclipse.jetty.jndi;
 
 import java.util.HashMap;
 import java.util.Map;
-
 import javax.naming.Binding;
 import javax.naming.Context;
 import javax.naming.Name;
@@ -29,6 +28,7 @@ import javax.naming.NameParser;
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
 
+import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
 
 
@@ -37,7 +37,7 @@ import org.eclipse.jetty.util.log.Logger;
  */
 public class NamingUtil
 {
-    public final static Logger __log=org.eclipse.jetty.util.log.Log.getLogger("jndi");
+    private static final Logger LOG = Log.getLogger(NamingUtil.class);
 
     /* ------------------------------------------------------------ */
     /**
@@ -67,20 +67,20 @@ public class NamingUtil
             try
             {
                 subCtx = (Context)subCtx.lookup (name.get(i));
-                if(__log.isDebugEnabled())
-                    __log.debug("Subcontext "+name.get(i)+" already exists");
+                if(LOG.isDebugEnabled())
+                    LOG.debug("Subcontext "+name.get(i)+" already exists");
             }
             catch (NameNotFoundException e)
             {
                 subCtx = subCtx.createSubcontext(name.get(i));
-                if(__log.isDebugEnabled())
-                    __log.debug("Subcontext "+name.get(i)+" created");
+                if(LOG.isDebugEnabled())
+                    LOG.debug("Subcontext "+name.get(i)+" created");
             }
         }
 
         subCtx.rebind (name.get(name.size() - 1), obj);
-        if(__log.isDebugEnabled())
-            __log.debug("Bound object to "+name.get(name.size() - 1));
+        if(LOG.isDebugEnabled())
+            LOG.debug("Bound object to "+name.get(name.size() - 1));
         return subCtx;
     }
 
diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/java/javaRootURLContext.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/java/javaRootURLContext.java
index f32344e60d5..2d5b5117feb 100644
--- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/java/javaRootURLContext.java
+++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/java/javaRootURLContext.java
@@ -18,9 +18,7 @@
 
 package org.eclipse.jetty.jndi.java;
 
-
 import java.util.Hashtable;
-
 import javax.naming.Context;
 import javax.naming.Name;
 import javax.naming.NameParser;
@@ -31,7 +29,7 @@ import javax.naming.StringRefAddr;
 
 import org.eclipse.jetty.jndi.ContextFactory;
 import org.eclipse.jetty.jndi.NamingContext;
-import org.eclipse.jetty.jndi.NamingUtil;
+import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
 
 
@@ -46,7 +44,7 @@ import org.eclipse.jetty.util.log.Logger;
  */
 public class javaRootURLContext implements Context
 {
-    private static Logger __log = NamingUtil.__log;
+    private static final Logger LOG = Log.getLogger(javaRootURLContext.class);
 
     public static final String URL_PREFIX = "java:";
 
@@ -76,7 +74,7 @@ public class javaRootURLContext implements Context
         }
         catch (Exception e)
         {
-            __log.warn(e);
+            LOG.warn(e);
         }
     }
 
@@ -319,7 +317,8 @@ public class javaRootURLContext implements Context
         {
             String head = name.get(0);
 
-            if(__log.isDebugEnabled())__log.debug("Head element of name is: "+head);
+            if(LOG.isDebugEnabled())
+                LOG.debug("Head element of name is: "+head);
 
             if (head.startsWith(URL_PREFIX))
             {
@@ -328,7 +327,8 @@ public class javaRootURLContext implements Context
                 if (head.length() > 0)
                     name.add(0, head);
 
-                if(__log.isDebugEnabled())__log.debug("name modified to "+name.toString());
+                if(LOG.isDebugEnabled())
+                    LOG.debug("name modified to "+name.toString());
             }
         }
 
diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/local/localContextRoot.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/local/localContextRoot.java
index 1fb4183b438..f712d329be6 100644
--- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/local/localContextRoot.java
+++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/local/localContextRoot.java
@@ -18,11 +18,8 @@
 
 package org.eclipse.jetty.jndi.local;
 
-import java.util.Collections;
 import java.util.Hashtable;
-import java.util.List;
 import java.util.Properties;
-
 import javax.naming.Binding;
 import javax.naming.CompoundName;
 import javax.naming.Context;
@@ -40,10 +37,8 @@ import javax.naming.Reference;
 import javax.naming.Referenceable;
 import javax.naming.spi.NamingManager;
 
-import org.eclipse.jetty.jndi.BindingEnumeration;
-import org.eclipse.jetty.jndi.NameEnumeration;
 import org.eclipse.jetty.jndi.NamingContext;
-import org.eclipse.jetty.jndi.NamingUtil;
+import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
 
 /**
@@ -58,11 +53,10 @@ import org.eclipse.jetty.util.log.Logger;
  */
 public class localContextRoot implements Context
 {
-    private final static Logger __log=NamingUtil.__log;
+    private static final Logger LOG = Log.getLogger(localContextRoot.class);
     protected final static NamingContext __root = new NamingRoot();
     private final Hashtable _env;
 
-
     static class NamingRoot extends NamingContext
     {
         public NamingRoot()
@@ -71,8 +65,6 @@ public class localContextRoot implements Context
         }
     }
 
-
-
     static class LocalNameParser implements NameParser
     {
         Properties syntax = new Properties();
@@ -100,10 +92,6 @@ public class localContextRoot implements Context
      */
 
 
-
-
-
-
     public static NamingContext getRoot()
     {
         return __root;
@@ -122,7 +110,6 @@ public class localContextRoot implements Context
     @Override
     public void close() throws NamingException
     {
-
     }
 
     /**
@@ -136,7 +123,6 @@ public class localContextRoot implements Context
         return "";
     }
 
-
     /**
      *
      *
@@ -145,13 +131,9 @@ public class localContextRoot implements Context
     @Override
     public void destroySubcontext(Name name) throws NamingException
     {
-        synchronized (__root)
-        {
-            __root.destroySubcontext(getSuffix(name));
-        }
+        __root.destroySubcontext(getSuffix(name));
     }
 
-
     /**
      *
      *
@@ -160,14 +142,9 @@ public class localContextRoot implements Context
     @Override
     public void destroySubcontext(String name) throws NamingException
     {
-        synchronized (__root)
-        {
-
-           destroySubcontext(__root.getNameParser("").parse(getSuffix(name)));
-        }
+        destroySubcontext(__root.getNameParser("").parse(getSuffix(name)));
     }
 
-
     /**
      *
      *
@@ -179,7 +156,49 @@ public class localContextRoot implements Context
         return _env;
     }
 
+    private Object dereference(Object ctx, String firstComponent) throws NamingException
+    {
+        if (ctx instanceof Reference)
+        {
+            //deference the object
+            try
+            {
+                return NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), __root, _env);
+            }
+            catch (NamingException e)
+            {
+                throw e;
+            }
+            catch (Exception e)
+            {
+                LOG.warn(e);
+                throw new NamingException (e.getMessage());
+            }
+        }
+        return ctx;
+    }
 
+    private Context getContext(Name cname) throws NamingException
+    {
+        String firstComponent = cname.get(0);
+
+        if (firstComponent.equals(""))
+            return this;
+
+        Binding binding = __root.getBinding (firstComponent);
+        if (binding == null)
+        {
+            NameNotFoundException nnfe = new NameNotFoundException(firstComponent + " is not bound");
+            nnfe.setRemainingName(cname);
+            throw nnfe;
+        }
+        Object ctx = dereference(binding.getObject(), firstComponent);
+
+        if (!(ctx instanceof Context))
+            throw new NotContextException(firstComponent + " not a context in " + this.getNameInNamespace());
+
+        return (Context)ctx;
+    }
 
     /**
      *
@@ -189,79 +208,30 @@ public class localContextRoot implements Context
     @Override
     public void unbind(Name name) throws NamingException
     {
-        synchronized (__root)
+        //__root.unbind(getSuffix(name));
+
+        if (name.size() == 0)
+            return;
+
+        if (__root.isLocked())
+            throw new NamingException ("This context is immutable");
+
+        Name cname = __root.toCanonicalName(name);
+
+        if (cname == null)
+            throw new NamingException ("Name is null");
+
+        if (cname.size() == 0)
+            throw new NamingException ("Name is empty");
+
+        //if no subcontexts, just unbind it
+        if (cname.size() == 1)
         {
-            //__root.unbind(getSuffix(name));
-
-            if (name.size() == 0)
-                return;
-
-
-            if (__root.isLocked())
-                throw new NamingException ("This context is immutable");
-
-            Name cname = __root.toCanonicalName(name);
-
-            if (cname == null)
-                throw new NamingException ("Name is null");
-
-            if (cname.size() == 0)
-                throw new NamingException ("Name is empty");
-
-
-            //if no subcontexts, just unbind it
-            if (cname.size() == 1)
-            {
-                __root.removeBinding (cname);
-            }
-            else
-            {
-                //walk down the subcontext hierarchy
-                if(__log.isDebugEnabled())__log.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
-
-                String firstComponent = cname.get(0);
-                Object ctx = null;
-
-
-                if (firstComponent.equals(""))
-                    ctx = this;
-                else
-                {
-                    Binding  binding = __root.getBinding (name.get(0));
-                    if (binding == null)
-                        throw new NameNotFoundException (name.get(0)+ " is not bound");
-
-                    ctx = binding.getObject();
-
-                    if (ctx instanceof Reference)
-                    {
-                        //deference the object
-                        try
-                        {
-                            ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), __root, _env);
-                        }
-                        catch (NamingException e)
-                        {
-                            throw e;
-                        }
-                        catch (Exception e)
-                        {
-                            __log.warn("",e);
-                            throw new NamingException (e.getMessage());
-                        }
-                    }
-                }
-
-                if (ctx instanceof Context)
-                {
-                    ((Context)ctx).unbind (cname.getSuffix(1));
-                }
-                else
-                    throw new NotContextException ("Object bound at "+firstComponent +" is not a Context");
-            }
-
-
-
+            __root.removeBinding (cname);
+        }
+        else
+        {
+            getContext(cname).unbind (cname.getSuffix(1));
         }
     }
 
@@ -276,8 +246,6 @@ public class localContextRoot implements Context
         unbind(__root.getNameParser("").parse(getSuffix(name)));
     }
 
-
-
     /**
      *
      *
@@ -286,10 +254,7 @@ public class localContextRoot implements Context
     @Override
     public Object lookupLink(String name) throws NamingException
     {
-        synchronized (__root)
-        {
-            return lookupLink(__root.getNameParser("").parse(getSuffix(name)));
-        }
+        return lookupLink(__root.getNameParser("").parse(getSuffix(name)));
     }
 
     /**
@@ -300,101 +265,54 @@ public class localContextRoot implements Context
     @Override
     public Object lookupLink(Name name) throws NamingException
     {
-        synchronized (__root)
+        Name cname = __root.toCanonicalName(name);
+
+        if (cname == null || cname.isEmpty())
         {
-            //return __root.lookupLink(getSuffix(name));
+            //If no name create copy of this context with same bindings, but with copy of the environment so it can be modified
+            return __root.shallowCopy();
+        }
 
+        if (cname.size() == 0)
+            throw new NamingException ("Name is empty");
 
-            Name cname = __root.toCanonicalName(name);
+        if (cname.size() == 1)
+        {
+            Binding binding = __root.getBinding (cname);
+            if (binding == null)
+                throw new NameNotFoundException();
 
-            if (cname == null)
+            Object o = binding.getObject();
+
+            //handle links by looking up the link
+            if (o instanceof Reference)
             {
-                //If no name create copy of this context with same bindings, but with copy of the environment so it can be modified
-                NamingContext ctx = new NamingContext (_env, null, null, __root.getNameParser(""));
-                ctx.setBindings(__root.getBindings());
-                return ctx;
-            }
-
-            if (cname.size() == 0)
-                throw new NamingException ("Name is empty");
-
-            if (cname.size() == 1)
-            {
-                Binding binding = __root.getBinding (cname);
-                if (binding == null)
-                    throw new NameNotFoundException();
-
-                Object o = binding.getObject();
-
-                //handle links by looking up the link
-                if (o instanceof Reference)
+                //deference the object
+                try
                 {
-                    //deference the object
-                    try
-                    {
-                        return NamingManager.getObjectInstance(o, cname.getPrefix(1), __root, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
+                    return NamingManager.getObjectInstance(o, cname.getPrefix(1), __root, _env);
                 }
-                else
+                catch (NamingException e)
                 {
-                    //object is either a LinkRef which we don't dereference
-                    //or a plain object in which case spec says we return it
-                    return o;
+                    throw e;
+                }
+                catch (Exception e)
+                {
+                    LOG.warn("",e);
+                    throw new NamingException (e.getMessage());
                 }
             }
-
-
-            //it is a multipart name, recurse to the first subcontext
-            String firstComponent = cname.get(0);
-            Object ctx = null;
-
-            if (firstComponent.equals(""))
-                ctx = this;
             else
             {
-                Binding binding = __root.getBinding (firstComponent);
-                if (binding == null)
-                    throw new NameNotFoundException ();
-
-                ctx = binding.getObject();
-
-                if (ctx instanceof Reference)
-                {
-                    //deference the object
-                    try
-                    {
-                        ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), __root, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
-                }
+                //object is either a LinkRef which we don't dereference
+                //or a plain object in which case spec says we return it
+                return o;
             }
-
-            if (!(ctx instanceof Context))
-                throw new NotContextException();
-
-            return ((Context)ctx).lookup (cname.getSuffix(1));
-
-
         }
-    }
 
+        //it is a multipart name, recurse to the first subcontext
+        return getContext(cname).lookup (cname.getSuffix(1));
+    }
 
     /**
      *
@@ -407,7 +325,6 @@ public class localContextRoot implements Context
         return _env.remove(propName);
     }
 
-
     /**
      *
      *
@@ -416,122 +333,66 @@ public class localContextRoot implements Context
     @Override
     public Object lookup(Name name) throws NamingException
     {
-        synchronized (__root)
+        if(LOG.isDebugEnabled())
+            LOG.debug("Looking up name=\""+name+"\"");
+        Name cname = __root.toCanonicalName(name);
+
+        if ((cname == null) || cname.isEmpty())
         {
-            //return __root.lookup(getSuffix(name));
-
-            if(__log.isDebugEnabled())__log.debug("Looking up name=\""+name+"\"");
-            Name cname = __root.toCanonicalName(name);
-
-            if ((cname == null) || (cname.size() == 0))
-            {
-                __log.debug("Null or empty name, returning copy of this context");
-                NamingContext ctx = new NamingContext (_env, null, null, __root.getNameParser(""));
-                ctx.setBindings(__root.getBindings());
-                return ctx;
-            }
-
-
-
-            if (cname.size() == 1)
-            {
-                Binding binding = __root.getBinding (cname);
-                if (binding == null)
-                {
-                    NameNotFoundException nnfe = new NameNotFoundException();
-                    nnfe.setRemainingName(cname);
-                    throw nnfe;
-                }
-
-
-                Object o = binding.getObject();
-
-                //handle links by looking up the link
-                if (o instanceof LinkRef)
-                {
-                    //if link name starts with ./ it is relative to current context
-                    String linkName = ((LinkRef)o).getLinkName();
-                    if (linkName.startsWith("./"))
-                        return lookup (linkName.substring(2));
-                    else
-                    {
-                        //link name is absolute
-                        InitialContext ictx = new InitialContext();
-                        return ictx.lookup (linkName);
-                    }
-                }
-                else if (o instanceof Reference)
-                {
-                    //deference the object
-                    try
-                    {
-                        return NamingManager.getObjectInstance(o, cname, __root, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (final Exception e)
-                    {
-                        throw new NamingException (e.getMessage())
-                        {
-                            { initCause(e);}
-                        };
-                    }
-                }
-                else
-                    return o;
-            }
-
-            //it is a multipart name, get the first subcontext
-
-            String firstComponent = cname.get(0);
-            Object ctx = null;
-
-            if (firstComponent.equals(""))
-                ctx = this;
-            else
-            {
-
-                Binding binding = __root.getBinding (firstComponent);
-                if (binding == null)
-                {
-                    NameNotFoundException nnfe = new NameNotFoundException();
-                    nnfe.setRemainingName(cname);
-                    throw nnfe;
-                }
-
-                //as we have bound a reference to an object factory
-                //for the component specific contexts
-                //at "comp" we need to resolve the reference
-                ctx = binding.getObject();
-
-                if (ctx instanceof Reference)
-                {
-                    //deference the object
-                    try
-                    {
-                        ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), __root, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
-                }
-            }
-            if (!(ctx instanceof Context))
-                throw new NotContextException();
-
-            return ((Context)ctx).lookup (cname.getSuffix(1));
-
+            return __root.shallowCopy();
         }
-    }
 
+        if (cname.size() == 1)
+        {
+            Binding binding = __root.getBinding (cname);
+            if (binding == null)
+            {
+                NameNotFoundException nnfe = new NameNotFoundException();
+                nnfe.setRemainingName(cname);
+                throw nnfe;
+            }
+
+            Object o = binding.getObject();
+
+            //handle links by looking up the link
+            if (o instanceof LinkRef)
+            {
+                //if link name starts with ./ it is relative to current context
+                String linkName = ((LinkRef)o).getLinkName();
+                if (linkName.startsWith("./"))
+                    return lookup (linkName.substring(2));
+                else
+                {
+                    //link name is absolute
+                    InitialContext ictx = new InitialContext();
+                    return ictx.lookup (linkName);
+                }
+            }
+            else if (o instanceof Reference)
+            {
+                // TODO use deference
+                try
+                {
+                    return NamingManager.getObjectInstance(o, cname, __root, _env);
+                }
+                catch (NamingException e)
+                {
+                    throw e;
+                }
+                catch (final Exception e)
+                {
+                    throw new NamingException (e.getMessage())
+                    {
+                        { initCause(e);}
+                    };
+                }
+            }
+            else
+                return o;
+        }
+
+        return getContext(cname).lookup(cname.getSuffix(1));
+    }
 
     /**
      *
@@ -541,13 +402,9 @@ public class localContextRoot implements Context
     @Override
     public Object lookup(String name) throws NamingException
     {
-        synchronized (__root)
-        {
-            return lookup(__root.getNameParser("").parse(getSuffix(name)));
-        }
+        return lookup(__root.getNameParser("").parse(getSuffix(name)));
     }
 
-
     /**
      *
      *
@@ -556,14 +413,9 @@ public class localContextRoot implements Context
     @Override
     public void bind(String name, Object obj) throws NamingException
     {
-        synchronized (__root)
-        {
-           bind(__root.getNameParser("").parse(getSuffix(name)), obj);
-
-        }
+        bind(__root.getNameParser("").parse(getSuffix(name)), obj);
     }
 
-
     /**
      *
      *
@@ -572,85 +424,37 @@ public class localContextRoot implements Context
     @Override
     public void bind(Name name, Object obj) throws NamingException
     {
-        synchronized (__root)
+        if (__root.isLocked())
+            throw new NamingException ("This context is immutable");
+
+        Name cname = __root.toCanonicalName(name);
+
+        if (cname == null)
+            throw new NamingException ("Name is null");
+
+        if (cname.size() == 0)
+            throw new NamingException ("Name is empty");
+
+        //if no subcontexts, just bind it
+        if (cname.size() == 1)
         {
-           // __root.bind(getSuffix(name), obj);
-
-
-            if (__root.isLocked())
-                throw new NamingException ("This context is immutable");
-
-            Name cname = __root.toCanonicalName(name);
-
-            if (cname == null)
-                throw new NamingException ("Name is null");
-
-            if (cname.size() == 0)
-                throw new NamingException ("Name is empty");
-
-
-            //if no subcontexts, just bind it
-            if (cname.size() == 1)
+            //get the object to be bound
+            Object objToBind = NamingManager.getStateToBind(obj, name,this, _env);
+            // Check for Referenceable
+            if (objToBind instanceof Referenceable)
             {
-                //get the object to be bound
-                Object objToBind = NamingManager.getStateToBind(obj, name,this, _env);
-                // Check for Referenceable
-                if (objToBind instanceof Referenceable)
-                {
-                    objToBind = ((Referenceable)objToBind).getReference();
-                }
-
-                //anything else we should be able to bind directly
-                __root.addBinding (cname, objToBind);
+                objToBind = ((Referenceable)objToBind).getReference();
             }
-            else
-            {
-                if(__log.isDebugEnabled())__log.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
 
-                //walk down the subcontext hierarchy
-                //need to ignore trailing empty "" name components
+            //anything else we should be able to bind directly
+            __root.addBinding (cname, objToBind);
+        }
+        else
+        {
+            if(LOG.isDebugEnabled())
+                LOG.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
 
-                String firstComponent = cname.get(0);
-                Object ctx = null;
-
-                if (firstComponent.equals(""))
-                    ctx = this;
-                else
-                {
-
-                    Binding  binding = __root.getBinding (firstComponent);
-                    if (binding == null)
-                        throw new NameNotFoundException (firstComponent+ " is not bound");
-
-                    ctx = binding.getObject();
-
-                    if (ctx instanceof Reference)
-                    {
-                        //deference the object
-                        try
-                        {
-                            ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), this, _env);
-                        }
-                        catch (NamingException e)
-                        {
-                            throw e;
-                        }
-                        catch (Exception e)
-                        {
-                            __log.warn("",e);
-                            throw new NamingException (e.getMessage());
-                        }
-                    }
-                }
-
-
-                if (ctx instanceof Context)
-                {
-                    ((Context)ctx).bind (cname.getSuffix(1), obj);
-                }
-                else
-                    throw new NotContextException ("Object bound at "+firstComponent +" is not a Context");
-            }
+            getContext(cname).bind (cname.getSuffix(1), obj);
         }
     }
 
@@ -662,82 +466,37 @@ public class localContextRoot implements Context
     @Override
     public void rebind(Name name, Object obj) throws NamingException
     {
-        synchronized (__root)
+        if (__root.isLocked())
+            throw new NamingException ("This context is immutable");
+
+        Name cname = __root.toCanonicalName(name);
+
+        if (cname == null)
+            throw new NamingException ("Name is null");
+
+        if (cname.size() == 0)
+            throw new NamingException ("Name is empty");
+
+        //if no subcontexts, just bind it
+        if (cname.size() == 1)
         {
-            //__root.rebind(getSuffix(name), obj);
+            //check if it is a Referenceable
+            Object objToBind = NamingManager.getStateToBind(obj, name, __root, _env);
 
-
-            if (__root.isLocked())
-                throw new NamingException ("This context is immutable");
-
-            Name cname = __root.toCanonicalName(name);
-
-            if (cname == null)
-                throw new NamingException ("Name is null");
-
-            if (cname.size() == 0)
-                throw new NamingException ("Name is empty");
-
-
-            //if no subcontexts, just bind it
-            if (cname.size() == 1)
+            if (objToBind instanceof Referenceable)
             {
-                //check if it is a Referenceable
-                Object objToBind = NamingManager.getStateToBind(obj, name, __root, _env);
-
-                if (objToBind instanceof Referenceable)
-                {
-                    objToBind = ((Referenceable)objToBind).getReference();
-                }
-                __root.removeBinding(cname);
-                __root.addBinding (cname, objToBind);
+                objToBind = ((Referenceable)objToBind).getReference();
             }
-            else
-            {
-                //walk down the subcontext hierarchy
-                if(__log.isDebugEnabled())__log.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
+            __root.removeBinding(cname);
+            __root.addBinding (cname, objToBind);
+        }
+        else
+        {
+            //walk down the subcontext hierarchy
+            if(LOG.isDebugEnabled())
+                LOG.debug("Checking for existing binding for name="+cname+" for first element of name="+cname.get(0));
 
-                String firstComponent = cname.get(0);
-                Object ctx = null;
-
-
-                if (firstComponent.equals(""))
-                    ctx = this;
-                else
-                {
-                    Binding  binding = __root.getBinding (name.get(0));
-                    if (binding == null)
-                        throw new NameNotFoundException (name.get(0)+ " is not bound");
-
-                    ctx = binding.getObject();
-
-
-                    if (ctx instanceof Reference)
-                    {
-                        //deference the object
-                        try
-                        {
-                            ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), __root, _env);
-                        }
-                        catch (NamingException e)
-                        {
-                            throw e;
-                        }
-                        catch (Exception e)
-                        {
-                            __log.warn("",e);
-                            throw new NamingException (e.getMessage());
-                        }
-                    }
-                }
-
-                if (ctx instanceof Context)
-                {
-                    ((Context)ctx).rebind (cname.getSuffix(1), obj);
-                }
-                else
-                    throw new NotContextException ("Object bound at "+firstComponent +" is not a Context");
-            }
+            getContext(cname).rebind (cname.getSuffix(1), obj);
         }
     }
 
@@ -749,11 +508,9 @@ public class localContextRoot implements Context
     @Override
     public void rebind(String name, Object obj) throws NamingException
     {
-        synchronized (__root)
-        {
-            rebind(__root.getNameParser("").parse(getSuffix(name)), obj);
-        }
+        rebind(__root.getNameParser("").parse(getSuffix(name)), obj);
     }
+
     /**
      *
      *
@@ -762,10 +519,7 @@ public class localContextRoot implements Context
     @Override
     public void rename(Name oldName, Name newName) throws NamingException
     {
-        synchronized (__root)
-        {
-            throw new OperationNotSupportedException();
-        }
+        throw new OperationNotSupportedException();
     }
 
     /**
@@ -776,10 +530,7 @@ public class localContextRoot implements Context
     @Override
     public void rename(String oldName, String newName) throws NamingException
     {
-        synchronized (__root)
-        {
-           throw new OperationNotSupportedException();
-        }
+        throw new OperationNotSupportedException();
     }
 
     /**
@@ -790,18 +541,15 @@ public class localContextRoot implements Context
     @Override
     public Context createSubcontext(String name) throws NamingException
     {
-        synchronized (__root)
-        {
-            //if the subcontext comes directly off the root, use the env of the InitialContext
-            //as the root itself has no environment. Otherwise, it inherits the env of the parent
-            //Context further down the tree.
-            //NamingContext ctx = (NamingContext)__root.createSubcontext(name);
-            //if (ctx.getParent() == __root)
-            //    ctx.setEnv(_env);
-            //return ctx;
+        //if the subcontext comes directly off the root, use the env of the InitialContext
+        //as the root itself has no environment. Otherwise, it inherits the env of the parent
+        //Context further down the tree.
+        //NamingContext ctx = (NamingContext)__root.createSubcontext(name);
+        //if (ctx.getParent() == __root)
+        //    ctx.setEnv(_env);
+        //return ctx;
 
-            return createSubcontext(__root.getNameParser("").parse(name));
-        }
+        return createSubcontext(__root.getNameParser("").parse(name));
     }
 
     /**
@@ -812,92 +560,45 @@ public class localContextRoot implements Context
     @Override
     public Context createSubcontext(Name name) throws NamingException
     {
-        synchronized (__root)
+        //if the subcontext comes directly off the root, use the env of the InitialContext
+        //as the root itself has no environment. Otherwise, it inherits the env of the parent
+        //Context further down the tree.
+        //NamingContext ctx = (NamingContext)__root.createSubcontext(getSuffix(name));
+        //if (ctx.getParent() == __root)
+        //    ctx.setEnv(_env);
+        //return ctx;
+
+        if (__root.isLocked())
         {
-            //if the subcontext comes directly off the root, use the env of the InitialContext
-            //as the root itself has no environment. Otherwise, it inherits the env of the parent
-            //Context further down the tree.
-            //NamingContext ctx = (NamingContext)__root.createSubcontext(getSuffix(name));
-            //if (ctx.getParent() == __root)
-            //    ctx.setEnv(_env);
-            //return ctx;
-
-
-
-
-            if (__root.isLocked())
-            {
-                NamingException ne = new NamingException ("This context is immutable");
-                ne.setRemainingName(name);
-                throw ne;
-            }
-
-            Name cname = __root.toCanonicalName (name);
-
-            if (cname == null)
-                throw new NamingException ("Name is null");
-            if (cname.size() == 0)
-                throw new NamingException ("Name is empty");
-
-            if (cname.size() == 1)
-            {
-                //not permitted to bind if something already bound at that name
-                Binding binding = __root.getBinding (cname);
-                if (binding != null)
-                    throw new NameAlreadyBoundException (cname.toString());
-
-                //make a new naming context with the root as the parent
-                Context ctx = new NamingContext ((Hashtable)_env.clone(), cname.get(0), __root,  __root.getNameParser(""));
-                __root.addBinding (cname, ctx);
-                return ctx;
-            }
-
-
-            //If the name has multiple subcontexts, walk the hierarchy by
-            //fetching the first one. All intermediate subcontexts in the
-            //name must already exist.
-            String firstComponent = cname.get(0);
-            Object ctx = null;
-
-            if (firstComponent.equals(""))
-                ctx = this;
-            else
-            {
-                Binding binding = __root.getBinding (firstComponent);
-                if (binding == null)
-                    throw new NameNotFoundException (firstComponent + " is not bound");
-
-                ctx = binding.getObject();
-
-                if (ctx instanceof Reference)
-                {
-                    //deference the object
-                    if(__log.isDebugEnabled())__log.debug("Object bound at "+firstComponent +" is a Reference");
-                    try
-                    {
-                        ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), __root, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
-                }
-            }
-
-            if (ctx instanceof Context)
-            {
-                return ((Context)ctx).createSubcontext (cname.getSuffix(1));
-            }
-            else
-                throw new NotContextException (firstComponent +" is not a Context");
+            NamingException ne = new NamingException ("This context is immutable");
+            ne.setRemainingName(name);
+            throw ne;
         }
-    }
 
+        Name cname = __root.toCanonicalName (name);
+
+        if (cname == null)
+            throw new NamingException ("Name is null");
+        if (cname.size() == 0)
+            throw new NamingException ("Name is empty");
+
+        if (cname.size() == 1)
+        {
+            //not permitted to bind if something already bound at that name
+            Binding binding = __root.getBinding (cname);
+            if (binding != null)
+                throw new NameAlreadyBoundException (cname.toString());
+
+            //make a new naming context with the root as the parent
+            Context ctx = new NamingContext (_env, cname.get(0), __root,  __root.getNameParser(""));
+            __root.addBinding (cname, ctx);
+            return ctx;
+        }
+
+        //If the name has multiple subcontexts,
+        return getContext(cname).createSubcontext(cname.getSuffix(1));
+
+    }
 
     /**
      *
@@ -929,13 +630,9 @@ public class localContextRoot implements Context
     @Override
     public NamingEnumeration list(String name) throws NamingException
     {
-        synchronized (__root)
-        {
-            return list(__root.getNameParser("").parse(getSuffix(name)));
-        }
+        return __root.list(name);
     }
 
-
     /**
      *
      *
@@ -944,67 +641,7 @@ public class localContextRoot implements Context
     @Override
     public NamingEnumeration list(Name name) throws NamingException
     {
-        synchronized (__root)
-        {
-            //return __root.list(getSuffix(name));
-
-
-            Name cname = __root.toCanonicalName(name);
-
-            if (cname == null)
-            {
-                List empty = Collections.emptyList();
-                return new NameEnumeration(empty.iterator());
-            }
-
-
-            if (cname.size() == 0)
-            {
-               return new NameEnumeration (__root.getBindings().values().iterator());
-            }
-
-
-
-            //multipart name
-            String firstComponent = cname.get(0);
-            Object ctx = null;
-
-            if (firstComponent.equals(""))
-                ctx = this;
-            else
-            {
-                Binding binding = __root.getBinding (firstComponent);
-                if (binding == null)
-                    throw new NameNotFoundException ();
-
-                ctx = binding.getObject();
-
-                if (ctx instanceof Reference)
-                {
-                    //deference the object
-                    if(__log.isDebugEnabled())__log.debug("Dereferencing Reference for "+name.get(0));
-                    try
-                    {
-                        ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), __root, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
-                }
-            }
-
-            if (!(ctx instanceof Context))
-                throw new NotContextException();
-
-            return ((Context)ctx).list (cname.getSuffix(1));
-
-        }
+        return __root.list(name);
     }
 
     /**
@@ -1015,70 +652,9 @@ public class localContextRoot implements Context
     @Override
     public NamingEnumeration listBindings(Name name) throws NamingException
     {
-        synchronized (__root)
-        {
-            //return __root.listBindings(getSuffix(name));
-
-            Name cname = __root.toCanonicalName (name);
-
-            if (cname == null)
-            {
-                List empty = Collections.emptyList();
-                return new BindingEnumeration(empty.iterator());
-            }
-
-            if (cname.size() == 0)
-            {
-               return new BindingEnumeration (__root.getBindings().values().iterator());
-            }
-
-
-
-            //multipart name
-            String firstComponent = cname.get(0);
-            Object ctx = null;
-
-            //if a name has a leading "/" it is parsed as "" so ignore it by staying
-            //at this level in the tree
-            if (firstComponent.equals(""))
-                ctx = this;
-            else
-            {
-                //it is a non-empty name component
-                Binding binding = __root.getBinding (firstComponent);
-                if (binding == null)
-                    throw new NameNotFoundException ();
-
-                ctx = binding.getObject();
-
-                if (ctx instanceof Reference)
-                {
-                    //deference the object
-                    try
-                    {
-                        ctx = NamingManager.getObjectInstance(ctx, getNameParser("").parse(firstComponent), __root, _env);
-                    }
-                    catch (NamingException e)
-                    {
-                        throw e;
-                    }
-                    catch (Exception e)
-                    {
-                        __log.warn("",e);
-                        throw new NamingException (e.getMessage());
-                    }
-                }
-            }
-
-            if (!(ctx instanceof Context))
-                throw new NotContextException();
-
-            return ((Context)ctx).listBindings (cname.getSuffix(1));
-
-        }
+        return __root.listBindings(name);
     }
 
-
     /**
      *
      *
@@ -1087,13 +663,9 @@ public class localContextRoot implements Context
     @Override
     public NamingEnumeration listBindings(String name) throws NamingException
     {
-        synchronized (__root)
-        {
-            return listBindings(__root.getNameParser("").parse(getSuffix(name)));
-        }
+        return __root.listBindings(name);
     }
 
-
     /**
      *
      *
diff --git a/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestJNDI.java b/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestJNDI.java
index f966b57f89a..c66269293fe 100644
--- a/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestJNDI.java
+++ b/jetty-jndi/src/test/java/org/eclipse/jetty/jndi/java/TestJNDI.java
@@ -18,7 +18,9 @@
 
 package org.eclipse.jetty.jndi.java;
 
+import static org.hamcrest.MatcherAssert.assertThat;
 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;
@@ -49,6 +51,7 @@ import org.eclipse.jetty.server.handler.ContextHandler;
 import org.eclipse.jetty.server.handler.HandlerList;
 import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
+import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Test;
 /**
  *
@@ -288,7 +291,7 @@ public class TestJNDI
         ClassLoader currentLoader = currentThread.getContextClassLoader();
         ClassLoader childLoader1 = new URLClassLoader(new URL[0], currentLoader);
         ClassLoader childLoader2 = new URLClassLoader(new URL[0], currentLoader);
-        
+        InitialContext initCtx = null;
         try
         {
 
@@ -325,7 +328,7 @@ public class TestJNDI
             
             //Set up the tccl before doing any jndi operations
             currentThread.setContextClassLoader(childLoader1);
-            InitialContext initCtx = new InitialContext();
+            initCtx = new InitialContext();
             
             //Test we can lookup the root java: naming tree
             Context sub0 = (Context)initCtx.lookup("java:");
@@ -439,30 +442,69 @@ public class TestJNDI
 
             //check locking the context
             Context ectx = (Context)initCtx.lookup("java:comp");
+            //make a deep structure lie ttt/ttt2 for later use
+            Context ttt = ectx.createSubcontext("ttt");
+            ttt.createSubcontext("ttt2");
+            //bind a value
             ectx.bind("crud", "xxx");
-            ectx.addToEnvironment("org.eclipse.jndi.immutable", "TRUE");
+            //lock
+            ectx.addToEnvironment("org.eclipse.jetty.jndi.lock", "TRUE");
+            //check we can't get the lock
+            assertFalse(ectx.getEnvironment().containsKey("org.eclipse.jetty.jndi.lock"));
+            //check once locked we can still do lookups
             assertEquals ("xxx", initCtx.lookup("java:comp/crud"));
+            assertNotNull(initCtx.lookup("java:comp/ttt/ttt2"));
+            
+            //test trying to bind into java:comp after lock
+            InitialContext zzz = null;
             try
             {
-                ectx.bind("crud2", "xxx2");
+                zzz = new InitialContext();
+
+                ((Context)zzz.lookup("java:comp")).bind("crud2", "xxx2");
+                fail("Should not be able to write to locked context");
+
             }
             catch (NamingException ne)
             {
-                //expected failure to modify immutable context
+                assertThat(ne.getMessage(), Matchers.containsString("immutable"));
+            }
+            finally
+            {
+                zzz.close();
             }
-            
 
-            initCtx.close();
+            //test trying to bind into a deep structure inside java:comp after lock
+            try
+            {
+                zzz = new InitialContext();
+
+                //TODO test deep locking
+                //  ((Context)zzz.lookup("java:comp/ttt/ttt2")).bind("zzz2", "zzz2");
+                // fail("Should not be able to write to locked context");
+                ((Context)zzz.lookup("java:comp")).bind("foo", "bar");
+                fail("Should not be able to write to locked context");
+            }
+            catch (NamingException ne)
+            {
+                assertThat(ne.getMessage(), Matchers.containsString("immutable"));
+            }
+            finally
+            {
+                zzz.close();
+            }
         }
         finally
         {
             //make some effort to clean up
+            initCtx.close();
             InitialContext ic = new InitialContext();
             Context java = (Context)ic.lookup("java:");
             java.destroySubcontext("zero");
             java.destroySubcontext("fee");
             currentThread.setContextClassLoader(childLoader1);
             Context comp = (Context)ic.lookup("java:comp");
+            comp.addToEnvironment("org.eclipse.jetty.jndi.unlock", "TRUE");
             comp.destroySubcontext("env");
             comp.unbind("crud");
             comp.unbind("crud2");
diff --git a/jetty-maven-plugin/README_INTEGRATION_TEST.md b/jetty-maven-plugin/README_INTEGRATION_TEST.md
index d59f4def50f..93ff4c3bd58 100644
--- a/jetty-maven-plugin/README_INTEGRATION_TEST.md
+++ b/jetty-maven-plugin/README_INTEGRATION_TEST.md
@@ -14,10 +14,10 @@ As they can be long to run, the tests do not run per default. So to run them you
 
 Running single test
 --------------------
-You can run single or set of test as well using the command line argument: ```-Dinvoker.test=jetty-run-mojo-it,jetty-run-war*-it,!jetty-run-distro*```
+You can run single or set of test as well using the command line argument: ```-Dinvoker.test=it-parent-pom,jetty-run-mojo-it,jetty-run-war*-it,!jetty-run-distro*```
 The parameter supports pattern and exclusion with !
 
-NOTE: if you use ```clean``` arg to maven, you will also need to add the test ```it-parent-pom```  first for invoker.test, eg ```-Dinvoker.test=it-parent-pom,jetty-run-mojo-it```.
+Due to [files  filtering](http://maven.apache.org/plugins/maven-invoker-plugin/examples/filtering.html), ```it-parent-pom``` must be included - otherwise tests will fail during execution. 
 
 Running Logs
 --------------------
diff --git a/jetty-maven-plugin/src/it/it-parent-pom/pom.xml b/jetty-maven-plugin/src/it/it-parent-pom/pom.xml
index 8fb2e9d199d..ed397c76f56 100644
--- a/jetty-maven-plugin/src/it/it-parent-pom/pom.xml
+++ b/jetty-maven-plugin/src/it/it-parent-pom/pom.xml
@@ -26,6 +26,11 @@
         @servlet.api.version@
         provided
       
+      
+        org.apache.commons
+        commons-lang3
+        3.8.1
+      
       
         org.jboss.weld.servlet
         weld-servlet
diff --git a/jetty-maven-plugin/src/it/javax-annotation-api/src/config/jetty.xml b/jetty-maven-plugin/src/it/javax-annotation-api/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/javax-annotation-api/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/javax-annotation-api/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-cdi-run-forked/src/main/jetty/jetty-context.xml b/jetty-maven-plugin/src/it/jetty-cdi-run-forked/src/main/jetty/jetty-context.xml
index 89c749ced9a..c7d7bf4cde7 100644
--- a/jetty-maven-plugin/src/it/jetty-cdi-run-forked/src/main/jetty/jetty-context.xml
+++ b/jetty-maven-plugin/src/it/jetty-cdi-run-forked/src/main/jetty/jetty-context.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-maven-plugin/src/it/jetty-cdi-run-forked/src/main/jetty/jetty.xml b/jetty-maven-plugin/src/it/jetty-cdi-run-forked/src/main/jetty/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/jetty-cdi-run-forked/src/main/jetty/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-cdi-run-forked/src/main/jetty/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-deploy-war-mojo-it/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-deploy-war-mojo-it/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/jetty-deploy-war-mojo-it/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-deploy-war-mojo-it/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/config/jetty.xml
index 2a17fe49cd7..dedc6a814cf 100644
--- a/jetty-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-maven-plugin-provided-module-dep/web/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-run-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty.xml b/jetty-maven-plugin/src/it/jetty-run-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty.xml
index 5062d20c4fc..08d848ec4c6 100644
--- a/jetty-maven-plugin/src/it/jetty-run-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-run-distro-mojo-it/jetty-simple-webapp/src/base/etc/test-jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-run-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-run-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/jetty-run-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-run-forked-mojo-it/jetty-simple-webapp/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpDestinationOverHTTP2.java b/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-base/src/test/java/org/eclipse/jetty/its/jetty_run_mojo_it_test/HelloTestServlet.java
similarity index 55%
rename from jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpDestinationOverHTTP2.java
rename to jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-base/src/test/java/org/eclipse/jetty/its/jetty_run_mojo_it_test/HelloTestServlet.java
index 5a030431de9..dbacf9498f3 100644
--- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpDestinationOverHTTP2.java
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-base/src/test/java/org/eclipse/jetty/its/jetty_run_mojo_it_test/HelloTestServlet.java
@@ -16,25 +16,30 @@
 //  ========================================================================
 //
 
-package org.eclipse.jetty.http2.client.http;
 
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.HttpExchange;
-import org.eclipse.jetty.client.MultiplexHttpDestination;
-import org.eclipse.jetty.client.Origin;
-import org.eclipse.jetty.client.SendFailure;
-import org.eclipse.jetty.client.api.Connection;
+package org.eclipse.jetty.its.jetty_run_mojo_it_test;
 
-public class HttpDestinationOverHTTP2 extends MultiplexHttpDestination
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ *
+ */
+@WebServlet("/testhello")
+public class HelloTestServlet
+    extends HttpServlet
 {
-    public HttpDestinationOverHTTP2(HttpClient client, Origin origin)
-    {
-        super(client, origin);
-    }
 
     @Override
-    protected SendFailure send(Connection connection, HttpExchange exchange)
+    protected void doGet( HttpServletRequest req, HttpServletResponse resp )
+        throws ServletException, IOException
     {
-        return ((HttpConnectionOverHTTP2)connection).send(exchange);
+        String who = req.getParameter( "name" );
+
+        resp.getWriter().write( "Hello from test " + (who == null ? "unknown" : who) );
     }
 }
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-webapp/pom.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-webapp/pom.xml
index 099146ad7e0..d0fb927984a 100644
--- a/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-webapp/pom.xml
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-webapp/pom.xml
@@ -23,26 +23,44 @@
       org.eclipse.jetty.its.jetty-run-mojo-it
       jetty-simple-base
     
-
+    
+      org.eclipse.jetty.its.jetty-run-mojo-it
+      jetty-simple-base
+      test
+      test-jar
+    
     
       org.slf4j
       slf4j-simple
     
-
     
       org.eclipse.jetty
       jetty-servlet
       provided
     
-
+    
+      org.eclipse.jetty
+      jetty-client
+      test
+    
+    
+      org.apache.commons
+      commons-lang3
+      test
+    
     
       org.eclipse.jetty
       jetty-maven-plugin
       tests
       test-jar
       test
+      
+        
+          *
+          *
+        
+      
     
-
     
       org.junit.jupiter
       junit-jupiter-engine
@@ -73,6 +91,7 @@
             ${jetty.port.file}
             true
             true
+            true
             ${project.groupId}:${project.artifactId}
           
           
@@ -99,6 +118,7 @@
               
               true
               ${basedir}/src/config/jetty.xml
+              true
             
           
         
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-webapp/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-webapp/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-it/jetty-simple-webapp/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-it/pom.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-it/pom.xml
index 1625960b537..453b36ef875 100644
--- a/jetty-maven-plugin/src/it/jetty-run-mojo-it/pom.xml
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-it/pom.xml
@@ -35,6 +35,13 @@
         jetty-simple-base
         ${project.version}
       
+      
+        org.eclipse.jetty.its.jetty-run-mojo-it
+        jetty-simple-base
+        ${project.version}
+        test
+        test-jar
+      
     
   
 
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-jsp/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-jsp/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/jetty-run-mojo-jsp/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-jsp/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/common/pom.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/common/pom.xml
new file mode 100644
index 00000000000..045791a4491
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/common/pom.xml
@@ -0,0 +1,12 @@
+
+
+  4.0.0
+  
+    test.jetty-run-mojo-multi-module-single-war-it
+    jetty-multi-module-project
+    1.0-SNAPSHOT
+  
+  common
+
+
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/common/src/main/java/mca/common/CommonService.java b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/common/src/main/java/mca/common/CommonService.java
new file mode 100644
index 00000000000..594d387ae36
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/common/src/main/java/mca/common/CommonService.java
@@ -0,0 +1,24 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package mca.common;
+
+public class CommonService
+{
+
+}
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/invoker.properties b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/invoker.properties
new file mode 100644
index 00000000000..fd18ebccf10
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/invoker.properties
@@ -0,0 +1 @@
+invoker.goals = test
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-api/pom.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-api/pom.xml
new file mode 100644
index 00000000000..33d0edf5d8d
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-api/pom.xml
@@ -0,0 +1,12 @@
+
+
+  4.0.0
+  
+    test.jetty-run-mojo-multi-module-single-war-it
+    module
+    1.0-SNAPSHOT
+  
+  module-api
+
+
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-api/src/main/java/mca/module/ModuleApi.java b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-api/src/main/java/mca/module/ModuleApi.java
new file mode 100644
index 00000000000..c5fc25f27d6
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-api/src/main/java/mca/module/ModuleApi.java
@@ -0,0 +1,24 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package mca.module;
+
+public interface ModuleApi
+{
+
+}
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-impl/pom.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-impl/pom.xml
new file mode 100644
index 00000000000..8ae3faa0415
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-impl/pom.xml
@@ -0,0 +1,18 @@
+
+
+  4.0.0
+  
+    test.jetty-run-mojo-multi-module-single-war-it
+    module
+    1.0-SNAPSHOT
+  
+  module-impl
+
+  
+    
+      test.jetty-run-mojo-multi-module-single-war-it
+      module-api
+    
+  
+
diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionListener.java b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-impl/src/main/java/mca/module/ModuleImpl.java
similarity index 78%
rename from jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionListener.java
rename to jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-impl/src/main/java/mca/module/ModuleImpl.java
index 98e40a2d1a4..ef59d156fcc 100644
--- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionListener.java
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/module-impl/src/main/java/mca/module/ModuleImpl.java
@@ -16,13 +16,13 @@
 //  ========================================================================
 //
 
-package org.eclipse.jetty.websocket.common;
+package mca.module;
 
-public interface SessionListener
+import mca.common.CommonService;
+
+public class ModuleImpl implements ModuleApi
 {
-    void onCreated(WebSocketSessionImpl session);
 
-    void onOpened(WebSocketSessionImpl session);
+    private static final CommonService cs = new CommonService();
 
-    void onClosed(WebSocketSessionImpl session);
 }
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/pom.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/pom.xml
new file mode 100644
index 00000000000..80ebae8d327
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/module/pom.xml
@@ -0,0 +1,25 @@
+
+
+  4.0.0
+  
+    test.jetty-run-mojo-multi-module-single-war-it
+    jetty-multi-module-project
+    1.0-SNAPSHOT
+  
+  module
+  pom
+
+  
+    module-api
+    module-impl
+  
+
+  
+    
+      test.jetty-run-mojo-multi-module-single-war-it
+      common
+    
+  
+
+
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/pom.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/pom.xml
new file mode 100644
index 00000000000..40e1ddbd775
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/pom.xml
@@ -0,0 +1,52 @@
+
+
+  4.0.0
+
+  
+    org.eclipse.jetty.its
+    it-parent-pom
+    0.0.1-SNAPSHOT
+  
+
+  test.jetty-run-mojo-multi-module-single-war-it
+  jetty-multi-module-project
+  1.0-SNAPSHOT
+  pom
+
+  Jetty :: multi-module project
+
+  
+    common
+    module
+    webapp-war
+  
+
+  
+    UTF-8
+    UTF-8
+    1.8
+    @project.version@
+  
+
+  
+    
+      
+        test.jetty-run-mojo-multi-module-single-war-it
+        common
+        ${project.version}
+      
+      
+        test.jetty-run-mojo-multi-module-single-war-it
+        module-api
+        ${project.version}
+      
+      
+        test.jetty-run-mojo-multi-module-single-war-it
+        module-impl
+        ${project.version}
+      
+    
+  
+
+
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/postbuild.groovy b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/postbuild.groovy
new file mode 100644
index 00000000000..d79f13ca50f
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/postbuild.groovy
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+File buildLog = new File( basedir, 'build.log' )
+assert buildLog.text.contains( 'Started Jetty Server' )
+
+assert buildLog.text.contains( '(1a) >> javax.servlet.ServletContextListener loaded from jar:' )
+assert buildLog.text.contains( 'local-repo/org/eclipse/jetty/toolchain/jetty-servlet-api/4.0.2/jetty-servlet-api-4.0.2.jar!/javax/servlet/ServletContextListener.class << (1b)' )
+
+assert buildLog.text.contains( '(2a) >> mca.common.CommonService loaded from file:' )
+assert buildLog.text.contains( 'common/target/classes/mca/common/CommonService.class << (2b)' )
+
+assert buildLog.text.contains( '(3a) >> mca.module.ModuleApi loaded from file:' )
+assert buildLog.text.contains( 'module/module-api/target/classes/mca/module/ModuleApi.class << (3b)' )
+
+assert buildLog.text.contains( '(4a) >> mca.module.ModuleImpl loaded from file:' )
+assert buildLog.text.contains( 'module/module-impl/target/classes/mca/module/ModuleImpl.class << (4b)' )
+
+assert buildLog.text.contains( '(5a) >> mca.webapp.WebAppServletListener loaded from file:' )
+assert buildLog.text.contains( 'webapp-war/target/classes/mca/webapp/WebAppServletListener.class << (5b)' )
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/pom.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/pom.xml
new file mode 100644
index 00000000000..b1ad640a9e8
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/pom.xml
@@ -0,0 +1,56 @@
+
+
+  4.0.0
+  
+    test.jetty-run-mojo-multi-module-single-war-it
+    jetty-multi-module-project
+    1.0-SNAPSHOT
+  
+  webapp-war
+  war
+
+  
+    
+      org.eclipse.jetty
+      jetty-servlet
+    
+    
+      test.jetty-run-mojo-multi-module-single-war-it
+      module-impl
+    
+  
+
+  
+    ${project.build.directory}/jetty-run-mojo.txt
+  
+
+  
+    
+      
+        org.eclipse.jetty
+        jetty-maven-plugin
+        
+          
+            start-jetty
+            test-compile
+            
+              start
+            
+            
+              
+                
+                  jetty.port.file
+                  ${jetty.port.file}
+                
+              
+              true
+              ${basedir}/src/config/jetty.xml
+            
+          
+        
+      
+    
+  
+
+
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/config/jetty.xml
new file mode 100644
index 00000000000..5389324070a
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/config/jetty.xml
@@ -0,0 +1,40 @@
+
+
+
+
+  
+    https
+    
+    32768
+    8192
+    8192
+    4096
+  
+
+  
+    
+      
+        
+        
+          
+            
+              
+                
+              
+            
+          
+        
+        
+          
+            
+              
+            
+          
+        
+        
+        0
+        30000
+      
+    
+  
+
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/main/java/mca/webapp/WebAppServletListener.java b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/main/java/mca/webapp/WebAppServletListener.java
new file mode 100644
index 00000000000..af8b389bd18
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/main/java/mca/webapp/WebAppServletListener.java
@@ -0,0 +1,56 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package mca.webapp;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+import java.net.URL;
+
+import static java.lang.String.format;
+
+public class WebAppServletListener implements ServletContextListener
+{
+
+    @Override
+    public void contextInitialized(ServletContextEvent servletContextEvent)
+    {
+        print("1", "javax.servlet.ServletContextListener");
+        print("2", "mca.common.CommonService");
+        print("3", "mca.module.ModuleApi");
+        print("4", "mca.module.ModuleImpl");
+        print("5", "mca.webapp.WebAppServletListener");
+    }
+
+    @Override
+    public void contextDestroyed(ServletContextEvent servletContextEvent)
+    {
+
+    }
+
+    private void print(String counter, String className)
+    {
+        String res = className.replaceAll("\\.", "/") + ".class";
+        URL url = Thread.currentThread().getContextClassLoader().getResource(res);
+        System.out.println(
+                format("(%sa) >> %s loaded from %s << (%sb)",
+                        counter, className, url, counter)
+        );
+    }
+}
diff --git a/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/main/webapp/WEB-INF/web.xml b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..5d48dd82060
--- /dev/null
+++ b/jetty-maven-plugin/src/it/jetty-run-mojo-multi-module-single-war-it/webapp-war/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+    
+        mca.webapp.WebAppServletListener
+    
+
diff --git a/jetty-maven-plugin/src/it/jetty-run-war-exploded-mojo-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-run-war-exploded-mojo-it/jetty-simple-webapp/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/jetty-run-war-exploded-mojo-it/jetty-simple-webapp/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-run-war-exploded-mojo-it/jetty-simple-webapp/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-run-war-mojo-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-run-war-mojo-it/jetty-simple-webapp/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/jetty-run-war-mojo-it/jetty-simple-webapp/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-run-war-mojo-it/jetty-simple-webapp/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty.xml b/jetty-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/jetty-start-mojo-it/jetty-simple-webapp/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/it/run-mojo-gwt-it/beer-server/src/config/jetty.xml b/jetty-maven-plugin/src/it/run-mojo-gwt-it/beer-server/src/config/jetty.xml
index 4fb92bbea50..5389324070a 100644
--- a/jetty-maven-plugin/src/it/run-mojo-gwt-it/beer-server/src/config/jetty.xml
+++ b/jetty-maven-plugin/src/it/run-mojo-gwt-it/beer-server/src/config/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
   
diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java
index d33dbc10180..661b9523326 100644
--- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java
+++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java
@@ -51,6 +51,7 @@ import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.server.handler.HandlerCollection;
 import org.eclipse.jetty.util.PathWatcher;
 import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.resource.PathResource;
 import org.eclipse.jetty.util.resource.Resource;
 import org.eclipse.jetty.xml.XmlConfiguration;
 
@@ -556,7 +557,7 @@ public abstract class AbstractJettyMojo extends AbstractMojo
                 contextXml = path.toFile().getAbsolutePath();
             }
     
-            XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(path.toFile()));
+            XmlConfiguration xmlConfiguration = new XmlConfiguration(new PathResource(path));
             getLog().info("Applying context xml file "+contextXml);
             xmlConfiguration.configure(webApp);   
         }
diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java
index dfd923201a1..56b50cf11cf 100644
--- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java
+++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java
@@ -21,13 +21,15 @@ package org.eclipse.jetty.maven.plugin;
 import java.io.File;
 import java.io.IOException;
 import java.net.URL;
-import java.nio.file.Paths;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.apache.maven.artifact.Artifact;
 import org.apache.maven.plugin.MojoExecutionException;
@@ -38,7 +40,7 @@ import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.plugins.annotations.ResolutionScope;
 import org.apache.maven.project.MavenProject;
-import org.codehaus.plexus.util.StringUtils;
+import org.eclipse.jetty.maven.plugin.utils.MavenProjectHelper;
 import org.eclipse.jetty.util.PathWatcher;
 import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
 import org.eclipse.jetty.util.resource.Resource;
@@ -284,8 +286,15 @@ public class JettyRunMojo extends AbstractJettyMojo
        if (useTestScope && (testClassesDirectory != null))
            webApp.setTestClasses (testClassesDirectory);
 
-       webApp.setWebInfLib(getDependencyFiles());
-
+        MavenProjectHelper mavenProjectHelper = new MavenProjectHelper(project);
+        List webInfLibs = getWebInfLibArtifacts(project).stream()
+                .map(a -> {
+                    Path p = mavenProjectHelper.getArtifactPath(a);
+                    getLog().debug("Artifact " + a.getId() + " loaded from " + p + " added to WEB-INF/lib");
+                    return p.toFile();
+                }).collect(Collectors.toList());
+        getLog().debug("WEB-INF/lib initialized (at root)");
+        webApp.setWebInfLib(webInfLibs);
 
        //if we have not already set web.xml location, need to set one up
        if (webApp.getDescriptor() == null)
@@ -518,64 +527,41 @@ public class JettyRunMojo extends AbstractJettyMojo
         startScanner();
         getLog().info("Restart completed at "+new Date().toString());
     }
-    
-    
-    
-    
-    /**
-     * @return
-     */
-    private List getDependencyFiles()
+
+    private Collection getWebInfLibArtifacts(Set artifacts)
     {
-        List dependencyFiles = new ArrayList<>();
-        for ( Artifact artifact : projectArtifacts)
-        {
-            // Include runtime and compile time libraries, and possibly test libs too
-            if(artifact.getType().equals("war"))
-                continue;
-
-            if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
-                continue; //never add dependencies of scope=provided to the webapp's classpath (see also  param)
-
-            if (Artifact.SCOPE_TEST.equals(artifact.getScope()) && !useTestScope)
-                continue; //only add dependencies of scope=test if explicitly required
-
-            MavenProject mavenProject = getProjectReference( artifact, project );
-            if (mavenProject != null)
-            {
-                File projectPath = Paths.get(mavenProject.getBuild().getOutputDirectory()).toFile();
-                getLog().debug( "Adding project directory " + projectPath.toString() );
-                dependencyFiles.add( projectPath );
-                continue;
-            }
-
-            dependencyFiles.add(artifact.getFile());
-            getLog().debug( "Adding artifact " + artifact.getFile().getName() + " with scope "+artifact.getScope()+" for WEB-INF/lib " );   
-        }
-              
-        return dependencyFiles; 
+        return artifacts.stream()
+                .filter(this::canPutArtifactInWebInfLib)
+                .collect(Collectors.toList());
     }
 
-    protected MavenProject getProjectReference(Artifact artifact, MavenProject project )
+    private Collection getWebInfLibArtifacts(MavenProject mavenProject)
     {
-        if ( project.getProjectReferences() == null || project.getProjectReferences().isEmpty() )
+        String type = mavenProject.getArtifact().getType();
+        if (!"war".equalsIgnoreCase(type) && !"zip".equalsIgnoreCase(type))
         {
-            return null;
+            return Collections.emptyList();
         }
-        Collection mavenProjects = project.getProjectReferences().values();
-        for ( MavenProject mavenProject : mavenProjects )
-        {
-            if ( StringUtils.equals( mavenProject.getId(), artifact.getId() ) )
-            {
-                return mavenProject;
-            }
-        }
-        return null;
+        return getWebInfLibArtifacts(mavenProject.getArtifacts());
     }
 
+    private boolean canPutArtifactInWebInfLib(Artifact artifact)
+    {
+        if ("war".equalsIgnoreCase(artifact.getType()))
+        {
+            return false;
+        }
+        if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
+        {
+            return false;
+        }
+        if (Artifact.SCOPE_TEST.equals(artifact.getScope()) && !useTestScope)
+        {
+            return false;
+        }
+        return true;
+    }
 
-
-    
     private List getOverlays()
     throws Exception
     {
diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java
index a66ed47e152..a6156dd7b98 100644
--- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java
+++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java
@@ -43,6 +43,7 @@ import org.eclipse.jetty.util.StringUtil;
 import org.eclipse.jetty.util.URIUtil;
 import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.PathResource;
 import org.eclipse.jetty.util.resource.Resource;
 import org.eclipse.jetty.util.resource.ResourceCollection;
 import org.eclipse.jetty.webapp.Configuration;
@@ -398,7 +399,7 @@ public class JettyWebAppContext extends WebAppContext
             for (Configuration c : getWebAppConfigurations())
             {
                 if (c instanceof EnvConfiguration && getJettyEnvXml() != null)
-                    ((EnvConfiguration) c).setJettyEnvXml(Resource.toURL(new File(getJettyEnvXml())));
+                    ((EnvConfiguration) c).setJettyEnvResource(new PathResource(new File(getJettyEnvXml())));
             }
         }
         catch (Exception e)
diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ServerSupport.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ServerSupport.java
index 56da7940eb7..338427e3e90 100644
--- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ServerSupport.java
+++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ServerSupport.java
@@ -33,8 +33,7 @@ import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.server.handler.DefaultHandler;
 import org.eclipse.jetty.server.handler.HandlerCollection;
-import org.eclipse.jetty.util.resource.Resource;
-import org.eclipse.jetty.webapp.Configuration;
+import org.eclipse.jetty.util.resource.PathResource;
 import org.eclipse.jetty.webapp.Configurations;
 import org.eclipse.jetty.webapp.WebAppContext;
 import org.eclipse.jetty.xml.XmlConfiguration;
@@ -194,7 +193,7 @@ public class ServerSupport
                 PluginLog.getLog().info( "Configuring Jetty from xml configuration file = " + xmlFile.getCanonicalPath() );   
 
 
-            XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(xmlFile));
+            XmlConfiguration xmlConfiguration = new XmlConfiguration(new PathResource(xmlFile));
             
             //add in any properties
             if (properties != null)
diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/WebAppPropertyConverter.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/WebAppPropertyConverter.java
index c8a4f71b7d1..c929fa59677 100644
--- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/WebAppPropertyConverter.java
+++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/WebAppPropertyConverter.java
@@ -19,13 +19,9 @@
 
 package org.eclipse.jetty.maven.plugin;
 
-import java.io.BufferedOutputStream;
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
@@ -253,7 +249,7 @@ public class WebAppPropertyConverter
         str = props.getProperty("context.xml");
         if (!StringUtil.isBlank(str))
         {
-            XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.newResource(str).getURI().toURL());
+            XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.newResource(str));
             xmlConfiguration.getIdMap().put("Server",server);
             //add in any properties
             if (jettyProperties != null)
diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/utils/MavenProjectHelper.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/utils/MavenProjectHelper.java
new file mode 100644
index 00000000000..cdae1dc7a01
--- /dev/null
+++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/utils/MavenProjectHelper.java
@@ -0,0 +1,96 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package org.eclipse.jetty.maven.plugin.utils;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.project.MavenProject;
+
+public class MavenProjectHelper
+{
+
+    private final Map artifactToLocalProjectMap;
+
+    public MavenProjectHelper(MavenProject project)
+    {
+        Set mavenProjects = resolveProjectDependencies(project, new HashSet<>());
+        artifactToLocalProjectMap = mavenProjects.stream()
+                .collect(Collectors.toMap(MavenProject::getId, Function.identity()));
+        artifactToLocalProjectMap.put(project.getArtifact().getId(), project);
+    }
+
+    /**
+     * Gets maven project if referenced in reactor
+     * @param artifact - maven artifact
+     * @return {@link MavenProject} if artifact is referenced in reactor, otherwise null
+     */
+    public MavenProject getMavenProject(Artifact artifact)
+    {
+        return artifactToLocalProjectMap.get(artifact.getId());
+    }
+
+    /**
+     * Gets path to artifact.
+     * If artifact is referenced in reactor, returns path to ${project.build.outputDirectory}.
+     * Otherwise, returns path to location in local m2 repo.
+     *
+     * Cannot return null - maven will complain about unsatisfied dependency during project built.
+     *
+     * @param artifact maven artifact
+     * @return path to artifact
+     */
+    public Path getArtifactPath(Artifact artifact)
+    {
+        Path path = artifact.getFile().toPath();
+        MavenProject mavenProject = getMavenProject(artifact);
+        if (mavenProject != null)
+        {
+            if ( "test-jar".equals( artifact.getType() )) {
+                path = Paths.get(mavenProject.getBuild().getTestOutputDirectory());
+            } else {
+                path = Paths.get(mavenProject.getBuild().getOutputDirectory());
+            }
+        }
+        return path;
+    }
+
+    private static Set resolveProjectDependencies(MavenProject project, Set visitedProjects)
+    {
+        if (visitedProjects.contains(project))
+        {
+            return Collections.emptySet();
+        }
+        visitedProjects.add(project);
+        Set availableProjects = new HashSet<>(project.getProjectReferences().values());
+        for (MavenProject ref : project.getProjectReferences().values())
+        {
+            availableProjects.addAll(resolveProjectDependencies(ref, visitedProjects));
+        }
+        return availableProjects;
+    }
+
+}
diff --git a/jetty-maven-plugin/src/main/resources/jetty-maven.xml b/jetty-maven-plugin/src/main/resources/jetty-maven.xml
index 5c107d8dd59..8ee79fbec55 100644
--- a/jetty-maven-plugin/src/main/resources/jetty-maven.xml
+++ b/jetty-maven-plugin/src/main/resources/jetty-maven.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
    
diff --git a/jetty-maven-plugin/src/main/resources/maven.xml b/jetty-maven-plugin/src/main/resources/maven.xml
index 27029beb34a..b477d77edee 100644
--- a/jetty-maven-plugin/src/main/resources/maven.xml
+++ b/jetty-maven-plugin/src/main/resources/maven.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-maven-plugin/src/test/java/org/eclipse/jetty/maven/plugin/it/TestGetContent.java b/jetty-maven-plugin/src/test/java/org/eclipse/jetty/maven/plugin/it/TestGetContent.java
index 37b2bb1b643..6fffe96d0cc 100644
--- a/jetty-maven-plugin/src/test/java/org/eclipse/jetty/maven/plugin/it/TestGetContent.java
+++ b/jetty-maven-plugin/src/test/java/org/eclipse/jetty/maven/plugin/it/TestGetContent.java
@@ -77,6 +77,14 @@ public class TestGetContent
                     + ", response not contentCheck: " + contentCheck + ", response:" + response);
                 System.out.println( "contentCheck" );
             }
+            if (Boolean.getBoolean( "helloTestServlet" ))
+            {
+                String response = httpClient.GET( "http://localhost:" + port + "/testhello?name=beer" ).getContentAsString();
+                assertEquals( "Hello from test beer", response.trim(), "it test " + System.getProperty( "maven.it.name" ) );
+                response = httpClient.GET( "http://localhost:" + port + "/testhello?name=foo" ).getContentAsString();
+                assertEquals( "Hello from test foo", response.trim(), "it test " + System.getProperty( "maven.it.name" )  );
+                System.out.println( "helloServlet" );
+            }
         }
         finally
         {
diff --git a/jetty-memcached/jetty-memcached-sessions/pom.xml b/jetty-memcached/jetty-memcached-sessions/pom.xml
index 1635ddf8177..1b889901d19 100644
--- a/jetty-memcached/jetty-memcached-sessions/pom.xml
+++ b/jetty-memcached/jetty-memcached-sessions/pom.xml
@@ -24,7 +24,7 @@
     
       org.slf4j
       slf4j-simple
-      1.7.9
+      ${slf4j.version}
       test
     
     
diff --git a/jetty-memcached/jetty-memcached-sessions/src/main/config/etc/sessions/session-data-cache/xmemcached.xml b/jetty-memcached/jetty-memcached-sessions/src/main/config/etc/sessions/session-data-cache/xmemcached.xml
index 60448c0ac06..202259eced7 100644
--- a/jetty-memcached/jetty-memcached-sessions/src/main/config/etc/sessions/session-data-cache/xmemcached.xml
+++ b/jetty-memcached/jetty-memcached-sessions/src/main/config/etc/sessions/session-data-cache/xmemcached.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
     
diff --git a/jetty-nosql/src/main/config/etc/sessions/mongo/session-store-by-address.xml b/jetty-nosql/src/main/config/etc/sessions/mongo/session-store-by-address.xml
index 06c1aa6e261..281297548e8 100644
--- a/jetty-nosql/src/main/config/etc/sessions/mongo/session-store-by-address.xml
+++ b/jetty-nosql/src/main/config/etc/sessions/mongo/session-store-by-address.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-nosql/src/main/config/etc/sessions/mongo/session-store-by-uri.xml b/jetty-nosql/src/main/config/etc/sessions/mongo/session-store-by-uri.xml
index e20eb0284e1..eaa6030064a 100644
--- a/jetty-nosql/src/main/config/etc/sessions/mongo/session-store-by-uri.xml
+++ b/jetty-nosql/src/main/config/etc/sessions/mongo/session-store-by-uri.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-deployer.xml b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-deployer.xml
index 566ed9c686e..ba8c4da7feb 100644
--- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-deployer.xml
+++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-deployer.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml
index 319ae6cbef7..c5925ef6b77 100644
--- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml
+++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml
index 95355f229d0..2a73c7c88cd 100644
--- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml
+++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/AbstractContextProvider.java b/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/AbstractContextProvider.java
index 4505341c78b..896f6e85edf 100644
--- a/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/AbstractContextProvider.java
+++ b/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/AbstractContextProvider.java
@@ -158,7 +158,7 @@ public abstract class AbstractContextProvider extends AbstractLifeCycle implemen
                     {
                         Thread.currentThread().setContextClassLoader(classLoader);
                         
-                        XmlConfiguration xmlConfiguration = new XmlConfiguration(res.getInputStream());
+                        XmlConfiguration xmlConfiguration = new XmlConfiguration(res);
                         HashMap properties = new HashMap();
                         //put the server instance in
                         properties.put("Server", getServerInstanceWrapper().getServer());
diff --git a/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/AbstractWebAppProvider.java b/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/AbstractWebAppProvider.java
index 8ec84811fc2..fe54fd50f51 100644
--- a/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/AbstractWebAppProvider.java
+++ b/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/AbstractWebAppProvider.java
@@ -414,7 +414,7 @@ public abstract class AbstractWebAppProvider extends AbstractLifeCycle implement
                 // Apply it just as the standard jetty ContextProvider would do
                 LOG.info("Applying " + contextXmlUri + " to " + _webApp);
 
-                XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXmlUri);
+                XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.newResource(contextXmlUri));
                 WebAppClassLoader.runWithServerClassAccess(()->
                 {
                     HashMap properties = new HashMap<>();
diff --git a/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/serverfactory/ServerInstanceWrapper.java b/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/serverfactory/ServerInstanceWrapper.java
index 7b6d31983fb..1862a8f51a4 100644
--- a/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/serverfactory/ServerInstanceWrapper.java
+++ b/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/serverfactory/ServerInstanceWrapper.java
@@ -18,7 +18,6 @@
 
 package org.eclipse.jetty.osgi.boot.internal.serverfactory;
 
-import java.io.InputStream;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -54,6 +53,7 @@ import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.util.StringUtil;
 import org.eclipse.jetty.util.log.Log;
 import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
 import org.eclipse.jetty.xml.XmlConfiguration;
 
 /**
@@ -142,10 +142,10 @@ public class ServerInstanceWrapper
 
         for (URL jettyConfiguration : jettyConfigurations)
         {
-            try(InputStream in = jettyConfiguration.openStream())
+            try
             {
                 // Execute a Jetty configuration file
-                XmlConfiguration config = new XmlConfiguration(in);
+                XmlConfiguration config = new XmlConfiguration(Resource.newResource(jettyConfiguration));
 
                 config.getIdMap().putAll(id_map);
                 config.getProperties().putAll(properties);
diff --git a/jetty-osgi/jetty-osgi-httpservice/contexts/httpservice.xml b/jetty-osgi/jetty-osgi-httpservice/contexts/httpservice.xml
index 98c00314b90..7b57f96be2c 100644
--- a/jetty-osgi/jetty-osgi-httpservice/contexts/httpservice.xml
+++ b/jetty-osgi/jetty-osgi-httpservice/contexts/httpservice.xml
@@ -1,5 +1,5 @@
 
-
+
 
   
   
diff --git a/jetty-osgi/test-jetty-osgi-context/src/main/context/acme.xml b/jetty-osgi/test-jetty-osgi-context/src/main/context/acme.xml
index f6c5cf581a6..f2052300efb 100644
--- a/jetty-osgi/test-jetty-osgi-context/src/main/context/acme.xml
+++ b/jetty-osgi/test-jetty-osgi-context/src/main/context/acme.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/pom.xml b/jetty-osgi/test-jetty-osgi/pom.xml
index bb3f3b65328..684aafbf77a 100644
--- a/jetty-osgi/test-jetty-osgi/pom.xml
+++ b/jetty-osgi/test-jetty-osgi/pom.xml
@@ -315,8 +315,8 @@
       runtime
     
     
-      javax.websocket
-      javax.websocket-api
+      org.eclipse.jetty.toolchain
+      jetty-javax-websocket-api
       runtime
     
     
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-alpn.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-alpn.xml
index 04ffe1f026c..dd80dc34afc 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-alpn.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-alpn.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-deployer.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-deployer.xml
index 19b3a5db090..f56749297dd 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-deployer.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-deployer.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml
index 6a7fa8fd2fb..b936e8512e2 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml
index dc91ba73205..db576538760 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml
index 3c06b9a313e..7df42848e06 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-bundle.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-bundle.xml
index 1c098b37dba..fea514bc939 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-bundle.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-bundle.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml
index 13ce3f3df5b..afe5042fd0d 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml
index b967a95795b..9f0b3419964 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml
index ecab2fe4198..5ec8a408a72 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml
index e2ca8266243..120d5758e7f 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml
index 3d1b92e054b..8e8ac30ada3 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml
index 3d1b92e054b..1b82909ad8f 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
@@ -21,6 +21,13 @@
       
     
     true
+    
+      
+        
+          TLSv1.3
+        
+      
+    
   
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml
index 9efc415a698..2ef8b99804c 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-ssl.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-ssl.xml
index 117e8626733..caaf36efee1 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-ssl.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-ssl.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
@@ -29,7 +29,7 @@
   
   
   
-  
+  
     
     /
     
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-testrealm.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-testrealm.xml
index a9d9987cb15..77afc2e8a5d 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-testrealm.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-testrealm.xml
@@ -1,5 +1,5 @@
 
-
+
 
     
     
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml
index aa2ab155744..0417227df6b 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml
index fe2095f273b..129b6736824 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml
+++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml
@@ -1,5 +1,5 @@
 
-
+
 
 
 
diff --git a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootContextAsService.java b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootContextAsService.java
index ccb01acf9b4..7533105461d 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootContextAsService.java
+++ b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootContextAsService.java
@@ -19,6 +19,7 @@
 package org.eclipse.jetty.osgi.test;
 
 import java.util.ArrayList;
+
 import javax.inject.Inject;
 
 import org.eclipse.jetty.client.HttpClient;
@@ -43,18 +44,16 @@ import static org.ops4j.pax.exam.CoreOptions.systemProperty;
 
 /**
  * TestJettyOSGiBootContextAsService
- * 
+ *
  * Tests deployment of a ContextHandler as an osgi Service.
- * 
+ *
  * Tests the ServiceContextProvider.
- * 
  */
 @RunWith(PaxExam.class)
 public class TestJettyOSGiBootContextAsService
 {
     private static final String LOG_LEVEL = "WARN";
 
-
     @Inject
     BundleContext bundleContext = null;
 
@@ -66,27 +65,24 @@ public class TestJettyOSGiBootContextAsService
         options.addAll(TestOSGiUtil.configureJettyHomeAndPort(false, "jetty-http-boot-context-as-service.xml"));
         options.add(CoreOptions.bootDelegationPackages("org.xml.sax", "org.xml.*", "org.w3c.*", "javax.xml.*"));
         options.addAll(TestOSGiUtil.coreJettyDependencies());
+        options.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-alpn-java-client").versionAsInProject().start());
+        options.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-alpn-client").versionAsInProject().start());
 
-        // a bundle that registers a webapp as a service for the jetty osgi core
-        // to pick up and deploy
+        // a bundle that registers a webapp as a service for the jetty osgi core to pick up and deploy
         options.add(mavenBundle().groupId("org.eclipse.jetty.osgi").artifactId("test-jetty-osgi-context").versionAsInProject().start());
         options.add(systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value(LOG_LEVEL));
         options.add(systemProperty("org.eclipse.jetty.LEVEL").value(LOG_LEVEL));
-        options.add( systemProperty( "org.ops4j.pax.url.mvn.localRepository" ).value( System.getProperty( "mavenRepoPath" ) ) );
+        options.add(systemProperty("org.ops4j.pax.url.mvn.localRepository").value(System.getProperty("mavenRepoPath")));
 
-        return options.toArray(new Option[options.size()]);
+        return options.toArray(new Option[0]);
     }
 
-
-
-    /**
-     */
     @Test
     public void testContextHandlerAsOSGiService() throws Exception
     {
         if (Boolean.getBoolean(TestOSGiUtil.BUNDLE_DEBUG))
             TestOSGiUtil.assertAllBundlesActiveOrResolved(bundleContext);
-        
+
         // now test the context
         HttpClient client = new HttpClient();
         try
@@ -94,12 +90,12 @@ public class TestJettyOSGiBootContextAsService
             client.start();
             String tmp = System.getProperty("boot.context.service.port");
             assertNotNull(tmp);
-            int port = Integer.parseInt(tmp);
+            int port = Integer.valueOf(tmp);
             ContentResponse response = client.GET("http://127.0.0.1:" + port + "/acme/index.html");
             assertEquals(HttpStatus.OK_200, response.getStatus());
 
             String content = new String(response.getContent());
-            assertTrue(content.indexOf("

Test OSGi Context

") != -1); + assertTrue(content.contains("

Test OSGi Context

")); } finally { @@ -109,7 +105,7 @@ public class TestJettyOSGiBootContextAsService ServiceReference[] refs = bundleContext.getServiceReferences(ContextHandler.class.getName(), null); assertNotNull(refs); assertEquals(1, refs.length); - ContextHandler ch = (ContextHandler) bundleContext.getService(refs[0]); + ContextHandler ch = (ContextHandler)bundleContext.getService(refs[0]); assertEquals("/acme", ch.getContextPath()); // Stop the bundle with the ContextHandler in it and check the jetty @@ -118,7 +114,7 @@ public class TestJettyOSGiBootContextAsService // than checking stderr output Bundle testWebBundle = TestOSGiUtil.getBundle(bundleContext, "org.eclipse.jetty.osgi.testcontext"); assertNotNull("Could not find the org.eclipse.jetty.test-jetty-osgi-context.jar bundle", testWebBundle); - assertTrue("The bundle org.eclipse.jetty.testcontext is not correctly resolved", testWebBundle.getState() == Bundle.ACTIVE); + assertEquals("The bundle org.eclipse.jetty.testcontext is not correctly resolved", Bundle.ACTIVE, testWebBundle.getState()); testWebBundle.stop(); } } diff --git a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootHTTP2Conscrypt.java b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootHTTP2Conscrypt.java index 85e26c5976f..9099331f971 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootHTTP2Conscrypt.java +++ b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootHTTP2Conscrypt.java @@ -24,13 +24,14 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; - import javax.inject.Inject; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.JavaVersion; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.Test; @@ -66,12 +67,12 @@ public class TestJettyOSGiBootHTTP2Conscrypt { ArrayList
diff --git a/jetty-rewrite/src/main/config/etc/rewrite-compactpath.xml b/jetty-rewrite/src/main/config/etc/rewrite-compactpath.xml index 7cd53e64fc2..999ed552bd0 100644 --- a/jetty-rewrite/src/main/config/etc/rewrite-compactpath.xml +++ b/jetty-rewrite/src/main/config/etc/rewrite-compactpath.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-rewrite/src/main/config/modules/rewrite.mod b/jetty-rewrite/src/main/config/modules/rewrite.mod index 60c229dfae9..939acebdd12 100644 --- a/jetty-rewrite/src/main/config/modules/rewrite.mod +++ b/jetty-rewrite/src/main/config/modules/rewrite.mod @@ -13,8 +13,12 @@ server [lib] lib/jetty-rewrite-${jetty.version}.jar +[files] +basehome:modules/rewrite/rewrite-rules.xml|etc/rewrite-rules.xml + [xml] etc/jetty-rewrite.xml +etc/rewrite-rules.xml [ini-template] ## Whether to rewrite the request URI diff --git a/jetty-rewrite/src/main/config/modules/rewrite/rewrite-rules.xml b/jetty-rewrite/src/main/config/modules/rewrite/rewrite-rules.xml new file mode 100644 index 00000000000..62446eb9040 --- /dev/null +++ b/jetty-rewrite/src/main/config/modules/rewrite/rewrite-rules.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectRegexRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectRegexRule.java index ef5cc56d04f..3eb9da7ec95 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectRegexRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectRegexRule.java @@ -31,9 +31,9 @@ import org.eclipse.jetty.util.annotation.Name; *

* The replacement string may use $n" to replace the nth capture group. *

- * All redirects are part of the 3xx Redirection status code set. + * All redirects are part of the {@code 3xx Redirection} status code set. *

- * Defaults to 302 Found + * Defaults to {@code 302 Found} */ public class RedirectRegexRule extends RegexRule { @@ -42,9 +42,9 @@ public class RedirectRegexRule extends RegexRule public RedirectRegexRule() { - this(null,null); + this(null, null); } - + public RedirectRegexRule(@Name("regex") String regex, @Name("location") String location) { super(regex); @@ -53,59 +53,54 @@ public class RedirectRegexRule extends RegexRule setLocation(location); } + /** + * Sets the redirect location. + * + * @param location the URI to redirect to + */ public void setLocation(String location) { _location = location; } - + /** * Sets the redirect status code. - * + * * @param statusCode the 3xx redirect status code */ public void setStatusCode(int statusCode) { - if ((300 <= statusCode) || (statusCode >= 399)) - { + if (statusCode >= 300 && statusCode <= 399) _statusCode = statusCode; - } else - { throw new IllegalArgumentException("Invalid redirect status code " + statusCode + " (must be a value between 300 and 399)"); - } } - + @Override - protected String apply(String target, HttpServletRequest request, HttpServletResponse response, Matcher matcher) - throws IOException + protected String apply(String target, HttpServletRequest request, HttpServletResponse response, Matcher matcher) throws IOException { - target=_location; - for (int g=1;g<=matcher.groupCount();g++) + target = _location; + for (int g = 1; g <= matcher.groupCount(); g++) { String group = matcher.group(g); - target=target.replaceAll("\\$"+g,group); + target = target.replaceAll("\\$" + g, group); } - + target = response.encodeRedirectURL(target); - response.setHeader("Location",RedirectUtil.toRedirectURL(request,target)); + response.setHeader("Location", RedirectUtil.toRedirectURL(request, target)); response.setStatus(_statusCode); response.getOutputStream().flush(); // no output / content response.getOutputStream().close(); return target; } - + /** * Returns the redirect status code and replacement. */ @Override public String toString() { - StringBuilder str = new StringBuilder(); - str.append(super.toString()); - str.append('[').append(_statusCode); - str.append('>').append(_location); - str.append(']'); - return str.toString(); + return String.format("%s[%d>%s]", super.toString(), _statusCode, _location); } } diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewritePatternRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewritePatternRuleTest.java index d22971569cb..a714da758e2 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewritePatternRuleTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewritePatternRuleTest.java @@ -18,14 +18,14 @@ package org.eclipse.jetty.rewrite.handler; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.IOException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + public class RewritePatternRuleTest extends AbstractRuleTestCase { // TODO: Parameterize diff --git a/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml b/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml index a1a54898bd0..90dc198b4f3 100644 --- a/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml +++ b/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java b/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java index 76ad8c155b2..cb28acd362d 100644 --- a/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java +++ b/jetty-runner/src/main/java/org/eclipse/jetty/runner/Runner.java @@ -327,7 +327,7 @@ public class Runner for (String cfg : _configFiles) { try (Resource resource = Resource.newResource(cfg)) { - XmlConfiguration xmlConfiguration = new XmlConfiguration(resource.getURI()); + XmlConfiguration xmlConfiguration = new XmlConfiguration(resource); xmlConfiguration.configure(_server); } } @@ -432,7 +432,7 @@ public class Runner if (!ctx.isDirectory() && ctx.toString().toLowerCase(Locale.ENGLISH).endsWith(".xml")) { // It is a context config file - XmlConfiguration xmlConfiguration = new XmlConfiguration(ctx.getURI()); + XmlConfiguration xmlConfiguration = new XmlConfiguration(ctx); xmlConfiguration.getIdMap().put("Server", _server); ContextHandler handler = (ContextHandler) xmlConfiguration.configure(); if (contextPathSet) diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java index 5ccf42ea9e9..ae62d0e989e 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractLoginService.java @@ -27,6 +27,7 @@ import javax.servlet.ServletRequest; import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Credential; @@ -34,7 +35,7 @@ import org.eclipse.jetty.util.security.Credential; /** * AbstractLoginService */ -public abstract class AbstractLoginService extends AbstractLifeCycle implements LoginService +public abstract class AbstractLoginService extends ContainerLifeCycle implements LoginService { private static final Logger LOG = Log.getLogger(AbstractLoginService.class); @@ -116,6 +117,11 @@ public abstract class AbstractLoginService extends AbstractLifeCycle implements /* ------------------------------------------------------------ */ protected abstract UserPrincipal loadUserInfo (String username); + protected AbstractLoginService() + { + addBean(_identityService); + } + /* ------------------------------------------------------------ */ /** * @see org.eclipse.jetty.security.LoginService#getName() @@ -135,6 +141,7 @@ public abstract class AbstractLoginService extends AbstractLifeCycle implements { if (isRunning()) throw new IllegalStateException("Running"); + updateBean(_identityService, identityService); _identityService = identityService; } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java index 5ca2bc7ce02..6a197dae7b8 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java @@ -21,6 +21,11 @@ package org.eclipse.jetty.security; import java.io.Serializable; import java.util.Set; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.security.authentication.LoginAuthenticator; +import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication.User; import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.server.UserIdentity.Scope; @@ -95,4 +100,23 @@ public abstract class AbstractUserAuthentication implements User, Serializable return false; } + + @Override + public Authentication logout (ServletRequest request) + { + SecurityHandler security=SecurityHandler.getCurrentSecurityHandler(); + if (security!=null) + { + security.logout(this); + Authenticator authenticator = security.getAuthenticator(); + if (authenticator instanceof LoginAuthenticator) + { + ((LoginAuthenticator)authenticator).logout(request); + return new LoggedOutAuthentication((LoginAuthenticator)authenticator); + } + } + + return Authentication.UNAUTHENTICATED; + } + } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java index 42229058576..56b026e019e 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java @@ -80,7 +80,7 @@ public interface Authenticator * @param mandatory True if authentication is mandatory. * @return An Authentication. If Authentication is successful, this will be a {@link org.eclipse.jetty.server.Authentication.User}. If a response has * been sent by the Authenticator (which can be done for both successful and unsuccessful authentications), then the result will - * implement {@link org.eclipse.jetty.server.Authentication.ResponseSent}. If Authentication is not manditory, then a + * implement {@link org.eclipse.jetty.server.Authentication.ResponseSent}. If Authentication is not mandatory, then a * {@link org.eclipse.jetty.server.Authentication.Deferred} may be returned. * * @throws ServerAuthException if unable to validate request diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java index 617e67a9465..44b0a76fa76 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java @@ -123,7 +123,8 @@ public class HashLoginService extends AbstractLoginService */ public void setUserStore(UserStore userStore) { - this._userStore = userStore; + updateBean(_userStore, userStore); + _userStore = userStore; } /* ------------------------------------------------------------ */ @@ -179,12 +180,29 @@ public class HashLoginService extends AbstractLoginService PropertyUserStore propertyUserStore = new PropertyUserStore(); propertyUserStore.setHotReload(hotReload); propertyUserStore.setConfig(_config); - propertyUserStore.start(); - _userStore = propertyUserStore; + setUserStore(propertyUserStore); _userStoreAutoCreate = true; } } + + /** + * To facilitate testing. + * @return the UserStore + */ + UserStore getUserStore () + { + return _userStore; + } + + /** + * To facilitate testing. + * @return true if a UserStore has been created from a config, false if a UserStore was provided. + */ + boolean isUserStoreAutoCreate () + { + return _userStoreAutoCreate; + } /* ------------------------------------------------------------ */ /** * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop() @@ -193,8 +211,10 @@ public class HashLoginService extends AbstractLoginService protected void doStop() throws Exception { super.doStop(); - if (_userStore != null && _userStoreAutoCreate) - _userStore.stop(); - _userStore = null; + if ( _userStoreAutoCreate) + { + setUserStore(null); + _userStoreAutoCreate = false; + } } } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/LoggedOutAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/LoggedOutAuthentication.java new file mode 100644 index 00000000000..fff4d41b991 --- /dev/null +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/LoggedOutAuthentication.java @@ -0,0 +1,60 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.security; + +import javax.servlet.ServletRequest; + +import org.eclipse.jetty.security.authentication.LoginAuthenticator; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.UserIdentity; + +/** + * LoggedOutAuthentication + * + * An Authentication indicating that a user has been previously, but is not currently logged in, + * but may be capable of logging in after a call to Request.login(String,String) + */ +public class LoggedOutAuthentication implements Authentication.NonAuthenticated +{ + private LoginAuthenticator _authenticator; + + public LoggedOutAuthentication (LoginAuthenticator authenticator) + { + _authenticator = authenticator; + } + + + @Override + public Authentication login(String username, Object password, ServletRequest request) + { + if (username == null) + return null; + + UserIdentity identity = _authenticator.login(username, password, request); + if (identity != null) + { + IdentityService identity_service = _authenticator.getLoginService().getIdentityService(); + UserAuthentication authentication = new UserAuthentication("API",identity); + if (identity_service != null) + identity_service.associate(identity); + return authentication; + } + return null; + } +} diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java index 68f19442bdf..1c4be92dc12 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java @@ -581,8 +581,11 @@ public abstract class SecurityHandler extends HandlerWrapper implements Authenti public void logout(Authentication.User user) { LOG.debug("logout {}",user); + if (user == null) + return; + LoginService login_service=getLoginService(); - if (login_service!=null) + if (login_service != null) { login_service.logout(user.getUserIdentity()); } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java index 6c2d586d5e9..12659c9034c 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java @@ -40,10 +40,9 @@ public class UserAuthentication extends AbstractUserAuthentication } @Override + @Deprecated public void logout() { - SecurityHandler security=SecurityHandler.getCurrentSecurityHandler(); - if (security!=null) - security.logout(this); } + } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/BasicAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/BasicAuthenticator.java index e1654a793f3..31e941fce95 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/BasicAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/BasicAuthenticator.java @@ -20,7 +20,7 @@ package org.eclipse.jetty.security.authentication; import java.io.IOException; import java.nio.charset.StandardCharsets; - +import java.util.Base64; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; @@ -32,7 +32,6 @@ import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication.User; import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.security.Constraint; /** @@ -82,7 +81,7 @@ public class BasicAuthenticator extends LoginAuthenticator if ("basic".equalsIgnoreCase(method)) { credentials = credentials.substring(space+1); - credentials = B64Code.decode(credentials, StandardCharsets.ISO_8859_1); + credentials = new String(Base64.getDecoder().decode(credentials), StandardCharsets.ISO_8859_1); int i = credentials.indexOf(':'); if (i>0) { diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ClientCertAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ClientCertAuthenticator.java index 48928f46416..3c7c147f25b 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ClientCertAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ClientCertAuthenticator.java @@ -22,6 +22,7 @@ import java.security.KeyStore; import java.security.Principal; import java.security.cert.CRL; import java.security.cert.X509Certificate; +import java.util.Base64; import java.util.Collection; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; @@ -33,7 +34,6 @@ import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication.User; import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.security.CertificateUtils; import org.eclipse.jetty.util.security.CertificateValidator; @@ -113,7 +113,8 @@ public class ClientCertAuthenticator extends LoginAuthenticator if (principal == null) principal = cert.getIssuerDN(); final String username = principal == null ? "clientcert" : principal.getName(); - final char[] credential = B64Code.encode(cert.getSignature()); + // TODO: investigate if using a raw byte[] is better vs older char[] + final char[] credential = Base64.getEncoder().encodeToString(cert.getSignature()).toCharArray(); UserIdentity user = login(username, credential, req); if (user!=null) diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java index 8a76ad089fe..dc9cd7e1978 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/ConfigurableSpnegoAuthenticator.java @@ -37,6 +37,7 @@ import org.eclipse.jetty.security.SpnegoUserPrincipal; import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication.User; +import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -99,6 +100,23 @@ public class ConfigurableSpnegoAuthenticator extends LoginAuthenticator _authenticationDuration = authenticationDuration; } + /** + * Only renew the session id if the user has been fully authenticated, don't + * renew the session for any of the intermediate request/response handshakes. + */ + @Override + public UserIdentity login(String username, Object password, ServletRequest servletRequest) + { + SpnegoUserIdentity user = (SpnegoUserIdentity)_loginService.login(username, password, servletRequest); + if (user != null && user.isEstablished()) + { + Request request = Request.getBaseRequest(servletRequest); + renewSession(request, request == null ? null : request.getResponse()); + } + return user; + } + + @Override public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException { diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java index a630cf90b36..a9880b0486a 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java @@ -33,6 +33,8 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.security.IdentityService; import org.eclipse.jetty.security.LoginService; +import org.eclipse.jetty.security.LoggedOutAuthentication; +import org.eclipse.jetty.security.SecurityHandler; import org.eclipse.jetty.security.ServerAuthException; import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.server.Authentication; @@ -65,7 +67,6 @@ public class DeferredAuthentication implements Authentication.Deferred try { Authentication authentication = _authenticator.validateRequest(request,__deferredResponse,true); - if (authentication!=null && (authentication instanceof Authentication.User) && !(authentication instanceof Authentication.ResponseSent)) { LoginService login_service= _authenticator.getLoginService(); @@ -130,6 +131,25 @@ public class DeferredAuthentication implements Authentication.Deferred } return null; } + + + + @Override + public Authentication logout (ServletRequest request) + { + SecurityHandler security=SecurityHandler.getCurrentSecurityHandler(); + if (security!=null) + { + security.logout(null); + if (_authenticator instanceof LoginAuthenticator) + { + ((LoginAuthenticator)_authenticator).logout(request); + return new LoggedOutAuthentication((LoginAuthenticator)_authenticator); + } + } + + return Authentication.UNAUTHENTICATED; + } /* ------------------------------------------------------------ */ public Object getPreviousAssociation() diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java index f0b41ad5880..06200821cbf 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java @@ -22,13 +22,13 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.SecureRandom; +import java.util.Base64; import java.util.BitSet; import java.util.Objects; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; - import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; @@ -42,7 +42,6 @@ import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication.User; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; @@ -233,7 +232,7 @@ public class DigestAuthenticator extends LoginAuthenticator byte[] nounce = new byte[24]; _random.nextBytes(nounce); - nonce = new Nonce(new String(B64Code.encode(nounce)), request.getTimeStamp(), getMaxNonceCount()); + nonce = new Nonce(Base64.getEncoder().encodeToString(nounce), request.getTimeStamp(), getMaxNonceCount()); } while (_nonceMap.putIfAbsent(nonce._nonce, nonce) != null); _nonceQueue.add(nonce); diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java index 78d4b8bc133..75c0927b440 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java @@ -192,6 +192,7 @@ public class FormAuthenticator extends LoginAuthenticator UserIdentity user = super.login(username,password,request); if (user!=null) { + HttpSession session = ((HttpServletRequest)request).getSession(true); Authentication cached=new SessionAuthentication(getAuthMethod(),user,password); session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached); @@ -199,7 +200,22 @@ public class FormAuthenticator extends LoginAuthenticator return user; } - + + + @Override + public void logout(ServletRequest request) + { + super.logout(request); + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpSession session = httpRequest.getSession(false); + + if (session == null) + return; + + //clean up session + session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED); + } + /* ------------------------------------------------------------ */ @Override public void prepareRequest(ServletRequest request) @@ -537,7 +553,8 @@ public class FormAuthenticator extends LoginAuthenticator } /* ------------------------------------------------------------ */ - /** This Authentication represents a just completed Form authentication. + /** + * This Authentication represents a just completed Form authentication. * Subsequent requests from the same user are authenticated by the presents * of a {@link SessionAuthentication} instance in their session. */ diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java index 817dcc094d7..66a74773cb6 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java @@ -33,6 +33,7 @@ import org.eclipse.jetty.server.session.Session; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; + public abstract class LoginAuthenticator implements Authenticator { private static final Logger LOG = Log.getLogger(LoginAuthenticator.class); @@ -40,6 +41,8 @@ public abstract class LoginAuthenticator implements Authenticator protected LoginService _loginService; protected IdentityService _identityService; private boolean _renewSession; + + protected LoginAuthenticator() { @@ -51,6 +54,18 @@ public abstract class LoginAuthenticator implements Authenticator //empty implementation as the default } + /** + * If the UserIdentity is not null after this method calls {@link LoginService#login(String,Object,ServletRequest)}, it + * is assumed that the user is fully authenticated and we need to change the session id to prevent + * session fixation vulnerability. If the UserIdentity is not necessarily fully + * authenticated, then subclasses must override this method and + * determine when the UserIdentity IS fully authenticated and renew the session id. + * + * @param username the username of the client to be authenticated + * @param password the user's credential + * @param servletRequest the inbound request that needs authentication + * @return + */ public UserIdentity login(String username, Object password, ServletRequest servletRequest) { UserIdentity user = _loginService.login(username, password, servletRequest); @@ -63,6 +78,19 @@ public abstract class LoginAuthenticator implements Authenticator return null; } + + public void logout (ServletRequest request) + { + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpSession session = httpRequest.getSession(false); + if (session == null) + return; + + session.removeAttribute(Session.SESSION_CREATED_SECURE); + } + + + @Override public void setConfiguration(AuthConfiguration configuration) { diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java index db529af60cc..3f778edcd67 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java @@ -33,18 +33,25 @@ import org.eclipse.jetty.security.AbstractUserAuthentication; import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.SecurityHandler; import org.eclipse.jetty.server.UserIdentity; -import org.eclipse.jetty.server.session.Session; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -public class SessionAuthentication extends AbstractUserAuthentication implements Serializable, HttpSessionActivationListener, HttpSessionBindingListener + +/** + * SessionAuthentication + * + * When a user has been successfully authenticated with some types + * of Authenticator, the Authenticator stashes a SessionAuthentication + * into a HttpSession to remember that the user is authenticated. + * + */ +public class SessionAuthentication extends AbstractUserAuthentication + implements Serializable, HttpSessionActivationListener, HttpSessionBindingListener { private static final Logger LOG = Log.getLogger(SessionAuthentication.class); private static final long serialVersionUID = -4643200685888258706L; - - public final static String __J_AUTHENTICATED="org.eclipse.jetty.security.UserIdentity"; private final String _name; @@ -92,23 +99,12 @@ public class SessionAuthentication extends AbstractUserAuthentication implements } @Override + @Deprecated public void logout() { - if (_session!=null && _session.getAttribute(__J_AUTHENTICATED)!=null) - _session.removeAttribute(__J_AUTHENTICATED); - - doLogout(); - } - - private void doLogout() - { - SecurityHandler security=SecurityHandler.getCurrentSecurityHandler(); - if (security!=null) - security.logout(this); - if (_session!=null) - _session.removeAttribute(Session.SESSION_CREATED_SECURE); } + @Override public String toString() { @@ -118,7 +114,6 @@ public class SessionAuthentication extends AbstractUserAuthentication implements @Override public void sessionWillPassivate(HttpSessionEvent se) { - } @Override @@ -131,18 +126,15 @@ public class SessionAuthentication extends AbstractUserAuthentication implements } @Override + @Deprecated public void valueBound(HttpSessionBindingEvent event) { - if (_session==null) - { - _session=event.getSession(); - } } @Override + @Deprecated public void valueUnbound(HttpSessionBindingEvent event) { - doLogout(); } } diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java index 72d6ba354c7..96a395f19d8 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java @@ -19,10 +19,10 @@ package org.eclipse.jetty.security; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -60,7 +60,7 @@ import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.session.SessionHandler; -import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Password; @@ -71,10 +71,11 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static java.nio.charset.StandardCharsets.ISO_8859_1; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.in; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -416,7 +417,7 @@ public class ConstraintTest uncoveredPaths = _security.getPathsWithUncoveredHttpMethods(); assertNotNull(uncoveredPaths); assertEquals(1, uncoveredPaths.size()); - assertThat("/user/*", isIn(uncoveredPaths)); + assertThat("/user/*", is(in(uncoveredPaths))); //Test an explicitly named method with a http-method-omission to cover all other methods Constraint constraint2a = new Constraint(); @@ -444,7 +445,7 @@ public class ConstraintTest _security.addConstraintMapping(mapping3); uncoveredPaths = _security.getPathsWithUncoveredHttpMethods(); assertNotNull(uncoveredPaths); - assertThat("/omit/*", isIn(uncoveredPaths)); + assertThat("/omit/*", is(in(uncoveredPaths))); _security.setDenyUncoveredHttpMethods(true); uncoveredPaths = _security.getPathsWithUncoveredHttpMethods(); @@ -496,7 +497,7 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "GET /ctx/auth/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user:wrong") + "\r\n" + + "Authorization: Basic " + authBase64("user:wrong") + "\r\n" + "\r\n", HttpStatus.UNAUTHORIZED_401, (response) -> { @@ -507,7 +508,7 @@ public class ConstraintTest )); // rawResponse = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + -// "Authorization: Basic " + B64Code.encode("user:wrong") + "\r\n" + +// "Authorization: Basic " + authBase64("user:wrong") + "\r\n" + // "\r\n"); // assertThat(rawResponse, startsWith("HTTP/1.1 401 Unauthorized")); // assertThat(rawResponse, containsString("WWW-Authenticate: basic realm=\"TestRealm\"")); @@ -515,14 +516,14 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "GET /ctx/auth/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user:password") + "\r\n" + + "Authorization: Basic " + authBase64("user:password") + "\r\n" + "\r\n", HttpStatus.OK_200 ) )); // rawResponse = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + -// "Authorization: Basic " + B64Code.encode("user:password") + "\r\n" + +// "Authorization: Basic " + authBase64("user:password") + "\r\n" + // "\r\n"); // assertThat(rawResponse, startsWith("HTTP/1.1 200 OK")); @@ -545,7 +546,7 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "GET /ctx/admin/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("admin:wrong") + "\r\n" + + "Authorization: Basic " + authBase64("admin:wrong") + "\r\n" + "\r\n", HttpStatus.UNAUTHORIZED_401, (response) -> { @@ -558,7 +559,7 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "GET /ctx/admin/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user:password") + "\r\n" + + "Authorization: Basic " + authBase64("user:password") + "\r\n" + "\r\n", HttpStatus.FORBIDDEN_403, (response) -> { @@ -571,7 +572,7 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "GET /ctx/admin/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("admin:password") + "\r\n" + + "Authorization: Basic " + authBase64("admin:password") + "\r\n" + "\r\n", HttpStatus.OK_200) )); @@ -587,7 +588,7 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "GET /ctx/omit/x HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("admin:password") + "\r\n" + + "Authorization: Basic " + authBase64("admin:password") + "\r\n" + "\r\n", HttpStatus.OK_200 @@ -598,7 +599,7 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "POST /ctx/omit/x HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user2:password") + "\r\n" + + "Authorization: Basic " + authBase64("user2:password") + "\r\n" + "\r\n", HttpStatus.OK_200) )); @@ -606,7 +607,7 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "POST /ctx/omit/x HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user3:password") + "\r\n" + + "Authorization: Basic " + authBase64("user3:password") + "\r\n" + "\r\n", HttpStatus.OK_200) )); @@ -615,7 +616,7 @@ public class ConstraintTest scenarios.add(Arguments.of( new Scenario( "HEAD /ctx/omit/x HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user2:password") + "\r\n" + + "Authorization: Basic " + authBase64("user2:password") + "\r\n" + "\r\n", HttpStatus.FORBIDDEN_403) )); @@ -688,17 +689,17 @@ public class ConstraintTest MessageDigest md = MessageDigest.getInstance("MD5"); byte[] ha1; // calc A1 digest - md.update(username.getBytes(StandardCharsets.ISO_8859_1)); + md.update(username.getBytes(ISO_8859_1)); md.update((byte) ':'); - md.update("TestRealm".getBytes(StandardCharsets.ISO_8859_1)); + md.update("TestRealm".getBytes(ISO_8859_1)); md.update((byte) ':'); - md.update(password.getBytes(StandardCharsets.ISO_8859_1)); + md.update(password.getBytes(ISO_8859_1)); ha1 = md.digest(); // calc A2 digest md.reset(); - md.update("GET".getBytes(StandardCharsets.ISO_8859_1)); + md.update("GET".getBytes(ISO_8859_1)); md.update((byte) ':'); - md.update(uri.getBytes(StandardCharsets.ISO_8859_1)); + md.update(uri.getBytes(ISO_8859_1)); byte[] ha2 = md.digest(); // calc digest @@ -708,17 +709,17 @@ public class ConstraintTest // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) // ) > <"> - md.update(TypeUtil.toString(ha1, 16).getBytes(StandardCharsets.ISO_8859_1)); + md.update(TypeUtil.toString(ha1, 16).getBytes(ISO_8859_1)); md.update((byte) ':'); - md.update(nonce.getBytes(StandardCharsets.ISO_8859_1)); + md.update(nonce.getBytes(ISO_8859_1)); md.update((byte) ':'); - md.update(nc.getBytes(StandardCharsets.ISO_8859_1)); + md.update(nc.getBytes(ISO_8859_1)); md.update((byte) ':'); - md.update(CNONCE.getBytes(StandardCharsets.ISO_8859_1)); + md.update(CNONCE.getBytes(ISO_8859_1)); md.update((byte) ':'); - md.update("auth".getBytes(StandardCharsets.ISO_8859_1)); + md.update("auth".getBytes(ISO_8859_1)); md.update((byte) ':'); - md.update(TypeUtil.toString(ha2, 16).getBytes(StandardCharsets.ISO_8859_1)); + md.update(TypeUtil.toString(ha2, 16).getBytes(ISO_8859_1)); byte[] digest = md.digest(); // check digest @@ -832,7 +833,7 @@ public class ConstraintTest assertThat(response, containsString("Expires")); assertThat(response, containsString("URI=/ctx/testLoginPage")); - String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -852,7 +853,7 @@ public class ConstraintTest assertThat(response, containsString("Location")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -887,7 +888,7 @@ public class ConstraintTest assertThat(response, containsString(" 302 Found")); assertThat(response, containsString("/ctx/testLoginPage")); assertThat(response, containsString("JSESSIONID=")); - String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/testLoginPage HTTP/1.0\r\n"+ "Cookie: JSESSIONID=" + session + "\r\n" + @@ -916,7 +917,7 @@ public class ConstraintTest assertThat(response, containsString("/ctx/auth/info")); assertThat(response, containsString("JSESSIONID=")); assertThat(response, not(containsString("JSESSIONID=" + session))); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -953,7 +954,7 @@ public class ConstraintTest "test_parameter=test_value\r\n"); assertThat(response, containsString(" 302 Found")); assertThat(response, containsString("/ctx/testLoginPage")); - String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/testLoginPage HTTP/1.0\r\n"+ "Cookie: JSESSIONID=" + session + "\r\n" + @@ -979,7 +980,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); // sneak in other request response = _connector.getResponse("GET /ctx/auth/other HTTP/1.0\r\n" + @@ -1042,7 +1043,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info;jsessionid="+session+";other HTTP/1.0\r\n" + "\r\n"); @@ -1053,6 +1054,174 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 403")); assertThat(response, containsString("!role")); } + + + /** + * Test Request.login() Request.logout() with FORM authenticator + * @throws Exception + */ + @Test + public void testFormProgrammaticLoginLogout() throws Exception + { + //Test programmatic login/logout within same request: + // login - perform programmatic login that should succeed, next request should be also logged in + // loginfail - perform programmatic login that should fail, next request should not be logged in + // loginfaillogin - perform programmatic login that should fail then another that succeeds, next request should be logged in + // loginlogin - perform successful login then try another that should fail, next request should be logged in + // loginlogout - perform successful login then logout, next request should not be logged in + // loginlogoutlogin - perform successful login then logout then login successfully again, next request should be logged in + _security.setHandler(new ProgrammaticLoginRequestHandler()); + _security.setAuthenticator(new FormAuthenticator("/testLoginPage","/testErrorPage",false)); + _server.start(); + + String response; + + //login + response = _connector.getResponse("GET /ctx/prog?action=login HTTP/1.0\r\n\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + assertThat(response, containsString("user=admin")); + _server.stop(); + + //loginfail + _server.start(); + response = _connector.getResponse("GET /ctx/prog?action=loginfail HTTP/1.0\r\n\r\n"); + assertThat(response, startsWith("HTTP/1.1 500 Server Error")); + if (response.contains("JSESSIONID")) + { + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + } + else + response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n\r\n"); + + assertThat(response, not(containsString("user=admin"))); + _server.stop(); + + //loginfaillogin + _server.start(); + response = _connector.getResponse("GET /ctx/prog?action=loginfail HTTP/1.0\r\n\r\n"); + assertThat(response, startsWith("HTTP/1.1 500 Server Error")); + response = _connector.getResponse("GET /ctx/prog?action=login HTTP/1.0\r\n\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + assertThat(response, containsString("user=admin")); + _server.stop(); + + //loginlogin + _server.start(); + response = _connector.getResponse("GET /ctx/prog?action=loginlogin HTTP/1.0\r\n\r\n"); + assertThat(response, startsWith("HTTP/1.1 500 Server Error")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + assertThat(response, containsString("user=admin")); + _server.stop(); + + //loginlogout + _server.start(); + response = _connector.getResponse("GET /ctx/prog?action=loginlogout HTTP/1.0\r\n\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + assertThat(response, containsString("user=null")); + _server.stop(); + + //loginlogoutlogin + _server.start(); + response = _connector.getResponse("GET /ctx/prog?action=loginlogoutlogin HTTP/1.0\r\n\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, startsWith("HTTP/1.1 200 OK")); + assertThat(response, containsString("user=user0")); + _server.stop(); + + + //Test constraint-based login with programmatic login/logout: + // constraintlogin - perform constraint login, followed by programmatic login which should fail (already logged in) + _server.start(); + response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); + assertThat(response, containsString(" 302 Found")); + assertThat(response, containsString("/ctx/testLoginPage")); + assertThat(response, containsString("JSESSIONID=")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + + response = _connector.getResponse("GET /ctx/testLoginPage HTTP/1.0\r\n"+ + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, containsString(" 200 OK")); + assertThat(response, not(containsString("JSESSIONID=" + session))); + response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Content-Length: 35\r\n" + + "\r\n" + + "j_username=user&j_password=password"); + assertThat(response, startsWith("HTTP/1.1 302 ")); + assertThat(response, containsString("Location")); + assertThat(response, containsString("/ctx/auth/info")); + assertThat(response, containsString("JSESSIONID=")); + assertThat(response, not(containsString("JSESSIONID=" + session))); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + response = _connector.getResponse("GET /ctx/prog?action=constraintlogin HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, startsWith("HTTP/1.1 500 Server Error")); + _server.stop(); + + // logout - perform constraint login, followed by programmatic logout, which should succeed + _server.start(); + response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); + assertThat(response, containsString(" 302 Found")); + assertThat(response, containsString("/ctx/testLoginPage")); + assertThat(response, containsString("JSESSIONID=")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + + response = _connector.getResponse("GET /ctx/testLoginPage HTTP/1.0\r\n"+ + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, containsString(" 200 OK")); + assertThat(response, not(containsString("JSESSIONID=" + session))); + response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Content-Length: 35\r\n" + + "\r\n" + + "j_username=user&j_password=password"); + assertThat(response, startsWith("HTTP/1.1 302 ")); + assertThat(response, containsString("Location")); + assertThat(response, containsString("/ctx/auth/info")); + assertThat(response, containsString("JSESSIONID=")); + assertThat(response, not(containsString("JSESSIONID=" + session))); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); + response = _connector.getResponse("GET /ctx/prog?action=logout HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, containsString(" 200 OK")); + response = _connector.getResponse("GET /ctx/prog?x=y HTTP/1.0\r\n" + + "Cookie: JSESSIONID=" + session + "\r\n" + + "\r\n"); + assertThat(response, containsString(" 200 OK")); + assertThat(response, containsString("user=null")); + } @Test public void testStrictBasic() throws Exception @@ -1072,18 +1241,18 @@ public class ConstraintTest assertThat(response, containsString("WWW-Authenticate: basic realm=\"TestRealm\"")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user:wrong") + "\r\n" + + "Authorization: Basic " + authBase64("user:wrong") + "\r\n" + "\r\n"); assertThat(response, startsWith("HTTP/1.1 401 Unauthorized")); assertThat(response, containsString("WWW-Authenticate: basic realm=\"TestRealm\"")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user3:password") + "\r\n" + + "Authorization: Basic " + authBase64("user3:password") + "\r\n" + "\r\n"); assertThat(response, startsWith("HTTP/1.1 403")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user2:password") + "\r\n" + + "Authorization: Basic " + authBase64("user2:password") + "\r\n" + "\r\n"); assertThat(response, startsWith("HTTP/1.1 200 OK")); @@ -1094,20 +1263,20 @@ public class ConstraintTest assertThat(response, containsString("WWW-Authenticate: basic realm=\"TestRealm\"")); response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("admin:wrong") + "\r\n" + + "Authorization: Basic " + authBase64("admin:wrong") + "\r\n" + "\r\n"); assertThat(response, startsWith("HTTP/1.1 401 Unauthorized")); assertThat(response, containsString("WWW-Authenticate: basic realm=\"TestRealm\"")); response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user:password") + "\r\n" + + "Authorization: Basic " + authBase64("user:password") + "\r\n" + "\r\n"); assertThat(response, startsWith("HTTP/1.1 403 ")); assertThat(response, containsString("!role")); response = _connector.getResponse("GET /ctx/admin/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("admin:password") + "\r\n" + + "Authorization: Basic " + authBase64("admin:password") + "\r\n" + "\r\n"); assertThat(response, startsWith("HTTP/1.1 200 OK")); @@ -1138,7 +1307,7 @@ public class ConstraintTest assertThat(response, containsString("Expires")); assertThat(response, containsString("URI=/ctx/testLoginPage")); - String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1158,7 +1327,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1178,7 +1347,7 @@ public class ConstraintTest response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); // assertThat(response,startsWith("HTTP/1.1 302 ")); // assertThat(response,containsString("testLoginPage")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1189,7 +1358,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1208,7 +1377,7 @@ public class ConstraintTest response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); // assertThat(response,startsWith("HTTP/1.1 302 ")); // assertThat(response,containsString("testLoginPage")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1219,7 +1388,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1250,7 +1419,7 @@ public class ConstraintTest assertThat(response, containsString(" 302 Found")); assertThat(response, containsString("http://wibble.com:8888/ctx/testLoginPage")); - String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + String session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1269,7 +1438,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1289,7 +1458,7 @@ public class ConstraintTest response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("testLoginPage")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1300,7 +1469,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + @@ -1320,7 +1489,7 @@ public class ConstraintTest response = _connector.getResponse("GET /ctx/starstar/info HTTP/1.0\r\n\r\n"); assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("testLoginPage")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1331,7 +1500,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/starstar/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/starstar/info HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1343,7 +1512,7 @@ public class ConstraintTest response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); // assertThat(response,startsWith("HTTP/1.1 302 ")); // assertThat(response,containsString("testLoginPage")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("POST /ctx/j_security_check HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1354,7 +1523,7 @@ public class ConstraintTest assertThat(response, startsWith("HTTP/1.1 302 ")); assertThat(response, containsString("Location")); assertThat(response, containsString("/ctx/auth/info")); - session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx")); + session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf("; Path=/ctx")); response = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + "Cookie: JSESSIONID=" + session + "\r\n" + @@ -1428,7 +1597,7 @@ public class ConstraintTest assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200)); rawResponse = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user2:password") + "\r\n" + + "Authorization: Basic " + authBase64("user2:password") + "\r\n" + "\r\n", 100000, TimeUnit.MILLISECONDS); response = HttpTester.parseResponse(rawResponse); assertThat(response.toString(), response.getStatus(), is(HttpStatus.INTERNAL_SERVER_ERROR_500)); @@ -1443,7 +1612,7 @@ public class ConstraintTest _server.start(); rawResponse = _connector.getResponse("GET /ctx/auth/info HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("user2:password") + "\r\n" + + "Authorization: Basic " + authBase64("user2:password") + "\r\n" + "\r\n", 100000, TimeUnit.MILLISECONDS); response = HttpTester.parseResponse(rawResponse); assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200)); @@ -1463,13 +1632,13 @@ public class ConstraintTest assertThat(response, containsString("user=null")); response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n"+ - "Authorization: Basic " + B64Code.encode("admin:wrong") + "\r\n" + + "Authorization: Basic " + authBase64("admin:wrong") + "\r\n" + "\r\n"); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("user=null")); response = _connector.getResponse("GET /ctx/noauth/info HTTP/1.0\r\n"+ - "Authorization: Basic " + B64Code.encode("admin:password") + "\r\n" + + "Authorization: Basic " + authBase64("admin:password") + "\r\n" + "\r\n"); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("user=admin")); @@ -1491,6 +1660,13 @@ public class ConstraintTest response = _connector.getResponse("GET /ctx/forbid/post HTTP/1.0\r\n\r\n"); assertThat(response, startsWith("HTTP/1.1 200 ")); // This is so stupid, but it is the S P E C } + + private static String authBase64(String authorization) + { + byte raw[] = authorization.getBytes(ISO_8859_1); + return Base64.getEncoder().encodeToString(raw); + } + private class RequestHandler extends AbstractHandler { @Override @@ -1512,6 +1688,72 @@ public class ConstraintTest } } + private class ProgrammaticLoginRequestHandler extends AbstractHandler + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response ) throws IOException, ServletException + { + baseRequest.setHandled(true); + + String action = request.getParameter("action"); + if (StringUtil.isBlank(action)) + { + response.setStatus(200); + response.setContentType("text/plain; charset=UTF-8"); + response.getWriter().println("user="+request.getRemoteUser()); + return; + } + else if ("login".equals(action)) + { + request.login("admin", "password"); + return; + } + else if ("loginfail".equals(action)) + { + request.login("admin", "fail"); + return; + } + else if ("loginfaillogin".equals(action)) + { + try + { + request.login("admin", "fail"); + } + catch (ServletException se) + { + request.login("admin", "password"); + } + return; + } + else if ("loginlogin".equals(action)) + { + request.login("admin", "password"); + request.login("foo", "bar"); + } + else if ("loginlogout".equals(action)) + { + request.login("admin", "password"); + request.logout(); + } + else if ("loginlogoutlogin".equals(action)) + { + request.login("admin", "password"); + request.logout(); + request.login("user0", "password"); + } + else if ("constraintlogin".equals(action)) + { + String user = request.getRemoteUser(); + request.login("admin", "password"); + } + else if ("logout".equals(action)) + { + request.logout(); + } + else + response.sendError(500); + } + } private class RoleRefHandler extends HandlerWrapper { /* ------------------------------------------------------------ */ diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/HashLoginServiceTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/HashLoginServiceTest.java new file mode 100644 index 00000000000..81db6ec867b --- /dev/null +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/HashLoginServiceTest.java @@ -0,0 +1,70 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + + +package org.eclipse.jetty.security; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Tests of the HashLoginService. + */ +public class HashLoginServiceTest +{ + @Test + public void testAutoCreatedUserStore() throws Exception + { + HashLoginService loginService = new HashLoginService("foo", MavenTestingUtils.getTestResourceFile("foo.properties").getAbsolutePath()); + assertThat(loginService.getIdentityService(), is(notNullValue())); + loginService.start(); + assertTrue(loginService.getUserStore().isStarted()); + assertTrue(loginService.isUserStoreAutoCreate()); + + loginService.stop(); + assertFalse(loginService.isUserStoreAutoCreate()); + assertThat(loginService.getUserStore(), is(nullValue())); + } + + @Test + public void testProvidedUserStore() throws Exception + { + HashLoginService loginService = new HashLoginService("foo"); + assertThat(loginService.getIdentityService(), is(notNullValue())); + UserStore store = new UserStore(); + loginService.setUserStore(store); + assertFalse(store.isStarted()); + loginService.start(); + assertTrue(loginService.getUserStore().isStarted()); + assertFalse(loginService.isUserStoreAutoCreate()); + + loginService.stop(); + + assertFalse(loginService.isUserStoreAutoCreate()); + assertFalse(store.isStarted()); + assertThat(loginService.getUserStore(), is(notNullValue())); + } +} diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java index e057058a83a..4971795188d 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java @@ -20,9 +20,9 @@ package org.eclipse.jetty.security; import java.io.IOException; import java.util.Arrays; +import java.util.Base64; import java.util.HashSet; import java.util.Set; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -35,7 +35,6 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.session.SessionHandler; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Password; import org.junit.jupiter.api.AfterEach; @@ -43,6 +42,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static java.nio.charset.StandardCharsets.ISO_8859_1; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -297,37 +297,41 @@ public class SpecExampleConstraintTest response = _connector.getResponse("HEAD /ctx/index.html HTTP/1.0\r\n\r\n"); assertTrue(response.startsWith("HTTP/1.1 403 Forbidden")); + Base64.Encoder authEncoder = Base64.getEncoder(); + String encodedHarry = authEncoder.encodeToString("harry:password".getBytes(ISO_8859_1)); + String encodedChris = authEncoder.encodeToString("chris:password".getBytes(ISO_8859_1)); + response = _connector.getResponse("HEAD /ctx/index.html HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("harry:password") + "\r\n" + + "Authorization: Basic " + encodedHarry + "\r\n" + "\r\n"); assertThat(response,startsWith("HTTP/1.1 403 Forbidden")); response = _connector.getResponse("HEAD /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("harry:password") + "\r\n" + + "Authorization: Basic " + encodedHarry + "\r\n" + "\r\n"); assertThat(response,startsWith("HTTP/1.1 403 Forbidden")); response = _connector.getResponse("HEAD /ctx/acme/retail/index.html HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("harry:password") + "\r\n" + + "Authorization: Basic " + encodedHarry + "\r\n" + "\r\n"); assertThat(response,startsWith("HTTP/1.1 403 Forbidden")); //a user in role CONTRACTOR can do a GET response = _connector.getResponse("GET /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("chris:password") + "\r\n" + + "Authorization: Basic " + encodedChris + "\r\n" + "\r\n"); assertThat(response,startsWith("HTTP/1.1 200 OK")); //a user in role CONTRACTOR can only do a post if confidential response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("chris:password") + "\r\n" + + "Authorization: Basic " + encodedChris + "\r\n" + "\r\n"); assertThat(response,startsWith("HTTP/1.1 403 ")); //a user in role HOMEOWNER can do a GET response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" + - "Authorization: Basic " + B64Code.encode("harry:password") + "\r\n" + + "Authorization: Basic " + encodedHarry + "\r\n" + "\r\n"); assertThat(response,startsWith("HTTP/1.1 200 OK")); } diff --git a/jetty-security/src/test/resources/foo.properties b/jetty-security/src/test/resources/foo.properties new file mode 100644 index 00000000000..aee03ecb84b --- /dev/null +++ b/jetty-security/src/test/resources/foo.properties @@ -0,0 +1,2 @@ +ken=123 +amy=456 diff --git a/jetty-server/pom.xml b/jetty-server/pom.xml index a49b18098f6..16cfef78863 100644 --- a/jetty-server/pom.xml +++ b/jetty-server/pom.xml @@ -79,11 +79,6 @@ ${project.version} test - - org.hamcrest - hamcrest-core - test - diff --git a/jetty-server/src/main/config/etc/home-base-warning.xml b/jetty-server/src/main/config/etc/home-base-warning.xml index 4d568f138ea..dc06d1096ef 100644 --- a/jetty-server/src/main/config/etc/home-base-warning.xml +++ b/jetty-server/src/main/config/etc/home-base-warning.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-acceptratelimit.xml b/jetty-server/src/main/config/etc/jetty-acceptratelimit.xml index a6e53f11979..3baccd6d2d6 100644 --- a/jetty-server/src/main/config/etc/jetty-acceptratelimit.xml +++ b/jetty-server/src/main/config/etc/jetty-acceptratelimit.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-bytebufferpool.xml b/jetty-server/src/main/config/etc/jetty-bytebufferpool.xml index 6da3421a938..6c622de7a00 100644 --- a/jetty-server/src/main/config/etc/jetty-bytebufferpool.xml +++ b/jetty-server/src/main/config/etc/jetty-bytebufferpool.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-connectionlimit.xml b/jetty-server/src/main/config/etc/jetty-connectionlimit.xml index be2927949fa..2e4fbeec552 100644 --- a/jetty-server/src/main/config/etc/jetty-connectionlimit.xml +++ b/jetty-server/src/main/config/etc/jetty-connectionlimit.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-debug.xml b/jetty-server/src/main/config/etc/jetty-debug.xml index 2e47a5f9ffc..2a1cfdff7d7 100644 --- a/jetty-server/src/main/config/etc/jetty-debug.xml +++ b/jetty-server/src/main/config/etc/jetty-debug.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-debuglog.xml b/jetty-server/src/main/config/etc/jetty-debuglog.xml index a330294d94d..e390547543a 100644 --- a/jetty-server/src/main/config/etc/jetty-debuglog.xml +++ b/jetty-server/src/main/config/etc/jetty-debuglog.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-gzip.xml b/jetty-server/src/main/config/etc/jetty-gzip.xml index 76e9d8606f4..4353d387bda 100644 --- a/jetty-server/src/main/config/etc/jetty-gzip.xml +++ b/jetty-server/src/main/config/etc/jetty-gzip.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-http-forwarded.xml b/jetty-server/src/main/config/etc/jetty-http-forwarded.xml index 50b80976a2a..573afe99174 100644 --- a/jetty-server/src/main/config/etc/jetty-http-forwarded.xml +++ b/jetty-server/src/main/config/etc/jetty-http-forwarded.xml @@ -1,5 +1,5 @@ - + @@ -11,6 +11,7 @@ + diff --git a/jetty-server/src/main/config/etc/jetty-http.xml b/jetty-server/src/main/config/etc/jetty-http.xml index 28e0fa42802..c4fd4272656 100644 --- a/jetty-server/src/main/config/etc/jetty-http.xml +++ b/jetty-server/src/main/config/etc/jetty-http.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-https.xml b/jetty-server/src/main/config/etc/jetty-https.xml index c9d497e2c15..0fd29d5028d 100644 --- a/jetty-server/src/main/config/etc/jetty-https.xml +++ b/jetty-server/src/main/config/etc/jetty-https.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-lowresources.xml b/jetty-server/src/main/config/etc/jetty-lowresources.xml index 4e9fbd8d1ea..6d282da0a95 100644 --- a/jetty-server/src/main/config/etc/jetty-lowresources.xml +++ b/jetty-server/src/main/config/etc/jetty-lowresources.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-proxy-protocol-ssl.xml b/jetty-server/src/main/config/etc/jetty-proxy-protocol-ssl.xml index 91452f27385..5fcf3008dfc 100644 --- a/jetty-server/src/main/config/etc/jetty-proxy-protocol-ssl.xml +++ b/jetty-server/src/main/config/etc/jetty-proxy-protocol-ssl.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-proxy-protocol.xml b/jetty-server/src/main/config/etc/jetty-proxy-protocol.xml index 5169c4fddd6..b4b747aae4d 100644 --- a/jetty-server/src/main/config/etc/jetty-proxy-protocol.xml +++ b/jetty-server/src/main/config/etc/jetty-proxy-protocol.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/modules/requestlog/jetty-customrequestlog.xml b/jetty-server/src/main/config/etc/jetty-requestlog.xml similarity index 89% rename from jetty-server/src/main/config/modules/requestlog/jetty-customrequestlog.xml rename to jetty-server/src/main/config/etc/jetty-requestlog.xml index baae68fb8c3..037042ff5cd 100644 --- a/jetty-server/src/main/config/modules/requestlog/jetty-customrequestlog.xml +++ b/jetty-server/src/main/config/etc/jetty-requestlog.xml @@ -1,5 +1,5 @@ - + @@ -30,7 +30,7 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-ssl-context.xml b/jetty-server/src/main/config/etc/jetty-ssl-context.xml index e0946470f88..343d6993c3a 100644 --- a/jetty-server/src/main/config/etc/jetty-ssl-context.xml +++ b/jetty-server/src/main/config/etc/jetty-ssl-context.xml @@ -1,5 +1,5 @@ - + @@ -10,7 +10,7 @@ https://www.eclipse.org/jetty/documentation/current/configuring-ssl.html#configuring-sslcontextfactory-cipherSuites --> - + / diff --git a/jetty-server/src/main/config/etc/jetty-ssl.xml b/jetty-server/src/main/config/etc/jetty-ssl.xml index 019dc8c944e..2316fc2c34d 100644 --- a/jetty-server/src/main/config/etc/jetty-ssl.xml +++ b/jetty-server/src/main/config/etc/jetty-ssl.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-stats.xml b/jetty-server/src/main/config/etc/jetty-stats.xml index 954acd761df..4e8312ba013 100644 --- a/jetty-server/src/main/config/etc/jetty-stats.xml +++ b/jetty-server/src/main/config/etc/jetty-stats.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-threadlimit.xml b/jetty-server/src/main/config/etc/jetty-threadlimit.xml index 11376e0478e..fb933971658 100644 --- a/jetty-server/src/main/config/etc/jetty-threadlimit.xml +++ b/jetty-server/src/main/config/etc/jetty-threadlimit.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-threadpool.xml b/jetty-server/src/main/config/etc/jetty-threadpool.xml index 1931932b9a1..e003701db21 100644 --- a/jetty-server/src/main/config/etc/jetty-threadpool.xml +++ b/jetty-server/src/main/config/etc/jetty-threadpool.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty.xml b/jetty-server/src/main/config/etc/jetty.xml index 365bdbdf7cf..e784fe487d7 100644 --- a/jetty-server/src/main/config/etc/jetty.xml +++ b/jetty-server/src/main/config/etc/jetty.xml @@ -1,5 +1,5 @@ - + @@ -67,6 +67,7 @@ + diff --git a/jetty-server/src/main/config/etc/sessions/file/session-store.xml b/jetty-server/src/main/config/etc/sessions/file/session-store.xml index 97ad907d78d..3855b01de41 100644 --- a/jetty-server/src/main/config/etc/sessions/file/session-store.xml +++ b/jetty-server/src/main/config/etc/sessions/file/session-store.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/sessions/id-manager.xml b/jetty-server/src/main/config/etc/sessions/id-manager.xml index 945d5076275..fb174604fc8 100644 --- a/jetty-server/src/main/config/etc/sessions/id-manager.xml +++ b/jetty-server/src/main/config/etc/sessions/id-manager.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/sessions/jdbc/datasource.xml b/jetty-server/src/main/config/etc/sessions/jdbc/datasource.xml index 1a5cff37c05..5e0a499c5e9 100644 --- a/jetty-server/src/main/config/etc/sessions/jdbc/datasource.xml +++ b/jetty-server/src/main/config/etc/sessions/jdbc/datasource.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/sessions/jdbc/driver.xml b/jetty-server/src/main/config/etc/sessions/jdbc/driver.xml index 6a0a8720fe0..11cd41a9161 100644 --- a/jetty-server/src/main/config/etc/sessions/jdbc/driver.xml +++ b/jetty-server/src/main/config/etc/sessions/jdbc/driver.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/sessions/jdbc/session-store.xml b/jetty-server/src/main/config/etc/sessions/jdbc/session-store.xml index 2bc8295ec5b..701938044ba 100644 --- a/jetty-server/src/main/config/etc/sessions/jdbc/session-store.xml +++ b/jetty-server/src/main/config/etc/sessions/jdbc/session-store.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/sessions/session-cache-hash.xml b/jetty-server/src/main/config/etc/sessions/session-cache-hash.xml index 9909e774915..64ff4325b98 100644 --- a/jetty-server/src/main/config/etc/sessions/session-cache-hash.xml +++ b/jetty-server/src/main/config/etc/sessions/session-cache-hash.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/sessions/session-cache-null.xml b/jetty-server/src/main/config/etc/sessions/session-cache-null.xml index b65c224cc46..a3a52ffc721 100644 --- a/jetty-server/src/main/config/etc/sessions/session-cache-null.xml +++ b/jetty-server/src/main/config/etc/sessions/session-cache-null.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/etc/sessions/session-data-cache/session-caching-store.xml b/jetty-server/src/main/config/etc/sessions/session-data-cache/session-caching-store.xml index 7d15aa41284..d06da254435 100644 --- a/jetty-server/src/main/config/etc/sessions/session-data-cache/session-caching-store.xml +++ b/jetty-server/src/main/config/etc/sessions/session-data-cache/session-caching-store.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/modules/customrequestlog.mod b/jetty-server/src/main/config/modules/customrequestlog.mod index 3bda6ee4005..275f7d3ca13 100644 --- a/jetty-server/src/main/config/modules/customrequestlog.mod +++ b/jetty-server/src/main/config/modules/customrequestlog.mod @@ -1,43 +1,11 @@ DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html [description] -An implementation of requestlog using CustomRequestLog and AsyncRequestLogWriter - -[provides] -requestlog-impl +Deprecated name for requestlog using custom request logger [tags] requestlog -logging -[depend] -server +[depends] +requestlog -[xml] -etc/jetty-customrequestlog.xml - -[files] -logs/ -basehome:modules/requestlog/jetty-customrequestlog.xml|etc/jetty-customrequestlog.xml - -[ini-template] -## Format string -# jetty.customrequestlog.formatString=%a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t "%r" %s %B "%{Referer}i" "%{User-Agent}i" "%C" - -## Logging directory (relative to $jetty.base) -# jetty.requestlog.dir=logs - -## File path -# jetty.requestlog.filePath=${jetty.requestlog.dir}/yyyy_mm_dd.request.log - -## Date format for rollovered files (uses SimpleDateFormat syntax) -# jetty.requestlog.filenameDateFormat=yyyy_MM_dd - -## How many days to retain old log files -# jetty.requestlog.retainDays=90 - -## Whether to append to existing file -# jetty.requestlog.append=false - -## Timezone of the log file rollover -# jetty.requestlog.timezone=GMT diff --git a/jetty-server/src/main/config/modules/http-forwarded.mod b/jetty-server/src/main/config/modules/http-forwarded.mod index 34e25642b2c..f67822065a4 100644 --- a/jetty-server/src/main/config/modules/http-forwarded.mod +++ b/jetty-server/src/main/config/modules/http-forwarded.mod @@ -23,6 +23,7 @@ etc/jetty-http-forwarded.xml # jetty.httpConfig.forwardedServerHeader=X-Forwarded-Server # jetty.httpConfig.forwardedProtoHeader=X-Forwarded-Proto # jetty.httpConfig.forwardedForHeader=X-Forwarded-For +# jetty.httpConfig.forwardedPortHeader=X-Forwarded-Port # jetty.httpConfig.forwardedHttpsHeader=X-Proxied-Https # jetty.httpConfig.forwardedSslSessionIdHeader=Proxy-ssl-id # jetty.httpConfig.forwardedCipherSuiteHeader=Proxy-auth-cert diff --git a/jetty-server/src/main/config/modules/http.mod b/jetty-server/src/main/config/modules/http.mod index 430d8519e2e..e1f89b00dbe 100644 --- a/jetty-server/src/main/config/modules/http.mod +++ b/jetty-server/src/main/config/modules/http.mod @@ -41,6 +41,3 @@ etc/jetty-http.xml ## Connect Timeout in milliseconds # jetty.http.connectTimeout=15000 - -## HTTP Compliance: RFC7230, RFC7230_LEGACY, RFC2616, RFC2616_LEGACY, LEGACY or CUSTOMn -# jetty.http.compliance=RFC7230_LEGACY diff --git a/jetty-server/src/main/config/modules/inetaccess.mod b/jetty-server/src/main/config/modules/inetaccess.mod index 075464c631e..9619b6a4f8e 100644 --- a/jetty-server/src/main/config/modules/inetaccess.mod +++ b/jetty-server/src/main/config/modules/inetaccess.mod @@ -1,3 +1,5 @@ +DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html + [description] Enable the InetAccessHandler to apply a include/exclude control of the remote IP of requests. @@ -9,7 +11,22 @@ handler server [files] -basehome:modules/inetaccess/inetaccess.xml|etc/inetaccess.xml +basehome:modules/inetaccess/jetty-inetaccess.xml|etc/jetty-inetaccess.xml [xml] -etc/inetaccess.xml +etc/jetty-inetaccess.xml + +[ini-template] + +## List of InetAddress patterns to include +#jetty.inetaccess.include=127.0.0.1,127.0.0.2 + +## List of InetAddress patterns to exclude +#jetty.inetaccess.exclude=127.0.0.1,127.0.0.2 + +## List of Connector names to include +#jetty.inetaccess.includeConnectors=http + +## List of Connector names to exclude +#jetty.inetaccess.excludeConnectors=tls + diff --git a/jetty-server/src/main/config/modules/inetaccess/inetaccess.xml b/jetty-server/src/main/config/modules/inetaccess/inetaccess.xml index d58c878c5bf..5e4a7b406e0 100644 --- a/jetty-server/src/main/config/modules/inetaccess/inetaccess.xml +++ b/jetty-server/src/main/config/modules/inetaccess/inetaccess.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/modules/inetaccess/jetty-inetaccess.xml b/jetty-server/src/main/config/modules/inetaccess/jetty-inetaccess.xml new file mode 100644 index 00000000000..d977a004f45 --- /dev/null +++ b/jetty-server/src/main/config/modules/inetaccess/jetty-inetaccess.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetty-server/src/main/config/modules/logback-access/jetty-logback-access.xml b/jetty-server/src/main/config/modules/logback-access/jetty-logback-access.xml index b2cf4b2efb9..f58880eb666 100644 --- a/jetty-server/src/main/config/modules/logback-access/jetty-logback-access.xml +++ b/jetty-server/src/main/config/modules/logback-access/jetty-logback-access.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-server/src/main/config/modules/requestlog.mod b/jetty-server/src/main/config/modules/requestlog.mod index 52edafc3c27..0cd25f780b5 100644 --- a/jetty-server/src/main/config/modules/requestlog.mod +++ b/jetty-server/src/main/config/modules/requestlog.mod @@ -1,12 +1,39 @@ DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html [description] -Enables the default custom request log. +Log requests using CustomRequestLog and AsyncRequestLogWriter [tags] requestlog +logging -[depends] -customrequestlog +[depend] +server +[xml] +etc/jetty-requestlog.xml +[files] +logs/ + +[ini-template] +## Format string +# jetty.requestlog.formatString=%a - %u %{dd/MMM/yyyy:HH:mm:ss ZZZ|GMT}t "%r" %s %B "%{Referer}i" "%{User-Agent}i" "%C" + +## Logging directory (relative to $jetty.base) +# jetty.requestlog.dir=logs + +## File path +# jetty.requestlog.filePath=${jetty.requestlog.dir}/yyyy_mm_dd.request.log + +## Date format for rollovered files (uses SimpleDateFormat syntax) +# jetty.requestlog.filenameDateFormat=yyyy_MM_dd + +## How many days to retain old log files +# jetty.requestlog.retainDays=90 + +## Whether to append to existing file +# jetty.requestlog.append=false + +## Timezone of the log file rollover +# jetty.requestlog.timezone=GMT diff --git a/jetty-server/src/main/config/modules/server.mod b/jetty-server/src/main/config/modules/server.mod index d07f9d63c0e..be9bff04e0d 100644 --- a/jetty-server/src/main/config/modules/server.mod +++ b/jetty-server/src/main/config/modules/server.mod @@ -59,15 +59,15 @@ etc/jetty.xml ## Maximum number of error dispatches to prevent looping # jetty.httpConfig.maxErrorDispatches=10 +## HTTP Compliance: RFC7230, RFC7230_LEGACY, RFC2616, RFC2616_LEGACY, LEGACY +# jetty.httpConfig.compliance=RFC7230 + ## Cookie compliance mode for parsing request Cookie headers: RFC2965, RFC6265 # jetty.httpConfig.requestCookieCompliance=RFC6265 ## Cookie compliance mode for generating response Set-Cookie: RFC2965, RFC6265 # jetty.httpConfig.responseCookieCompliance=RFC6265 -## multipart/form-data compliance mode of: LEGACY(slow), RFC7578(fast) -# jetty.httpConfig.multiPartFormDataCompliance=LEGACY - ### Server configuration ## Whether ctrl+c on the console gracefully stops the Jetty server # jetty.server.stopAtShutdown=true diff --git a/jetty-server/src/main/config/modules/ssl.mod b/jetty-server/src/main/config/modules/ssl.mod index 1fa918eadf5..be27d35546f 100644 --- a/jetty-server/src/main/config/modules/ssl.mod +++ b/jetty-server/src/main/config/modules/ssl.mod @@ -58,7 +58,7 @@ etc/jetty-ssl-context.xml ## The Endpoint Identification Algorithm ## Same as javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String) -#jetty.sslContext.endpointIdentificationAlgorithm=HTTPS +#jetty.sslContext.endpointIdentificationAlgorithm= ## SSL JSSE Provider # jetty.sslContext.provider= diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java index 70a7474d1bf..dda65f6af3f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnectionFactory.java @@ -111,7 +111,7 @@ public abstract class AbstractConnectionFactory extends ContainerLifeCycle imple return String.format("%s@%x%s",this.getClass().getSimpleName(),hashCode(),getProtocols()); } - public static ConnectionFactory[] getFactories(SslContextFactory sslContextFactory, ConnectionFactory... factories) + public static ConnectionFactory[] getFactories(SslContextFactory.Server sslContextFactory, ConnectionFactory... factories) { factories=ArrayUtil.removeNulls(factories); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java index d84f52c8219..1a92d1f630c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -183,7 +183,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co _executor=executor!=null?executor:_server.getThreadPool(); if (scheduler==null) scheduler=_server.getBean(Scheduler.class); - _scheduler=scheduler!=null?scheduler:new ScheduledExecutorScheduler(); + _scheduler=scheduler!=null?scheduler:new ScheduledExecutorScheduler(String.format("Connector-Scheduler-%x",hashCode()),false); if (pool==null) pool=_server.getBean(ByteBufferPool.class); _byteBufferPool = pool!=null?pool:new ArrayByteBufferPool(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Authentication.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Authentication.java index eaadb742bdc..60df8fe1988 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Authentication.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Authentication.java @@ -45,11 +45,12 @@ public interface Authentication /* ------------------------------------------------------------ */ /** A successful Authentication with User information. */ - public interface User extends Authentication + public interface User extends LogoutAuthentication { String getAuthMethod(); UserIdentity getUserIdentity(); boolean isUserInRole(UserIdentity.Scope scope,String role); + @Deprecated void logout(); } @@ -63,31 +64,13 @@ public interface Authentication HttpServletResponse getHttpServletResponse(); } - /* ------------------------------------------------------------ */ - /** A deferred authentication with methods to progress - * the authentication process. + /** + * An authentication that is capable of performing a programmatic login + * operation. + * */ - public interface Deferred extends Authentication + public interface LoginAuthentication extends Authentication { - /* ------------------------------------------------------------ */ - /** Authenticate if possible without sending a challenge. - * This is used to check credentials that have been sent for - * non-manditory authentication. - * @param request the request - * @return The new Authentication state. - */ - Authentication authenticate(ServletRequest request); - - /* ------------------------------------------------------------ */ - /** Authenticate and possibly send a challenge. - * This is used to initiate authentication for previously - * non-manditory authentication. - * @param request the request - * @param response the response - * @return The new Authentication state. - */ - Authentication authenticate(ServletRequest request,ServletResponse response); - /* ------------------------------------------------------------ */ /** Login with the LOGIN authenticator @@ -98,6 +81,53 @@ public interface Authentication */ Authentication login(String username,Object password,ServletRequest request); } + + + /** + * An authentication that is capable of performing a programmatic + * logout operation. + * + */ + public interface LogoutAuthentication extends Authentication + { + /* ------------------------------------------------------------ */ + /** + * Remove any user information that may be present in the request + * such that a call to getUserPrincipal/getRemoteUser will return null. + * + * @param request the request + * @return NoAuthentication if we successfully logged out + */ + Authentication logout (ServletRequest request); + } + + + /* ------------------------------------------------------------ */ + /** A deferred authentication with methods to progress + * the authentication process. + */ + public interface Deferred extends LoginAuthentication, LogoutAuthentication + { + /* ------------------------------------------------------------ */ + /** Authenticate if possible without sending a challenge. + * This is used to check credentials that have been sent for + * non-mandatory authentication. + * @param request the request + * @return The new Authentication state. + */ + Authentication authenticate(ServletRequest request); + + /* ------------------------------------------------------------ */ + /** Authenticate and possibly send a challenge. + * This is used to initiate authentication for previously + * non-mandatory authentication. + * @param request the request + * @param response the response + * @return The new Authentication state. + */ + Authentication authenticate(ServletRequest request,ServletResponse response); + + } /* ------------------------------------------------------------ */ @@ -127,6 +157,14 @@ public interface Authentication public interface SendSuccess extends ResponseSent { } + + /* ------------------------------------------------------------ */ + /** After a logout, the authentication reverts to a state + * where it is possible to programmatically log in again. + */ + public interface NonAuthenticated extends LoginAuthentication + { + } /* ------------------------------------------------------------ */ /** Unauthenticated state. diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Cookies.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Cookies.java index 8ee5482d008..7fbaa67b1c7 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Cookies.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Cookies.java @@ -17,11 +17,12 @@ // package org.eclipse.jetty.server; + import java.util.ArrayList; import java.util.List; - import javax.servlet.http.Cookie; +import org.eclipse.jetty.http.ComplianceViolation; import org.eclipse.jetty.http.CookieCompliance; import org.eclipse.jetty.http.CookieCutter; import org.eclipse.jetty.util.log.Log; @@ -49,12 +50,12 @@ public class Cookies extends CookieCutter public Cookies() { - this(CookieCompliance.RFC6265); + this(CookieCompliance.RFC6265, null); } - public Cookies(CookieCompliance compliance) + public Cookies(CookieCompliance compliance, ComplianceViolation.Listener complianceListener) { - super(compliance); + super(compliance, complianceListener); } public void addCookieField(String rawField) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java index 5034d7c3bdf..10c95aa6955 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ForwardedRequestCustomizer.java @@ -18,7 +18,11 @@ package org.eclipse.jetty.server; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.net.InetSocketAddress; + import javax.servlet.ServletRequest; import org.eclipse.jetty.http.HostPortHttpField; @@ -26,16 +30,20 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; -import org.eclipse.jetty.http.QuotedCSV; +import org.eclipse.jetty.http.QuotedCSVParser; import org.eclipse.jetty.server.HttpConfiguration.Customizer; +import org.eclipse.jetty.util.ArrayTrie; import org.eclipse.jetty.util.HostPort; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.Trie; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import static java.lang.invoke.MethodType.methodType; -/* ------------------------------------------------------------ */ -/** Customize Requests for Proxy Forwarding. + +/** + * Customize Requests for Proxy Forwarding. *

* This customizer looks at at HTTP request for headers that indicate * it has been forwarded by one or more proxies. Specifically handled are @@ -51,7 +59,8 @@ import org.eclipse.jetty.util.log.Logger; * so that the proxy is not seen as the other end point of the connection on which * the request came

*

Headers can also be defined so that forwarded SSL Session IDs and Cipher - * suites may be customised

+ * suites may be customised

+ * * @see Wikipedia: X-Forwarded-For */ public class ForwardedRequestCustomizer implements Customizer @@ -62,14 +71,21 @@ public class ForwardedRequestCustomizer implements Customizer private String _forwardedHeader = HttpHeader.FORWARDED.toString(); private String _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString(); private String _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString(); - private String _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString(); private String _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString(); + private String _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString(); + private String _forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString(); private String _forwardedHttpsHeader = "X-Proxied-Https"; private String _forwardedCipherSuiteHeader = "Proxy-auth-cert"; private String _forwardedSslSessionIdHeader = "Proxy-ssl-id"; - private boolean _proxyAsAuthority=false; - private boolean _sslIsSecure=true; - + private boolean _proxyAsAuthority = false; + private boolean _sslIsSecure = true; + private Trie _handles; + + public ForwardedRequestCustomizer() + { + updateHandles(); + } + /** * @return true if the proxy address obtained via * {@code X-Forwarded-Server} or RFC7239 "by" is used as @@ -82,7 +98,7 @@ public class ForwardedRequestCustomizer implements Customizer /** * @param proxyAsAuthority if true, use the proxy address obtained via - * {@code X-Forwarded-Server} or RFC7239 "by" as the request authority. + * {@code X-Forwarded-Server} or RFC7239 "by" as the request authority. */ public void setProxyAsAuthority(boolean proxyAsAuthority) { @@ -99,44 +115,47 @@ public class ForwardedRequestCustomizer implements Customizer { if (rfc7239only) { - if (_forwardedHeader==null) - _forwardedHeader=HttpHeader.FORWARDED.toString(); - _forwardedHostHeader=null; - _forwardedHostHeader=null; - _forwardedServerHeader=null; - _forwardedForHeader=null; - _forwardedProtoHeader=null; - _forwardedHttpsHeader=null; + if (_forwardedHeader == null) + _forwardedHeader = HttpHeader.FORWARDED.toString(); + _forwardedHostHeader = null; + _forwardedServerHeader = null; + _forwardedForHeader = null; + _forwardedPortHeader = null; + _forwardedProtoHeader = null; + _forwardedHttpsHeader = null; } else { - if (_forwardedHostHeader==null) + if (_forwardedHostHeader == null) _forwardedHostHeader = HttpHeader.X_FORWARDED_HOST.toString(); - if (_forwardedServerHeader==null) + if (_forwardedServerHeader == null) _forwardedServerHeader = HttpHeader.X_FORWARDED_SERVER.toString(); - if (_forwardedForHeader==null) + if (_forwardedForHeader == null) _forwardedForHeader = HttpHeader.X_FORWARDED_FOR.toString(); - if (_forwardedProtoHeader==null) + if (_forwardedPortHeader == null) + _forwardedPortHeader = HttpHeader.X_FORWARDED_PORT.toString(); + if (_forwardedProtoHeader == null) _forwardedProtoHeader = HttpHeader.X_FORWARDED_PROTO.toString(); - if (_forwardedHttpsHeader==null) + if (_forwardedHttpsHeader == null) _forwardedHttpsHeader = "X-Proxied-Https"; } + + updateHandles(); } - + public String getForcedHost() { return _forcedHost.getValue(); } - + /** * Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}. * - * @param hostAndPort - * The value of the host header to force. + * @param hostAndPort The value of the host header to force. */ public void setForcedHost(String hostAndPort) { - _forcedHost = new HostPortHttpField(hostAndPort); + _forcedHost = new HostPortHttpField(new ForcedHostPort(hostAndPort)); } /** @@ -148,12 +167,15 @@ public class ForwardedRequestCustomizer implements Customizer } /** - * @param forwardedHeader - * The header name for RFC forwarded (default Forwarded) + * @param forwardedHeader The header name for RFC forwarded (default Forwarded) */ public void setForwardedHeader(String forwardedHeader) { - _forwardedHeader = forwardedHeader; + if (_forwardedHeader == null || !_forwardedHeader.equals(forwardedHeader)) + { + _forwardedHeader = forwardedHeader; + updateHandles(); + } } public String getForwardedHostHeader() @@ -162,12 +184,15 @@ public class ForwardedRequestCustomizer implements Customizer } /** - * @param forwardedHostHeader - * The header name for forwarded hosts (default {@code X-Forwarded-Host}) + * @param forwardedHostHeader The header name for forwarded hosts (default {@code X-Forwarded-Host}) */ public void setForwardedHostHeader(String forwardedHostHeader) { - _forwardedHostHeader = forwardedHostHeader; + if (_forwardedHostHeader == null || !_forwardedHostHeader.equalsIgnoreCase(forwardedHostHeader)) + { + _forwardedHostHeader = forwardedHostHeader; + updateHandles(); + } } /** @@ -179,12 +204,15 @@ public class ForwardedRequestCustomizer implements Customizer } /** - * @param forwardedServerHeader - * The header name for forwarded server (default {@code X-Forwarded-Server}) + * @param forwardedServerHeader The header name for forwarded server (default {@code X-Forwarded-Server}) */ public void setForwardedServerHeader(String forwardedServerHeader) { - _forwardedServerHeader = forwardedServerHeader; + if (_forwardedServerHeader == null || !_forwardedServerHeader.equalsIgnoreCase(forwardedServerHeader)) + { + _forwardedServerHeader = forwardedServerHeader; + updateHandles(); + } } /** @@ -196,12 +224,32 @@ public class ForwardedRequestCustomizer implements Customizer } /** - * @param forwardedRemoteAddressHeader - * The header name for forwarded for (default {@code X-Forwarded-For}) + * @param forwardedRemoteAddressHeader The header name for forwarded for (default {@code X-Forwarded-For}) */ public void setForwardedForHeader(String forwardedRemoteAddressHeader) { - _forwardedForHeader = forwardedRemoteAddressHeader; + if (_forwardedForHeader == null || !_forwardedForHeader.equalsIgnoreCase(forwardedRemoteAddressHeader)) + { + _forwardedForHeader = forwardedRemoteAddressHeader; + updateHandles(); + } + } + + public String getForwardedPortHeader() + { + return _forwardedHostHeader; + } + + /** + * @param forwardedPortHeader The header name for forwarded hosts (default {@code X-Forwarded-Port}) + */ + public void setForwardedPortHeader(String forwardedPortHeader) + { + if (_forwardedHostHeader == null || !_forwardedHostHeader.equalsIgnoreCase(forwardedPortHeader)) + { + _forwardedHostHeader = forwardedPortHeader; + updateHandles(); + } } /** @@ -217,12 +265,15 @@ public class ForwardedRequestCustomizer implements Customizer /** * Set the forwardedProtoHeader. * - * @param forwardedProtoHeader - * the forwardedProtoHeader to set (default {@code X-Forwarded-Proto}) + * @param forwardedProtoHeader the forwardedProtoHeader to set (default {@code X-Forwarded-Proto}) */ public void setForwardedProtoHeader(String forwardedProtoHeader) { - _forwardedProtoHeader = forwardedProtoHeader; + if (_forwardedProtoHeader == null || !_forwardedProtoHeader.equalsIgnoreCase(forwardedProtoHeader)) + { + _forwardedProtoHeader = forwardedProtoHeader; + updateHandles(); + } } /** @@ -234,12 +285,15 @@ public class ForwardedRequestCustomizer implements Customizer } /** - * @param forwardedCipherSuite - * The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert}) + * @param forwardedCipherSuiteHeader The header name holding a forwarded cipher suite (default {@code Proxy-auth-cert}) */ - public void setForwardedCipherSuiteHeader(String forwardedCipherSuite) + public void setForwardedCipherSuiteHeader(String forwardedCipherSuiteHeader) { - _forwardedCipherSuiteHeader = forwardedCipherSuite; + if (_forwardedCipherSuiteHeader == null || !_forwardedCipherSuiteHeader.equalsIgnoreCase(forwardedCipherSuiteHeader)) + { + _forwardedCipherSuiteHeader = forwardedCipherSuiteHeader; + updateHandles(); + } } /** @@ -251,12 +305,15 @@ public class ForwardedRequestCustomizer implements Customizer } /** - * @param forwardedSslSessionId - * The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id}) + * @param forwardedSslSessionIdHeader The header name holding a forwarded SSL Session ID (default {@code Proxy-ssl-id}) */ - public void setForwardedSslSessionIdHeader(String forwardedSslSessionId) + public void setForwardedSslSessionIdHeader(String forwardedSslSessionIdHeader) { - _forwardedSslSessionIdHeader = forwardedSslSessionId; + if (_forwardedSslSessionIdHeader == null || !_forwardedSslSessionIdHeader.equalsIgnoreCase(forwardedSslSessionIdHeader)) + { + _forwardedSslSessionIdHeader = forwardedSslSessionIdHeader; + updateHandles(); + } } /** @@ -272,9 +329,13 @@ public class ForwardedRequestCustomizer implements Customizer */ public void setForwardedHttpsHeader(String forwardedHttpsHeader) { - _forwardedHttpsHeader = forwardedHttpsHeader; + if (_forwardedHttpsHeader == null || !_forwardedHttpsHeader.equalsIgnoreCase(forwardedHttpsHeader)) + { + _forwardedHttpsHeader = forwardedHttpsHeader; + updateHandles(); + } } - + /** * @return true if the presence of a SSL session or certificate header is sufficient * to indicate a secure request (default is true) @@ -286,7 +347,7 @@ public class ForwardedRequestCustomizer implements Customizer /** * @param sslIsSecure true if the presence of a SSL session or certificate header is sufficient - * to indicate a secure request (default is true) + * to indicate a secure request (default is true) */ public void setSslIsSecure(boolean sslIsSecure) { @@ -298,126 +359,42 @@ public class ForwardedRequestCustomizer implements Customizer { HttpFields httpFields = request.getHttpFields(); - RFC7239 rfc7239 = null; - String forwardedHost = null; - String forwardedServer = null; - HostPort forwardedFor = null; - String forwardedProto = null; - String forwardedHttps = null; - // Do a single pass through the header fields as it is a more efficient single iteration. - for (HttpField field : httpFields) + Forwarded forwarded = new Forwarded(request, config); + try { - String name = field.getName(); - - if (getForwardedCipherSuiteHeader()!=null && getForwardedCipherSuiteHeader().equalsIgnoreCase(name)) + for (HttpField field : httpFields) { - request.setAttribute("javax.servlet.request.cipher_suite",field.getValue()); - if (isSslIsSecure()) - { - request.setSecure(true); - request.setScheme(config.getSecureScheme()); - } - } - - if (getForwardedSslSessionIdHeader()!=null && getForwardedSslSessionIdHeader().equalsIgnoreCase(name)) - { - request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue()); - if (isSslIsSecure()) - { - request.setSecure(true); - request.setScheme(config.getSecureScheme()); - } - } - - if (forwardedHost==null && _forwardedHostHeader!=null && _forwardedHostHeader.equalsIgnoreCase(name)) - forwardedHost = getLeftMost(field.getValue()); - - if (forwardedServer==null && _forwardedServerHeader!=null && _forwardedServerHeader.equalsIgnoreCase(name)) - forwardedServer = getLeftMost(field.getValue()); - - if (forwardedFor==null && _forwardedForHeader!=null && _forwardedForHeader.equalsIgnoreCase(name)) - forwardedFor = getRemoteAddr(field.getValue()); - - if (forwardedProto==null && _forwardedProtoHeader!=null && _forwardedProtoHeader.equalsIgnoreCase(name)) - forwardedProto = getLeftMost(field.getValue()); - - if (forwardedHttps==null && _forwardedHttpsHeader!=null && _forwardedHttpsHeader.equalsIgnoreCase(name)) - forwardedHttps = getLeftMost(field.getValue()); - - if (_forwardedHeader!=null && _forwardedHeader.equalsIgnoreCase(name)) - { - if (rfc7239==null) - rfc7239= new RFC7239(); - rfc7239.addValue(field.getValue()); + MethodHandle handle = _handles.get(field.getName()); + if (handle != null) + handle.invoke(forwarded, field); } } - - // Handle host header if if not available any RFC7230.by or X-ForwardedServer header - if (_forcedHost != null) + catch (Throwable e) { - // Update host header - httpFields.put(_forcedHost); - request.setAuthority(_forcedHost.getHost(),_forcedHost.getPort()); - } - else if (rfc7239!=null && rfc7239._host!=null) - { - HostPortHttpField auth = rfc7239._host; - httpFields.put(auth); - request.setAuthority(auth.getHost(),auth.getPort()); - } - else if (forwardedHost != null) - { - HostPortHttpField auth = new HostPortHttpField(forwardedHost); - httpFields.put(auth); - request.setAuthority(auth.getHost(),auth.getPort()); - } - else if (_proxyAsAuthority) - { - if (rfc7239!=null && rfc7239._by!=null) - { - HostPortHttpField auth = rfc7239._by; - httpFields.put(auth); - request.setAuthority(auth.getHost(),auth.getPort()); - } - else if (forwardedServer != null) - { - request.setAuthority(forwardedServer,request.getServerPort()); - } + throw new RuntimeException(e); } - // handle remote end identifier - if (rfc7239!=null && rfc7239._for!=null) + if (forwarded._proto!=null) { - request.setRemoteAddr(InetSocketAddress.createUnresolved(rfc7239._for.getHost(),rfc7239._for.getPort())); - } - else if (forwardedFor != null) - { - request.setRemoteAddr(InetSocketAddress.createUnresolved(forwardedFor.getHost(), (forwardedFor.getPort() > 0) ? forwardedFor.getPort() : request.getRemotePort())); + request.setScheme(forwarded._proto); + if (forwarded._proto.equalsIgnoreCase(config.getSecureScheme())) + request.setSecure(true); } - // handle protocol identifier - if (rfc7239!=null && rfc7239._proto!=null) + if (forwarded._host!=null) { - request.setScheme(rfc7239._proto); - if (rfc7239._proto.equals(config.getSecureScheme())) - request.setSecure(true); + httpFields.put(new HostPortHttpField(forwarded._host)); + request.setAuthority(forwarded._host.getHost(), forwarded._host.getPort()); } - else if (forwardedProto != null) + + if (forwarded._for!=null) { - request.setScheme(forwardedProto); - if (forwardedProto.equals(config.getSecureScheme())) - request.setSecure(true); - } - else if (forwardedHttps !=null && ("on".equalsIgnoreCase(forwardedHttps)||"true".equalsIgnoreCase(forwardedHttps))) - { - request.setScheme(HttpScheme.HTTPS.asString()); - if (HttpScheme.HTTPS.asString().equals(config.getSecureScheme())) - request.setSecure(true); + int port = forwarded._for.getPort()>0 ? forwarded._for.getPort() : request.getRemotePort(); + request.setRemoteAddr(InetSocketAddress.createUnresolved(forwarded._for.getHost(),port)); } } - /* ------------------------------------------------------------ */ protected String getLeftMost(String headerValue) { if (headerValue == null) @@ -432,71 +409,218 @@ public class ForwardedRequestCustomizer implements Customizer } // The left-most value is the farthest downstream client - return headerValue.substring(0,commaIndex).trim(); + return headerValue.substring(0, commaIndex).trim(); } - protected HostPort getRemoteAddr(String headerValue) - { - String leftMost = getLeftMost(headerValue); - if (leftMost == null) - { - return null; - } - - try - { - return new HostPort(leftMost); - } - catch (Exception e) - { - // failed to parse in host[:port] format - LOG.ignore(e); - return null; - } - } - @Override public String toString() { - return String.format("%s@%x",this.getClass().getSimpleName(),hashCode()); + return String.format("%s@%x", this.getClass().getSimpleName(), hashCode()); } - private final class RFC7239 extends QuotedCSV + public String getHostHeader() { - HostPortHttpField _by; - HostPortHttpField _for; - HostPortHttpField _host; + return _forcedHost.getValue(); + } + + /** + * Set a forced valued for the host header to control what is returned by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}. + * + * @param hostHeader The value of the host header to force. + */ + public void setHostHeader(String hostHeader) + { + _forcedHost = new HostPortHttpField(hostHeader); + } + + private void updateHandles() + { + int size = 0; + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodType type = methodType(Void.TYPE, HttpField.class); + + while (true) + { + try + { + size += 128; + _handles = new ArrayTrie<>(size); + + if (_forwardedCipherSuiteHeader != null && !_handles.put(_forwardedCipherSuiteHeader, lookup.findVirtual(Forwarded.class, "handleCipherSuite", type))) + continue; + if (_forwardedSslSessionIdHeader != null && !_handles.put(_forwardedSslSessionIdHeader, lookup.findVirtual(Forwarded.class, "handleSslSessionId", type))) + continue; + if (_forwardedHeader != null && !_handles.put(_forwardedHeader, lookup.findVirtual(Forwarded.class, "handleRFC7239", type))) + continue; + if (_forwardedForHeader != null && !_handles.put(_forwardedForHeader, lookup.findVirtual(Forwarded.class, "handleFor", type))) + continue; + if (_forwardedPortHeader != null && !_handles.put(_forwardedPortHeader, lookup.findVirtual(Forwarded.class, "handlePort", type))) + continue; + if (_forwardedHostHeader != null && !_handles.put(_forwardedHostHeader, lookup.findVirtual(Forwarded.class, "handleHost", type))) + continue; + if (_forwardedProtoHeader != null && !_handles.put(_forwardedProtoHeader, lookup.findVirtual(Forwarded.class, "handleProto", type))) + continue; + if (_forwardedHttpsHeader != null && !_handles.put(_forwardedHttpsHeader, lookup.findVirtual(Forwarded.class, "handleHttps", type))) + continue; + if (_forwardedServerHeader != null && !_handles.put(_forwardedServerHeader, lookup.findVirtual(Forwarded.class, "handleServer", type))) + continue; + break; + } + catch (NoSuchMethodException | IllegalAccessException e) + { + throw new IllegalStateException(e); + } + } + } + + private static class ForcedHostPort extends HostPort + { + ForcedHostPort(String authority) + { + super(authority); + } + } + + private static class XHostPort extends HostPort + { + XHostPort(String authority) + { + super(authority); + } + + XHostPort(String host, int port) + { + super(host, port); + } + } + + private static class Rfc7239HostPort extends HostPort + { + Rfc7239HostPort(String authority) + { + super(authority); + } + } + + private class Forwarded extends QuotedCSVParser + { + HttpConfiguration _config; + Request _request; + + boolean _protoRfc7239; String _proto; - - private RFC7239() + HostPort _for; + HostPort _host; + + public Forwarded(Request request, HttpConfiguration config) { super(false); + _request = request; + _config = config; + if (_forcedHost!=null) + _host = _forcedHost.getHostPort(); + } + + public void handleCipherSuite(HttpField field) + { + _request.setAttribute("javax.servlet.request.cipher_suite", field.getValue()); + if (isSslIsSecure()) + { + _request.setSecure(true); + _request.setScheme(_config.getSecureScheme()); + } + } + + public void handleSslSessionId(HttpField field) + { + _request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue()); + if (isSslIsSecure()) + { + _request.setSecure(true); + _request.setScheme(_config.getSecureScheme()); + } + } + + public void handleHost(HttpField field) + { + if (_host==null) + _host = new XHostPort(getLeftMost(field.getValue())); + } + + public void handleServer(HttpField field) + { + if (_proxyAsAuthority && _host==null) + _host = new XHostPort(getLeftMost(field.getValue())); + } + + public void handleProto(HttpField field) + { + if (_proto==null) + _proto = getLeftMost(field.getValue()); + } + + public void handleFor(HttpField field) + { + if (_for==null) + _for = new XHostPort(getLeftMost(field.getValue())); + else if (_for instanceof XHostPort && "unknown".equals(_for.getHost())) + _for = new XHostPort(HostPort.normalizeHost(getLeftMost(field.getValue())),_for.getPort()); + } + + public void handlePort(HttpField field) + { + if (_for == null) + _for = new XHostPort("unknown", field.getIntValue()); + else if (_for instanceof XHostPort && _for.getPort()<=0) + _for = new XHostPort(HostPort.normalizeHost(_for.getHost()), field.getIntValue()); + } + + public void handleHttps(HttpField field) + { + if (_proto==null && ("on".equalsIgnoreCase(field.getValue()) || "true".equalsIgnoreCase(field.getValue()))) + _proto = HttpScheme.HTTPS.asString(); + } + + public void handleRFC7239(HttpField field) + { + addValue(field.getValue()); } @Override protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) { - if (valueLength==0 && paramValue>paramName) + if (valueLength == 0 && paramValue > paramName) { - String name=StringUtil.asciiToLowerCase(buffer.substring(paramName,paramValue-1)); - String value=buffer.substring(paramValue); - switch(name) + String name = StringUtil.asciiToLowerCase(buffer.substring(paramName, paramValue - 1)); + String value = buffer.substring(paramValue); + switch (name) { case "by": - if (_by==null && !value.startsWith("_") && !"unknown".equals(value)) - _by=new HostPortHttpField(value); + if (!_proxyAsAuthority) + break; + if (value.startsWith("_") || "unknown".equals(value)) + break; + if (_host == null || _host instanceof XHostPort) + _host = new Rfc7239HostPort(value); break; case "for": - if (_for==null && !value.startsWith("_") && !"unknown".equals(value)) - _for=new HostPortHttpField(value); + if (value.startsWith("_") || "unknown".equals(value)) + break; + if (_for == null || _for instanceof XHostPort) + _for = new Rfc7239HostPort(value); break; case "host": - if (_host==null) - _host=new HostPortHttpField(value); + if (value.startsWith("_") || "unknown".equals(value)) + break; + if (_host == null || _host instanceof XHostPort) + _host = new Rfc7239HostPort(value); break; case "proto": - if (_proto==null) - _proto=value; + if (_proto == null || !_protoRfc7239) + { + _protoRfc7239 = true; + _proto = value; + } break; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 63fdcbc833c..c84664bb050 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -765,9 +765,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void onBadMessage(BadMessageException failure) { int status = failure.getCode(); - String reason = failure.getReason(); + String message = failure.getReason(); if (status < 400 || status > 599) - failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, reason, failure); + failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, message, failure); notifyRequestFailure(_request, failure); @@ -793,9 +793,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor ErrorHandler handler=getServer().getBean(ErrorHandler.class); if (handler!=null) - content=handler.badMessageError(status,reason,fields); + content=handler.badMessageError(status,message,fields); - sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1,status,reason,fields,BufferUtil.length(content)),content ,true); + sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1,status,null,fields,BufferUtil.length(content)),content ,true); } } catch (IOException e) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java index 78121a21b93..fe2abc7d17e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java @@ -25,9 +25,9 @@ import java.util.ArrayList; import java.util.List; import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.ComplianceViolation; import org.eclipse.jetty.http.HostPortHttpField; import org.eclipse.jetty.http.HttpCompliance; -import org.eclipse.jetty.http.HttpComplianceSection; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpGenerator; @@ -47,7 +47,7 @@ import org.eclipse.jetty.util.log.Logger; /** * A HttpChannel customized to be transported over the HTTP/1 protocol */ -public class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandler, HttpParser.ComplianceHandler +public class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandler, ComplianceViolation.Listener { private static final Logger LOG = Log.getLogger(HttpChannelOverHttp.class); private final static HttpField PREAMBLE_UPGRADE_H2C = new HttpField(HttpHeader.UPGRADE, "h2c"); @@ -516,7 +516,13 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque } @Override - public void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String reason) + public boolean isHeaderCacheCaseSensitive() + { + return getHttpConfiguration().isHeaderCacheCaseSensitive(); + } + + @Override + public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String details) { if (_httpConnection.isRecordHttpComplianceViolations()) { @@ -524,8 +530,8 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque { _complianceViolations = new ArrayList<>(); } - String record = String.format("%s (see %s) in mode %s for %s in %s", - violation.getDescription(), violation.getURL(), compliance, reason, getHttpTransport()); + String record = String.format("%s (see %s) in mode %s for %s in %s", + violation.getDescription(), violation.getURL(), mode, details, getHttpTransport()); _complianceViolations.add(record); if (LOG.isDebugEnabled()) LOG.debug(record); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index de296ced18a..7507c8c92b3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -740,13 +740,13 @@ public class HttpChannelState final Request baseRequest = _channel.getRequest(); int code=HttpStatus.INTERNAL_SERVER_ERROR_500; - String reason=null; + String message=null; Throwable cause = _channel.unwrap(th,BadMessageException.class,UnavailableException.class); if (cause instanceof BadMessageException) { BadMessageException bme = (BadMessageException)cause; code = bme.getCode(); - reason = bme.getReason(); + message = bme.getReason(); } else if (cause instanceof UnavailableException) { @@ -768,7 +768,7 @@ public class HttpChannelState _event.getSuppliedRequest().setAttribute(ERROR_STATUS_CODE,code); _event.getSuppliedRequest().setAttribute(ERROR_EXCEPTION,th); _event.getSuppliedRequest().setAttribute(ERROR_EXCEPTION_TYPE,th==null?null:th.getClass()); - _event.getSuppliedRequest().setAttribute(ERROR_MESSAGE,reason); + _event.getSuppliedRequest().setAttribute(ERROR_MESSAGE,message); } else { @@ -778,7 +778,7 @@ public class HttpChannelState baseRequest.setAttribute(ERROR_STATUS_CODE,code); baseRequest.setAttribute(ERROR_EXCEPTION,th); baseRequest.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE,th==null?null:th.getClass()); - baseRequest.setAttribute(ERROR_MESSAGE,reason); + baseRequest.setAttribute(ERROR_MESSAGE,message); } // Are we blocking? diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index 812a38397f8..a81898fd7b3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -61,6 +61,7 @@ public class HttpConfiguration implements Dumpable private int _requestHeaderSize=8*1024; private int _responseHeaderSize=8*1024; private int _headerCacheSize=4*1024; + private boolean _headerCacheCaseSensitive=false; private int _securePort; private long _idleTimeout=-1; private String _secureScheme = HttpScheme.HTTPS.asString(); @@ -197,6 +198,12 @@ public class HttpConfiguration implements Dumpable return _headerCacheSize; } + @ManagedAttribute("True if the header field cache is case sensitive") + public boolean isHeaderCacheCaseSensitive() + { + return _headerCacheCaseSensitive; + } + @ManagedAttribute("The port to which Integral or Confidential security constraints are redirected") public int getSecurePort() { @@ -390,6 +397,11 @@ public class HttpConfiguration implements Dumpable _headerCacheSize = headerCacheSize; } + public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive) + { + this._headerCacheCaseSensitive = headerCacheCaseSensitive; + } + /** *

Sets the TCP/IP port used for CONFIDENTIAL and INTEGRAL redirections.

* @@ -520,9 +532,9 @@ public class HttpConfiguration implements Dumpable return _httpCompliance; } - public void setHttpCompliance(HttpCompliance _httpCompliance) + public void setHttpCompliance(HttpCompliance httpCompliance) { - this._httpCompliance = _httpCompliance; + _httpCompliance = httpCompliance; } /** diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index 94d90faf42b..0df89da0ed9 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -730,7 +730,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable else continue; } - else if (crlf.hasRemaining()) + else if (crlf != null && crlf.hasRemaining()) { result = encoder.encode(crlf, out, true); if (result.isUnderflow()) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java index 11882200ee3..15f97c3d69b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.server; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.BlockingQueue; @@ -63,7 +62,7 @@ public class LocalConnector extends AbstractConnector this(server, null, null, null, -1, new HttpConnectionFactory()); } - public LocalConnector(Server server, SslContextFactory sslContextFactory) + public LocalConnector(Server server, SslContextFactory.Server sslContextFactory) { this(server, null, null, null, -1,AbstractConnectionFactory.getFactories(sslContextFactory,new HttpConnectionFactory())); } @@ -73,7 +72,7 @@ public class LocalConnector extends AbstractConnector this(server, null, null, null, -1, connectionFactory); } - public LocalConnector(Server server, ConnectionFactory connectionFactory, SslContextFactory sslContextFactory) + public LocalConnector(Server server, ConnectionFactory connectionFactory, SslContextFactory.Server sslContextFactory) { this(server, null, null, null, -1, AbstractConnectionFactory.getFactories(sslContextFactory,connectionFactory)); } @@ -113,7 +112,7 @@ public class LocalConnector extends AbstractConnector } @Override - protected void accept(int acceptorID) throws IOException, InterruptedException + protected void accept(int acceptorID) throws InterruptedException { if (LOG.isDebugEnabled()) LOG.debug("accepting {}", acceptorID); @@ -374,7 +373,13 @@ public class LocalConnector extends AbstractConnector { return 0; } - + + @Override + public boolean isHeaderCacheCaseSensitive() + { + return false; + } + @Override public void earlyEOF() { @@ -395,7 +400,7 @@ public class LocalConnector extends AbstractConnector HttpParser parser = new HttpParser(handler); parser.setHeadResponse(head); - try(ByteArrayOutputStream2 bout = new ByteArrayOutputStream2();) + try(ByteArrayOutputStream2 bout = new ByteArrayOutputStream2()) { loop: while(true) { @@ -410,7 +415,7 @@ public class LocalConnector extends AbstractConnector { parser.atEOF(); parser.parseNext(BufferUtil.EMPTY_BUFFER); - break loop; + break; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java b/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java index 375b94abb14..cd4566813cb 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/MultiParts.java @@ -37,8 +37,8 @@ import org.eclipse.jetty.server.handler.ContextHandler.Context; */ public interface MultiParts extends Closeable { - Collection getParts(); - Part getPart(String name); + Collection getParts() throws IOException; + Part getPart(String name) throws IOException; boolean isEmpty(); ContextHandler.Context getContext(); @@ -51,32 +51,18 @@ public interface MultiParts extends Closeable { _httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir); _context = request.getContext(); - _httpParser.getParts(); } @Override - public Collection getParts() + public Collection getParts() throws IOException { - try - { - return _httpParser.getParts(); - } - catch (IOException e) - { - throw new RuntimeException(e); - } + return _httpParser.getParts(); } @Override - public Part getPart(String name) { - try - { - return _httpParser.getPart(name); - } - catch (IOException e) - { - throw new RuntimeException(e); - } + public Part getPart(String name) throws IOException + { + return _httpParser.getPart(name); } @Override @@ -96,6 +82,5 @@ public interface MultiParts extends Closeable { return _context; } - } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java index d2bfae69b69..e0d95212f53 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java @@ -47,7 +47,7 @@ public class NetworkTrafficServerConnector extends ServerConnector this(server, null, null, null, 0, 0, new HttpConnectionFactory()); } - public NetworkTrafficServerConnector(Server server, ConnectionFactory connectionFactory, SslContextFactory sslContextFactory) + public NetworkTrafficServerConnector(Server server, ConnectionFactory connectionFactory, SslContextFactory.Server sslContextFactory) { super(server, sslContextFactory, connectionFactory); } @@ -62,7 +62,7 @@ public class NetworkTrafficServerConnector extends ServerConnector super(server, executor, scheduler, pool, acceptors, selectors, factories); } - public NetworkTrafficServerConnector(Server server, SslContextFactory sslContextFactory) + public NetworkTrafficServerConnector(Server server, SslContextFactory.Server sslContextFactory) { super(server, sslContextFactory); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index c09d283a64f..860891421a9 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -66,6 +66,7 @@ import javax.servlet.http.Part; import javax.servlet.http.PushBuilder; import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.ComplianceViolation; import org.eclipse.jetty.http.HostPortHttpField; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; @@ -202,6 +203,7 @@ public class Request implements HttpServletRequest private boolean _requestedSessionIdFromCookie = false; private Attributes _attributes; private Authentication _authentication; + private String _contentType; private String _characterEncoding; private ContextHandler.Context _context; private Cookies _cookies; @@ -453,8 +455,8 @@ public class Request implements HttpServletRequest int contentLength = getContentLength(); if (contentLength != 0 && _inputState == __NONE) { - contentType = HttpFields.valueParameters(contentType, null); - if (MimeTypes.Type.FORM_ENCODED.is(contentType) && + String baseType = HttpFields.valueParameters(contentType, null); + if (MimeTypes.Type.FORM_ENCODED.is(baseType) && _channel.getHttpConfiguration().isFormEncodedMethod(getMethod())) { if (_metaData!=null) @@ -465,7 +467,7 @@ public class Request implements HttpServletRequest } extractFormParameters(_contentParameters); } - else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(contentType) && + else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) && getAttribute(__MULTIPART_CONFIG_ELEMENT) != null && _multiParts == null) { @@ -475,7 +477,7 @@ public class Request implements HttpServletRequest throw new BadMessageException(HttpStatus.NOT_IMPLEMENTED_501,"Unsupported Content-Encoding"); getParts(_contentParameters); } - catch (IOException | ServletException e) + catch (IOException e) { LOG.debug(e); throw new RuntimeIOException(e); @@ -566,6 +568,22 @@ public class Request implements HttpServletRequest return _channel.getState(); } + /* ------------------------------------------------------------ */ + public ComplianceViolation.Listener getComplianceViolationListener() + { + if (_channel instanceof ComplianceViolation.Listener) + { + return (ComplianceViolation.Listener) _channel; + } + + ComplianceViolation.Listener listener = _channel.getConnector().getBean(ComplianceViolation.Listener.class); + if (listener == null) + { + listener = _channel.getServer().getBean(ComplianceViolation.Listener.class); + } + return listener; + } + /* ------------------------------------------------------------ */ /** * Get Request Attribute. @@ -653,7 +671,16 @@ public class Request implements HttpServletRequest public String getCharacterEncoding() { if (_characterEncoding==null) - getContentType(); + { + String contentType = getContentType(); + if (contentType!=null) + { + MimeTypes.Type mime = MimeTypes.CACHE.get(contentType); + String charset = (mime == null || mime.getCharset() == null) ? MimeTypes.getCharsetFromContentType(contentType) : mime.getCharset().toString(); + if (charset != null) + _characterEncoding=charset; + } + } return _characterEncoding; } @@ -709,16 +736,12 @@ public class Request implements HttpServletRequest @Override public String getContentType() { - MetaData.Request metadata = _metaData; - String content_type = metadata==null?null:metadata.getFields().get(HttpHeader.CONTENT_TYPE); - if (_characterEncoding==null && content_type!=null) + if (_contentType==null) { - MimeTypes.Type mime = MimeTypes.CACHE.get(content_type); - String charset = (mime == null || mime.getCharset() == null) ? MimeTypes.getCharsetFromContentType(content_type) : mime.getCharset().toString(); - if (charset != null) - _characterEncoding=charset; + MetaData.Request metadata = _metaData; + _contentType = metadata == null ? null : metadata.getFields().get(HttpHeader.CONTENT_TYPE); } - return content_type; + return _contentType; } /* ------------------------------------------------------------ */ @@ -763,7 +786,7 @@ public class Request implements HttpServletRequest if (field.getHeader()==HttpHeader.COOKIE) { if (_cookies==null) - _cookies = new Cookies(getHttpChannel().getHttpConfiguration().getRequestCookieCompliance()); + _cookies = new Cookies(getHttpChannel().getHttpConfiguration().getRequestCookieCompliance(), getComplianceViolationListener()); _cookies.addCookieField(field.getValue()); } } @@ -1801,6 +1824,7 @@ public class Request implements HttpServletRequest protected void recycle() { _metaData=null; + _originalUri=null; if (_context != null) throw new IllegalStateException("Request in context!"); @@ -1830,6 +1854,7 @@ public class Request implements HttpServletRequest _handled = false; if (_attributes != null) _attributes.clearAttributes(); + _contentType = null; _characterEncoding = null; _contextPath = null; if (_cookies != null) @@ -1988,10 +2013,8 @@ public class Request implements HttpServletRequest * @see javax.servlet.ServletRequest#getContentType() */ public void setContentType(String contentType) - { - MetaData.Request metadata = _metaData; - if (metadata!=null) - metadata.getFields().put(HttpHeader.CONTENT_TYPE,contentType); + { + _contentType = contentType; } /* ------------------------------------------------------------ */ @@ -2038,7 +2061,7 @@ public class Request implements HttpServletRequest public void setCookies(Cookie[] cookies) { if (_cookies == null) - _cookies = new Cookies(getHttpChannel().getHttpConfiguration().getRequestCookieCompliance()); + _cookies = new Cookies(getHttpChannel().getHttpConfiguration().getRequestCookieCompliance(), getComplianceViolationListener()); _cookies.setCookies(cookies); } @@ -2294,7 +2317,6 @@ public class Request implements HttpServletRequest public Part getPart(String name) throws IOException, ServletException { getParts(); - return _multiParts.getPart(name); } @@ -2302,13 +2324,13 @@ public class Request implements HttpServletRequest @Override public Collection getParts() throws IOException, ServletException { - if (getContentType() == null || - !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpFields.valueParameters(getContentType(),null))) + String contentType = getContentType(); + if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpFields.valueParameters(contentType,null))) throw new ServletException("Content-Type != multipart/form-data"); return getParts(null); } - private Collection getParts(MultiMap params) throws IOException, ServletException + private Collection getParts(MultiMap params) throws IOException { if (_multiParts == null) _multiParts = (MultiParts)getAttribute(__MULTIPARTS); @@ -2319,12 +2341,9 @@ public class Request implements HttpServletRequest if (config == null) throw new IllegalStateException("No multipart config for servlet"); - _multiParts = newMultiParts(getInputStream(), - getContentType(), config, - (_context != null?(File)_context.getAttribute("javax.servlet.context.tempdir"):null)); - + _multiParts = newMultiParts(config); setAttribute(__MULTIPARTS, _multiParts); - Collection parts = _multiParts.getParts(); //causes parsing + Collection parts = _multiParts.getParts(); String _charset_ = null; Part charsetPart = _multiParts.getPart("_charset_"); @@ -2386,9 +2405,9 @@ public class Request implements HttpServletRequest } - private MultiParts newMultiParts(ServletInputStream inputStream, String contentType, MultipartConfigElement config, Object object) throws IOException + private MultiParts newMultiParts(MultipartConfigElement config) throws IOException { - return new MultiParts.MultiPartsHttpParser(getInputStream(), getContentType(), config, + return new MultiParts.MultiPartsHttpParser(getInputStream(), getContentType(), config, (_context != null ? (File) _context.getAttribute("javax.servlet.context.tempdir") : null), this); } @@ -2396,11 +2415,13 @@ public class Request implements HttpServletRequest @Override public void login(String username, String password) throws ServletException { - if (_authentication instanceof Authentication.Deferred) + if (_authentication instanceof Authentication.LoginAuthentication) { - _authentication=((Authentication.Deferred)_authentication).login(username,password,this); - if (_authentication == null) + Authentication auth = ((Authentication.LoginAuthentication)_authentication).login(username,password,this); + if (auth == null) throw new Authentication.Failed("Authentication failed for username '"+username+"'"); + else + _authentication = auth; } else { @@ -2412,9 +2433,8 @@ public class Request implements HttpServletRequest @Override public void logout() throws ServletException { - if (_authentication instanceof Authentication.User) - ((Authentication.User)_authentication).logout(); - _authentication=Authentication.UNAUTHENTICATED; + if (_authentication instanceof Authentication.LogoutAuthentication) + _authentication = ((Authentication.LogoutAuthentication)_authentication).logout(this); } /* ------------------------------------------------------------ */ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java index 3d3f05c7f2a..048bd714d89 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.server; -import static java.util.Arrays.stream; -import static java.util.Collections.emptyList; -import static org.eclipse.jetty.http.HttpHeaderValue.IDENTITY; - import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -33,7 +29,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; - import javax.servlet.AsyncContext; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; @@ -47,7 +42,6 @@ import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http.QuotedCSV; import org.eclipse.jetty.http.QuotedQualityCSV; @@ -61,6 +55,10 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; +import static java.util.Arrays.stream; +import static java.util.Collections.emptyList; +import static org.eclipse.jetty.http.HttpHeaderValue.IDENTITY; + /** * Abstract resource service, used by DefaultServlet and ResourceHandler * @@ -625,7 +623,7 @@ public class ResourceService byte[] data=null; String base = URIUtil.addEncodedPaths(request.getRequestURI(),URIUtil.SLASH); - String dir = resource.getListHTML(base,pathInContext.length()>1); + String dir = resource.getListHTML(base,pathInContext.length()>1, request.getQueryString()); if (dir==null) { response.sendError(HttpServletResponse.SC_FORBIDDEN, diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 2c6f6da7f20..9e4fc626e68 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -24,14 +24,12 @@ import java.nio.channels.IllegalSelectorException; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -import java.util.stream.Collectors; import javax.servlet.RequestDispatcher; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; @@ -42,6 +40,7 @@ import org.eclipse.jetty.http.CookieCompliance; import org.eclipse.jetty.http.DateGenerator; import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpGenerator; @@ -71,12 +70,8 @@ import org.eclipse.jetty.util.log.Logger; public class Response implements HttpServletResponse { private static final Logger LOG = Log.getLogger(Response.class); - private static final String __COOKIE_DELIM="\",;\\ \t"; - private static final String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim(); private static final int __MIN_BUFFER_SIZE = 1; private static final HttpField __EXPIRES_01JAN1970 = new PreEncodedHttpField(HttpHeader.EXPIRES,DateGenerator.__01Jan1970); - // Cookie building buffer. Reduce garbage for cookie using applications - private static final ThreadLocal __cookieBuilder = ThreadLocal.withInitial(() -> new StringBuilder(128)); public enum OutputType { @@ -170,30 +165,13 @@ public class Response implements HttpServletResponse public void addCookie(HttpCookie cookie) { if (StringUtil.isBlank(cookie.getName())) - { throw new IllegalArgumentException("Cookie.name cannot be blank/null"); - } - - if (getHttpChannel().getHttpConfiguration().getResponseCookieCompliance()==CookieCompliance.RFC2965) - addSetRFC2965Cookie( - cookie.getName(), - cookie.getValue(), - cookie.getDomain(), - cookie.getPath(), - cookie.getMaxAge(), - cookie.getComment(), - cookie.isSecure(), - cookie.isHttpOnly(), - cookie.getVersion()); - else - addSetRFC6265Cookie( - cookie.getName(), - cookie.getValue(), - cookie.getDomain(), - cookie.getPath(), - cookie.getMaxAge(), - cookie.isSecure(), - cookie.isHttpOnly()); + + // add the set cookie + _fields.add(new SetCookieHttpField(cookie, getHttpChannel().getHttpConfiguration().getResponseCookieCompliance())); + + // Expire responses with set-cookie headers so they do not get cached. + _fields.put(__EXPIRES_01JAN1970); } /** @@ -209,73 +187,34 @@ public class Response implements HttpServletResponse if (field.getHeader() == HttpHeader.SET_COOKIE) { - String old_set_cookie = field.getValue(); - String name = cookie.getName(); - if (!old_set_cookie.startsWith(name) || old_set_cookie.length()<= name.length() || old_set_cookie.charAt(name.length())!='=') - continue; + CookieCompliance compliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance(); - String domain = cookie.getDomain(); - if (domain!=null) - { - if (getHttpChannel().getHttpConfiguration().getResponseCookieCompliance()==CookieCompliance.RFC2965) - { - StringBuilder buf = new StringBuilder(); - buf.append(";Domain="); - quoteOnlyOrAppend(buf,domain,isQuoteNeededForCookie(domain)); - domain = buf.toString(); - } - else - { - domain = ";Domain="+domain; - } - if (!old_set_cookie.contains(domain)) - continue; - } - else if (old_set_cookie.contains(";Domain=")) - continue; - - String path = cookie.getPath(); - if (path!=null) - { - if (getHttpChannel().getHttpConfiguration().getResponseCookieCompliance()==CookieCompliance.RFC2965) - { - StringBuilder buf = new StringBuilder(); - buf.append(";Path="); - quoteOnlyOrAppend(buf,path,isQuoteNeededForCookie(path)); - path = buf.toString(); - } - else - { - path = ";Path="+path; - } - if (!old_set_cookie.contains(path)) - continue; - } - else if (old_set_cookie.contains(";Path=")) - continue; - - if (getHttpChannel().getHttpConfiguration().getResponseCookieCompliance() == CookieCompliance.RFC2965) - i.set(new HttpField(HttpHeader.CONTENT_ENCODING.SET_COOKIE, newRFC2965SetCookie( - cookie.getName(), - cookie.getValue(), - cookie.getDomain(), - cookie.getPath(), - cookie.getMaxAge(), - cookie.getComment(), - cookie.isSecure(), - cookie.isHttpOnly(), - cookie.getVersion()) - )); + HttpCookie oldCookie; + if (field instanceof SetCookieHttpField) + oldCookie = ((SetCookieHttpField)field).getHttpCookie(); else - i.set(new HttpField(HttpHeader.CONTENT_ENCODING.SET_COOKIE, newRFC6265SetCookie( - cookie.getName(), - cookie.getValue(), - cookie.getDomain(), - cookie.getPath(), - cookie.getMaxAge(), - cookie.isSecure(), - cookie.isHttpOnly() - ))); + oldCookie = new HttpCookie(field.getValue()); + + if (!cookie.getName().equals(oldCookie.getName())) + continue; + + if (cookie.getDomain()==null) + { + if (oldCookie.getDomain() != null) + continue; + } + else if (!cookie.getDomain().equalsIgnoreCase(oldCookie.getDomain())) + continue; + + if (cookie.getPath()==null) + { + if (oldCookie.getPath() != null) + continue; + } + else if (!cookie.getPath().equals(oldCookie.getPath())) + continue; + + i.set(new SetCookieHttpField(cookie, compliance)); return; } } @@ -287,8 +226,11 @@ public class Response implements HttpServletResponse @Override public void addCookie(Cookie cookie) { + if (StringUtil.isBlank(cookie.getName())) + throw new IllegalArgumentException("Cookie.name cannot be blank/null"); + String comment = cookie.getComment(); - boolean httpOnly = false; + boolean httpOnly = cookie.isHttpOnly(); if (comment != null) { @@ -301,264 +243,19 @@ public class Response implements HttpServletResponse comment = null; } } - - if (StringUtil.isBlank(cookie.getName())) - { - throw new IllegalArgumentException("Cookie.name cannot be blank/null"); - } - if (getHttpChannel().getHttpConfiguration().getResponseCookieCompliance()==CookieCompliance.RFC2965) - addSetRFC2965Cookie(cookie.getName(), - cookie.getValue(), - cookie.getDomain(), - cookie.getPath(), - cookie.getMaxAge(), - comment, - cookie.getSecure(), - httpOnly || cookie.isHttpOnly(), - cookie.getVersion()); - else - addSetRFC6265Cookie(cookie.getName(), - cookie.getValue(), - cookie.getDomain(), - cookie.getPath(), - cookie.getMaxAge(), - cookie.getSecure(), - httpOnly || cookie.isHttpOnly()); + addCookie(new HttpCookie( + cookie.getName(), + cookie.getValue(), + cookie.getDomain(), + cookie.getPath(), + (long) cookie.getMaxAge(), + httpOnly, + cookie.getSecure(), + comment, + cookie.getVersion())); } - - /** - * Format a set cookie value by RFC6265 - * - * @param name the name - * @param value the value - * @param domain the domain - * @param path the path - * @param maxAge the maximum age - * @param isSecure true if secure cookie - * @param isHttpOnly true if for http only - */ - public void addSetRFC6265Cookie( - final String name, - final String value, - final String domain, - final String path, - final long maxAge, - final boolean isSecure, - final boolean isHttpOnly) - { - String set_cookie = newRFC6265SetCookie(name, value, domain, path, maxAge, isSecure, isHttpOnly); - - // add the set cookie - _fields.add(HttpHeader.SET_COOKIE, set_cookie); - - // Expire responses with set-cookie headers so they do not get cached. - _fields.put(__EXPIRES_01JAN1970); - - } - - private String newRFC6265SetCookie(String name, String value, String domain, String path, long maxAge, boolean isSecure, boolean isHttpOnly) - { - // Check arguments - if (name == null || name.length() == 0) - throw new IllegalArgumentException("Bad cookie name"); - - // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting - // Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules - Syntax.requireValidRFC2616Token(name, "RFC6265 Cookie name"); - // Ensure that Per RFC6265, Cookie.value follows syntax rules - Syntax.requireValidRFC6265CookieValue(value); - - // Format value and params - StringBuilder buf = __cookieBuilder.get(); - buf.setLength(0); - buf.append(name).append('=').append(value==null?"":value); - - // Append path - if (path!=null && path.length()>0) - buf.append(";Path=").append(path); - - // Append domain - if (domain!=null && domain.length()>0) - buf.append(";Domain=").append(domain); - - // Handle max-age and/or expires - if (maxAge >= 0) - { - // Always use expires - // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies - buf.append(";Expires="); - if (maxAge == 0) - buf.append(__01Jan1970_COOKIE); - else - DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge); - - buf.append(";Max-Age="); - buf.append(maxAge); - } - - // add the other fields - if (isSecure) - buf.append(";Secure"); - if (isHttpOnly) - buf.append(";HttpOnly"); - return buf.toString(); - } - - /** - * Format a set cookie value - * - * @param name the name - * @param value the value - * @param domain the domain - * @param path the path - * @param maxAge the maximum age - * @param comment the comment (only present on versions > 0) - * @param isSecure true if secure cookie - * @param isHttpOnly true if for http only - * @param version version of cookie logic to use (0 == default behavior) - */ - public void addSetRFC2965Cookie( - final String name, - final String value, - final String domain, - final String path, - final long maxAge, - final String comment, - final boolean isSecure, - final boolean isHttpOnly, - int version) - { - String set_cookie = newRFC2965SetCookie(name, value, domain, path, maxAge, comment, isSecure, isHttpOnly, version); - - // add the set cookie - _fields.add(HttpHeader.SET_COOKIE, set_cookie); - - // Expire responses with set-cookie headers so they do not get cached. - _fields.put(__EXPIRES_01JAN1970); - } - - private String newRFC2965SetCookie(String name, String value, String domain, String path, long maxAge, String comment, boolean isSecure, boolean isHttpOnly, int version) - { - // Check arguments - if (name == null || name.length() == 0) - throw new IllegalArgumentException("Bad cookie name"); - - // Format value and params - StringBuilder buf = __cookieBuilder.get(); - buf.setLength(0); - - // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting - boolean quote_name=isQuoteNeededForCookie(name); - quoteOnlyOrAppend(buf,name,quote_name); - - buf.append('='); - - // Append the value - boolean quote_value=isQuoteNeededForCookie(value); - quoteOnlyOrAppend(buf,value,quote_value); - - // Look for domain and path fields and check if they need to be quoted - boolean has_domain = domain!=null && domain.length()>0; - boolean quote_domain = has_domain && isQuoteNeededForCookie(domain); - boolean has_path = path!=null && path.length()>0; - boolean quote_path = has_path && isQuoteNeededForCookie(path); - - // Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted - if (version==0 && ( comment!=null || quote_name || quote_value || quote_domain || quote_path || - QuotedStringTokenizer.isQuoted(name) || QuotedStringTokenizer.isQuoted(value) || - QuotedStringTokenizer.isQuoted(path) || QuotedStringTokenizer.isQuoted(domain))) - version=1; - - // Append version - if (version==1) - buf.append (";Version=1"); - else if (version>1) - buf.append (";Version=").append(version); - - // Append path - if (has_path) - { - buf.append(";Path="); - quoteOnlyOrAppend(buf,path,quote_path); - } - - // Append domain - if (has_domain) - { - buf.append(";Domain="); - quoteOnlyOrAppend(buf,domain,quote_domain); - } - - // Handle max-age and/or expires - if (maxAge >= 0) - { - // Always use expires - // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies - buf.append(";Expires="); - if (maxAge == 0) - buf.append(__01Jan1970_COOKIE); - else - DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge); - - // for v1 cookies, also send max-age - if (version>=1) - { - buf.append(";Max-Age="); - buf.append(maxAge); - } - } - - // add the other fields - if (isSecure) - buf.append(";Secure"); - if (isHttpOnly) - buf.append(";HttpOnly"); - if (comment != null) - { - buf.append(";Comment="); - quoteOnlyOrAppend(buf,comment,isQuoteNeededForCookie(comment)); - } - return buf.toString(); - } - - - /* ------------------------------------------------------------ */ - /** Does a cookie value need to be quoted? - * @param s value string - * @return true if quoted; - * @throws IllegalArgumentException If there a control characters in the string - */ - private static boolean isQuoteNeededForCookie(String s) - { - if (s==null || s.length()==0) - return true; - - if (QuotedStringTokenizer.isQuoted(s)) - return false; - - for (int i=0;i=0) - return true; - - if (c<0x20 || c>=0x7f) - throw new IllegalArgumentException("Illegal character in cookie value"); - } - - return false; - } - - - private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote) - { - if (quote) - QuotedStringTokenizer.quoteOnly(buf,s); - else - buf.append(s); - } @Override public boolean containsHeader(String name) @@ -1344,37 +1041,27 @@ public class Response implements HttpServletResponse _reason = null; _contentLength = -1; - List cookies = preserveCookies - ?_fields.stream() - .filter(f->f.getHeader()==HttpHeader.SET_COOKIE) - .collect(Collectors.toList()):null; - + List cookies = preserveCookies ?_fields.getFields(HttpHeader.SET_COOKIE):null; _fields.clear(); - String connection = _channel.getRequest().getHeader(HttpHeader.CONNECTION.asString()); - if (connection != null) + for (String value: _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION,false)) { - for (String value: StringUtil.csvSplit(null,connection,0,connection.length())) + HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value); + if (cb != null) { - HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value); - - if (cb != null) + switch (cb) { - switch (cb) - { - case CLOSE: - _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.toString()); - break; - - case KEEP_ALIVE: - if (HttpVersion.HTTP_1_0.is(_channel.getRequest().getProtocol())) - _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.toString()); - break; - case TE: - _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.TE.toString()); - break; - default: - } + case CLOSE: + _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.toString()); + break; + case KEEP_ALIVE: + if (HttpVersion.HTTP_1_0.is(_channel.getRequest().getProtocol())) + _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.toString()); + break; + case TE: + _fields.put(HttpHeader.CONNECTION, HttpHeaderValue.TE.toString()); + break; + default: } } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java index af4e8a7dafe..b88061272f9 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.server; import java.security.cert.X509Certificate; import java.util.concurrent.TimeUnit; - import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLSession; @@ -30,8 +29,8 @@ import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; -import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.http.PreEncodedHttpField; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.io.ssl.SslConnection.DecryptedEndPoint; import org.eclipse.jetty.util.TypeUtil; @@ -72,7 +71,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer { this(sniHostCheck,-1,false); } - + /** * @param sniHostCheck True if the SNI Host name must match. * @param stsMaxAgeSeconds The max age in seconds for a Strict-Transport-Security response header. If set less than zero then no header is sent. @@ -98,7 +97,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer } /** - * @param sniHostCheck True if the SNI Host name must match. + * @param sniHostCheck True if the SNI Host name must match. */ public void setSniHostCheck(boolean sniHostCheck) { @@ -176,7 +175,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer { ProxyConnectionFactory.ProxyEndPoint proxy = (ProxyConnectionFactory.ProxyEndPoint)endp; if (request.getHttpURI().getScheme()==null && proxy.getAttribute(ProxyConnectionFactory.TLS_VERSION)!=null) - request.setScheme(HttpScheme.HTTPS.asString()); + request.setScheme(HttpScheme.HTTPS.asString()); } if (HttpScheme.HTTPS.is(request.getScheme())) @@ -186,14 +185,14 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer /** * Customizes the request attributes for general secure settings. * The default impl calls {@link Request#setSecure(boolean)} with true - * and sets a response header if the Strict-Transport-Security options + * and sets a response header if the Strict-Transport-Security options * are set. * @param request the request being customized */ protected void customizeSecure(Request request) { request.setSecure(true); - + if (_stsField!=null) request.getResponse().getHttpFields().add(_stsField); } @@ -214,7 +213,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer * trust. The first certificate in the chain is the one set by the client, the next is the one used to authenticate * the first, and so on. * - * + * * @param sslEngine * the sslEngine to be customized. * @param request @@ -256,7 +255,7 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer else { keySize=SslContextFactory.deduceKeyLength(cipherSuite); - certs=SslContextFactory.getCertChain(sslSession); + certs = getCertChain(request, sslSession); byte[] bytes = sslSession.getId(); idStr = TypeUtil.toHexString(bytes); cachedInfo=new CachedInfo(keySize,certs,idStr); @@ -278,7 +277,25 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer LOG.warn(Log.EXCEPTION,e); } } - + + private X509Certificate[] getCertChain(Request request, SSLSession sslSession) + { + // The in-use SslContextFactory should be present in the Connector's SslConnectionFactory + Connector connector = request.getHttpChannel().getConnector(); + SslConnectionFactory sslConnectionFactory = connector.getConnectionFactory(SslConnectionFactory.class); + if (sslConnectionFactory != null) + { + SslContextFactory sslContextFactory = sslConnectionFactory.getSslContextFactory(); + if (sslConnectionFactory != null) + { + return sslContextFactory.getX509CertChain(sslSession); + } + } + + // Fallback, either no SslConnectionFactory or no SslContextFactory instance found + return SslContextFactory.getCertChain(sslSession); + } + public void setSslSessionAttribute(String attribute) { this.sslSessionAttribute = attribute; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java index a92ba964e81..e34a3d18b39 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -369,14 +369,11 @@ public class Server extends HandlerWrapper implements Attributes String gitHash = Jetty.GIT_HASH; String timestamp = Jetty.BUILD_TIMESTAMP; - LOG.info("jetty-{}; built: {}; git: {}; jvm {}", getVersion(), timestamp, gitHash, System.getProperty("java.runtime.version",System.getProperty("java.version"))); - if (!Jetty.STABLE) - { - LOG.warn("THIS IS NOT A STABLE RELEASE! DO NOT USE IN PRODUCTION!"); - LOG.warn("Download a stable release from http://download.eclipse.org/jetty/"); - } - - HttpGenerator.setJettyVersion(HttpConfiguration.SERVER_VERSION); + LOG.info("jetty-{}; built: {}; git: {}; jvm {}", getVersion(), timestamp, gitHash, System.getProperty("java.runtime.version",System.getProperty("java.version"))); + if (!Jetty.STABLE) + LOG.warn("THIS IS NOT A STABLE RELEASE! DO NOT USE IN PRODUCTION!"); + + HttpGenerator.setJettyVersion(HttpConfiguration.SERVER_VERSION); MultiException mex=new MultiException(); @@ -392,7 +389,7 @@ public class Server extends HandlerWrapper implements Attributes mex.add(th); } }); - + // Throw now if verified start sequence and there was an open exception mex.ifExceptionThrow(); @@ -410,29 +407,30 @@ public class Server extends HandlerWrapper implements Attributes catch(Throwable e) { mex.add(e); + // stop any started connectors + _connectors.stream().filter(LifeCycle::isRunning).map(Object.class::cast).forEach(LifeCycle::stop); } } mex.ifExceptionThrow(); LOG.info(String.format("Started @%dms",Uptime.getUptime())); } - catch(Throwable e1) + catch(Throwable th) { - try + // Close any connectors that were opened + _connectors.stream().filter(NetworkConnector.class::isInstance).map(NetworkConnector.class::cast).forEach(nc-> { - // Stop any components already started! - super.doStop(); - } - catch(Exception e2) - { - e1.addSuppressed(e2); - } - finally - { - // Close any connectors that were opened - _connectors.stream().filter(NetworkConnector.class::isInstance).map(NetworkConnector.class::cast).forEach(NetworkConnector::close); - } - throw e1; + try + { + nc.close(); + } + catch(Throwable t2) + { + if (th!=t2) + th.addSuppressed(t2); + } + }); + throw th; } finally { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java index 516a112e435..c2397574f1d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java @@ -150,7 +150,7 @@ public class ServerConnector extends AbstractNetworkConnector */ public ServerConnector( @Name("server") Server server, - @Name("sslContextFactory") SslContextFactory sslContextFactory) + @Name("sslContextFactory") SslContextFactory.Server sslContextFactory) { this(server,null,null,null,-1,-1,AbstractConnectionFactory.getFactories(sslContextFactory,new HttpConnectionFactory())); } @@ -170,7 +170,7 @@ public class ServerConnector extends AbstractNetworkConnector @Name("server") Server server, @Name("acceptors") int acceptors, @Name("selectors") int selectors, - @Name("sslContextFactory") SslContextFactory sslContextFactory) + @Name("sslContextFactory") SslContextFactory.Server sslContextFactory) { this(server,null,null,null,acceptors,selectors,AbstractConnectionFactory.getFactories(sslContextFactory,new HttpConnectionFactory())); } @@ -183,7 +183,7 @@ public class ServerConnector extends AbstractNetworkConnector */ public ServerConnector( @Name("server") Server server, - @Name("sslContextFactory") SslContextFactory sslContextFactory, + @Name("sslContextFactory") SslContextFactory.Server sslContextFactory, @Name("factories") ConnectionFactory... factories) { this(server, null, null, null, -1, -1, AbstractConnectionFactory.getFactories(sslContextFactory, factories)); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java index 98470a9d617..bf91fcea3f6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SslConnectionFactory.java @@ -35,7 +35,7 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; public class SslConnectionFactory extends AbstractConnectionFactory { - private final SslContextFactory _sslContextFactory; + private final SslContextFactory.Server _sslContextFactory; private final String _nextProtocol; private boolean _directBuffersForEncryption = false; private boolean _directBuffersForDecryption = false; @@ -50,15 +50,15 @@ public class SslConnectionFactory extends AbstractConnectionFactory this(null,nextProtocol); } - public SslConnectionFactory(@Name("sslContextFactory") SslContextFactory factory, @Name("next") String nextProtocol) + public SslConnectionFactory(@Name("sslContextFactory") SslContextFactory.Server factory, @Name("next") String nextProtocol) { super("SSL"); - _sslContextFactory=factory==null?new SslContextFactory():factory; - _nextProtocol=nextProtocol; + _sslContextFactory = factory == null ? new SslContextFactory.Server() : factory; + _nextProtocol = nextProtocol; addBean(_sslContextFactory); } - public SslContextFactory getSslContextFactory() + public SslContextFactory.Server getSslContextFactory() { return _sslContextFactory; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 6d0b30a09dd..55694960603 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -303,6 +303,8 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu /* ------------------------------------------------------------ */ public void setUsingSecurityManager(boolean usingSecurityManager) { + if (usingSecurityManager && System.getSecurityManager() == null) + throw new IllegalStateException("No security manager"); _usingSecurityManager = usingSecurityManager; } @@ -1143,7 +1145,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu case UNAVAILABLE: baseRequest.setHandled(true); response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - return false; + return true; default: if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled())) return false; @@ -1618,9 +1620,13 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (getServer() != null && (getServer().isStarting() || getServer().isStarted())) { - Handler[] contextCollections = getServer().getChildHandlersByClass(ContextHandlerCollection.class); - for (int h = 0; contextCollections != null && h < contextCollections.length; h++) - ((ContextHandlerCollection)contextCollections[h]).mapContexts(); + Class handlerClass = ContextHandlerCollection.class; + Handler[] contextCollections = getServer().getChildHandlersByClass(handlerClass); + if (contextCollections != null) + { + for (Handler contextCollection : contextCollections) + handlerClass.cast(contextCollection).mapContexts(); + } } } @@ -2621,7 +2627,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu else callerLoader = callerLoader.getParent(); } - AccessController.checkPermission(new RuntimePermission("getClassLoader")); + System.getSecurityManager().checkPermission(new RuntimePermission("getClassLoader")); return _classLoader; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java index 8c6fc7c57bf..d082ca7a89e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java @@ -24,8 +24,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -36,15 +35,16 @@ import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.ArrayTernaryTrie; import org.eclipse.jetty.util.ArrayUtil; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Trie; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedOperation; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.SerializedExecutor; /* ------------------------------------------------------------ */ -/** ContextHandlerCollection. - * +/** * This {@link org.eclipse.jetty.server.handler.HandlerCollection} is creates a * Map of contexts to it's contained handlers based * on the context path and virtual hosts of any contained {@link org.eclipse.jetty.server.handler.ContextHandler}s. @@ -57,9 +57,9 @@ import org.eclipse.jetty.util.log.Logger; public class ContextHandlerCollection extends HandlerCollection { private static final Logger LOG = Log.getLogger(ContextHandlerCollection.class); + private final SerializedExecutor _serializedExecutor = new SerializedExecutor(); - private final ConcurrentMap _contextBranches = new ConcurrentHashMap<>(); - private volatile Trie> _pathBranches; + @Deprecated private Class _contextClass = ContextHandler.class; /* ------------------------------------------------------------ */ @@ -71,43 +71,57 @@ public class ContextHandlerCollection extends HandlerCollection /* ------------------------------------------------------------ */ public ContextHandlerCollection(ContextHandler... contexts) { - super(true,contexts); + super(true); + setHandlers(contexts); } - /* ------------------------------------------------------------ */ /** - * Remap the context paths. + * Remap the contexts. Normally this is not required as context + * mapping is maintained as a side effect of {@link #setHandlers(Handler[])} + * However, if configuration changes in the deep handler structure (eg contextpath is changed), then + * this call will trigger a remapping. + * This method is mutually excluded from {@link #deployHandler(Handler, Callback)} and + * {@link #undeployHandler(Handler, Callback)} */ - @ManagedOperation("update the mapping of context path to context") + @ManagedOperation("Update the mapping of context path to context") public void mapContexts() { - _contextBranches.clear(); - - Handler[] handlers = getHandlers(); - if (handlers==null) + _serializedExecutor.execute(()-> { - _pathBranches=new ArrayTernaryTrie<>(false,16); - return; - } - + while(true) + { + Handlers handlers = _handlers.get(); + if (handlers==null) + break; + if (updateHandlers(handlers, newHandlers(handlers.getHandlers()))) + break; + } + }); + } + + /* ------------------------------------------------------------ */ + @Override + protected Handlers newHandlers(Handler[] handlers) + { + if (handlers==null || handlers.length==0) + return null; + // Create map of contextPath to handler Branch - Map map = new HashMap<>(); + // A branch is a Handler that could contain 0 or more ContextHandlers + Map path2Branches = new HashMap<>(); for (Handler handler:handlers) { Branch branch=new Branch(handler); for (String contextPath : branch.getContextPaths()) { - Branch[] branches=map.get(contextPath); - map.put(contextPath, ArrayUtil.addToArray(branches, branch, Branch.class)); + Branch[] branches=path2Branches.get(contextPath); + path2Branches.put(contextPath, ArrayUtil.addToArray(branches, branch, Branch.class)); } - - for (ContextHandler context : branch.getContextHandlers()) - _contextBranches.putIfAbsent(context, branch.getHandler()); } - // Sort the branches so those with virtual hosts are considered before those without - for (Map.Entry entry: map.entrySet()) + // Sort the branches for each contextPath so those with virtual hosts are considered before those without + for (Map.Entry entry: path2Branches.entrySet()) { Branch[] branches=entry.getValue(); Branch[] sorted=new Branch[branches.length]; @@ -123,69 +137,56 @@ public class ContextHandlerCollection extends HandlerCollection // Loop until we have a big enough trie to hold all the context paths int capacity=512; - Trie> trie; + Mapping mapping; loop: while(true) { - trie=new ArrayTernaryTrie<>(false,capacity); - for (Map.Entry entry: map.entrySet()) + mapping = new Mapping(handlers, capacity); + for (Map.Entry entry: path2Branches.entrySet()) { - if (!trie.put(entry.getKey().substring(1),entry)) + if (!mapping._pathBranches.put(entry.getKey().substring(1),entry)) { capacity+=512; continue loop; } } - break loop; + break; } - if (LOG.isDebugEnabled()) { - for (String ctx : trie.keySet()) - LOG.debug("{}->{}",ctx,Arrays.asList(trie.get(ctx).getValue())); + for (String ctx : mapping._pathBranches.keySet()) + LOG.debug("{}->{}",ctx,Arrays.asList(mapping._pathBranches.get(ctx).getValue())); } - _pathBranches=trie; + + // add new context branches to concurrent map + for (Branch[] branches: path2Branches.values()) + { + for (Branch branch : branches) + { + for (ContextHandler context : branch.getContextHandlers()) + mapping._contextBranches.put(context, branch.getHandler()); + } + } + + return mapping; } /* ------------------------------------------------------------ */ - /* - * @see org.eclipse.jetty.server.server.handler.HandlerCollection#setHandlers(org.eclipse.jetty.server.server.Handler[]) - */ - @Override - public void setHandlers(Handler[] handlers) - { - super.setHandlers(handlers); - if (isStarted()) - mapContexts(); - } - - /* ------------------------------------------------------------ */ - @Override - protected void doStart() throws Exception - { - mapContexts(); - super.doStart(); - } - - - /* ------------------------------------------------------------ */ - /* - * @see org.eclipse.jetty.server.server.Handler#handle(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) - */ @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - Handler[] handlers = getHandlers(); - if (handlers==null || handlers.length==0) + Handlers handlers = _handlers.get(); + if (handlers==null) return; + Mapping mapping = (Mapping)handlers; HttpChannelState async = baseRequest.getHttpChannelState(); if (async.isAsync()) { ContextHandler context=async.getContextHandler(); if (context!=null) { - Handler branch = _contextBranches.get(context); + Handler branch = mapping._contextBranches.get(context); if (branch==null) context.handle(target,baseRequest,request, response); @@ -195,19 +196,19 @@ public class ContextHandlerCollection extends HandlerCollection } } - // data structure which maps a request to a context; first-best match wins - // { context path => [ context ] } - // } if (target.startsWith("/")) { + Trie> pathBranches = mapping._pathBranches; + if (pathBranches==null) + return; + int limit = target.length()-1; while (limit>=0) { // Get best match - Map.Entry branches = _pathBranches.getBest(target,1,limit); - - + Map.Entry branches = pathBranches.getBest(target,1,limit); + if (branches==null) break; @@ -227,10 +228,11 @@ public class ContextHandlerCollection extends HandlerCollection } else { - // This may not work in all circumstances... but then I think it should never be called - for (int i=0;i + * This method is the equivalent of {@link #addHandler(Handler)}, + * but its execution is non-block and mutually excluded from all + * other calls to {@link #deployHandler(Handler, Callback)} and + * {@link #undeployHandler(Handler, Callback)}. + * The handler may be added after this call returns. + *

+ * @param handler the handler to deploy + * @param callback Called after handler has been added + */ + public void deployHandler(Handler handler, Callback callback) + { + if (handler.getServer()!=getServer()) + handler.setServer(getServer()); + _serializedExecutor.execute(new SerializedExecutor.ErrorHandlingTask() + { + @Override + public void run() + { + addHandler(handler); + callback.succeeded(); + } + + @Override + public void accept(Throwable throwable) + { + callback.failed(throwable); + } + }); + } + + /* ------------------------------------------------------------ */ + /** + * Thread safe undeploy of a Handler. + *

+ * This method is the equivalent of {@link #removeHandler(Handler)}, + * but its execution is non-block and mutually excluded from all + * other calls to {@link #deployHandler(Handler,Callback)} and + * {@link #undeployHandler(Handler,Callback)}. + * The handler may be removed after this call returns. + *

+ * @param handler The handler to undeploy + * @param callback Called after handler has been removed + */ + public void undeployHandler(Handler handler, Callback callback) + { + _serializedExecutor.execute(new SerializedExecutor.ErrorHandlingTask() + { + @Override + public void run() + { + removeHandler(handler); + callback.succeeded(); + } + + @Override + public void accept(Throwable throwable) + { + callback.failed(throwable); + } + }); + } /* ------------------------------------------------------------ */ /** * @return The class to use to add new Contexts + * @deprecated Unused convenience mechanism not used. */ + @Deprecated public Class getContextClass() { return _contextClass; } - /* ------------------------------------------------------------ */ /** * @param contextClass The class to use to add new Contexts + * @deprecated Unused convenience mechanism not used. */ + @Deprecated public void setContextClass(Class contextClass) { if (contextClass ==null || !(ContextHandler.class.isAssignableFrom(contextClass))) @@ -342,5 +416,18 @@ public class ContextHandlerCollection extends HandlerCollection } } + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private static class Mapping extends Handlers + { + private final Map _contextBranches = new HashMap<>(); + private final Trie> _pathBranches; + private Mapping(Handler[] handlers, int capacity) + { + super(handlers); + _pathBranches = new ArrayTernaryTrie<>(false, capacity); + } + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java index 15b18a76e44..60fcb0927e1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java @@ -18,10 +18,11 @@ package org.eclipse.jetty.server.handler; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.net.URL; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -32,12 +33,15 @@ 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.util.ByteArrayISO8859Writer; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; +import static java.nio.charset.StandardCharsets.UTF_8; + /* ------------------------------------------------------------ */ /** Default Handler. @@ -118,64 +122,87 @@ public class DefaultHandler extends AbstractHandler } response.setStatus(HttpServletResponse.SC_NOT_FOUND); - response.setContentType(MimeTypes.Type.TEXT_HTML_8859_1.asString()); + response.setContentType(MimeTypes.Type.TEXT_HTML_UTF_8.toString()); - try (ByteArrayISO8859Writer writer = new ByteArrayISO8859Writer(1500);) + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(outputStream, UTF_8)) { - writer.write("\n\nError 404 - Not Found"); - writer.write("\n\n

Error 404 - Not Found.

\n"); - writer.write("No context on this server matched or handled this request.
"); - writer.write("Contexts known to this server are:
    "); + writer.append("\n"); + writer.append("\n\n"); + writer.append("Error 404 - Not Found\n"); + writer.append("\n"); + writer.append("\n"); + writer.append("\n\n"); + writer.append("

    Error 404 - Not Found.

    \n"); + writer.append("

    No context on this server matched or handled this request.

    \n"); + writer.append("

    Contexts known to this server are:

    \n"); Server server = getServer(); Handler[] handlers = server==null?null:server.getChildHandlersByClass(ContextHandler.class); + writer.append(""); + writer.append(""); + writer.append(""); + writer.append(""); + writer.append(""); + writer.append("\n"); + for (int i=0;handlers!=null && i\n"); } - writer.write("
    "); - - baseRequest.getHttpChannel().getHttpConfiguration() - .writePoweredBy(writer," ","
    \n"); - - writer.write("\n\n\n"); + writer.append("
    Context PathDisplay NameStatusLifeCycle
    "); + // Context Path ContextHandler context = (ContextHandler)handlers[i]; + + String contextPath = context.getContextPath(); + String href = URIUtil.encodePath(contextPath); + if (contextPath.length() > 1 && !contextPath.endsWith("/")) + { + href += '/'; + } + if (context.isRunning()) { - writer.write("
  • 0) - writer.write(request.getScheme()+"://"+context.getVirtualHosts()[0]+":"+request.getLocalPort()); - writer.write(context.getContextPath()); - if (context.getContextPath().length()>1 && context.getContextPath().endsWith("/")) - writer.write("/"); - writer.write("\">"); - writer.write(context.getContextPath()); - if (context.getVirtualHosts()!=null && context.getVirtualHosts().length>0) - writer.write(" @ "+context.getVirtualHosts()[0]+":"+request.getLocalPort()); - writer.write(" ---> "); - writer.write(context.toString()); - writer.write("
  • \n"); + writer.append(""); + } + writer.append(contextPath.replaceAll("%", "%")); + if (context.isRunning()) + { + writer.append(""); + } + writer.append("
    "); + // Display Name + + if (StringUtil.isNotBlank(context.getDisplayName())) + { + writer.append(StringUtil.sanitizeXmlString(context.getDisplayName())); + } + writer.append(" "); + // Available + + if (context.isAvailable()) + { + writer.append("Available"); } else { - writer.write("
  • "); - writer.write(context.getContextPath()); - if (context.getVirtualHosts()!=null && context.getVirtualHosts().length>0) - writer.write(" @ "+context.getVirtualHosts()[0]+":"+request.getLocalPort()); - writer.write(" ---> "); - writer.write(context.toString()); - if (context.isFailed()) - writer.write(" [failed]"); - if (context.isStopped()) - writer.write(" [stopped]"); - writer.write("
  • \n"); + writer.append("Not Available"); } + writer.append("
    "); + // State + writer.append(context.getState()); + writer.append("

    \n"); + writer.append("\"icon\" "); + writer.append("Powered by Eclipse Jetty:// Server
    \n"); + writer.append("\n\n"); writer.flush(); - response.setContentLength(writer.size()); - try (OutputStream out=response.getOutputStream()) + byte content[] = outputStream.toByteArray(); + response.setContentLength(content.length); + try (OutputStream out = response.getOutputStream()) { - writer.writeTo(out); + out.write(content); } } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java index f7697255b06..e39077489ab 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.server.handler; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -47,7 +48,7 @@ import org.eclipse.jetty.util.annotation.ManagedObject; public class HandlerCollection extends AbstractHandlerContainer { private final boolean _mutableWhenRunning; - private volatile Handler[] _handlers; + protected final AtomicReference _handlers = new AtomicReference<>(); /* ------------------------------------------------------------ */ public HandlerCollection() @@ -71,72 +72,93 @@ public class HandlerCollection extends AbstractHandlerContainer /* ------------------------------------------------------------ */ /** - * @return Returns the handlers. + * @return the array of handlers. */ @Override @ManagedAttribute(value="Wrapped handlers", readonly=true) public Handler[] getHandlers() { - return _handlers; + Handlers handlers = _handlers.get(); + return handlers==null ? null : handlers._handlers; } /* ------------------------------------------------------------ */ /** - * @param handlers The handlers to set. + * @param handlers the array of handlers to set. */ public void setHandlers(Handler[] handlers) { if (!_mutableWhenRunning && isStarted()) throw new IllegalStateException(STARTED); - if (handlers!=null) + while(true) { - // check for loops - for (Handler handler:handlers) - if (handler == this || (handler instanceof HandlerContainer && - Arrays.asList(((HandlerContainer)handler).getChildHandlers()).contains(this))) - throw new IllegalStateException("setHandler loop"); - - // Set server - for (Handler handler:handlers) - if (handler.getServer()!=getServer()) - handler.setServer(getServer()); + if (updateHandlers(_handlers.get(), newHandlers(handlers))) + break; } - Handler[] old=_handlers;; - _handlers = handlers; - updateBeans(old, handlers); } /* ------------------------------------------------------------ */ - /** - * @see Handler#handle(String, Request, HttpServletRequest, HttpServletResponse) - */ + protected Handlers newHandlers(Handler[] handlers) + { + if (handlers==null || handlers.length==0) + return null; + return new Handlers(handlers); + } + + /* ------------------------------------------------------------ */ + protected boolean updateHandlers(Handlers old, Handlers handlers) + { + if (handlers!=null) + { + // check for loops + for (Handler handler:handlers._handlers) + if (handler == this || (handler instanceof HandlerContainer && + Arrays.asList(((HandlerContainer)handler).getChildHandlers()).contains(this))) + throw new IllegalStateException("setHandler loop"); + + // Set server + for (Handler handler:handlers._handlers) + if (handler.getServer()!=getServer()) + handler.setServer(getServer()); + } + + if (_handlers.compareAndSet(old, handlers)) + { + Handler[] oldBeans = old == null ? null : old._handlers; + Handler[] newBeans = handlers == null ? null : handlers._handlers; + updateBeans(oldBeans, newBeans); + return true; + } + return false; + } + + /* ------------------------------------------------------------ */ @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - if (_handlers!=null && isStarted()) + if (isStarted()) { - MultiException mex=null; + Handlers handlers = _handlers.get(); + if (handlers==null) + return; - for (int i=0;i<_handlers.length;i++) + MultiException mex=null; + for (Handler handler : handlers._handlers) { try { - _handlers[i].handle(target,baseRequest, request, response); + handler.handle(target, baseRequest, request, response); } - catch(IOException e) + catch (IOException | RuntimeException e) { throw e; } - catch(RuntimeException e) + catch (Exception e) { - throw e; - } - catch(Exception e) - { - if (mex==null) - mex=new MultiException(); + if (mex == null) + mex = new MultiException(); mex.add(e); } } @@ -147,37 +169,54 @@ public class HandlerCollection extends AbstractHandlerContainer else throw new ServletException(mex); } - } } /* ------------------------------------------------------------ */ - /* Add a handler. + /** + * Adds a handler. * This implementation adds the passed handler to the end of the existing collection of handlers. - * @see org.eclipse.jetty.server.server.HandlerContainer#addHandler(org.eclipse.jetty.server.server.Handler) + * If the handler is already added, it is removed and readded */ public void addHandler(Handler handler) { - setHandlers(ArrayUtil.addToArray(getHandlers(), handler, Handler.class)); + while(true) + { + Handlers old = _handlers.get(); + Handlers handlers = newHandlers(ArrayUtil.addToArray(old==null?null:ArrayUtil.removeFromArray(old._handlers, handler), handler, Handler.class)); + if (updateHandlers(old,handlers)) + break; + } } /* ------------------------------------------------------------ */ - /* Prepend a handler. + /** + * Prepends a handler. * This implementation adds the passed handler to the start of the existing collection of handlers. - * @see org.eclipse.jetty.server.server.HandlerContainer#addHandler(org.eclipse.jetty.server.server.Handler) */ public void prependHandler(Handler handler) { - setHandlers(ArrayUtil.prependToArray(handler, getHandlers(), Handler.class)); + while(true) + { + Handlers old = _handlers.get(); + Handlers handlers = newHandlers(ArrayUtil.prependToArray(handler, old==null?null:old._handlers, Handler.class)); + if (updateHandlers(old,handlers)) + break; + } } /* ------------------------------------------------------------ */ public void removeHandler(Handler handler) { - Handler[] handlers = getHandlers(); - - if (handlers!=null && handlers.length>0 ) - setHandlers(ArrayUtil.removeFromArray(handlers, handler)); + while(true) + { + Handlers old = _handlers.get(); + if (old==null || old._handlers.length==0) + break; + Handlers handlers = newHandlers(ArrayUtil.removeFromArray(old._handlers, handler)); + if (updateHandlers(old,handlers)) + break; + } } /* ------------------------------------------------------------ */ @@ -196,10 +235,28 @@ public class HandlerCollection extends AbstractHandlerContainer { if (!isStopped()) throw new IllegalStateException("!STOPPED"); - Handler[] children=getChildHandlers(); + Handler[] children = getChildHandlers(); setHandlers(null); for (Handler child: children) child.destroy(); super.destroy(); } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + protected static class Handlers + { + private final Handler[] _handlers; + + protected Handlers(Handler[] handlers) + { + this._handlers = handlers; + } + + public Handler[] getHandlers() + { + return _handlers; + } + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java index 0573f237286..236b9153008 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/InetAccessHandler.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.IncludeExclude; import org.eclipse.jetty.util.IncludeExcludeSet; import org.eclipse.jetty.util.InetAddressSet; import org.eclipse.jetty.util.component.DumpableCollection; @@ -39,16 +40,32 @@ import org.eclipse.jetty.util.log.Logger; /** * InetAddress Access Handler *

    - * Controls access to the wrapped handler using the real remote IP. Control is provided - * by and {@link IncludeExcludeSet} over a {@link InetAddressSet}. This handler - * uses the real internet address of the connection, not one reported in the forwarded - * for headers, as this cannot be as easily forged. + * Controls access to the wrapped handler using the real remote IP. Control is + * provided by and {@link IncludeExcludeSet} over a {@link InetAddressSet}. This + * handler uses the real internet address of the connection, not one reported in + * the forwarded for headers, as this cannot be as easily forged. + *

    + * Additionally, there may be times when you want to only apply this handler to + * a subset of your connectors. In this situation you can use + * connectorNames to specify the connector names that you want this IP + * access filter to apply to. */ public class InetAccessHandler extends HandlerWrapper { private static final Logger LOG = Log.getLogger(InetAccessHandler.class); - private final IncludeExcludeSet _set = new IncludeExcludeSet<>(InetAddressSet.class); + private final IncludeExcludeSet _addrs = new IncludeExcludeSet<>(InetAddressSet.class); + private final IncludeExclude _names = new IncludeExclude<>(); + + /** + * Clears all the includes, excludes, included connector names and excluded + * connector names. + */ + public void clear() + { + _addrs.clear(); + _names.clear(); + } /** * Includes an InetAddress pattern @@ -58,7 +75,7 @@ public class InetAccessHandler extends HandlerWrapper */ public void include(String pattern) { - _set.include(pattern); + _addrs.include(pattern); } /** @@ -69,7 +86,7 @@ public class InetAccessHandler extends HandlerWrapper */ public void include(String... patterns) { - _set.include(patterns); + _addrs.include(patterns); } /** @@ -80,7 +97,7 @@ public class InetAccessHandler extends HandlerWrapper */ public void exclude(String pattern) { - _set.exclude(pattern); + _addrs.exclude(pattern); } /** @@ -91,14 +108,55 @@ public class InetAccessHandler extends HandlerWrapper */ public void exclude(String... patterns) { - _set.exclude(patterns); + _addrs.exclude(patterns); + } + + /** + * Includes a connector name. + * + * @param name Connector name to include in this handler. + */ + public void includeConnector(String name) + { + _names.include(name); + } + + /** + * Excludes a connector name. + * + * @param name Connector name to exclude in this handler. + */ + public void excludeConnector(String name) + { + _names.exclude(name); + } + + /** + * Includes connector names. + * + * @param names Connector names to include in this handler. + */ + public void includeConnectors(String... names) + { + _names.include(names); + } + + /** + * Excludes connector names. + * + * @param names Connector names to exclude in this handler. + */ + public void excludeConnectors(String... names) + { + _names.exclude(names); } /** * Checks the incoming request against the whitelist and blacklist */ @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { // Get the real remote IP (not the one set by the forwarded headers (which may be forged)) HttpChannel channel = baseRequest.getHttpChannel(); @@ -108,7 +166,7 @@ public class InetAccessHandler extends HandlerWrapper if (endp != null) { InetSocketAddress address = endp.getRemoteAddress(); - if (address != null && !isAllowed(address.getAddress(), request)) + if (address != null && !isAllowed(address.getAddress(), baseRequest, request)) { response.sendError(HttpStatus.FORBIDDEN_403); baseRequest.setHandled(true); @@ -123,23 +181,30 @@ public class InetAccessHandler extends HandlerWrapper /** * Checks if specified address and request are allowed by current InetAddress rules. * - * @param address the inetAddress to check - * @param request the request to check + * @param addr the inetAddress to check + * @param baseRequest the base request to check + * @param request the HttpServletRequest request to check * @return true if inetAddress and request are allowed */ - protected boolean isAllowed(InetAddress address, HttpServletRequest request) + protected boolean isAllowed(InetAddress addr, Request baseRequest, HttpServletRequest request) { - boolean allowed = _set.test(address); + String name = baseRequest.getHttpChannel().getConnector().getName(); if (LOG.isDebugEnabled()) - LOG.debug("{} {} {} for {}", this, allowed ? "allowed" : "denied", address, request); - return allowed; + { + Boolean allowedByName = _names.isIncludedAndNotExcluded(name); + Boolean allowedByAddr = _addrs.isIncludedAndNotExcluded(addr); + LOG.debug("{} allowedByName={} allowedByAddr={} for {}/{}", this, allowedByName, allowedByAddr, addr, request); + } + return _names.test(name) && _addrs.test(addr); } @Override public void dump(Appendable out, String indent) throws IOException { dumpObjects(out, indent, - DumpableCollection.from("included",_set.getIncluded()), - DumpableCollection.from("excluded",_set.getExcluded())); + new DumpableCollection("included", _addrs.getIncluded()), + new DumpableCollection("excluded", _addrs.getExcluded()), + new DumpableCollection("includedConnector", _names.getIncluded()), + new DumpableCollection("excludedConnector", _names.getExcluded())); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java index 5525a49990f..c8e54f1a06d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/HouseKeeper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HouseKeeper.java index 82bc5e1f4d1..6e5faaa6b05 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/HouseKeeper.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HouseKeeper.java @@ -121,7 +121,7 @@ public class HouseKeeper extends AbstractLifeCycle if (_scheduler == null) { - _scheduler = new ScheduledExecutorScheduler(); + _scheduler = new ScheduledExecutorScheduler(String.format("Session-HouseKeeper-%x",hashCode()),false); _ownScheduler = true; _scheduler.start(); if (LOG.isDebugEnabled()) LOG.debug("Using own scheduler for scavenging"); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionData.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionData.java index 6d1e7b8018f..bb8d15d8d6b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionData.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionData.java @@ -28,6 +28,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jetty.util.ClassLoadingObjectInputStream; +import org.eclipse.jetty.util.ClassVisibilityChecker; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -78,22 +79,47 @@ public class SessionData implements Serializable out.writeObject(entries); for (Entry entry: data._attributes.entrySet()) { - out.writeUTF(entry.getKey()); - ClassLoader loader = entry.getValue().getClass().getClassLoader(); - boolean isServerLoader = false; - - if (loader == Thread.currentThread().getContextClassLoader()) //is it the webapp classloader? - isServerLoader = false; - else if (loader == Thread.currentThread().getContextClassLoader().getParent() || loader == SessionData.class.getClassLoader() || loader == null) // is it the container loader? - isServerLoader = true; - else - throw new IOException ("Unknown loader"); // we don't know what loader to use + out.writeUTF(entry.getKey()); - out.writeBoolean(isServerLoader); + Class clazz = entry.getValue().getClass(); + ClassLoader loader = clazz.getClassLoader(); + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + boolean isContextLoader; + + if (loader == contextLoader) //is it the context classloader? + isContextLoader = true; + else if (contextLoader == null) //not context classloader + isContextLoader = false; + else if (contextLoader instanceof ClassVisibilityChecker) + { + //Clazz not loaded by context classloader, but ask if loadable by context classloader, + //because preferable to use context classloader if possible (eg for deep structures). + ClassVisibilityChecker checker = (ClassVisibilityChecker)(contextLoader); + isContextLoader = (checker.isSystemClass(clazz) && !(checker.isServerClass(clazz))); + } + else + { + //Class wasn't loaded by context classloader, but try loading from context loader, + //because preferable to use context classloader if possible (eg for deep structures). + try + { + Class result = contextLoader.loadClass(clazz.getName()); + isContextLoader = (result == clazz); //only if TTCL loaded this instance of the class + } + catch (Throwable e) + { + isContextLoader = false; //TCCL can't see the class + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("Attribute {} class={} isServerLoader={}", entry.getKey(),clazz.getName(),(!isContextLoader)); + out.writeBoolean(!isContextLoader); out.writeObject(entry.getValue()); } } - + + /** * De-serialize the attribute map of a session. * @@ -118,12 +144,15 @@ public class SessionData implements Serializable data._attributes = new ConcurrentHashMap<>(); int entries = ((Integer)o).intValue(); + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + ClassLoader serverLoader = SessionData.class.getClassLoader(); for (int i=0; i < entries; i++) { String name = in.readUTF(); //attribute name boolean isServerClassLoader = in.readBoolean(); //use server or webapp classloader to load - - Object value = ((ClassLoadingObjectInputStream)in).readObject(isServerClassLoader?SessionData.class.getClassLoader():Thread.currentThread().getContextClassLoader()); + if (LOG.isDebugEnabled()) + LOG.debug("Deserialize {} isServerLoader={} serverLoader={} tccl={}", name, isServerClassLoader,serverLoader, contextLoader); + Object value = ((ClassLoadingObjectInputStream)in).readObject(isServerClassLoader?serverLoader:contextLoader); data._attributes.put(name, value); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java index 63f7380bf95..f18449de8aa 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java @@ -369,11 +369,22 @@ public class SessionHandler extends ScopedHandler if (_sessionListeners!=null) { - HttpSessionEvent event=new HttpSessionEvent(session); - for (int i = _sessionListeners.size()-1; i>=0; i--) + //We annoint the calling thread with + //the webapp's classloader because the calling thread may + //come from the scavenger, rather than a request thread + Runnable r = new Runnable() { - _sessionListeners.get(i).sessionDestroyed(event); - } + @Override + public void run () + { + HttpSessionEvent event=new HttpSessionEvent(session); + for (int i = _sessionListeners.size()-1; i>=0; i--) + { + _sessionListeners.get(i).sessionDestroyed(event); + } + } + }; + _sessionContext.run(r); } } @@ -516,10 +527,9 @@ public class SessionHandler extends ScopedHandler _scheduler = server.getBean(Scheduler.class); if (_scheduler == null) { - _scheduler = new ScheduledExecutorScheduler(); + _scheduler = new ScheduledExecutorScheduler(String.format("Session-Scheduler-%x",hashCode()), false); _ownScheduler = true; _scheduler.start(); - } } @@ -719,7 +729,7 @@ public class SessionHandler extends ScopedHandler if (isUsingCookies()) { String sessionPath = (_cookieConfig.getPath()==null) ? contextPath : _cookieConfig.getPath(); - sessionPath = (sessionPath==null||sessionPath.length()==0) ? "/" : sessionPath; + sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath; String id = getExtendedId(session); HttpCookie cookie = null; if (_sessionComment == null) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java index 3f448a49e01..d94cc6bdc42 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java @@ -170,7 +170,7 @@ public class ConnectionOpenCloseTest extends AbstractHttpTest @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review public void testSSLOpenRequestClose() throws Exception { - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); File keystore = MavenTestingUtils.getTestResourceFile("keystore"); sslContextFactory.setKeyStoreResource(Resource.newResource(keystore)); sslContextFactory.setKeyStorePassword("storepwd"); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java index e1b2c69d467..4f93cf5503d 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ForwardedRequestCustomizerTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.server; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.IOException; import java.util.ArrayDeque; import java.util.Deque; @@ -41,6 +36,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class ForwardedRequestCustomizerTest { private Server _server; @@ -95,6 +95,68 @@ public class ForwardedRequestCustomizerTest _server.join(); } + @Test + public void testHostIpv4() throws Exception + { + String response=_connector.getResponse( + "GET / HTTP/1.1\n"+ + "Host: 1.2.3.4:2222\n"+ + "\n"); + assertThat(response, Matchers.containsString("200 OK")); + assertEquals("http",_results.poll()); + assertEquals("1.2.3.4",_results.poll()); + assertEquals("2222",_results.poll()); + assertEquals("0.0.0.0",_results.poll()); + assertEquals("0",_results.poll()); + } + + @Test + public void testHostIpv6() throws Exception + { + String response=_connector.getResponse( + "GET / HTTP/1.1\n"+ + "Host: [::1]:2222\n"+ + "\n"); + assertThat(response, Matchers.containsString("200 OK")); + assertEquals("http",_results.poll()); + assertEquals("[::1]",_results.poll()); + assertEquals("2222",_results.poll()); + assertEquals("0.0.0.0",_results.poll()); + assertEquals("0",_results.poll()); + } + + + + @Test + public void testURIIpv4() throws Exception + { + String response=_connector.getResponse( + "GET http://1.2.3.4:2222/ HTTP/1.1\n"+ + "Host: wrong\n"+ + "\n"); + assertThat(response, Matchers.containsString("200 OK")); + assertEquals("http",_results.poll()); + assertEquals("1.2.3.4",_results.poll()); + assertEquals("2222",_results.poll()); + assertEquals("0.0.0.0",_results.poll()); + assertEquals("0",_results.poll()); + } + + @Test + public void testURIIpv6() throws Exception + { + String response=_connector.getResponse( + "GET http://[::1]:2222/ HTTP/1.1\n"+ + "Host: wrong\n"+ + "\n"); + assertThat(response, Matchers.containsString("200 OK")); + assertEquals("http",_results.poll()); + assertEquals("[::1]",_results.poll()); + assertEquals("2222",_results.poll()); + assertEquals("0.0.0.0",_results.poll()); + assertEquals("0",_results.poll()); + } + @Test public void testRFC7239_Examples_4() throws Exception @@ -224,6 +286,7 @@ public class ForwardedRequestCustomizerTest "GET / HTTP/1.1\n"+ "Host: myhost\n"+ "X-Forwarded-For: 10.9.8.7,6.5.4.3\n"+ + "X-Forwarded-For: 8.9.8.7,7.5.4.3\n"+ "\n"); assertThat(response, Matchers.containsString("200 OK")); assertEquals("http",_results.poll()); @@ -265,6 +328,38 @@ public class ForwardedRequestCustomizerTest assertEquals("1111",_results.poll()); } + @Test + public void testForIpv6AndPort() throws Exception + { + String response=_connector.getResponse( + "GET / HTTP/1.1\n"+ + "Host: myhost\n"+ + "X-Forwarded-For: 1:2:3:4:5:6:7:8\n"+ + "X-Forwarded-Port: 2222\n"+ + "\n"); + assertThat(response, Matchers.containsString("200 OK")); + assertEquals("http",_results.poll()); + assertEquals("myhost",_results.poll()); + assertEquals("80",_results.poll()); + assertEquals("[1:2:3:4:5:6:7:8]",_results.poll()); + assertEquals("2222",_results.poll()); + + response=_connector.getResponse( + "GET / HTTP/1.1\n"+ + "Host: myhost\n"+ + "X-Forwarded-Port: 2222\n"+ + "X-Forwarded-For: 1:2:3:4:5:6:7:8\n"+ + "X-Forwarded-For: 7:7:7:7:7:7:7:7\n"+ + "X-Forwarded-Port: 3333\n"+ + "\n"); + assertThat(response, Matchers.containsString("200 OK")); + assertEquals("http",_results.poll()); + assertEquals("myhost",_results.poll()); + assertEquals("80",_results.poll()); + assertEquals("[1:2:3:4:5:6:7:8]",_results.poll()); + assertEquals("2222",_results.poll()); + } + @Test public void testLegacyProto() throws Exception { @@ -354,7 +449,26 @@ public class ForwardedRequestCustomizerTest assertTrue(_wasSecure.get()); assertEquals("0123456789abcdef",_sslCertificate.get()); } - + + + @Test + public void testMixed() throws Exception + { + String response = _connector.getResponse( + "GET / HTTP/1.1\n" + + "Host: myhost\n" + + "X-Forwarded-For: 11.9.8.7:1111,8.5.4.3:2222\n" + + "X-Forwarded-Port: 3333\n" + + "Forwarded: for=192.0.2.43,for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com\n"+ + "X-Forwarded-For: 11.9.8.7:1111,8.5.4.3:2222\n" + + "\n"); + + assertEquals("http",_results.poll()); + assertEquals("example.com",_results.poll()); + assertEquals("80",_results.poll()); + assertEquals("192.0.2.43",_results.poll()); + assertEquals("0",_results.poll()); + } interface RequestTester diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulStopTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulStopTest.java index 4147c0922d9..7b8cd0379f4 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulStopTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/GracefulStopTest.java @@ -18,18 +18,6 @@ package org.eclipse.jetty.server; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.lessThan; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.condition.OS.WINDOWS; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -44,7 +32,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -54,18 +41,32 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.LocalConnector.LocalEndPoint; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.hamcrest.Matcher; import org.hamcrest.Matchers; - import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + public class GracefulStopTest { /** @@ -657,6 +658,60 @@ public class GracefulStopTest assertThat(response,Matchers.not(Matchers.containsString("Connection: close"))); assertTrue(latch.await(10,TimeUnit.SECONDS)); } + + @Test + public void testFailedStart() + { + Server server= new Server(); + + LocalConnector connector = new LocalConnector(server); + server.addConnector(connector); + + ContextHandlerCollection contexts = new ContextHandlerCollection(); + server.setHandler(contexts); + AtomicBoolean context0Started = new AtomicBoolean(false); + ContextHandler context0 = new ContextHandler("/zero") + { + @Override + protected void doStart() throws Exception + { + context0Started.set(true); + } + }; + ContextHandler context1 = new ContextHandler("/one") + { + @Override + protected void doStart() throws Exception + { + throw new Exception("Test start failure"); + } + }; + AtomicBoolean context2Started = new AtomicBoolean(false); + ContextHandler context2 = new ContextHandler("/two") + { + @Override + protected void doStart() throws Exception + { + context2Started.set(true); + } + }; + contexts.setHandlers(new Handler[]{context0, context1, context2}); + + try + { + server.start(); + fail(); + } + catch(Exception e) + { + assertThat(e.getMessage(),is("Test start failure")); + } + + assertTrue(server.getContainedBeans(LifeCycle.class).stream().noneMatch(LifeCycle::isRunning)); + assertTrue(server.getContainedBeans(LifeCycle.class).stream().anyMatch(LifeCycle::isFailed)); + assertTrue(context0Started.get()); + assertFalse(context2Started.get()); + } static class NoopHandler extends AbstractHandler { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java index 06662fe7b1b..2bf2e9a05c3 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java @@ -33,7 +33,6 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -55,12 +54,12 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertTrue; public class HttpConnectionTest @@ -151,12 +150,14 @@ public class HttpConnectionTest connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setHttpCompliance(HttpCompliance.RFC2616); String request = "GET / HTTP/0.9\r\n\r\n"; String response = connector.getResponse(request); - assertThat(response, containsString("400 Bad Version")); + assertThat(response, containsString("400 Bad Request")); + assertThat(response, containsString("reason: Bad Version")); connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().setHttpCompliance(HttpCompliance.RFC7230); request = "GET / HTTP/0.9\r\n\r\n"; response = connector.getResponse(request); - assertThat(response, containsString("400 Bad Version")); + assertThat(response, containsString("400 Bad Request")); + assertThat(response, containsString("reason: Bad Version")); } /** @@ -365,7 +366,8 @@ public class HttpConnectionTest public void testBadPathDotDotPath() throws Exception { String response=connector.getResponse("GET /ooops/../../path HTTP/1.0\r\nHost: localhost:80\r\n\n"); - checkContains(response,0,"HTTP/1.1 400 Bad URI"); + checkContains(response,0,"HTTP/1.1 400 Bad Request"); + checkContains(response,0,"reason: Bad URI"); } @Test @@ -380,28 +382,32 @@ public class HttpConnectionTest public void testBadPathEncodedDotDotPath() throws Exception { String response=connector.getResponse("GET /ooops/%2e%2e/%2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n"); - checkContains(response,0,"HTTP/1.1 400 Bad URI"); + checkContains(response,0,"HTTP/1.1 400 Bad Request"); + checkContains(response,0,"reason: Bad URI"); } @Test public void testBadDotDotPath() throws Exception { String response=connector.getResponse("GET ../path HTTP/1.0\r\nHost: localhost:80\r\n\n"); - checkContains(response,0,"HTTP/1.1 400 Bad URI"); + checkContains(response,0,"HTTP/1.1 400 Bad Request"); + checkContains(response,0,"reason: Bad URI"); } @Test public void testBadSlashDotDotPath() throws Exception { String response=connector.getResponse("GET /../path HTTP/1.0\r\nHost: localhost:80\r\n\n"); - checkContains(response,0,"HTTP/1.1 400 Bad URI"); + checkContains(response,0,"HTTP/1.1 400 Bad Request"); + checkContains(response,0,"reason: Bad URI"); } @Test public void testEncodedBadDotDotPath() throws Exception { String response=connector.getResponse("GET %2e%2e/path HTTP/1.0\r\nHost: localhost:80\r\n\n"); - checkContains(response,0,"HTTP/1.1 400 Bad URI"); + checkContains(response,0,"HTTP/1.1 400 Bad Request"); + checkContains(response,0,"reason: Bad URI"); } @Test @@ -538,6 +544,7 @@ public class HttpConnectionTest public void testChunkNoTrailer() throws Exception { // Expect TimeoutException logged + connector.setIdleTimeout(1000); String response=connector.getResponse("GET /R1 HTTP/1.1\r\n"+ "Host: localhost\r\n"+ "Transfer-Encoding: chunked\r\n"+ diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/OptionalSslConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/OptionalSslConnectionTest.java index e1de577b548..e513b8cff10 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/OptionalSslConnectionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/OptionalSslConnectionTest.java @@ -52,7 +52,7 @@ public class OptionalSslConnectionTest server = new Server(serverThreads); String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath(); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystore); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); @@ -113,7 +113,7 @@ public class OptionalSslConnectionTest } // Then try a SSL connection. - SslContextFactory sslContextFactory = new SslContextFactory(true); + SslContextFactory sslContextFactory = new SslContextFactory.Client(true); sslContextFactory.start(); try (Socket ssl = sslContextFactory.newSslSocket()) { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java index 579f6305194..2fbca58a82d 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java @@ -18,17 +18,18 @@ package org.eclipse.jetty.server; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertNull; - import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.toolchain.test.Net; import org.eclipse.jetty.util.log.StacklessLogging; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; - +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; + /** * */ @@ -84,6 +85,7 @@ public class ProxyConnectionTest @Test public void testIPv6() throws Exception { + Assumptions.assumeTrue(Net.isIpv6InterfaceAvailable()); String response=_connector.getResponse("PROXY UNKNOWN eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n"+ "GET /path HTTP/1.1\n"+ "Host: server:80\n"+ diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index f05f8105efa..b765880d5f1 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.server; -import static org.hamcrest.Matchers.contains; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; @@ -34,9 +33,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; +import java.util.List; import java.util.Locale; - -import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -74,10 +72,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; @@ -85,6 +84,7 @@ import static org.hamcrest.Matchers.startsWith; 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.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -198,7 +198,7 @@ public class ResponseTest { Response response = getResponse(); - assertEquals(null, response.getContentType()); + assertNull(response.getContentType()); response.setHeader("Content-Type", "text/something"); assertEquals("text/something", response.getContentType()); @@ -304,7 +304,7 @@ public class ResponseTest // Inferred from encoding.properties Response response = getResponse(); - assertEquals(null, response.getContentType()); + assertNull(response.getContentType()); response.setHeader("Content-Type", "application/xhtml+xml"); assertEquals("application/xhtml+xml", response.getContentType()); @@ -319,7 +319,7 @@ public class ResponseTest Response response = getResponse(); // Assumed from known types - assertEquals(null, response.getContentType()); + assertNull(response.getContentType()); response.setHeader("Content-Type", "text/json"); assertEquals("text/json", response.getContentType()); response.getWriter(); @@ -329,7 +329,7 @@ public class ResponseTest response.recycle(); // Assumed from encoding.properties - assertEquals(null, response.getContentType()); + assertNull(response.getContentType()); response.setHeader("Content-Type", "application/vnd.api+json"); assertEquals("application/vnd.api+json", response.getContentType()); response.getWriter(); @@ -342,7 +342,7 @@ public class ResponseTest { Response response = getResponse(); - assertEquals(null, response.getContentType()); + assertNull(response.getContentType()); response.recycle(); response.setContentType("text/html;charset=utf-8;charset=UTF-8"); @@ -352,7 +352,7 @@ public class ResponseTest } @Test - public void testLocale() throws Exception + public void testLocale() { Response response = getResponse(); @@ -362,7 +362,7 @@ public class ResponseTest response.getHttpChannel().getRequest().setContext(context.getServletContext()); response.setLocale(java.util.Locale.ITALIAN); - assertEquals(null, response.getContentType()); + assertNull(response.getContentType()); response.setContentType("text/plain"); assertEquals("text/plain;charset=ISO-8859-2", response.getContentType()); @@ -580,24 +580,30 @@ public class ResponseTest assertEquals("foo2/bar2;charset=utf-8", response.getContentType()); } + @Test + public void testPrint_Empty() throws Exception + { + Response response = getResponse(); + response.setCharacterEncoding(UTF_8.name()); + + try(ServletOutputStream outputStream = response.getOutputStream()) + { + outputStream.print("ABC"); + outputStream.print(""); + outputStream.println(); + outputStream.flush(); + } + + String expected = "ABC\r\n"; + assertEquals(expected,BufferUtil.toString(_content, UTF_8)); + } @Test public void testPrintln() throws Exception { Response response = getResponse(); - Request request = response.getHttpChannel().getRequest(); - - SessionHandler session_handler = new SessionHandler(); - session_handler.setServer(_server); - session_handler.setUsingCookies(true); - session_handler.start(); - request.setSessionHandler(session_handler); - HttpSession session = request.getSession(true); response.setCharacterEncoding(UTF_8.name()); - assertThat(session,not(nullValue())); - assertTrue(session.isNew()); - String expected = ""; response.getOutputStream().print("ABC"); expected += "ABC"; @@ -687,7 +693,7 @@ public class ResponseTest response.setStatus(200); assertEquals(200, response.getStatus()); - assertEquals(null, response.getReason()); + assertNull(response.getReason()); response = getResponse(); @@ -720,7 +726,7 @@ public class ResponseTest response.setStatus(200); assertEquals(200, response.getStatus()); - assertEquals(null, response.getReason()); + assertNull(response.getReason()); response = getResponse(); @@ -752,7 +758,6 @@ public class ResponseTest @Test public void testEncodeRedirect() - throws Exception { Response response = getResponse(); Request request = response.getHttpChannel().getRequest(); @@ -875,7 +880,7 @@ public class ResponseTest } @Test - public void testInvalidSendRedirect() throws Exception + public void testInvalidSendRedirect() { // Request is /path/info, so we need 3 ".." for an invalid redirect. Response response = getResponse(); @@ -898,10 +903,10 @@ public class ResponseTest Response response = getResponse(); PrintWriter writer = response.getWriter(); response.setContentLength(0); - assertTrue(!response.isCommitted()); - assertTrue(!writer.checkError()); + assertFalse(response.isCommitted()); + assertFalse(writer.checkError()); writer.print(""); - assertTrue(!writer.checkError()); + assertFalse(writer.checkError()); assertTrue(response.isCommitted()); } @@ -914,7 +919,7 @@ public class ResponseTest server.setHandler(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { response.setStatus(200); response.setContentType("text/plain"); @@ -964,7 +969,7 @@ public class ResponseTest } @Test - public void testAddCookie() throws Exception + public void testAddCookie() { Response response = getResponse(); @@ -978,11 +983,11 @@ public class ResponseTest String set = response.getHttpFields().get("Set-Cookie"); - assertEquals("name=value;Path=/path;Domain=domain;Secure;HttpOnly", set); + assertEquals("name=value; Path=/path; Domain=domain; Secure; HttpOnly", set); } @Test - public void testAddCookieComplianceRFC2965() throws Exception + public void testAddCookieComplianceRFC2965() { Response response = getResponse(); response.getHttpChannel().getHttpConfiguration().setResponseCookieCompliance(CookieCompliance.RFC2965); @@ -1016,7 +1021,7 @@ public class ResponseTest String set = response.getHttpFields().get("Set-Cookie"); - assertEquals("foo=bar%3Bbaz;Path=/secure", set); + assertEquals("foo=bar%3Bbaz; Path=/secure", set); } /** @@ -1033,7 +1038,7 @@ public class ResponseTest } @Test - public void testCookiesWithReset() throws Exception + public void testCookiesWithReset() { Response response = getResponse(); @@ -1057,8 +1062,8 @@ public class ResponseTest assertNotNull(set); ArrayList list = Collections.list(set); assertThat(list, containsInAnyOrder( - "name=value;Path=/path;Domain=domain;Secure;HttpOnly", - "name2=value2;Path=/path;Domain=domain" + "name=value; Path=/path; Domain=domain; Secure; HttpOnly", + "name2=value2; Path=/path; Domain=domain" )); //get rid of the cookies @@ -1089,15 +1094,43 @@ public class ResponseTest response.replaceCookie(new HttpCookie("Foo","value", "A", "/path")); response.replaceCookie(new HttpCookie("Foo","value")); - assertThat(Collections.list(response.getHttpFields().getValues("Set-Cookie")), - contains( - "Foo=value", - "Foo=value;Path=/path;Domain=A", - "Foo=value;Path=/path;Domain=B", - "Bar=value", - "Bar=value;Path=/left", - "Bar=value;Path=/right" - )); + String[] expected = new String[]{ + "Foo=value", + "Foo=value; Path=/path; Domain=A", + "Foo=value; Path=/path; Domain=B", + "Bar=value", + "Bar=value; Path=/left", + "Bar=value; Path=/right" + }; + + List actual = Collections.list(response.getHttpFields().getValues("Set-Cookie")); + assertThat("HttpCookie order", actual, hasItems(expected)); + } + + + @Test + public void testReplaceParsedHttpCookie() + { + Response response = getResponse(); + + response.addHeader(HttpHeader.SET_COOKIE.asString(), "Foo=123456"); + response.replaceCookie(new HttpCookie("Foo","value")); + List actual = Collections.list(response.getHttpFields().getValues("Set-Cookie")); + assertThat(actual, hasItems(new String[] {"Foo=value"})); + + response.setHeader(HttpHeader.SET_COOKIE,"Foo=123456; domain=Bah; Path=/path"); + response.replaceCookie(new HttpCookie("Foo","other")); + actual = Collections.list(response.getHttpFields().getValues("Set-Cookie")); + assertThat(actual, hasItems(new String[] {"Foo=123456; domain=Bah; Path=/path", "Foo=other"})); + + response.replaceCookie(new HttpCookie("Foo","replaced", "Bah", "/path")); + actual = Collections.list(response.getHttpFields().getValues("Set-Cookie")); + assertThat(actual, hasItems(new String[] {"Foo=replaced; Path=/path; Domain=Bah", "Foo=other"})); + + response.setHeader(HttpHeader.SET_COOKIE,"Foo=123456; domain=Bah; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; Path=/path"); + response.replaceCookie(new HttpCookie("Foo","replaced", "Bah", "/path")); + actual = Collections.list(response.getHttpFields().getValues("Set-Cookie")); + assertThat(actual, hasItems(new String[] {"Foo=replaced; Path=/path; Domain=Bah"})); } @Test @@ -1112,312 +1145,6 @@ public class ResponseTest // Must not throw output.flush(); } - - @Test - public void testSetRFC2965Cookie() throws Exception - { - Response response = _channel.getResponse(); - HttpFields fields = response.getHttpFields(); - - response.addSetRFC2965Cookie("null",null,null,null,-1,null,false,false,-1); - assertEquals("null=",fields.get("Set-Cookie")); - - fields.clear(); - - response.addSetRFC2965Cookie("minimal","value",null,null,-1,null,false,false,-1); - assertEquals("minimal=value",fields.get("Set-Cookie")); - - fields.clear(); - //test cookies with same name, domain and path - response.addSetRFC2965Cookie("everything","something","domain","path",0,"noncomment",true,true,0); - response.addSetRFC2965Cookie("everything","value","domain","path",0,"comment",true,true,0); - Enumeration e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=something;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=noncomment",e.nextElement()); - assertEquals("everything=value;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement()); - assertFalse(e.hasMoreElements()); - assertEquals("Thu, 01 Jan 1970 00:00:00 GMT",fields.get("Expires")); - assertFalse(e.hasMoreElements()); - - //test cookies with same name, different domain - fields.clear(); - response.addSetRFC2965Cookie("everything","other","domain1","path",0,"blah",true,true,0); - response.addSetRFC2965Cookie("everything","value","domain2","path",0,"comment",true,true,0); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Version=1;Path=path;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement()); - assertTrue(e.hasMoreElements()); - assertEquals("everything=value;Version=1;Path=path;Domain=domain2;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement()); - assertFalse(e.hasMoreElements()); - - //test cookies with same name, same path, one with domain, one without - fields.clear(); - response.addSetRFC2965Cookie("everything","other","domain1","path",0,"blah",true,true,0); - response.addSetRFC2965Cookie("everything","value","","path",0,"comment",true,true,0); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Version=1;Path=path;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement()); - assertTrue(e.hasMoreElements()); - assertEquals("everything=value;Version=1;Path=path;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement()); - assertFalse(e.hasMoreElements()); - - - //test cookies with same name, different path - fields.clear(); - response.addSetRFC2965Cookie("everything","other","domain1","path1",0,"blah",true,true,0); - response.addSetRFC2965Cookie("everything","value","domain1","path2",0,"comment",true,true,0); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Version=1;Path=path1;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement()); - assertTrue(e.hasMoreElements()); - assertEquals("everything=value;Version=1;Path=path2;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement()); - assertFalse(e.hasMoreElements()); - - //test cookies with same name, same domain, one with path, one without - fields.clear(); - response.addSetRFC2965Cookie("everything","other","domain1","path1",0,"blah",true,true,0); - response.addSetRFC2965Cookie("everything","value","domain1","",0,"comment",true,true,0); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Version=1;Path=path1;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement()); - assertTrue(e.hasMoreElements()); - assertEquals("everything=value;Version=1;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement()); - assertFalse(e.hasMoreElements()); - - //test cookies same name only, no path, no domain - fields.clear(); - response.addSetRFC2965Cookie("everything","other","","",0,"blah",true,true,0); - response.addSetRFC2965Cookie("everything","value","","",0,"comment",true,true,0); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Version=1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement()); - assertEquals("everything=value;Version=1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement()); - assertFalse(e.hasMoreElements()); - - fields.clear(); - response.addSetRFC2965Cookie("ev erything","va lue","do main","pa th",1,"co mment",true,true,1); - String setCookie=fields.get("Set-Cookie"); - assertThat(setCookie,Matchers.startsWith("\"ev erything\"=\"va lue\";Version=1;Path=\"pa th\";Domain=\"do main\";Expires=")); - assertThat(setCookie,Matchers.endsWith(" GMT;Max-Age=1;Secure;HttpOnly;Comment=\"co mment\"")); - - fields.clear(); - response.addSetRFC2965Cookie("name","value",null,null,-1,null,false,false,0); - setCookie=fields.get("Set-Cookie"); - assertEquals(-1,setCookie.indexOf("Version=")); - fields.clear(); - response.addSetRFC2965Cookie("name","v a l u e",null,null,-1,null,false,false,0); - setCookie=fields.get("Set-Cookie"); - - fields.clear(); - response.addSetRFC2965Cookie("json","{\"services\":[\"cwa\", \"aa\"]}",null,null,-1,null,false,false,-1); - assertEquals("json=\"{\\\"services\\\":[\\\"cwa\\\", \\\"aa\\\"]}\"",fields.get("Set-Cookie")); - - fields.clear(); - response.addSetRFC2965Cookie("name","value","domain",null,-1,null,false,false,-1); - response.addSetRFC2965Cookie("name","other","domain",null,-1,null,false,false,-1); - response.addSetRFC2965Cookie("name","more","domain",null,-1,null,false,false,-1); - e = fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertThat(e.nextElement(), Matchers.startsWith("name=value")); - assertThat(e.nextElement(), Matchers.startsWith("name=other")); - assertThat(e.nextElement(), Matchers.startsWith("name=more")); - - response.addSetRFC2965Cookie("foo","bar","domain",null,-1,null,false,false,-1); - response.addSetRFC2965Cookie("foo","bob","domain",null,-1,null,false,false,-1); - assertThat(fields.get("Set-Cookie"), Matchers.startsWith("name=value")); - - - fields.clear(); - response.addSetRFC2965Cookie("name","value%=",null,null,-1,null,false,false,0); - setCookie=fields.get("Set-Cookie"); - assertEquals("name=value%=",setCookie); - } - - @Test - public void testSetRFC6265Cookie() throws Exception - { - Response response = _channel.getResponse(); - HttpFields fields = response.getHttpFields(); - - response.addSetRFC6265Cookie("null",null,null,null,-1,false,false); - assertEquals("null=",fields.get("Set-Cookie")); - - fields.clear(); - - response.addSetRFC6265Cookie("minimal","value",null,null,-1,false,false); - assertEquals("minimal=value",fields.get("Set-Cookie")); - - fields.clear(); - //test cookies with same name, domain and path - response.addSetRFC6265Cookie("everything","something","domain","path",0,true,true); - response.addSetRFC6265Cookie("everything","value","domain","path",0,true,true); - Enumeration e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=something;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertEquals("everything=value;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertFalse(e.hasMoreElements()); - assertEquals("Thu, 01 Jan 1970 00:00:00 GMT",fields.get("Expires")); - assertFalse(e.hasMoreElements()); - - //test cookies with same name, different domain - fields.clear(); - response.addSetRFC6265Cookie("everything","other","domain1","path",0,true,true); - response.addSetRFC6265Cookie("everything","value","domain2","path",0,true,true); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Path=path;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertTrue(e.hasMoreElements()); - assertEquals("everything=value;Path=path;Domain=domain2;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertFalse(e.hasMoreElements()); - - //test cookies with same name, same path, one with domain, one without - fields.clear(); - response.addSetRFC6265Cookie("everything","other","domain1","path",0,true,true); - response.addSetRFC6265Cookie("everything","value","","path",0,true,true); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Path=path;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertTrue(e.hasMoreElements()); - assertEquals("everything=value;Path=path;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertFalse(e.hasMoreElements()); - - - //test cookies with same name, different path - fields.clear(); - response.addSetRFC6265Cookie("everything","other","domain1","path1",0,true,true); - response.addSetRFC6265Cookie("everything","value","domain1","path2",0,true,true); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Path=path1;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertTrue(e.hasMoreElements()); - assertEquals("everything=value;Path=path2;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertFalse(e.hasMoreElements()); - - //test cookies with same name, same domain, one with path, one without - fields.clear(); - response.addSetRFC6265Cookie("everything","other","domain1","path1",0,true,true); - response.addSetRFC6265Cookie("everything","value","domain1","",0,true,true); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Path=path1;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertTrue(e.hasMoreElements()); - assertEquals("everything=value;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertFalse(e.hasMoreElements()); - - //test cookies same name only, no path, no domain - fields.clear(); - response.addSetRFC6265Cookie("everything","other","","",0,true,true); - response.addSetRFC6265Cookie("everything","value","","",0,true,true); - e =fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertEquals("everything=other;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertEquals("everything=value;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly",e.nextElement()); - assertFalse(e.hasMoreElements()); - - String badNameExamples[] = { - "\"name\"", - "name\t", - "na me", - "name\u0082", - "na\tme", - "na;me", - "{name}", - "[name]", - "\"" - }; - - for (String badNameExample : badNameExamples) - { - fields.clear(); - try - { - response.addSetRFC6265Cookie(badNameExample, "value", null, "/", 1, true, true); - } - catch (IllegalArgumentException ex) - { - // System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage()); - assertThat("Testing bad name: [" + badNameExample + "]", ex.getMessage(), - allOf(containsString("RFC6265"), containsString("RFC2616"))); - } - } - - String badValueExamples[] = { - "va\tlue", - "\t", - "value\u0000", - "val\u0082ue", - "va lue", - "va;lue", - "\"value", - "value\"", - "val\\ue", - "val\"ue", - "\"" - }; - - for (String badValueExample : badValueExamples) - { - fields.clear(); - try - { - response.addSetRFC6265Cookie("name", badValueExample, null, "/", 1, true, true); - } - catch (IllegalArgumentException ex) - { - // System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage()); - assertThat("Testing bad value [" + badValueExample + "]", ex.getMessage(), Matchers.containsString("RFC6265")); - } - } - - String goodNameExamples[] = { - "name", - "n.a.m.e", - "na-me", - "+name", - "na*me", - "na$me", - "#name" - }; - - for (String goodNameExample : goodNameExamples) - { - fields.clear(); - response.addSetRFC6265Cookie(goodNameExample, "value", null, "/", 1, true, true); - // should not throw an exception - } - - String goodValueExamples[] = { - "value", - "", - null, - "val=ue", - "val-ue", - "val/ue", - "v.a.l.u.e" - }; - - for (String goodValueExample : goodValueExamples) - { - fields.clear(); - response.addSetRFC6265Cookie("name", goodValueExample, null, "/", 1, true, true); - // should not throw an exception - } - - fields.clear(); - - response.addSetRFC6265Cookie("name","value","domain",null,-1,false,false); - response.addSetRFC6265Cookie("name","other","domain",null,-1,false,false); - response.addSetRFC6265Cookie("name","more","domain",null,-1,false,false); - e = fields.getValues("Set-Cookie"); - assertTrue(e.hasMoreElements()); - assertThat(e.nextElement(), Matchers.startsWith("name=value")); - assertThat(e.nextElement(), Matchers.startsWith("name=other")); - assertThat(e.nextElement(), Matchers.startsWith("name=more")); - - response.addSetRFC6265Cookie("foo","bar","domain",null,-1,false,false); - response.addSetRFC6265Cookie("foo","bob","domain",null,-1,false,false); - assertThat(fields.get("Set-Cookie"), Matchers.startsWith("name=value")); - } private Response getResponse() { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java index 3ddb18825cb..a7b8325aa59 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ThreadStarvationTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.server; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -33,7 +28,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -56,12 +50,16 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.util.thread.Scheduler; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class ThreadStarvationTest { final static int BUFFER_SIZE=1024*1024; @@ -82,14 +80,14 @@ public class ThreadStarvationTest List params = new ArrayList<>(); // HTTP - ConnectorProvider http = (server, acceptors, selectors) -> new ServerConnector(server, acceptors, selectors); - ClientSocketProvider httpClient = (host, port) -> new Socket(host, port); + ConnectorProvider http = ServerConnector::new; + ClientSocketProvider httpClient = Socket::new; params.add(new Scenario("http", http, httpClient)); // HTTPS/SSL/TLS ConnectorProvider https = (server, acceptors, selectors) -> { Path keystorePath = MavenTestingUtils.getTestResourcePath("keystore"); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystorePath.toString()); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); @@ -98,8 +96,7 @@ public class ThreadStarvationTest ByteBufferPool pool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged()); HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(); - ServerConnector connector = new ServerConnector(server,(Executor)null,(Scheduler)null, - pool, acceptors, selectors, + ServerConnector connector = new ServerConnector(server, null, null, pool, acceptors, selectors, AbstractConnectionFactory.getFactories(sslContextFactory,httpConnectionFactory)); SecureRequestCustomizer secureRequestCustomer = new SecureRequestCustomizer(); secureRequestCustomer.setSslSessionAttribute("SSL_SESSION"); @@ -312,8 +309,8 @@ public class ThreadStarvationTest // Read Response long bodyCount = 0; long len; - - byte buf[] = new byte[1024]; + + byte[] buf = new byte[1024]; try { @@ -343,7 +340,7 @@ public class ThreadStarvationTest for (Future responseFut : responses) { Long bodyCount = responseFut.get(); - assertThat(bodyCount.longValue(), is(expected)); + assertThat(bodyCount, is(expected)); } } finally diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerCollectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerCollectionTest.java index b0eda81f5c3..d6b04538afe 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerCollectionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerCollectionTest.java @@ -18,16 +18,6 @@ package org.eclipse.jetty.server.handler; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - import java.io.IOException; import javax.servlet.AsyncContext; @@ -41,9 +31,18 @@ import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.hamcrest.Matchers; - import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +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.junit.jupiter.api.Assertions.fail; + public class ContextHandlerCollectionTest { @Test @@ -214,7 +213,7 @@ public class ContextHandlerCollectionTest IsHandledHandler handler = (IsHandledHandler)context.getHandler(); context.setVirtualHosts(contextHosts); - // trigger this manually; it's supposed to be called when adding the handler + // trigger this manually handlerCollection.mapContexts(); for(String host : requestHosts) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java index dff21b14281..80d0517f6e6 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java @@ -18,17 +18,15 @@ package org.eclipse.jetty.server.handler; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; - import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,11 +35,20 @@ import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.resource.Resource; import org.hamcrest.Matchers; - import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +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; + public class ContextHandlerTest { @Test @@ -717,6 +724,29 @@ public class ContextHandlerTest } } + @Test + public void testClassPath_WithSpaces() throws IOException + { + ContextHandler handler = new ContextHandler(); + handler.setServer(new Server()); + handler.setContextPath("/"); + + Path baseDir = MavenTestingUtils.getTargetTestingPath("testClassPath_WithSpaces"); + FS.ensureEmpty(baseDir); + + Path spacey = baseDir.resolve("and extra directory"); + FS.ensureEmpty(spacey); + + Path jar = spacey.resolve("empty.jar"); + FS.touch(jar); + + URLClassLoader cl = new URLClassLoader(new URL[]{jar.toUri().toURL()}); + handler.setClassLoader(cl); + + String classpath = handler.getClassPath(); + assertThat("classpath", classpath, containsString(jar.toString())); + } + private void checkResourcePathsForExampleWebApp(String root) throws IOException { File testDirectory = setupTestDirectory(); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DebugHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DebugHandlerTest.java index 54d0e7a6608..f5eacc981cc 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DebugHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DebugHandlerTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.server.handler; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -31,14 +26,11 @@ import java.net.HttpURLConnection; import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.KeyStore; -import java.util.concurrent.Executor; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManagerFactory; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -53,21 +45,18 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.eclipse.jetty.util.thread.Scheduler; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + public class DebugHandlerTest { - public final static HostnameVerifier __hostnameverifier = new HostnameVerifier() - { - @Override - public boolean verify(String hostname, SSLSession session) - { - return true; - } - }; + public final static HostnameVerifier __hostnameverifier = (hostname, session) -> true; private SSLContext sslContext; private Server server; @@ -77,7 +66,6 @@ public class DebugHandlerTest private DebugHandler debugHandler; private ByteArrayOutputStream capturedLog; - @SuppressWarnings("deprecation") @BeforeEach public void startServer() throws Exception { @@ -88,16 +76,14 @@ public class DebugHandlerTest server.addConnector(httpConnector); File keystorePath = MavenTestingUtils.getTestResourceFile("keystore"); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystorePath.getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); sslContextFactory.setTrustStorePath(keystorePath.getAbsolutePath()); sslContextFactory.setTrustStorePassword("storepwd"); ByteBufferPool pool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged()); - ServerConnector sslConnector = new ServerConnector(server, - (Executor)null, - (Scheduler)null, pool, 1, 1, + ServerConnector sslConnector = new ServerConnector(server, null, null, pool, 1, 1, AbstractConnectionFactory.getFactories(sslContextFactory,new HttpConnectionFactory())); server.addConnector(sslConnector); @@ -108,7 +94,7 @@ public class DebugHandlerTest debugHandler.setHandler(new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); response.setStatus(HttpStatus.OK_200); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java index 835aa0321ee..c0690b50859 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DefaultHandlerTest.java @@ -89,9 +89,9 @@ public class DefaultHandlerTest HttpTester.Response response = HttpTester.parseResponse(input); assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); - assertEquals("text/html;charset=ISO-8859-1", response.get(HttpHeader.CONTENT_TYPE)); + assertEquals("text/html;charset=UTF-8", response.get(HttpHeader.CONTENT_TYPE)); - String content = new String(response.getContentBytes(),StandardCharsets.ISO_8859_1); + String content = new String(response.getContentBytes(),StandardCharsets.UTF_8); assertThat(content,containsString("Contexts known to this server are:")); assertThat(content,containsString("/foo")); assertThat(content,containsString("/bar")); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java new file mode 100644 index 00000000000..705915d8b9f --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java @@ -0,0 +1,174 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.stream.Stream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class InetAccessHandlerTest +{ + private static Server _server; + private static ServerConnector _connector; + private static InetAccessHandler _handler; + + @BeforeAll + public static void setUp() throws Exception + { + _server = new Server(); + _connector = new ServerConnector(_server); + _connector.setName("http"); + _server.setConnectors(new Connector[] + { _connector }); + + _handler = new InetAccessHandler(); + _handler.setHandler(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + baseRequest.setHandled(true); + response.setStatus(HttpStatus.OK_200); + } + }); + _server.setHandler(_handler); + _server.start(); + } + + @AfterAll + public static void tearDown() throws Exception + { + _server.stop(); + } + + @ParameterizedTest + @MethodSource("data") + public void testHandler(String include, String exclude, String includeConnectors, String excludeConnectors, String code) + throws Exception + { + _handler.clear(); + for (String inc : include.split(";", -1)) + { + if (inc.length() > 0) + { + _handler.include(inc); + } + } + for (String exc : exclude.split(";", -1)) + { + if (exc.length() > 0) + { + _handler.exclude(exc); + } + } + for (String inc : includeConnectors.split(";", -1)) + { + if (inc.length() > 0) + { + _handler.includeConnector(inc); + } + } + for (String exc : excludeConnectors.split(";", -1)) + { + if (exc.length() > 0) + { + _handler.excludeConnector(exc); + } + } + + try (Socket socket = new Socket("127.0.0.1", _connector.getLocalPort());) + { + socket.setSoTimeout(5000); + + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setURI("/path"); + request.setHeader("Host","127.0.0.1"); + request.setVersion(HttpVersion.HTTP_1_0); + + ByteBuffer output = request.generate(); + socket.getOutputStream().write(output.array(),output.arrayOffset()+output.position(),output.remaining()); + HttpTester.Input input = HttpTester.from(socket.getInputStream()); + HttpTester.Response response = HttpTester.parseResponse(input); + Object[] params = new Object[] + { "Request WBHUC", include, exclude, includeConnectors, excludeConnectors, code, "Response", response.getStatus() }; + assertEquals(Integer.parseInt(code), response.getStatus(), Arrays.deepToString(params)); + } + } + + public static Stream data() + { + Object[][] data = new Object[][] + { + // Empty lists + { "", "", "", "", "200" }, + + // test simple filters + { "127.0.0.1", "", "", "", "200" }, + { "127.0.0.1-127.0.0.254", "", "", "", "200" }, + { "192.0.0.1", "", "", "", "403" }, + { "192.0.0.1-192.0.0.254", "", "", "", "403" }, + + // test connector name filters + { "127.0.0.1", "", "http", "", "200" }, + { "127.0.0.1-127.0.0.254", "", "http", "", "200" }, + { "192.0.0.1", "", "http", "", "403" }, + { "192.0.0.1-192.0.0.254", "", "http", "", "403" }, + + { "127.0.0.1", "", "nothttp", "", "403" }, + { "127.0.0.1-127.0.0.254", "", "nothttp", "", "403" }, + { "192.0.0.1", "", "nothttp", "", "403" }, + { "192.0.0.1-192.0.0.254", "", "nothttp", "", "403" }, + + { "127.0.0.1", "", "", "http", "403" }, + { "127.0.0.1-127.0.0.254", "", "", "http", "403" }, + { "192.0.0.1", "", "", "http", "403" }, + { "192.0.0.1-192.0.0.254", "", "", "http", "403" }, + + { "127.0.0.1", "", "", "nothttp", "200" }, + { "127.0.0.1-127.0.0.254", "", "", "nothttp", "200" }, + { "192.0.0.1", "", "", "nothttp", "403" }, + { "192.0.0.1-192.0.0.254", "", "", "nothttp", "403" }, + + }; + return Arrays.asList(data).stream().map(Arguments::of); + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java index bc7a6c24b1e..1ca0baa01b7 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ResourceHandlerTest.java @@ -167,7 +167,7 @@ public class ResourceHandlerTest _local.getResponse("GET /resource/ HTTP/1.0\r\n\r\n")); assertThat(response.getStatus(),equalTo(200)); assertThat(response.getContent(),containsString("jetty-dir.css")); - assertThat(response.getContent(),containsString("

    Directory: /resource/")); + assertThat(response.getContent(),containsString("Directory: /resource/")); assertThat(response.getContent(),containsString("big.txt")); assertThat(response.getContent(),containsString("bigger.txt")); assertThat(response.getContent(),containsString("directory")); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java index f95c4d43fb6..3497df8d094 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/SecuredRedirectHandlerTest.java @@ -18,10 +18,6 @@ package org.eclipse.jetty.server.handler; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -55,6 +51,10 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + public class SecuredRedirectHandlerTest { private static Server server; @@ -68,7 +68,7 @@ public class SecuredRedirectHandlerTest { // Setup SSL File keystore = MavenTestingUtils.getTestResourceFile("keystore"); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystore.getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLCloseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLCloseTest.java index 04033335e28..db5ef241ac3 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLCloseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLCloseTest.java @@ -27,7 +27,6 @@ import java.net.Socket; import java.nio.charset.StandardCharsets; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,7 +36,6 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.Test; @@ -48,7 +46,7 @@ public class SSLCloseTest public void testClose() throws Exception { File keystore = MavenTestingUtils.getTestResourceFile("keystore"); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStoreResource(Resource.newResource(keystore)); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java index 4f2b68aa842..208821488c9 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java @@ -23,12 +23,6 @@ package org.eclipse.jetty.server.ssl; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -63,6 +57,12 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + /** * */ @@ -109,7 +109,7 @@ public class SSLEngineTest public void startServer() throws Exception { String keystore = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath(); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystore); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLReadEOFAfterResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLReadEOFAfterResponseTest.java index e6d027179a2..691b34e1cab 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLReadEOFAfterResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLReadEOFAfterResponseTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.ssl; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -30,7 +27,6 @@ import java.net.Socket; import java.nio.charset.StandardCharsets; import javax.net.ssl.SSLContext; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -46,6 +42,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.JRE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + // Only in JDK 11 is possible to use SSLSocket.shutdownOutput(). @DisabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10}) public class SSLReadEOFAfterResponseTest @@ -54,7 +53,7 @@ public class SSLReadEOFAfterResponseTest public void testReadEOFAfterResponse() throws Exception { File keystore = MavenTestingUtils.getTestResourceFile("keystore"); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStoreResource(Resource.newResource(keystore)); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); @@ -70,7 +69,7 @@ public class SSLReadEOFAfterResponseTest server.setHandler(new AbstractHandler.ErrorDispatchHandler() { @Override - protected void doNonErrorHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + protected void doNonErrorHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { // First: read the whole content. InputStream input = request.getInputStream(); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLSelectChannelConnectorLoadTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLSelectChannelConnectorLoadTest.java index d1f68b93cd9..f5db817e03d 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLSelectChannelConnectorLoadTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLSelectChannelConnectorLoadTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.ssl; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; @@ -53,6 +50,9 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + public class SSLSelectChannelConnectorLoadTest { private static Server server; @@ -63,7 +63,7 @@ public class SSLSelectChannelConnectorLoadTest public static void startServer() throws Exception { String keystorePath = System.getProperty("basedir", ".") + "/src/test/resources/keystore"; - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystorePath); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SelectChannelServerSslTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SelectChannelServerSslTest.java index 7c1d0466aca..d81bddc732d 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SelectChannelServerSslTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SelectChannelServerSslTest.java @@ -18,14 +18,6 @@ package org.eclipse.jetty.server.ssl; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isEmptyOrNullString; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.condition.OS.WINDOWS; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -37,7 +29,6 @@ import java.security.KeyStore; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; - import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; @@ -62,11 +53,19 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.condition.DisabledOnOs; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.condition.OS.WINDOWS; + /** * HttpServer Tester. */ @@ -83,7 +82,7 @@ public class SelectChannelServerSslTest extends HttpServerTestBase public void init() throws Exception { String keystorePath = MavenTestingUtils.getTestResourcePath("keystore").toString(); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystorePath); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); @@ -270,19 +269,19 @@ public class SelectChannelServerSslTest extends HttpServerTestBase Matcher matcher=Pattern.compile("cipher_suite='([^']*)'").matcher(response); matcher.find(); - assertThat(matcher.group(1), Matchers.allOf(not(isEmptyOrNullString()),not(is("null")))); + assertThat(matcher.group(1), Matchers.allOf(not(is(emptyOrNullString()))),not(is("null"))); matcher=Pattern.compile("key_size='([^']*)'").matcher(response); matcher.find(); - assertThat(matcher.group(1), Matchers.allOf(not(isEmptyOrNullString()),not(is("null")))); + assertThat(matcher.group(1), Matchers.allOf(not(is(emptyOrNullString())),not(is("null")))); matcher=Pattern.compile("ssl_session_id='([^']*)'").matcher(response); matcher.find(); - assertThat(matcher.group(1), Matchers.allOf(not(isEmptyOrNullString()),not(is("null")))); + assertThat(matcher.group(1), Matchers.allOf(not(is(emptyOrNullString())),not(is("null")))); matcher=Pattern.compile("ssl_session='([^']*)'").matcher(response); matcher.find(); - assertThat(matcher.group(1), Matchers.allOf(not(isEmptyOrNullString()),not(is("null")))); + assertThat(matcher.group(1), Matchers.allOf(not(is(emptyOrNullString())),not(is("null")))); } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SlowClientsTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SlowClientsTest.java index f66fa3be0be..8536e975271 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SlowClientsTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SlowClientsTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.server.ssl; -import static java.time.Duration.ofSeconds; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -51,6 +49,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import static java.time.Duration.ofSeconds; + @Tag("Unstable") @Disabled public class SlowClientsTest @@ -61,7 +61,7 @@ public class SlowClientsTest public void testSlowClientsWithSmallThreadPool() throws Exception { File keystore = MavenTestingUtils.getTestResourceFile("keystore"); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystore.getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java index ec912540f29..c05d0b614d7 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java @@ -118,7 +118,7 @@ public class SniSslConnectionFactoryTest if (!keystoreFile.exists()) throw new FileNotFoundException(keystoreFile.getAbsolutePath()); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath()); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); @@ -224,7 +224,7 @@ public class SniSslConnectionFactoryTest { start("src/test/resources/keystore_sni.p12"); - SslContextFactory clientContextFactory = new SslContextFactory(true); + SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory(); try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port)) @@ -283,7 +283,7 @@ public class SniSslConnectionFactoryTest { start("src/test/resources/keystore_sni.p12"); - SslContextFactory clientContextFactory = new SslContextFactory(true); + SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory(); try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port)) @@ -362,7 +362,7 @@ public class SniSslConnectionFactoryTest private String getResponse(String sniHost, String reqHost, String cn) throws Exception { - SslContextFactory clientContextFactory = new SslContextFactory(true); + SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory(); try (SSLSocket sslSocket = (SSLSocket)factory.createSocket("127.0.0.1", _port)) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslConnectionFactoryTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslConnectionFactoryTest.java index 03378b1864a..ac10cfc2313 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslConnectionFactoryTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslConnectionFactoryTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.server.ssl; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -59,10 +54,14 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class SslConnectionFactoryTest { private Server _server; @@ -87,7 +86,7 @@ public class SslConnectionFactoryTest https_config.addCustomizer(new SecureRequestCustomizer()); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath()); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); @@ -210,7 +209,7 @@ public class SslConnectionFactoryTest private String getResponse(String sniHost, String reqHost, String cn) throws Exception { - SslContextFactory clientContextFactory = new SslContextFactory(true); + SslContextFactory clientContextFactory = new SslContextFactory.Client(true); clientContextFactory.start(); SSLSocketFactory factory = clientContextFactory.getSslContext().getSocketFactory(); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java index 4af5511ed2e..e46330e4d93 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslContextFactoryReloadTest.java @@ -64,14 +64,14 @@ public class SslContextFactoryReloadTest public static final String KEYSTORE_2 = "src/test/resources/reload_keystore_2.jks"; private Server server; - private SslContextFactory sslContextFactory; + private SslContextFactory.Server sslContextFactory; private ServerConnector connector; private void start(Handler handler) throws Exception { server = new Server(); - sslContextFactory = new SslContextFactory(); + sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(KEYSTORE_1); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyStoreType("JKS"); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslSelectChannelTimeoutTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslSelectChannelTimeoutTest.java index d71c89d9e82..25a63f4d107 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslSelectChannelTimeoutTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslSelectChannelTimeoutTest.java @@ -45,7 +45,7 @@ public class SslSelectChannelTimeoutTest extends ConnectorTimeoutTest public void init() throws Exception { String keystorePath = System.getProperty("basedir",".") + "/src/test/resources/keystore"; - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystorePath); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); @@ -64,7 +64,5 @@ public class SslSelectChannelTimeoutTest extends ConnectorTimeoutTest trustManagerFactory.init(keystore); __sslContext = SSLContext.getInstance("SSL"); __sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - } - } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslUploadTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslUploadTest.java index 707e6f8c874..6d64933a3aa 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslUploadTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SslUploadTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.ssl; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -49,6 +46,9 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** */ public class SslUploadTest @@ -62,7 +62,7 @@ public class SslUploadTest { File keystore = MavenTestingUtils.getTestResourceFile("keystore"); - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(keystore.getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); diff --git a/jetty-servlet/pom.xml b/jetty-servlet/pom.xml index 795784d0c78..bf018c32c37 100644 --- a/jetty-servlet/pom.xml +++ b/jetty-servlet/pom.xml @@ -65,6 +65,12 @@ ${project.version} test + + org.eclipse.jetty + jetty-client + ${project.version} + test + diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java index 69563a9c9f8..d7f87544921 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java @@ -23,7 +23,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; - import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.UnavailableException; @@ -48,8 +47,7 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; - -/** +/** * The default servlet. *

    * This servlet, normally mapped to /, provides the handling for static @@ -79,9 +77,9 @@ import org.eclipse.jetty.util.resource.ResourceFactory; * * gzip If set to true, then static content will be served as * gzip content encoded if a matching resource is - * found ending with ".gz" (default false) + * found ending with ".gz" (default false) * (deprecated: use precompressed) - * + * * precompressed If set to a comma separated list of encoding types (that may be * listed in a requests Accept-Encoding header) to file * extension mappings to look for and serve. For example: @@ -131,10 +129,10 @@ import org.eclipse.jetty.util.resource.ResourceFactory; public class DefaultServlet extends HttpServlet implements ResourceFactory, WelcomeFactory { public static final String CONTEXT_INIT = "org.eclipse.jetty.servlet.Default."; - + private static final Logger LOG = Log.getLogger(DefaultServlet.class); - private static final long serialVersionUID = 4930458713846881193L; + private static final long serialVersionUID = 4930458713846881193L; private final ResourceService _resourceService; private ServletContext _servletContext; @@ -165,7 +163,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc { this(new ResourceService()); } - + /* ------------------------------------------------------------ */ @Override public void init() @@ -186,7 +184,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc _resourceService.setPrecompressedFormats(parsePrecompressedFormats(getInitParameter("precompressed"), getInitBoolean("gzip", false))); _resourceService.setPathInfoOnly(getInitBoolean("pathInfoOnly",_resourceService.isPathInfoOnly())); _resourceService.setEtags(getInitBoolean("etags",_resourceService.isEtags())); - + if ("exact".equals(getInitParameter("welcomeServlets"))) { _welcomeExactServlets=true; @@ -242,8 +240,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc String cc=getInitParameter("cacheControl"); if (cc!=null) _resourceService.setCacheControl(new PreEncodedHttpField(HttpHeader.CACHE_CONTROL,cc)); - - + String resourceCache = getInitParameter("resourceCache"); int max_cache_size=getInitInt("maxCacheSize", -2); int max_cached_file_size=getInitInt("maxCachedFileSize", -2); @@ -286,7 +283,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc } _resourceService.setContentFactory(contentFactory); _resourceService.setWelcomeFactory(this); - + List gzip_equivalent_file_extensions = new ArrayList(); String otherGzipExtensions = getInitParameter("otherGzipFileExtensions"); if (otherGzipExtensions != null) diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java index c6cc1da109b..ae84c52e3a4 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java @@ -114,7 +114,7 @@ public class FilterMapping implements Dumpable /* ------------------------------------------------------------ */ private int _dispatches=DEFAULT; private String _filterName; - private transient FilterHolder _holder; + private FilterHolder _holder; private String[] _pathSpecs; private String[] _servletNames; @@ -148,8 +148,11 @@ public class FilterMapping implements Dumpable */ boolean appliesTo(int type) { + FilterHolder holder = _holder; + if (_holder==null) + return false; if (_dispatches==0) - return type==REQUEST || type==ASYNC && _holder.isAsyncSupported(); + return type==REQUEST || type==ASYNC && holder.isAsyncSupported(); return (_dispatches&type)!=0; } diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java index e78663f4aa9..d3a82545c77 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java @@ -613,7 +613,7 @@ public class ServletHandler extends ScopedHandler FilterChain chain = null; if (_filterChainsCached) { - if (filters.size() > 0) + if (!filters.isEmpty()) chain = newCachedChain(filters, servletHolder); final Map cache=_chainCache[dispatch]; @@ -637,7 +637,7 @@ public class ServletHandler extends ScopedHandler cache.put(key,chain); lru.add(key); } - else if (filters.size() > 0) + else if (!filters.isEmpty()) chain = new Chain(baseRequest,filters, servletHolder); return chain; @@ -1534,7 +1534,7 @@ public class ServletHandler extends ScopedHandler */ protected CachedChain(List filters, ServletHolder servletHolder) { - if (filters.size()>0) + if (!filters.isEmpty()) { _filterHolder=filters.get(0); filters.remove(0); diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java index 87a13e21667..1b192237ea2 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java @@ -748,11 +748,14 @@ public class ServletHolder extends Holder implements UserIdentity.Scope //cleaned up correctly if (((Registration)getRegistration()).getMultipartConfig() != null) { + if (LOG.isDebugEnabled()) + LOG.debug("multipart cleanup listener added for {}", this); + //Register a listener to delete tmp files that are created as a result of this //servlet calling Request.getPart() or Request.getParts() - ContextHandler ch = ContextHandler.getContextHandler(getServletHandler().getServletContext()); - ch.addEventListener(MultiPartCleanerListener.INSTANCE); + if(!Arrays.asList(ch.getEventListeners()).contains(MultiPartCleanerListener.INSTANCE)) + ch.addEventListener(MultiPartCleanerListener.INSTANCE); } } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java index de9802413ad..d7530581977 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncContextTest.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.servlet; import java.io.IOException; import java.util.concurrent.TimeUnit; - import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; @@ -48,9 +47,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertTrue; /** diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ComplianceViolations2616Test.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ComplianceViolations2616Test.java index 47863287309..d205c7f158b 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ComplianceViolations2616Test.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ComplianceViolations2616Test.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.servlet; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; - import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; @@ -48,6 +45,9 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + public class ComplianceViolations2616Test { private static Server server; @@ -183,7 +183,7 @@ public class ComplianceViolations2616Test String response = connector.getResponse(req1.toString()); assertThat("Response status", response, containsString("HTTP/1.1 200")); - assertThat("Response headers", response, containsString("X-Http-Violation-0: No line Folding")); + assertThat("Response headers", response, containsString("X-Http-Violation-0: Line Folding not supported")); assertThat("Response body", response, containsString("[Name] = [Some Value]")); } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java index d4cae832321..152a840ed90 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/CustomRequestLogTest.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.servlet; import java.io.IOException; +import java.io.File; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; @@ -84,7 +85,7 @@ public class CustomRequestLogTest _connector.getResponse("GET /context/servlet/info HTTP/1.0\n\n"); String log = _entries.poll(5,TimeUnit.SECONDS); - assertThat(log, is("Filename: " + _tmpDir + "/servlet/info")); + assertThat(log, is("Filename: " + _tmpDir + File.separator + "servlet" + File.separator + "info")); } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultHandlerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultHandlerTest.java new file mode 100644 index 00000000000..aa8993854ea --- /dev/null +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultHandlerTest.java @@ -0,0 +1,109 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.servlet; + +import java.io.File; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.DefaultHandler; +import org.eclipse.jetty.server.handler.HandlerList; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.resource.PathResource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +public class DefaultHandlerTest +{ + private Server server; + private LocalConnector localConnector; + private File baseA; + private File baseFoo; + + @BeforeEach + public void startServer() throws Exception + { + server = new Server(); + + localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + File docRoot = MavenTestingUtils.getTargetTestingDir(DefaultHandlerTest.class.getName()); + FS.ensureDirExists(docRoot); + + baseA = new File(docRoot, "baseA"); + FS.ensureDirExists(baseA); + + baseFoo = new File(docRoot, "baseFoo"); + FS.ensureDirExists(baseFoo); + + ServletContextHandler contextA = new ServletContextHandler(); + contextA.setContextPath("/a"); + contextA.setBaseResource(new PathResource(baseA)); + + ServletContextHandler contextFoo = new ServletContextHandler(); + contextFoo.setContextPath("/foo"); + contextFoo.setBaseResource(new PathResource(baseFoo)); + + HandlerList handlers = new HandlerList(); + handlers.addHandler(contextA); + handlers.addHandler(contextFoo); + handlers.addHandler(new DefaultHandler()); + + server.setHandler(handlers); + server.start(); + } + + @AfterEach + public void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testNotRevealBaseResource() throws Exception + { + StringBuilder req = new StringBuilder(); + req.append("GET / HTTP/1.0\r\n"); + req.append("\r\n"); + + String rawResponse = localConnector.getResponse(req.toString()); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404)); + + String body = response.getContent(); + assertThat(body, containsString("No context on this server matched or handled this request")); + assertThat(body, containsString("Contexts known to this server are")); + assertThat(body, containsString("")); + assertThat(body, containsString("")); + assertThat(body, not(containsString(baseA.toString()))); + assertThat(body, not(containsString(baseFoo.toString()))); + } +} diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java index 7d7c2b163a7..a759c5237b1 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/DefaultServletTest.java @@ -21,6 +21,8 @@ package org.eclipse.jetty.servlet; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -33,7 +35,6 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; - import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -52,12 +53,14 @@ import org.eclipse.jetty.http.tools.HttpTester; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.ResourceContentFactory; +import org.eclipse.jetty.server.ResourceService; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; import org.junit.jupiter.api.AfterEach; @@ -81,6 +84,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assumptions.assumeTrue; @ExtendWith(WorkDirExtension.class) @@ -90,6 +94,9 @@ public class DefaultServletTest public Path docRoot; + // The name of the odd-jar used for testing "jar:file://" based resource access. + private static final String ODD_JAR = "jar-resource-odd.jar"; + private Server server; private LocalConnector connector; private ServletContextHandler context; @@ -105,10 +112,17 @@ public class DefaultServletTest connector = new LocalConnector(server); connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendServerVersion(false); + File extraJarResources = MavenTestingUtils.getTestResourceFile(ODD_JAR); + URL urls[] = new URL[] { extraJarResources.toURI().toURL() }; + + ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader(); + URLClassLoader extraClassLoader = new URLClassLoader(urls, parentClassLoader); + context = new ServletContextHandler(); context.setBaseResource(new PathResource(docRoot)); context.setContextPath("/context"); context.setWelcomeFiles(new String[]{"index.html", "index.jsp", "index.htm"}); + context.setClassLoader(extraClassLoader); server.setHandler(context); server.addConnector(connector); @@ -157,10 +171,14 @@ public class DefaultServletTest defholder.setInitParameter("gzip", "false"); /* create some content in the docroot */ - FS.ensureDirExists(docRoot.resolve("one")); + Path one = docRoot.resolve("one"); + FS.ensureDirExists(one); FS.ensureDirExists(docRoot.resolve("two")); FS.ensureDirExists(docRoot.resolve("three")); + Path alert = one.resolve("onmouseclick='alert(oops)'"); + FS.touch(alert); + /* * Intentionally bad request URI. Sending a non-encoded URI with typically * encoded characters '<', '>', and '"'. @@ -172,6 +190,16 @@ public class DefaultServletTest String body = response.getContent(); assertThat(body, not(containsString("">Link @@ -599,21 +821,21 @@ public abstract class Resource implements ResourceFactory, Closeable char c=raw.charAt(i); switch(c) { - case '"': - buf.append("%22"); - continue; - case '\'': - buf.append("%27"); - continue; - case '<': - buf.append("%3C"); - continue; - case '>': - buf.append("%3E"); - continue; - default: - buf.append(c); - continue; + case '"': + buf.append("%22"); + break; + case '\'': + buf.append("%27"); + break; + case '<': + buf.append("%3C"); + break; + case '>': + buf.append("%3E"); + break; + default: + buf.append(c); + break; } } @@ -627,7 +849,7 @@ public abstract class Resource implements ResourceFactory, Closeable /* ------------------------------------------------------------ */ /** - * @param out the output stream to write to + * @param out the output stream to write to * @param start First byte to write * @param count Bytes to write or -1 for all of them. * @throws IOException if unable to copy the Resource to the output @@ -650,7 +872,7 @@ public abstract class Resource implements ResourceFactory, Closeable * Copy the Resource to the new destination file. *

    * Will not replace existing destination file. - * + * * @param destination the destination file to create * @throws IOException if unable to copy the resource */ @@ -659,7 +881,7 @@ public abstract class Resource implements ResourceFactory, Closeable { if (destination.exists()) throw new IllegalArgumentException(destination + " exists"); - + try (OutputStream out = new FileOutputStream(destination)) { writeTo(out,0,-1); @@ -669,37 +891,42 @@ public abstract class Resource implements ResourceFactory, Closeable /* ------------------------------------------------------------ */ /** * Generate a weak ETag reference for this Resource. - * + * * @return the weak ETag reference for this resource. */ public String getWeakETag() { return getWeakETag(""); } - + public String getWeakETag(String suffix) { - try + StringBuilder b = new StringBuilder(32); + b.append("W/\""); + + String name=getName(); + int length=name.length(); + long lhash=0; + for (int i=0; i= 0; i--) { - StringBuilder b = new StringBuilder(32); - b.append("W/\""); - - String name=getName(); - int length=name.length(); - long lhash=0; - for (int i=0; i>= 8; } + return result; } /* ------------------------------------------------------------ */ diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollators.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollators.java new file mode 100644 index 00000000000..9c31cb752b4 --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollators.java @@ -0,0 +1,91 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.resource; + +import java.text.Collator; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; + +public class ResourceCollators +{ + private static Comparator BY_NAME_ASCENDING = + new Comparator<>() + { + private final Collator collator = Collator.getInstance(Locale.ENGLISH); + + @Override + public int compare(Resource o1, Resource o2) + { + return collator.compare(o1.getName(), o2.getName()); + } + }; + + private static Comparator BY_NAME_DESCENDING = + Collections.reverseOrder(BY_NAME_ASCENDING); + + private static Comparator BY_LAST_MODIFIED_ASCENDING = + Comparator.comparingLong(Resource::lastModified); + + private static Comparator BY_LAST_MODIFIED_DESCENDING = + Collections.reverseOrder(BY_LAST_MODIFIED_ASCENDING); + + private static Comparator BY_SIZE_ASCENDING = + Comparator.comparingLong(Resource::length); + + private static Comparator BY_SIZE_DESCENDING = + Collections.reverseOrder(BY_SIZE_ASCENDING); + + + public static Comparator byLastModified(boolean sortOrderAscending) + { + if (sortOrderAscending) + { + return BY_LAST_MODIFIED_ASCENDING; + } + else + { + return BY_LAST_MODIFIED_DESCENDING; + } + } + + public static Comparator byName(boolean sortOrderAscending) + { + if (sortOrderAscending) + { + return BY_NAME_ASCENDING; + } + else + { + return BY_NAME_DESCENDING; + } + } + + public static Comparator bySize(boolean sortOrderAscending) + { + if (sortOrderAscending) + { + return BY_SIZE_ASCENDING; + } + else + { + return BY_SIZE_DESCENDING; + } + } +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index 9fbea8c00f6..a37b1ef7ffa 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -22,13 +22,17 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.security.InvalidAlgorithmParameterException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.Security; import java.security.cert.CRL; import java.security.cert.CertStore; +import java.security.cert.CertStoreParameters; import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.PKIXBuilderParameters; import java.security.cert.PKIXCertPathChecker; @@ -86,13 +90,13 @@ import org.eclipse.jetty.util.security.CertificateValidator; import org.eclipse.jetty.util.security.Password; /** - * SslContextFactory is used to configure SSL connectors - * as well as HttpClient. It holds all SSL parameters and - * creates SSL context based on these parameters to be - * used by the SSL connectors. + *

    SslContextFactory is used to configure SSL parameters + * to be used by server and client connectors.

    + *

    Use {@link Server} to configure server-side connectors, + * and {@link Client} to configure HTTP or WebSocket clients.

    */ @ManagedObject -public class SslContextFactory extends AbstractLifeCycle implements Dumpable +public abstract class SslContextFactory extends AbstractLifeCycle implements Dumpable { public final static TrustManager[] TRUST_ALL_CERTS = new X509TrustManager[]{new X509TrustManager() { @@ -116,13 +120,9 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable private static final Logger LOG = Log.getLogger(SslContextFactory.class); private static final Logger LOG_CONFIG = LOG.getLogger("SslContextFactoryConfig"); - public static final String DEFAULT_KEYMANAGERFACTORY_ALGORITHM = - (Security.getProperty("ssl.KeyManagerFactory.algorithm") == null ? - KeyManagerFactory.getDefaultAlgorithm() : Security.getProperty("ssl.KeyManagerFactory.algorithm")); + public static final String DEFAULT_KEYMANAGERFACTORY_ALGORITHM = KeyManagerFactory.getDefaultAlgorithm(); - public static final String DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM = - (Security.getProperty("ssl.TrustManagerFactory.algorithm") == null ? - TrustManagerFactory.getDefaultAlgorithm() : Security.getProperty("ssl.TrustManagerFactory.algorithm")); + public static final String DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM = TrustManagerFactory.getDefaultAlgorithm(); /** String name of key password property. */ public static final String KEYPASSWORD_PROPERTY = "org.eclipse.jetty.ssl.keypassword"; @@ -166,8 +166,6 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable private Resource _trustStoreResource; private String _trustStoreProvider; private String _trustStoreType; - private boolean _needClientAuth = false; - private boolean _wantClientAuth = false; private Password _keyStorePassword; private Password _keyManagerPassword; private Password _trustStorePassword; @@ -198,44 +196,24 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable private HostnameVerifier _hostnameVerifier; /** - * Construct an instance of SslContextFactory - * Default constructor for use in XmlConfiguration files + * Construct an instance of SslContextFactory with the default configuration. */ - public SslContextFactory() + protected SslContextFactory() { this(false); } /** - * Construct an instance of SslContextFactory - * Default constructor for use in XmlConfiguration files + * Construct an instance of SslContextFactory that trusts all certificates * * @param trustAll whether to blindly trust all certificates * @see #setTrustAll(boolean) */ public SslContextFactory(boolean trustAll) - { - this(trustAll, null); - } - - /** - * Construct an instance of SslContextFactory - * - * @param keyStorePath default keystore location - */ - public SslContextFactory(String keyStorePath) - { - this(false, keyStorePath); - } - - private SslContextFactory(boolean trustAll, String keyStorePath) { setTrustAll(trustAll); setExcludeProtocols(DEFAULT_EXCLUDED_PROTOCOLS); setExcludeCipherSuites(DEFAULT_EXCLUDED_CIPHER_SUITES); - - if (keyStorePath != null) - setKeyStorePath(keyStorePath); } /** @@ -249,21 +227,33 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable { load(); } - - secureConfigurationCheck(); + checkConfiguration(); } - protected void secureConfigurationCheck() + protected void checkConfiguration() { - if (isTrustAll()) - LOG_CONFIG.warn("Trusting all certificates configured for {}",this); - if (getEndpointIdentificationAlgorithm()==null) - LOG_CONFIG.warn("No Client EndPointIdentificationAlgorithm configured for {}",this); - SSLEngine engine = _factory._context.createSSLEngine(); customize(engine); SSLParameters supported = engine.getSSLParameters(); + checkProtocols(supported); + checkCiphers(supported); + } + + protected void checkTrustAll() + { + if (isTrustAll()) + LOG_CONFIG.warn("Trusting all certificates configured for {}", this); + } + + protected void checkEndPointIdentificationAlgorithm() + { + if (getEndpointIdentificationAlgorithm() == null) + LOG_CONFIG.warn("No Client EndPointIdentificationAlgorithm configured for {}", this); + } + + protected void checkProtocols(SSLParameters supported) + { for (String protocol : supported.getProtocols()) { for (String excluded : DEFAULT_EXCLUDED_PROTOCOLS) @@ -272,7 +262,10 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable LOG_CONFIG.warn("Protocol {} not excluded for {}", protocol, this); } } + } + protected void checkCiphers(SSLParameters supported) + { for (String suite : supported.getCipherSuites()) { for (String excludedSuiteRegex : DEFAULT_EXCLUDED_CIPHER_SUITES) @@ -304,10 +297,8 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable trust_managers = TRUST_ALL_CERTS; } - String algorithm = getSecureRandomAlgorithm(); - SecureRandom secureRandom = algorithm == null ? null : SecureRandom.getInstance(algorithm); - context = _sslProvider == null ? SSLContext.getInstance(_sslProtocol) : SSLContext.getInstance(_sslProtocol, _sslProvider); - context.init(null, trust_managers, secureRandom); + context = getSSLContextInstance(); + context.init(null, trust_managers, getSecureRandomInstance()); } else { @@ -363,9 +354,8 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable TrustManager[] trustManagers = getTrustManagers(trustStore, crls); // Initialize context - SecureRandom secureRandom = (_secureRandomAlgorithm == null) ? null : SecureRandom.getInstance(_secureRandomAlgorithm); - context = _sslProvider == null ? SSLContext.getInstance(_sslProtocol) : SSLContext.getInstance(_sslProtocol, _sslProvider); - context.init(keyManagers, trustManagers, secureRandom); + context = getSSLContextInstance(); + context.init(keyManagers, trustManagers, getSecureRandomInstance()); } } @@ -417,9 +407,9 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable getExcludeCipherSuites(), getIncludeCipherSuites())); } - catch (NoSuchAlgorithmException ignore) + catch (NoSuchAlgorithmException x) { - LOG.ignore(ignore); + LOG.ignore(x); } } @@ -751,44 +741,6 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable _trustStoreType = trustStoreType; } - /** - * @return True if SSL needs client authentication. - * @see SSLEngine#getNeedClientAuth() - */ - @ManagedAttribute("Whether client authentication is needed") - public boolean getNeedClientAuth() - { - return _needClientAuth; - } - - /** - * @param needClientAuth True if SSL needs client authentication. - * @see SSLEngine#getNeedClientAuth() - */ - public void setNeedClientAuth(boolean needClientAuth) - { - _needClientAuth = needClientAuth; - } - - /** - * @return True if SSL wants client authentication. - * @see SSLEngine#getWantClientAuth() - */ - @ManagedAttribute("Whether client authentication is wanted") - public boolean getWantClientAuth() - { - return _wantClientAuth; - } - - /** - * @param wantClientAuth True if SSL wants client authentication. - * @see SSLEngine#getWantClientAuth() - */ - public void setWantClientAuth(boolean wantClientAuth) - { - _wantClientAuth = wantClientAuth; - } - /** * @return true if SSL certificate has to be validated */ @@ -858,8 +810,22 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable } /** - * @return The SSL provider name, which if set is passed to - * {@link SSLContext#getInstance(String, String)} + *

    + * Get the optional Security Provider name. + *

    + *

    + * Security Provider name used with: + *

    + *
      + *
    • {@link SecureRandom#getInstance(String, String)}
    • + *
    • {@link SSLContext#getInstance(String, String)}
    • + *
    • {@link TrustManagerFactory#getInstance(String, String)}
    • + *
    • {@link KeyManagerFactory#getInstance(String, String)}
    • + *
    • {@link CertStore#getInstance(String, CertStoreParameters, String)}
    • + *
    • {@link java.security.cert.CertificateFactory#getInstance(String, String)}
    • + *
    + * + * @return The optional Security Provider name. */ @ManagedAttribute("The provider name") public String getProvider() @@ -868,8 +834,22 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable } /** - * @param provider The SSL provider name, which if set is passed to - * {@link SSLContext#getInstance(String, String)} + *

    + * Set the optional Security Provider name. + *

    + *

    + * Security Provider name used with: + *

    + *
      + *
    • {@link SecureRandom#getInstance(String, String)}
    • + *
    • {@link SSLContext#getInstance(String, String)}
    • + *
    • {@link TrustManagerFactory#getInstance(String, String)}
    • + *
    • {@link KeyManagerFactory#getInstance(String, String)}
    • + *
    • {@link CertStore#getInstance(String, CertStoreParameters, String)}
    • + *
    • {@link java.security.cert.CertificateFactory#getInstance(String, String)}
    • + *
    + * + * @param provider The optional Security Provider name. */ public void setProvider(String provider) { @@ -1079,6 +1059,7 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable * Deployments can be vulnerable to a man-in-the-middle attack if a EndpointIndentificationAlgorithm * is not set. * @param endpointIdentificationAlgorithm Set the endpointIdentificationAlgorithm + * @see #setHostnameVerifier(HostnameVerifier) */ public void setEndpointIdentificationAlgorithm(String endpointIdentificationAlgorithm) { @@ -1150,7 +1131,7 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable if (keyStore != null) { - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(getKeyManagerFactoryAlgorithm()); + KeyManagerFactory keyManagerFactory = getKeyManagerFactoryInstance(); keyManagerFactory.init(keyStore, _keyManagerPassword == null ? (_keyStorePassword == null ? null : _keyStorePassword.toString().toCharArray()) : _keyManagerPassword.toString().toCharArray()); managers = keyManagerFactory.getKeyManagers(); @@ -1167,7 +1148,7 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable } // Is SNI needed to select a certificate? - if (!_certWilds.isEmpty() || _certHosts.size()>1 || _certHosts.size()==1 && _aliasX509.size()>1) + if (!_certWilds.isEmpty() || _certHosts.size()>1 || (_certHosts.size()==1 && _aliasX509.size()>1)) { for (int idx = 0; idx < managers.length; idx++) { @@ -1194,14 +1175,14 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable { PKIXBuilderParameters pbParams = newPKIXBuilderParameters(trustStore, crls); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(_trustManagerFactoryAlgorithm); + TrustManagerFactory trustManagerFactory = getTrustManagerFactoryInstance(); trustManagerFactory.init(new CertPathTrustManagerParameters(pbParams)); managers = trustManagerFactory.getTrustManagers(); } else { - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(_trustManagerFactoryAlgorithm); + TrustManagerFactory trustManagerFactory = getTrustManagerFactoryInstance(); trustManagerFactory.init(trustStore); managers = trustManagerFactory.getTrustManagers(); @@ -1226,7 +1207,7 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable if (crls != null && !crls.isEmpty()) { - pbParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(crls))); + pbParams.addCertStore(getCertStoreInstance(crls)); } if (_enableCRLDP) @@ -1642,6 +1623,145 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable return socket; } + protected CertificateFactory getCertificateFactoryInstance(String type) throws CertificateException + { + String provider = getProvider(); + + try + { + if (provider != null) + { + return CertificateFactory.getInstance(type, provider); + } + } + catch (Throwable cause) + { + LOG.info("Unable to get CertificateFactory instance for type [{}] on provider [{}], using default", type, provider); + if (LOG.isDebugEnabled()) + LOG.debug(cause); + } + + return CertificateFactory.getInstance(type); + } + + protected CertStore getCertStoreInstance(Collection crls) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException + { + String type = "Collection"; + String provider = getProvider(); + + try + { + if (provider != null) + { + return CertStore.getInstance(type, new CollectionCertStoreParameters(crls), provider); + } + } + catch (Throwable cause) + { + LOG.info("Unable to get CertStore instance for type [{}] on provider [{}], using default", type, provider); + if (LOG.isDebugEnabled()) + LOG.debug(cause); + } + + return CertStore.getInstance(type, new CollectionCertStoreParameters(crls)); + } + + protected KeyManagerFactory getKeyManagerFactoryInstance() throws NoSuchAlgorithmException + { + String algorithm = getKeyManagerFactoryAlgorithm(); + String provider = getProvider(); + + try + { + if (provider != null) + { + return KeyManagerFactory.getInstance(algorithm, provider); + } + } + catch (Throwable cause) + { + // fall back to non-provider option + LOG.info("Unable to get KeyManagerFactory instance for algorithm [{}] on provider [{}], using default", algorithm, provider); + if (LOG.isDebugEnabled()) + LOG.debug(cause); + } + + return KeyManagerFactory.getInstance(algorithm); + } + + protected SecureRandom getSecureRandomInstance() throws NoSuchAlgorithmException + { + String algorithm = getSecureRandomAlgorithm(); + + if (algorithm != null) + { + String provider = getProvider(); + + try + { + if (provider != null) + { + return SecureRandom.getInstance(algorithm, provider); + } + } + catch (Throwable cause) + { + LOG.info("Unable to get SecureRandom instance for algorithm [{}] on provider [{}], using default", algorithm, provider); + if (LOG.isDebugEnabled()) + LOG.debug(cause); + } + + return SecureRandom.getInstance(algorithm); + } + + return null; + } + + protected SSLContext getSSLContextInstance() throws NoSuchAlgorithmException + { + String protocol = getProtocol(); + String provider = getProvider(); + + try + { + if (provider != null) + { + return SSLContext.getInstance(protocol, provider); + } + } + catch (Throwable cause) + { + LOG.info("Unable to get SSLContext instance for protocol [{}] on provider [{}], using default", protocol, provider); + if (LOG.isDebugEnabled()) + LOG.debug(cause); + } + + return SSLContext.getInstance(protocol); + } + + protected TrustManagerFactory getTrustManagerFactoryInstance() throws NoSuchAlgorithmException + { + String algorithm = getTrustManagerFactoryAlgorithm(); + String provider = getProvider(); + try + { + if (provider != null) + { + return TrustManagerFactory.getInstance(algorithm, provider); + } + } + catch (Throwable cause) + { + LOG.info("Unable to get TrustManagerFactory instance for algorithm [{}] on provider [{}], using default", algorithm, provider); + if (LOG.isDebugEnabled()) + { + LOG.debug(cause); + } + } + + return TrustManagerFactory.getInstance(algorithm); + } + /** * Factory method for "scratch" {@link SSLEngine}s, usually only used for retrieving configuration * information such as the application buffer size or the list of protocols/ciphers. @@ -1731,10 +1851,14 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable sslParams.setCipherSuites(_selectedCipherSuites); if (_selectedProtocols != null) sslParams.setProtocols(_selectedProtocols); - if (getWantClientAuth()) - sslParams.setWantClientAuth(true); - if (getNeedClientAuth()) - sslParams.setNeedClientAuth(true); + if (this instanceof Server) + { + Server server = (Server)this; + if (server.getWantClientAuth()) + sslParams.setWantClientAuth(true); + if (server.getNeedClientAuth()) + sslParams.setNeedClientAuth(true); + } return sslParams; } @@ -1748,7 +1872,31 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable } } + /** + * Obtain the X509 Certificate Chain from the provided SSLSession using this + * SslContextFactory's optional Provider specific {@link CertificateFactory}. + * + * @param sslSession the session to use for active peer certificates + * @return the certificate chain + */ + public X509Certificate[] getX509CertChain(SSLSession sslSession) + { + return getX509CertChain(this, sslSession); + } + + /** + * Obtain the X509 Certificate Chain from the provided SSLSession using the + * default {@link CertificateFactory} behaviors + * + * @param sslSession the session to use for active peer certificates + * @return the certificate chain + */ public static X509Certificate[] getCertChain(SSLSession sslSession) + { + return getX509CertChain(null, sslSession); + } + + private static X509Certificate[] getX509CertChain(SslContextFactory sslContextFactory, SSLSession sslSession) { try { @@ -1759,10 +1907,20 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable int length = javaxCerts.length; X509Certificate[] javaCerts = new X509Certificate[length]; - java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory.getInstance("X.509"); + String type = "X.509"; + CertificateFactory cf; + if (sslContextFactory != null) + { + cf = sslContextFactory.getCertificateFactoryInstance(type); + } + else + { + cf = CertificateFactory.getInstance(type); + } + for (int i = 0; i < length; i++) { - byte bytes[] = javaxCerts[i].getEncoded(); + byte[] bytes = javaxCerts[i].getEncoded(); ByteArrayInputStream stream = new ByteArrayInputStream(bytes); javaCerts[i] = (X509Certificate)cf.generateCertificate(stream); } @@ -1923,4 +2081,74 @@ public class SslContextFactory extends AbstractLifeCycle implements Dumpable return _x509; } } + + public static class Client extends SslContextFactory + { + public Client() + { + this(false); + } + + public Client(boolean trustAll) + { + super(trustAll); + } + + @Override + protected void checkConfiguration() + { + checkTrustAll(); + checkEndPointIdentificationAlgorithm(); + super.checkConfiguration(); + } + } + + public static class Server extends SslContextFactory + { + private boolean _needClientAuth; + private boolean _wantClientAuth; + + public Server() + { + setEndpointIdentificationAlgorithm(null); + } + + /** + * @return True if SSL needs client authentication. + * @see SSLEngine#getNeedClientAuth() + */ + @ManagedAttribute("Whether client authentication is needed") + public boolean getNeedClientAuth() + { + return _needClientAuth; + } + + /** + * @param needClientAuth True if SSL needs client authentication. + * @see SSLEngine#getNeedClientAuth() + */ + public void setNeedClientAuth(boolean needClientAuth) + { + _needClientAuth = needClientAuth; + } + + /** + * @return True if SSL wants client authentication. + * @see SSLEngine#getWantClientAuth() + */ + @ManagedAttribute("Whether client authentication is wanted") + public boolean getWantClientAuth() + { + return _wantClientAuth; + } + + /** + * @param wantClientAuth True if SSL wants client authentication. + * @see SSLEngine#getWantClientAuth() + */ + public void setWantClientAuth(boolean wantClientAuth) + { + _wantClientAuth = wantClientAuth; + } + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorThreadPool.java index 7b7253c9e29..c9da93cdb64 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorThreadPool.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ExecutorThreadPool.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -63,7 +64,12 @@ public class ExecutorThreadPool extends ContainerLifeCycle implements ThreadPool public ExecutorThreadPool(int maxThreads, int minThreads) { - this(new ThreadPoolExecutor(maxThreads, maxThreads, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()), minThreads, -1, null); + this(maxThreads, minThreads, new LinkedBlockingQueue<>()); + } + + public ExecutorThreadPool(int maxThreads, int minThreads, BlockingQueue queue) + { + this(new ThreadPoolExecutor(maxThreads, maxThreads, 60, TimeUnit.SECONDS, queue), minThreads, -1, null); } public ExecutorThreadPool(ThreadPoolExecutor executor) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java old mode 100755 new mode 100644 index 745912e7abe..94dab54c6db --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.util.thread; +import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -26,10 +27,11 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import org.eclipse.jetty.util.AtomicBiInteger; import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedOperation; @@ -46,9 +48,16 @@ import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool; public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadPool, Dumpable, TryExecutor { private static final Logger LOG = Log.getLogger(QueuedThreadPool.class); + private static Runnable NOOP = () -> {}; - private final AtomicInteger _threadsStarted = new AtomicInteger(); - private final AtomicInteger _threadsIdle = new AtomicInteger(); + /** + * Encodes thread counts: + *
    + *
    Hi
    Total thread count or Integer.MIN_VALUE if stopping
    + *
    Lo
    Net idle threads == idle threads - job queue size
    + *
    + */ + private final AtomicBiInteger _counts = new AtomicBiInteger(Integer.MIN_VALUE, 0); private final AtomicLong _lastShrink = new AtomicLong(); private final Set _threads = ConcurrentHashMap.newKeySet(); private final Object _joinLock = new Object(); @@ -76,12 +85,17 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP this(maxThreads, Math.min(8, maxThreads)); } - public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads) + public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads) { this(maxThreads, minThreads, 60000); } - public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("idleTimeout")int idleTimeout) + public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("queue") BlockingQueue queue) + { + this(maxThreads, minThreads, 60000, -1, queue, null); + } + + public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("idleTimeout") int idleTimeout) { this(maxThreads, minThreads, idleTimeout, null); } @@ -95,26 +109,24 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP { this(maxThreads, minThreads, idleTimeout, -1, queue, threadGroup); } - + public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("idleTimeout") int idleTimeout, @Name("reservedThreads") int reservedThreads, @Name("queue") BlockingQueue queue, @Name("threadGroup") ThreadGroup threadGroup) { - if (maxThreads < minThreads) { - throw new IllegalArgumentException("max threads ("+maxThreads+") less than min threads (" - +minThreads+")"); - } - + if (maxThreads < minThreads) + throw new IllegalArgumentException("max threads (" + maxThreads + ") less than min threads (" + + minThreads + ")"); setMinThreads(minThreads); setMaxThreads(maxThreads); setIdleTimeout(idleTimeout); setStopTimeout(5000); setReservedThreads(reservedThreads); - if (queue==null) + if (queue == null) { - int capacity=Math.max(_minThreads, 8); - queue=new BlockingArrayQueue<>(capacity, capacity); + int capacity = Math.max(_minThreads, 8) * 1024; + queue = new BlockingArrayQueue<>(capacity, capacity); } - _jobs=queue; - _threadGroup=threadGroup; + _jobs = queue; + _threadGroup = threadGroup; setThreadPoolBudget(new ThreadPoolBudget(this)); } @@ -126,7 +138,7 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP public void setThreadPoolBudget(ThreadPoolBudget budget) { - if (budget!=null && budget.getSizedThreadPool()!=this) + if (budget != null && budget.getSizedThreadPool() != this) throw new IllegalArgumentException(); _budget = budget; } @@ -134,66 +146,66 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP @Override protected void doStart() throws Exception { - _tryExecutor = new ReservedThreadExecutor(this,_reservedThreads); + if (_reservedThreads == 0) + { + _tryExecutor = NO_TRY; + } + else + { + ReservedThreadExecutor reserved = new ReservedThreadExecutor(this, _reservedThreads); + reserved.setIdleTimeout(_idleTimeout, TimeUnit.MILLISECONDS); + _tryExecutor = reserved; + } addBean(_tryExecutor); - - super.doStart(); - _threadsStarted.set(0); - startThreads(_minThreads); + super.doStart(); + // The threads count set to MIN_VALUE is used to signal to Runners that the pool is stopped. + _counts.set(0, 0); // threads, idle + ensureThreads(); } @Override protected void doStop() throws Exception { + if (LOG.isDebugEnabled()) + LOG.debug("Stopping {}", this); + removeBean(_tryExecutor); _tryExecutor = TryExecutor.NO_TRY; - + super.doStop(); + // Signal the Runner threads that we are stopping + int threads = _counts.getAndSetHi(Integer.MIN_VALUE); + + // If stop timeout try to gracefully stop long timeout = getStopTimeout(); BlockingQueue jobs = getQueue(); - - // If no stop timeout, clear job queue - if (timeout <= 0) - jobs.clear(); - - // Fill job Q with noop jobs to wakeup idle - Runnable noop = () -> {}; - for (int i = _threadsStarted.get(); i-- > 0; ) - jobs.offer(noop); - - // try to jobs complete naturally for half our stop time - long stopby = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout) / 2; - for (Thread thread : _threads) + if (timeout > 0) { - long canwait = TimeUnit.NANOSECONDS.toMillis(stopby - System.nanoTime()); - if (canwait > 0) - thread.join(canwait); - } + // Fill the job queue with noop jobs to wakeup idle threads. + for (int i = 0; i < threads; ++i) + { + jobs.offer(NOOP); + } - // If we still have threads running, get a bit more aggressive + // try to let jobs complete naturally for half our stop time + joinThreads(System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout) / 2); - // interrupt remaining threads - if (_threadsStarted.get() > 0) + // If we still have threads running, get a bit more aggressive + + // interrupt remaining threads for (Thread thread : _threads) + { + if (LOG.isDebugEnabled()) + LOG.debug("Interrupting {}", thread); thread.interrupt(); + } - // wait again for the other half of our stop time - stopby = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout) / 2; - for (Thread thread : _threads) - { - long canwait = TimeUnit.NANOSECONDS.toMillis(stopby - System.nanoTime()); - if (canwait > 0) - thread.join(canwait); - } + // wait again for the other half of our stop time + joinThreads(System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout) / 2); - Thread.yield(); - int size = _threads.size(); - if (size > 0) - { Thread.yield(); - if (LOG.isDebugEnabled()) { for (Thread unstopped : _threads) @@ -209,11 +221,32 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP else { for (Thread unstopped : _threads) - LOG.warn("{} Couldn't stop {}",this,unstopped); + { + LOG.warn("{} Couldn't stop {}", this, unstopped); + } } } - if (_budget!=null) + // Close any un-executed jobs + while (!_jobs.isEmpty()) + { + Runnable job = _jobs.poll(); + if (job instanceof Closeable) + { + try + { + ((Closeable)job).close(); + } + catch (Throwable t) + { + LOG.warn(t); + } + } + else if (job != NOOP) + LOG.warn("Stopped without executing or closing {}", job); + } + + if (_budget != null) _budget.reset(); synchronized (_joinLock) @@ -222,8 +255,20 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP } } + private void joinThreads(long stopByNanos) throws InterruptedException + { + for (Thread thread : _threads) + { + long canWait = TimeUnit.NANOSECONDS.toMillis(stopByNanos - System.nanoTime()); + if (LOG.isDebugEnabled()) + LOG.debug("Waiting for {} for {}", thread, canWait); + if (canWait > 0) + thread.join(canWait); + } + } + /** - * Thread Pool should use Daemon Threading. + * Thread Pool should use Daemon Threading. * * @param daemon true to enable delegation * @see Thread#setDaemon(boolean) @@ -255,7 +300,7 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP @Override public void setMaxThreads(int maxThreads) { - if (_budget!=null) + if (_budget != null) _budget.check(maxThreads); _maxThreads = maxThreads; if (_minThreads > _maxThreads) @@ -276,15 +321,14 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP if (_minThreads > _maxThreads) _maxThreads = _minThreads; - int threads = _threadsStarted.get(); - if (isStarted() && threads < _minThreads) - startThreads(_minThreads - threads); + if (isStarted()) + ensureThreads(); } - + /** * Set the number of reserved threads. * - * @param reservedThreads number of reserved threads or -1 for heuristically determined + * @param reservedThreads number of reserved threads or -1 for heuristically determined * @see #getReservedThreads */ public void setReservedThreads(int reservedThreads) @@ -362,7 +406,11 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP public int getReservedThreads() { if (isStarted()) - return getBean(ReservedThreadExecutor.class).getCapacity(); + { + ReservedThreadExecutor reservedThreadExecutor = getBean(ReservedThreadExecutor.class); + if (reservedThreadExecutor != null) + return reservedThreadExecutor.getCapacity(); + } return _reservedThreads; } @@ -385,10 +433,10 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP { return _priority; } - + /** * Get the size of the job queue. - * + * * @return Number of jobs queued waiting for a thread */ @ManagedAttribute("size of the job queue") @@ -417,7 +465,7 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP { _detailedDump = detailedDump; } - + @ManagedAttribute("threshold at which the pool is low on threads") public int getLowThreadsThreshold() { @@ -432,26 +480,55 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP @Override public void execute(Runnable job) { - if (LOG.isDebugEnabled()) - LOG.debug("queue {}",job); - if (!isRunning() || !_jobs.offer(job)) + // Determine if we need to start a thread, use and idle thread or just queue this job + boolean startThread; + while (true) { - LOG.warn("{} rejected {}", this, job); + // Get the atomic counts + long counts = _counts.get(); + + // Get the number of threads started (might not yet be running) + int threads = AtomicBiInteger.getHi(counts); + if (threads == Integer.MIN_VALUE) + throw new RejectedExecutionException(job.toString()); + + // Get the number of truly idle threads. This count is reduced by the + // job queue size so that any threads that are idle but are about to take + // a job from the queue are not counted. + int idle = AtomicBiInteger.getLo(counts); + + // Start a thread if we have insufficient idle threads to meet demand + // and we are not at max threads. + startThread = (idle <= 0 && threads < _maxThreads); + + // The job will be run by an idle thread when available + if (!_counts.compareAndSet(counts, threads + (startThread ? 1 : 0), idle - 1)) + continue; + + break; + } + + if (!_jobs.offer(job)) + { + // reverse our changes to _counts. + if (addCounts(startThread ? -1 : 0, 1)) + LOG.warn("{} rejected {}", this, job); throw new RejectedExecutionException(job.toString()); } - else - { - // Make sure there is at least one thread executing the job. - if (getThreads() == 0) - startThreads(1); - } + + if (LOG.isDebugEnabled()) + LOG.debug("queue {} startThread={}", job, startThread); + + // Start a thread if one was needed + if (startThread) + startThread(); } @Override public boolean tryExecute(Runnable task) { TryExecutor tryExecutor = _tryExecutor; - return tryExecutor!=null && tryExecutor.tryExecute(task); + return tryExecutor != null && tryExecutor.tryExecute(task); } /** @@ -463,11 +540,15 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP synchronized (_joinLock) { while (isRunning()) + { _joinLock.wait(); + } } while (isStopping()) + { Thread.sleep(1); + } } /** @@ -477,7 +558,8 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP @ManagedAttribute("number of threads in the pool") public int getThreads() { - return _threadsStarted.get(); + int threads = _counts.getHi(); + return Math.max(0, threads); } /** @@ -487,7 +569,8 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP @ManagedAttribute("number of idle threads in the pool") public int getIdleThreads() { - return _threadsIdle.get(); + int idle = _counts.getLo(); + return Math.max(0, idle); } /** @@ -499,7 +582,7 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP int reserved = _tryExecutor instanceof ReservedThreadExecutor ? ((ReservedThreadExecutor)_tryExecutor).getAvailable() : 0; return getThreads() - getIdleThreads() - reserved; } - + /** *

    Returns whether this thread pool is low on threads.

    *

    The current formula is:

    @@ -517,37 +600,66 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP return getMaxThreads() - getThreads() + getIdleThreads() - getQueueSize() <= getLowThreadsThreshold(); } - private boolean startThreads(int threadsToStart) + private void ensureThreads() { - while (threadsToStart > 0 && isRunning()) + while (true) { - int threads = _threadsStarted.get(); - if (threads >= _maxThreads) - return false; + long counts = _counts.get(); + int threads = AtomicBiInteger.getHi(counts); + if (threads == Integer.MIN_VALUE) + break; - if (!_threadsStarted.compareAndSet(threads, threads + 1)) + // If we have less than min threads + // OR insufficient idle threads to meet demand + int idle = AtomicBiInteger.getLo(counts); + if (threads < _minThreads || (idle < 0 && threads < _maxThreads)) + { + // Then try to start a thread. + if (_counts.compareAndSet(counts, threads + 1, idle)) + startThread(); + // Otherwise continue to check state again. continue; - - boolean started = false; - try - { - Thread thread = newThread(_runnable); - thread.setDaemon(isDaemon()); - thread.setPriority(getThreadsPriority()); - thread.setName(_name + "-" + thread.getId()); - _threads.add(thread); - _lastShrink.set(System.nanoTime()); - thread.start(); - started = true; - --threadsToStart; - } - finally - { - if (!started) - _threadsStarted.decrementAndGet(); } + break; + } + } + + protected void startThread() + { + boolean started = false; + try + { + Thread thread = newThread(_runnable); + thread.setDaemon(isDaemon()); + thread.setPriority(getThreadsPriority()); + thread.setName(_name + "-" + thread.getId()); + if (LOG.isDebugEnabled()) + LOG.debug("Starting {}", thread); + _threads.add(thread); + _lastShrink.set(System.nanoTime()); + thread.start(); + started = true; + } + finally + { + if (!started) + addCounts(-1, 0); // threads, idle + } + } + + private boolean addCounts(int deltaThreads, int deltaIdle) + { + while (true) + { + long encoded = _counts.get(); + int threads = AtomicBiInteger.getHi(encoded); + int idle = AtomicBiInteger.getLo(encoded); + if (threads == Integer.MIN_VALUE) // This is a marker that the pool is stopped. + return false; + long update = AtomicBiInteger.encode(threads + deltaThreads, idle + deltaIdle); + if (_counts.compareAndSet(encoded, update)) + return true; } - return true; } protected Thread newThread(Runnable runnable) @@ -570,24 +682,24 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP String knownMethod = ""; for (StackTraceElement t : trace) { - if ("idleJobPoll".equals(t.getMethodName()) && t.getClassName().endsWith("QueuedThreadPool")) + if ("idleJobPoll".equals(t.getMethodName()) && t.getClassName().equals(Runner.class.getName())) { knownMethod = "IDLE "; break; } - + if ("reservedWait".equals(t.getMethodName()) && t.getClassName().endsWith("ReservedThread")) { knownMethod = "RESERVED "; break; } - + if ("select".equals(t.getMethodName()) && t.getClassName().endsWith("SelectorProducer")) { knownMethod = "SELECTING "; break; } - + if ("accept".equals(t.getMethodName()) && t.getClassName().contains("ServerConnector")) { knownMethod = "ACCEPTING "; @@ -603,11 +715,10 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP @Override public void dump(Appendable out, String indent) throws IOException { - String s = thread.getId()+" "+thread.getName()+" "+thread.getState()+" "+thread.getPriority(); - if (known.length()==0) - Dumpable.dumpObjects(out, indent, s, (Object[])trace); + if (StringUtil.isBlank(known)) + Dumpable.dumpObjects(out, indent, String.format("%s %s %s %d", thread.getId(), thread.getName(), thread.getState(), thread.getPriority()), (Object[])trace); else - Dumpable.dumpObjects(out, indent, s); + Dumpable.dumpObjects(out, indent, String.format("%s %s %s %s %d", thread.getId(), thread.getName(), known, thread.getState(), thread.getPriority())); } @Override @@ -619,8 +730,8 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP } else { - int p=thread.getPriority(); - threads.add(thread.getId() + " " + thread.getName() + " " + known + thread.getState() + " @ " + (trace.length > 0 ? trace[0] : "???") + (p==Thread.NORM_PRIORITY?"":(" prio="+p))); + int p = thread.getPriority(); + threads.add(thread.getId() + " " + thread.getName() + " " + known + thread.getState() + " @ " + (trace.length > 0 ? trace[0] : "???") + (p == Thread.NORM_PRIORITY ? "" : (" prio=" + p))); } } @@ -638,121 +749,26 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP @Override public String toString() { - return String.format("%s[%s]@%x{%s,%d<=%d<=%d,i=%d,q=%d}[%s]", + long count = _counts.get(); + int threads = Math.max(0, AtomicBiInteger.getHi(count)); + int idle = Math.max(0, AtomicBiInteger.getLo(count)); + int queue = getQueueSize(); + + return String.format("%s[%s]@%x{%s,%d<=%d<=%d,i=%d,r=%d,q=%d}[%s]", getClass().getSimpleName(), _name, hashCode(), getState(), getMinThreads(), - getThreads(), + threads, getMaxThreads(), - getIdleThreads(), - _jobs.size(), + idle, + getReservedThreads(), + queue, _tryExecutor); } - private Runnable idleJobPoll() throws InterruptedException - { - return _jobs.poll(_idleTimeout, TimeUnit.MILLISECONDS); - } - - private Runnable _runnable = new Runnable() - { - @Override - public void run() - { - boolean shrink = false; - boolean ignore = false; - try - { - Runnable job = _jobs.poll(); - - if (job != null && _threadsIdle.get() == 0) - { - startThreads(1); - } - - loop: - while (isRunning()) - { - // Job loop - while (job != null && isRunning()) - { - if (LOG.isDebugEnabled()) - LOG.debug("run {}", job); - runJob(job); - if (LOG.isDebugEnabled()) - LOG.debug("ran {}", job); - if (Thread.interrupted()) - { - ignore = true; - break loop; - } - job = _jobs.poll(); - } - - // Idle loop - try - { - _threadsIdle.incrementAndGet(); - - while (isRunning() && job == null) - { - if (_idleTimeout <= 0) - job = _jobs.take(); - else - { - // maybe we should shrink? - final int size = _threadsStarted.get(); - if (size > _minThreads) - { - long last = _lastShrink.get(); - long now = System.nanoTime(); - if (last == 0 || (now - last) > TimeUnit.MILLISECONDS.toNanos(_idleTimeout)) - { - if (_lastShrink.compareAndSet(last, now) && _threadsStarted.compareAndSet(size, size - 1)) - { - shrink = true; - break loop; - } - } - } - job = idleJobPoll(); - } - } - } - finally - { - if (_threadsIdle.decrementAndGet() == 0) - { - startThreads(1); - } - } - } - } - catch (InterruptedException e) - { - ignore = true; - LOG.ignore(e); - } - catch (Throwable e) - { - LOG.warn(e); - } - finally - { - if (!shrink && isRunning()) - { - if (!ignore) - LOG.warn("Unexpected thread death: {} in {}", this, QueuedThreadPool.this); - // This is an unexpected thread death! - if (_threadsStarted.decrementAndGet() < getMaxThreads()) - startThreads(1); - } - removeThread(Thread.currentThread()); - } - } - }; + private final Runnable _runnable = new Runner(); /** *

    Runs the given job in the {@link Thread#currentThread() current thread}.

    @@ -773,6 +789,16 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP return _jobs; } + /** + * @param queue the job queue + * @deprecated pass the queue to the constructor instead + */ + @Deprecated + public void setQueue(BlockingQueue queue) + { + throw new UnsupportedOperationException("Use constructor injection"); + } + /** * @param id the thread ID to interrupt. * @return true if the thread was found and interrupted. @@ -806,10 +832,121 @@ public class QueuedThreadPool extends ContainerLifeCycle implements SizedThreadP buf.append(thread.getId()).append(" ").append(thread.getName()).append(" "); buf.append(thread.getState()).append(":").append(System.lineSeparator()); for (StackTraceElement element : thread.getStackTrace()) + { buf.append(" at ").append(element.toString()).append(System.lineSeparator()); + } return buf.toString(); } } return null; } + + private class Runner implements Runnable + { + private Runnable idleJobPoll(long idleTimeout) throws InterruptedException + { + if (idleTimeout <= 0) + return _jobs.take(); + return _jobs.poll(idleTimeout, TimeUnit.MILLISECONDS); + } + + @Override + public void run() + { + if (LOG.isDebugEnabled()) + LOG.debug("Runner started for {}", QueuedThreadPool.this); + + Runnable job = null; + + try + { + // All threads start idle (not yet taken a job) + if (!addCounts(0, 1)) + return; + + while (true) + { + // If we had a job, signal that we are idle again + if (job != null) + { + if (!addCounts(0, 1)) + break; + } + // else check we are still running + else if (_counts.getHi() == Integer.MIN_VALUE) + { + break; + } + + try + { + // Look for an immediately available job + job = _jobs.poll(); + if (job == null) + { + // Wait for a job + long idleTimeout = getIdleTimeout(); + job = idleJobPoll(idleTimeout); + + // If still no job? + if (job == null) + { + // maybe we should shrink + if (getThreads() > _minThreads && idleTimeout > 0) + { + long last = _lastShrink.get(); + long now = System.nanoTime(); + if (last == 0 || (now - last) > TimeUnit.MILLISECONDS.toNanos(idleTimeout)) + { + if (_lastShrink.compareAndSet(last, now)) + { + if (LOG.isDebugEnabled()) + LOG.debug("shrinking {}", QueuedThreadPool.this); + break; + } + } + } + // continue to try again + continue; + } + } + + // run job + if (LOG.isDebugEnabled()) + LOG.debug("run {} in {}", job, QueuedThreadPool.this); + runJob(job); + if (LOG.isDebugEnabled()) + LOG.debug("ran {} in {}", job, QueuedThreadPool.this); + + // Clear any interrupted status + Thread.interrupted(); + } + catch (InterruptedException e) + { + if (LOG.isDebugEnabled()) + LOG.debug("interrupted {} in {}", job, QueuedThreadPool.this); + LOG.ignore(e); + } + catch (Throwable e) + { + LOG.warn(e); + } + } + } + finally + { + Thread thread = Thread.currentThread(); + removeThread(thread); + + // Decrement the total thread count and the idle count if we had no job + addCounts(-1, job == null ? -1 : 0); + if (LOG.isDebugEnabled()) + LOG.debug("{} exited for {}", thread, QueuedThreadPool.this); + + // There is a chance that we shrunk just as a job was queued for us, so + // check again if we have sufficient threads to meet demand + ensureThreads(); + } + } + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ScheduledExecutorScheduler.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ScheduledExecutorScheduler.java old mode 100755 new mode 100644 index 0ad6b76bb86..a0c889d162c --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ScheduledExecutorScheduler.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ScheduledExecutorScheduler.java @@ -21,7 +21,6 @@ package org.eclipse.jetty.util.thread; import java.io.IOException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.util.component.AbstractLifeCycle; @@ -47,13 +46,13 @@ public class ScheduledExecutorScheduler extends AbstractLifeCycle implements Sch public ScheduledExecutorScheduler() { this(null, false); - } + } public ScheduledExecutorScheduler(String name, boolean daemon) { - this (name,daemon, Thread.currentThread().getContextClassLoader()); + this(name, daemon, Thread.currentThread().getContextClassLoader()); } - + public ScheduledExecutorScheduler(String name, boolean daemon, ClassLoader threadFactoryClassLoader) { this(name, daemon, threadFactoryClassLoader, null); @@ -70,16 +69,12 @@ public class ScheduledExecutorScheduler extends AbstractLifeCycle implements Sch @Override protected void doStart() throws Exception { - scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactory() + scheduler = new ScheduledThreadPoolExecutor(1, r -> { - @Override - public Thread newThread(Runnable r) - { - Thread thread = ScheduledExecutorScheduler.this.thread = new Thread(threadGroup, r, name); - thread.setDaemon(daemon); - thread.setContextClassLoader(classloader); - return thread; - } + Thread thread = ScheduledExecutorScheduler.this.thread = new Thread(threadGroup, r, name); + thread.setDaemon(daemon); + thread.setContextClassLoader(classloader); + return thread; }); scheduler.setRemoveOnCancelPolicy(true); super.doStart(); @@ -97,8 +92,8 @@ public class ScheduledExecutorScheduler extends AbstractLifeCycle implements Sch public Task schedule(Runnable task, long delay, TimeUnit unit) { ScheduledThreadPoolExecutor s = scheduler; - if (s==null) - return ()->false; + if (s == null) + return () -> false; ScheduledFuture result = s.schedule(task, delay, unit); return new ScheduledFutureTask(result); } @@ -116,7 +111,7 @@ public class ScheduledExecutorScheduler extends AbstractLifeCycle implements Sch if (thread == null) Dumpable.dumpObject(out, this); else - Dumpable.dumpObjects(out,indent,this, (Object[])thread.getStackTrace()); + Dumpable.dumpObjects(out, indent, this, (Object[])thread.getStackTrace()); } private static class ScheduledFutureTask implements Task diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/SerializedExecutor.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/SerializedExecutor.java new file mode 100644 index 00000000000..73a67475525 --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/SerializedExecutor.java @@ -0,0 +1,111 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.thread; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.eclipse.jetty.util.log.Log; + +/** + * An executor than ensurers serial execution of submitted tasks. + *

    + * Callers of this execute will never block in the executor, but they may + * be required to either execute the task they submit or tasks submitted + * by other threads whilst they are executing tasks. + *

    + *

    + * This class was inspired by the public domain class + * NonBlockingMutexExecutor + *

    + */ +public class SerializedExecutor implements Executor +{ + private final AtomicReference _tail = new AtomicReference<>(); + + @Override + public void execute(Runnable task) + { + Link link = new Link(task); + Link lastButOne = _tail.getAndSet(link); + if (lastButOne==null) + run(link); + else + lastButOne._next.lazySet(link); + } + + protected void onError(Runnable task, Throwable t) + { + if (task instanceof ErrorHandlingTask) + ((ErrorHandlingTask)task).accept(t); + Log.getLogger(task.getClass()).warn(t); + } + + private void run(Link link) + { + while(link!=null) + { + try + { + link._task.run(); + } + catch (Throwable t) + { + onError(link._task, t); + } + finally + { + // Are we the current the last Link? + if (_tail.compareAndSet(link, null)) + link = null; + else + { + // not the last task, so its next link will eventually be set + Link next = link._next.get(); + while (next == null) + { + Thread.yield(); // Thread.onSpinWait(); + next = link._next.get(); + } + link = next; + } + } + } + } + + private class Link + { + private final Runnable _task; + private final AtomicReference _next = new AtomicReference<>(); + + public Link(Runnable task) + { + _task = task; + } + } + + /** + * Error handling task + *

    If a submitted task implements this interface, it will be passed + * any exceptions thrown when running the task.

    + */ + public interface ErrorHandlingTask extends Runnable, Consumer + {} +} diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TryExecutor.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TryExecutor.java index a9696fb6db9..034261a5ca4 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TryExecutor.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/TryExecutor.java @@ -75,5 +75,19 @@ public interface TryExecutor extends Executor } } - public static final TryExecutor NO_TRY = task -> false; + TryExecutor NO_TRY = new TryExecutor() + { + @Override + public boolean tryExecute(Runnable task) + { + return false; + } + + @Override + public String toString() + { + return "NO_TRY"; + } + }; + } diff --git a/jetty-util/src/main/resources/jetty-dir.css b/jetty-util/src/main/resources/jetty-dir.css index 1686813ff78..cbcc88d91ae 100644 --- a/jetty-util/src/main/resources/jetty-dir.css +++ b/jetty-util/src/main/resources/jetty-dir.css @@ -1,19 +1,38 @@ -body -{ +body { background-color: #FFFFFF; margin: 10px; padding: 5px; + font-family: sans-serif; } -h1 -{ +h1.title { text-shadow: #000000 -1px -1px 1px; color: #FC390E; font-weight: bold; } -a -{ +table.listing { + border: 0px; +} + +thead a { + color: blue; +} + +thead th { + border-bottom: black 1px solid; +} + +.name, .lastmodified { + text-align: left; + padding-right: 15px; +} + +.size { + text-align: right; +} + +a { color: #7036be; font-weight: bold; font-style: normal; @@ -21,10 +40,9 @@ a font-size:inherit; } -td -{ +td { font-style: italic; - padding: 2px 15px 2px 0px; + padding: 2px; } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ArrayUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ArrayUtilTest.java new file mode 100644 index 00000000000..b27d46ac17d --- /dev/null +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ArrayUtilTest.java @@ -0,0 +1,103 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Unit tests for class {@link ArrayUtil}. + * + * @see ArrayUtil + */ +public class ArrayUtilTest +{ + + @Test + public void testAddToArrayWithEmptyArray() + { + String[] stringArray = new String[0]; + String[] resultArray = ArrayUtil.addToArray(stringArray, "Ca?", Object.class); + + assertEquals(0, stringArray.length); + assertEquals(1, resultArray.length); + + assertNotSame(stringArray, resultArray); + assertNotSame(resultArray, stringArray); + + assertFalse(resultArray.equals(stringArray)); + assertEquals(String.class, resultArray[0].getClass()); + } + + @Test + public void testAddUsingNull() + { + String[] stringArray = new String[7]; + String[] stringArrayTwo = ArrayUtil.add(stringArray, null); + + assertEquals(7, stringArray.length); + assertEquals(7, stringArrayTwo.length); + + assertSame(stringArray, stringArrayTwo); + assertSame(stringArrayTwo, stringArray); + } + + @Test + public void testAddWithNonEmptyArray() + { + Object[] objectArray = new Object[3]; + Object[] objectArrayTwo = ArrayUtil.add(objectArray, objectArray); + + assertEquals(3, objectArray.length); + assertEquals(6, objectArrayTwo.length); + + assertNotSame(objectArray, objectArrayTwo); + assertNotSame(objectArrayTwo, objectArray); + + assertFalse(objectArrayTwo.equals(objectArray)); + } + + @Test + public void testRemoveFromNullArrayReturningNull() + { + assertNull(ArrayUtil.removeFromArray((Integer[])null, new Object())); + } + + @Test + public void testRemoveNulls() + { + Object[] objectArray = new Object[2]; + objectArray[0] = new Object(); + Object[] resultArray = ArrayUtil.removeNulls(objectArray); + + assertEquals(2, objectArray.length); + assertEquals(1, resultArray.length); + + assertNotSame(objectArray, resultArray); + assertNotSame(resultArray, objectArray); + + assertFalse(resultArray.equals(objectArray)); + } + +} diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/B64CodeTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/B64CodeTest.java index c0de3b489ac..e1c3848218a 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/B64CodeTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/B64CodeTest.java @@ -18,70 +18,121 @@ package org.eclipse.jetty.util; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.charset.StandardCharsets; - +import java.util.Base64; import org.junit.jupiter.api.Test; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + public class B64CodeTest { - String text = "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure."; + String text = "Man is distinguished, not only by his reason, but by this singular passion " + + "from other animals, which is a lust of the mind, that by a perseverance of delight in " + + "the continued and indefatigable generation of knowledge, exceeds the short vehemence " + + "of any carnal pleasure."; @Test - public void testRFC1421() throws Exception + public void testEncode_RFC1421() { - String b64 = B64Code.encode(text, StandardCharsets.ISO_8859_1); - assertEquals("TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz"+ - "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg"+ - "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu"+ - "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo"+ - "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=",b64); - - char[] chars = B64Code.encode(text.getBytes(StandardCharsets.ISO_8859_1),false); + String expected = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz" + + "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg" + + "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu" + + "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo" + + "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4="; + + // Default Encode + String b64 = B64Code.encode(text, ISO_8859_1); + assertThat("B64Code.encode(String)", b64, is(expected)); + + // Specified RFC Encode + byte[] rawInputBytes = text.getBytes(ISO_8859_1); + char[] chars = B64Code.encode(rawInputBytes,false); b64 = new String(chars,0,chars.length); - assertEquals("TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz"+ - "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg"+ - "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu"+ - "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo"+ - "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=",b64); - + assertThat("B64Code.encode(byte[], false)", b64, is(expected)); + + // Standard Java Encode + String javaBase64 = Base64.getEncoder().encodeToString(rawInputBytes); + assertThat("Base64.getEncoder().encodeToString((byte[])", javaBase64, is(expected)); } @Test - public void testRFC2045() throws Exception + public void testEncode_RFC2045() { - char[] chars = B64Code.encode(text.getBytes(StandardCharsets.ISO_8859_1),true); + byte[] rawInputBytes = text.getBytes(ISO_8859_1); + + // Old Jetty way + char[] chars = B64Code.encode(rawInputBytes,true); String b64 = new String(chars,0,chars.length); - assertEquals("TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz\r\n"+ - "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg\r\n"+ - "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu\r\n"+ - "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo\r\n"+ - "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=\r\n",b64); - + + String expected = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz\r\n"+ + "IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg\r\n"+ + "dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu\r\n"+ + "dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo\r\n"+ + "ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=\r\n"; + + assertThat(b64, is(expected)); + + // Standard Java way + String javaBase64 = Base64.getMimeEncoder().encodeToString(rawInputBytes); + // NOTE: MIME standard for encoding should not include final "\r\n" + assertThat(javaBase64 + "\r\n", is(expected)); } - @Test public void testInteger() throws Exception { - byte[] bytes = text.getBytes(StandardCharsets.ISO_8859_1); - int value=(bytes[0]<<24)+(bytes[1]<<16)+(bytes[2]<<8)+(bytes[3]); - + byte[] bytes = text.getBytes(ISO_8859_1); + int value = (bytes[0] << 24) + + (bytes[1] << 16) + + (bytes[2] << 8) + + (bytes[3]); + + String expected = "TWFuIA"; + + // Old Jetty way StringBuilder b = new StringBuilder(); B64Code.encode(value,b); - assertEquals("TWFuIA=",b.toString()); + assertThat("Old Jetty B64Code", b.toString(), is(expected)); + + // Standard Java technique + byte[] intBytes = new byte[Integer.BYTES]; + for (int i = Integer.BYTES - 1; i >= 0; i--) + { + intBytes[i] = (byte) (value & 0xFF); + value >>= 8; + } + assertThat("Standard Java Base64", Base64.getEncoder().withoutPadding().encodeToString(intBytes), is(expected)); } + @Test public void testLong() throws Exception { - byte[] bytes = text.getBytes(StandardCharsets.ISO_8859_1); - long value=((0xffL&bytes[0])<<56)+((0xffL&bytes[1])<<48)+((0xffL&bytes[2])<<40)+((0xffL&bytes[3])<<32)+ - ((0xffL&bytes[4])<<24)+((0xffL&bytes[5])<<16)+((0xffL&bytes[6])<<8)+(0xffL&bytes[7]); - + byte[] bytes = text.getBytes(ISO_8859_1); + long value = ((0xffL & bytes[0]) << 56) + + ((0xffL & bytes[1]) << 48) + + ((0xffL & bytes[2]) << 40) + + ((0xffL & bytes[3]) << 32) + + ((0xffL & bytes[4]) << 24) + + ((0xffL & bytes[5]) << 16) + + ((0xffL & bytes[6]) << 8) + + (0xffL & bytes[7]); + + String expected = "TWFuIGlzIGQ"; + + // Old Jetty way StringBuilder b = new StringBuilder(); B64Code.encode(value,b); - assertEquals("TWFuIGlzIGQ",b.toString()); + assertThat("Old Jetty B64Code", b.toString(), is(expected)); + + // Standard Java technique + byte[] longBytes = new byte[Long.BYTES]; + for (int i = Long.BYTES - 1; i >= 0; i--) + { + longBytes[i] = (byte) (value & 0xFF); + value >>= 8; + } + assertThat("Standard Java Base64", Base64.getEncoder().withoutPadding().encodeToString(longBytes), is(expected)); } } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/BlockingArrayQueueTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/BlockingArrayQueueTest.java index 0d59a1ec4af..3a35ff6fb92 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/BlockingArrayQueueTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/BlockingArrayQueueTest.java @@ -18,12 +18,9 @@ package org.eclipse.jetty.util; -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.junit.jupiter.api.Assertions.fail; - +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.ListIterator; import java.util.Random; import java.util.concurrent.ConcurrentLinkedQueue; @@ -31,10 +28,16 @@ import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; - +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import static org.hamcrest.MatcherAssert.assertThat; +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.junit.jupiter.api.Assertions.fail; + public class BlockingArrayQueueTest { @Test @@ -494,4 +497,28 @@ public class BlockingArrayQueueTest assertTrue(iterator.hasNext()); assertFalse(iterator.hasPrevious()); } + + + @Test + public void testDrainTo() throws Exception + { + BlockingArrayQueue queue = new BlockingArrayQueue<>(); + queue.add("one"); + queue.add("two"); + queue.add("three"); + queue.add("four"); + queue.add("five"); + queue.add("six"); + + List to = new ArrayList<>(); + queue.drainTo(to,3); + assertThat(to, Matchers.contains("one", "two", "three")); + assertThat(queue.size(),Matchers.is(3)); + assertThat(queue, Matchers.contains("four", "five", "six")); + + queue.drainTo(to); + assertThat(to, Matchers.contains("one", "two", "three", "four", "five", "six")); + assertThat(queue.size(),Matchers.is(0)); + assertThat(queue, Matchers.empty()); + } } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java index ecbdb6cad37..e10f4f36356 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java @@ -19,11 +19,6 @@ package org.eclipse.jetty.util; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.BufferOverflowException; @@ -31,9 +26,14 @@ import java.nio.ByteBuffer; import java.util.Arrays; import java.util.concurrent.ThreadLocalRandom; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.is; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java index cb3e268024d..b18606a409a 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java @@ -42,7 +42,29 @@ public class HostPortTest Arguments.of("10.10.10.1", "10.10.10.1", null), Arguments.of("10.10.10.1:80", "10.10.10.1", "80"), Arguments.of("[0::0::0::1]", "[0::0::0::1]", null), - Arguments.of("[0::0::0::1]:80", "[0::0::0::1]", "80") + Arguments.of("[0::0::0::1]:80", "[0::0::0::1]", "80"), + Arguments.of("0:1:2:3:4:5:6","[0:1:2:3:4:5:6]",null), + // Localhost tests + Arguments.of("localhost:80", "localhost", "80"), + Arguments.of("127.0.0.1:80", "127.0.0.1", "80"), + Arguments.of("::1","[::1]",null), + Arguments.of("[::1]:443","[::1]","443"), + // Examples from https://tools.ietf.org/html/rfc2732#section-2 + Arguments.of("[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", "80"), + Arguments.of("[1080:0:0:0:8:800:200C:417A]", "[1080:0:0:0:8:800:200C:417A]", null), + Arguments.of("[3ffe:2a00:100:7031::1]", "[3ffe:2a00:100:7031::1]", null), + Arguments.of("[1080::8:800:200C:417A]", "[1080::8:800:200C:417A]", null), + Arguments.of("[::192.9.5.5]", "[::192.9.5.5]", null), + Arguments.of("[::FFFF:129.144.52.38]:80", "[::FFFF:129.144.52.38]", "80"), + Arguments.of("[2010:836B:4179::836B:4179]", "[2010:836B:4179::836B:4179]", null), + // Modified Examples from above, not using square brackets (valid, but should never have a port) + Arguments.of("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", null), + Arguments.of("1080:0:0:0:8:800:200C:417A", "[1080:0:0:0:8:800:200C:417A]", null), + Arguments.of("3ffe:2a00:100:7031::1", "[3ffe:2a00:100:7031::1]", null), + Arguments.of("1080::8:800:200C:417A", "[1080::8:800:200C:417A]", null), + Arguments.of("::192.9.5.5", "[::192.9.5.5]", null), + Arguments.of("::FFFF:129.144.52.38", "[::FFFF:129.144.52.38]", null), + Arguments.of("2010:836B:4179::836B:4179", "[2010:836B:4179::836B:4179]", null) ); } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/IntrospectionUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/IntrospectionUtilTest.java new file mode 100644 index 00000000000..40d8c2d91a6 --- /dev/null +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/IntrospectionUtilTest.java @@ -0,0 +1,76 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Array; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for class {@link IntrospectionUtil}. + * + * @see IntrospectionUtil + */ +public class IntrospectionUtilTest +{ + + @Test + public void testIsTypeCompatibleWithTwoTimesString() + { + assertTrue(IntrospectionUtil.isTypeCompatible(String.class, String.class, true)); + } + + @Test + public void testIsSameSignatureWithNull() + { + assertFalse(IntrospectionUtil.isSameSignature(null, null)); + } + + @Test + public void testFindMethodWithEmptyString() + { + assertThrows(NoSuchMethodException.class, + () -> IntrospectionUtil.findMethod(Integer.class, "", null, false, false)); + } + + @Test + public void testFindMethodWithNullMethodParameter() + { + assertThrows(NoSuchMethodException.class, + () -> IntrospectionUtil.findMethod(String.class, null, (Class[])Array.newInstance(Class.class, 3), true, true)); + } + + @Test + public void testFindMethodWithNullClassParameter() throws NoSuchMethodException + { + assertThrows(NoSuchMethodException.class, + () -> IntrospectionUtil.findMethod(null, "subSequence", (Class[])Array.newInstance(Class.class, 9), false, false)); + } + + @Test + public void testIsJavaBeanCompliantSetterWithNull() + { + assertFalse(IntrospectionUtil.isJavaBeanCompliantSetter(null)); + } + +} diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/LoaderTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/LoaderTest.java new file mode 100644 index 00000000000..cec85d6d1b4 --- /dev/null +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/LoaderTest.java @@ -0,0 +1,55 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; +import java.util.MissingResourceException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests for class {@link Loader}. + * + * @see Loader + */ +public class LoaderTest +{ + + @Test + public void testGetResourceBundleThrowsMissingResourceException() + { + assertThrows(MissingResourceException.class, () -> Loader.getResourceBundle("nothing", true, Locale.ITALIAN)); + } + + @Test + public void testLoadClassThrowsClassNotFoundException() + { + assertThrows(ClassNotFoundException.class, () -> Loader.loadClass(Object.class, "String")); + } + + @Test + public void testLoadClassSucceeds() throws ClassNotFoundException + { + assertEquals(LazyList.class, Loader.loadClass(Object.class, "org.eclipse.jetty.util.LazyList")); + } + +} diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/RolloverFileOutputStreamTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/RolloverFileOutputStreamTest.java index c0c58767f12..106eea45b82 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/RolloverFileOutputStreamTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/RolloverFileOutputStreamTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.util; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; @@ -43,6 +40,9 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + @ExtendWith(WorkDirExtension.class) public class RolloverFileOutputStreamTest { diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java index c12965f3bf5..0abde884537 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/StringUtilTest.java @@ -225,6 +225,22 @@ public class StringUtilTest assertTrue(StringUtil.isNotBlank(".")); assertTrue(StringUtil.isNotBlank(";\n")); } + + @Test + public void testIsEmpty() + { + assertTrue(StringUtil.isEmpty(null)); + assertTrue(StringUtil.isEmpty("")); + assertFalse(StringUtil.isEmpty("\r\n")); + assertFalse(StringUtil.isEmpty("\t")); + assertFalse(StringUtil.isEmpty(" ")); + + assertFalse(StringUtil.isEmpty("a")); + assertFalse(StringUtil.isEmpty(" a")); + assertFalse(StringUtil.isEmpty("a ")); + assertFalse(StringUtil.isEmpty(".")); + assertFalse(StringUtil.isEmpty(";\n")); + } @Test public void testSanitizeHTML() diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java index 3e41437aa6c..44d7fd07ddf 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/TrieTest.java @@ -18,13 +18,6 @@ package org.eclipse.jetty.util; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.isIn; -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 java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -34,6 +27,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class TrieTest { public static Stream implementations() @@ -95,7 +95,7 @@ public class TrieTest ""}; for(String value: values) - assertThat(value, isIn(trie.keySet())); + assertThat(value, is(in(trie.keySet()))); } @ParameterizedTest diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java index 3251ad0d0c6..ec37ad53ecd 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java @@ -19,13 +19,6 @@ package org.eclipse.jetty.util; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; -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.junit.jupiter.api.Assumptions.assumeTrue; - import java.nio.file.Path; import java.nio.file.Paths; @@ -36,6 +29,13 @@ import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.EnabledOnJre; import org.junit.jupiter.api.condition.JRE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +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.junit.jupiter.api.Assumptions.assumeTrue; + public class TypeUtilTest { @Test diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java index 71dbb742344..bea10a0aee2 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URIUtilCanonicalPathTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.util; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - import java.util.ArrayList; import java.util.stream.Stream; @@ -28,6 +25,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + public class URIUtilCanonicalPathTest { public static Stream data() diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java index 99e5afb56b4..60d8327e891 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/URLEncodedTest.java @@ -18,12 +18,6 @@ package org.eclipse.jetty.util; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; - import java.io.ByteArrayInputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -36,6 +30,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + /** * URL Encoding / Decoding Tests */ diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/component/DumpableTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/component/DumpableTest.java new file mode 100644 index 00000000000..a0dcc405d42 --- /dev/null +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/component/DumpableTest.java @@ -0,0 +1,53 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.component; + +import java.util.ArrayList; +import java.util.Collection; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; + +public class DumpableTest +{ + @Test + public void testNullDumpableCollection () throws Exception + { + DumpableCollection dc = new DumpableCollection("null test", null); + String dump = dc.dump(); + assertThat(dump, Matchers.containsString("size=0")); + } + + @Test + public void testNonNullDumpableCollection () throws Exception + { + Collection collection = new ArrayList<>(); + collection.add("one"); + collection.add("two"); + collection.add("three"); + + DumpableCollection dc = new DumpableCollection("non null test", collection); + String dump = dc.dump(); + assertThat(dump, Matchers.containsString("one")); + assertThat(dump, Matchers.containsString("two")); + assertThat(dump, Matchers.containsString("three")); + } +} diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/log/LogTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/log/LogTest.java index 9b2f919f4d9..08f2147bca8 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/log/LogTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/log/LogTest.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.util.log; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -34,6 +29,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class LogTest { private static Logger originalLogger; @@ -101,20 +100,41 @@ public class LogTest public static Stream packageCases() { return Stream.of( - Arguments.of(null, ""), - Arguments.of("org.eclipse.Foo.\u0000", "oe.Foo"), - Arguments.of(".foo", "foo"), - Arguments.of(".bar.Foo", "b.Foo"), - Arguments.of("org...bar..Foo", "ob.Foo") + // null entry + Arguments.of(null, ""), + // empty entry + Arguments.of("", ""), + // all whitespace entry + Arguments.of(" \t ", ""), + // bad / invalid characters + Arguments.of("org.eclipse.Foo.\u0000", "oe.Foo"), + Arguments.of("org.eclipse.\u20ac.Euro", "oe\u20ac.Euro"), + // bad package segments + Arguments.of(".foo", "foo"), + Arguments.of(".bar.Foo", "b.Foo"), + Arguments.of("org...bar..Foo", "ob.Foo"), + Arguments.of("org . . . bar . . Foo ", "ob.Foo"), + Arguments.of("org . . . bar . . Foo ", "ob.Foo"), + // long-ish classname + Arguments.of("org.eclipse.jetty.websocket.common.extensions.compress.DeflateFrameExtension", "oejwcec.DeflateFrameExtension"), + // internal class + Arguments.of("org.eclipse.jetty.foo.Bar$Internal", "oejf.Bar$Internal") ); } @ParameterizedTest @MethodSource("packageCases") - public void testCondensePackage(String input, String expected) + public void testCondensePackageViaLogger(String input, String expected) { StdErrLog log = new StdErrLog(); StdErrLog logger = (StdErrLog) log.newLogger(input); assertThat("log[" + input + "] condenses to name", logger._abbrevname, is(expected)); } + + @ParameterizedTest + @MethodSource("packageCases") + public void testCondensePackageDirect(String input, String expected) + { + assertThat("log[" + input + "] condenses to name", AbstractLogger.condensePackageString(input), is(expected)); + } } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java index 610c9b2c56f..a97ae15f82d 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java @@ -32,8 +32,8 @@ import org.eclipse.jetty.util.IO; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java index 44e7d8a49be..a0f0b3a3083 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java @@ -18,21 +18,6 @@ package org.eclipse.jetty.util.ssl; -import static org.eclipse.jetty.toolchain.test.matchers.RegexMatcher.matchesPattern; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - - import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; @@ -40,7 +25,6 @@ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; - import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -50,6 +34,22 @@ import org.eclipse.jetty.util.resource.Resource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.matchesRegex; +import static org.hamcrest.Matchers.not; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class SslContextFactoryTest { private SslContextFactory cf; @@ -57,7 +57,7 @@ public class SslContextFactoryTest @BeforeEach public void setUp() throws Exception { - cf = new SslContextFactory(); + cf = new SslContextFactory.Server(); java.security.cert.CertPathBuilder certPathBuilder = java.security.cert.CertPathBuilder.getInstance("PKIX"); java.security.cert.PKIXRevocationChecker revocationChecker = (java.security.cert.PKIXRevocationChecker)certPathBuilder.getRevocationChecker(); @@ -85,7 +85,7 @@ public class SslContextFactoryTest for(String enabledCipher : cipherDump.enabled) { - assertThat("Enabled Cipher Suite", enabledCipher, not(matchesPattern(".*_RSA_.*(SHA1|MD5|SHA)"))); + assertThat("Enabled Cipher Suite", enabledCipher, not(matchesRegex(".*_RSA_.*(SHA1|MD5|SHA)"))); } } @@ -325,18 +325,36 @@ public class SslContextFactoryTest @Test public void testNonDefaultKeyStoreTypeUsedForTrustStore() throws Exception { - cf = new SslContextFactory(); + cf = new SslContextFactory.Server(); cf.setKeyStoreResource(Resource.newSystemResource("keystore.p12")); cf.setKeyStoreType("pkcs12"); cf.setKeyStorePassword("storepwd"); cf.start(); cf.stop(); - cf = new SslContextFactory(); + cf = new SslContextFactory.Server(); cf.setKeyStoreResource(Resource.newSystemResource("keystore.jce")); cf.setKeyStoreType("jceks"); cf.setKeyStorePassword("storepwd"); cf.start(); cf.stop(); } + + @Test + public void testClientSslContextFactory() throws Exception + { + cf = new SslContextFactory.Client(); + cf.start(); + + assertEquals("HTTPS", cf.getEndpointIdentificationAlgorithm()); + } + + @Test + public void testServerSslContextFactory() throws Exception + { + cf = new SslContextFactory.Server(); + cf.start(); + + assertNull(cf.getEndpointIdentificationAlgorithm()); + } } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509Test.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509Test.java index d907889dacf..5a1454158f4 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509Test.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/X509Test.java @@ -18,13 +18,13 @@ package org.eclipse.jetty.util.ssl; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - import java.security.cert.X509Certificate; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + public class X509Test { @Test diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java index c896a1dd2f2..e03322d7b74 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/QueuedThreadPoolTest.java @@ -18,22 +18,32 @@ package org.eclipse.jetty.util.thread; -import org.eclipse.jetty.util.log.StacklessLogging; -import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool; -import org.junit.jupiter.api.Test; - +import java.io.Closeable; +import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.log.StacklessLogging; +import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.*; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class QueuedThreadPoolTest extends AbstractThreadPoolTest { + private static final Logger LOG = Log.getLogger(QueuedThreadPoolTest.class); private final AtomicInteger _jobs=new AtomicInteger(); private class RunningJob implements Runnable @@ -41,6 +51,29 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest private final CountDownLatch _run = new CountDownLatch(1); private final CountDownLatch _stopping = new CountDownLatch(1); private final CountDownLatch _stopped = new CountDownLatch(1); + private final String _name; + private final boolean _fail; + RunningJob() + { + this(null, false); + } + + public RunningJob(String name) + { + this(name, false); + } + + public RunningJob(boolean fail) + { + this(null, fail); + } + + public RunningJob(String name, boolean fail) + { + _name = name; + _fail = fail; + } + @Override public void run() { @@ -48,10 +81,16 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest { _run.countDown(); _stopping.await(); + if (_fail) + throw new IllegalStateException("Testing!"); + } + catch(IllegalStateException e) + { + throw e; } catch(Exception e) { - e.printStackTrace(); + LOG.debug(e); } finally { @@ -67,6 +106,25 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest if (!_stopped.await(10,TimeUnit.SECONDS)) throw new IllegalStateException(); } + + @Override + public String toString() + { + if (_name==null) + return super.toString(); + return String.format("%s@%x",_name,hashCode()); + } + } + + private class CloseableJob extends RunningJob implements Closeable + { + private final CountDownLatch _closed = new CountDownLatch(1); + + @Override + public void close() throws IOException + { + _closed.countDown(); + } } @Test @@ -84,44 +142,46 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest waitForThreads(tp,2); waitForIdle(tp,2); - // Doesn't shrink less than 1 - Thread.sleep(1100); + // Doesn't shrink to less than min threads + Thread.sleep(3*tp.getIdleTimeout()/2); waitForThreads(tp,2); waitForIdle(tp,2); // Run job0 - RunningJob job0=new RunningJob(); + RunningJob job0=new RunningJob("JOB0"); tp.execute(job0); assertTrue(job0._run.await(10,TimeUnit.SECONDS)); waitForIdle(tp,1); // Run job1 - RunningJob job1=new RunningJob(); + RunningJob job1=new RunningJob("JOB1"); tp.execute(job1); assertTrue(job1._run.await(10,TimeUnit.SECONDS)); - waitForThreads(tp,3); - waitForIdle(tp,1); + waitForThreads(tp,2); + waitForIdle(tp,0); // Run job2 - RunningJob job2=new RunningJob(); + RunningJob job2=new RunningJob("JOB2"); tp.execute(job2); assertTrue(job2._run.await(10,TimeUnit.SECONDS)); - waitForThreads(tp,4); - waitForIdle(tp,1); + waitForThreads(tp,3); + waitForIdle(tp,0); // Run job3 - RunningJob job3=new RunningJob(); + RunningJob job3=new RunningJob("JOB3"); tp.execute(job3); assertTrue(job3._run.await(10,TimeUnit.SECONDS)); waitForThreads(tp,4); + waitForIdle(tp,0); assertThat(tp.getIdleThreads(),is(0)); Thread.sleep(100); assertThat(tp.getIdleThreads(),is(0)); // Run job4. will be queued - RunningJob job4=new RunningJob(); + RunningJob job4=new RunningJob("JOB4"); tp.execute(job4); assertFalse(job4._run.await(1,TimeUnit.SECONDS)); + assertThat(tp.getThreads(),is(4)); // finish job 0 job0._stopping.countDown(); @@ -129,23 +189,220 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest // job4 should now run assertTrue(job4._run.await(10,TimeUnit.SECONDS)); - waitForThreads(tp,4); - waitForIdle(tp,0); - - // finish job 1,2,3,4 + assertThat(tp.getThreads(),is(4)); + assertThat(tp.getIdleThreads(),is(0)); + + // finish job 1 job1._stopping.countDown(); + assertTrue(job1._stopped.await(10,TimeUnit.SECONDS)); + waitForIdle(tp,1); + assertThat(tp.getThreads(),is(4)); + + // finish job 2,3,4 job2._stopping.countDown(); job3._stopping.countDown(); job4._stopping.countDown(); - assertTrue(job1._stopped.await(10,TimeUnit.SECONDS)); assertTrue(job2._stopped.await(10,TimeUnit.SECONDS)); assertTrue(job3._stopped.await(10,TimeUnit.SECONDS)); assertTrue(job4._stopped.await(10,TimeUnit.SECONDS)); - waitForThreads(tp,2); - waitForIdle(tp,2); + waitForIdle(tp,4); + assertThat(tp.getThreads(),is(4)); + + long duration = System.nanoTime(); + waitForThreads(tp,3); + assertThat(tp.getIdleThreads(),is(3)); + duration = System.nanoTime() - duration; + assertThat(TimeUnit.NANOSECONDS.toMillis(duration), Matchers.greaterThan(tp.getIdleTimeout()/2L)); + assertThat(TimeUnit.NANOSECONDS.toMillis(duration), Matchers.lessThan(tp.getIdleTimeout()*2L)); + + tp.stop(); } + @Test + public void testThreadPoolFailingJobs() throws Exception + { + QueuedThreadPool tp= new QueuedThreadPool(); + tp.setMinThreads(2); + tp.setMaxThreads(4); + tp.setIdleTimeout(900); + tp.setThreadsPriority(Thread.NORM_PRIORITY-1); + + try (StacklessLogging stackless = new StacklessLogging(QueuedThreadPool.class)) + { + tp.start(); + + // min threads started + waitForThreads(tp,2); + waitForIdle(tp,2); + + // Doesn't shrink to less than min threads + Thread.sleep(3*tp.getIdleTimeout()/2); + waitForThreads(tp,2); + waitForIdle(tp,2); + + // Run job0 + RunningJob job0=new RunningJob("JOB0", true); + tp.execute(job0); + assertTrue(job0._run.await(10,TimeUnit.SECONDS)); + waitForIdle(tp,1); + + // Run job1 + RunningJob job1=new RunningJob("JOB1", true); + tp.execute(job1); + assertTrue(job1._run.await(10,TimeUnit.SECONDS)); + waitForThreads(tp,2); + waitForIdle(tp,0); + + // Run job2 + RunningJob job2=new RunningJob("JOB2", true); + tp.execute(job2); + assertTrue(job2._run.await(10,TimeUnit.SECONDS)); + waitForThreads(tp,3); + waitForIdle(tp,0); + + // Run job3 + RunningJob job3=new RunningJob("JOB3", true); + tp.execute(job3); + assertTrue(job3._run.await(10,TimeUnit.SECONDS)); + waitForThreads(tp,4); + waitForIdle(tp,0); + assertThat(tp.getIdleThreads(),is(0)); + Thread.sleep(100); + assertThat(tp.getIdleThreads(),is(0)); + + // Run job4. will be queued + RunningJob job4=new RunningJob("JOB4", true); + tp.execute(job4); + assertFalse(job4._run.await(1,TimeUnit.SECONDS)); + + // finish job 0 + job0._stopping.countDown(); + assertTrue(job0._stopped.await(10,TimeUnit.SECONDS)); + + // job4 should now run + assertTrue(job4._run.await(10,TimeUnit.SECONDS)); + assertThat(tp.getThreads(),is(4)); + assertThat(tp.getIdleThreads(),is(0)); + + // finish job 1 + job1._stopping.countDown(); + assertTrue(job1._stopped.await(10,TimeUnit.SECONDS)); + waitForThreads(tp,3); + assertThat(tp.getIdleThreads(),is(0)); + + // finish job 2,3,4 + job2._stopping.countDown(); + job3._stopping.countDown(); + job4._stopping.countDown(); + assertTrue(job2._stopped.await(10,TimeUnit.SECONDS)); + assertTrue(job3._stopped.await(10,TimeUnit.SECONDS)); + assertTrue(job4._stopped.await(10,TimeUnit.SECONDS)); + + waitForIdle(tp,2); + waitForThreads(tp,2); + } + + tp.stop(); + } + + @Test + public void testExecuteNoIdleThreads() throws Exception + { + QueuedThreadPool tp= new QueuedThreadPool(); + tp.setDetailedDump(true); + tp.setMinThreads(1); + tp.setMaxThreads(10); + tp.setIdleTimeout(500); + + tp.start(); + + RunningJob job1 = new RunningJob(); + tp.execute(job1); + + RunningJob job2 = new RunningJob(); + tp.execute(job2); + + RunningJob job3 = new RunningJob(); + tp.execute(job3); + + // make sure these jobs have started running + assertTrue(job1._run.await(5, TimeUnit.SECONDS)); + assertTrue(job2._run.await(5, TimeUnit.SECONDS)); + assertTrue(job3._run.await(5, TimeUnit.SECONDS)); + + waitForThreads(tp, 3); + assertThat(tp.getIdleThreads(),is(0)); + + job1._stopping.countDown(); + assertTrue(job1._stopped.await(10,TimeUnit.SECONDS)); + waitForIdle(tp, 1); + assertThat(tp.getThreads(),is(3)); + + waitForIdle(tp, 0); + assertThat(tp.getThreads(),is(2)); + + RunningJob job4 = new RunningJob(); + tp.execute(job4); + assertTrue(job4._run.await(5, TimeUnit.SECONDS)); + + tp.stop(); + } + + @Test + public void testLifeCycleStop() throws Exception + { + QueuedThreadPool tp= new QueuedThreadPool(); + tp.setName("TestPool"); + tp.setMinThreads(1); + tp.setMaxThreads(2); + tp.setIdleTimeout(900); + tp.setStopTimeout(500); + tp.setThreadsPriority(Thread.NORM_PRIORITY-1); + tp.start(); + + // min threads started + waitForThreads(tp,1); + waitForIdle(tp,1); + + // Run job0 and job1 + RunningJob job0=new RunningJob(); + RunningJob job1=new RunningJob(); + tp.execute(job0); + tp.execute(job1); + + // Add more jobs (which should not be run) + RunningJob job2=new RunningJob(); + CloseableJob job3=new CloseableJob(); + RunningJob job4=new RunningJob(); + tp.execute(job2); + tp.execute(job3); + tp.execute(job4); + + // Wait until the first 2 start running + waitForThreads(tp,2); + waitForIdle(tp,0); + + // Queue should be empty after thread pool is stopped + tp.stop(); + assertThat(tp.getQueue().size(), is(0)); + + // First 2 jobs closed by InterruptedException + assertThat(job0._stopped.await(200, TimeUnit.MILLISECONDS), is(true)); + assertThat(job1._stopped.await(200, TimeUnit.MILLISECONDS), is(true)); + + // Verify RunningJobs in the queue have not been run + assertThat(job2._run.await(200, TimeUnit.MILLISECONDS), is(false)); + assertThat(job4._run.await(200, TimeUnit.MILLISECONDS), is(false)); + + // Verify ClosableJobs have not been run but have been closed + assertThat(job4._run.await(200, TimeUnit.MILLISECONDS), is(false)); + assertThat(job3._closed.await(200, TimeUnit.MILLISECONDS), is(true)); + + tp.stop(); + } + + @Test public void testShrink() throws Exception { @@ -189,6 +446,7 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest } waitForThreads(tp,2); waitForIdle(tp,2); + tp.stop(); } @Test @@ -236,6 +494,27 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest assertEquals(idle, tp.getIdleThreads()); } + + private void waitForReserved(QueuedThreadPool tp, int reserved) + { + long now=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); + long start=now; + ReservedThreadExecutor reservedThreadExecutor = tp.getBean(ReservedThreadExecutor.class); + while (reservedThreadExecutor.getAvailable()!=reserved && (now-start)<10000) + { + try + { + Thread.sleep(50); + } + catch(InterruptedException ignored) + { + } + now=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); + } + assertEquals(reserved, reservedThreadExecutor.getAvailable()); + } + + private void waitForThreads(QueuedThreadPool tp, int threads) { long now=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); @@ -272,6 +551,7 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest Thread.sleep(100); assertThat(tp.getThreads(),greaterThanOrEqualTo(5)); } + tp.stop(); } @Test @@ -296,10 +576,100 @@ public class QueuedThreadPoolTest extends AbstractThreadPoolTest }); } + @Test + public void testDump() throws Exception + { + QueuedThreadPool pool = new QueuedThreadPool(4, 3); + pool.setIdleTimeout(10000); + + String dump = pool.dump(); + // TODO use hamcrest 2.0 regex matcher + assertThat(dump,containsString("STOPPED")); + assertThat(dump,containsString(",3<=0<=4,i=0,r=-1,q=0")); + assertThat(dump,containsString("[NO_TRY]")); + + pool.setReservedThreads(2); + dump = pool.dump(); + assertThat(dump,containsString("STOPPED")); + assertThat(dump,containsString(",3<=0<=4,i=0,r=2,q=0")); + assertThat(dump,containsString("[NO_TRY]")); + + pool.start(); + waitForIdle(pool,3); + Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle + dump = pool.dump(); + assertThat(count(dump," - STARTED"),is(2)); + assertThat(dump,containsString(",3<=3<=4,i=3,r=2,q=0")); + assertThat(dump,containsString("[ReservedThreadExecutor@")); + assertThat(count(dump," IDLE "),is(3)); + assertThat(count(dump," RESERVED "),is(0)); + + CountDownLatch started = new CountDownLatch(1); + CountDownLatch waiting = new CountDownLatch(1); + pool.execute(()-> + { + try + { + started.countDown(); + waiting.await(); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + }); + started.await(); + Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle + dump = pool.dump(); + assertThat(count(dump," - STARTED"),is(2)); + assertThat(dump,containsString(",3<=3<=4,i=2,r=2,q=0")); + assertThat(dump,containsString("[ReservedThreadExecutor@")); + assertThat(count(dump," IDLE "),is(2)); + assertThat(count(dump," WAITING "),is(1)); + assertThat(count(dump," RESERVED "),is(0)); + assertThat(count(dump,"QueuedThreadPoolTest.lambda$testDump$"),is(0)); + + pool.setDetailedDump(true); + dump = pool.dump(); + assertThat(count(dump," - STARTED"),is(2)); + assertThat(dump,containsString(",3<=3<=4,i=2,r=2,q=0")); + assertThat(dump,containsString("s=0/2")); + assertThat(dump,containsString("[ReservedThreadExecutor@")); + assertThat(count(dump," IDLE "),is(2)); + assertThat(count(dump," WAITING "),is(1)); + assertThat(count(dump," RESERVED "),is(0)); + assertThat(count(dump,"QueuedThreadPoolTest.lambda$testDump$"),is(1)); + + assertFalse(pool.tryExecute(()->{})); + waitForReserved(pool,1); + Thread.sleep(250); // TODO need to give time for threads to read idle poll after setting idle + dump = pool.dump(); + assertThat(count(dump," - STARTED"),is(2)); + assertThat(dump,containsString(",3<=3<=4,i=1,r=2,q=0")); + assertThat(dump,containsString("s=1/2")); + assertThat(dump,containsString("[ReservedThreadExecutor@")); + assertThat(count(dump," IDLE "),is(1)); + assertThat(count(dump," WAITING "),is(1)); + assertThat(count(dump," RESERVED "),is(1)); + assertThat(count(dump,"QueuedThreadPoolTest.lambda$testDump$"),is(1)); + } + + private int count(String s, String p) + { + int c = 0; + int i = s.indexOf(p); + while (i>=0) + { + c++; + i = s.indexOf(p, i+1); + } + return c; + } + @Override protected SizedThreadPool newPool(int max) { return new QueuedThreadPool(max); } - + } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SerializedExecutorTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SerializedExecutorTest.java new file mode 100644 index 00000000000..4facf1e73f4 --- /dev/null +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/SerializedExecutorTest.java @@ -0,0 +1,92 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.thread; + +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SerializedExecutorTest +{ + @Test + public void test() throws Exception + { + int threads = 64; + int loops = 1000; + int depth = 100; + + AtomicInteger ran = new AtomicInteger(); + AtomicBoolean running = new AtomicBoolean(); + SerializedExecutor executor = new SerializedExecutor(); + CountDownLatch start = new CountDownLatch(1); + CountDownLatch stop = new CountDownLatch(threads); + Random random = new Random(); + + for (int t = threads; t-- > 0; ) + { + new Thread(() -> + { + try + { + start.await(); + + for (int l = loops; l-- > 0; ) + { + final AtomicInteger d = new AtomicInteger(depth); + executor.execute(new Runnable() + { + @Override + public void run() + { + ran.incrementAndGet(); + if (!running.compareAndSet(false, true)) + throw new IllegalStateException(); + if (d.decrementAndGet() > 0) + executor.execute(this); + if (!running.compareAndSet(true, false)) + throw new IllegalStateException(); + } + }); + Thread.sleep(random.nextInt(5)); + } + } + catch (Throwable th) + { + th.printStackTrace(); + } + finally + { + stop.countDown(); + } + }).start(); + } + + start.countDown(); + assertTrue(stop.await(30, TimeUnit.SECONDS)); + assertThat(ran.get(), Matchers.is(threads * loops * depth)); + } +} diff --git a/jetty-webapp/src/main/config/etc/jetty-webapp.xml b/jetty-webapp/src/main/config/etc/jetty-webapp.xml index 1f8e737e9bf..25892b21cb5 100644 --- a/jetty-webapp/src/main/config/etc/jetty-webapp.xml +++ b/jetty-webapp/src/main/config/etc/jetty-webapp.xml @@ -1,5 +1,5 @@ - + diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClassMatcher.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClassMatcher.java index 3f45fb5d7a2..eb03afca509 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClassMatcher.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClassMatcher.java @@ -23,6 +23,7 @@ import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; @@ -36,6 +37,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; +import java.util.function.Supplier; import org.eclipse.jetty.util.ArrayTernaryTrie; import org.eclipse.jetty.util.IncludeExcludeSet; @@ -708,21 +710,7 @@ public class ClassMatcher extends AbstractSet { try { - Boolean byName = _patterns.isIncludedAndNotExcluded(clazz.getName()); - if (Boolean.FALSE.equals(byName)) - return byName; // Already excluded so no need to check location. - URI location = TypeUtil.getLocationOfClass(clazz); - Boolean byLocation = location == null ? null - : _locations.isIncludedAndNotExcluded(location); - - if (LOG.isDebugEnabled()) - LOG.debug("match {} from {} byName={} byLocation={} in {}",clazz,location,byName,byLocation,this); - - // Combine the tri-state match of both IncludeExclude Sets - boolean included = Boolean.TRUE.equals(byName) || Boolean.TRUE.equals(byLocation) - || (byName==null && !_patterns.hasIncludes() && byLocation==null && !_locations.hasIncludes()); - boolean excluded = Boolean.FALSE.equals(byName) || Boolean.FALSE.equals(byLocation); - return included && !excluded; + return combine(_patterns, clazz.getName(), _locations, ()->TypeUtil.getLocationOfClass(clazz)); } catch (Exception e) { @@ -740,29 +728,33 @@ public class ClassMatcher extends AbstractSet // Treat path elements as packages for name matching name=name.replace("/","."); - Boolean byName = _patterns.isIncludedAndNotExcluded(name); - if (Boolean.FALSE.equals(byName)) - return byName; // Already excluded so no need to check location. - - // Try to find a file path for location matching - Boolean byLocation = null; - try + return combine(_patterns, name, _locations, ()-> { - URI jarUri = URIUtil.getJarSource(url.toURI()); - if ("file".equalsIgnoreCase(jarUri.getScheme())) + try { - byLocation = _locations.isIncludedAndNotExcluded(jarUri); + return URIUtil.getJarSource(url.toURI()); } - } - catch(Exception e) - { - LOG.ignore(e); - } + catch (URISyntaxException e) + { + LOG.ignore(e); + return null; + } + }); + } + + private static boolean combine(IncludeExcludeSet names, String name, IncludeExcludeSet locations, Supplier location) + { + Boolean byName = names.isIncludedAndNotExcluded(name); + if (Boolean.FALSE==byName) + return false; + + Boolean byLocation = locations.isIncludedAndNotExcluded(location.get()); + if (Boolean.FALSE==byLocation) + return false; + + return Boolean.TRUE.equals(byName) + || Boolean.TRUE.equals(byLocation) + || !(names.hasIncludes() || locations.hasIncludes()); + } - // Combine the tri-state match of both IncludeExclude Sets - boolean included = Boolean.TRUE.equals(byName) || Boolean.TRUE.equals(byLocation) - || (byName==null && !_patterns.hasIncludes() && byLocation==null && !_locations.hasIncludes()); - boolean excluded = Boolean.FALSE.equals(byName) || Boolean.FALSE.equals(byLocation); - return included && !excluded; - } } diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JettyWebXmlConfiguration.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JettyWebXmlConfiguration.java index e0d5a3b14fa..a6963dddc79 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JettyWebXmlConfiguration.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JettyWebXmlConfiguration.java @@ -78,7 +78,7 @@ public class JettyWebXmlConfiguration extends AbstractConfiguration Object xml_attr=context.getAttribute(XML_CONFIGURATION); context.removeAttribute(XML_CONFIGURATION); - final XmlConfiguration jetty_config = xml_attr instanceof XmlConfiguration?(XmlConfiguration)xml_attr:new XmlConfiguration(jetty.getURI().toURL()); + final XmlConfiguration jetty_config = xml_attr instanceof XmlConfiguration?(XmlConfiguration)xml_attr:new XmlConfiguration(jetty); setupXmlConfiguration(context, jetty_config, web_inf); diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppClassLoader.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppClassLoader.java index f75b0436156..910c33eb8b6 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppClassLoader.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppClassLoader.java @@ -39,6 +39,7 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.CopyOnWriteArrayList; +import org.eclipse.jetty.util.ClassVisibilityChecker; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; @@ -65,7 +66,7 @@ import org.eclipse.jetty.util.resource.ResourceCollection; * context classloader will be used. If that is null then the * classloader that loaded this class is used as the parent. */ -public class WebAppClassLoader extends URLClassLoader +public class WebAppClassLoader extends URLClassLoader implements ClassVisibilityChecker { static { @@ -85,7 +86,7 @@ public class WebAppClassLoader extends URLClassLoader /* ------------------------------------------------------------ */ /** The Context in which the classloader operates. */ - public interface Context + public interface Context extends ClassVisibilityChecker { /* ------------------------------------------------------------ */ /** Convert a URL or path to a Resource. @@ -103,26 +104,6 @@ public class WebAppClassLoader extends URLClassLoader */ PermissionCollection getPermissions(); - /* ------------------------------------------------------------ */ - /** Is the class a System Class. - * A System class is a class that is visible to a webapplication, - * but that cannot be overridden by the contents of WEB-INF/lib or - * WEB-INF/classes - * @param clazz The fully qualified name of the class. - * @return True if the class is a system class. - */ - boolean isSystemClass(Class clazz); - - /* ------------------------------------------------------------ */ - /** Is the class a Server Class. - * A Server class is a class that is part of the implementation of - * the server and is NIT visible to a webapplication. The web - * application may provide it's own implementation of the class, - * to be loaded from WEB-INF/lib or WEB-INF/classes - * @param clazz The fully qualified name of the class. - * @return True if the class is a server class. - */ - boolean isServerClass(Class clazz); /* ------------------------------------------------------------ */ /** @@ -280,20 +261,29 @@ public class WebAppClassLoader extends URLClassLoader while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken().trim(); - Resource resource= _context.newResource(token); - if (LOG.isDebugEnabled()) - LOG.debug("Path resource=" + resource); if(token.endsWith("*")) { if(token.length() > 1) { token = token.substring(0, token.length() - 1); + Resource resource= _context.newResource(token); + if (LOG.isDebugEnabled()) + LOG.debug("Glob Path resource=" + resource); resource= _context.newResource(token); addJars(resource); } - } else if (resource.isDirectory() && resource instanceof ResourceCollection) + return; + } + + Resource resource= _context.newResource(token); + if (LOG.isDebugEnabled()) + LOG.debug("Path resource=" + resource); + + if (resource.isDirectory() && resource instanceof ResourceCollection) + { addClassPath(resource); + } else { // Resolve file path if possible @@ -713,5 +703,17 @@ public class WebAppClassLoader extends URLClassLoader { return "WebAppClassLoader=" + _name+"@"+Long.toHexString(hashCode()); } + + @Override + public boolean isSystemClass(Class clazz) + { + return _context.isSystemClass(clazz); + } + + @Override + public boolean isServerClass(Class clazz) + { + return _context.isServerClass(clazz); + } } diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppConfiguration.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppConfiguration.java index e02d6ba14f1..a3f58df5cb5 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppConfiguration.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppConfiguration.java @@ -32,14 +32,10 @@ public class WebAppConfiguration extends AbstractConfiguration addDependencies(WebXmlConfiguration.class, MetaInfConfiguration.class, WebInfConfiguration.class); addDependents(JettyWebXmlConfiguration.class); protectAndExpose( - "org.eclipse.jetty.util.log.", - "org.eclipse.jetty.server.session.SessionData", - "org.eclipse.jetty.servlet.StatisticsServlet", + "org.eclipse.jetty.servlet.StatisticsServlet", "org.eclipse.jetty.servlet.DefaultServlet", - "org.eclipse.jetty.servlet.NoJspServlet", - "org.eclipse.jetty.continuation."); - expose( // TODO Evaluate why these are not protectAndExpose? - "org.eclipse.jetty.servlet.listener.", - "org.eclipse.jetty.alpn."); + "org.eclipse.jetty.servlet.NoJspServlet" + ); + expose("org.eclipse.jetty.servlet.listener."); } } diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java index a4faac87568..632bf4d5c09 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java @@ -931,7 +931,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL protected void loadConfigurations() { //if the configuration instances have been set explicitly, use them - if (_configurations.size()>0) + if (!_configurations.isEmpty()) return; _configurations.add(Configurations.getServerDefault(getServer()).toArray()); diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionListener.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionListener.java new file mode 100644 index 00000000000..c0444899a68 --- /dev/null +++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionListener.java @@ -0,0 +1,26 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.jsr356; + +public interface JsrSessionListener +{ + void onSessionOpened(JsrSession session); + + void onSessionClosed(JsrSession session); +} diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionTracker.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionTracker.java new file mode 100644 index 00000000000..be97c94d05b --- /dev/null +++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionTracker.java @@ -0,0 +1,58 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.jsr356; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; + +public class JsrSessionTracker extends AbstractLifeCycle implements JsrSessionListener +{ + private CopyOnWriteArraySet sessions = new CopyOnWriteArraySet<>(); + + public Set getSessions() + { + return Collections.unmodifiableSet(sessions); + } + + @Override + public void onSessionOpened(JsrSession session) + { + sessions.add(session); + } + + @Override + public void onSessionClosed(JsrSession session) + { + sessions.remove(session); + } + + @Override + protected void doStop() throws Exception + { + for (JsrSession session : sessions) + { + LifeCycle.stop(session); + } + super.doStop(); + } +} \ No newline at end of file diff --git a/jetty-websocket/javax-websocket-client/pom.xml b/jetty-websocket/javax-websocket-client/pom.xml index 3a9dfbda99f..01c903ec8d9 100644 --- a/jetty-websocket/javax-websocket-client/pom.xml +++ b/jetty-websocket/javax-websocket-client/pom.xml @@ -28,8 +28,8 @@ ${project.version} - javax.websocket - javax.websocket-api + org.eclipse.jetty.toolchain + jetty-javax-websocket-api org.eclipse.jetty.toolchain diff --git a/jetty-websocket/javax-websocket-client/src/main/java/module-info.java b/jetty-websocket/javax-websocket-client/src/main/java/module-info.java index b70e44d315a..98e3d7ae491 100644 --- a/jetty-websocket/javax-websocket-client/src/main/java/module-info.java +++ b/jetty-websocket/javax-websocket-client/src/main/java/module-info.java @@ -24,7 +24,7 @@ module org.eclipse.jetty.websocket.javax.client { exports org.eclipse.jetty.websocket.javax.client; - requires javax.websocket.api; + requires jetty.websocket.api; requires org.eclipse.jetty.client; requires org.eclipse.jetty.http; requires org.eclipse.jetty.io; diff --git a/jetty-websocket/javax-websocket-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxClientUpgradeRequest.java b/jetty-websocket/javax-websocket-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxClientUpgradeRequest.java index b3ee91af5dc..2494f81b862 100644 --- a/jetty-websocket/javax-websocket-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxClientUpgradeRequest.java +++ b/jetty-websocket/javax-websocket-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxClientUpgradeRequest.java @@ -19,52 +19,41 @@ package org.eclipse.jetty.websocket.javax.client; import java.net.URI; -import java.util.concurrent.CompletableFuture; - -import javax.websocket.Session; import org.eclipse.jetty.client.HttpResponse; +import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandler; import org.eclipse.jetty.websocket.javax.common.UpgradeRequest; -import org.eclipse.jetty.websocket.javax.common.UpgradeResponse; public class JavaxClientUpgradeRequest extends ClientUpgradeRequest { private final JavaxWebSocketClientContainer containerContext; - private final Object websocketPojo; - private final CompletableFuture futureSession; + private final JavaxWebSocketFrameHandler frameHandler; + public JavaxClientUpgradeRequest(JavaxWebSocketClientContainer clientContainer, WebSocketCoreClient coreClient, URI requestURI, Object websocketPojo) { super(coreClient, requestURI); this.containerContext = clientContainer; - this.websocketPojo = websocketPojo; - this.futureSession = new CompletableFuture<>(); - } - @Override - protected void handleException(Throwable failure) - { - super.handleException(failure); - futureSession.completeExceptionally(failure); - } - - @Override - public FrameHandler getFrameHandler(WebSocketCoreClient coreClient, HttpResponse response) - { UpgradeRequest upgradeRequest = new DelegatedJavaxClientUpgradeRequest(this); - UpgradeResponse upgradeResponse = new DelegatedJavaxClientUpgradeResponse(response); + frameHandler = containerContext.newFrameHandler(websocketPojo, upgradeRequest); + } - JavaxWebSocketFrameHandler frameHandler = containerContext.newFrameHandler(websocketPojo, upgradeRequest, upgradeResponse, futureSession); + @Override + public void upgrade(HttpResponse response, HttpConnectionOverHTTP httpConnection) + { + frameHandler.setUpgradeRequest(new DelegatedJavaxClientUpgradeRequest(this)); + frameHandler.setUpgradeResponse(new DelegatedJavaxClientUpgradeResponse(response)); + super.upgrade(response, httpConnection); + } + @Override + public FrameHandler getFrameHandler() + { return frameHandler; } - - public CompletableFuture getFutureSession() - { - return futureSession; - } } diff --git a/jetty-websocket/javax-websocket-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketClientContainer.java b/jetty-websocket/javax-websocket-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketClientContainer.java index 9d0b128e01e..3dc118a4907 100644 --- a/jetty-websocket/javax-websocket-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketClientContainer.java +++ b/jetty-websocket/javax-websocket-client/src/main/java/org/eclipse/jetty/websocket/javax/client/JavaxWebSocketClientContainer.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.net.URI; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -37,15 +38,14 @@ import javax.websocket.Extension; import javax.websocket.Session; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.annotation.ManagedObject; -import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; +import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.javax.common.ConfiguredEndpoint; import org.eclipse.jetty.websocket.javax.common.InvalidWebSocketException; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketContainer; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketExtensionConfig; +import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandler; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerFactory; /** @@ -59,47 +59,29 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple protected WebSocketCoreClient coreClient; protected Supplier coreClientFactory; private final JavaxWebSocketClientFrameHandlerFactory frameHandlerFactory; - private DecoratedObjectFactory objectFactory; - private WebSocketExtensionRegistry extensionRegistry; public JavaxWebSocketClientContainer() { - this(() -> { - WebSocketCoreClient coreClient = new WebSocketCoreClient(); + this(new WebSocketComponents()); + } + + public JavaxWebSocketClientContainer(WebSocketComponents components) + { + this(components, ()-> + { + WebSocketCoreClient coreClient = new WebSocketCoreClient(components); coreClient.getHttpClient().setName("Javax-WebSocketClient@" + Integer.toHexString(coreClient.getHttpClient().hashCode())); return coreClient; }); } - public JavaxWebSocketClientContainer(Supplier coreClientFactory) + public JavaxWebSocketClientContainer(WebSocketComponents components, Supplier coreClientFactory) { - this((WebSocketCoreClient)null); + super(components); this.coreClientFactory = coreClientFactory; - this.addBean(coreClientFactory); - } - - public JavaxWebSocketClientContainer(WebSocketCoreClient coreClient) - { - super(); - this.coreClient = coreClient; - this.addBean(coreClient); - this.objectFactory = new DecoratedObjectFactory(); - this.extensionRegistry = new WebSocketExtensionRegistry(); this.frameHandlerFactory = new JavaxWebSocketClientFrameHandlerFactory(this); } - @Override - public JavaxWebSocketFrameHandlerFactory getFrameHandlerFactory() - { - return frameHandlerFactory; - } - - @Override - protected WebSocketExtensionRegistry getExtensionRegistry() - { - return this.extensionRegistry; - } - protected HttpClient getHttpClient() { return getWebSocketCoreClient().getHttpClient(); @@ -110,10 +92,7 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple if (coreClient == null) { coreClient = coreClientFactory.get(); - if (coreClient.isRunning()) - addBean(coreClient,false); - else - addManaged(coreClient); + addManaged(coreClient); } return coreClient; @@ -127,17 +106,30 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple */ private CompletableFuture connect(JavaxClientUpgradeRequest upgradeRequest) { - CompletableFuture fut = upgradeRequest.getFutureSession(); + upgradeRequest.setConfiguration(defaultCustomizer); + CompletableFuture futureSession = new CompletableFuture<>(); + try { - getWebSocketCoreClient().connect(upgradeRequest); - return fut; + WebSocketCoreClient coreClient = getWebSocketCoreClient(); + coreClient.connect(upgradeRequest).whenComplete((coreSession, error)-> + { + if (error != null) + { + futureSession.completeExceptionally(error); + return; + } + + JavaxWebSocketFrameHandler frameHandler = (JavaxWebSocketFrameHandler)upgradeRequest.getFrameHandler(); + futureSession.complete(frameHandler.getSession()); + }); } catch (Exception e) { - fut.completeExceptionally(e); - return fut; + futureSession.completeExceptionally(e); } + + return futureSession; } private Session connect(ConfiguredEndpoint configuredEndpoint, URI destURI) throws IOException @@ -148,7 +140,7 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple JavaxClientUpgradeRequest upgradeRequest = new JavaxClientUpgradeRequest(this, getWebSocketCoreClient(), destURI, configuredEndpoint); EndpointConfig config = configuredEndpoint.getConfig(); - if (config != null && config instanceof ClientEndpointConfig) + if (config instanceof ClientEndpointConfig) { ClientEndpointConfig clientEndpointConfig = (ClientEndpointConfig)config; @@ -156,41 +148,41 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple upgradeRequest.addListener(jsrUpgradeListener); for (Extension ext : clientEndpointConfig.getExtensions()) - { - if (!getExtensionRegistry().isAvailable(ext.getName())) - { - throw new IllegalArgumentException("Requested extension [" + ext.getName() + "] is not installed"); - } upgradeRequest.addExtensions(new JavaxWebSocketExtensionConfig(ext)); - } if (clientEndpointConfig.getPreferredSubprotocols().size() > 0) - { upgradeRequest.setSubProtocols(clientEndpointConfig.getPreferredSubprotocols()); - } } + long timeout = getWebSocketCoreClient().getHttpClient().getConnectTimeout(); try { Future sessionFuture = connect(upgradeRequest); - long timeout = coreClient.getHttpClient().getConnectTimeout(); if (timeout>0) return sessionFuture.get(timeout+1000, TimeUnit.MILLISECONDS); return sessionFuture.get(); } + catch (ExecutionException e) + { + var cause = e.getCause(); + if (cause instanceof RuntimeException) + throw (RuntimeException)cause; + if (cause instanceof IOException) + throw (IOException)cause; + throw new IOException(cause); + } catch (TimeoutException e) { - throw new IOException("Connection future not completed " + destURI, e); + throw new IOException("Connection future timeout " + timeout + " ms for " + destURI, e); } - catch (Exception e) + catch (Throwable e) { throw new IOException("Unable to connect to " + destURI, e); } } @Override - public Session connectToServer(final Class endpointClass, final ClientEndpointConfig config, URI path) - throws DeploymentException, IOException + public Session connectToServer(final Class endpointClass, final ClientEndpointConfig config, URI path) throws IOException { ClientEndpointConfig clientEndpointConfig = config; if (clientEndpointConfig == null) @@ -202,7 +194,7 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple } @Override - public Session connectToServer(final Class annotatedEndpointClass, final URI path) throws DeploymentException, IOException + public Session connectToServer(final Class annotatedEndpointClass, final URI path) throws IOException { ConfiguredEndpoint instance = newConfiguredEndpoint(annotatedEndpointClass, new EmptyClientEndpointConfig()); return connect(instance, path); @@ -228,21 +220,9 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple } @Override - public long getDefaultMaxSessionIdleTimeout() + public JavaxWebSocketFrameHandlerFactory getFrameHandlerFactory() { - return getHttpClient().getIdleTimeout(); - } - - @Override - public void setDefaultMaxSessionIdleTimeout(long timeout) - { - getHttpClient().setIdleTimeout(timeout); - } - - @Override - public ByteBufferPool getBufferPool() - { - return getHttpClient().getByteBufferPool(); + return frameHandlerFactory; } @Override @@ -255,19 +235,14 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple { try { - return newConfiguredEndpoint(endpointClass.newInstance(), config); + return newConfiguredEndpoint(endpointClass.getConstructor().newInstance(), config); } - catch (DeploymentException | InstantiationException | IllegalAccessException e) + catch (Throwable e) { - throw new InvalidWebSocketException("Unable to instantiate websocket: " + endpointClass.getClass()); + throw new InvalidWebSocketException("Unable to instantiate websocket: " + endpointClass.getName()); } } - public DecoratedObjectFactory getObjectFactory() - { - return objectFactory; - } - public ConfiguredEndpoint newConfiguredEndpoint(Object endpoint, EndpointConfig providedConfig) throws DeploymentException { EndpointConfig config = providedConfig; diff --git a/jetty-websocket/javax-websocket-common/pom.xml b/jetty-websocket/javax-websocket-common/pom.xml index eee29af0122..2b26c2b8456 100644 --- a/jetty-websocket/javax-websocket-common/pom.xml +++ b/jetty-websocket/javax-websocket-common/pom.xml @@ -74,8 +74,8 @@ ${project.version} - javax.websocket - javax.websocket-api + org.eclipse.jetty.toolchain + jetty-javax-websocket-api diff --git a/jetty-websocket/javax-websocket-common/src/main/java/module-info.java b/jetty-websocket/javax-websocket-common/src/main/java/module-info.java index f0a3bbce74d..85af50b04e0 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/module-info.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/module-info.java @@ -24,7 +24,7 @@ module org.eclipse.jetty.websocket.javax.common exports org.eclipse.jetty.websocket.javax.common.messages; exports org.eclipse.jetty.websocket.javax.common.util; - requires javax.websocket.api; + requires jetty.websocket.api; requires org.eclipse.jetty.http; requires org.eclipse.jetty.io; requires org.eclipse.jetty.util; diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketAsyncRemote.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketAsyncRemote.java index 586a646df19..6aa04e80e4c 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketAsyncRemote.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketAsyncRemote.java @@ -18,6 +18,15 @@ package org.eclipse.jetty.websocket.javax.common; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.Future; + +import javax.websocket.EncodeException; +import javax.websocket.Encoder; +import javax.websocket.SendHandler; +import javax.websocket.SendResult; + import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.FutureCallback; import org.eclipse.jetty.util.log.Log; @@ -29,35 +38,25 @@ import org.eclipse.jetty.websocket.javax.common.messages.MessageOutputStream; import org.eclipse.jetty.websocket.javax.common.messages.MessageWriter; import org.eclipse.jetty.websocket.javax.common.util.TextUtil; -import javax.websocket.EncodeException; -import javax.websocket.Encoder; -import javax.websocket.SendHandler; -import javax.websocket.SendResult; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.concurrent.Future; - public class JavaxWebSocketAsyncRemote extends JavaxWebSocketRemoteEndpoint implements javax.websocket.RemoteEndpoint.Async { static final Logger LOG = Log.getLogger(JavaxWebSocketAsyncRemote.class); - protected JavaxWebSocketAsyncRemote(JavaxWebSocketSession session, FrameHandler.CoreSession channel) + protected JavaxWebSocketAsyncRemote(JavaxWebSocketSession session, FrameHandler.CoreSession coreSession) { - super(session, channel); + super(session, coreSession); } @Override public long getSendTimeout() { - // TODO: verify that JSR356 "send timeout" means the same as connection idle timeout - return getIdleTimeout(); + return getWriteTimeout(); } @Override public void setSendTimeout(long timeoutmillis) { - // TODO: verify that JSR356 "send timeout" means the same as connection idle timeout - setIdleTimeout(timeoutmillis); + setWriteTimeout(timeoutmillis); } @Override diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketBasicRemote.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketBasicRemote.java index 05c0f91ebde..8c30b6acf6a 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketBasicRemote.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketBasicRemote.java @@ -18,6 +18,14 @@ package org.eclipse.jetty.websocket.javax.common; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; + +import javax.websocket.EncodeException; +import javax.websocket.RemoteEndpoint; + import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.SharedBlockingCallback; import org.eclipse.jetty.util.log.Log; @@ -27,22 +35,15 @@ import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.javax.common.util.TextUtil; -import javax.websocket.EncodeException; -import javax.websocket.RemoteEndpoint; -import java.io.IOException; -import java.io.OutputStream; -import java.io.Writer; -import java.nio.ByteBuffer; - import static java.nio.charset.StandardCharsets.UTF_8; public class JavaxWebSocketBasicRemote extends JavaxWebSocketRemoteEndpoint implements RemoteEndpoint.Basic { private static final Logger LOG = Log.getLogger(JavaxWebSocketBasicRemote.class); - protected JavaxWebSocketBasicRemote(JavaxWebSocketSession session, FrameHandler.CoreSession channel) + protected JavaxWebSocketBasicRemote(JavaxWebSocketSession session, FrameHandler.CoreSession coreSession) { - super(session, channel); + super(session, coreSession); } @Override diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketContainer.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketContainer.java index e7185b1fc05..e7bd960a24d 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketContainer.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketContainer.java @@ -18,15 +18,15 @@ package org.eclipse.jetty.websocket.javax.common; +import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; + import javax.websocket.Extension; -import javax.websocket.Session; import javax.websocket.WebSocketContainer; import org.eclipse.jetty.io.ByteBufferPool; @@ -34,59 +34,97 @@ import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.FrameHandler; +import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; public abstract class JavaxWebSocketContainer extends ContainerLifeCycle implements javax.websocket.WebSocketContainer { private final static Logger LOG = Log.getLogger(JavaxWebSocketContainer.class); private final SessionTracker sessionTracker = new SessionTracker(); - private long defaultAsyncSendTimeout = -1; - private int defaultMaxBinaryMessageBufferSize = 64 * 1024; - private int defaultMaxTextMessageBufferSize = 64 * 1024; private List sessionListeners = new ArrayList<>(); + protected FrameHandler.ConfigurationCustomizer defaultCustomizer = new FrameHandler.ConfigurationCustomizer(); + private WebSocketComponents components; - public JavaxWebSocketContainer() + public JavaxWebSocketContainer(WebSocketComponents components) { + this.components = components; addSessionListener(sessionTracker); addBean(sessionTracker); } - public abstract ByteBufferPool getBufferPool(); + public abstract Executor getExecutor(); + + protected abstract JavaxWebSocketFrameHandlerFactory getFrameHandlerFactory(); + + public ByteBufferPool getBufferPool() + { + return components.getBufferPool(); + } + + public WebSocketExtensionRegistry getExtensionRegistry() + { + return components.getExtensionRegistry(); + } + + public DecoratedObjectFactory getObjectFactory() + { + return components.getObjectFactory(); + } - @Override public long getDefaultAsyncSendTimeout() { - return this.defaultAsyncSendTimeout; + return defaultCustomizer.getWriteTimeout().toMillis(); } @Override public int getDefaultMaxBinaryMessageBufferSize() { - return this.defaultMaxBinaryMessageBufferSize; + long max = defaultCustomizer.getMaxBinaryMessageSize(); + if (max > (long)Integer.MAX_VALUE) + return Integer.MAX_VALUE; + return (int)max; } - public abstract DecoratedObjectFactory getObjectFactory(); - @Override - public void setDefaultMaxBinaryMessageBufferSize(int max) + public long getDefaultMaxSessionIdleTimeout() { - this.defaultMaxBinaryMessageBufferSize = max; + return defaultCustomizer.getIdleTimeout().toMillis(); } @Override public int getDefaultMaxTextMessageBufferSize() { - return this.defaultMaxTextMessageBufferSize; + long max = defaultCustomizer.getMaxTextMessageSize(); + if (max > (long)Integer.MAX_VALUE) + return Integer.MAX_VALUE; + return (int)max; + } + + @Override + public void setAsyncSendTimeout(long ms) + { + defaultCustomizer.setWriteTimeout(Duration.ofMillis(ms)); + } + + @Override + public void setDefaultMaxBinaryMessageBufferSize(int max) + { + defaultCustomizer.setMaxBinaryMessageSize(max); + } + + @Override + public void setDefaultMaxSessionIdleTimeout(long ms) + { + defaultCustomizer.setIdleTimeout(Duration.ofMillis(ms)); } @Override public void setDefaultMaxTextMessageBufferSize(int max) { - this.defaultMaxTextMessageBufferSize = max; + defaultCustomizer.setMaxTextMessageSize(max); } - public abstract Executor getExecutor(); - /** * {@inheritDoc} * @@ -116,22 +154,11 @@ public abstract class JavaxWebSocketContainer extends ContainerLifeCycle impleme return sessionTracker.getSessions(); } - public JavaxWebSocketFrameHandler newFrameHandler(Object websocketPojo, UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse, - CompletableFuture futureSession) + public JavaxWebSocketFrameHandler newFrameHandler(Object websocketPojo, UpgradeRequest upgradeRequest) { - return getFrameHandlerFactory().newJavaxWebSocketFrameHandler(websocketPojo, upgradeRequest, upgradeResponse, futureSession); + return getFrameHandlerFactory().newJavaxWebSocketFrameHandler(websocketPojo, upgradeRequest); } - @Override - public void setAsyncSendTimeout(long timeoutInMillis) - { - this.defaultAsyncSendTimeout = timeoutInMillis; - } - - protected abstract WebSocketExtensionRegistry getExtensionRegistry(); - - protected abstract JavaxWebSocketFrameHandlerFactory getFrameHandlerFactory(); - /** * Register a WebSocketSessionListener with the container * diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java index 0a2c8885ad3..70907557192 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler.java @@ -27,14 +27,13 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; + import javax.websocket.CloseReason; import javax.websocket.Decoder; import javax.websocket.EndpointConfig; import javax.websocket.MessageHandler; import javax.websocket.PongMessage; -import javax.websocket.Session; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -45,7 +44,6 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.ProtocolException; -import org.eclipse.jetty.websocket.core.WebSocketConstants; import org.eclipse.jetty.websocket.core.WebSocketException; import org.eclipse.jetty.websocket.javax.common.decoders.AvailableDecoders; import org.eclipse.jetty.websocket.javax.common.messages.DecodedBinaryMessageSink; @@ -96,22 +94,14 @@ public class JavaxWebSocketFrameHandler implements FrameHandler private JavaxWebSocketFrameHandlerMetadata.MessageMetadata binaryMetadata; // TODO: need pingHandle ? private MethodHandle pongHandle; - /** - * Immutable HandshakeRequest available via Session - */ - private final UpgradeRequest upgradeRequest; - /** - * Immutable javax.websocket.HandshakeResponse available via Session - */ - private final UpgradeResponse upgradeResponse; - private final String id; + + private UpgradeRequest upgradeRequest; + private UpgradeResponse upgradeResponse; + private final EndpointConfig endpointConfig; - private final CompletableFuture futureSession; private MessageSink textSink; private MessageSink binarySink; private MessageSink activeMessageSink; - private int maxTextMessageBufferSize = WebSocketConstants.DEFAULT_MAX_TEXT_MESSAGE_SIZE; - private int maxBinaryMessageBufferSize = WebSocketConstants.DEFAULT_MAX_BINARY_MESSAGE_SIZE; private JavaxWebSocketSession session; private Map messageHandlerMap; private CoreSession coreSession; @@ -120,14 +110,11 @@ public class JavaxWebSocketFrameHandler implements FrameHandler public JavaxWebSocketFrameHandler(JavaxWebSocketContainer container, Object endpointInstance, - UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse, MethodHandle openHandle, MethodHandle closeHandle, MethodHandle errorHandle, JavaxWebSocketFrameHandlerMetadata.MessageMetadata textMetadata, JavaxWebSocketFrameHandlerMetadata.MessageMetadata binaryMetadata, MethodHandle pongHandle, - String id, - EndpointConfig endpointConfig, - CompletableFuture futureSession) + EndpointConfig endpointConfig) { this.LOG = Log.getLogger(endpointInstance.getClass()); @@ -139,8 +126,6 @@ public class JavaxWebSocketFrameHandler implements FrameHandler throw oops; } this.endpointInstance = endpointInstance; - this.upgradeRequest = upgradeRequest; - this.upgradeResponse = upgradeResponse; this.openHandle = openHandle; this.closeHandle = closeHandle; @@ -149,9 +134,7 @@ public class JavaxWebSocketFrameHandler implements FrameHandler this.binaryMetadata = binaryMetadata; this.pongHandle = pongHandle; - this.id = id; this.endpointConfig = endpointConfig; - this.futureSession = futureSession; this.messageHandlerMap = new HashMap<>(); } @@ -170,33 +153,13 @@ public class JavaxWebSocketFrameHandler implements FrameHandler return session; } - public int getMaxTextMessageBufferSize() - { - return maxTextMessageBufferSize; - } - - public void setMaxTextMessageBufferSize(int maxTextMessageBufferSize) - { - this.maxTextMessageBufferSize = maxTextMessageBufferSize; - } - - public int getMaxBinaryMessageBufferSize() - { - return maxBinaryMessageBufferSize; - } - - public void setMaxBinaryMessageBufferSize(int maxBinaryMessageBufferSize) - { - this.maxBinaryMessageBufferSize = maxBinaryMessageBufferSize; - } - @Override public void onOpen(CoreSession coreSession, Callback callback) { try { this.coreSession = coreSession; - session = new JavaxWebSocketSession(container, coreSession, this, upgradeRequest.getUserPrincipal(), id, endpointConfig); + session = new JavaxWebSocketSession(container, coreSession, this, endpointConfig); openHandle = InvokerUtils.bindTo(openHandle, session, endpointConfig); closeHandle = InvokerUtils.bindTo(closeHandle, session); @@ -209,6 +172,9 @@ public class JavaxWebSocketFrameHandler implements FrameHandler if (actualTextMetadata != null) { + if (actualTextMetadata.isMaxMessageSizeSet()) + session.setMaxTextMessageBufferSize(actualTextMetadata.maxMessageSize); + actualTextMetadata.handle = InvokerUtils.bindTo(actualTextMetadata.handle, endpointInstance, endpointConfig, session); actualTextMetadata.handle = JavaxWebSocketFrameHandlerFactory.wrapNonVoidReturnType(actualTextMetadata.handle, session); textSink = JavaxWebSocketFrameHandlerFactory.createMessageSink(session, actualTextMetadata); @@ -218,6 +184,9 @@ public class JavaxWebSocketFrameHandler implements FrameHandler if (actualBinaryMetadata != null) { + if (actualBinaryMetadata.isMaxMessageSizeSet()) + session.setMaxBinaryMessageBufferSize(actualBinaryMetadata.maxMessageSize); + actualBinaryMetadata.handle = InvokerUtils.bindTo(actualBinaryMetadata.handle, endpointInstance, endpointConfig, session); actualBinaryMetadata.handle = JavaxWebSocketFrameHandlerFactory.wrapNonVoidReturnType(actualBinaryMetadata.handle, session); binarySink = JavaxWebSocketFrameHandlerFactory.createMessageSink(session, actualBinaryMetadata); @@ -230,13 +199,11 @@ public class JavaxWebSocketFrameHandler implements FrameHandler container.notifySessionListeners((listener) -> listener.onJavaxWebSocketSessionOpened(session)); callback.succeeded(); - futureSession.complete(session); } catch (Throwable cause) { - Exception wse = new WebSocketException(endpointInstance.getClass().getName() + " OPEN method error: " + cause.getMessage(), cause); + Exception wse = new WebSocketException(endpointInstance.getClass().getSimpleName() + " OPEN method error: " + cause.getMessage(), cause); callback.failed(wse); - futureSession.completeExceptionally(wse); } } @@ -283,12 +250,13 @@ public class JavaxWebSocketFrameHandler implements FrameHandler closeHandle.invoke(closeReason); } callback.succeeded(); - container.notifySessionListeners((listener) -> listener.onJavaxWebSocketSessionClosed(session)); } catch (Throwable cause) { - callback.failed(new WebSocketException(endpointInstance.getClass().getName() + " CLOSE method error: " + cause.getMessage(), cause)); + callback.failed(new WebSocketException(endpointInstance.getClass().getSimpleName() + " CLOSE method error: " + cause.getMessage(), cause)); } + + container.notifySessionListeners((listener) -> listener.onJavaxWebSocketSessionClosed(session)); } @Override @@ -296,8 +264,6 @@ public class JavaxWebSocketFrameHandler implements FrameHandler { try { - futureSession.completeExceptionally(cause); - if (errorHandle != null) errorHandle.invoke(cause); else @@ -306,10 +272,9 @@ public class JavaxWebSocketFrameHandler implements FrameHandler } catch (Throwable t) { - WebSocketException wsError = new WebSocketException(endpointInstance.getClass().getName() + " ERROR method error: " + cause.getMessage(), t); + WebSocketException wsError = new WebSocketException(endpointInstance.getClass().getSimpleName() + " ERROR method error: " + cause.getMessage(), t); wsError.addSuppressed(cause); callback.failed(wsError); - // TODO should futureSession be failed here? } } @@ -609,7 +574,7 @@ public class JavaxWebSocketFrameHandler implements FrameHandler } catch (Throwable cause) { - throw new WebSocketException(endpointInstance.getClass().getName() + " PONG method error: " + cause.getMessage(), cause); + throw new WebSocketException(endpointInstance.getClass().getSimpleName() + " PONG method error: " + cause.getMessage(), cause); } } callback.succeeded(); @@ -645,4 +610,24 @@ public class JavaxWebSocketFrameHandler implements FrameHandler throw new ProtocolException("Unable to process continuation during dataType " + dataType); } } + + public void setUpgradeRequest(UpgradeRequest upgradeRequest) + { + this.upgradeRequest = upgradeRequest; + } + + public void setUpgradeResponse(UpgradeResponse upgradeResponse) + { + this.upgradeResponse = upgradeResponse; + } + + public UpgradeRequest getUpgradeRequest() + { + return upgradeRequest; + } + + public UpgradeResponse getUpgradeResponse() + { + return upgradeResponse; + } } diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandlerFactory.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandlerFactory.java index 778b3a3d695..dc8e878ce38 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandlerFactory.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandlerFactory.java @@ -31,7 +31,6 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import javax.websocket.CloseReason; @@ -108,9 +107,7 @@ public abstract class JavaxWebSocketFrameHandlerFactory public abstract JavaxWebSocketFrameHandlerMetadata createMetadata(Class endpointClass, EndpointConfig endpointConfig); - public JavaxWebSocketFrameHandler newJavaxWebSocketFrameHandler(Object endpointInstance, UpgradeRequest upgradeRequest, - UpgradeResponse upgradeResponse, - CompletableFuture futureSession) + public JavaxWebSocketFrameHandler newJavaxWebSocketFrameHandler(Object endpointInstance, UpgradeRequest upgradeRequest) { Object endpoint; EndpointConfig config; @@ -162,27 +159,13 @@ public abstract class JavaxWebSocketFrameHandlerFactory errorHandle = InvokerUtils.bindTo(errorHandle, endpoint); pongHandle = InvokerUtils.bindTo(pongHandle, endpoint); - CompletableFuture future = futureSession; - if (future == null) - future = new CompletableFuture<>(); - - String id = upgradeRequest.toString(); - JavaxWebSocketFrameHandler frameHandler = new JavaxWebSocketFrameHandler( container, endpoint, - upgradeRequest, upgradeResponse, openHandle, closeHandle, errorHandle, textMetadata, binaryMetadata, pongHandle, - id, - config, - future); - - if (metadata.hasTextMetdata() && metadata.getTextMetadata().isMaxMessageSizeSet()) - frameHandler.setMaxTextMessageBufferSize(metadata.getTextMetadata().maxMessageSize); - if (metadata.hasBinaryMetadata() && metadata.getBinaryMetadata().isMaxMessageSizeSet()) - frameHandler.setMaxBinaryMessageBufferSize(metadata.getBinaryMetadata().maxMessageSize); + config); return frameHandler; } diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketRemoteEndpoint.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketRemoteEndpoint.java index 1d86a52e741..88489c78c2e 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketRemoteEndpoint.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketRemoteEndpoint.java @@ -18,6 +18,14 @@ package org.eclipse.jetty.websocket.javax.common; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Duration; + +import javax.websocket.EncodeException; +import javax.websocket.Encoder; +import javax.websocket.SendHandler; + import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.SharedBlockingCallback; @@ -31,36 +39,29 @@ import org.eclipse.jetty.websocket.core.WebSocketException; import org.eclipse.jetty.websocket.javax.common.messages.MessageOutputStream; import org.eclipse.jetty.websocket.javax.common.messages.MessageWriter; -import javax.websocket.EncodeException; -import javax.websocket.Encoder; -import javax.websocket.SendHandler; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.time.Duration; - public class JavaxWebSocketRemoteEndpoint implements javax.websocket.RemoteEndpoint, OutgoingFrames { private static final Logger LOG = Log.getLogger(JavaxWebSocketRemoteEndpoint.class); protected final JavaxWebSocketSession session; - private final FrameHandler.CoreSession channel; + private final FrameHandler.CoreSession coreSession; protected boolean batch = false; protected byte messageType = -1; - protected JavaxWebSocketRemoteEndpoint(JavaxWebSocketSession session, FrameHandler.CoreSession channel) + protected JavaxWebSocketRemoteEndpoint(JavaxWebSocketSession session, FrameHandler.CoreSession coreSession) { this.session = session; - this.channel = channel; + this.coreSession = coreSession; } protected MessageWriter newMessageWriter() { - return new MessageWriter(channel, channel.getOutputBufferSize()); + return new MessageWriter(coreSession, coreSession.getOutputBufferSize()); } protected MessageOutputStream newMessageOutputStream() { - return new MessageOutputStream(channel, channel.getOutputBufferSize(), session.getContainerImpl().getBufferPool()); + return new MessageOutputStream(coreSession, coreSession.getOutputBufferSize(), session.getContainerImpl().getBufferPool()); } @Override @@ -68,7 +69,7 @@ public class JavaxWebSocketRemoteEndpoint implements javax.websocket.RemoteEndpo { try (SharedBlockingCallback.Blocker blocker = session.getBlocking().acquire()) { - channel.flush(blocker); + coreSession.flush(blocker); } } @@ -88,12 +89,22 @@ public class JavaxWebSocketRemoteEndpoint implements javax.websocket.RemoteEndpo public long getIdleTimeout() { - return channel.getIdleTimeout().toMillis(); + return coreSession.getIdleTimeout().toMillis(); } public void setIdleTimeout(long ms) { - channel.setIdleTimeout(Duration.ofMillis(ms)); + coreSession.setIdleTimeout(Duration.ofMillis(ms)); + } + + public long getWriteTimeout() + { + return coreSession.getWriteTimeout().toMillis(); + } + + public void setWriteTimeout(long ms) + { + coreSession.setWriteTimeout(Duration.ofMillis(ms)); } @Override @@ -135,7 +146,7 @@ public class JavaxWebSocketRemoteEndpoint implements javax.websocket.RemoteEndpo try { - channel.sendFrame(frame, callback, batch); + coreSession.sendFrame(frame, callback, batch); } finally { diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSession.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSession.java index 357d8538e8d..313eebf4f3a 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSession.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketSession.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; + import javax.websocket.CloseReason; import javax.websocket.EndpointConfig; import javax.websocket.Extension; @@ -60,10 +61,8 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we protected final SharedBlockingCallback blocking = new SharedBlockingCallback(); private final JavaxWebSocketContainer container; private final FrameHandler.CoreSession coreSession; - private final Principal principal; private final JavaxWebSocketFrameHandler frameHandler; private final EndpointConfig config; - private final String id; private final AvailableDecoders availableDecoders; private final AvailableEncoders availableEncoders; private final Map pathParameters; @@ -76,15 +75,11 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we public JavaxWebSocketSession(JavaxWebSocketContainer container, FrameHandler.CoreSession coreSession, JavaxWebSocketFrameHandler frameHandler, - Principal upgradeRequestPrincipal, - String id, EndpointConfig endpointConfig) { this.container = container; this.coreSession = coreSession; this.frameHandler = frameHandler; - this.principal = upgradeRequestPrincipal; - this.id = id; this.config = endpointConfig == null?new BasicEndpointConfig():endpointConfig; @@ -138,7 +133,6 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we } frameHandler.addMessageHandler(this, clazz, handler); - } /** @@ -307,7 +301,7 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we @Override public String getId() { - return this.id; + return this.frameHandler.getUpgradeRequest().toString(); } /** @@ -319,7 +313,8 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we @Override public int getMaxBinaryMessageBufferSize() { - return frameHandler.getMaxBinaryMessageBufferSize(); + long maxBinaryMsgSize = coreSession.getMaxBinaryMessageSize(); + return (maxBinaryMsgSize > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)maxBinaryMsgSize; } /** @@ -332,7 +327,7 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we @Override public void setMaxBinaryMessageBufferSize(int length) { - frameHandler.setMaxBinaryMessageBufferSize(length); + coreSession.setMaxBinaryMessageSize(length); } /** @@ -368,7 +363,8 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we @Override public int getMaxTextMessageBufferSize() { - return frameHandler.getMaxTextMessageBufferSize(); + long maxTextMsgSize = coreSession.getMaxTextMessageSize(); + return (maxTextMsgSize > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)maxTextMsgSize; } /** @@ -381,7 +377,7 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we @Override public void setMaxTextMessageBufferSize(int length) { - frameHandler.setMaxTextMessageBufferSize(length); + coreSession.setMaxTextMessageSize(length); } /** @@ -513,7 +509,7 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we @Override public Principal getUserPrincipal() { - return this.principal; + return this.frameHandler.getUpgradeRequest().getUserPrincipal(); } /** diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/SessionTracker.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/SessionTracker.java index e2ee48a731d..dd76efa0b95 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/SessionTracker.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/SessionTracker.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.websocket.javax.common; import java.util.Collections; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; + import javax.websocket.Session; import org.eclipse.jetty.util.component.AbstractLifeCycle; @@ -44,7 +45,7 @@ public class SessionTracker extends AbstractLifeCycle implements JavaxWebSocketS @Override public void onJavaxWebSocketSessionClosed(JavaxWebSocketSession session) { - sessions.remove(sessions); + sessions.remove(session); } @Override diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/messages/MessageOutputStream.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/messages/MessageOutputStream.java index ef6b0660777..faddedbb80a 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/messages/MessageOutputStream.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/messages/MessageOutputStream.java @@ -18,6 +18,10 @@ package org.eclipse.jetty.websocket.javax.common.messages; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -28,10 +32,6 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - /** * Support for writing a single WebSocket BINARY message via a {@link OutputStream} */ @@ -39,7 +39,7 @@ public class MessageOutputStream extends OutputStream { private static final Logger LOG = Log.getLogger(MessageOutputStream.class); - private final FrameHandler.CoreSession channel; + private final FrameHandler.CoreSession coreSession; private final ByteBufferPool bufferPool; private final SharedBlockingCallback blocker; private long frameCount; @@ -49,9 +49,9 @@ public class MessageOutputStream extends OutputStream private Callback callback; private boolean closed; - public MessageOutputStream(FrameHandler.CoreSession channel, int bufferSize, ByteBufferPool bufferPool) + public MessageOutputStream(FrameHandler.CoreSession coreSession, int bufferSize, ByteBufferPool bufferPool) { - this.channel = channel; + this.coreSession = coreSession; this.bufferPool = bufferPool; this.blocker = new SharedBlockingCallback(); this.buffer = bufferPool.acquire(bufferSize, true); @@ -139,7 +139,7 @@ public class MessageOutputStream extends OutputStream try (SharedBlockingCallback.Blocker b = blocker.acquire()) { - channel.sendFrame(frame, b, false); + coreSession.sendFrame(frame, b, false); b.block(); } diff --git a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/messages/MessageWriter.java b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/messages/MessageWriter.java index 3cfd16e4cce..f7300570631 100644 --- a/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/messages/MessageWriter.java +++ b/jetty-websocket/javax-websocket-common/src/main/java/org/eclipse/jetty/websocket/javax/common/messages/MessageWriter.java @@ -18,6 +18,13 @@ package org.eclipse.jetty.websocket.javax.common.messages; +import java.io.IOException; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; + import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.SharedBlockingCallback; @@ -27,13 +34,6 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; -import java.io.IOException; -import java.io.Writer; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CodingErrorAction; - import static java.nio.charset.StandardCharsets.UTF_8; /** @@ -49,7 +49,7 @@ public class MessageWriter extends Writer .onUnmappableCharacter(CodingErrorAction.REPORT) .onMalformedInput(CodingErrorAction.REPORT); - private final FrameHandler.CoreSession channel; + private final FrameHandler.CoreSession coreSession; private final SharedBlockingCallback blocker; private long frameCount; private Frame frame; @@ -57,9 +57,9 @@ public class MessageWriter extends Writer private Callback callback; private boolean closed; - public MessageWriter(FrameHandler.CoreSession channel, int bufferSize) + public MessageWriter(FrameHandler.CoreSession coreSession, int bufferSize) { - this.channel = channel; + this.coreSession = coreSession; this.blocker = new SharedBlockingCallback(); this.buffer = CharBuffer.allocate(bufferSize); this.frame = new Frame(OpCode.TEXT); @@ -149,7 +149,7 @@ public class MessageWriter extends Writer try (SharedBlockingCallback.Blocker b = blocker.acquire()) { - channel.sendFrame(frame, b, false); + coreSession.sendFrame(frame, b, false); b.block(); } diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/AbstractJavaxWebSocketFrameHandlerTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/AbstractJavaxWebSocketFrameHandlerTest.java index 02a7b22bbf1..4bb07f4a547 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/AbstractJavaxWebSocketFrameHandlerTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/AbstractJavaxWebSocketFrameHandlerTest.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.websocket.javax.common; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CompletableFuture; import javax.websocket.EndpointConfig; @@ -51,7 +50,7 @@ public abstract class AbstractJavaxWebSocketFrameHandlerTest protected AvailableDecoders decoders; protected Map uriParams; protected EndpointConfig endpointConfig; - protected FrameHandler.CoreSession channel = new FrameHandler.CoreSession.Empty(); + protected FrameHandler.CoreSession coreSession = new FrameHandler.CoreSession.Empty(); public AbstractJavaxWebSocketFrameHandlerTest() { @@ -67,10 +66,8 @@ public abstract class AbstractJavaxWebSocketFrameHandlerTest BasicEndpointConfig config = new BasicEndpointConfig(); ConfiguredEndpoint endpoint = new ConfiguredEndpoint(websocket, config); UpgradeRequest upgradeRequest = new UpgradeRequestAdapter(); - UpgradeResponse upgradeResponse = new UpgradeResponseAdapter(); - JavaxWebSocketFrameHandler localEndpoint = factory.newJavaxWebSocketFrameHandler(endpoint, - upgradeRequest, upgradeResponse, new CompletableFuture<>()); + JavaxWebSocketFrameHandler localEndpoint = factory.newJavaxWebSocketFrameHandler(endpoint, upgradeRequest); return localEndpoint; } diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/AbstractSessionTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/AbstractSessionTest.java index 207d3e380ed..b7dc6d895ad 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/AbstractSessionTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/AbstractSessionTest.java @@ -38,18 +38,9 @@ public abstract class AbstractSessionTest container.start(); Object websocketPojo = new DummyEndpoint(); UpgradeRequest upgradeRequest = new UpgradeRequestAdapter(); - UpgradeResponse upgradeResponse = new UpgradeResponseAdapter(); - JavaxWebSocketFrameHandler frameHandler = - container.newFrameHandler(websocketPojo, upgradeRequest, upgradeResponse, null); - FrameHandler.CoreSession channel = new FrameHandler.CoreSession.Empty(); - String id = "dummy"; - EndpointConfig endpointConfig = null; - session = new JavaxWebSocketSession(container, - channel, - frameHandler, - null, - id, - endpointConfig); + JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(websocketPojo, upgradeRequest); + FrameHandler.CoreSession coreSession = new FrameHandler.CoreSession.Empty(); + session = new JavaxWebSocketSession(container, coreSession, frameHandler, null); container.addManaged(session); } diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/DummyContainer.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/DummyContainer.java index 622cfb179ad..6c40aab4992 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/DummyContainer.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/DummyContainer.java @@ -18,19 +18,18 @@ package org.eclipse.jetty.websocket.javax.common; -import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.MappedByteBufferPool; -import org.eclipse.jetty.util.DecoratedObjectFactory; -import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.Executor; import javax.websocket.ClientEndpointConfig; import javax.websocket.DeploymentException; import javax.websocket.Endpoint; import javax.websocket.Session; -import java.io.IOException; -import java.net.URI; -import java.util.concurrent.Executor; + +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; /** * Dummy Container for testing. @@ -38,15 +37,12 @@ import java.util.concurrent.Executor; public class DummyContainer extends JavaxWebSocketContainer { private final JavaxWebSocketFrameHandlerFactory frameHandlerFactory; - private final ByteBufferPool bufferPool; private final QueuedThreadPool executor; - private final DecoratedObjectFactory objectFactory; public DummyContainer() { + super(new WebSocketComponents()); this.frameHandlerFactory = new DummyFrameHandlerFactory(this); - this.bufferPool = new MappedByteBufferPool(); - this.objectFactory = new DecoratedObjectFactory(); this.executor = new QueuedThreadPool(); this.executor.setName("qtp-DummyContainer"); addBean(this.executor, true); @@ -61,7 +57,6 @@ public class DummyContainer extends JavaxWebSocketContainer @Override public void setAsyncSendTimeout(long timeoutmillis) { - } @Override @@ -97,7 +92,6 @@ public class DummyContainer extends JavaxWebSocketContainer @Override public void setDefaultMaxSessionIdleTimeout(long timeout) { - } @Override @@ -106,16 +100,9 @@ public class DummyContainer extends JavaxWebSocketContainer return 0; } - @Override - public DecoratedObjectFactory getObjectFactory() - { - return this.objectFactory; - } - @Override public void setDefaultMaxBinaryMessageBufferSize(int max) { - } @Override @@ -127,13 +114,6 @@ public class DummyContainer extends JavaxWebSocketContainer @Override public void setDefaultMaxTextMessageBufferSize(int max) { - - } - - @Override - public ByteBufferPool getBufferPool() - { - return bufferPool; } @Override @@ -149,7 +129,7 @@ public class DummyContainer extends JavaxWebSocketContainer } @Override - protected WebSocketExtensionRegistry getExtensionRegistry() + public WebSocketExtensionRegistry getExtensionRegistry() { return null; } diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnCloseTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnCloseTest.java index 8f87f4a18c3..fe81663331a 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnCloseTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnCloseTest.java @@ -45,7 +45,7 @@ public class JavaxWebSocketFrameHandler_OnCloseTest extends AbstractJavaxWebSock JavaxWebSocketFrameHandler localEndpoint = newJavaxFrameHandler(socket); // These invocations are the same for all tests - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); CloseStatus status = new CloseStatus(CloseStatus.NORMAL, "Normal"); Frame closeFrame = status.toFrame(); localEndpoint.onFrame(closeFrame, Callback.from(() -> diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnErrorTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnErrorTest.java index eaf62d31d26..955e3bf8d82 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnErrorTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnErrorTest.java @@ -42,7 +42,7 @@ public class JavaxWebSocketFrameHandler_OnErrorTest extends AbstractJavaxWebSock JavaxWebSocketFrameHandler localEndpoint = newJavaxFrameHandler(socket); // These invocations are the same for all tests - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onError(new RuntimeException("From Testcase"), Callback.NOOP); String event = socket.events.poll(1, TimeUnit.SECONDS); assertThat("Event", event, eventMatcher); diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_BinaryStreamTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_BinaryStreamTest.java index 6e53a2bbca1..0fcc29c79b1 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_BinaryStreamTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_BinaryStreamTest.java @@ -34,8 +34,8 @@ import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.javax.common.sockets.TrackingSocket; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class JavaxWebSocketFrameHandler_OnMessage_BinaryStreamTest extends AbstractJavaxWebSocketFrameHandlerTest { @@ -45,7 +45,7 @@ public class JavaxWebSocketFrameHandler_OnMessage_BinaryStreamTest extends Abstr JavaxWebSocketFrameHandler localEndpoint = newJavaxFrameHandler(socket); // This invocation is the same for all tests - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); func.apply(localEndpoint); diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_BinaryTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_BinaryTest.java index f50220a502b..da1efac7e52 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_BinaryTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_BinaryTest.java @@ -35,9 +35,9 @@ import org.eclipse.jetty.websocket.javax.common.util.InvalidSignatureException; import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -48,7 +48,7 @@ public class JavaxWebSocketFrameHandler_OnMessage_BinaryTest extends AbstractJav JavaxWebSocketFrameHandler localEndpoint = newJavaxFrameHandler(socket); // This invocation is the same for all tests - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); assertThat("Has Binary Metadata", localEndpoint.getBinaryMetadata(), notNullValue()); diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_TextStreamTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_TextStreamTest.java index b689aa387ee..e0fe14d4e7e 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_TextStreamTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_TextStreamTest.java @@ -33,8 +33,8 @@ import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.javax.common.sockets.TrackingSocket; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class JavaxWebSocketFrameHandler_OnMessage_TextStreamTest extends AbstractJavaxWebSocketFrameHandlerTest { @@ -44,7 +44,7 @@ public class JavaxWebSocketFrameHandler_OnMessage_TextStreamTest extends Abstrac JavaxWebSocketFrameHandler localEndpoint = newJavaxFrameHandler(socket); // This invocation is the same for all tests - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); func.apply(localEndpoint); diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_TextTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_TextTest.java index 8ceb54c2801..ff7cb1e63f6 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_TextTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnMessage_TextTest.java @@ -47,7 +47,7 @@ public class JavaxWebSocketFrameHandler_OnMessage_TextTest extends AbstractJavax JavaxWebSocketFrameHandler localEndpoint = newJavaxFrameHandler(socket); // This invocation is the same for all tests - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); ByteBuffer payload = BufferUtil.toBuffer(msg, StandardCharsets.UTF_8); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload(payload).setFin(true), Callback.NOOP); diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnOpenTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnOpenTest.java index 22d0a7800db..66c0507fbc0 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnOpenTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketFrameHandler_OnOpenTest.java @@ -40,7 +40,7 @@ public class JavaxWebSocketFrameHandler_OnOpenTest extends AbstractJavaxWebSocke JavaxWebSocketFrameHandler localEndpoint = newJavaxFrameHandler(socket); // This invocation is the same for all tests - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); String event = socket.events.poll(1, TimeUnit.SECONDS); assertThat("Event", event, eventMatcher); } diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedBinaryMessageSinkTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedBinaryMessageSinkTest.java index a89f3110056..101541f3ed9 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedBinaryMessageSinkTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedBinaryMessageSinkTest.java @@ -18,15 +18,6 @@ package org.eclipse.jetty.websocket.javax.common.messages; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.javax.common.AbstractSessionTest; -import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; -import org.junit.jupiter.api.Test; - -import javax.websocket.DecodeException; -import javax.websocket.Decoder; -import javax.websocket.EndpointConfig; import java.lang.invoke.MethodHandle; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; @@ -35,9 +26,18 @@ import java.util.TimeZone; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import javax.websocket.DecodeException; +import javax.websocket.Decoder; +import javax.websocket.EndpointConfig; + +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.javax.common.AbstractSessionTest; +import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class DecodedBinaryMessageSinkTest extends AbstractMessageSinkTest { diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedBinaryStreamMessageSinkTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedBinaryStreamMessageSinkTest.java index ff047544b67..044c06d3260 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedBinaryStreamMessageSinkTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedBinaryStreamMessageSinkTest.java @@ -18,15 +18,6 @@ package org.eclipse.jetty.websocket.javax.common.messages; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; -import org.junit.jupiter.api.Test; - -import javax.websocket.DecodeException; -import javax.websocket.Decoder; -import javax.websocket.EndpointConfig; import java.io.IOException; import java.io.InputStream; import java.lang.invoke.MethodHandle; @@ -37,9 +28,18 @@ import java.util.TimeZone; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import javax.websocket.DecodeException; +import javax.websocket.Decoder; +import javax.websocket.EndpointConfig; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class DecodedBinaryStreamMessageSinkTest extends AbstractMessageSinkTest { diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedTextMessageSinkTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedTextMessageSinkTest.java index bbd7d869595..182873580c9 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedTextMessageSinkTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedTextMessageSinkTest.java @@ -18,14 +18,6 @@ package org.eclipse.jetty.websocket.javax.common.messages; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; -import org.junit.jupiter.api.Test; - -import javax.websocket.DecodeException; -import javax.websocket.Decoder; -import javax.websocket.EndpointConfig; import java.lang.invoke.MethodHandle; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -34,9 +26,17 @@ import java.util.TimeZone; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import javax.websocket.DecodeException; +import javax.websocket.Decoder; +import javax.websocket.EndpointConfig; + +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class DecodedTextMessageSinkTest extends AbstractMessageSinkTest { diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedTextStreamMessageSinkTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedTextStreamMessageSinkTest.java index 092b48d66a6..306282dacc5 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedTextStreamMessageSinkTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/DecodedTextStreamMessageSinkTest.java @@ -18,15 +18,6 @@ package org.eclipse.jetty.websocket.javax.common.messages; -import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; -import org.junit.jupiter.api.Test; - -import javax.websocket.DecodeException; -import javax.websocket.Decoder; -import javax.websocket.EndpointConfig; import java.io.IOException; import java.io.Reader; import java.lang.invoke.MethodHandle; @@ -37,9 +28,18 @@ import java.util.TimeZone; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import javax.websocket.DecodeException; +import javax.websocket.Decoder; +import javax.websocket.EndpointConfig; + +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; +import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class DecodedTextStreamMessageSinkTest extends AbstractMessageSinkTest { diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/InputStreamMessageSinkTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/InputStreamMessageSinkTest.java index 7421a15938b..556c8b35c72 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/InputStreamMessageSinkTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/InputStreamMessageSinkTest.java @@ -18,15 +18,6 @@ package org.eclipse.jetty.websocket.javax.common.messages; -import org.eclipse.jetty.util.BlockingArrayQueue; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.javax.common.AbstractSessionTest; -import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; -import org.junit.jupiter.api.Test; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -38,9 +29,18 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.javax.common.AbstractSessionTest; +import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; +import org.junit.jupiter.api.Test; + import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class InputStreamMessageSinkTest extends AbstractMessageSinkTest { diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/ReaderMessageSinkTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/ReaderMessageSinkTest.java index 6e1bbae839c..ed1f2ef89d8 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/ReaderMessageSinkTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/messages/ReaderMessageSinkTest.java @@ -18,12 +18,6 @@ package org.eclipse.jetty.websocket.javax.common.messages; -import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; -import org.junit.jupiter.api.Test; - import java.io.IOException; import java.io.Reader; import java.io.StringWriter; @@ -34,8 +28,14 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; -import static org.hamcrest.CoreMatchers.is; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.javax.common.CompletableFutureCallback; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class ReaderMessageSinkTest extends AbstractMessageSinkTest { diff --git a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/util/InvokerUtilsTest.java b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/util/InvokerUtilsTest.java index 69670e5cfc7..dc2faeb42aa 100644 --- a/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/util/InvokerUtilsTest.java +++ b/jetty-websocket/javax-websocket-common/src/test/java/org/eclipse/jetty/websocket/javax/common/util/InvokerUtilsTest.java @@ -18,15 +18,15 @@ package org.eclipse.jetty.websocket.javax.common.util; -import org.eclipse.jetty.util.annotation.Name; -import org.junit.jupiter.api.Test; - import java.io.File; import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; -import static org.hamcrest.CoreMatchers.is; +import org.eclipse.jetty.util.annotation.Name; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; @SuppressWarnings("Duplicates") public class InvokerUtilsTest diff --git a/jetty-websocket/javax-websocket-server/pom.xml b/jetty-websocket/javax-websocket-server/pom.xml index 5b5b1135b1b..5ba24d40c98 100644 --- a/jetty-websocket/javax-websocket-server/pom.xml +++ b/jetty-websocket/javax-websocket-server/pom.xml @@ -33,8 +33,8 @@ ${project.version} - javax.websocket - javax.websocket-api + org.eclipse.jetty.toolchain + jetty-javax-websocket-api org.eclipse.jetty.toolchain diff --git a/jetty-websocket/javax-websocket-server/src/main/java/module-info.java b/jetty-websocket/javax-websocket-server/src/main/java/module-info.java index 90a58e9818d..3c20f2e9d1d 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/module-info.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/module-info.java @@ -29,7 +29,7 @@ module org.eclipse.jetty.websocket.javax.server exports org.eclipse.jetty.websocket.javax.server; requires jetty.servlet.api; - requires javax.websocket.api; + requires jetty.websocket.api; requires org.eclipse.jetty.client; requires org.eclipse.jetty.http; requires org.eclipse.jetty.io; diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerContainer.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerContainer.java index 3c73c75a236..4dd48323d34 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerContainer.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerContainer.java @@ -18,21 +18,29 @@ package org.eclipse.jetty.websocket.javax.server; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import javax.servlet.ServletContext; +import javax.websocket.DeploymentException; +import javax.websocket.EndpointConfig; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpoint; +import javax.websocket.server.ServerEndpointConfig; + import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; -import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.WebSocketException; -import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.javax.client.JavaxWebSocketClientContainer; import org.eclipse.jetty.websocket.javax.server.internal.AnnotatedServerEndpointConfig; @@ -40,118 +48,94 @@ import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketCreator; import org.eclipse.jetty.websocket.javax.server.internal.UndefinedServerEndpointConfig; import org.eclipse.jetty.websocket.servlet.WebSocketMapping; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.websocket.DeploymentException; -import javax.websocket.EndpointConfig; -import javax.websocket.WebSocketContainer; -import javax.websocket.server.ServerContainer; -import javax.websocket.server.ServerEndpoint; -import javax.websocket.server.ServerEndpointConfig; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; - @ManagedObject("JSR356 Server Container") -public class JavaxWebSocketServerContainer - extends JavaxWebSocketClientContainer - implements javax.websocket.server.ServerContainer, LifeCycle.Listener +public class JavaxWebSocketServerContainer extends JavaxWebSocketClientContainer implements javax.websocket.server.ServerContainer, LifeCycle.Listener { + public static final String JAVAX_WEBSOCKET_CONTAINER_ATTRIBUTE = ServerContainer.class.getName(); private static final Logger LOG = Log.getLogger(JavaxWebSocketServerContainer.class); - /** - * Get the WebSocketContainer out of the current ThreadLocal reference - * of the active ContextHandler. - * - * @return the WebSocketContainer if found, null if not found. - */ - public static WebSocketContainer getWebSocketContainer() - { - ContextHandler.Context context = ContextHandler.getCurrentContext(); - if (context == null) - return null; - - ContextHandler handler = ContextHandler.getContextHandler(context); - if (handler == null) - return null; - - if (!(handler instanceof ServletContextHandler)) - return null; - - return (javax.websocket.WebSocketContainer)handler.getServletContext().getAttribute("javax.websocket.server.ServerContainer"); - } - public static JavaxWebSocketServerContainer ensureContainer(ServletContext servletContext) { ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext, "Javax Websocket"); if (contextHandler.getServer() == null) throw new IllegalStateException("Server has not been set on the ServletContextHandler"); - JavaxWebSocketServerContainer container = contextHandler.getBean(JavaxWebSocketServerContainer.class); + JavaxWebSocketServerContainer container = (JavaxWebSocketServerContainer)servletContext.getAttribute(JAVAX_WEBSOCKET_CONTAINER_ATTRIBUTE); if (container==null) { - // Find Pre-Existing (Shared?) HttpClient and/or executor - HttpClient httpClient = (HttpClient)servletContext.getAttribute(JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE); - if (httpClient == null) - httpClient = (HttpClient)contextHandler.getServer() - .getAttribute(JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE); + Supplier coreClientSupplier = () -> + { + WebSocketCoreClient coreClient = (WebSocketCoreClient)servletContext.getAttribute(WebSocketCoreClient.WEBSOCKET_CORECLIENT_ATTRIBUTE); + if (coreClient == null) + { + // Find Pre-Existing (Shared?) HttpClient and/or executor + HttpClient httpClient = (HttpClient)servletContext.getAttribute(JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE); + if (httpClient == null) + httpClient = (HttpClient)contextHandler.getServer().getAttribute(JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE); - Executor executor = httpClient == null?null:httpClient.getExecutor(); - if (executor == null) - executor = (Executor)servletContext.getAttribute("org.eclipse.jetty.server.Executor"); - if (executor == null) - executor = contextHandler.getServer().getThreadPool(); + Executor executor = httpClient == null?null:httpClient.getExecutor(); + if (executor == null) + executor = (Executor)servletContext.getAttribute("org.eclipse.jetty.server.Executor"); + if (executor == null) + executor = contextHandler.getServer().getThreadPool(); - if (httpClient != null && httpClient.getExecutor() == null) - httpClient.setExecutor(executor); + if (httpClient != null && httpClient.getExecutor() == null) + httpClient.setExecutor(executor); + + // create the core client + coreClient = new WebSocketCoreClient(httpClient, WebSocketComponents.ensureWebSocketComponents(servletContext)); + coreClient.getHttpClient().setName("Javax-WebSocketClient@" + Integer.toHexString(coreClient.getHttpClient().hashCode())); + if (executor != null && httpClient == null) + coreClient.getHttpClient().setExecutor(executor); + servletContext.setAttribute(WebSocketCoreClient.WEBSOCKET_CORECLIENT_ATTRIBUTE, coreClient); + } + return coreClient; + }; // Create the Jetty ServerContainer implementation container = new JavaxWebSocketServerContainer( WebSocketMapping.ensureMapping(servletContext, WebSocketMapping.DEFAULT_KEY), WebSocketComponents.ensureWebSocketComponents(servletContext), - httpClient, executor); + coreClientSupplier); contextHandler.addManaged(container); contextHandler.addLifeCycleListener(container); } // Store a reference to the ServerContainer per - javax.websocket spec 1.0 final - section 6.4: Programmatic Server Deployment - servletContext.setAttribute(ServerContainer.class.getName(), container); + servletContext.setAttribute(JAVAX_WEBSOCKET_CONTAINER_ATTRIBUTE, container); return container; } private final WebSocketMapping webSocketMapping; - private final WebSocketComponents webSocketComponents; private final JavaxWebSocketServerFrameHandlerFactory frameHandlerFactory; - private final Executor executor; - private final FrameHandler.ConfigurationCustomizer customizer = new FrameHandler.ConfigurationCustomizer(); - private long asyncSendTimeout = -1; private List> deferredEndpointClasses; private List deferredEndpointConfigs; - - public JavaxWebSocketServerContainer(WebSocketMapping webSocketMapping, HttpClient httpClient, Executor executor) + /** + * Main entry point for {@link JavaxWebSocketServletContainerInitializer}. + * @param webSocketMapping the {@link WebSocketMapping} that this container belongs to + */ + public JavaxWebSocketServerContainer(WebSocketMapping webSocketMapping) { - this(webSocketMapping, new WebSocketComponents(), httpClient, executor); + this(webSocketMapping, new WebSocketComponents()); + } + + public JavaxWebSocketServerContainer(WebSocketMapping webSocketMapping, WebSocketComponents components) + { + super(components); + this.webSocketMapping = webSocketMapping; + this.frameHandlerFactory = new JavaxWebSocketServerFrameHandlerFactory(this); } /** * Main entry point for {@link JavaxWebSocketServletContainerInitializer}. * @param webSocketMapping the {@link WebSocketMapping} that this container belongs to - * @param webSocketComponents the {@link WebSocketComponents} instance to use - * @param httpClient the {@link HttpClient} instance to use + * @param components the {@link WebSocketComponents} instance to use + * @param coreClientSupplier the supplier of the {@link WebSocketCoreClient} instance to use */ - public JavaxWebSocketServerContainer(WebSocketMapping webSocketMapping, WebSocketComponents webSocketComponents, HttpClient httpClient, Executor executor) + public JavaxWebSocketServerContainer(WebSocketMapping webSocketMapping, WebSocketComponents components, Supplier coreClientSupplier) { - super(() -> - { - WebSocketCoreClient client = new WebSocketCoreClient(httpClient); - if (executor != null && httpClient == null) - client.getHttpClient().setExecutor(executor); - return client; - }); + super(components, coreClientSupplier); this.webSocketMapping = webSocketMapping; - this.webSocketComponents = webSocketComponents; - this.executor = executor; this.frameHandlerFactory = new JavaxWebSocketServerFrameHandlerFactory(this); } @@ -167,37 +151,12 @@ public class JavaxWebSocketServerContainer } } - - @Override - public ByteBufferPool getBufferPool() - { - return webSocketComponents.getBufferPool(); - } - - @Override - public Executor getExecutor() - { - return this.executor; - } - - @Override - public WebSocketExtensionRegistry getExtensionRegistry() - { - return webSocketComponents.getExtensionRegistry(); - } - @Override public JavaxWebSocketServerFrameHandlerFactory getFrameHandlerFactory() { return frameHandlerFactory; } - @Override - public DecoratedObjectFactory getObjectFactory() - { - return webSocketComponents.getObjectFactory(); - } - @Override protected EndpointConfig newEmptyConfig(Object endpoint) { @@ -289,11 +248,10 @@ public class JavaxWebSocketServerContainer { frameHandlerFactory.getMetadata(config.getEndpointClass(), config); - JavaxWebSocketCreator creator = new JavaxWebSocketCreator(this, config, webSocketComponents - .getExtensionRegistry()); + JavaxWebSocketCreator creator = new JavaxWebSocketCreator(this, config, getExtensionRegistry()); PathSpec pathSpec = new UriTemplatePathSpec(config.getPath()); - webSocketMapping.addMapping(pathSpec, creator, frameHandlerFactory, customizer); + webSocketMapping.addMapping(pathSpec, creator, frameHandlerFactory, defaultCustomizer); } @Override @@ -321,58 +279,4 @@ public class JavaxWebSocketServerContainer deferredEndpointConfigs.clear(); } } - - @Override - public long getDefaultAsyncSendTimeout() - { - return this.asyncSendTimeout; - } - - @Override - public int getDefaultMaxBinaryMessageBufferSize() - { - long max = customizer.getMaxBinaryMessageSize(); - if (max > (long)Integer.MAX_VALUE) - return Integer.MAX_VALUE; - return (int)max; - } - - @Override - public long getDefaultMaxSessionIdleTimeout() - { - return customizer.getIdleTimeout().toMillis(); - } - - @Override - public int getDefaultMaxTextMessageBufferSize() - { - long max = customizer.getMaxTextMessageSize(); - if (max > (long)Integer.MAX_VALUE) - return Integer.MAX_VALUE; - return (int)max; - } - - @Override - public void setAsyncSendTimeout(long ms) - { - this.asyncSendTimeout = ms; - } - - @Override - public void setDefaultMaxBinaryMessageBufferSize(int max) - { - customizer.setMaxBinaryMessageSize(max); - } - - @Override - public void setDefaultMaxSessionIdleTimeout(long ms) - { - customizer.setIdleTimeout(Duration.ofMillis(ms)); - } - - @Override - public void setDefaultMaxTextMessageBufferSize(int max) - { - customizer.setMaxTextMessageSize(max); - } } diff --git a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerFrameHandlerFactory.java b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerFrameHandlerFactory.java index 12d7746ae20..8a5bdee8907 100644 --- a/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerFrameHandlerFactory.java +++ b/jetty-websocket/javax-websocket-server/src/main/java/org/eclipse/jetty/websocket/javax/server/JavaxWebSocketServerFrameHandlerFactory.java @@ -18,11 +18,8 @@ package org.eclipse.jetty.websocket.javax.server; -import java.util.concurrent.CompletableFuture; - import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; -import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; @@ -32,7 +29,6 @@ import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerFactor import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerMetadata; import org.eclipse.jetty.websocket.javax.server.internal.DelegatedJavaxServletUpgradeRequest; import org.eclipse.jetty.websocket.javax.server.internal.PathParamIdentifier; -import org.eclipse.jetty.websocket.javax.server.internal.UpgradeResponseAdapter; import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; @@ -68,7 +64,6 @@ public class JavaxWebSocketServerFrameHandlerFactory extends JavaxWebSocketFrame @Override public FrameHandler newFrameHandler(Object websocketPojo, ServletUpgradeRequest upgradeRequest, ServletUpgradeResponse upgradeResponse) { - CompletableFuture completableFuture = new CompletableFuture<>(); - return newJavaxWebSocketFrameHandler(websocketPojo, new DelegatedJavaxServletUpgradeRequest(upgradeRequest), new UpgradeResponseAdapter(upgradeResponse), completableFuture); + return newJavaxWebSocketFrameHandler(websocketPojo, new DelegatedJavaxServletUpgradeRequest(upgradeRequest)); } } diff --git a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java index 4f83b3d57b4..c39c5169bea 100644 --- a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java +++ b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java @@ -18,16 +18,12 @@ package org.eclipse.jetty.websocket.javax.server; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.websocket.servlet.WebSocketMapping; public class DummyServerContainer extends JavaxWebSocketServerContainer { public DummyServerContainer() { - super(new WebSocketMapping(), new HttpClient(), new QueuedThreadPool()); - addBean(getHttpClient(), true); - addBean(getExecutor(), true); + super(new WebSocketMapping()); } } diff --git a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/WebSocketServerExamplesTest.java b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/WebSocketServerExamplesTest.java index c9673d5aa8c..90aa212497d 100644 --- a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/WebSocketServerExamplesTest.java +++ b/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/WebSocketServerExamplesTest.java @@ -43,6 +43,8 @@ import org.eclipse.jetty.security.authentication.BasicAuthenticator; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.websocket.javax.server.examples.GetHttpSessionSocket; @@ -57,6 +59,8 @@ import static org.hamcrest.Matchers.is; public class WebSocketServerExamplesTest { + private static final Logger LOG = Log.getLogger(WebSocketServerExamplesTest.class); + @ClientEndpoint public static class ClientSocket { @@ -66,27 +70,27 @@ public class WebSocketServerExamplesTest @OnOpen public void onOpen(Session sess) { - System.err.println("ClientSocket Connected: " + sess); + LOG.debug("ClientSocket Connected: " + sess); } @OnMessage public void onMessage(String message) { messageQueue.offer(message); - System.err.println("Received TEXT message: " + message); + LOG.debug("Received TEXT message: " + message); } @OnClose public void onClose(CloseReason closeReason) { - System.err.println("ClientSocket Closed: " + closeReason); + LOG.debug("ClientSocket Closed: " + closeReason); closed.countDown(); } @OnError public void onError(Throwable cause) { - cause.printStackTrace(System.err); + LOG.debug(cause); } } diff --git a/jetty-websocket/javax-websocket-server/src/test/resources/jetty-websocket-httpclient.xml b/jetty-websocket/javax-websocket-server/src/test/resources/jetty-websocket-httpclient.xml index 5530e171db7..ee97fb070fa 100644 --- a/jetty-websocket/javax-websocket-server/src/test/resources/jetty-websocket-httpclient.xml +++ b/jetty-websocket/javax-websocket-server/src/test/resources/jetty-websocket-httpclient.xml @@ -1,9 +1,6 @@ - - - @@ -18,4 +15,4 @@ - \ No newline at end of file + diff --git a/jetty-websocket/javax-websocket-tests/pom.xml b/jetty-websocket/javax-websocket-tests/pom.xml index c1a1fd9349d..aabad38e3b1 100644 --- a/jetty-websocket/javax-websocket-tests/pom.xml +++ b/jetty-websocket/javax-websocket-tests/pom.xml @@ -33,8 +33,8 @@ ${project.version} - javax.websocket - javax.websocket-api + org.eclipse.jetty.toolchain + jetty-javax-websocket-api org.eclipse.jetty.toolchain diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/CoreServer.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/CoreServer.java index fe1fc5650df..9f1dc1b2ff5 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/CoreServer.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/CoreServer.java @@ -18,6 +18,11 @@ package org.eclipse.jetty.websocket.javax.tests; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.function.Function; + import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.server.Server; @@ -35,11 +40,6 @@ import org.eclipse.jetty.websocket.core.server.WebSocketUpgradeHandler; import org.eclipse.jetty.websocket.javax.tests.framehandlers.FrameEcho; import org.eclipse.jetty.websocket.javax.tests.framehandlers.WholeMessageEcho; -import java.io.IOException; -import java.net.URI; -import java.util.List; -import java.util.function.Function; - public class CoreServer extends ContainerLifeCycle { private Server server; @@ -121,9 +121,8 @@ public class CoreServer extends ContainerLifeCycle } @Override - public void customize(FrameHandler.CoreSession session) + public void customize(FrameHandler.Configuration configurable) { - } @Override @@ -178,7 +177,7 @@ public class CoreServer extends ContainerLifeCycle } @Override - public void customize(FrameHandler.CoreSession session) + public void customize(FrameHandler.Configuration configurable) { } } diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EventSocket.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EventSocket.java new file mode 100644 index 00000000000..745a2dd807e --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/EventSocket.java @@ -0,0 +1,80 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests; + +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; + +import javax.websocket.ClientEndpoint; +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +@ServerEndpoint("/") +@ClientEndpoint +public class EventSocket +{ + private final static Logger LOG = Log.getLogger(EventSocket.class); + + public Session session; + + public BlockingQueue messageQueue = new BlockingArrayQueue<>(); + public volatile Throwable error = null; + + public CountDownLatch openLatch = new CountDownLatch(1); + public CountDownLatch closeLatch = new CountDownLatch(1); + + @OnOpen + public void onOpen(Session session) + { + this.session = session; + LOG.info("{} onOpen(): {}", toString(), session); + openLatch.countDown(); + } + + @OnMessage + public void onMessage(String message) throws IOException + { + LOG.info("{} onMessage(): {}", toString(), message); + messageQueue.offer(message); + } + + @OnClose + public void onClose(CloseReason reason) + { + LOG.info("{} onClose(): {}", toString(), reason); + closeLatch.countDown(); + } + + @OnError + public void onError(Throwable cause) + { + LOG.info("{} onError(): {}", toString(), cause); + error = cause; + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/Fuzzer.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/Fuzzer.java index 22ce73a101a..d1f751e6e3e 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/Fuzzer.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/Fuzzer.java @@ -18,6 +18,12 @@ package org.eclipse.jetty.websocket.javax.tests; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + import org.eclipse.jetty.toolchain.test.ByteBufferAssert; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -25,15 +31,9 @@ import org.eclipse.jetty.websocket.core.CloseStatus; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public interface Fuzzer extends AutoCloseable { diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalFuzzer.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalFuzzer.java index f54db6e32e8..89e61531060 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalFuzzer.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalFuzzer.java @@ -35,9 +35,9 @@ import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.internal.Generator; import org.eclipse.jetty.websocket.core.internal.Parser; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public class LocalFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseable { diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java index 04698f54526..6c17e31860b 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.websocket.javax.tests; import java.net.URI; import java.util.Map; import java.util.function.BiConsumer; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.websocket.OnMessage; @@ -42,20 +43,25 @@ import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.websocket.core.internal.Parser; +import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSession; +import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSessionListener; +import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerContainer; +import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerFrameHandlerFactory; import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; import org.eclipse.jetty.websocket.servlet.WebSocketCreator; import org.eclipse.jetty.websocket.servlet.WebSocketServlet; import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; public class LocalServer extends ContainerLifeCycle implements LocalFuzzer.Provider { - @ServerEndpoint("/echo/text") public static class TextEchoSocket { @@ -72,11 +78,12 @@ public class LocalServer extends ContainerLifeCycle implements LocalFuzzer.Provi private ServerConnector connector; private LocalConnector localConnector; private ServletContextHandler servletContextHandler; - private ServerContainer serverContainer; + private JavaxWebSocketServerContainer serverContainer; + private TrackingListener trackingListener = new TrackingListener(); private URI serverUri; private URI wsUri; private boolean ssl = false; - private SslContextFactory sslContextFactory; + private SslContextFactory.Server sslContextFactory; public void enableSsl(boolean ssl) { @@ -162,6 +169,7 @@ public class LocalServer extends ContainerLifeCycle implements LocalFuzzer.Provi servletContextHandler = new ServletContextHandler(server, "/", true, false); servletContextHandler.setContextPath("/"); serverContainer = JavaxWebSocketServletContainerInitializer.configureContext(servletContextHandler); + serverContainer.addSessionListener(trackingListener); configureServletContextHandler(servletContextHandler); return servletContextHandler; } @@ -191,7 +199,7 @@ public class LocalServer extends ContainerLifeCycle implements LocalFuzzer.Provi http_config.setSendServerVersion(true); http_config.setSendDateHeader(false); - sslContextFactory = new SslContextFactory(); + sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath()); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); @@ -256,12 +264,20 @@ public class LocalServer extends ContainerLifeCycle implements LocalFuzzer.Provi { ServletHolder holder = new ServletHolder(new WebSocketServlet() { + JavaxWebSocketServerFrameHandlerFactory factory = new JavaxWebSocketServerFrameHandlerFactory(JavaxWebSocketServerContainer.ensureContainer(getServletContext())); + @Override public void configure(WebSocketServletFactory factory) { PathSpec pathSpec = factory.parsePathSpec("/"); factory.addMapping(pathSpec, creator); } + + @Override + public FrameHandlerFactory getFactory() + { + return factory; + } }); servletContextHandler.addServlet(holder, urlPattern); } @@ -275,4 +291,37 @@ public class LocalServer extends ContainerLifeCycle implements LocalFuzzer.Provi { return server; } + + public TrackingListener getTrackingListener() + { + return trackingListener; + } + + public static class TrackingListener implements JavaxWebSocketSessionListener + { + private BlockingArrayQueue openedSessions = new BlockingArrayQueue<>(); + private BlockingArrayQueue closedSessions = new BlockingArrayQueue<>(); + + @Override + public void onJavaxWebSocketSessionOpened(JavaxWebSocketSession session) + { + openedSessions.offer(session); + } + + @Override + public void onJavaxWebSocketSessionClosed(JavaxWebSocketSession session) + { + closedSessions.offer(session); + } + + public BlockingArrayQueue getOpenedSessions() + { + return openedSessions; + } + + public BlockingArrayQueue getClosedSessions() + { + return closedSessions; + } + } } diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/NetworkFuzzer.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/NetworkFuzzer.java index 6d9061280f2..e57c86d9bd5 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/NetworkFuzzer.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/NetworkFuzzer.java @@ -25,10 +25,10 @@ import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.BufferUtil; @@ -42,6 +42,8 @@ import org.eclipse.jetty.websocket.core.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.core.internal.Generator; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseable { private final LocalServer server; @@ -66,8 +68,7 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab super(); this.server = server; this.client = new WebSocketCoreClient(); - CompletableFuture futureOnCapture = new CompletableFuture<>(); - this.upgradeRequest = new RawUpgradeRequest(client, wsURI, futureOnCapture); + this.upgradeRequest = new RawUpgradeRequest(client, wsURI); if (requestHeaders != null) { HttpFields fields = this.upgradeRequest.getHeaders(); @@ -81,7 +82,7 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab this.generator = new UnitGenerator(Behavior.CLIENT); CompletableFuture futureHandler = this.client.connect(upgradeRequest); - CompletableFuture futureCapture = futureHandler.thenCombine(futureOnCapture, (channel, capture) -> capture); + CompletableFuture futureCapture = futureHandler.thenCombine(upgradeRequest.getFuture(), (session, capture) -> capture); this.frameCapture = futureCapture.get(10, TimeUnit.SECONDS); } @@ -156,7 +157,7 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab { try (SharedBlockingCallback.Blocker blocker = sharedBlockingCallback.acquire()) { - frameCapture.session.sendFrame(f, blocker, false); + frameCapture.coreSession.sendFrame(f, blocker, false); } } } @@ -168,7 +169,7 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab { try (SharedBlockingCallback.Blocker blocker = sharedBlockingCallback.acquire()) { - frameCapture.session.sendFrame(f, blocker, false); + frameCapture.coreSession.sendFrame(f, blocker, false); } } } @@ -186,27 +187,31 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab public static class RawUpgradeRequest extends ClientUpgradeRequest { + private final FrameCapture frameCapture = new FrameCapture(); private final CompletableFuture futureCapture; - private EndPoint endPoint; - public RawUpgradeRequest(WebSocketCoreClient webSocketClient, URI requestURI, CompletableFuture futureCapture) + public RawUpgradeRequest(WebSocketCoreClient webSocketClient, URI requestURI) { super(webSocketClient, requestURI); - this.futureCapture = futureCapture; + this.futureCapture = new CompletableFuture<>(); + } + + public CompletableFuture getFuture() + { + return futureCapture; } @Override - public FrameHandler getFrameHandler(WebSocketCoreClient coreClient, HttpResponse response) + public FrameHandler getFrameHandler() { - FrameCapture frameCapture = new FrameCapture(this.endPoint); - futureCapture.complete(frameCapture); return frameCapture; } @Override protected void customize(EndPoint endp) { - this.endPoint = endp; + frameCapture.setEndPoint(endp); + futureCapture.complete(frameCapture); } @Override @@ -220,20 +225,21 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab public static class FrameCapture implements FrameHandler { private final BlockingQueue receivedFrames = new LinkedBlockingQueue<>(); - private final EndPoint endPoint; + private EndPoint endPoint; + private CountDownLatch openLatch = new CountDownLatch(1); private final SharedBlockingCallback blockingCallback = new SharedBlockingCallback(); - private CoreSession session; + private CoreSession coreSession; - public FrameCapture(EndPoint endPoint) + public void setEndPoint(EndPoint endpoint) { - this.endPoint = endPoint; + this.endPoint = endpoint; } - @Override public void onOpen(CoreSession coreSession, Callback callback) { - this.session = coreSession; + this.coreSession = coreSession; + this.openLatch.countDown(); callback.succeeded(); } @@ -256,14 +262,22 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab callback.succeeded(); } - public void writeRaw(ByteBuffer buffer) throws IOException { + try + { + assertTrue(openLatch.await(1, TimeUnit.SECONDS)); + } + catch (InterruptedException e) + { + throw new IOException(e); + } + synchronized (this) { try (SharedBlockingCallback.Blocker blocker = blockingCallback.acquire()) { - this.endPoint.write(blocker, buffer); + endPoint.write(blocker, buffer); } } } diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java index d81ca88d395..716ec2934b8 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java @@ -18,6 +18,12 @@ package org.eclipse.jetty.websocket.javax.tests; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; + import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.plus.webapp.PlusConfiguration; import org.eclipse.jetty.server.Handler; @@ -34,14 +40,8 @@ import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration; -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Path; - -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; /** diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/matchers/IsMessageHandlerType.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/matchers/IsMessageHandlerType.java index 29c7536c161..ebc0fd45eaa 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/matchers/IsMessageHandlerType.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/matchers/IsMessageHandlerType.java @@ -18,18 +18,17 @@ package org.eclipse.jetty.websocket.javax.tests.matchers; -import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSession; -import org.eclipse.jetty.websocket.javax.common.decoders.AvailableDecoders; -import org.eclipse.jetty.websocket.javax.tests.MessageType; -import org.eclipse.jetty.websocket.javax.common.util.ReflectUtils; -import org.hamcrest.Description; -import org.hamcrest.Factory; -import org.hamcrest.TypeSafeMatcher; - import javax.websocket.Decoder; import javax.websocket.MessageHandler; import javax.websocket.PongMessage; +import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSession; +import org.eclipse.jetty.websocket.javax.common.decoders.AvailableDecoders; +import org.eclipse.jetty.websocket.javax.common.util.ReflectUtils; +import org.eclipse.jetty.websocket.javax.tests.MessageType; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + public class IsMessageHandlerType extends TypeSafeMatcher { private final JavaxWebSocketSession session; @@ -98,7 +97,6 @@ public class IsMessageHandlerType extends TypeSafeMatcher return false; } - @Factory public static IsMessageHandlerType isMessageHandlerType(JavaxWebSocketSession session, MessageType messageType) { return new IsMessageHandlerType(session, messageType); diff --git a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/matchers/IsMessageHandlerTypeRegistered.java b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/matchers/IsMessageHandlerTypeRegistered.java index 51b17806b60..5790070b10c 100644 --- a/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/matchers/IsMessageHandlerTypeRegistered.java +++ b/jetty-websocket/javax-websocket-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/matchers/IsMessageHandlerTypeRegistered.java @@ -18,19 +18,18 @@ package org.eclipse.jetty.websocket.javax.tests.matchers; +import java.util.Map; +import javax.websocket.Decoder; +import javax.websocket.MessageHandler; +import javax.websocket.PongMessage; + import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSession; import org.eclipse.jetty.websocket.javax.common.RegisteredMessageHandler; import org.eclipse.jetty.websocket.javax.common.decoders.AvailableDecoders; import org.eclipse.jetty.websocket.javax.tests.MessageType; import org.hamcrest.Description; -import org.hamcrest.Factory; import org.hamcrest.TypeSafeMatcher; -import javax.websocket.Decoder; -import javax.websocket.MessageHandler; -import javax.websocket.PongMessage; -import java.util.Map; - public class IsMessageHandlerTypeRegistered extends TypeSafeMatcher { private final MessageType expectedType; @@ -144,7 +143,6 @@ public class IsMessageHandlerTypeRegistered extends TypeSafeMatcher futureSession = new CompletableFuture<>(); - - JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(endpoint, request, response, futureSession); + JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(endpoint, request); frameHandler.onOpen(new FrameHandler.CoreSession.Empty(), Callback.NOOP); // Execute onClose call diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/SessionAddMessageHandlerTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/SessionAddMessageHandlerTest.java index ebee871ac30..0556c50be7f 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/SessionAddMessageHandlerTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/SessionAddMessageHandlerTest.java @@ -21,12 +21,10 @@ package org.eclipse.jetty.websocket.javax.tests.client; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CompletableFuture; import javax.websocket.ClientEndpoint; import javax.websocket.ClientEndpointConfig; import javax.websocket.MessageHandler; -import javax.websocket.Session; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -42,8 +40,6 @@ import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerFactor import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSession; import org.eclipse.jetty.websocket.javax.common.UpgradeRequest; import org.eclipse.jetty.websocket.javax.common.UpgradeRequestAdapter; -import org.eclipse.jetty.websocket.javax.common.UpgradeResponse; -import org.eclipse.jetty.websocket.javax.common.UpgradeResponseAdapter; import org.eclipse.jetty.websocket.javax.tests.MessageType; import org.eclipse.jetty.websocket.javax.tests.SessionMatchers; import org.eclipse.jetty.websocket.javax.tests.handlers.ByteArrayWholeHandler; @@ -80,11 +76,9 @@ public class SessionAddMessageHandlerTest ConfiguredEndpoint ei = new ConfiguredEndpoint(new DummyEndpoint(), endpointConfig); UpgradeRequest handshakeRequest = new UpgradeRequestAdapter(); - UpgradeResponse handshakeResponse = new UpgradeResponseAdapter(); JavaxWebSocketFrameHandlerFactory frameHandlerFactory = new JavaxWebSocketClientFrameHandlerFactory(container); - CompletableFuture futureSession = new CompletableFuture<>(); - frameHandler = frameHandlerFactory.newJavaxWebSocketFrameHandler(ei, handshakeRequest, handshakeResponse, futureSession); + frameHandler = frameHandlerFactory.newJavaxWebSocketFrameHandler(ei, handshakeRequest); frameHandler.onOpen(new FrameHandler.CoreSession.Empty(), Callback.NOOP); // Session diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/WriteTimeoutTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/WriteTimeoutTest.java new file mode 100644 index 00000000000..6d02be0872b --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/WriteTimeoutTest.java @@ -0,0 +1,121 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests.client; + +import java.util.concurrent.TimeUnit; + +import javax.websocket.ContainerProvider; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.ServerEndpoint; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.WebSocketWriteTimeoutException; +import org.eclipse.jetty.websocket.javax.tests.LocalServer; +import org.eclipse.jetty.websocket.javax.tests.WSEndpointTracker; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class WriteTimeoutTest +{ + private static LocalServer server; + + @BeforeAll + public static void startServer() throws Exception + { + server = new LocalServer(); + server.start(); + server.getServerContainer().addEndpoint(LoggingSocket.class); + } + + @AfterAll + public static void stopServer() throws Exception + { + server.stop(); + } + + public static class ClientEndpoint extends WSEndpointTracker implements MessageHandler.Whole + { + @Override + public void onOpen(Session session, EndpointConfig config) + { + super.onOpen(session, config); + session.addMessageHandler(this); + } + + @Override + public void onMessage(String message) + { + super.onWsText(message); + } + } + + @Test + public void testEchoInstance() throws Exception + { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + ClientEndpoint clientEndpoint = new ClientEndpoint(); + assertThat(clientEndpoint, Matchers.instanceOf(javax.websocket.Endpoint.class)); + Session session = container.connectToServer(clientEndpoint, server.getWsUri().resolve("/logSocket")); + + session.getAsyncRemote().setSendTimeout(5); + + session.setMaxTextMessageBufferSize(1000000); + String string = "xxxxxxx"; + StringBuilder sb = new StringBuilder(); + while (sb.length() < session.getMaxTextMessageBufferSize() - string.length()) + sb.append(string); + string = sb.toString(); + + while (session.isOpen()) + session.getAsyncRemote().sendText(string); + + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientEndpoint.error.get(), instanceOf(WebSocketWriteTimeoutException.class)); + } + + @ServerEndpoint("/logSocket") + public static class LoggingSocket + { + private final Logger LOG = Log.getLogger(LoggingSocket.class); + + @OnMessage + public void onMessage(String msg) + { + LOG.debug("onMessage(): {}", msg); + } + + @OnError + public void onError(Throwable t) + { + LOG.debug("onError(): {}", t); + } + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/misbehaving/MisbehavingClassTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/misbehaving/MisbehavingClassTest.java index d5ffdcafe1d..73fe4427531 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/misbehaving/MisbehavingClassTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/misbehaving/MisbehavingClassTest.java @@ -18,52 +18,40 @@ package org.eclipse.jetty.websocket.javax.tests.client.misbehaving; -import org.eclipse.jetty.util.log.StacklessLogging; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; -import org.eclipse.jetty.websocket.javax.tests.CoreServer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; +import java.util.concurrent.TimeUnit; import javax.websocket.ContainerProvider; import javax.websocket.WebSocketContainer; -import java.io.IOException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.util.log.StacklessLogging; +import org.eclipse.jetty.websocket.core.CloseException; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; +import org.eclipse.jetty.websocket.javax.tests.CoreServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertThrows; public class MisbehavingClassTest { + private CoreServer server; - private static CoreServer server; - - @SuppressWarnings("Duplicates") - @BeforeAll - public static void startServer() throws Exception + @BeforeEach + public void startServer() throws Exception { server = new CoreServer(new CoreServer.EchoNegotiator()); - // Start Server server.start(); } - @AfterAll - public static void stopServer() + @AfterEach + public void stopServer() throws Exception { - try - { - server.stop(); - } - catch (Exception e) - { - e.printStackTrace(System.err); - } + server.stop(); } - @SuppressWarnings("Duplicates") @Test public void testEndpointRuntimeOnOpen() throws Exception { @@ -71,20 +59,16 @@ public class MisbehavingClassTest server.addBean(container); // allow to shutdown with server EndpointRuntimeOnOpen socket = new EndpointRuntimeOnOpen(); - try (StacklessLogging ignored = new StacklessLogging(WebSocketChannel.class)) + try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class)) { - // expecting IOException during onOpen - Exception e = assertThrows(IOException.class, () -> container.connectToServer(socket, server.getWsUri()), "Should have failed .connectToServer()"); - assertThat(e.getCause(), instanceOf(ExecutionException.class)); - + // expecting RuntimeException during onOpen + container.connectToServer(socket, server.getWsUri()); assertThat("Close should have occurred", socket.closeLatch.await(1, TimeUnit.SECONDS), is(true)); - Throwable cause = socket.errors.pop(); assertThat("Error", cause, instanceOf(RuntimeException.class)); } } - @SuppressWarnings("Duplicates") @Test public void testAnnotatedRuntimeOnOpen() throws Exception { @@ -92,14 +76,11 @@ public class MisbehavingClassTest server.addBean(container); // allow to shutdown with server AnnotatedRuntimeOnOpen socket = new AnnotatedRuntimeOnOpen(); - try (StacklessLogging ignored = new StacklessLogging(WebSocketChannel.class)) + try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class)) { - // expecting IOException during onOpen - Exception e = assertThrows(IOException.class, () -> container.connectToServer(socket, server.getWsUri()), "Should have failed .connectToServer()"); - assertThat(e.getCause(), instanceOf(ExecutionException.class)); - + // expecting RuntimeException during onOpen + container.connectToServer(socket, server.getWsUri()); assertThat("Close should have occurred", socket.closeLatch.await(5, TimeUnit.SECONDS), is(true)); - Throwable cause = socket.errors.pop(); assertThat("Error", cause, instanceOf(RuntimeException.class)); } diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderTextTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderTextTest.java index 3a2b59a1b81..e8aae36d6d8 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderTextTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/EncoderTextTest.java @@ -20,8 +20,8 @@ package org.eclipse.jetty.websocket.javax.tests.coders; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; /** * Test various {@link javax.websocket.Encoder.Text} scenarios diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/FloatDecoderTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/FloatDecoderTest.java new file mode 100644 index 00000000000..4c31a62d10d --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/FloatDecoderTest.java @@ -0,0 +1,67 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests.coders; + +import org.eclipse.jetty.websocket.javax.common.decoders.FloatDecoder; +import org.junit.jupiter.api.Test; + +import javax.websocket.DecodeException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for class {@link FloatDecoder}. + * + * @see FloatDecoder + */ +public class FloatDecoderTest +{ + @Test + public void testWillDecodeReturningTrue() + { + assertTrue(new FloatDecoder().willDecode("21")); + } + + @Test + public void testWillDecodeReturningFalse() + { + assertFalse(new FloatDecoder().willDecode("NaN")); + } + + @Test + public void testWillDecodeWithNull() + { + assertFalse(FloatDecoder.INSTANCE.willDecode(null)); + } + + @Test + public void testDecodeThrowsDecodeException() + { + assertThrows(DecodeException.class, () -> new FloatDecoder().decode("NaN")); + } + + @Test + public void testDecode() throws DecodeException + { + assertEquals(4.1F, new FloatDecoder().decode("4.1"), 0.01F); + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/IntegerDecoderTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/IntegerDecoderTest.java index ba0d20d9113..832d9a9f3ec 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/IntegerDecoderTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/IntegerDecoderTest.java @@ -25,6 +25,8 @@ import javax.websocket.DecodeException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; public class IntegerDecoderTest { @@ -35,4 +37,23 @@ public class IntegerDecoderTest Integer val = decoder.decode("123"); assertThat("Decoded value", val, is(123)); } + + @Test + public void testWillDecodeWithNull() + { + assertFalse(new IntegerDecoder().willDecode(null)); + } + + @Test + public void testWillDecodeWithNonEmptyString() + { + assertFalse(new IntegerDecoder().willDecode("a")); + } + + @Test + public void testDecodeThrowsDecodeException() + { + assertThrows(DecodeException.class, () -> IntegerDecoder.INSTANCE.decode("")); + + } } diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/LongDecoderTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/LongDecoderTest.java new file mode 100644 index 00000000000..01c26f6a6e3 --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/LongDecoderTest.java @@ -0,0 +1,53 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests.coders; + +import org.eclipse.jetty.websocket.javax.common.decoders.LongDecoder; +import org.junit.jupiter.api.Test; + +import javax.websocket.DecodeException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests for class {@link LongDecoder}. + * + * @see LongDecoder + */ +public class LongDecoderTest +{ + @Test + public void testCreatesLongDecoder() + { + assertFalse(new LongDecoder().willDecode(null)); + } + + @Test + public void testWillDecodeWithNonEmptyString() + { + assertFalse(LongDecoder.INSTANCE.willDecode("Unable to parse Long")); + } + + @Test + public void testDecodeThrowsDecodeException() + { + assertThrows(DecodeException.class, () -> LongDecoder.INSTANCE.decode("Unable to parse Long")); + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/ShortDecoderTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/ShortDecoderTest.java new file mode 100644 index 00000000000..606ebee55cf --- /dev/null +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/coders/ShortDecoderTest.java @@ -0,0 +1,54 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests.coders; + +import org.eclipse.jetty.websocket.javax.common.decoders.ShortDecoder; +import org.junit.jupiter.api.Test; + +import javax.websocket.DecodeException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests for class {@link ShortDecoder}. + * + * @see ShortDecoder + */ +public class ShortDecoderTest +{ + @Test + public void testWillDecodeWithNull() + { + assertFalse(new ShortDecoder().willDecode(null)); + } + + @Test + public void testWillDecodeWithNonEmptyString() + { + assertFalse(new ShortDecoder().willDecode(".iix/PN}f[&- ShortDecoder.INSTANCE.decode("$Yta3*m*%")); + + } +} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AbstractJavaxWebSocketServerFrameHandlerTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AbstractJavaxWebSocketServerFrameHandlerTest.java index 469dba6eaec..f9d78ffd3e9 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AbstractJavaxWebSocketServerFrameHandlerTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AbstractJavaxWebSocketServerFrameHandlerTest.java @@ -23,14 +23,13 @@ import java.util.Map; import javax.websocket.EndpointConfig; -import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.websocket.javax.client.EmptyClientEndpointConfig; import org.eclipse.jetty.websocket.javax.common.decoders.AvailableDecoders; import org.eclipse.jetty.websocket.javax.common.encoders.AvailableEncoders; import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerContainer; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; +import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -46,15 +45,7 @@ public abstract class AbstractJavaxWebSocketServerFrameHandlerTest server = new Server(); context = new ServletContextHandler(); server.setHandler(context); - - WebSocketMapping factory = new WebSocketMapping(); - HttpClient httpClient = new HttpClient(); - - container = new JavaxWebSocketServerContainer(factory, httpClient, server.getThreadPool()); - container.addBean(httpClient, true); - container.addBean(factory, true); - - server.addBean(container, true); + container = JavaxWebSocketServletContainerInitializer.configureContext(context); server.start(); } @@ -67,7 +58,7 @@ public abstract class AbstractJavaxWebSocketServerFrameHandlerTest protected AvailableEncoders encoders; protected AvailableDecoders decoders; - protected Map uriParams = new HashMap<>(); + protected Map uriParams; protected EndpointConfig endpointConfig; public AbstractJavaxWebSocketServerFrameHandlerTest() diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AltFilterTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AltFilterTest.java index c2f34bb9375..de40f16150c 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AltFilterTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AltFilterTest.java @@ -18,6 +18,9 @@ package org.eclipse.jetty.websocket.javax.tests.server; +import java.util.ArrayList; +import java.util.List; + import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; @@ -31,12 +34,9 @@ import org.eclipse.jetty.websocket.javax.tests.server.sockets.echo.BasicEchoSock import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import java.util.ArrayList; -import java.util.List; - -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; /** * Testing the use of an alternate {@link org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter} diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ConfiguratorTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ConfiguratorTest.java index 9dae9ac822d..a73a0446dfe 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ConfiguratorTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ConfiguratorTest.java @@ -435,17 +435,17 @@ public class ConfiguratorTest upgradeRequest.addExtensions("identity"); Future clientConnectFuture = client.connect(upgradeRequest); - FrameHandler.CoreSession channel = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { - channel.sendFrame(new Frame(OpCode.TEXT).setPayload(HttpHeader.SEC_WEBSOCKET_EXTENSIONS.asString()), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(HttpHeader.SEC_WEBSOCKET_EXTENSIONS.asString()), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); assertThat("Incoming Message", incomingMessage, is("Request Header [" + HttpHeader.SEC_WEBSOCKET_EXTENSIONS.asString() + "]: identity")); } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } @@ -459,17 +459,17 @@ public class ConfiguratorTest upgradeRequest.addExtensions("identity"); Future clientConnectFuture = client.connect(upgradeRequest); - FrameHandler.CoreSession channel = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("NegoExts"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("NegoExts"), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); assertThat("Incoming Message", incomingMessage, is("negotiatedExtensions=[]")); } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } @@ -483,17 +483,17 @@ public class ConfiguratorTest upgradeRequest.header("X-Dummy", "Bogus"); Future clientConnectFuture = client.connect(upgradeRequest); - FrameHandler.CoreSession channel = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("X-Dummy"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("X-Dummy"), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); assertThat("Incoming Message", incomingMessage, is("Request Header [X-Dummy]: Bogus")); } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } @@ -507,18 +507,18 @@ public class ConfiguratorTest ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, wsUri, clientSocket); Future clientConnectFuture = client.connect(upgradeRequest); - FrameHandler.CoreSession channel = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { // first request has this UserProperty - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("apple"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("apple"), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); assertThat("Incoming Message", incomingMessage, is("Requested User Property: [apple] = \"fruit from tree\"")); } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } // Second request @@ -526,13 +526,13 @@ public class ConfiguratorTest upgradeRequest = ClientUpgradeRequest.from(client, wsUri, clientSocket); clientConnectFuture = client.connect(upgradeRequest); - channel = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { // as this is second request, this should be null - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("apple"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("apple"), Callback.NOOP, false); // second request has this UserProperty - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("blueberry"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("blueberry"), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); assertThat("Incoming Message", incomingMessage, is("Requested User Property: [apple] = ")); @@ -541,7 +541,7 @@ public class ConfiguratorTest } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } @@ -554,13 +554,13 @@ public class ConfiguratorTest ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, wsUri, clientSocket); Future clientConnectFuture = client.connect(upgradeRequest); - FrameHandler.CoreSession channel = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { - SocketAddress expectedLocal = channel.getLocalAddress(); - SocketAddress expectedRemote = channel.getRemoteAddress(); + SocketAddress expectedLocal = coreSession.getLocalAddress(); + SocketAddress expectedRemote = coreSession.getRemoteAddress(); - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("addr"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("addr"), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); @@ -576,7 +576,7 @@ public class ConfiguratorTest } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } @@ -631,7 +631,7 @@ public class ConfiguratorTest FrameHandlerTracker clientSocket = new FrameHandlerTracker(); ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, wsUri, clientSocket); - upgradeRequest.header("sec-websocket-protocol", "echo, chat, status"); + upgradeRequest.setSubProtocols("echo","chat","status"); Future clientConnectFuture = client.connect(upgradeRequest); assertProtocols(clientSocket, clientConnectFuture, is("Requested Protocols: [echo,chat,status]")); @@ -650,8 +650,7 @@ public class ConfiguratorTest FrameHandlerTracker clientSocket = new FrameHandlerTracker(); ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, wsUri, clientSocket); - // header name is not to spec (case wise) - upgradeRequest.header("Sec-Websocket-Protocol", "echo, chat, status"); + upgradeRequest.setSubProtocols("echo","chat","status"); Future clientConnectFuture = client.connect(upgradeRequest); assertProtocols(clientSocket, clientConnectFuture, is("Requested Protocols: [echo,chat,status]")); @@ -660,17 +659,17 @@ public class ConfiguratorTest protected void assertProtocols(FrameHandlerTracker clientSocket, Future clientConnectFuture, Matcher responseMatcher) throws InterruptedException, java.util.concurrent.ExecutionException, java.util.concurrent.TimeoutException { - FrameHandler.CoreSession channel = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("getProtocols"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("getProtocols"), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); assertThat("Incoming message", incomingMessage, responseMatcher); } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } @@ -687,10 +686,10 @@ public class ConfiguratorTest upgradeRequest.setSubProtocols("gmt"); Future clientConnectFuture = client.connect(upgradeRequest); - FrameHandler.CoreSession channel = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("2016-06-20T14:27:44"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("2016-06-20T14:27:44"), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); assertThat("Incoming message", incomingMessage, is("cal=2016.06.20 AD at 14:27:44 +0000")); @@ -698,7 +697,7 @@ public class ConfiguratorTest } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } } diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentExceptionTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentExceptionTest.java index df131caa573..ca16d869bed 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentExceptionTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentExceptionTest.java @@ -25,20 +25,19 @@ import java.util.stream.Stream; import javax.websocket.DeploymentException; import javax.websocket.server.ServerEndpoint; -import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.javax.common.util.InvalidSignatureException; import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServerContainer; +import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidCloseIntSocket; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidErrorErrorSocket; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidErrorIntSocket; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidOpenCloseReasonSocket; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidOpenIntSocket; import org.eclipse.jetty.websocket.javax.tests.server.sockets.InvalidOpenSessionIntSocket; -import org.eclipse.jetty.websocket.javax.common.util.InvalidSignatureException; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -98,11 +97,8 @@ public class DeploymentExceptionTest public void testDeploy_InvalidSignature(Class pojo) throws Exception { ServletContextHandler context = new ServletContextHandler(); - - WebSocketMapping factory = new WebSocketMapping(); - HttpClient httpClient = new HttpClient(); - - JavaxWebSocketServerContainer container = new JavaxWebSocketServerContainer(factory, httpClient, server.getThreadPool()); + context.setServer(server); + JavaxWebSocketServerContainer container = JavaxWebSocketServletContainerInitializer.configureContext(context); context.addBean(container); contexts.addHandler(context); diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java index 1bee8b7deff..f440010c924 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java @@ -136,17 +136,17 @@ public class EndpointViaConfigTest FrameHandlerTracker clientSocket = new FrameHandlerTracker(); Future clientConnectFuture = client.connect(clientSocket, uri.resolve("/app/echo")); // wait for connect - FrameHandler.CoreSession channel = clientConnectFuture.get(5, TimeUnit.SECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(5, TimeUnit.SECONDS); try { - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("Hello World"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("Hello World"), Callback.NOOP, false); String incomingMessage = clientSocket.messageQueue.poll(1, TimeUnit.SECONDS); assertThat("Expected message", incomingMessage, is("Hello World")); } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } finally diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java index 2736767e2ed..261f265c3d0 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java @@ -35,9 +35,9 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; public class IdleTimeoutTest { diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/JavaxWebSocketFrameHandler_OnMessage_TextStreamTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/JavaxWebSocketFrameHandler_OnMessage_TextStreamTest.java index 680f06f594f..878aa6ce9f9 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/JavaxWebSocketFrameHandler_OnMessage_TextStreamTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/JavaxWebSocketFrameHandler_OnMessage_TextStreamTest.java @@ -21,12 +21,10 @@ package org.eclipse.jetty.websocket.javax.tests.server; import java.io.IOException; import java.io.Reader; import java.net.URI; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import javax.websocket.OnMessage; -import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; @@ -38,13 +36,11 @@ import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandler; import org.eclipse.jetty.websocket.javax.common.UpgradeRequest; import org.eclipse.jetty.websocket.javax.common.UpgradeRequestAdapter; -import org.eclipse.jetty.websocket.javax.common.UpgradeResponse; -import org.eclipse.jetty.websocket.javax.common.UpgradeResponseAdapter; import org.eclipse.jetty.websocket.javax.tests.WSEventTracker; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class JavaxWebSocketFrameHandler_OnMessage_TextStreamTest extends AbstractJavaxWebSocketServerFrameHandlerTest { @@ -52,11 +48,9 @@ public class JavaxWebSocketFrameHandler_OnMessage_TextStreamTest extends Abstrac private T performOnMessageInvocation(T socket, Consumer func) throws Exception { UpgradeRequest request = new UpgradeRequestAdapter(URI.create("http://localhost:8080/msg/foo")); - UpgradeResponse response = new UpgradeResponseAdapter(); - CompletableFuture futureSession = new CompletableFuture<>(); // Establish endpoint function - JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(socket, request, response, futureSession); + JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(socket, request); frameHandler.onOpen(new FrameHandler.CoreSession.Empty(), Callback.NOOP); func.accept(frameHandler); return socket; diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeAnnotatedTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeAnnotatedTest.java index 2caab03dbc4..eb8a57e623f 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeAnnotatedTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeAnnotatedTest.java @@ -18,6 +18,15 @@ package org.eclipse.jetty.websocket.javax.tests.server; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.websocket.OnMessage; +import javax.websocket.server.ServerEndpoint; + import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -31,16 +40,8 @@ import org.eclipse.jetty.websocket.javax.tests.framehandlers.FrameHandlerTracker import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import javax.websocket.OnMessage; -import javax.websocket.server.ServerEndpoint; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; /** * Test Echo of Large messages, targeting the {@link javax.websocket.Session#setMaxTextMessageBufferSize(int)} functionality @@ -85,7 +86,7 @@ public class LargeAnnotatedTest Future clientConnectFuture = client.connect(clientSocket, uri.resolve("/app/echo/large")); // wait for connect - FrameHandler.CoreSession channel = clientConnectFuture.get(1, TimeUnit.SECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(1, TimeUnit.SECONDS); try { @@ -93,7 +94,7 @@ public class LargeAnnotatedTest byte txt[] = new byte[100 * 1024]; Arrays.fill(txt, (byte)'o'); String msg = new String(txt, StandardCharsets.UTF_8); - channel.sendFrame(new Frame(OpCode.TEXT).setPayload(msg), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(msg), Callback.NOOP, false); // Receive echo String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); @@ -101,7 +102,7 @@ public class LargeAnnotatedTest } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } finally diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java index 0e7831a8f49..e46721d99fd 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java @@ -18,6 +18,18 @@ package org.eclipse.jetty.websocket.javax.tests.server; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.websocket.OnMessage; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpoint; + import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -31,19 +43,8 @@ import org.eclipse.jetty.websocket.javax.tests.framehandlers.FrameHandlerTracker import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; -import javax.websocket.OnMessage; -import javax.websocket.server.ServerContainer; -import javax.websocket.server.ServerEndpoint; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; /** * Test Echo of Large messages, targeting the {@link javax.websocket.WebSocketContainer#setDefaultMaxTextMessageBufferSize(int)} functionality @@ -107,14 +108,14 @@ public class LargeContainerTest Future clientConnectFuture = client.connect(clientSocket, uri.resolve("/app/echo/large")); // wait for connect - FrameHandler.CoreSession channel = clientConnectFuture.get(5, TimeUnit.SECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(5, TimeUnit.SECONDS); try { // The message size should be bigger than default, but smaller than the limit that LargeEchoSocket specifies byte txt[] = new byte[100 * 1024]; Arrays.fill(txt, (byte)'o'); String msg = new String(txt, StandardCharsets.UTF_8); - channel.sendFrame(new Frame(OpCode.TEXT).setPayload(msg), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(msg), Callback.NOOP, false); // Confirm echo String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); @@ -122,7 +123,7 @@ public class LargeContainerTest } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } finally diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/OnMessageReturnTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/OnMessageReturnTest.java index ce5e6cd90a0..021f89e91c9 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/OnMessageReturnTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/OnMessageReturnTest.java @@ -18,6 +18,17 @@ package org.eclipse.jetty.websocket.javax.tests.server; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import javax.websocket.CloseReason; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.server.ServerEndpoint; + import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; @@ -31,18 +42,8 @@ import org.eclipse.jetty.websocket.javax.tests.framehandlers.FrameHandlerTracker import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import javax.websocket.CloseReason; -import javax.websocket.OnMessage; -import javax.websocket.OnOpen; -import javax.websocket.server.ServerEndpoint; -import java.io.IOException; -import java.net.URI; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; @ExtendWith(WorkDirExtension.class) public class OnMessageReturnTest @@ -108,11 +109,11 @@ public class OnMessageReturnTest Future clientConnectFuture = client.connect(clientSocket, uri.resolve("/app/echoreturn")); // wait for connect - FrameHandler.CoreSession channel = clientConnectFuture.get(5, TimeUnit.SECONDS); + FrameHandler.CoreSession coreSession = clientConnectFuture.get(5, TimeUnit.SECONDS); try { // Send message - channel.sendFrame(new Frame(OpCode.TEXT).setPayload("Hello World"), Callback.NOOP, false); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload("Hello World"), Callback.NOOP, false); // Confirm response String incomingMessage = clientSocket.messageQueue.poll(5, TimeUnit.SECONDS); @@ -120,7 +121,7 @@ public class OnMessageReturnTest } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } finally diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java index c9e508c3b64..b92e9bad20e 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java @@ -18,22 +18,13 @@ package org.eclipse.jetty.websocket.javax.tests.server; -import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.webapp.WebAppContext; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.FrameHandler; -import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; -import org.eclipse.jetty.websocket.javax.tests.Timeouts; -import org.eclipse.jetty.websocket.javax.tests.WSServer; -import org.eclipse.jetty.websocket.javax.tests.framehandlers.FrameHandlerTracker; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; @@ -50,13 +41,23 @@ import javax.websocket.server.HandshakeRequest; import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.time.Duration; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; + +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.FrameHandler; +import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; +import org.eclipse.jetty.websocket.javax.tests.Timeouts; +import org.eclipse.jetty.websocket.javax.tests.WSServer; +import org.eclipse.jetty.websocket.javax.tests.framehandlers.FrameHandlerTracker; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -184,12 +185,12 @@ public class PingPongTest URI toUri = server.getWsUri().resolve(endpointPath); // Connect - Future futureChannel = client.connect(clientSocket, toUri); - FrameHandler.CoreSession channel = futureChannel.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); + Future futureSession = client.connect(clientSocket, toUri); + FrameHandler.CoreSession coreSession = futureSession.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); try { // Apply send action - sendAction.accept(channel); + sendAction.accept(coreSession); // Validate Responses for (int i = 0; i < expectedMsgs.length; i++) @@ -200,7 +201,7 @@ public class PingPongTest } finally { - channel.close(Callback.NOOP); + coreSession.close(Callback.NOOP); } } @@ -209,9 +210,9 @@ public class PingPongTest { assertTimeout(Duration.ofMillis(6000), () -> { - assertEcho("/app/pong", (channel) -> + assertEcho("/app/pong", (session) -> { - channel.sendFrame(new Frame(OpCode.PONG).setPayload("hello"), Callback.NOOP, false); + session.sendFrame(new Frame(OpCode.PONG).setPayload("hello"), Callback.NOOP, false); }, "PongMessageEndpoint.onMessage(PongMessage):[/pong]:hello"); }); } @@ -221,9 +222,9 @@ public class PingPongTest { assertTimeout(Duration.ofMillis(6000), () -> { - assertEcho("/app/pong-socket", (channel) -> + assertEcho("/app/pong-socket", (session) -> { - channel.sendFrame(new Frame(OpCode.PONG).setPayload("hello"), Callback.NOOP, false); + session.sendFrame(new Frame(OpCode.PONG).setPayload("hello"), Callback.NOOP, false); }, "PongSocket.onPong(PongMessage)[/pong-socket]:hello"); }); } diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTrackingTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTrackingTest.java index 67109c7a512..45d571cd2c1 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTrackingTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/SessionTrackingTest.java @@ -18,33 +18,31 @@ package org.eclipse.jetty.websocket.javax.tests.server; -import org.eclipse.jetty.util.BlockingArrayQueue; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.javax.tests.Fuzzer; -import org.eclipse.jetty.websocket.javax.tests.LocalServer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.util.Collection; +import java.util.concurrent.TimeUnit; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.websocket.javax.client.JavaxWebSocketClientContainer; +import org.eclipse.jetty.websocket.javax.tests.EventSocket; +import org.eclipse.jetty.websocket.javax.tests.LocalServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -@Disabled("Replaced with ServerCloseTest.testOpenSessionCleanup()") // TODO: Remove once ServerCloseTest is fixed public class SessionTrackingTest { - static BlockingArrayQueue serverSessions = new BlockingArrayQueue<>(); @ServerEndpoint("/session-info/{sessionId}") @@ -80,6 +78,7 @@ public class SessionTrackingTest } private static LocalServer server; + private static JavaxWebSocketClientContainer client; @BeforeAll public static void startServer() throws Exception @@ -87,72 +86,83 @@ public class SessionTrackingTest server = new LocalServer(); server.start(); server.getServerContainer().addEndpoint(SessionTrackingSocket.class); + + client = new JavaxWebSocketClientContainer(); + client.start(); } @AfterAll public static void stopServer() throws Exception { + client.stop(); server.stop(); } @Test public void testAddRemoveSessions() throws Exception { - List expectedFrames = new ArrayList<>(); + EventSocket clientSocket1 = new EventSocket(); + EventSocket clientSocket2 = new EventSocket(); + EventSocket clientSocket3 = new EventSocket(); - try (Fuzzer session1 = server.newNetworkFuzzer("/session-info/1")) + try (Session session1 = client.connectToServer(clientSocket1, server.getWsUri().resolve("/session-info/1"))) { - assertNotNull(serverSessions.poll(10, TimeUnit.SECONDS)); - expectedFrames.clear(); + Session serverSession1 = serverSessions.poll(5, TimeUnit.SECONDS); + assertNotNull(serverSession1); sendTextFrameToAll("openSessions|in-1", session1); - session1.expect(Arrays.asList(new Frame(OpCode.TEXT).setPayload("openSessions(@in-1).size=1"))); + assertThat(clientSocket1.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@in-1).size=1")); - try (Fuzzer session2 = server.newNetworkFuzzer("/session-info/2")) + try (Session session2 = client.connectToServer(clientSocket2, server.getWsUri().resolve("/session-info/2"))) { - assertNotNull(serverSessions.poll(10, TimeUnit.SECONDS)); - expectedFrames.clear(); + Session serverSession2 = serverSessions.poll(5, TimeUnit.SECONDS); + assertNotNull(serverSession2); sendTextFrameToAll("openSessions|in-2", session1, session2); - session1.expect(Arrays.asList(new Frame(OpCode.TEXT).setPayload("openSessions(@in-2).size=2"))); - session2.expect(Arrays.asList(new Frame(OpCode.TEXT).setPayload("openSessions(@in-2).size=2"))); + assertThat(clientSocket1.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@in-2).size=2")); + assertThat(clientSocket2.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@in-2).size=2")); - try (Fuzzer session3 = server.newNetworkFuzzer("/session-info/3")) + try (Session session3 = client.connectToServer(clientSocket3, server.getWsUri().resolve("/session-info/3"))) { - assertNotNull(serverSessions.poll(10, TimeUnit.SECONDS)); + Session serverSession3 = serverSessions.poll(5, TimeUnit.SECONDS); + assertNotNull(serverSession3); sendTextFrameToAll("openSessions|in-3", session1, session2, session3); + assertThat(clientSocket1.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@in-3).size=3")); + assertThat(clientSocket2.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@in-3).size=3")); + assertThat(clientSocket3.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@in-3).size=3")); + sendTextFrameToAll("openSessions|lvl-3", session1, session2, session3); + assertThat(clientSocket1.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@lvl-3).size=3")); + assertThat(clientSocket2.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@lvl-3).size=3")); + assertThat(clientSocket3.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@lvl-3).size=3")); - expectedFrames.clear(); - expectedFrames.add(new Frame(OpCode.TEXT).setPayload("openSessions(@in-3).size=3")); - expectedFrames.add(new Frame(OpCode.TEXT).setPayload("openSessions(@lvl-3).size=3")); - session1.expect(expectedFrames); - session2.expect(expectedFrames); - session3.expect(expectedFrames); - - session3.sendFrames(new Frame(OpCode.CLOSE)); - session3.expect(Arrays.asList(new Frame(OpCode.CLOSE))); + // assert session is closed, and we have received the notification from the SessionListener + session3.close(); + assertThat(server.getTrackingListener().getClosedSessions().poll(5, TimeUnit.SECONDS), sameInstance(serverSession3)); + assertTrue(clientSocket3.closeLatch.await(5, TimeUnit.SECONDS)); } sendTextFrameToAll("openSessions|lvl-2", session1, session2); - session1.expect(Arrays.asList(new Frame(OpCode.TEXT).setPayload("openSessions(@lvl-2).size=2"))); - session2.expect(Arrays.asList(new Frame(OpCode.TEXT).setPayload("openSessions(@lvl-2).size=2"))); + assertThat(clientSocket1.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@lvl-2).size=2")); + assertThat(clientSocket2.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@lvl-2).size=2")); - session2.sendFrames(new Frame(OpCode.CLOSE)); - session2.expect(Arrays.asList(new Frame(OpCode.CLOSE))); + // assert session is closed, and we have received the notification from the SessionListener + session2.close(); + assertThat(server.getTrackingListener().getClosedSessions().poll(5, TimeUnit.SECONDS), sameInstance(serverSession2)); + assertTrue(clientSocket2.closeLatch.await(5, TimeUnit.SECONDS)); } sendTextFrameToAll("openSessions|lvl-1", session1); - session1.sendFrames(new Frame(OpCode.CLOSE)); + assertThat(clientSocket1.messageQueue.poll(5, TimeUnit.SECONDS), is("openSessions(@lvl-1).size=1")); - expectedFrames.clear(); - expectedFrames.add(new Frame(OpCode.TEXT).setPayload("openSessions(@lvl-1).size=1")); - expectedFrames.add(new Frame(OpCode.CLOSE)); - session1.expect(expectedFrames); + // assert session is closed, and we have received the notification from the SessionListener + session1.close(); + assertThat(server.getTrackingListener().getClosedSessions().poll(5, TimeUnit.SECONDS), sameInstance(serverSession1)); + assertTrue(clientSocket1.closeLatch.await(5, TimeUnit.SECONDS)); } } - private void sendTextFrameToAll(String msg, Fuzzer... sessions) throws IOException + private static void sendTextFrameToAll(String msg, Session... sessions) throws IOException { - for (Fuzzer session : sessions) - session.sendFrames(new Frame(OpCode.TEXT).setPayload(msg)); + for (Session session : sessions) + session.getBasicRemote().sendText(msg); } } diff --git a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebSocketServerContainerExecutorTest.java b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebSocketServerContainerExecutorTest.java index edaaa030aef..ae2345ce60c 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebSocketServerContainerExecutorTest.java +++ b/jetty-websocket/javax-websocket-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebSocketServerContainerExecutorTest.java @@ -51,10 +51,10 @@ import org.eclipse.jetty.websocket.javax.tests.WSURI; import org.junit.jupiter.api.Test; import static org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer.HTTPCLIENT_ATTRIBUTE; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; +import static org.hamcrest.Matchers.startsWith; public class WebSocketServerContainerExecutorTest { @@ -206,36 +206,6 @@ public class WebSocketServerContainerExecutorTest } } - @Test - public void testContextExecutor() throws Exception - { - Server server = new Server(0); - ServletContextHandler contextHandler = new ServletContextHandler(); - server.setHandler(contextHandler); - - //Executor to use - Executor executor = new QueuedThreadPool(); - contextHandler.setAttribute("org.eclipse.jetty.server.Executor", executor); - - // Using JSR356 Server Techniques to connectToServer() - contextHandler.addServlet(ServerConnectServlet.class, "/connect"); - javax.websocket.server.ServerContainer container = JavaxWebSocketServletContainerInitializer.configureContext(contextHandler); - container.addEndpoint(EchoSocket.class); - try - { - server.start(); - String response = GET(server.getURI().resolve("/connect")); - assertThat("Response", response, startsWith("Connected to ws://")); - - Executor containerExecutor = ((JavaxWebSocketServerContainer)container).getExecutor(); - assertThat(containerExecutor, sameInstance(executor)); - } - finally - { - server.stop(); - } - } - private String GET(URI destURI) throws IOException { HttpURLConnection http = (HttpURLConnection)destURI.toURL().openConnection(); diff --git a/jetty-websocket/javax-websocket-tests/src/test/resources/jetty-logging.properties b/jetty-websocket/javax-websocket-tests/src/test/resources/jetty-logging.properties index d51a5031cea..d078063b659 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/resources/jetty-logging.properties +++ b/jetty-websocket/javax-websocket-tests/src/test/resources/jetty-logging.properties @@ -28,7 +28,7 @@ org.eclipse.jetty.LEVEL=WARN # org.eclipse.jetty.io.LEVEL=DEBUG # org.eclipse.jetty.io.ManagedSelector.LEVEL=INFO # org.eclipse.jetty.websocket.LEVEL=DEBUG -# org.eclipse.jetty.websocket.core.internal.WebSocketChannel.LEVEL=DEBUG +# org.eclipse.jetty.websocket.core.internal.WebSocketCoreSessionsion.LEVEL=DEBUG # org.eclipse.jetty.websocket.jsr356.tests.LEVEL=DEBUG # org.eclipse.jetty.websocket.LEVEL=INFO # org.eclipse.jetty.websocket.jsr356.messages.LEVEL=DEBUG diff --git a/jetty-websocket/javax-websocket-tests/src/test/resources/simple/jetty-websocket-httpclient.xml b/jetty-websocket/javax-websocket-tests/src/test/resources/simple/jetty-websocket-httpclient.xml index 86ed90f948a..4ba6b4cbaa4 100644 --- a/jetty-websocket/javax-websocket-tests/src/test/resources/simple/jetty-websocket-httpclient.xml +++ b/jetty-websocket/javax-websocket-tests/src/test/resources/simple/jetty-websocket-httpclient.xml @@ -1,9 +1,9 @@ - + - + false @@ -20,4 +20,4 @@ XmlBasedClient@ - \ No newline at end of file + diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/module-info.java b/jetty-websocket/jetty-websocket-api/src/main/java/module-info.java index 85341d11ab8..77689102cb3 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/module-info.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/module-info.java @@ -16,14 +16,10 @@ // ======================================================================== // -import org.eclipse.jetty.websocket.api.extensions.Extension; - module org.eclipse.jetty.websocket.jetty.api { exports org.eclipse.jetty.websocket.api; exports org.eclipse.jetty.websocket.api.annotations; exports org.eclipse.jetty.websocket.api.extensions; exports org.eclipse.jetty.websocket.api.util; - - uses Extension; } diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/BatchMode.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/BatchMode.java index 412b116150d..e0ad8564469 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/BatchMode.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/BatchMode.java @@ -18,11 +18,8 @@ package org.eclipse.jetty.websocket.api; -import org.eclipse.jetty.websocket.api.extensions.Frame; -import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; - /** - * The possible batch modes when invoking {@link OutgoingFrames#outgoingFrame(Frame, WriteCallback, BatchMode)}. + * The possible batch modes when enqueuing outgoing frames. */ public enum BatchMode { diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Frame.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Frame.java similarity index 97% rename from jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Frame.java rename to jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Frame.java index ade9fc6cd29..eaf32c64a13 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Frame.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Frame.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.api.extensions; +package org.eclipse.jetty.websocket.api; import java.nio.ByteBuffer; diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/RemoteEndpoint.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/RemoteEndpoint.java index 0086e0ab45f..fe80f1978ee 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/RemoteEndpoint.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/RemoteEndpoint.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.api; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.nio.ByteBuffer; import java.util.concurrent.Future; @@ -36,19 +37,8 @@ public interface RemoteEndpoint void sendBytes(ByteBuffer data) throws IOException; /** - * Initiates the asynchronous transmission of a binary message. This method returns before the message is - * transmitted. Developers may use the returned - * Future object to track progress of the transmission. - * - * @param data the data being sent - * @return the Future object representing the send operation. - */ - Future sendBytesByFuture(ByteBuffer data); - - /** - * Initiates the asynchronous transmission of a binary message. This method returns before the message is - * transmitted. Developers may provide a callback to - * be notified when the message has been transmitted or resulted in an error. + * Initiates the asynchronous transmission of a binary message. This method returns before the message is transmitted. + * Developers may provide a callback to be notified when the message has been transmitted or resulted in an error. * * @param data the data being sent * @param callback callback to notify of success or failure of the write operation @@ -56,8 +46,18 @@ public interface RemoteEndpoint void sendBytes(ByteBuffer data, WriteCallback callback); /** - * Send a binary message in pieces, blocking until all of the message has been transmitted. The runtime reads the - * message in order. Non-final pieces are + * Initiates the asynchronous transmission of a binary message. This method returns before the message is transmitted. + * Developers may use the returned Future object to track progress of the transmission. + * + * @param data the data being sent + * @return the Future object representing the send operation. + */ + @Deprecated + Future sendBytesByFuture(ByteBuffer data); + + /** + * Send a binary message in pieces, blocking until all of the message has been transmitted. + * The runtime reads the message in order. Non-final pieces are * sent with isLast set to false. The final piece must be sent with isLast set to true. * * @param fragment the piece of the message being sent @@ -67,35 +67,31 @@ public interface RemoteEndpoint void sendPartialBytes(ByteBuffer fragment, boolean isLast) throws IOException; /** - * Send a text message in pieces, blocking until all of the message has been transmitted. The runtime reads the - * message in order. Non-final pieces are sent - * with isLast set to false. The final piece must be sent with isLast set to true. + * Initiates the asynchronous transmission of a partial binary message. This method returns before the message is + * transmitted. + * The runtime reads the message in order. Non-final pieces are sent with isLast + * set to false. The final piece must be sent with isLast set to true. + * Developers may provide a callback to be notified when the message has been transmitted or resulted in an error. * - * @param fragment the piece of the message being sent + * @param fragment the data being sent * @param isLast true if this is the last piece of the partial bytes - * @throws IOException if unable to send the partial bytes + * @param callback callback to notify of success or failure of the write operation */ - void sendPartialString(String fragment, boolean isLast) throws IOException; + void sendPartialBytes(ByteBuffer fragment, boolean isLast, WriteCallback callback); /** - * Send a Ping message containing the given application data to the remote endpoint. The corresponding Pong message - * may be picked up using the - * MessageHandler.Pong handler. + * Initiates the asynchronous transmission of a partial binary message. This method returns before the message is + * transmitted. + * The runtime reads the message in order. Non-final pieces are sent with isLast + * set to false. The final piece must be sent with isLast set to true. + * Developers may use the returned Future object to track progress of the transmission. * - * @param applicationData the data to be carried in the ping request - * @throws IOException if unable to send the ping + * @param fragment the data being sent + * @param isLast true if this is the last piece of the partial bytes + * @return the Future object representing the send operation. */ - void sendPing(ByteBuffer applicationData) throws IOException; - - /** - * Allows the developer to send an unsolicited Pong message containing the given application data in order to serve - * as a unidirectional heartbeat for the - * session. - * - * @param applicationData the application data to be carried in the pong response. - * @throws IOException if unable to send the pong - */ - void sendPong(ByteBuffer applicationData) throws IOException; + @Deprecated + Future sendPartialBytesByFuture(ByteBuffer fragment, boolean isLast); /** * Send a text message, blocking until all bytes of the message has been transmitted. @@ -107,16 +103,6 @@ public interface RemoteEndpoint */ void sendString(String text) throws IOException; - /** - * Initiates the asynchronous transmission of a text message. This method may return before the message is - * transmitted. Developers may use the returned - * Future object to track progress of the transmission. - * - * @param text the text being sent - * @return the Future object representing the send operation. - */ - Future sendStringByFuture(String text); - /** * Initiates the asynchronous transmission of a text message. This method may return before the message is * transmitted. Developers may provide a callback to @@ -127,6 +113,113 @@ public interface RemoteEndpoint */ void sendString(String text, WriteCallback callback); + /** + * Initiates the asynchronous transmission of a text message. This method may return before the message is + * transmitted. Developers may use the returned + * Future object to track progress of the transmission. + * + * @param text the text being sent + * @return the Future object representing the send operation. + */ + @Deprecated + Future sendStringByFuture(String text); + + /** + * Send a text message in pieces, blocking until all of the message has been transmitted. The runtime reads the + * message in order. Non-final pieces are sent + * with isLast set to false. The final piece must be sent with isLast set to true. + * + * @param fragment the piece of the message being sent + * @param isLast true if this is the last piece of the partial bytes + * @throws IOException if unable to send the partial bytes + */ + void sendPartialString(String fragment, boolean isLast) throws IOException; + + /** + * Initiates the asynchronous transmission of a partial text message. + * This method may return before the message is transmitted. + * The runtime reads the message in order. Non-final pieces are sent with isLast + * set to false. The final piece must be sent with isLast set to true. + * Developers may provide a callback to be notified when the message has been transmitted or resulted in an error. + * + * @param fragment the text being sent + * @param isLast true if this is the last piece of the partial bytes + * @param callback callback to notify of success or failure of the write operation + */ + void sendPartialString(String fragment, boolean isLast, WriteCallback callback) throws IOException; + + /** + * Initiates the asynchronous transmission of a partial text message. + * This method may return before the message is transmitted. + * The runtime reads the message in order. Non-final pieces are sent with isLast + * set to false. The final piece must be sent with isLast set to true. + * Developers may use the returned Future object to track progress of the transmission. + * + * @param fragment the text being sent + * @param isLast true if this is the last piece of the partial bytes + * @return the Future object representing the send operation. + */ + @Deprecated + Future sendPartialStringByFuture(String fragment, boolean isLast) throws IOException; + + /** + * Send a Ping message containing the given application data to the remote endpoint, blocking until all of the + * message has been transmitted. + * The corresponding Pong message may be picked up using the MessageHandler.Pong handler. + * + * @param applicationData the data to be carried in the ping request + * @throws IOException if unable to send the ping + */ + void sendPing(ByteBuffer applicationData) throws IOException; + + /** + * Asynchronously send a Ping message containing the given application data to the remote endpoint. + * The corresponding Pong message may be picked up using the MessageHandler.Pong handler. + * + * @param applicationData the data to be carried in the ping request + * @param callback callback to notify of success or failure of the write operation + */ + void sendPing(ByteBuffer applicationData, WriteCallback callback); + + /** + * Asynchronously send a Ping message containing the given application data to the remote endpoint. + * The corresponding Pong message may be picked up using the MessageHandler.Pong handler. + * + * @param applicationData the data to be carried in the ping request + * @return the Future object representing the send operation. + */ + @Deprecated + Future sendPingByFuture(ByteBuffer applicationData); + + /** + * Allows the developer to send an unsolicited Pong message containing the given application data + * in order to serve as a unidirectional heartbeat for the session, this will block until + * all of the message has been transmitted. + * + * @param applicationData the application data to be carried in the pong response. + * @throws IOException if unable to send the pong + */ + void sendPong(ByteBuffer applicationData) throws IOException; + + /** + * Allows the developer to asynchronously send an unsolicited Pong message containing the given application data + * in order to serve as a unidirectional heartbeat for the session. + * + * @param applicationData the application data to be carried in the pong response. + * @param callback callback to notify of success or failure of the write operation + */ + void sendPong(ByteBuffer applicationData, WriteCallback callback); + + /** + * Allows the developer to asynchronously send an unsolicited Pong message containing the given application data + * in order to serve as a unidirectional heartbeat for the session. + * + * @param applicationData the application data to be carried in the pong response. + * @return the Future object representing the send operation. + */ + @Deprecated + Future sendPongByFuture(ByteBuffer applicationData); + /** * @return the batch mode with which messages are sent. * @see #flush() @@ -144,10 +237,18 @@ public interface RemoteEndpoint /** * Get the InetSocketAddress for the established connection. * - * @return the InetSocketAddress for the established connection. (or null, if the connection is no longer established) + * @return the InetSocketAddress for the established connection. (or null, if there is none) */ + @Deprecated InetSocketAddress getInetSocketAddress(); + /** + * Get the SocketAddress for the established connection. + * + * @return the SocketAddress for the established connection. + */ + SocketAddress getRemoteAddress(); + /** * Flushes messages that may have been batched by the implementation. * diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Session.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Session.java index af166e8b517..ecf96b9bb3f 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Session.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Session.java @@ -18,12 +18,12 @@ package org.eclipse.jetty.websocket.api; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; - import java.io.Closeable; import java.net.InetSocketAddress; import java.net.SocketAddress; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; + /** * Session represents an active link of communications with a Remote WebSocket Endpoint. */ @@ -163,8 +163,12 @@ public interface Session extends WebSocketPolicy, Closeable boolean isSecure(); /** - * Suspend the incoming read events on the connection. - * + * Suspend the delivery of incoming WebSocket frames. + *

    + * If this is called from inside the scope of the message handler the suspend takes effect immediately. + * If suspend is called outside the scope of the message handler then the call may take effect + * after 1 more frame is delivered. + *

    * @return the suspend token suitable for resuming the reading of data on the connection. */ SuspendToken suspend(); diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketAdapter.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketAdapter.java index 3406d895053..1211ce140c0 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketAdapter.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketAdapter.java @@ -26,11 +26,10 @@ package org.eclipse.jetty.websocket.api; public class WebSocketAdapter implements WebSocketListener { private volatile Session session; - private RemoteEndpoint remote; public RemoteEndpoint getRemote() { - return remote; + return session.getRemote(); } public Session getSession() @@ -59,15 +58,13 @@ public class WebSocketAdapter implements WebSocketListener @Override public void onWebSocketClose(int statusCode, String reason) { - this.session = null; - this.remote = null; + /* do nothing */ } @Override public void onWebSocketConnect(Session sess) { this.session = sess; - this.remote = sess.getRemote(); } @Override diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketBehavior.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketBehavior.java index dd024c425ee..df2ba5f3fd8 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketBehavior.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketBehavior.java @@ -27,5 +27,5 @@ package org.eclipse.jetty.websocket.api; public enum WebSocketBehavior { CLIENT, - SERVER; + SERVER } diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketFrameListener.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketFrameListener.java index e8748739b1e..244d50359e1 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketFrameListener.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketFrameListener.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.api; -import org.eclipse.jetty.websocket.api.extensions.Frame; - /** * WebSocket Frame Listener interface for incoming WebSocket frames. */ diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/OnWebSocketFrame.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/OnWebSocketFrame.java index dae7b73b336..a05dc76aba4 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/OnWebSocketFrame.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/OnWebSocketFrame.java @@ -18,22 +18,23 @@ package org.eclipse.jetty.websocket.api.annotations; -import org.eclipse.jetty.websocket.api.Session; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.eclipse.jetty.websocket.api.Frame; +import org.eclipse.jetty.websocket.api.Session; + /** * (ADVANCED) Annotation for tagging methods to receive frame events. *

    * Acceptable method patterns.
    * Note: {@code methodName} can be any name you want to use. *

      - *
    1. public void methodName({@link org.eclipse.jetty.websocket.api.extensions.Frame} frame)
    2. - *
    3. public void methodName({@link Session} session, {@link org.eclipse.jetty.websocket.api.extensions.Frame} frame)
    4. + *
    5. public void methodName({@link Frame} frame)
    6. + *
    7. public void methodName({@link Session} session, {@link Frame} frame)
    8. *
    */ @Documented diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/WebSocket.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/WebSocket.java index 47e354fd829..efe590a3d84 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/WebSocket.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/WebSocket.java @@ -18,15 +18,15 @@ package org.eclipse.jetty.websocket.api.annotations; -import org.eclipse.jetty.websocket.api.BatchMode; -import org.eclipse.jetty.websocket.api.StatusCode; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.StatusCode; + /** * Tags a POJO as being a WebSocket class. */ @@ -39,26 +39,33 @@ public @interface WebSocket /** * The size of the buffer (in bytes) used to read from the network layer. */ - int inputBufferSize() default -2; + int inputBufferSize() default -1; /** * The maximum size of a binary message (in bytes) during parsing/generating. *

    * Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE} */ - int maxBinaryMessageSize() default -2; + int maxBinaryMessageSize() default -1; + + /** + * The time in ms (milliseconds) that a websocket may be idle before closing. + * @deprecated use {@link #idleTimeout()} instead + */ + @Deprecated + int maxIdleTime() default -1; /** * The time in ms (milliseconds) that a websocket may be idle before closing. */ - int maxIdleTime() default -2; + int idleTimeout() default -1; /** * The maximum size of a text message during parsing/generating. *

    * Text messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE} */ - int maxTextMessageSize() default -2; + int maxTextMessageSize() default -1; /** * The output frame buffering mode. diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Extension.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Extension.java deleted file mode 100644 index ba4c25dc529..00000000000 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Extension.java +++ /dev/null @@ -1,84 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.api.extensions; - -/** - * Interface for WebSocket Extensions. - *

    - * That {@link Frame}s are passed through the Extension via the {@link IncomingFrames} and {@link OutgoingFrames} interfaces - */ -public interface Extension extends IncomingFrames, OutgoingFrames -{ - /** - * The active configuration for this extension. - * - * @return the configuration for this extension. never null. - */ - public ExtensionConfig getConfig(); - - /** - * The {@code Sec-WebSocket-Extensions} name for this extension. - *

    - * Also known as the {@code extension-token} per Section 9.1. Negotiating Extensions. - * - * @return the name of the extension - */ - public String getName(); - - /** - * Used to indicate that the extension makes use of the RSV1 bit of the base websocket framing. - *

    - * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV1. - * - * @return true if extension uses RSV1 for its own purposes. - */ - public abstract boolean isRsv1User(); - - /** - * Used to indicate that the extension makes use of the RSV2 bit of the base websocket framing. - *

    - * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV2. - * - * @return true if extension uses RSV2 for its own purposes. - */ - public abstract boolean isRsv2User(); - - /** - * Used to indicate that the extension makes use of the RSV3 bit of the base websocket framing. - *

    - * This is used to adjust validation during parsing, as well as a checkpoint against 2 or more extensions all simultaneously claiming ownership of RSV3. - * - * @return true if extension uses RSV3 for its own purposes. - */ - public abstract boolean isRsv3User(); - - /** - * Set the next {@link IncomingFrames} to call in the chain. - * - * @param nextIncoming the next incoming extension - */ - public void setNextIncomingFrames(IncomingFrames nextIncoming); - - /** - * Set the next {@link OutgoingFrames} to call in the chain. - * - * @param nextOutgoing the next outgoing extension - */ - public void setNextOutgoingFrames(OutgoingFrames nextOutgoing); -} diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionConfig.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionConfig.java index 697481bb49a..b13d5e70ac7 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionConfig.java +++ b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionConfig.java @@ -18,236 +18,50 @@ package org.eclipse.jetty.websocket.api.extensions; -import org.eclipse.jetty.websocket.api.util.QuoteUtil; - -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; import java.util.Map; +import java.util.ServiceLoader; import java.util.Set; /** * Represents an Extension Configuration, as seen during the connection Handshake process. */ -public class ExtensionConfig +public interface ExtensionConfig { - /** - * Parse a single parameterized name. - * - * @param parameterizedName the parameterized name - * @return the ExtensionConfig - */ - public static ExtensionConfig parse(String parameterizedName) + interface Parser { - return new ExtensionConfig(parameterizedName); + ExtensionConfig parse(String parameterizedName); } - /** - * Parse enumeration of {@code Sec-WebSocket-Extensions} header values into a {@code ExtensionConfig} list - * - * @param valuesEnum the raw header values enum - * @return the list of extension configs - */ - public static List parseEnum(Enumeration valuesEnum) + private static ExtensionConfig.Parser getParser() { - List configs = new ArrayList<>(); - - if (valuesEnum != null) - { - while (valuesEnum.hasMoreElements()) - { - Iterator extTokenIter = QuoteUtil.splitAt(valuesEnum.nextElement(), ","); - while (extTokenIter.hasNext()) - { - String extToken = extTokenIter.next(); - configs.add(ExtensionConfig.parse(extToken)); - } - } - } - - return configs; + return ServiceLoader.load(ExtensionConfig.Parser.class).findFirst().get(); } - /** - * Parse 1 or more raw {@code Sec-WebSocket-Extensions} header values into a {@code ExtensionConfig} list - * - * @param rawSecWebSocketExtensions the raw header values - * @return the list of extension configs - */ - public static List parseList(String... rawSecWebSocketExtensions) + static ExtensionConfig parse(String parameterizedName) { - List configs = new ArrayList<>(); - - for (String rawValue : rawSecWebSocketExtensions) - { - Iterator extTokenIter = QuoteUtil.splitAt(rawValue, ","); - while (extTokenIter.hasNext()) - { - String extToken = extTokenIter.next(); - configs.add(ExtensionConfig.parse(extToken)); - } - } - - return configs; + return getParser().parse(parameterizedName); } - /** - * Convert a list of {@code ExtensionConfig} to a header value - * - * @param configs the list of extension configs - * @return the header value (null if no configs present) - */ - public static String toHeaderValue(List configs) - { - if ((configs == null) || (configs.isEmpty())) - { - return null; - } - StringBuilder parameters = new StringBuilder(); - boolean needsDelim = false; - for (ExtensionConfig ext : configs) - { - if (needsDelim) - { - parameters.append(", "); - } - parameters.append(ext.getParameterizedName()); - needsDelim = true; - } - return parameters.toString(); - } + String getName(); - private final String name; - private final Map parameters; + int getParameter(String key, int defValue); - /** - * Copy constructor - * - * @param copy the extension config to copy - */ - public ExtensionConfig(ExtensionConfig copy) - { - this.name = copy.name; - this.parameters = new HashMap<>(); - this.parameters.putAll(copy.parameters); - } + String getParameter(String key, String defValue); - public ExtensionConfig(String parameterizedName) - { - Iterator extListIter = QuoteUtil.splitAt(parameterizedName, ";"); - this.name = extListIter.next(); - this.parameters = new HashMap<>(); + String getParameterizedName(); - // now for parameters - while (extListIter.hasNext()) - { - String extParam = extListIter.next(); - Iterator extParamIter = QuoteUtil.splitAt(extParam, "="); - String key = extParamIter.next().trim(); - String value = null; - if (extParamIter.hasNext()) - { - value = extParamIter.next(); - } - parameters.put(key, value); - } - } - - public ExtensionConfig(String name, Map parameters) - { - this.name = name; - this.parameters = parameters; - } - - public String getName() - { - return name; - } - - public final int getParameter(String key, int defValue) - { - String val = parameters.get(key); - if (val == null) - { - return defValue; - } - return Integer.parseInt(val); - } - - public final String getParameter(String key, String defValue) - { - String val = parameters.get(key); - if (val == null) - { - return defValue; - } - return val; - } - - public final String getParameterizedName() - { - StringBuilder str = new StringBuilder(); - str.append(name); - for (String param : parameters.keySet()) - { - str.append(';'); - str.append(param); - String value = parameters.get(param); - if (value != null) - { - str.append('='); - QuoteUtil.quoteIfNeeded(str, value, ";="); - } - } - return str.toString(); - } - - public final Set getParameterKeys() - { - return parameters.keySet(); - } + Set getParameterKeys(); /** * Return parameters found in request URI. * * @return the parameter map */ - public final Map getParameters() - { - return parameters; - } + Map getParameters(); - /** - * Initialize the parameters on this config from the other configuration. - * - * @param other the other configuration. - */ - public final void init(ExtensionConfig other) - { - this.parameters.clear(); - this.parameters.putAll(other.parameters); - } + void setParameter(String key); - public final void setParameter(String key) - { - parameters.put(key, null); - } + void setParameter(String key, int value); - public final void setParameter(String key, int value) - { - parameters.put(key, Integer.toString(value)); - } - - public final void setParameter(String key, String value) - { - parameters.put(key, value); - } - - @Override - public String toString() - { - return getParameterizedName(); - } + void setParameter(String key, String value); } diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java deleted file mode 100644 index 4d6115cd487..00000000000 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java +++ /dev/null @@ -1,81 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.api.extensions; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.Set; - -public abstract class ExtensionFactory implements Iterable> -{ - private Map> availableExtensions; - - public ExtensionFactory() - { - ServiceLoader extensionLoader = ServiceLoader.load(Extension.class); - availableExtensions = new HashMap<>(); - for (Extension ext : extensionLoader) - { - if (ext != null) - { - availableExtensions.put(ext.getName(), ext.getClass()); - } - } - } - - public Map> getAvailableExtensions() - { - return availableExtensions; - } - - public Class getExtension(String name) - { - return availableExtensions.get(name); - } - - public Set getExtensionNames() - { - return availableExtensions.keySet(); - } - - public boolean isAvailable(String name) - { - return availableExtensions.containsKey(name); - } - - @Override - public Iterator> iterator() - { - return availableExtensions.values().iterator(); - } - - public abstract Extension newInstance(ExtensionConfig config); - - public void register(String name, Class extension) - { - availableExtensions.put(name, extension); - } - - public void unregister(String name) - { - availableExtensions.remove(name); - } -} diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/OutgoingFrames.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/OutgoingFrames.java deleted file mode 100644 index ec81fddb4eb..00000000000 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/OutgoingFrames.java +++ /dev/null @@ -1,44 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.api.extensions; - -import org.eclipse.jetty.websocket.api.BatchMode; -import org.eclipse.jetty.websocket.api.WriteCallback; - -/** - * Interface for dealing with frames outgoing to (eventually) the network layer. - */ -public interface OutgoingFrames -{ - /** - * A frame, and optional callback, intended for the network layer. - *

    - * Note: the frame can undergo many transformations in the various - * layers and extensions present in the implementation. - *

    - * If you are implementing a mutation, you are obliged to handle - * the incoming WriteCallback appropriately. - * - * @param frame the frame to eventually write to the network layer. - * @param callback the callback to notify when the frame is written. - * @param batchMode the batch mode requested by the sender. - */ - void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode); - -} diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java b/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java deleted file mode 100644 index 27d7e1e83c8..00000000000 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java +++ /dev/null @@ -1,493 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.api.util; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.NoSuchElementException; - -/** - * Provide some consistent Http header value and Extension configuration parameter quoting support. - *

    - * While QuotedStringTokenizer exists in jetty-util, and works great with http header values, using it in websocket-api is undesired. - *

      - *
    • Using QuotedStringTokenizer would introduce a dependency to jetty-util that would need to be exposed via the WebAppContext classloader
    • - *
    • ABNF defined extension parameter parsing requirements of RFC-6455 (WebSocket) ABNF, is slightly different than the ABNF parsing defined in RFC-2616 - * (HTTP/1.1).
    • - *
    • Future HTTPbis ABNF changes for parsing will impact QuotedStringTokenizer
    • - *
    - * It was decided to keep this implementation separate for the above reasons. - */ -public class QuoteUtil -{ - private static class DeQuotingStringIterator implements Iterator - { - private enum State - { - START, - TOKEN, - QUOTE_SINGLE, - QUOTE_DOUBLE - } - - private final String input; - private final String delims; - private StringBuilder token; - private boolean hasToken = false; - private int i = 0; - - public DeQuotingStringIterator(String input, String delims) - { - this.input = input; - this.delims = delims; - int len = input.length(); - token = new StringBuilder(len > 1024?512:len / 2); - } - - private void appendToken(char c) - { - if (hasToken) - { - token.append(c); - } - else - { - if (Character.isWhitespace(c)) - { - return; // skip whitespace at start of token. - } - else - { - token.append(c); - hasToken = true; - } - } - } - - @Override - public boolean hasNext() - { - // already found a token - if (hasToken) - { - return true; - } - - State state = State.START; - boolean escape = false; - int inputLen = input.length(); - - while (i < inputLen) - { - char c = input.charAt(i++); - - switch (state) - { - case START: - { - if (c == '\'') - { - state = State.QUOTE_SINGLE; - appendToken(c); - } - else if (c == '\"') - { - state = State.QUOTE_DOUBLE; - appendToken(c); - } - else - { - appendToken(c); - state = State.TOKEN; - } - break; - } - case TOKEN: - { - if (delims.indexOf(c) >= 0) - { - // System.out.printf("hasNext/t: %b [%s]%n",hasToken,token); - return hasToken; - } - else if (c == '\'') - { - state = State.QUOTE_SINGLE; - } - else if (c == '\"') - { - state = State.QUOTE_DOUBLE; - } - appendToken(c); - break; - } - case QUOTE_SINGLE: - { - if (escape) - { - escape = false; - appendToken(c); - } - else if (c == '\'') - { - appendToken(c); - state = State.TOKEN; - } - else if (c == '\\') - { - escape = true; - } - else - { - appendToken(c); - } - break; - } - case QUOTE_DOUBLE: - { - if (escape) - { - escape = false; - appendToken(c); - } - else if (c == '\"') - { - appendToken(c); - state = State.TOKEN; - } - else if (c == '\\') - { - escape = true; - } - else - { - appendToken(c); - } - break; - } - } - // System.out.printf("%s <%s> : [%s]%n",state,c,token); - } - // System.out.printf("hasNext/e: %b [%s]%n",hasToken,token); - return hasToken; - } - - @Override - public String next() - { - if (!hasNext()) - { - throw new NoSuchElementException(); - } - String ret = token.toString(); - token.setLength(0); - hasToken = false; - return QuoteUtil.dequote(ret.trim()); - } - - @Override - public void remove() - { - throw new UnsupportedOperationException("Remove not supported with this iterator"); - } - } - - /** - * ABNF from RFC 2616, RFC 822, and RFC 6455 specified characters requiring quoting. - */ - public static final String ABNF_REQUIRED_QUOTING = "\"'\\\n\r\t\f\b%+ ;="; - - private static final char UNICODE_TAG = 0xFFFF; - private static final char[] escapes = new char[32]; - - static - { - Arrays.fill(escapes, UNICODE_TAG); - // non-unicode - escapes['\b'] = 'b'; - escapes['\t'] = 't'; - escapes['\n'] = 'n'; - escapes['\f'] = 'f'; - escapes['\r'] = 'r'; - } - - private static int dehex(byte b) - { - if ((b >= '0') && (b <= '9')) - { - return (byte)(b - '0'); - } - if ((b >= 'a') && (b <= 'f')) - { - return (byte)((b - 'a') + 10); - } - if ((b >= 'A') && (b <= 'F')) - { - return (byte)((b - 'A') + 10); - } - throw new IllegalArgumentException("!hex:" + Integer.toHexString(0xff & b)); - } - - /** - * Remove quotes from a string, only if the input string start with and end with the same quote character. - * - * @param str the string to remove surrounding quotes from - * @return the de-quoted string - */ - public static String dequote(String str) - { - char start = str.charAt(0); - if ((start == '\'') || (start == '\"')) - { - // possibly quoted - char end = str.charAt(str.length() - 1); - if (start == end) - { - // dequote - return str.substring(1, str.length() - 1); - } - } - return str; - } - - public static void escape(StringBuilder buf, String str) - { - for (char c : str.toCharArray()) - { - if (c >= 32) - { - // non special character - if ((c == '"') || (c == '\\')) - { - buf.append('\\'); - } - buf.append(c); - } - else - { - // special characters, requiring escaping - char escaped = escapes[c]; - - // is this a unicode escape? - if (escaped == UNICODE_TAG) - { - buf.append("\\u00"); - if (c < 0x10) - { - buf.append('0'); - } - buf.append(Integer.toString(c, 16)); // hex - } - else - { - // normal escape - buf.append('\\').append(escaped); - } - } - } - } - - /** - * Simple quote of a string, escaping where needed. - * - * @param buf the StringBuilder to append to - * @param str the string to quote - */ - public static void quote(StringBuilder buf, String str) - { - buf.append('"'); - escape(buf, str); - buf.append('"'); - } - - /** - * Append into buf the provided string, adding quotes if needed. - *

    - * Quoting is determined if any of the characters in the {@code delim} are found in the input {@code str}. - * - * @param buf the buffer to append to - * @param str the string to possibly quote - * @param delim the delimiter characters that will trigger automatic quoting - */ - public static void quoteIfNeeded(StringBuilder buf, String str, String delim) - { - if (str == null) - { - return; - } - // check for delimiters in input string - int len = str.length(); - if (len == 0) - { - return; - } - int ch; - for (int i = 0; i < len; i++) - { - ch = str.codePointAt(i); - if (delim.indexOf(ch) >= 0) - { - // found a delimiter codepoint. we need to quote it. - quote(buf, str); - return; - } - } - - // no special delimiters used, no quote needed. - buf.append(str); - } - - /** - * Create an iterator of the input string, breaking apart the string at the provided delimiters, removing quotes and triming the parts of the string as - * needed. - * - * @param str the input string to split apart - * @param delims the delimiter characters to split the string on - * @return the iterator of the parts of the string, trimmed, with quotes around the string part removed, and unescaped - */ - public static Iterator splitAt(String str, String delims) - { - return new DeQuotingStringIterator(str.trim(), delims); - } - - public static String unescape(String str) - { - if (str == null) - { - // nothing there - return null; - } - - int len = str.length(); - if (len <= 1) - { - // impossible to be escaped - return str; - } - - StringBuilder ret = new StringBuilder(len - 2); - boolean escaped = false; - char c; - for (int i = 0; i < len; i++) - { - c = str.charAt(i); - if (escaped) - { - escaped = false; - switch (c) - { - case 'n': - ret.append('\n'); - break; - case 'r': - ret.append('\r'); - break; - case 't': - ret.append('\t'); - break; - case 'f': - ret.append('\f'); - break; - case 'b': - ret.append('\b'); - break; - case '\\': - ret.append('\\'); - break; - case '/': - ret.append('/'); - break; - case '"': - ret.append('"'); - break; - case 'u': - ret.append( - (char)((dehex((byte)str.charAt(i++)) << 24) + (dehex((byte)str.charAt(i++)) << 16) + (dehex((byte)str.charAt(i++)) << 8) + (dehex( - (byte)str - .charAt(i++))))); - break; - default: - ret.append(c); - } - } - else if (c == '\\') - { - escaped = true; - } - else - { - ret.append(c); - } - } - return ret.toString(); - } - - public static String join(Object[] objs, String delim) - { - if (objs == null) - { - return ""; - } - StringBuilder ret = new StringBuilder(); - int len = objs.length; - for (int i = 0; i < len; i++) - { - if (i > 0) - { - ret.append(delim); - } - if (objs[i] instanceof String) - { - ret.append('"').append(objs[i]).append('"'); - } - else - { - ret.append(objs[i]); - } - } - return ret.toString(); - } - - public static String join(Collection objs, String delim) - { - if (objs == null) - { - return ""; - } - StringBuilder ret = new StringBuilder(); - boolean needDelim = false; - for (Object obj : objs) - { - if (needDelim) - { - ret.append(delim); - } - if (obj instanceof String) - { - ret.append('"').append(obj).append('"'); - } - else - { - ret.append(obj); - } - needDelim = true; - } - return ret.toString(); - } -} diff --git a/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/QuoteUtilTest.java b/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/QuoteUtilTest.java deleted file mode 100644 index 68dc61c4603..00000000000 --- a/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/QuoteUtilTest.java +++ /dev/null @@ -1,156 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.api.util; - -import org.junit.jupiter.api.Test; - -import java.util.Iterator; -import java.util.NoSuchElementException; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * Test QuoteUtil - */ -public class QuoteUtilTest -{ - private void assertSplitAt(Iterator iter, String... expectedParts) - { - int len = expectedParts.length; - for (int i = 0; i < len; i++) - { - String expected = expectedParts[i]; - assertThat("Split[" + i + "].hasNext()", iter.hasNext(), is(true)); - assertThat("Split[" + i + "].next()", iter.next(), is(expected)); - } - } - - @Test - public void testSplitAt_PreserveQuoting() - { - Iterator iter = QuoteUtil.splitAt("permessage-compress; method=\"foo, bar\"", ";"); - assertSplitAt(iter, "permessage-compress", "method=\"foo, bar\""); - } - - @Test - public void testSplitAt_PreserveQuotingWithNestedDelim() - { - Iterator iter = QuoteUtil.splitAt("permessage-compress; method=\"foo; x=10\"", ";"); - assertSplitAt(iter, "permessage-compress", "method=\"foo; x=10\""); - } - - @Test - public void testSplitAtAllWhitespace() - { - Iterator iter = QuoteUtil.splitAt(" ", "="); - assertThat("Has Next", iter.hasNext(), is(false)); - assertThrows(NoSuchElementException.class, () -> iter.next()); - } - - @Test - public void testSplitAtEmpty() - { - Iterator iter = QuoteUtil.splitAt("", "="); - assertThat("Has Next", iter.hasNext(), is(false)); - assertThrows(NoSuchElementException.class, () -> iter.next()); - } - - @Test - public void testSplitAtHelloWorld() - { - Iterator iter = QuoteUtil.splitAt("Hello World", " ="); - assertSplitAt(iter, "Hello", "World"); - } - - @Test - public void testSplitAtKeyValue_Message() - { - Iterator iter = QuoteUtil.splitAt("method=\"foo, bar\"", "="); - assertSplitAt(iter, "method", "foo, bar"); - } - - @Test - public void testSplitAtQuotedDelim() - { - // test that split ignores delimiters that occur within a quoted - // part of the sequence. - Iterator iter = QuoteUtil.splitAt("A,\"B,C\",D", ","); - assertSplitAt(iter, "A", "B,C", "D"); - } - - @Test - public void testSplitAtSimple() - { - Iterator iter = QuoteUtil.splitAt("Hi", "="); - assertSplitAt(iter, "Hi"); - } - - @Test - public void testSplitKeyValue_Quoted() - { - Iterator iter = QuoteUtil.splitAt("Key = \"Value\"", "="); - assertSplitAt(iter, "Key", "Value"); - } - - @Test - public void testSplitKeyValue_QuotedValueList() - { - Iterator iter = QuoteUtil.splitAt("Fruit = \"Apple, Banana, Cherry\"", "="); - assertSplitAt(iter, "Fruit", "Apple, Banana, Cherry"); - } - - @Test - public void testSplitKeyValue_QuotedWithDelim() - { - Iterator iter = QuoteUtil.splitAt("Key = \"Option=Value\"", "="); - assertSplitAt(iter, "Key", "Option=Value"); - } - - @Test - public void testSplitKeyValue_Simple() - { - Iterator iter = QuoteUtil.splitAt("Key=Value", "="); - assertSplitAt(iter, "Key", "Value"); - } - - @Test - public void testSplitKeyValue_WithWhitespace() - { - Iterator iter = QuoteUtil.splitAt("Key = Value", "="); - assertSplitAt(iter, "Key", "Value"); - } - - @Test - public void testQuoteIfNeeded() - { - StringBuilder buf = new StringBuilder(); - QuoteUtil.quoteIfNeeded(buf, "key", ","); - assertThat("key", buf.toString(), is("key")); - } - - @Test - public void testQuoteIfNeeded_null() - { - StringBuilder buf = new StringBuilder(); - QuoteUtil.quoteIfNeeded(buf, null, ";="); - assertThat("", buf.toString(), is("")); - } -} diff --git a/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/QuoteUtil_QuoteTest.java b/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/QuoteUtil_QuoteTest.java deleted file mode 100644 index 42950454620..00000000000 --- a/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/QuoteUtil_QuoteTest.java +++ /dev/null @@ -1,72 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.api.util; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -/** - * Test QuoteUtil.quote(), and QuoteUtil.dequote() - */ -public class QuoteUtil_QuoteTest -{ - public static Stream data() - { - // The various quoting of a String - List data = new ArrayList<>(); - - data.add(new Object[] { "Hi", "\"Hi\"" }); - data.add(new Object[] { "Hello World", "\"Hello World\"" }); - data.add(new Object[] { "9.0.0", "\"9.0.0\"" }); - data.add(new Object[] { "Something \"Special\"", - "\"Something \\\"Special\\\"\"" }); - data.add(new Object[] { "A Few\n\"Good\"\tMen", - "\"A Few\\n\\\"Good\\\"\\tMen\"" }); - - return data.stream().map(Arguments::of); - } - - @ParameterizedTest - @MethodSource("data") - public void testDequoting(final String unquoted, final String quoted) - { - String actual = QuoteUtil.dequote(quoted); - actual = QuoteUtil.unescape(actual); - assertThat(actual, is(unquoted)); - } - - @ParameterizedTest - @MethodSource("data") - public void testQuoting(final String unquoted, final String quoted) - { - StringBuilder buf = new StringBuilder(); - QuoteUtil.quote(buf, unquoted); - - String actual = buf.toString(); - assertThat(actual, is(quoted)); - } -} diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/FrameCallback.java b/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/JettyUpgradeListener.java similarity index 58% rename from jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/FrameCallback.java rename to jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/JettyUpgradeListener.java index 1aaf5501e17..611ffcf4461 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/FrameCallback.java +++ b/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/JettyUpgradeListener.java @@ -16,38 +16,27 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.api; +package org.eclipse.jetty.websocket.client; -public interface FrameCallback +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.HttpResponse; + +public interface JettyUpgradeListener { /** - *

    - * Callback invoked when the frame fails. - *

    + * Event that triggers before the Handshake request is sent. * - * @param cause the reason for the frame failure + * @param request the request */ - void fail(Throwable cause); + default void onHandshakeRequest(HttpRequest request) + {} /** - *

    - * Callback invoked when the frame read/write completes. - *

    + * Event that triggers after the Handshake response has been received. * - * @see #fail(Throwable) + * @param request the request that was used + * @param response the response that was received */ - void succeed(); - - class Adapter implements FrameCallback - { - @Override - public void fail(Throwable cause) - { - } - - @Override - public void succeed() - { - } - } + default void onHandshakeResponse(HttpRequest request, HttpResponse response) + {} } diff --git a/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java b/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java index 00ecfb1fede..6528a9b5d1e 100644 --- a/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java +++ b/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java @@ -32,7 +32,10 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.function.Consumer; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.log.Log; @@ -40,7 +43,6 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.UpgradeRequest; -import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.api.WebSocketBehavior; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.client.impl.JettyClientUpgradeRequest; @@ -49,7 +51,8 @@ import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandlerFactory; import org.eclipse.jetty.websocket.common.SessionTracker; import org.eclipse.jetty.websocket.common.WebSocketContainer; import org.eclipse.jetty.websocket.common.WebSocketSessionListener; -import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; +import org.eclipse.jetty.websocket.core.FrameHandler; +import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.client.UpgradeListener; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; @@ -61,23 +64,15 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli private final JettyWebSocketFrameHandlerFactory frameHandlerFactory; private final List sessionListeners = new CopyOnWriteArrayList<>(); private final SessionTracker sessionTracker = new SessionTracker(); - private ClassLoader contextClassLoader; - private DecoratedObjectFactory objectFactory; - private WebSocketExtensionRegistry extensionRegistry; - private int inputBufferSize = 4 * 1024; - private int outputBufferSize = 4 * 1024; - private long maxBinaryMessageSize = 64 * 1024; - private long maxTextMessageSize = 64 * 1024; + private final FrameHandler.ConfigurationCustomizer configurationCustomizer = new FrameHandler.ConfigurationCustomizer(); + private WebSocketComponents components = new WebSocketComponents(); /** * Instantiate a WebSocketClient with defaults */ public WebSocketClient() { - this(new WebSocketCoreClient()); - this.coreClient.getHttpClient().setName("Jetty-WebSocketClient@" + hashCode()); - // We created WebSocketCoreClient, let lifecycle be managed by us - addManaged(coreClient); + this(null); } /** @@ -87,19 +82,14 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli */ public WebSocketClient(HttpClient httpClient) { - this(new WebSocketCoreClient(httpClient)); - // We created WebSocketCoreClient, let lifecycle be managed by us + coreClient = new WebSocketCoreClient(httpClient, components); addManaged(coreClient); - } - private WebSocketClient(WebSocketCoreClient coreClient) - { - this.coreClient = coreClient; - this.contextClassLoader = this.getClass().getClassLoader(); - this.objectFactory = new DecoratedObjectFactory(); - this.extensionRegistry = new WebSocketExtensionRegistry(); - this.frameHandlerFactory = new JettyWebSocketFrameHandlerFactory(this); - this.sessionListeners.add(sessionTracker); + if (httpClient == null) + coreClient.getHttpClient().setName("Jetty-WebSocketClient@" + hashCode()); + + frameHandlerFactory = new JettyWebSocketFrameHandlerFactory(this); + sessionListeners.add(sessionTracker); addBean(sessionTracker); } @@ -128,17 +118,49 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli * @param websocket the websocket object * @param toUri the websocket uri to connect to * @param request the upgrade request information - * @param listener the upgrade listener + * @param upgradeListener the upgrade listener * @return the future for the session, available on success of connect * @throws IOException if unable to connect */ - public CompletableFuture connect(Object websocket, URI toUri, UpgradeRequest request, UpgradeListener listener) throws IOException + public CompletableFuture connect(Object websocket, URI toUri, UpgradeRequest request, JettyUpgradeListener upgradeListener) throws IOException { + for (Connection.Listener listener : getBeans(Connection.Listener.class)) + coreClient.addBean(listener); + JettyClientUpgradeRequest upgradeRequest = new JettyClientUpgradeRequest(this, coreClient, request, toUri, websocket); - if (listener != null) - upgradeRequest.addListener(listener); - coreClient.connect(upgradeRequest); - return upgradeRequest.getFutureSession(); + if (upgradeListener != null) + { + upgradeRequest.addListener(new UpgradeListener() + { + @Override + public void onHandshakeRequest(HttpRequest request) + { + upgradeListener.onHandshakeRequest(request); + } + + @Override + public void onHandshakeResponse(HttpRequest request, HttpResponse response) + { + upgradeListener.onHandshakeResponse(request, response); + } + }); + } + upgradeRequest.setConfiguration(configurationCustomizer); + CompletableFuture futureSession = new CompletableFuture<>(); + + coreClient.connect(upgradeRequest).whenComplete((coreSession, error)-> + { + if (error != null) + { + futureSession.completeExceptionally(error); + return; + } + + JettyWebSocketFrameHandler frameHandler = (JettyWebSocketFrameHandler)upgradeRequest.getFrameHandler(); + futureSession.complete(frameHandler.getSession()); + }); + + return futureSession; } @Override @@ -184,61 +206,61 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli @Override public Duration getIdleTimeout() { - return Duration.ofMillis(getHttpClient().getIdleTimeout()); + return configurationCustomizer.getIdleTimeout(); } @Override public int getInputBufferSize() { - return this.inputBufferSize; + return configurationCustomizer.getInputBufferSize(); } @Override public int getOutputBufferSize() { - return this.outputBufferSize; + return configurationCustomizer.getOutputBufferSize(); } @Override public long getMaxBinaryMessageSize() { - return this.maxBinaryMessageSize; + return configurationCustomizer.getMaxBinaryMessageSize(); } @Override public long getMaxTextMessageSize() { - return this.maxTextMessageSize; + return configurationCustomizer.getMaxTextMessageSize(); } @Override public void setIdleTimeout(Duration duration) { - getHttpClient().setIdleTimeout(duration.toMillis()); + configurationCustomizer.setIdleTimeout(duration); } @Override public void setInputBufferSize(int size) { - this.inputBufferSize = size; + configurationCustomizer.setInputBufferSize(size); } @Override public void setOutputBufferSize(int size) { - this.outputBufferSize = size; + configurationCustomizer.setOutputBufferSize(size); } @Override public void setMaxBinaryMessageSize(long size) { - this.maxBinaryMessageSize = size; + configurationCustomizer.setMaxBinaryMessageSize(size); } @Override public void setMaxTextMessageSize(long size) { - this.maxTextMessageSize = size; + configurationCustomizer.setMaxTextMessageSize(size); } public SocketAddress getBindAddress() @@ -287,11 +309,6 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli return getHttpClient().getExecutor(); } - public WebSocketExtensionRegistry getExtensionRegistry() - { - return extensionRegistry; - } - public HttpClient getHttpClient() { return coreClient.getHttpClient(); @@ -299,7 +316,7 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli public DecoratedObjectFactory getObjectFactory() { - return objectFactory; + return components.getObjectFactory(); } public Collection getOpenSessions() @@ -307,10 +324,9 @@ public class WebSocketClient extends ContainerLifeCycle implements WebSocketPoli return sessionTracker.getSessions(); } - public JettyWebSocketFrameHandler newFrameHandler(Object websocketPojo, UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse, - CompletableFuture futureSession) + public JettyWebSocketFrameHandler newFrameHandler(Object websocketPojo) { - return frameHandlerFactory.newJettyFrameHandler(websocketPojo, upgradeRequest, upgradeResponse, futureSession); + return frameHandlerFactory.newJettyFrameHandler(websocketPojo); } /** diff --git a/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/impl/JettyClientUpgradeRequest.java b/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/impl/JettyClientUpgradeRequest.java index de112f4d05a..cef7520d78d 100644 --- a/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/impl/JettyClientUpgradeRequest.java +++ b/jetty-websocket/jetty-websocket-client/src/main/java/org/eclipse/jetty/websocket/client/impl/JettyClientUpgradeRequest.java @@ -21,18 +21,18 @@ package org.eclipse.jetty.websocket.client.impl; import java.net.HttpCookie; import java.net.URI; import java.util.List; -import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.eclipse.jetty.client.HttpResponse; +import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.UpgradeRequest; -import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler; +import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; @@ -40,17 +40,14 @@ import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; public class JettyClientUpgradeRequest extends ClientUpgradeRequest { private final WebSocketClient containerContext; - private final Object websocketPojo; - private final CompletableFuture futureSession; private final DelegatedJettyClientUpgradeRequest handshakeRequest; + private final JettyWebSocketFrameHandler frameHandler; public JettyClientUpgradeRequest(WebSocketClient clientContainer, WebSocketCoreClient coreClient, UpgradeRequest request, URI requestURI, Object websocketPojo) { super(coreClient, requestURI); this.containerContext = clientContainer; - this.websocketPojo = websocketPojo; - this.futureSession = new CompletableFuture<>(); if (request != null) { @@ -74,9 +71,9 @@ public class JettyClientUpgradeRequest extends ClientUpgradeRequest setSubProtocols(request.getSubProtocols()); // Copy extensions - /* TODO or not? - setExtensions(request.getExtensions()); - */ + setExtensions(request.getExtensions().stream() + .map(c -> new ExtensionConfig(c.getName(), c.getParameters())) + .collect(Collectors.toList())); // Copy method from upgradeRequest object if (request.getMethod() != null) @@ -88,6 +85,7 @@ public class JettyClientUpgradeRequest extends ClientUpgradeRequest } handshakeRequest = new DelegatedJettyClientUpgradeRequest(this); + frameHandler = containerContext.newFrameHandler(websocketPojo); } @Override @@ -97,25 +95,17 @@ public class JettyClientUpgradeRequest extends ClientUpgradeRequest handshakeRequest.configure(endp); } - protected void handleException(Throwable failure) + @Override + public void upgrade(HttpResponse response, HttpConnectionOverHTTP httpConnection) { - super.handleException(failure); - futureSession.completeExceptionally(failure); + frameHandler.setUpgradeRequest(new DelegatedJettyClientUpgradeRequest(this)); + frameHandler.setUpgradeResponse(new DelegatedJettyClientUpgradeResponse(response)); + super.upgrade(response, httpConnection); } @Override - public FrameHandler getFrameHandler(WebSocketCoreClient coreClient, HttpResponse response) + public FrameHandler getFrameHandler() { - UpgradeResponse upgradeResponse = new DelegatedJettyClientUpgradeResponse(response); - - JettyWebSocketFrameHandler frameHandler = containerContext.newFrameHandler(websocketPojo, - handshakeRequest, upgradeResponse, futureSession); - return frameHandler; } - - public CompletableFuture getFutureSession() - { - return futureSession; - } } diff --git a/jetty-websocket/jetty-websocket-client/src/test/java/org/eclipse/jetty/websocket/client/HttpClientInitTest.java b/jetty-websocket/jetty-websocket-client/src/test/java/org/eclipse/jetty/websocket/client/HttpClientInitTest.java index 221f582a3ec..9dfef772217 100644 --- a/jetty-websocket/jetty-websocket-client/src/test/java/org/eclipse/jetty/websocket/client/HttpClientInitTest.java +++ b/jetty-websocket/jetty-websocket-client/src/test/java/org/eclipse/jetty/websocket/client/HttpClientInitTest.java @@ -18,16 +18,16 @@ package org.eclipse.jetty.websocket.client; +import java.util.concurrent.Executor; + import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.Test; -import java.util.concurrent.Executor; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.core.Is.is; public class HttpClientInitTest diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/module-info.java b/jetty-websocket/jetty-websocket-common/src/main/java/module-info.java index d292fa13e0e..710d1ddfed5 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/module-info.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/module-info.java @@ -16,6 +16,9 @@ // ======================================================================== // +import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.eclipse.jetty.websocket.common.ExtensionConfigParser; + module org.eclipse.jetty.websocket.jetty.common { exports org.eclipse.jetty.websocket.common; @@ -27,4 +30,6 @@ module org.eclipse.jetty.websocket.jetty.common requires org.eclipse.jetty.util; requires org.eclipse.jetty.websocket.core; requires org.eclipse.jetty.websocket.jetty.api; + + provides ExtensionConfig.Parser with ExtensionConfigParser; } diff --git a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/IncomingFrames.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/ExtensionConfigParser.java similarity index 61% rename from jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/IncomingFrames.java rename to jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/ExtensionConfigParser.java index 384d9831c5a..e3cb79e1b2c 100644 --- a/jetty-websocket/jetty-websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/IncomingFrames.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/ExtensionConfigParser.java @@ -16,23 +16,21 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.api.extensions; +package org.eclipse.jetty.websocket.common; -/** - * Interface for dealing with Incoming Frames. - */ -public interface IncomingFrames +import org.eclipse.jetty.websocket.core.ExtensionConfig; + +public class ExtensionConfigParser implements org.eclipse.jetty.websocket.api.extensions.ExtensionConfig.Parser { - public void incomingError(Throwable t); - /** - * Process the incoming frame. - *

    - * Note: if you need to hang onto any information from the frame, be sure - * to copy it, as the information contained in the Frame will be released - * and/or reused by the implementation. + * Parse a single parameterized name. * - * @param frame the frame to process + * @param parameterizedName the parameterized name + * @return the ExtensionConfig */ - public void incomingFrame(Frame frame); + @Override + public JettyExtensionConfig parse(String parameterizedName) + { + return new JettyExtensionConfig(ExtensionConfig.parse(parameterizedName)); + } } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyExtensionConfig.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyExtensionConfig.java new file mode 100644 index 00000000000..3a07d11fb8c --- /dev/null +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyExtensionConfig.java @@ -0,0 +1,128 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.common; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.jetty.websocket.core.ExtensionConfig; + +/** + * Represents an Extension Configuration, as seen during the connection Handshake process. + */ +public class JettyExtensionConfig implements org.eclipse.jetty.websocket.api.extensions.ExtensionConfig +{ + + private final ExtensionConfig config; + + /** + * Copy constructor + * + * @param copy the extension config to copy + */ + public JettyExtensionConfig(JettyExtensionConfig copy) + { + this(copy.config); + } + + public JettyExtensionConfig(ExtensionConfig config) + { + this.config = config; + } + + public JettyExtensionConfig(String parameterizedName) + { + this.config = new ExtensionConfig(parameterizedName); + } + + public JettyExtensionConfig(String name, Map parameters) + { + this.config = new ExtensionConfig(name, parameters); + } + + public ExtensionConfig getCoreConfig() + { + return config; + } + + @Override + public String getName() + { + return config.getName(); + } + + @Override + public final int getParameter(String key, int defValue) + { + return config.getParameter(key, defValue); + } + + @Override + public final String getParameter(String key, String defValue) + { + return config.getParameter(key, defValue); + } + + @Override + public final String getParameterizedName() + { + return config.getParameterizedName(); + } + + @Override + public final Set getParameterKeys() + { + return config.getParameterKeys(); + } + + /** + * Return parameters found in request URI. + * + * @return the parameter map + */ + @Override + public final Map getParameters() + { + return config.getParameters(); + } + + @Override + public final void setParameter(String key) + { + config.setParameter(key); + } + + @Override + public final void setParameter(String key, int value) + { + config.setParameter(key, value); + } + + @Override + public final void setParameter(String key, String value) + { + config.setParameter(key, value); + } + + @Override + public String toString() + { + return config.toString(); + } +} diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrame.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrame.java index 76e415590f7..a1d6ad1aa9f 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrame.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrame.java @@ -18,11 +18,11 @@ package org.eclipse.jetty.websocket.common; -import org.eclipse.jetty.websocket.core.Frame; - import java.nio.ByteBuffer; -public class JettyWebSocketFrame implements org.eclipse.jetty.websocket.api.extensions.Frame +import org.eclipse.jetty.websocket.core.Frame; + +public class JettyWebSocketFrame implements org.eclipse.jetty.websocket.api.Frame { private final Frame frame; diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java index 40413395f7d..f5a91bfc2f0 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandler.java @@ -20,14 +20,13 @@ package org.eclipse.jetty.websocket.common; import java.lang.invoke.MethodHandle; import java.nio.ByteBuffer; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.common.invoke.InvalidSignatureException; @@ -45,9 +44,17 @@ import org.eclipse.jetty.websocket.core.WebSocketTimeoutException; public class JettyWebSocketFrameHandler implements FrameHandler { + private enum SuspendState + { + DEMANDING, + SUSPENDING, + SUSPENDED + } + private final Logger log; private final WebSocketContainer container; private final Object endpointInstance; + private final BatchMode batchMode; private MethodHandle openHandle; private MethodHandle closeHandle; private MethodHandle errorHandle; @@ -58,39 +65,32 @@ public class JettyWebSocketFrameHandler implements FrameHandler private MethodHandle frameHandle; private MethodHandle pingHandle; private MethodHandle pongHandle; - /** - * Immutable HandshakeRequest available via Session - */ - private final UpgradeRequest upgradeRequest; - /** - * Immutable HandshakeResponse available via Session - */ - private final UpgradeResponse upgradeResponse; - private final CompletableFuture futureSession; + private UpgradeRequest upgradeRequest; + private UpgradeResponse upgradeResponse; + private final Customizer customizer; private MessageSink textSink; private MessageSink binarySink; private MessageSink activeMessageSink; - private WebSocketSessionImpl session; + private WebSocketSession session; + private SuspendState state = SuspendState.DEMANDING; + private Runnable delayedOnFrame; public JettyWebSocketFrameHandler(WebSocketContainer container, Object endpointInstance, - UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse, MethodHandle openHandle, MethodHandle closeHandle, MethodHandle errorHandle, MethodHandle textHandle, MethodHandle binaryHandle, Class textSinkClass, Class binarySinkClass, MethodHandle frameHandle, MethodHandle pingHandle, MethodHandle pongHandle, - CompletableFuture futureSession, + BatchMode batchMode, Customizer customizer) { this.log = Log.getLogger(endpointInstance.getClass()); this.container = container; this.endpointInstance = endpointInstance; - this.upgradeRequest = upgradeRequest; - this.upgradeResponse = upgradeResponse; this.openHandle = openHandle; this.closeHandle = closeHandle; @@ -103,12 +103,36 @@ public class JettyWebSocketFrameHandler implements FrameHandler this.pingHandle = pingHandle; this.pongHandle = pongHandle; - this.futureSession = futureSession; - + this.batchMode = batchMode; this.customizer = customizer; } - public WebSocketSessionImpl getSession() + public void setUpgradeRequest(UpgradeRequest upgradeRequest) + { + this.upgradeRequest = upgradeRequest; + } + + public void setUpgradeResponse(UpgradeResponse upgradeResponse) + { + this.upgradeResponse = upgradeResponse; + } + + public UpgradeRequest getUpgradeRequest() + { + return upgradeRequest; + } + + public UpgradeResponse getUpgradeResponse() + { + return upgradeResponse; + } + + public BatchMode getBatchMode() + { + return batchMode; + } + + public WebSocketSession getSession() { return session; } @@ -119,8 +143,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler try { customizer.customize(coreSession); - - session = new WebSocketSessionImpl(coreSession, this, upgradeRequest, upgradeResponse); + session = new WebSocketSession(coreSession, this); frameHandle = JettyWebSocketFrameHandlerFactory.bindTo(frameHandle, session); openHandle = JettyWebSocketFrameHandlerFactory.bindTo(openHandle, session); @@ -134,11 +157,10 @@ public class JettyWebSocketFrameHandler implements FrameHandler Executor executor = container.getExecutor(); if (textHandle != null) - textSink = JettyWebSocketFrameHandlerFactory.createMessageSink(textHandle, textSinkClass, executor, coreSession.getMaxTextMessageSize()); + textSink = JettyWebSocketFrameHandlerFactory.createMessageSink(textHandle, textSinkClass, executor, session); if (binaryHandle != null) - binarySink = JettyWebSocketFrameHandlerFactory - .createMessageSink(binaryHandle, binarySinkClass, executor, coreSession.getMaxBinaryMessageSize()); + binarySink = JettyWebSocketFrameHandlerFactory.createMessageSink(binaryHandle, binarySinkClass, executor, session); if (openHandle != null) openHandle.invoke(); @@ -146,23 +168,35 @@ public class JettyWebSocketFrameHandler implements FrameHandler container.notifySessionListeners((listener) -> listener.onWebSocketSessionOpened(session)); callback.succeeded(); - futureSession.complete(session); + demand(); } catch (Throwable cause) { - callback.failed(new WebSocketException(endpointInstance.getClass().getName() + " OPEN method error: " + cause.getMessage(), cause)); - futureSession.completeExceptionally(cause); + callback.failed(new WebSocketException(endpointInstance.getClass().getSimpleName() + " OPEN method error: " + cause.getMessage(), cause)); } } - /** - * @see #onFrame(Frame,Callback) - */ - public final void onFrame(Frame frame) {} - @Override public void onFrame(Frame frame, Callback callback) { + synchronized (this) + { + switch(state) + { + case DEMANDING: + break; + + case SUSPENDING: + delayedOnFrame = ()->onFrame(frame, callback); + state = SuspendState.SUSPENDED; + return; + + case SUSPENDED: + default: + throw new IllegalStateException(); + } + } + // Send to raw frame handling on user side (eg: WebSocketFrameListener) if (frameHandle != null) { @@ -172,29 +206,47 @@ public class JettyWebSocketFrameHandler implements FrameHandler } catch (Throwable cause) { - throw new WebSocketException(endpointInstance.getClass().getName() + " FRAME method error: " + cause.getMessage(), cause); + throw new WebSocketException(endpointInstance.getClass().getSimpleName() + " FRAME method error: " + cause.getMessage(), cause); } } + // Demand after succeeding any received frame + Callback demandingCallback = Callback.from(()-> + { + try + { + demand(); + } + catch (Throwable t) + { + callback.failed(t); + return; + } + + callback.succeeded(); + }, + callback::failed + ); + switch (frame.getOpCode()) { case OpCode.CLOSE: onCloseFrame(frame, callback); break; case OpCode.PING: - onPingFrame(frame, callback); + onPingFrame(frame, demandingCallback); break; case OpCode.PONG: - onPongFrame(frame, callback); + onPongFrame(frame, demandingCallback); break; case OpCode.TEXT: - onTextFrame(frame, callback); + onTextFrame(frame, demandingCallback); break; case OpCode.BINARY: - onBinaryFrame(frame, callback); + onBinaryFrame(frame, demandingCallback); break; case OpCode.CONTINUATION: - onContinuationFrame(frame, callback); + onContinuationFrame(frame, demandingCallback); break; } } @@ -205,8 +257,6 @@ public class JettyWebSocketFrameHandler implements FrameHandler try { cause = convertCause(cause); - futureSession.completeExceptionally(cause); - if (errorHandle != null) errorHandle.invoke(cause); else @@ -219,7 +269,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler } catch (Throwable t) { - WebSocketException wsError = new WebSocketException(endpointInstance.getClass().getName() + " ERROR method error: " + cause.getMessage(), t); + WebSocketException wsError = new WebSocketException(endpointInstance.getClass().getSimpleName() + " ERROR method error: " + cause.getMessage(), t); wsError.addSuppressed(cause); callback.failed(wsError); } @@ -228,7 +278,18 @@ public class JettyWebSocketFrameHandler implements FrameHandler @Override public void onClosed(CloseStatus closeStatus, Callback callback) { - callback.succeeded(); + try + { + if (closeHandle != null) + closeHandle.invoke(closeStatus.getCode(), closeStatus.getReason()); + + callback.succeeded(); + } + catch (Throwable cause) + { + callback.failed(new WebSocketException(endpointInstance.getClass().getSimpleName() + " CLOSE method error: " + cause.getMessage(), cause)); + } + container.notifySessionListeners((listener) -> listener.onWebSocketSessionClosed(session)); } @@ -262,18 +323,6 @@ public class JettyWebSocketFrameHandler implements FrameHandler private void onCloseFrame(Frame frame, Callback callback) { - if (closeHandle != null) - { - try - { - CloseStatus close = new CloseStatus(frame.getPayload()); - closeHandle.invoke(close.getCode(), close.getReason()); - } - catch (Throwable cause) - { - throw new WebSocketException(endpointInstance.getClass().getName() + " CLOSE method error: " + cause.getMessage(), cause); - } - } callback.succeeded(); } @@ -296,7 +345,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler } catch (Throwable cause) { - throw new WebSocketException(endpointInstance.getClass().getName() + " PING method error: " + cause.getMessage(), cause); + throw new WebSocketException(endpointInstance.getClass().getSimpleName() + " PING method error: " + cause.getMessage(), cause); } } else @@ -324,7 +373,7 @@ public class JettyWebSocketFrameHandler implements FrameHandler } catch (Throwable cause) { - throw new WebSocketException(endpointInstance.getClass().getName() + " PONG method error: " + cause.getMessage(), cause); + throw new WebSocketException(endpointInstance.getClass().getSimpleName() + " PONG method error: " + cause.getMessage(), cause); } } callback.succeeded(); @@ -338,6 +387,92 @@ public class JettyWebSocketFrameHandler implements FrameHandler acceptMessage(frame, callback); } + @Override + public boolean isDemanding() + { + return true; + } + + public void suspend() + { + synchronized (this) + { + switch(state) + { + case DEMANDING: + state = SuspendState.SUSPENDING; + break; + + case SUSPENDED: + case SUSPENDING: + throw new IllegalStateException("Already Suspended"); + + default: + throw new IllegalStateException(); + } + } + } + + public void resume() + { + Runnable delayedFrame = null; + synchronized (this) + { + switch(state) + { + case DEMANDING: + throw new IllegalStateException("Already Resumed"); + + case SUSPENDED: + delayedFrame = delayedOnFrame; + delayedOnFrame = null; + state = SuspendState.DEMANDING; + break; + + case SUSPENDING: + if (delayedOnFrame != null) + throw new IllegalStateException(); + state = SuspendState.DEMANDING; + break; + + default: + throw new IllegalStateException(); + } + } + + if (delayedFrame != null) + delayedFrame.run(); + else + session.getCoreSession().demand(1); + } + + private void demand() + { + boolean demand = false; + synchronized (this) + { + switch(state) + { + case DEMANDING: + demand = true; + break; + + case SUSPENDED: + throw new IllegalStateException("Suspended"); + + case SUSPENDING: + state = SuspendState.SUSPENDED; + break; + + default: + throw new IllegalStateException(); + } + } + + if (demand) + session.getCoreSession().demand(1); + } + static Throwable convertCause(Throwable cause) { if (cause instanceof MessageTooLargeException) @@ -363,5 +498,4 @@ public class JettyWebSocketFrameHandler implements FrameHandler return cause; } - } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerFactory.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerFactory.java index 4a25d3b499e..2522c0ccf10 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerFactory.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerFactory.java @@ -31,15 +31,14 @@ import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.time.Duration; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.InvalidWebSocketException; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.UpgradeRequest; -import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.api.WebSocketConnectionListener; import org.eclipse.jetty.websocket.api.WebSocketFrameListener; import org.eclipse.jetty.websocket.api.WebSocketListener; @@ -117,8 +116,7 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle throw new InvalidWebSocketException("Unrecognized WebSocket endpoint: " + endpointClass.getName()); } - public JettyWebSocketFrameHandler newJettyFrameHandler(Object endpointInstance, UpgradeRequest upgradeRequest, UpgradeResponse upgradeResponse, - CompletableFuture futureSession) + public JettyWebSocketFrameHandler newJettyFrameHandler(Object endpointInstance) { JettyWebSocketFrameHandlerMetadata metadata = getMetadata(endpointInstance.getClass()); @@ -132,6 +130,7 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle MethodHandle frameHandle = metadata.getFrameHandle(); MethodHandle pingHandle = metadata.getPingHandle(); MethodHandle pongHandle = metadata.getPongHandle(); + BatchMode batchMode = metadata.getBatchMode(); openHandle = bindTo(openHandle, endpointInstance); closeHandle = bindTo(closeHandle, endpointInstance); @@ -142,25 +141,20 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle pingHandle = bindTo(pingHandle, endpointInstance); pongHandle = bindTo(pongHandle, endpointInstance); - CompletableFuture future = futureSession; - if (future == null) - future = new CompletableFuture<>(); - JettyWebSocketFrameHandler frameHandler = new JettyWebSocketFrameHandler( container, endpointInstance, - upgradeRequest, upgradeResponse, openHandle, closeHandle, errorHandle, textHandle, binaryHandle, textSinkClass, binarySinkClass, frameHandle, pingHandle, pongHandle, - future, + batchMode, metadata); return frameHandler; } - public static MessageSink createMessageSink(MethodHandle msgHandle, Class sinkClass, Executor executor, long maxMessageSize) + public static MessageSink createMessageSink(MethodHandle msgHandle, Class sinkClass, Executor executor, WebSocketSession session) { if (msgHandle == null) return null; @@ -171,8 +165,8 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle { try { - Constructor sinkConstructor = sinkClass.getConstructor(Executor.class, MethodHandle.class, Long.TYPE); - MessageSink messageSink = (MessageSink)sinkConstructor.newInstance(executor, msgHandle, maxMessageSize); + Constructor sinkConstructor = sinkClass.getConstructor(Executor.class, MethodHandle.class, Session.class); + MessageSink messageSink = (MessageSink)sinkConstructor.newInstance(executor, msgHandle, session); return messageSink; } catch (NoSuchMethodException e) @@ -280,7 +274,7 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle // Frame Listener if (WebSocketFrameListener.class.isAssignableFrom(endpointClass)) { - Method frameMethod = ReflectUtils.findMethod(endpointClass, "onWebSocketFrame", org.eclipse.jetty.websocket.api.extensions.Frame.class); + Method frameMethod = ReflectUtils.findMethod(endpointClass, "onWebSocketFrame", Frame.class); MethodHandle frame = toMethodHandle(lookup, frameMethod); metadata.setFrameHandler(frame, frameMethod); } @@ -292,14 +286,20 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle { JettyWebSocketFrameHandlerMetadata metadata = new JettyWebSocketFrameHandlerMetadata(); - if (anno.inputBufferSize()>=0) - metadata.setInputBufferSize(anno.inputBufferSize()); - if (anno.maxBinaryMessageSize()>=0) - metadata.setMaxBinaryMessageSize(anno.maxBinaryMessageSize()); - if (anno.maxTextMessageSize()>=0) - metadata.setMaxTextMessageSize(anno.maxTextMessageSize()); - if (anno.maxIdleTime()>=0) - metadata.setIdleTimeout(Duration.ofMillis(anno.maxIdleTime())); + int max = anno.inputBufferSize(); + if (max>=0) + metadata.setInputBufferSize(max); + max = anno.maxBinaryMessageSize(); + if (max>=0) + metadata.setMaxBinaryMessageSize(max); + max = anno.maxTextMessageSize(); + if (max>=0) + metadata.setMaxTextMessageSize(max); + max = anno.idleTimeout(); + if (max<0) + max = anno.maxIdleTime(); + if (max>=0) + metadata.setIdleTimeout(Duration.ofMillis(max)); metadata.setBatchMode(anno.batchMode()); Method onmethod; @@ -349,7 +349,7 @@ public class JettyWebSocketFrameHandlerFactory extends ContainerLifeCycle { assertSignatureValid(endpointClass, onmethod, OnWebSocketFrame.class); final InvokerUtils.Arg SESSION = new InvokerUtils.Arg(Session.class); - final InvokerUtils.Arg FRAME = new InvokerUtils.Arg(org.eclipse.jetty.websocket.api.extensions.Frame.class).required(); + final InvokerUtils.Arg FRAME = new InvokerUtils.Arg(Frame.class).required(); MethodHandle methodHandle = InvokerUtils.mutatedInvoker(endpointClass, onmethod, SESSION, FRAME); metadata.setFrameHandler(methodHandle, onmethod); } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerMetadata.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerMetadata.java index a04a457cb3e..a99c2877d7a 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerMetadata.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerMetadata.java @@ -18,13 +18,12 @@ package org.eclipse.jetty.websocket.common; +import java.lang.invoke.MethodHandle; + import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.InvalidWebSocketException; import org.eclipse.jetty.websocket.core.FrameHandler; -import java.lang.invoke.MethodHandle; -import java.time.Duration; - public class JettyWebSocketFrameHandlerMetadata extends FrameHandler.ConfigurationCustomizer { private MethodHandle openHandle; @@ -42,7 +41,6 @@ public class JettyWebSocketFrameHandlerMetadata extends FrameHandler.Configurati private MethodHandle pongHandle; // Batch Configuration - // TODO remove? private BatchMode batchMode = BatchMode.OFF; public void setBatchMode(BatchMode batchMode) diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketRemoteEndpoint.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketRemoteEndpoint.java index b33b53d9a53..ab96f43bcc7 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketRemoteEndpoint.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/JettyWebSocketRemoteEndpoint.java @@ -18,6 +18,13 @@ package org.eclipse.jetty.websocket.common; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.Future; + import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.FutureCallback; @@ -29,12 +36,6 @@ import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.ProtocolException; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.util.Objects; -import java.util.concurrent.Future; - import static java.nio.charset.StandardCharsets.UTF_8; public class JettyWebSocketRemoteEndpoint implements org.eclipse.jetty.websocket.api.RemoteEndpoint @@ -44,9 +45,10 @@ public class JettyWebSocketRemoteEndpoint implements org.eclipse.jetty.websocket private final SharedBlockingCallback blocker = new SharedBlockingCallback(); private BatchMode batchMode; - public JettyWebSocketRemoteEndpoint(FrameHandler.CoreSession coreSession) + public JettyWebSocketRemoteEndpoint(FrameHandler.CoreSession coreSession, BatchMode batchMode) { this.coreSession = Objects.requireNonNull(coreSession); + this.batchMode = batchMode; } /** @@ -85,26 +87,30 @@ public class JettyWebSocketRemoteEndpoint implements org.eclipse.jetty.websocket } } - protected FrameHandler.CoreSession getCoreSession() - { - return coreSession; - } - - private void sendBlocking(Frame frame) throws IOException - { - try (SharedBlockingCallback.Blocker b = blocker.acquire()) - { - coreSession.sendFrame(frame, b, false); - b.block(); - } - } - @Override public void sendBytes(ByteBuffer data) throws IOException { sendBlocking(new Frame(OpCode.BINARY).setPayload(data)); } + @Override + public void sendBytes(ByteBuffer data, WriteCallback callback) + { + coreSession.sendFrame(new Frame(OpCode.BINARY).setPayload(data), + Callback.from(callback::writeSuccess, callback::writeFailed), + isBatch()); + } + + @Override + public Future sendBytesByFuture(ByteBuffer data) + { + FutureCallback callback = new FutureCallback(); + coreSession.sendFrame(new Frame(OpCode.BINARY).setPayload(data), + callback, + isBatch()); + return callback; + } + @Override public void sendPartialBytes(ByteBuffer fragment, boolean isLast) throws IOException { @@ -115,6 +121,43 @@ public class JettyWebSocketRemoteEndpoint implements org.eclipse.jetty.websocket } } + @Override + public void sendPartialBytes(ByteBuffer fragment, boolean isLast, WriteCallback callback) + { + sendPartialBytes(fragment, isLast, Callback.from(callback::writeSuccess, callback::writeFailed)); + } + + @Override + public Future sendPartialBytesByFuture(ByteBuffer fragment, boolean isLast) + { + FutureCallback callback = new FutureCallback(); + sendPartialBytes(fragment, isLast, callback); + return callback; + } + + @Override + public void sendString(String text) throws IOException + { + sendBlocking(new Frame(OpCode.TEXT).setPayload(text)); + } + + @Override + public void sendString(String text, WriteCallback callback) + { + Callback cb = callback == null?Callback.NOOP:Callback.from(callback::writeSuccess, callback::writeFailed); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(text), cb, isBatch()); + } + + @Override + public Future sendStringByFuture(String text) + { + FutureCallback callback = new FutureCallback(); + coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(text), + callback, + isBatch()); + return callback; + } + @Override public void sendPartialString(String fragment, boolean isLast) throws IOException { @@ -125,39 +168,59 @@ public class JettyWebSocketRemoteEndpoint implements org.eclipse.jetty.websocket } } + @Override + public void sendPartialString(String fragment, boolean isLast, WriteCallback callback) throws IOException + { + sendPartialText(fragment, isLast, Callback.from(callback::writeSuccess, callback::writeFailed)); + } + + @Override + public Future sendPartialStringByFuture(String fragment, boolean isLast) throws IOException + { + FutureCallback callback = new FutureCallback(); + sendPartialText(fragment, isLast, callback); + return callback; + } + @Override public void sendPing(ByteBuffer applicationData) throws IOException { - coreSession.sendFrame(new Frame(OpCode.PING).setPayload(applicationData), Callback.NOOP, false); + sendBlocking(new Frame(OpCode.PING).setPayload(applicationData)); + } + + @Override + public void sendPing(ByteBuffer applicationData, WriteCallback callback) + { + coreSession.sendFrame(new Frame(OpCode.PING).setPayload(applicationData), + Callback.from(callback::writeSuccess, callback::writeFailed), false); + } + + @Override + public Future sendPingByFuture(ByteBuffer applicationData) + { + FutureCallback callback = new FutureCallback(); + coreSession.sendFrame(new Frame(OpCode.PING).setPayload(applicationData), callback, false); + return callback; } @Override public void sendPong(ByteBuffer applicationData) throws IOException { - coreSession.sendFrame(new Frame(OpCode.PONG).setPayload(applicationData), Callback.NOOP, false); + sendBlocking(new Frame(OpCode.PONG).setPayload(applicationData)); } @Override - public void sendString(String text) throws IOException + public void sendPong(ByteBuffer applicationData, WriteCallback callback) { - sendBlocking(new Frame(OpCode.TEXT).setPayload(text)); + coreSession.sendFrame(new Frame(OpCode.PONG).setPayload(applicationData), + Callback.from(callback::writeSuccess, callback::writeFailed), false); } @Override - public void sendBytes(ByteBuffer data, WriteCallback callback) - { - coreSession.sendFrame(new Frame(OpCode.BINARY).setPayload(data), - Callback.from(callback::writeSuccess, callback::writeFailed), - isBatch()); - } - - @Override - public Future sendBytesByFuture(ByteBuffer data) + public Future sendPongByFuture(ByteBuffer applicationData) { FutureCallback callback = new FutureCallback(); - coreSession.sendFrame(new Frame(OpCode.BINARY).setPayload(data), - callback, - isBatch()); + coreSession.sendFrame(new Frame(OpCode.PONG).setPayload(applicationData), callback, false); return callback; } @@ -217,21 +280,18 @@ public class JettyWebSocketRemoteEndpoint implements org.eclipse.jetty.websocket } } - @Override - public void sendString(String text, WriteCallback callback) + private void sendBlocking(Frame frame) throws IOException { - Callback cb = callback == null?Callback.NOOP:Callback.from(callback::writeSuccess, callback::writeFailed); - coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(text), cb, isBatch()); + try (SharedBlockingCallback.Blocker b = blocker.acquire()) + { + coreSession.sendFrame(frame, b, false); + b.block(); + } } - @Override - public Future sendStringByFuture(String text) + protected FrameHandler.CoreSession getCoreSession() { - FutureCallback callback = new FutureCallback(); - coreSession.sendFrame(new Frame(OpCode.TEXT).setPayload(text), - callback, - isBatch()); - return callback; + return coreSession; } @Override @@ -254,12 +314,26 @@ public class JettyWebSocketRemoteEndpoint implements org.eclipse.jetty.websocket @Override public InetSocketAddress getInetSocketAddress() { + SocketAddress remoteAddress = coreSession.getRemoteAddress(); + if (remoteAddress instanceof InetSocketAddress) + return (InetSocketAddress)remoteAddress; + return null; } + @Override + public SocketAddress getRemoteAddress() + { + return coreSession.getRemoteAddress(); + } + @Override public void flush() throws IOException { - + try (SharedBlockingCallback.Blocker b = blocker.acquire()) + { + coreSession.flush(b); + b.block(); + } } } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java index 206da1b19eb..1e4589c7a1f 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java @@ -36,13 +36,13 @@ public class SessionTracker extends AbstractLifeCycle implements WebSocketSessio } @Override - public void onWebSocketSessionOpened(WebSocketSessionImpl session) + public void onWebSocketSessionOpened(WebSocketSession session) { sessions.add(session); } @Override - public void onWebSocketSessionClosed(WebSocketSessionImpl session) + public void onWebSocketSessionClosed(WebSocketSession session) { sessions.remove(session); } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionImpl.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java similarity index 90% rename from jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionImpl.java rename to jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java index 93bb017b893..686149e065d 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionImpl.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java @@ -37,26 +37,22 @@ import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.api.WebSocketBehavior; import org.eclipse.jetty.websocket.core.FrameHandler; -public class WebSocketSessionImpl extends AbstractLifeCycle implements Session, Dumpable +public class WebSocketSession extends AbstractLifeCycle implements Session, SuspendToken, Dumpable { - private static final Logger LOG = Log.getLogger(WebSocketSessionImpl.class); + private static final Logger LOG = Log.getLogger(WebSocketSession.class); private final FrameHandler.CoreSession coreSession; private final JettyWebSocketFrameHandler frameHandler; private final JettyWebSocketRemoteEndpoint remoteEndpoint; private final UpgradeRequest upgradeRequest; private final UpgradeResponse upgradeResponse; - public WebSocketSessionImpl( - FrameHandler.CoreSession coreSession, - JettyWebSocketFrameHandler frameHandler, - UpgradeRequest upgradeRequest, - UpgradeResponse upgradeResponse) + public WebSocketSession(FrameHandler.CoreSession coreSession, JettyWebSocketFrameHandler frameHandler) { - this.coreSession = Objects.requireNonNull(coreSession); this.frameHandler = Objects.requireNonNull(frameHandler); - this.remoteEndpoint = new JettyWebSocketRemoteEndpoint(coreSession); - this.upgradeRequest = upgradeRequest; - this.upgradeResponse = upgradeResponse; + this.coreSession = Objects.requireNonNull(coreSession); + this.upgradeRequest = frameHandler.getUpgradeRequest(); + this.upgradeResponse = frameHandler.getUpgradeResponse(); + this.remoteEndpoint = new JettyWebSocketRemoteEndpoint(coreSession, frameHandler.getBatchMode()); } @Override @@ -208,8 +204,14 @@ public class WebSocketSessionImpl extends AbstractLifeCycle implements Session, @Override public SuspendToken suspend() { - // TODO: - return null; + frameHandler.suspend(); + return this; + } + + @Override + public void resume() + { + frameHandler.resume(); } public FrameHandler.CoreSession getCoreSession() @@ -254,6 +256,6 @@ public class WebSocketSessionImpl extends AbstractLifeCycle implements Session, @Override public String toString() { - return String.format("WebSocketSessionImpl[%s,to=%s,%s,%s]", getBehavior(), getIdleTimeout(), coreSession, frameHandler); + return String.format("WebSocketSession[%s,to=%s,%s,%s]", getBehavior(), getIdleTimeout(), coreSession, frameHandler); } } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionListener.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionListener.java index a4d32017409..e6e2a4e9526 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionListener.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionListener.java @@ -23,7 +23,7 @@ package org.eclipse.jetty.websocket.common; */ public interface WebSocketSessionListener { - void onWebSocketSessionOpened(WebSocketSessionImpl session); + void onWebSocketSessionOpened(WebSocketSession session); - void onWebSocketSessionClosed(WebSocketSessionImpl session); + void onWebSocketSessionClosed(WebSocketSession session); } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/ByteArrayMessageSink.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/ByteArrayMessageSink.java index 3f7e5831354..55801292edc 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/ByteArrayMessageSink.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/ByteArrayMessageSink.java @@ -18,13 +18,6 @@ package org.eclipse.jetty.websocket.common.message; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.websocket.common.AbstractMessageSink; -import org.eclipse.jetty.websocket.common.invoke.InvalidSignatureException; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.MessageTooLargeException; - import java.io.ByteArrayOutputStream; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; @@ -32,18 +25,26 @@ import java.nio.ByteBuffer; import java.util.Objects; import java.util.concurrent.Executor; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.common.AbstractMessageSink; +import org.eclipse.jetty.websocket.common.invoke.InvalidSignatureException; +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.MessageTooLargeException; + public class ByteArrayMessageSink extends AbstractMessageSink { private static final byte EMPTY_BUFFER[] = new byte[0]; private static final int BUFFER_SIZE = 65535; - private final long maxMessageSize; + private final Session session; private ByteArrayOutputStream out; private int size; - public ByteArrayMessageSink(Executor executor, MethodHandle methodHandle, long maxMessageSize) + public ByteArrayMessageSink(Executor executor, MethodHandle methodHandle, Session session) { super(executor, methodHandle); - this.maxMessageSize = maxMessageSize; + this.session = session; Objects.requireNonNull(methodHandle, "MethodHandle"); // byte[] buf, int offset, int length @@ -63,10 +64,10 @@ public class ByteArrayMessageSink extends AbstractMessageSink if (frame.hasPayload()) { ByteBuffer payload = frame.getPayload(); - int nextSize = size + payload.remaining(); + size = size + payload.remaining(); + long maxMessageSize = session.getMaxBinaryMessageSize(); if (maxMessageSize > 0 && size > maxMessageSize) throw new MessageTooLargeException("Message size [" + size + "] exceeds maximum size [" + maxMessageSize + "]"); - size = nextSize; if (out == null) out = new ByteArrayOutputStream(BUFFER_SIZE); diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/ByteBufferMessageSink.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/ByteBufferMessageSink.java index 5d45adbb378..d40d7ea1e0a 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/ByteBufferMessageSink.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/ByteBufferMessageSink.java @@ -18,13 +18,6 @@ package org.eclipse.jetty.websocket.common.message; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.websocket.common.AbstractMessageSink; -import org.eclipse.jetty.websocket.common.invoke.InvalidSignatureException; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.MessageTooLargeException; - import java.io.ByteArrayOutputStream; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; @@ -32,17 +25,25 @@ import java.nio.ByteBuffer; import java.util.Objects; import java.util.concurrent.Executor; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.common.AbstractMessageSink; +import org.eclipse.jetty.websocket.common.invoke.InvalidSignatureException; +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.MessageTooLargeException; + public class ByteBufferMessageSink extends AbstractMessageSink { private static final int BUFFER_SIZE = 65535; - private final long maxMessageSize; + private final Session session; private ByteArrayOutputStream out; private int size; - public ByteBufferMessageSink(Executor executor, MethodHandle methodHandle, long maxMessageSize) + public ByteBufferMessageSink(Executor executor, MethodHandle methodHandle, Session session) { super(executor, methodHandle); - this.maxMessageSize = maxMessageSize; + this.session = session; // Validate onMessageMethod Objects.requireNonNull(methodHandle, "MethodHandle"); @@ -62,10 +63,10 @@ public class ByteBufferMessageSink extends AbstractMessageSink if (frame.hasPayload()) { ByteBuffer payload = frame.getPayload(); - int nextSize = size + payload.remaining(); + size = size + payload.remaining(); + long maxMessageSize = session.getMaxBinaryMessageSize(); if (maxMessageSize > 0 && size > maxMessageSize) throw new MessageTooLargeException("Message size [" + size + "] exceeds maximum size [" + maxMessageSize + "]"); - size = nextSize; if (out == null) out = new ByteArrayOutputStream(BUFFER_SIZE); diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageOutputStream.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageOutputStream.java index 447e4673eb2..39518b7535c 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageOutputStream.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageOutputStream.java @@ -18,6 +18,10 @@ package org.eclipse.jetty.websocket.common.message; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -28,10 +32,6 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - /** * Support for writing a single WebSocket BINARY message via a {@link OutputStream} */ @@ -39,7 +39,7 @@ public class MessageOutputStream extends OutputStream { private static final Logger LOG = Log.getLogger(MessageOutputStream.class); - private final FrameHandler.CoreSession channel; + private final FrameHandler.CoreSession coreSession; private final ByteBufferPool bufferPool; private final SharedBlockingCallback blocker; private long frameCount; @@ -49,9 +49,9 @@ public class MessageOutputStream extends OutputStream private Callback callback; private boolean closed; - public MessageOutputStream(FrameHandler.CoreSession channel, int bufferSize, ByteBufferPool bufferPool) + public MessageOutputStream(FrameHandler.CoreSession coreSession, int bufferSize, ByteBufferPool bufferPool) { - this.channel = channel; + this.coreSession = coreSession; this.bufferPool = bufferPool; this.blocker = new SharedBlockingCallback(); this.buffer = bufferPool.acquire(bufferSize, true); @@ -138,7 +138,7 @@ public class MessageOutputStream extends OutputStream frame.setFin(fin); try (SharedBlockingCallback.Blocker b = blocker.acquire()) { - channel.sendFrame(frame, b, false); + coreSession.sendFrame(frame, b, false); b.block(); assert buffer.remaining() == 0; } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageWriter.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageWriter.java index f779756fd1d..331ff2ae556 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageWriter.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageWriter.java @@ -18,6 +18,13 @@ package org.eclipse.jetty.websocket.common.message; +import java.io.IOException; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; + import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.SharedBlockingCallback; @@ -27,13 +34,6 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; -import java.io.IOException; -import java.io.Writer; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CodingErrorAction; - import static java.nio.charset.StandardCharsets.UTF_8; /** @@ -49,7 +49,7 @@ public class MessageWriter extends Writer .onUnmappableCharacter(CodingErrorAction.REPORT) .onMalformedInput(CodingErrorAction.REPORT); - private final FrameHandler.CoreSession channel; + private final FrameHandler.CoreSession coreSession; private final SharedBlockingCallback blocker; private long frameCount; private Frame frame; @@ -57,9 +57,9 @@ public class MessageWriter extends Writer private Callback callback; private boolean closed; - public MessageWriter(FrameHandler.CoreSession channel, int bufferSize) + public MessageWriter(FrameHandler.CoreSession coreSession, int bufferSize) { - this.channel = channel; + this.coreSession = coreSession; this.blocker = new SharedBlockingCallback(); this.buffer = CharBuffer.allocate(bufferSize); this.frame = new Frame(OpCode.TEXT); @@ -149,7 +149,7 @@ public class MessageWriter extends Writer try (SharedBlockingCallback.Blocker b = blocker.acquire()) { - channel.sendFrame(frame, b, false); + coreSession.sendFrame(frame, b, false); b.block(); } diff --git a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/StringMessageSink.java b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/StringMessageSink.java index 9a205ade846..8d2780bd5d2 100644 --- a/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/StringMessageSink.java +++ b/jetty-websocket/jetty-websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/StringMessageSink.java @@ -18,33 +18,34 @@ package org.eclipse.jetty.websocket.common.message; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.Utf8StringBuilder; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.websocket.common.AbstractMessageSink; -import org.eclipse.jetty.websocket.common.invoke.InvalidSignatureException; -import org.eclipse.jetty.websocket.core.Frame; -import org.eclipse.jetty.websocket.core.MessageTooLargeException; - import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; import java.nio.ByteBuffer; import java.util.Objects; import java.util.concurrent.Executor; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Utf8StringBuilder; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.common.AbstractMessageSink; +import org.eclipse.jetty.websocket.common.invoke.InvalidSignatureException; +import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.MessageTooLargeException; + public class StringMessageSink extends AbstractMessageSink { private static final Logger LOG = Log.getLogger(StringMessageSink.class); - private final long maxMessageSize; + private final Session session; private Utf8StringBuilder utf; private int size = 0; - public StringMessageSink(Executor executor, MethodHandle methodHandle, long maxMessageSize) + public StringMessageSink(Executor executor, MethodHandle methodHandle, Session session) { super(executor, methodHandle); - this.maxMessageSize = maxMessageSize; + this.session = session; // Validate onMessageMethod Objects.requireNonNull(methodHandle, "MethodHandle"); @@ -66,10 +67,10 @@ public class StringMessageSink extends AbstractMessageSink if (frame.hasPayload()) { ByteBuffer payload = frame.getPayload(); - int nextSize = size + payload.remaining(); + size = size + payload.remaining(); + long maxMessageSize = session.getMaxTextMessageSize(); if (maxMessageSize > 0 && size > maxMessageSize) throw new MessageTooLargeException("Message size [" + size + "] exceeds maximum size [" + maxMessageSize + "]"); - size = nextSize; if (utf == null) utf = new Utf8StringBuilder(1024); diff --git a/jetty-websocket/jetty-websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.ExtensionConfig$Parser b/jetty-websocket/jetty-websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.ExtensionConfig$Parser new file mode 100644 index 00000000000..7073c007f4b --- /dev/null +++ b/jetty-websocket/jetty-websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.ExtensionConfig$Parser @@ -0,0 +1 @@ +org.eclipse.jetty.websocket.common.ExtensionConfigParser \ No newline at end of file diff --git a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/EventQueue.java b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/EventQueue.java index ec2423b3a96..b50f0154c5d 100644 --- a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/EventQueue.java +++ b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/EventQueue.java @@ -19,10 +19,12 @@ package org.eclipse.jetty.websocket.common; import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.concurrent.LinkedBlockingDeque; -import static org.eclipse.jetty.toolchain.test.matchers.RegexMatcher.matchesPattern; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.matchesRegex; +import static org.junit.jupiter.api.Assertions.fail; public class EventQueue extends LinkedBlockingDeque { @@ -33,10 +35,18 @@ public class EventQueue extends LinkedBlockingDeque public void assertEvents(String... regexEvents) { - Iterator capturedIterator = iterator(); - for (int i = 0; i < regexEvents.length; i++) + int i = 0; + try { - assertThat("Event [" + i + "]", capturedIterator.next(), matchesPattern(regexEvents[i])); + Iterator capturedIterator = iterator(); + for (i = 0; i < regexEvents.length; i++) + { + assertThat("Event [" + i + "]", capturedIterator.next(), matchesRegex(regexEvents[i])); + } + } + catch (NoSuchElementException e) + { + fail("Event [" + (i) + "] not found: " + regexEvents[i]); } } } diff --git a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerTest.java b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerTest.java index eaf3a37fbf5..6ac2ec2d348 100644 --- a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerTest.java +++ b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/JettyWebSocketFrameHandlerTest.java @@ -29,8 +29,6 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.StatusCode; -import org.eclipse.jetty.websocket.api.UpgradeRequest; -import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.api.WebSocketConnectionListener; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; @@ -38,8 +36,6 @@ import org.eclipse.jetty.websocket.common.endpoints.listeners.ListenerBasicSocke import org.eclipse.jetty.websocket.common.endpoints.listeners.ListenerFrameSocket; import org.eclipse.jetty.websocket.common.endpoints.listeners.ListenerPartialSocket; import org.eclipse.jetty.websocket.common.endpoints.listeners.ListenerPingPongSocket; -import org.eclipse.jetty.websocket.common.handshake.DummyUpgradeRequest; -import org.eclipse.jetty.websocket.common.handshake.DummyUpgradeResponse; import org.eclipse.jetty.websocket.core.Behavior; import org.eclipse.jetty.websocket.core.CloseStatus; import org.eclipse.jetty.websocket.core.Frame; @@ -71,7 +67,7 @@ public class JettyWebSocketFrameHandlerTest } private JettyWebSocketFrameHandlerFactory endpointFactory = new JettyWebSocketFrameHandlerFactory(container); - private FrameHandler.CoreSession channel = new FrameHandler.CoreSession.Empty() + private FrameHandler.CoreSession coreSession = new FrameHandler.CoreSession.Empty() { @Override public Behavior getBehavior() @@ -82,10 +78,7 @@ public class JettyWebSocketFrameHandlerTest private JettyWebSocketFrameHandler newLocalFrameHandler(Object wsEndpoint) { - UpgradeRequest upgradeRequest = new DummyUpgradeRequest(); - UpgradeResponse upgradeResponse = new DummyUpgradeResponse(); - JettyWebSocketFrameHandler localEndpoint = endpointFactory.newJettyFrameHandler(wsEndpoint, - upgradeRequest, upgradeResponse, null); + JettyWebSocketFrameHandler localEndpoint = endpointFactory.newJettyFrameHandler(wsEndpoint); return localEndpoint; } @@ -119,9 +112,9 @@ public class JettyWebSocketFrameHandlerTest JettyWebSocketFrameHandler localEndpoint = newLocalFrameHandler(socket); // Trigger Events - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload("Hello?").setFin(true), Callback.NOOP); - localEndpoint.onFrame(CloseStatus.toFrame(StatusCode.NORMAL, "Normal"), Callback.NOOP); + localEndpoint.onClosed(new CloseStatus(StatusCode.NORMAL, "Normal"), Callback.NOOP); // Validate Events socket.events.assertEvents( @@ -163,7 +156,7 @@ public class JettyWebSocketFrameHandlerTest JettyWebSocketFrameHandler localEndpoint = newLocalFrameHandler(socket); // Trigger Events - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload("Hello Text Stream").setFin(true), Callback.NOOP); localEndpoint.onFrame(CloseStatus.toFrame(StatusCode.NORMAL, "Normal"), Callback.NOOP); @@ -185,7 +178,7 @@ public class JettyWebSocketFrameHandlerTest JettyWebSocketFrameHandler localEndpoint = newLocalFrameHandler(socket); // Trigger Events - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload("Hel").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload("lo ").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload("Wor").setFin(false), Callback.NOOP); @@ -208,7 +201,7 @@ public class JettyWebSocketFrameHandlerTest JettyWebSocketFrameHandler localEndpoint = newLocalFrameHandler(socket); // Trigger Events - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload("Hello").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload(" ").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload("World").setFin(true), Callback.NOOP); @@ -216,6 +209,7 @@ public class JettyWebSocketFrameHandlerTest localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload(" the ").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload("Pig").setFin(true), Callback.NOOP); localEndpoint.onFrame(CloseStatus.toFrame(StatusCode.NORMAL), Callback.NOOP); + localEndpoint.onClosed(CloseStatus.NORMAL_STATUS, Callback.NOOP); // Validate Events socket.events.assertEvents( @@ -238,14 +232,14 @@ public class JettyWebSocketFrameHandlerTest JettyWebSocketFrameHandler localEndpoint = newLocalFrameHandler(socket); // Trigger Events - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload("Hello").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload(" ").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload("World").setFin(true), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.BINARY).setPayload("Save").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload(" the ").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload("Pig").setFin(true), Callback.NOOP); - localEndpoint.onFrame(CloseStatus.toFrame(StatusCode.NORMAL, "Normal"), Callback.NOOP); + localEndpoint.onClosed(new CloseStatus(StatusCode.NORMAL, "Normal"), Callback.NOOP); // Validate Events socket.events.assertEvents( @@ -264,7 +258,7 @@ public class JettyWebSocketFrameHandlerTest JettyWebSocketFrameHandler localEndpoint = newLocalFrameHandler(socket); // Trigger Events - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload("Hello").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload(" ").setFin(false), Callback.NOOP); localEndpoint.onError(new RuntimeException("Nothing to see here"), Callback.NOOP); @@ -284,7 +278,7 @@ public class JettyWebSocketFrameHandlerTest JettyWebSocketFrameHandler localEndpoint = newLocalFrameHandler(socket); // Trigger Events - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload("Hello").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload(" ").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload("World").setFin(true), Callback.NOOP); @@ -314,7 +308,7 @@ public class JettyWebSocketFrameHandlerTest JettyWebSocketFrameHandler localEndpoint = newLocalFrameHandler(socket); // Trigger Events - localEndpoint.onOpen(channel, Callback.NOOP); + localEndpoint.onOpen(coreSession, Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.TEXT).setPayload("Hello").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload(" ").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.PING).setPayload("You there?"), Callback.NOOP); @@ -323,7 +317,7 @@ public class JettyWebSocketFrameHandlerTest localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload(" the ").setFin(false), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.PONG).setPayload("You there?"), Callback.NOOP); localEndpoint.onFrame(new Frame(OpCode.CONTINUATION).setPayload("Pig").setFin(true), Callback.NOOP); - localEndpoint.onFrame(CloseStatus.toFrame(StatusCode.NORMAL, "Normal"), Callback.NOOP); + localEndpoint.onClosed(new CloseStatus(StatusCode.NORMAL, "Normal"), Callback.NOOP); // Validate Events socket.events.assertEvents( diff --git a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/LocalEndpointMetadataTest.java b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/LocalEndpointMetadataTest.java index 9fdf87adccd..58c6a09f2d3 100644 --- a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/LocalEndpointMetadataTest.java +++ b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/LocalEndpointMetadataTest.java @@ -43,12 +43,12 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.allOf; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertThrows; public class LocalEndpointMetadataTest diff --git a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/MessageOutputStreamTest.java b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/MessageOutputStreamTest.java index 9d33618b77f..50e8eacb484 100644 --- a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/MessageOutputStreamTest.java +++ b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/MessageOutputStreamTest.java @@ -47,26 +47,26 @@ public class MessageOutputStreamTest bufferPool.assertNoLeaks(); } - private OutgoingMessageCapture channelCapture; + private OutgoingMessageCapture sessionCapture; @BeforeEach public void setupTest() throws Exception { - channelCapture = new OutgoingMessageCapture(); + sessionCapture = new OutgoingMessageCapture(); } @Test public void testMultipleWrites() throws Exception { - try (MessageOutputStream stream = new MessageOutputStream(channelCapture, OUTPUT_BUFFER_SIZE, bufferPool)) + try (MessageOutputStream stream = new MessageOutputStream(sessionCapture, OUTPUT_BUFFER_SIZE, bufferPool)) { stream.write("Hello".getBytes("UTF-8")); stream.write(" ".getBytes("UTF-8")); stream.write("World".getBytes("UTF-8")); } - assertThat("Socket.binaryMessages.size", channelCapture.binaryMessages.size(), is(1)); - ByteBuffer buffer = channelCapture.binaryMessages.poll(1, TimeUnit.SECONDS); + assertThat("Socket.binaryMessages.size", sessionCapture.binaryMessages.size(), is(1)); + ByteBuffer buffer = sessionCapture.binaryMessages.poll(1, TimeUnit.SECONDS); String message = BufferUtil.toUTF8String(buffer); assertThat("Message", message, is("Hello World")); } @@ -74,13 +74,13 @@ public class MessageOutputStreamTest @Test public void testSingleWrite() throws Exception { - try (MessageOutputStream stream = new MessageOutputStream(channelCapture, OUTPUT_BUFFER_SIZE, bufferPool)) + try (MessageOutputStream stream = new MessageOutputStream(sessionCapture, OUTPUT_BUFFER_SIZE, bufferPool)) { stream.write("Hello World".getBytes("UTF-8")); } - assertThat("Socket.binaryMessages.size", channelCapture.binaryMessages.size(), is(1)); - ByteBuffer buffer = channelCapture.binaryMessages.poll(1, TimeUnit.SECONDS); + assertThat("Socket.binaryMessages.size", sessionCapture.binaryMessages.size(), is(1)); + ByteBuffer buffer = sessionCapture.binaryMessages.poll(1, TimeUnit.SECONDS); String message = BufferUtil.toUTF8String(buffer); assertThat("Message", message, is("Hello World")); } @@ -94,13 +94,13 @@ public class MessageOutputStreamTest Arrays.fill(buf, (byte)'x'); buf[bufsize - 1] = (byte)'o'; // mark last entry for debugging - try (MessageOutputStream stream = new MessageOutputStream(channelCapture, OUTPUT_BUFFER_SIZE, bufferPool)) + try (MessageOutputStream stream = new MessageOutputStream(sessionCapture, OUTPUT_BUFFER_SIZE, bufferPool)) { stream.write(buf); } - assertThat("Socket.binaryMessages.size", channelCapture.binaryMessages.size(), is(1)); - ByteBuffer buffer = channelCapture.binaryMessages.poll(1, TimeUnit.SECONDS); + assertThat("Socket.binaryMessages.size", sessionCapture.binaryMessages.size(), is(1)); + ByteBuffer buffer = sessionCapture.binaryMessages.poll(1, TimeUnit.SECONDS); String message = BufferUtil.toUTF8String(buffer); assertThat("Message", message, endsWith("xxxxxo")); } diff --git a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/OutgoingMessageCapture.java b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/OutgoingMessageCapture.java index f3ae09e3abd..5955296793f 100644 --- a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/OutgoingMessageCapture.java +++ b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/OutgoingMessageCapture.java @@ -21,7 +21,9 @@ package org.eclipse.jetty.websocket.common; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.net.SocketAddress; import java.nio.ByteBuffer; +import java.time.Duration; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; @@ -29,6 +31,12 @@ import org.eclipse.jetty.toolchain.test.Hex; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.SuspendToken; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.eclipse.jetty.websocket.api.UpgradeResponse; +import org.eclipse.jetty.websocket.api.WebSocketBehavior; import org.eclipse.jetty.websocket.common.message.ByteBufferMessageSink; import org.eclipse.jetty.websocket.common.message.StringMessageSink; import org.eclipse.jetty.websocket.core.CloseStatus; @@ -98,7 +106,7 @@ public class OutgoingMessageCapture extends FrameHandler.CoreSession.Empty imple String event = String.format("TEXT:fin=%b:len=%d", frame.isFin(), frame.getPayloadLength()); LOG.debug(event); events.offer(event); - messageSink = new StringMessageSink(null, wholeTextHandle, maxMessageSize); + messageSink = new StringMessageSink(null, wholeTextHandle, getFakeSession()); } break; case OpCode.BINARY: @@ -106,7 +114,7 @@ public class OutgoingMessageCapture extends FrameHandler.CoreSession.Empty imple String event = String.format("BINARY:fin=%b:len=%d", frame.isFin(), frame.getPayloadLength()); LOG.debug(event); events.offer(event); - messageSink = new ByteBufferMessageSink(null, wholeBinaryHandle, maxMessageSize); + messageSink = new ByteBufferMessageSink(null, wholeBinaryHandle, getFakeSession()); } break; case OpCode.CONTINUATION: @@ -172,4 +180,145 @@ public class OutgoingMessageCapture extends FrameHandler.CoreSession.Empty imple hint.append(']'); return hint.toString(); } + + private Session getFakeSession() + { + return new Session() + { + @Override + public void close() + { + } + + @Override + public void close(org.eclipse.jetty.websocket.api.CloseStatus closeStatus) + { + } + + @Override + public void close(int statusCode, String reason) + { + } + + @Override + public void disconnect() + { + } + + @Override + public SocketAddress getLocalAddress() + { + return null; + } + + @Override + public String getProtocolVersion() + { + return null; + } + + @Override + public RemoteEndpoint getRemote() + { + return null; + } + + @Override + public SocketAddress getRemoteAddress() + { + return null; + } + + @Override + public UpgradeRequest getUpgradeRequest() + { + return null; + } + + @Override + public UpgradeResponse getUpgradeResponse() + { + return null; + } + + @Override + public boolean isOpen() + { + return false; + } + + @Override + public boolean isSecure() + { + return false; + } + + @Override + public SuspendToken suspend() + { + return null; + } + + @Override + public WebSocketBehavior getBehavior() + { + return null; + } + + @Override + public Duration getIdleTimeout() + { + return null; + } + + @Override + public int getInputBufferSize() + { + return 0; + } + + @Override + public int getOutputBufferSize() + { + return 0; + } + + @Override + public long getMaxBinaryMessageSize() + { + return maxMessageSize; + } + + @Override + public long getMaxTextMessageSize() + { + return maxMessageSize; + } + + @Override + public void setIdleTimeout(Duration duration) + { + } + + @Override + public void setInputBufferSize(int size) + { + } + + @Override + public void setOutputBufferSize(int size) + { + } + + @Override + public void setMaxBinaryMessageSize(long size) + { + } + + @Override + public void setMaxTextMessageSize(long size) + { + } + }; + } } diff --git a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/endpoints/annotated/FrameSocket.java b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/endpoints/annotated/FrameSocket.java index 3d7e97ed992..e2babb5620b 100644 --- a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/endpoints/annotated/FrameSocket.java +++ b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/endpoints/annotated/FrameSocket.java @@ -18,9 +18,9 @@ package org.eclipse.jetty.websocket.common.endpoints.annotated; +import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.api.extensions.Frame; @WebSocket public class FrameSocket diff --git a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/endpoints/listeners/ListenerFrameSocket.java b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/endpoints/listeners/ListenerFrameSocket.java index 58f8141d70e..638c35cb58b 100644 --- a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/endpoints/listeners/ListenerFrameSocket.java +++ b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/endpoints/listeners/ListenerFrameSocket.java @@ -18,9 +18,9 @@ package org.eclipse.jetty.websocket.common.endpoints.listeners; +import org.eclipse.jetty.websocket.api.Frame; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WebSocketFrameListener; -import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.common.EventQueue; import org.eclipse.jetty.websocket.common.util.TextUtil; import org.eclipse.jetty.websocket.core.CloseStatus; diff --git a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/invoke/InvokerUtilsTest.java b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/invoke/InvokerUtilsTest.java index dfcff532634..c17e6dfba84 100644 --- a/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/invoke/InvokerUtilsTest.java +++ b/jetty-websocket/jetty-websocket-common/src/test/java/org/eclipse/jetty/websocket/common/invoke/InvokerUtilsTest.java @@ -18,16 +18,16 @@ package org.eclipse.jetty.websocket.common.invoke; -import org.eclipse.jetty.util.annotation.Name; -import org.eclipse.jetty.websocket.common.util.ReflectUtils; -import org.junit.jupiter.api.Test; - import java.io.File; import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; -import static org.hamcrest.CoreMatchers.is; +import org.eclipse.jetty.util.annotation.Name; +import org.eclipse.jetty.websocket.common.util.ReflectUtils; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class InvokerUtilsTest { diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerFrameHandlerFactory.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerFrameHandlerFactory.java index 8422f8e3d80..fcce48a3801 100644 --- a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerFrameHandlerFactory.java +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerFrameHandlerFactory.java @@ -18,9 +18,7 @@ package org.eclipse.jetty.websocket.server; -import java.util.concurrent.CompletableFuture; import javax.servlet.ServletContext; -import javax.servlet.ServletException; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler; @@ -28,9 +26,6 @@ import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandlerFactory; import org.eclipse.jetty.websocket.common.WebSocketContainer; import org.eclipse.jetty.websocket.core.FrameHandler; -import org.eclipse.jetty.websocket.server.internal.DelegatedJettyServletUpgradeRequest; -import org.eclipse.jetty.websocket.server.internal.JettyWebSocketServerContainer; -import org.eclipse.jetty.websocket.server.internal.UpgradeResponseAdapter; import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; @@ -39,21 +34,10 @@ public class JettyServerFrameHandlerFactory extends JettyWebSocketFrameHandlerFactory implements FrameHandlerFactory, LifeCycle.Listener { - public static JettyServerFrameHandlerFactory ensureFactory(ServletContext servletContext) - throws ServletException + public static JettyServerFrameHandlerFactory getFactory(ServletContext context) { - ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext, "Jetty Websocket"); - - JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class); - if (factory == null) - { - JettyWebSocketServerContainer container = new JettyWebSocketServerContainer(contextHandler); - servletContext.setAttribute(WebSocketContainer.class.getName(), container); - factory = new JettyServerFrameHandlerFactory(container); - contextHandler.addManaged(factory); - contextHandler.addLifeCycleListener(factory); - } - return factory; + ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context, "JettyServerFrameHandlerFactory"); + return contextHandler.getBean(JettyServerFrameHandlerFactory.class); } public JettyServerFrameHandlerFactory(WebSocketContainer container) @@ -64,8 +48,7 @@ public class JettyServerFrameHandlerFactory @Override public FrameHandler newFrameHandler(Object websocketPojo, ServletUpgradeRequest upgradeRequest, ServletUpgradeResponse upgradeResponse) { - return super.newJettyFrameHandler(websocketPojo, new DelegatedJettyServletUpgradeRequest(upgradeRequest), new UpgradeResponseAdapter(upgradeResponse), - new CompletableFuture<>()); + return super.newJettyFrameHandler(websocketPojo); } @Override diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeRequest.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeRequest.java new file mode 100644 index 00000000000..ce16747b665 --- /dev/null +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeRequest.java @@ -0,0 +1,328 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.server; + +import java.net.HttpCookie; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.eclipse.jetty.websocket.common.JettyExtensionConfig; +import org.eclipse.jetty.websocket.core.server.Negotiation; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; + +public class JettyServerUpgradeRequest +{ + private ServletUpgradeRequest upgradeRequest; + + public JettyServerUpgradeRequest(ServletUpgradeRequest request) + { + upgradeRequest = request; + } + + /** + * @return The {@link X509Certificate} instance at request attribute "javax.servlet.request.X509Certificate" or null. + */ + public X509Certificate[] getCertificates() + { + return upgradeRequest.getCertificates(); + } + + /** + * @see HttpServletRequest#getCookies() + * @return Request cookies + */ + public List getCookies() + { + return upgradeRequest.getCookies(); + } + + /** + * @return The extensions offered + * @see Negotiation#getOfferedExtensions() + */ + public List getExtensions() + { + return upgradeRequest.getExtensions().stream().map(JettyExtensionConfig::new).collect(Collectors.toList()); + } + + /** + * @param name Header name + * @return Header value or null + * @see HttpServletRequest#getHeader(String) + */ + public String getHeader(String name) + { + return upgradeRequest.getHeader(name); + } + + /** + * @param name Header name + * @return Header value as integer or -1 + * @see HttpServletRequest#getHeader(String) + */ + public int getHeaderInt(String name) + { + return upgradeRequest.getHeaderInt(name); + } + + /** + * @return Map of headers + */ + public Map> getHeadersMap() + { + return upgradeRequest.getHeadersMap(); + } + + /** + * @param name Header name + * @return List of header values or null + */ + public List getHeaders(String name) + { + return upgradeRequest.getHeaders(name); + } + + /** + * @return The requested host + * @see HttpServletRequest#getRequestURL() + */ + public String getHost() + { + return upgradeRequest.getHost(); + } + + /** + * @return Immutable version of {@link HttpServletRequest} + */ + public HttpServletRequest getHttpServletRequest() + { + return upgradeRequest.getHttpServletRequest(); + } + + /** + * @return The HTTP protocol version + * @see HttpServletRequest#getProtocol() + */ + public String getHttpVersion() + { + return upgradeRequest.getHttpVersion(); + } + + /** + * @return The requested Locale + * @see HttpServletRequest#getLocale() + */ + public Locale getLocale() + { + return upgradeRequest.getLocale(); + } + + /** + * @return The requested Locales + * @see HttpServletRequest#getLocales() + */ + public Enumeration getLocales() + { + return upgradeRequest.getLocales(); + } + + /** + * @return The local requested address, which is typically an {@link InetSocketAddress}, but may be another derivation of {@link SocketAddress} + * @see ServletRequest#getLocalAddr() + * @see ServletRequest#getLocalPort() + */ + public SocketAddress getLocalSocketAddress() + { + return upgradeRequest.getLocalSocketAddress(); + } + + /** + * @return The requested method + * @see HttpServletRequest#getMethod() + */ + public String getMethod() + { + return upgradeRequest.getMethod(); + } + + /** + * @return The origin header value + */ + public String getOrigin() + { + return upgradeRequest.getOrigin(); + } + + /** + * @return The request parameter map + * @see ServletRequest#getParameterMap() + */ + public Map> getParameterMap() + { + return upgradeRequest.getParameterMap(); + } + + /** + * @return WebSocket protocol version from "Sec-WebSocket-Version" header + */ + public String getProtocolVersion() + { + return upgradeRequest.getProtocolVersion(); + } + + /** + * @return The request query string + * @see HttpServletRequest#getQueryString() + */ + public String getQueryString() + { + return upgradeRequest.getQueryString(); + } + + /** + * @return The remote request address, which is typically an {@link InetSocketAddress}, but may be another derivation of {@link SocketAddress} + * @see ServletRequest#getRemoteAddr() + * @see ServletRequest#getRemotePort() + */ + public SocketAddress getRemoteSocketAddress() + { + return upgradeRequest.getRemoteSocketAddress(); + } + + /** + * @return The request URI path within the context + */ + public String getRequestPath() + { + return upgradeRequest.getRequestPath(); + } + + /** + * @return The request URI + * @see HttpServletRequest#getRequestURL() + */ + public URI getRequestURI() + { + return upgradeRequest.getRequestURI(); + } + + /** + * @param name Attribute name + * @return Attribute value or null + * @see ServletRequest#getAttribute(String) + */ + public Object getServletAttribute(String name) + { + return upgradeRequest.getServletAttribute(name); + } + + /** + * @return Request attribute map + */ + public Map getServletAttributes() + { + return upgradeRequest.getServletAttributes(); + } + + /** + * @return Request parameters + * @see ServletRequest#getParameterMap() + */ + public Map> getServletParameters() + { + return upgradeRequest.getServletParameters(); + } + + /** + * @return The HttpSession, which may be null or invalidated + * @see HttpServletRequest#getSession(boolean) + */ + public HttpSession getSession() + { + return upgradeRequest.getSession(); + } + + /** + * @return Get WebSocket negotiation offered sub protocols + */ + public List getSubProtocols() + { + return upgradeRequest.getSubProtocols(); + } + + /** + * @return The User's {@link Principal} or null + * @see HttpServletRequest#getUserPrincipal() + */ + public Principal getUserPrincipal() + { + return upgradeRequest.getUserPrincipal(); + } + + /** + * @param subprotocol A sub protocol name + * @return True if the sub protocol was offered + */ + public boolean hasSubProtocol(String subprotocol) + { + return upgradeRequest.hasSubProtocol(subprotocol); + } + + /** + * @return True if the request is secure + * @see ServletRequest#isSecure() + */ + public boolean isSecure() + { + return upgradeRequest.isSecure(); + } + + /** + * @param role The user role + * @return True if the requests user has the role + * @see HttpServletRequest#isUserInRole(String) + */ + public boolean isUserInRole(String role) + { + return upgradeRequest.isUserInRole(role); + } + + /** + * @param name Attribute name + * @param value Attribute value to set + * @see ServletRequest#setAttribute(String, Object) + */ + public void setServletAttribute(String name, Object value) + { + upgradeRequest.setServletAttribute(name, value); + } +} diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeResponse.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeResponse.java new file mode 100644 index 00000000000..8fad4ea6d0f --- /dev/null +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeResponse.java @@ -0,0 +1,121 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.server; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.eclipse.jetty.websocket.common.JettyExtensionConfig; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; + +public class JettyServerUpgradeResponse +{ + private ServletUpgradeResponse upgradeResponse; + + public JettyServerUpgradeResponse(ServletUpgradeResponse response) + { + upgradeResponse = response; + } + + public void addHeader(String name, String value) + { + upgradeResponse.addHeader(name, value); + } + + public void setHeader(String name, String value) + { + upgradeResponse.setHeader(name, value); + } + + public void setHeader(String name, List values) + { + upgradeResponse.setHeader(name, values); + } + + public String getAcceptedSubProtocol() + { + return upgradeResponse.getAcceptedSubProtocol(); + } + + public List getExtensions() + { + return upgradeResponse.getExtensions().stream().map(JettyExtensionConfig::new).collect(Collectors.toList()); + } + + public String getHeader(String name) + { + return upgradeResponse.getHeader(name); + } + + public Set getHeaderNames() + { + return upgradeResponse.getHeaderNames(); + } + + public Map> getHeadersMap() + { + return upgradeResponse.getHeadersMap(); + } + + public List getHeaders(String name) + { + return upgradeResponse.getHeaders(name); + } + + public int getStatusCode() + { + return upgradeResponse.getStatusCode(); + } + + public boolean isCommitted() + { + return upgradeResponse.isCommitted(); + } + + public void sendError(int statusCode, String message) throws IOException + { + upgradeResponse.sendError(statusCode, message); + } + + public void sendForbidden(String message) throws IOException + { + upgradeResponse.sendForbidden(message); + } + + public void setAcceptedSubProtocol(String protocol) + { + upgradeResponse.setAcceptedSubProtocol(protocol); + } + + public void setExtensions(List configs) + { + upgradeResponse.setExtensions(configs.stream() + .map(c->new org.eclipse.jetty.websocket.core.ExtensionConfig(c.getName(), c.getParameters())) + .collect(Collectors.toList())); + } + + public void setStatusCode(int statusCode) + { + upgradeResponse.setStatusCode(statusCode); + } +} diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketConfiguration.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketConfiguration.java index 19dfe778fa8..a28fb6e7c42 100644 --- a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketConfiguration.java +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketConfiguration.java @@ -18,6 +18,8 @@ package org.eclipse.jetty.websocket.server; +import java.util.ServiceLoader; + import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -29,8 +31,6 @@ import org.eclipse.jetty.webapp.WebAppConfiguration; import org.eclipse.jetty.webapp.WebInfConfiguration; import org.eclipse.jetty.webapp.WebXmlConfiguration; -import java.util.ServiceLoader; - /** *

    Websocket Configuration

    *

    This configuration configures the WebAppContext server/system classes to @@ -58,9 +58,7 @@ public class JettyWebSocketConfiguration extends AbstractConfiguration protectAndExpose( "org.eclipse.jetty.websocket.api.", - "org.eclipse.jetty.websocket.common.", - "org.eclipse.jetty.websocket.client.", - "org.eclipse.jetty.websocket.server."); + "org.eclipse.jetty.websocket.server."); } @Override diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketCreator.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketCreator.java new file mode 100644 index 00000000000..0f6fd8033e7 --- /dev/null +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketCreator.java @@ -0,0 +1,39 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.server; + + +/** + * Abstract WebSocket creator interface. + *

    + * Should you desire filtering of the WebSocket object creation due to criteria such as origin or sub-protocol, then you will be required to implement a custom + * WebSocketCreator implementation. + *

    + */ +public interface JettyWebSocketCreator +{ + /** + * Create a websocket from the incoming request. + * + * @param req the request details + * @param resp the response details + * @return a websocket object to use, or null if no websocket should be created from this request. + */ + Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp); +} diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java new file mode 100644 index 00000000000..710f5ce01d1 --- /dev/null +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java @@ -0,0 +1,232 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.server; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +import javax.servlet.ServletContext; + +import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketBehavior; +import org.eclipse.jetty.websocket.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.common.SessionTracker; +import org.eclipse.jetty.websocket.common.WebSocketContainer; +import org.eclipse.jetty.websocket.common.WebSocketSessionListener; +import org.eclipse.jetty.websocket.core.FrameHandler; +import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.core.WebSocketException; +import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; +import org.eclipse.jetty.websocket.servlet.WebSocketMapping; + +public class JettyWebSocketServerContainer extends ContainerLifeCycle implements WebSocketContainer, WebSocketPolicy, LifeCycle.Listener +{ + public static final String JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE = WebSocketContainer.class.getName(); + + public static JettyWebSocketServerContainer ensureContainer(ServletContext servletContext) + { + ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext, "Javax Websocket"); + if (contextHandler.getServer() == null) + throw new IllegalStateException("Server has not been set on the ServletContextHandler"); + + JettyWebSocketServerContainer container = (JettyWebSocketServerContainer)servletContext.getAttribute(JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE); + if (container == null) + { + // Find Pre-Existing executor + Executor executor = (Executor)servletContext.getAttribute("org.eclipse.jetty.server.Executor"); + if (executor == null) + executor = contextHandler.getServer().getThreadPool(); + + // Create the Jetty ServerContainer implementation + container = new JettyWebSocketServerContainer( + contextHandler, + WebSocketMapping.ensureMapping(servletContext, WebSocketMapping.DEFAULT_KEY), + WebSocketComponents.ensureWebSocketComponents(servletContext), executor); + servletContext.setAttribute(JETTY_WEBSOCKET_CONTAINER_ATTRIBUTE, container); + contextHandler.addManaged(container); + contextHandler.addLifeCycleListener(container); + } + + return container; + } + + private final static Logger LOG = Log.getLogger(JettyWebSocketServerContainer.class); + + private final WebSocketMapping webSocketMapping; + private final WebSocketComponents webSocketComponents; + private final FrameHandlerFactory frameHandlerFactory; + private final Executor executor; + private final FrameHandler.ConfigurationCustomizer customizer = new FrameHandler.ConfigurationCustomizer(); + + private final List sessionListeners = new ArrayList<>(); + private final SessionTracker sessionTracker = new SessionTracker(); + + /** + * Main entry point for {@link JettyWebSocketServletContainerInitializer}. + * + * @param webSocketMapping the {@link WebSocketMapping} that this container belongs to + * @param webSocketComponents the {@link WebSocketComponents} instance to use + * @param executor the {@link Executor} to use + */ + public JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMapping webSocketMapping, WebSocketComponents webSocketComponents, Executor executor) + { + this.webSocketMapping = webSocketMapping; + this.webSocketComponents = webSocketComponents; + this.executor = executor; + + // Ensure there is a FrameHandlerFactory + JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class); + if (factory == null) + { + factory = new JettyServerFrameHandlerFactory(this); + contextHandler.addManaged(factory); + contextHandler.addLifeCycleListener(factory); + } + frameHandlerFactory = factory; + + addSessionListener(sessionTracker); + } + + public void addMapping(String pathSpec, JettyWebSocketCreator creator) + { + PathSpec ps = WebSocketMapping.parsePathSpec(pathSpec); + if (webSocketMapping.getMapping(ps) != null) + throw new WebSocketException("Duplicate WebSocket Mapping for PathSpec"); + + webSocketMapping.addMapping(ps, + (req, resp)-> creator.createWebSocket(new JettyServerUpgradeRequest(req), new JettyServerUpgradeResponse(resp)), + frameHandlerFactory, customizer); + } + + @Override + public Executor getExecutor() + { + return this.executor; + } + + @Override + public void addSessionListener(WebSocketSessionListener listener) + { + sessionListeners.add(listener); + } + + @Override + public boolean removeSessionListener(WebSocketSessionListener listener) + { + return sessionListeners.remove(listener); + } + + @Override + public void notifySessionListeners(Consumer consumer) + { + for (WebSocketSessionListener listener : sessionListeners) + { + try + { + consumer.accept(listener); + } + catch (Throwable x) + { + LOG.info("Exception while invoking listener " + listener, x); + } + } + } + + @Override + public Collection getOpenSessions() + { + return sessionTracker.getSessions(); + } + + @Override + public WebSocketBehavior getBehavior() + { + return WebSocketBehavior.SERVER; + } + + @Override + public Duration getIdleTimeout() + { + return customizer.getIdleTimeout(); + } + + @Override + public int getInputBufferSize() + { + return customizer.getInputBufferSize(); + } + + @Override + public int getOutputBufferSize() + { + return customizer.getOutputBufferSize(); + } + + @Override + public long getMaxBinaryMessageSize() + { + return customizer.getMaxBinaryMessageSize(); + } + + @Override + public long getMaxTextMessageSize() + { + return customizer.getMaxTextMessageSize(); + } + + @Override + public void setIdleTimeout(Duration duration) + { + customizer.setIdleTimeout(duration); + } + + @Override + public void setInputBufferSize(int size) + { + customizer.setInputBufferSize(size); + } + + @Override + public void setOutputBufferSize(int size) + { + customizer.setOutputBufferSize(size); + } + + @Override + public void setMaxBinaryMessageSize(long size) + { + customizer.setMaxBinaryMessageSize(size); + } + + @Override + public void setMaxTextMessageSize(long size) + { + customizer.setMaxTextMessageSize(size); + } +} diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServlet.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServlet.java new file mode 100644 index 00000000000..c95a8acc351 --- /dev/null +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServlet.java @@ -0,0 +1,45 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.server; + +import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; +import org.eclipse.jetty.websocket.servlet.WebSocketServlet; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; + +public abstract class JettyWebSocketServlet extends WebSocketServlet +{ + protected abstract void configure(JettyWebSocketServletFactory factory); + + @Override + protected final void configure(WebSocketServletFactory factory) + { + configure(new JettyWebSocketServletFactory(factory)); + } + + @Override + public FrameHandlerFactory getFactory() + { + JettyServerFrameHandlerFactory frameHandlerFactory = JettyServerFrameHandlerFactory.getFactory(getServletContext()); + + if (frameHandlerFactory==null) + throw new IllegalStateException("JettyServerFrameHandlerFactory not found"); + + return frameHandlerFactory; + } +} diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletContainerInitializer.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletContainerInitializer.java index f85e596afac..8e0dee3e1c2 100644 --- a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletContainerInitializer.java +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletContainerInitializer.java @@ -18,17 +18,19 @@ package org.eclipse.jetty.websocket.server; -import java.util.Collections; import java.util.Set; + import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletException; +import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.servlet.WebSocketMapping; +import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; /** * ServletContext configuration for Jetty Native WebSockets API. @@ -37,45 +39,23 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain { private static final Logger LOG = Log.getLogger(JettyWebSocketServletContainerInitializer.class); - public static class JettyWebSocketEmbeddedStarter extends AbstractLifeCycle implements ServletContextHandler.ServletContainerInitializerCaller + public static JettyWebSocketServerContainer configureContext(ServletContextHandler context) { - private ServletContainerInitializer sci; - private ServletContextHandler context; + WebSocketComponents components = WebSocketComponents.ensureWebSocketComponents(context.getServletContext()); + FilterHolder filterHolder = WebSocketUpgradeFilter.ensureFilter(context.getServletContext()); + WebSocketMapping mapping = WebSocketMapping.ensureMapping(context.getServletContext(), WebSocketMapping.DEFAULT_KEY); + JettyWebSocketServerContainer container = JettyWebSocketServerContainer.ensureContainer(context.getServletContext()); - public JettyWebSocketEmbeddedStarter(ServletContainerInitializer sci, ServletContextHandler context) - { - this.sci = sci; - this.context = context; - } + if (LOG.isDebugEnabled()) + LOG.debug("configureContext {} {} {} {}", container, mapping, filterHolder, components); - public void doStart() - { - try - { - Set> c = Collections.emptySet(); - sci.onStartup(c, context.getServletContext()); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - } - } - - public static void configure(ServletContextHandler contextHandler) - { - JettyWebSocketServletContainerInitializer sci = new JettyWebSocketServletContainerInitializer(); - JettyWebSocketEmbeddedStarter starter = new JettyWebSocketEmbeddedStarter(sci, contextHandler); - contextHandler.addBean(starter); + return container; } @Override - public void onStartup(Set> c, ServletContext servletContext) throws ServletException + public void onStartup(Set> c, ServletContext context) throws ServletException { - WebSocketComponents components = WebSocketComponents.ensureWebSocketComponents(servletContext); - JettyServerFrameHandlerFactory factory = JettyServerFrameHandlerFactory.ensureFactory(servletContext); - - if (LOG.isDebugEnabled()) - LOG.debug("onStartup {} {}", components, factory); + ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context,"Jetty WebSocket SCI"); + JettyWebSocketServletContainerInitializer.configureContext(contextHandler); } } diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletFactory.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletFactory.java new file mode 100644 index 00000000000..0f40a81ec99 --- /dev/null +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletFactory.java @@ -0,0 +1,242 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.server; + +import java.time.Duration; +import java.util.Set; + +import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; + + +public class JettyWebSocketServletFactory +{ + private WebSocketServletFactory factory; + + public JettyWebSocketServletFactory(WebSocketServletFactory factory) + { + this.factory = factory; + } + + public Set getAvailableExtensionNames() + { + return factory.getExtensionRegistry().getAvailableExtensionNames(); + } + + public Duration getIdleTimeout() + { + return factory.getIdleTimeout(); + } + + public void setIdleTimeout(Duration duration) + { + factory.setIdleTimeout(duration); + } + + public int getInputBufferSize() + { + return factory.getInputBufferSize(); + } + + public void setInputBufferSize(int bufferSize) + { + factory.setInputBufferSize(bufferSize); + } + + public long getMaxFrameSize() + { + return factory.getMaxFrameSize(); + } + + public void setMaxFrameSize(long maxFrameSize) + { + factory.setMaxFrameSize(maxFrameSize); + } + + public long getMaxBinaryMessageSize() + { + return factory.getMaxBinaryMessageSize(); + } + + public void setMaxBinaryMessageSize(long bufferSize) + { + factory.setMaxBinaryMessageSize(bufferSize); + } + + public long getMaxTextMessageSize() + { + return factory.getMaxTextMessageSize(); + } + + public void setMaxTextMessageSize(long bufferSize) + { + factory.setMaxTextMessageSize(bufferSize); + } + + public int getOutputBufferSize() + { + return factory.getOutputBufferSize(); + } + + public void setOutputBufferSize(int bufferSize) + { + factory.setOutputBufferSize(bufferSize); + } + + public boolean isAutoFragment() + { + return factory.isAutoFragment(); + } + + public void setAutoFragment(boolean autoFragment) + { + factory.setAutoFragment(autoFragment); + } + + public void addMapping(String pathSpec, JettyWebSocketCreator creator) + { + factory.addMapping(pathSpec, new WrappedCreator(creator)); + } + + /** + * add a WebSocket mapping to a provided {@link JettyWebSocketCreator}. + *

    + * If mapping is added before this configuration is started, then it is persisted through + * stop/start of this configuration's lifecycle. Otherwise it will be removed when + * this configuration is stopped. + *

    + * + * @param pathSpec the pathspec to respond on + * @param creator the WebSocketCreator to use + * @since 10.0 + */ + public void addMapping(PathSpec pathSpec, JettyWebSocketCreator creator) + { + factory.addMapping(pathSpec, new WrappedCreator(creator)); + } + + /** + * Add a WebSocket mapping at PathSpec "/" for a creator which creates the endpointClass + * + * @param endpointClass the WebSocket class to use + */ + public void register(Class endpointClass) + { + factory.register(endpointClass); + } + + /** + * Add a WebSocket mapping at PathSpec "/" for a creator + * + * @param creator the WebSocketCreator to use + */ + public void setCreator(JettyWebSocketCreator creator) + { + factory.setCreator(new WrappedCreator(creator)); + } + + /** + * Returns the creator for the given path spec. + * + * @param pathSpec the pathspec to respond on + * @return the websocket creator if path spec exists, or null + */ + public JettyWebSocketCreator getMapping(PathSpec pathSpec) + { + WebSocketCreator creator = factory.getMapping(pathSpec); + if (creator instanceof WrappedCreator) + return ((WrappedCreator)creator).getCreator(); + + return null; + } + + /** + * Get the MappedResource for the given target path. + * + * @param target the target path + * @return the MappedResource if matched, or null if not matched. + */ + public JettyWebSocketCreator getMatch(String target) + { + WebSocketCreator creator = factory.getMatch(target); + if (creator instanceof WrappedCreator) + return ((WrappedCreator)creator).getCreator(); + + return null; + } + + + /** + * Parse a PathSpec string into a PathSpec instance. + *

    + * Recognized Path Spec syntaxes: + *

    + *
    + *
    /path/to or / or *.ext or servlet|{spec}
    + *
    Servlet Syntax
    + *
    ^{spec} or regex|{spec}
    + *
    Regex Syntax
    + *
    uri-template|{spec}
    + *
    URI Template (see JSR356 and RFC6570 level 1)
    + *
    + * + * @param rawSpec the raw path spec as String to parse. + * @return the {@link PathSpec} implementation for the rawSpec + */ + public PathSpec parsePathSpec(String rawSpec) + { + return factory.parsePathSpec(rawSpec); + } + + /** + * Removes the mapping based on the given path spec. + * + * @param pathSpec the pathspec to respond on + * @return true if underlying mapping were altered, false otherwise + */ + public boolean removeMapping(PathSpec pathSpec) + { + return factory.removeMapping(pathSpec); + } + + + private static class WrappedCreator implements WebSocketCreator + { + private JettyWebSocketCreator creator; + + private WrappedCreator(JettyWebSocketCreator creator) + { + this.creator = creator; + } + + public JettyWebSocketCreator getCreator() + { + return creator; + } + + @Override + public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) + { + return creator.createWebSocket(new JettyServerUpgradeRequest(req), new JettyServerUpgradeResponse(resp)); + } + } +} diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/MultiplexHttpDestinationOverFCGI.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java similarity index 55% rename from jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/MultiplexHttpDestinationOverFCGI.java rename to jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java index dcf7d2fc605..251ec7cb8c2 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/MultiplexHttpDestinationOverFCGI.java +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java @@ -16,25 +16,24 @@ // ======================================================================== // -package org.eclipse.jetty.fcgi.client.http; +package org.eclipse.jetty.websocket.server; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpExchange; -import org.eclipse.jetty.client.MultiplexHttpDestination; -import org.eclipse.jetty.client.Origin; -import org.eclipse.jetty.client.SendFailure; -import org.eclipse.jetty.client.api.Connection; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; -public class MultiplexHttpDestinationOverFCGI extends MultiplexHttpDestination +/** + * @deprecated Moved to #org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter + */ +@Deprecated +public class WebSocketUpgradeFilter extends org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter { - public MultiplexHttpDestinationOverFCGI(HttpClient client, Origin origin) - { - super(client, origin); - } - @Override - protected SendFailure send(Connection connection, HttpExchange exchange) + public void init(FilterConfig config) throws ServletException { - return ((HttpConnectionOverFCGI)connection).send(exchange); + super.init(config); + config.getServletContext().log( + WebSocketUpgradeFilter.class.getName() + + " is deprecated. Use " + + org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter.class.getName()); } } diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/DelegatedJettyServletUpgradeRequest.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/DelegatedJettyServletUpgradeRequest.java index b2d17f0873a..efb535cb9ef 100644 --- a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/DelegatedJettyServletUpgradeRequest.java +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/DelegatedJettyServletUpgradeRequest.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.eclipse.jetty.websocket.common.JettyExtensionConfig; import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; public class DelegatedJettyServletUpgradeRequest implements UpgradeRequest @@ -60,7 +61,7 @@ public class DelegatedJettyServletUpgradeRequest implements UpgradeRequest public List getExtensions() { return this.servletRequest.getExtensions().stream() - .map((ext) -> new org.eclipse.jetty.websocket.api.extensions.ExtensionConfig(ext.getName(), ext.getParameters())) + .map((ext) -> new JettyExtensionConfig(ext.getName(), ext.getParameters())) .collect(Collectors.toList()); } diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/JettyWebSocketServerContainer.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/JettyWebSocketServerContainer.java deleted file mode 100644 index 4b28f6d1ca4..00000000000 --- a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/JettyWebSocketServerContainer.java +++ /dev/null @@ -1,99 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.server.internal; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.function.Consumer; - -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.common.SessionTracker; -import org.eclipse.jetty.websocket.common.WebSocketContainer; -import org.eclipse.jetty.websocket.common.WebSocketSessionListener; - -public class JettyWebSocketServerContainer implements WebSocketContainer -{ - private final static Logger LOG = Log.getLogger(JettyWebSocketServerContainer.class); - private final Executor executor; - private final List sessionListeners = new ArrayList<>(); - private final SessionTracker sessionTracker = new SessionTracker(); - - public JettyWebSocketServerContainer(ContextHandler handler) - { - Executor executor = (Executor) handler - .getAttribute("org.eclipse.jetty.server.Executor"); - if (executor == null) - { - executor = handler.getServer().getThreadPool(); - } - if (executor == null) - { - executor = new QueuedThreadPool(); // default settings - } - this.executor = executor; - addSessionListener(sessionTracker); - handler.addBean(sessionTracker); - } - - @Override - public Executor getExecutor() - { - return this.executor; - } - - @Override - public void addSessionListener(WebSocketSessionListener listener) - { - sessionListeners.add(listener); - } - - @Override - public boolean removeSessionListener(WebSocketSessionListener listener) - { - return sessionListeners.remove(listener); - } - - @Override - public void notifySessionListeners(Consumer consumer) - { - for (WebSocketSessionListener listener : sessionListeners) - { - try - { - consumer.accept(listener); - } - catch (Throwable x) - { - LOG.info("Exception while invoking listener " + listener, x); - } - } - } - - @Override - public Collection getOpenSessions() - { - return sessionTracker.getSessions(); - } -} diff --git a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeResponseAdapter.java b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeResponseAdapter.java index c4a2faeb35d..c3de87cac3c 100644 --- a/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeResponseAdapter.java +++ b/jetty-websocket/jetty-websocket-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeResponseAdapter.java @@ -18,16 +18,17 @@ package org.eclipse.jetty.websocket.server.internal; -import org.eclipse.jetty.websocket.api.UpgradeResponse; -import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; - import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.eclipse.jetty.websocket.api.UpgradeResponse; +import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.eclipse.jetty.websocket.common.JettyExtensionConfig; +import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; + public class UpgradeResponseAdapter implements UpgradeResponse { private final ServletUpgradeResponse servletResponse; @@ -53,7 +54,7 @@ public class UpgradeResponseAdapter implements UpgradeResponse public List getExtensions() { return this.servletResponse.getExtensions().stream() - .map((ext) -> new org.eclipse.jetty.websocket.api.extensions.ExtensionConfig(ext.getName(), ext.getParameters())) + .map((ext) -> new JettyExtensionConfig(ext.getName(), ext.getParameters())) .collect(Collectors.toList()); } diff --git a/jetty-websocket/jetty-websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java b/jetty-websocket/jetty-websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java index 89b895e1fc1..4b25fadf684 100644 --- a/jetty-websocket/jetty-websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java +++ b/jetty-websocket/jetty-websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java @@ -42,13 +42,13 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.websocket.core.ExtensionConfig; +import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; /** * Tool to help debug websocket circumstances reported around browsers. @@ -101,7 +101,7 @@ public class BrowserDebugTool ServletContextHandler context = new ServletContextHandler(); - JettyWebSocketServletContainerInitializer.configure(context); + JettyWebSocketServletContainerInitializer.configureContext(context); context.setContextPath("/"); Resource staticResourceBase = findStaticResources(); @@ -137,10 +137,10 @@ public class BrowserDebugTool server.stop(); } - public static class BrowserSocketServlet extends WebSocketServlet + public static class BrowserSocketServlet extends JettyWebSocketServlet { @Override - public void configure(WebSocketServletFactory factory) { + public void configure(JettyWebSocketServletFactory factory) { LOG.debug("Configuring WebSocketServerFactory ..."); // Setup the desired Socket to use for all incoming upgrade requests @@ -160,10 +160,10 @@ public class BrowserDebugTool } } - public static class BrowserSocketCreator implements WebSocketCreator + public static class BrowserSocketCreator implements JettyWebSocketCreator { @Override - public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) + public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) { LOG.debug("Creating BrowserSocket"); @@ -182,7 +182,7 @@ public class BrowserDebugTool // manually negotiate extensions List negotiated = new ArrayList<>(); // adding frame debug - negotiated.add(new ExtensionConfig("@frame-capture; output-dir=target")); + negotiated.add(ExtensionConfig.parse("@frame-capture; output-dir=target")); for (ExtensionConfig config : req.getExtensions()) { if (config.getName().equals("permessage-deflate")) diff --git a/jetty-websocket/jetty-websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserSocket.java b/jetty-websocket/jetty-websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserSocket.java index f7d26550eb9..0e8f60602d8 100644 --- a/jetty-websocket/jetty-websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserSocket.java +++ b/jetty-websocket/jetty-websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserSocket.java @@ -40,11 +40,8 @@ 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.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.api.extensions.Frame; -import org.eclipse.jetty.websocket.core.OpCode; @WebSocket public class BrowserSocket diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/CloseTrackingEndpoint.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/CloseTrackingEndpoint.java index bf92d1ac490..5173d629b35 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/CloseTrackingEndpoint.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/CloseTrackingEndpoint.java @@ -29,9 +29,9 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.WebSocketAdapter; -import org.eclipse.jetty.websocket.common.WebSocketSessionImpl; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.common.WebSocketSession; import org.eclipse.jetty.websocket.core.internal.WebSocketConnection; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.hamcrest.Matcher; import static org.hamcrest.MatcherAssert.assertThat; @@ -117,11 +117,11 @@ public class CloseTrackingEndpoint extends WebSocketAdapter public EndPoint getEndPoint() { Session session = getSession(); - assertThat("Session type", session, instanceOf(WebSocketSessionImpl.class)); + assertThat("Session type", session, instanceOf(WebSocketSession.class)); - WebSocketSessionImpl wsSession = (WebSocketSessionImpl) session; - WebSocketChannel wsChannel = (WebSocketChannel) wsSession.getCoreSession(); - WebSocketConnection wsConnection = wsChannel.getConnection(); + WebSocketSession wsSession = (WebSocketSession) session; + WebSocketCoreSession wsCoreSession = (WebSocketCoreSession) wsSession.getCoreSession(); + WebSocketConnection wsConnection = wsCoreSession.getConnection(); return wsConnection.getEndPoint(); } diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoCreator.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoCreator.java index c9cc46b144a..61be8ec2617 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoCreator.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoCreator.java @@ -18,14 +18,14 @@ package org.eclipse.jetty.websocket.tests; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; -public class EchoCreator implements WebSocketCreator +public class EchoCreator implements JettyWebSocketCreator { @Override - public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) + public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) { if (req.hasSubProtocol("echo")) { diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoSocket.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoSocket.java index 4908c6cb29c..602951959be 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoSocket.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EchoSocket.java @@ -19,17 +19,25 @@ package org.eclipse.jetty.websocket.tests; import java.io.IOException; +import java.nio.ByteBuffer; -import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.annotations.WebSocket; @WebSocket -public class EchoSocket +public class EchoSocket extends EventSocket { - @OnWebSocketMessage - public void onMessage(Session session, String msg) throws IOException + @Override + public void onMessage(String message) throws IOException { - session.getRemote().sendString(msg); + super.onMessage(message); + session.getRemote().sendString(message); + } + + @Override + public void onMessage(byte[] buf, int offset, int len) + { + super.onMessage(buf, offset, len); + session.getRemote().sendBytes(ByteBuffer.wrap(buf, offset, len), WriteCallback.NOOP); } } diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EventSocket.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EventSocket.java new file mode 100644 index 00000000000..22c52d82ac7 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/EventSocket.java @@ -0,0 +1,102 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; + +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +@WebSocket +public class EventSocket +{ + private final static Logger LOG = Log.getLogger(EventSocket.class); + + public Session session; + private String behavior; + + public BlockingQueue messageQueue = new BlockingArrayQueue<>(); + public BlockingQueue binaryMessageQueue = new BlockingArrayQueue<>(); + public volatile int statusCode = StatusCode.UNDEFINED; + public volatile String reason; + public volatile Throwable error = null; + + public CountDownLatch openLatch = new CountDownLatch(1); + public CountDownLatch errorLatch = new CountDownLatch(1); + public CountDownLatch closeLatch = new CountDownLatch(1); + + @OnWebSocketConnect + public void onOpen(Session session) + { + this.session = session; + behavior = session.getPolicy().getBehavior().name(); + LOG.info("{} onOpen(): {}", toString(), session); + openLatch.countDown(); + } + + @OnWebSocketMessage + public void onMessage(String message) throws IOException + { + LOG.info("{} onMessage(): {}", toString(), message); + messageQueue.offer(message); + } + + + @OnWebSocketMessage + public void onMessage(byte buf[], int offset, int len) + { + ByteBuffer message = ByteBuffer.wrap(buf, offset, len); + LOG.info("{} onMessage(): {}", toString(), message); + binaryMessageQueue.offer(message); + } + + @OnWebSocketClose + public void onClose(int statusCode, String reason) + { + LOG.info("{} onClose(): {}:{}", toString(), statusCode, reason); + this.statusCode = statusCode; + this.reason = reason; + closeLatch.countDown(); + } + + @OnWebSocketError + public void onError(Throwable cause) + { + LOG.info("{} onError(): {}", toString(), cause); + error = cause; + errorLatch.countDown(); + } + + @Override + public String toString() + { + return String.format("[%s@%s]", behavior, Integer.toHexString(hashCode())); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketExtensionConfigTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketExtensionConfigTest.java new file mode 100644 index 00000000000..197e0b0450a --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketExtensionConfigTest.java @@ -0,0 +1,136 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.HttpResponse; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.JettyUpgradeListener; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JettyWebSocketExtensionConfigTest +{ + private Server server; + private WebSocketClient client; + + @BeforeEach + public void start() throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(8080); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + server.setHandler(contextHandler); + + JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.configureContext(contextHandler); + container.addMapping("/", (req, resp)-> + { + assertEquals(req.getExtensions().stream().filter(e -> e.getName().equals("permessage-deflate")).count(), 1); + assertEquals(resp.getExtensions().stream().filter(e -> e.getName().equals("permessage-deflate")).count(), 1); + + ExtensionConfig nonRequestedExtension = ExtensionConfig.parse("identity"); + assertNotNull(nonRequestedExtension); + + assertThrows(IllegalArgumentException.class, + ()->resp.setExtensions(List.of(nonRequestedExtension)), + "should not allow extensions not requested"); + + // Check identity extension was not added because it was not requested + assertEquals(resp.getExtensions().stream().filter(config -> config.getName().equals("identity")).count(), 0); + assertEquals(resp.getExtensions().stream().filter(e -> e.getName().equals("permessage-deflate")).count(), 1); + + return new EchoSocket(); + }); + server.start(); + + client = new WebSocketClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + @Test + public void testJettyExtensionConfig() throws Exception + { + URI uri = URI.create("ws://localhost:8080/filterPath"); + EventSocket socket = new EventSocket(); + + UpgradeRequest request = new ClientUpgradeRequest(); + request.addExtensions(ExtensionConfig.parse("permessage-deflate")); + + CountDownLatch correctResponseExtensions = new CountDownLatch(1); + JettyUpgradeListener listener = new JettyUpgradeListener() + { + @Override + public void onHandshakeResponse(HttpRequest request, HttpResponse response) + { + + String extensions = response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_EXTENSIONS); + if("permessage-deflate".equals(extensions)) + correctResponseExtensions.countDown(); + else + throw new IllegalStateException("Unexpected Negotiated Extensions: " + extensions); + } + }; + + CompletableFuture connect = client.connect(socket, uri, request, listener); + try(Session session = connect.get(5, TimeUnit.SECONDS)) + { + session.getRemote().sendString("hello world"); + } + assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS)); + assertTrue(correctResponseExtensions.await(5, TimeUnit.SECONDS)); + + String msg = socket.messageQueue.poll(); + assertThat(msg, is("hello world")); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketFilterTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketFilterTest.java new file mode 100644 index 00000000000..c3929ff64c0 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketFilterTest.java @@ -0,0 +1,87 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JettyWebSocketFilterTest +{ + private Server server; + private WebSocketClient client; + + @BeforeEach + public void start() throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(8080); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + server.setHandler(contextHandler); + + JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.configureContext(contextHandler); + container.addMapping("/", (req, resp)->new EchoSocket()); + server.start(); + + client = new WebSocketClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + @Test + public void test() throws Exception + { + URI uri = URI.create("ws://localhost:8080/filterPath"); + EventSocket socket = new EventSocket(); + CompletableFuture connect = client.connect(socket, uri); + try(Session session = connect.get(5, TimeUnit.SECONDS)) + { + session.getRemote().sendString("hello world"); + } + assertTrue(socket.closeLatch.await(10, TimeUnit.SECONDS)); + + String msg = socket.messageQueue.poll(); + assertThat(msg, is("hello world")); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketNegotiationTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketNegotiationTest.java new file mode 100644 index 00000000000..3f7991148f4 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketNegotiationTest.java @@ -0,0 +1,114 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.log.StacklessLogging; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.UpgradeRequest; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class JettyWebSocketNegotiationTest +{ + private Server server; + private WebSocketClient client; + private ServletContextHandler contextHandler; + + @BeforeEach + public void start() throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(8080); + server.addConnector(connector); + + contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + server.setHandler(contextHandler); + + server.start(); + client = new WebSocketClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + @Test + public void testBadRequest() throws Exception + { + JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.configureContext(contextHandler); + container.addMapping("/", (req, resp)->new EchoSocket()); + + URI uri = URI.create("ws://localhost:8080/filterPath"); + EventSocket socket = new EventSocket(); + + UpgradeRequest upgradeRequest = new ClientUpgradeRequest(); + upgradeRequest.addExtensions("permessage-deflate;invalidParameter"); + + CompletableFuture connect = client.connect(socket, uri, upgradeRequest); + Throwable t = assertThrows(ExecutionException.class, () -> connect.get(5, TimeUnit.SECONDS)); + assertThat(t.getMessage(), containsString("Failed to upgrade to websocket:")); + assertThat(t.getMessage(), containsString("400 Bad Request")); + } + + @Test + public void testServerError() throws Exception + { + JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.configureContext(contextHandler); + container.addMapping("/", (req, resp)-> + { + resp.setAcceptedSubProtocol("errorSubProtocol"); + return new EchoSocket(); + }); + + URI uri = URI.create("ws://localhost:8080/filterPath"); + EventSocket socket = new EventSocket(); + + try (StacklessLogging stacklessLogging = new StacklessLogging(HttpChannel.class)) + { + CompletableFuture connect = client.connect(socket, uri); + Throwable t = assertThrows(ExecutionException.class, () -> connect.get(5, TimeUnit.SECONDS)); + assertThat(t.getMessage(), containsString("Failed to upgrade to websocket:")); + assertThat(t.getMessage(), containsString("500 Server Error")); + } + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketServletTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketServletTest.java new file mode 100644 index 00000000000..046635b032f --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketServletTest.java @@ -0,0 +1,98 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JettyWebSocketServletTest +{ + public static class MyWebSocketServlet extends JettyWebSocketServlet + { + @Override + public void configure(JettyWebSocketServletFactory factory) + { + factory.addMapping("/",(req, resp)->new EchoSocket()); + } + } + + private Server server; + private WebSocketClient client; + + @BeforeEach + public void start() throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(8080); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + server.setHandler(contextHandler); + + contextHandler.addServlet(MyWebSocketServlet.class, "/servletPath"); + + JettyWebSocketServletContainerInitializer.configureContext(contextHandler); + server.start(); + + client = new WebSocketClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + @Test + public void test() throws Exception + { + URI uri = URI.create("ws://localhost:8080/servletPath"); + EventSocket socket = new EventSocket(); + CompletableFuture connect = client.connect(socket, uri); + try(Session session = connect.get(5, TimeUnit.SECONDS)) + { + session.getRemote().sendString("hello world"); + } + assertTrue(socket.closeLatch.await(10, TimeUnit.SECONDS)); + + String msg = socket.messageQueue.poll(); + assertThat(msg, is("hello world")); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketTest.java deleted file mode 100644 index 9f3f69adca7..00000000000 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/JettyWebSocketTest.java +++ /dev/null @@ -1,146 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.tests; - -import java.net.URI; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -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.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; -import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class JettyWebSocketTest -{ - - @WebSocket - public static class EventSocket - { - CountDownLatch closed = new CountDownLatch(1); - - String behavior; - - @OnWebSocketConnect - public void onOpen(Session sess) - { - behavior = sess.getPolicy().getBehavior().name(); - System.err.println(toString() + " Socket Connected: " + sess); - } - - @OnWebSocketMessage - public void onMessage(String message) - { - System.err.println(toString() + " Received TEXT message: " + message); - } - - @OnWebSocketClose - public void onClose(int statusCode, String reason) - { - System.err.println(toString() + " Socket Closed: " + statusCode + ":" + reason); - closed.countDown(); - } - - @OnWebSocketError - public void onError(Throwable cause) - { - cause.printStackTrace(System.err); - } - - @Override - public String toString() - { - return String.format("[%s@%s]", behavior, Integer.toHexString(hashCode())); - } - } - - public static class MyWebSocketServlet extends WebSocketServlet - { - @Override - public void configure(WebSocketServletFactory factory) - { - factory.addMapping("/",(req, resp)->new EventSocket()); - } - } - - - @Test - public void test() throws Exception - { - Server server = new Server(); - ServerConnector connector = new ServerConnector(server); - connector.setPort(8080); - server.addConnector(connector); - - ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); - contextHandler.setContextPath("/"); - server.setHandler(contextHandler); - - contextHandler.addServlet(MyWebSocketServlet.class, "/testPath1"); - contextHandler.addServlet(MyWebSocketServlet.class, "/testPath2"); - - try - { - JettyWebSocketServletContainerInitializer.configure(contextHandler); - server.start(); - - WebSocketClient client = new WebSocketClient(); - client.start(); - - URI uri = URI.create("ws://localhost:8080/testPath1"); - EventSocket socket = new EventSocket(); - CompletableFuture connect = client.connect(socket, uri); - try(Session session = connect.get(5, TimeUnit.SECONDS)) - { - session.getRemote().sendString("hello world"); - } - assertTrue(socket.closed.await(10, TimeUnit.SECONDS)); - - - uri = URI.create("ws://localhost:8080/testPath2"); - socket = new EventSocket(); - connect = client.connect(socket, uri); - try(Session session = connect.get(5, TimeUnit.SECONDS)) - { - session.getRemote().sendString("hello world"); - } - assertTrue(socket.closed.await(10, TimeUnit.SECONDS)); - - - server.stop(); - } - catch (Throwable t) - { - t.printStackTrace(); - } - } -} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SuspendResumeTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SuspendResumeTest.java new file mode 100644 index 00000000000..33285e7bf23 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/SuspendResumeTest.java @@ -0,0 +1,199 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +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.websocket.api.Session; +import org.eclipse.jetty.websocket.api.SuspendToken; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SuspendResumeTest +{ + @WebSocket + public static class SuspendSocket extends EventSocket + { + volatile SuspendToken suspendToken = null; + + @Override + public void onMessage(String message) throws IOException + { + if ("suspend".equals(message)) + suspendToken = session.suspend(); + super.onMessage(message); + } + } + + public class UpgradeServlet extends JettyWebSocketServlet + { + @Override + public void configure(JettyWebSocketServletFactory factory) + { + factory.setCreator(((req, resp) -> serverSocket)); + } + } + + private Server server = new Server(); + private WebSocketClient client = new WebSocketClient(); + private SuspendSocket serverSocket = new SuspendSocket(); + private ServerConnector connector; + + @BeforeEach + public void start() throws Exception + { + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + server.setHandler(contextHandler); + contextHandler.addServlet(new ServletHolder(new UpgradeServlet()), "/suspend"); + + JettyWebSocketServletContainerInitializer.configureContext(contextHandler); + + server.start(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + @Test + public void testSuspendWhenProcessingFrame() throws Exception + { + URI uri = new URI("ws://localhost:"+connector.getLocalPort()+"/suspend"); + EventSocket clientSocket = new EventSocket(); + Future connect = client.connect(clientSocket, uri); + connect.get(5, TimeUnit.SECONDS); + + clientSocket.session.getRemote().sendString("suspend"); + clientSocket.session.getRemote().sendString("suspend"); + clientSocket.session.getRemote().sendString("hello world"); + + assertThat(serverSocket.messageQueue.poll(5, TimeUnit.SECONDS), is("suspend")); + assertNull(serverSocket.messageQueue.poll(1, TimeUnit.SECONDS)); + + serverSocket.suspendToken.resume(); + assertThat(serverSocket.messageQueue.poll(5, TimeUnit.SECONDS), is("suspend")); + assertNull(serverSocket.messageQueue.poll(1, TimeUnit.SECONDS)); + + serverSocket.suspendToken.resume(); + assertThat(serverSocket.messageQueue.poll(5, TimeUnit.SECONDS), is("hello world")); + assertNull(serverSocket.messageQueue.poll(1, TimeUnit.SECONDS)); + + // make sure both sides are closed + clientSocket.session.close(); + assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); + + // check no errors occurred + assertNull(clientSocket.error); + assertNull(serverSocket.error); + } + + @Test + public void testExternalSuspend() throws Exception + { + URI uri = new URI("ws://localhost:"+connector.getLocalPort()+"/suspend"); + EventSocket clientSocket = new EventSocket(); + Future connect = client.connect(clientSocket, uri); + connect.get(5, TimeUnit.SECONDS); + + // verify connection by sending a message from server to client + assertTrue(serverSocket.openLatch.await(5, TimeUnit.SECONDS)); + serverSocket.session.getRemote().sendString("verification"); + assertThat(clientSocket.messageQueue.poll(5, TimeUnit.SECONDS), is("verification")); + + // suspend the client so that no read events occur + SuspendToken suspendToken = clientSocket.session.suspend(); + + // verify client can still send messages + clientSocket.session.getRemote().sendString("message-from-client"); + assertThat(serverSocket.messageQueue.poll(5, TimeUnit.SECONDS), is("message-from-client")); + + // the message is not received as it is suspended + serverSocket.session.getRemote().sendString("message-from-server"); + assertNull(clientSocket.messageQueue.poll(2, TimeUnit.SECONDS)); + + // client should receive message after it resumes + suspendToken.resume(); + assertThat(clientSocket.messageQueue.poll(5, TimeUnit.SECONDS), is("message-from-server")); + + // make sure both sides are closed + clientSocket.session.close(); + assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); + + // check no errors occurred + assertNull(clientSocket.error); + assertNull(serverSocket.error); + } + + @Disabled + @Test + public void testSuspendAfterClose() throws Exception + { + URI uri = new URI("ws://localhost:"+connector.getLocalPort()+"/suspend"); + EventSocket clientSocket = new EventSocket(); + Future connect = client.connect(clientSocket, uri); + connect.get(5, TimeUnit.SECONDS); + + // verify connection by sending a message from server to client + assertTrue(serverSocket.openLatch.await(5, TimeUnit.SECONDS)); + serverSocket.session.getRemote().sendString("verification"); + assertThat(clientSocket.messageQueue.poll(5, TimeUnit.SECONDS), is("verification")); + + // make sure both sides are closed + clientSocket.session.close(); + assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); + + // check no errors occurred + assertNull(clientSocket.error); + assertNull(serverSocket.error); + + // suspend the client so that no read events occur + SuspendToken suspendToken = clientSocket.session.suspend(); + suspendToken.resume(); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketServletExamplesTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketServletExamplesTest.java index 869478bd726..759111e0631 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketServletExamplesTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketServletExamplesTest.java @@ -19,9 +19,7 @@ package org.eclipse.jetty.websocket.tests; import java.net.URI; -import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.api.AuthenticationStore; @@ -39,19 +37,14 @@ import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.UpgradeRequest; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.tests.examples.MyAdvancedEchoServlet; import org.eclipse.jetty.websocket.tests.examples.MyAuthedServlet; import org.eclipse.jetty.websocket.tests.examples.MyEchoServlet; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -60,46 +53,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class WebSocketServletExamplesTest { + private Server _server; + private ServletContextHandler _context; - @WebSocket - public static class ClientSocket - { - CountDownLatch closed = new CountDownLatch(1); - ArrayBlockingQueue messageQueue = new ArrayBlockingQueue<>(2); - - @OnWebSocketConnect - public void onOpen(Session sess) - { - System.err.println("ClientSocket Connected: " + sess); - } - - @OnWebSocketMessage - public void onMessage(String message) - { - messageQueue.offer(message); - System.err.println("Received TEXT message: " + message); - } - - @OnWebSocketClose - public void onClose(int statusCode, String reason) - { - System.err.println("ClientSocket Closed: " + statusCode + ":" + reason); - closed.countDown(); - } - - @OnWebSocketError - public void onError(Throwable cause) - { - cause.printStackTrace(System.err); - } - } - - - static Server _server; - static ServletContextHandler _context; - - @BeforeAll - public static void setup() throws Exception + @BeforeEach + public void setup() throws Exception { _server = new Server(); ServerConnector connector = new ServerConnector(_server); @@ -115,12 +73,12 @@ public class WebSocketServletExamplesTest _context.addServlet(MyAdvancedEchoServlet.class, "/advancedEcho"); _context.addServlet(MyAuthedServlet.class, "/authed"); - JettyWebSocketServletContainerInitializer.configure(_context); + JettyWebSocketServletContainerInitializer.configureContext(_context); _server.start(); } - @AfterAll - public static void stop() throws Exception + @AfterEach + public void stop() throws Exception { _server.stop(); } @@ -150,7 +108,6 @@ public class WebSocketServletExamplesTest return security; } - @Test public void testEchoServlet() throws Exception { @@ -158,7 +115,7 @@ public class WebSocketServletExamplesTest client.start(); URI uri = URI.create("ws://localhost:8080/echo"); - ClientSocket socket = new ClientSocket(); + EventSocket socket = new EventSocket(); CompletableFuture connect = client.connect(socket, uri); try (Session session = connect.get(5, TimeUnit.SECONDS)) { @@ -169,10 +126,9 @@ public class WebSocketServletExamplesTest assertThat(response, is(message)); } - assertTrue(socket.closed.await(10, TimeUnit.SECONDS)); + assertTrue(socket.closeLatch.await(10, TimeUnit.SECONDS)); } - @Test public void testAdvancedEchoServlet() throws Exception { @@ -180,7 +136,7 @@ public class WebSocketServletExamplesTest client.start(); URI uri = URI.create("ws://localhost:8080/advancedEcho"); - ClientSocket socket = new ClientSocket(); + EventSocket socket = new EventSocket(); UpgradeRequest upgradeRequest = new ClientUpgradeRequest(); upgradeRequest.setSubProtocols("text"); @@ -194,10 +150,9 @@ public class WebSocketServletExamplesTest assertThat(response, is(message)); } - assertTrue(socket.closed.await(10, TimeUnit.SECONDS)); + assertTrue(socket.closeLatch.await(10, TimeUnit.SECONDS)); } - @Test public void testAuthedServlet() throws Exception { @@ -210,7 +165,7 @@ public class WebSocketServletExamplesTest BasicAuthentication basicAuthentication = new BasicAuthentication(uri, "testRealm", "user", "password"); authenticationStore.addAuthentication(basicAuthentication); - ClientSocket socket = new ClientSocket(); + EventSocket socket = new EventSocket(); CompletableFuture connect = client.connect(socket, uri); try (Session session = connect.get(5, TimeUnit.SECONDS)) { @@ -221,6 +176,6 @@ public class WebSocketServletExamplesTest assertThat(response, is(message)); } - assertTrue(socket.closed.await(10, TimeUnit.SECONDS)); + assertTrue(socket.closeLatch.await(10, TimeUnit.SECONDS)); } } diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketStatsTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketStatsTest.java index 8ee586c3f07..f5268d02011 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketStatsTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketStatsTest.java @@ -34,20 +34,14 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.WriteCallback; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; -import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; -import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.internal.Generator; import org.eclipse.jetty.websocket.core.internal.WebSocketConnection; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,66 +52,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class WebSocketStatsTest { - - @WebSocket - public static class ClientSocket - { - CountDownLatch closed = new CountDownLatch(1); - - String behavior; - - @OnWebSocketConnect - public void onOpen(Session session) - { - behavior = session.getPolicy().getBehavior().name(); - System.err.println(toString() + " Socket Connected: " + session); - } - - @OnWebSocketClose - public void onClose(int statusCode, String reason) - { - System.err.println(toString() + " Socket Closed: " + statusCode + ":" + reason); - closed.countDown(); - } - - @OnWebSocketError - public void onError(Throwable cause) - { - cause.printStackTrace(System.err); - } - - @Override - public String toString() - { - return String.format("[%s@%s]", behavior, Integer.toHexString(hashCode())); - } - } - - @WebSocket - public static class EchoSocket extends ClientSocket - { - @OnWebSocketMessage - public void onMessage(Session session, String message) - { - session.getRemote().sendString(message, WriteCallback.NOOP); - } - } - - public static class MyWebSocketServlet extends WebSocketServlet + public static class MyWebSocketServlet extends JettyWebSocketServlet { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { factory.setAutoFragment(false); factory.addMapping("/",(req, resp)->new EchoSocket()); } } - Server server; - WebSocketClient client; - ConnectionStatistics statistics; - CountDownLatch wsUpgradeComplete = new CountDownLatch(1); - CountDownLatch wsConnectionClosed = new CountDownLatch(1); + private Server server; + private WebSocketClient client; + private ConnectionStatistics statistics; + private CountDownLatch wsUpgradeComplete = new CountDownLatch(1); + private CountDownLatch wsConnectionClosed = new CountDownLatch(1); @BeforeEach public void start() throws Exception @@ -147,7 +96,7 @@ public class WebSocketStatsTest contextHandler.addServlet(MyWebSocketServlet.class, "/testPath"); server.setHandler(contextHandler); - JettyWebSocketServletContainerInitializer.configure(contextHandler); + JettyWebSocketServletContainerInitializer.configureContext(contextHandler); client = new WebSocketClient(); server.start(); @@ -176,10 +125,10 @@ public class WebSocketStatsTest public void echoStatsTest() throws Exception { URI uri = URI.create("ws://localhost:8080/testPath"); - ClientSocket socket = new ClientSocket(); + EventSocket socket = new EventSocket(); CompletableFuture connect = client.connect(socket, uri); - final long numMessages = 10000; + final long numMessages = 1000; final String msgText = "hello world"; long upgradeSentBytes; @@ -194,7 +143,7 @@ public class WebSocketStatsTest for (int i=0; i serverEndpoints = new BlockingArrayQueue<>(); private Session confirmConnection(CloseTrackingEndpoint clientSocket, Future clientFuture) throws Exception { @@ -121,16 +122,20 @@ public class ClientCloseTest server.addConnector(connector); ServletContextHandler context = new ServletContextHandler(); - JettyWebSocketServletContainerInitializer.configure(context); context.setContextPath("/"); - ServletHolder holder = new ServletHolder(new WebSocketServlet() + ServletHolder holder = new ServletHolder(new JettyWebSocketServlet() { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { factory.setIdleTimeout(Duration.ofSeconds(10)); factory.setMaxTextMessageSize(1024 * 1024 * 2); - factory.register(ServerEndpoint.class); + factory.setCreator((req,resp)-> + { + ServerEndpoint endpoint = new ServerEndpoint(); + serverEndpoints.offer(endpoint); + return endpoint; + }); } }); context.addServlet(holder, "/ws"); @@ -139,6 +144,7 @@ public class ClientCloseTest handlers.addHandler(context); handlers.addHandler(new DefaultHandler()); server.setHandler(handlers); + JettyWebSocketServletContainerInitializer.configureContext(context); server.start(); } @@ -230,7 +236,7 @@ public class ClientCloseTest public void testRemoteDisconnect() throws Exception { // Set client timeout - final int clientTimeout = 1000; + final int clientTimeout = 3000; client.setIdleTimeout(Duration.ofMillis(clientTimeout)); ClientOpenSessionTracker clientSessionTracker = new ClientOpenSessionTracker(1); @@ -241,20 +247,18 @@ public class ClientCloseTest CloseTrackingEndpoint clientSocket = new CloseTrackingEndpoint(); Future clientConnectFuture = client.connect(clientSocket, wsUri); - try (Session ignored = confirmConnection(clientSocket, clientConnectFuture)) - { - // client confirms connection via echo + // client confirms connection via echo + confirmConnection(clientSocket, clientConnectFuture); - // client sends close frame (triggering server connection abort) - final String origCloseReason = "abort"; - clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); + // client sends close frame (triggering server connection abort) + final String origCloseReason = "abort"; + clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); - // client reads -1 (EOF) - // client triggers close event on client ws-endpoint - clientSocket.assertReceivedCloseEvent(clientTimeout * 2, - is(StatusCode.SHUTDOWN), - containsString("timeout")); - } + // client reads -1 (EOF) + // client triggers close event on client ws-endpoint + clientSocket.assertReceivedCloseEvent(2000, + is(StatusCode.ABNORMAL), + containsString("Session Closed")); clientSessionTracker.assertClosedProperly(client); } @@ -279,18 +283,17 @@ public class ClientCloseTest // client confirms connection via echo // client sends close frame - final String origCloseReason = "sleep|5000"; + final String origCloseReason = "sleep|2500"; clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); // client close should occur clientSocket.assertReceivedCloseEvent(clientTimeout * 2, is(StatusCode.SHUTDOWN), - containsString("timeout")); + containsString("Timeout")); // client idle timeout triggers close event on client ws-endpoint assertThat("OnError Latch", clientSocket.errorLatch.await(2, SECONDS), is(true)); - assertThat("OnError", clientSocket.error.get(), instanceOf(CloseException.class)); - assertThat("OnError.cause", clientSocket.error.get().getCause(), instanceOf(TimeoutException.class)); + assertThat("OnError", clientSocket.error.get(), instanceOf(WebSocketTimeoutException.class)); } clientSessionTracker.assertClosedProperly(client); @@ -300,7 +303,7 @@ public class ClientCloseTest public void testStopLifecycle() throws Exception { // Set client timeout - final int timeout = 1000; + final int timeout = 3000; client.setIdleTimeout(Duration.ofMillis(timeout)); int sessionCount = 3; @@ -322,20 +325,35 @@ public class ClientCloseTest confirmConnection(clientSocket, clientConnectFuture); } - assertTimeoutPreemptively(ofSeconds(5), () -> { - // client lifecycle stop (the meat of this test) - client.stop(); - }); + assertThat(serverEndpoints.size(), is(sessionCount)); - // clients disconnect - for (int i = 0; i < sessionCount; i++) + try { - clientSockets.get(i).assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), containsString("Disconnected")); - } + // block all the server threads + for (int i = 0; i < sessionCount; i++) + clientSockets.get(i).getSession().getRemote().sendString("block"); - // ensure all Sessions are gone. connections are gone. etc. (client and server) - // ensure ConnectionListener onClose is called 3 times - clientSessionTracker.assertClosedProperly(client); + assertTimeoutPreemptively(ofSeconds(5), () -> + { + // client lifecycle stop (the meat of this test) + client.stop(); + }); + + // clients disconnect + for (int i = 0; i < sessionCount; i++) + clientSockets.get(i).assertReceivedCloseEvent(2000, is(StatusCode.ABNORMAL), containsString("Session Closed")); + + // ensure all Sessions are gone. connections are gone. etc. (client and server) + // ensure ConnectionListener onClose is called 3 times + clientSessionTracker.assertClosedProperly(client); + + assertThat(serverEndpoints.size(), is(sessionCount)); + } + finally + { + for (int i = 0; i < sessionCount; i++) + serverEndpoints.get(i).block.countDown(); + } } @Test @@ -356,29 +374,42 @@ public class ClientCloseTest // client confirms connection via echo confirmConnection(clientSocket, clientConnectFuture); - // setup client endpoint for write failure (test only) - EndPoint endp = clientSocket.getEndPoint(); - endp.shutdownOutput(); + try + { + // Block on the server so that the server does not detect a read failure + clientSocket.getSession().getRemote().sendString("block"); - // client enqueue close frame - // should result in a client write failure - final String origCloseReason = "Normal Close from Client"; - clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); + // setup client endpoint for write failure (test only) + EndPoint endp = clientSocket.getEndPoint(); + endp.shutdownOutput(); - assertThat("OnError Latch", clientSocket.errorLatch.await(2, SECONDS), is(true)); - assertThat("OnError", clientSocket.error.get(), instanceOf(ClosedChannelException.class)); + // client enqueue close frame + // should result in a client write failure + final String origCloseReason = "Normal Close from Client"; + clientSocket.getSession().close(StatusCode.NORMAL, origCloseReason); - // client triggers close event on client ws-endpoint - // assert - close code==1006 (abnormal) - clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), containsString("Eof")); + assertThat("OnError Latch", clientSocket.errorLatch.await(2, SECONDS), is(true)); + assertThat("OnError", clientSocket.error.get(), instanceOf(EofException.class)); - clientSessionTracker.assertClosedProperly(client); + // client triggers close event on client ws-endpoint + // assert - close code==1006 (abnormal) + clientSocket.assertReceivedCloseEvent(timeout, is(StatusCode.ABNORMAL), null); + clientSessionTracker.assertClosedProperly(client); + + assertThat(serverEndpoints.size(), is(1)); + } + finally + { + for (ServerEndpoint endpoint : serverEndpoints) + endpoint.block.countDown(); + } } public static class ServerEndpoint implements WebSocketFrameListener, WebSocketListener { private static final Logger LOG = Log.getLogger(ServerEndpoint.class); private Session session; + CountDownLatch block = new CountDownLatch(1); @Override public void onWebSocketBinary(byte[] payload, int offset, int len) @@ -398,15 +429,22 @@ public class ClientCloseTest String bigmsg = new String(buf, UTF_8); session.getRemote().sendString(bigmsg); } + else if (message.equals("block")) + { + LOG.debug("blocking"); + assertTrue(block.await(5, TimeUnit.MINUTES)); + LOG.debug("unblocked"); + } else { // simple echo session.getRemote().sendString(message); } } - catch (IOException ignore) + catch (Throwable t) { - LOG.debug(ignore); + LOG.debug(t); + throw new RuntimeException(t); } } @@ -425,13 +463,11 @@ public class ClientCloseTest public void onWebSocketError(Throwable cause) { if (LOG.isDebugEnabled()) - { - LOG.debug(cause); - } + LOG.debug("onWebSocketError(): ", cause); } @Override - public void onWebSocketFrame(org.eclipse.jetty.websocket.api.extensions.Frame frame) + public void onWebSocketFrame(Frame frame) { if (frame.getOpCode() == OpCode.CLOSE) { diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConfigTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConfigTest.java new file mode 100644 index 00000000000..af87718d240 --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConfigTest.java @@ -0,0 +1,219 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests.client; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.MessageTooLargeException; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.WebSocketTimeoutException; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.common.WebSocketSession; +import org.eclipse.jetty.websocket.core.internal.WebSocketConnection; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; +import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.tests.EchoSocket; +import org.eclipse.jetty.websocket.tests.EventSocket; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ClientConfigTest +{ + private Server server; + private WebSocketClient client; + private ServerConnector connector; + + private EchoSocket serverSocket = new EchoSocket(); + + private static String message = "this message is over 20 characters long"; + private final int inputBufferSize = 200; + private final int maxMessageSize = 20; + private final int idleTimeout = 500; + + public static Stream data() + { + return Stream.of("clientConfig", "annotatedConfig", "sessionConfig").map(Arguments::of); + } + + @BeforeEach + public void start() throws Exception + { + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + server.setHandler(contextHandler); + + JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.configureContext(contextHandler); + container.addMapping("/", (req, resp)->serverSocket); + server.start(); + + client = new WebSocketClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + @WebSocket(idleTimeout=idleTimeout, maxTextMessageSize=maxMessageSize, maxBinaryMessageSize=maxMessageSize, inputBufferSize=inputBufferSize, batchMode=BatchMode.ON) + public class AnnotatedConfigEndpoint extends EventSocket + { + } + + @WebSocket + public class SessionConfigEndpoint extends EventSocket + { + @Override + public void onOpen(Session session) + { + session.setIdleTimeout(Duration.ofMillis(idleTimeout)); + session.setMaxTextMessageSize(maxMessageSize); + session.setMaxBinaryMessageSize(maxMessageSize); + session.setInputBufferSize(inputBufferSize); + super.onOpen(session); + } + } + + public EventSocket getClientSocket(String param) + { + switch (param) + { + case "clientConfig": + client.setInputBufferSize(inputBufferSize); + client.setMaxBinaryMessageSize(maxMessageSize); + client.setIdleTimeout(Duration.ofMillis(idleTimeout)); + client.setMaxTextMessageSize(maxMessageSize); + return new EventSocket(); + + case "annotatedConfig": + return new AnnotatedConfigEndpoint(); + + case "sessionConfig": + return new SessionConfigEndpoint(); + + default: + throw new IllegalStateException(); + } + } + + @ParameterizedTest + @MethodSource("data") + public void testInputBufferSize(String param) throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/"); + EventSocket clientEndpoint = getClientSocket(param); + CompletableFuture connect = client.connect(clientEndpoint, uri); + + connect.get(5, TimeUnit.SECONDS); + + WebSocketCoreSession coreSession = (WebSocketCoreSession)((WebSocketSession)clientEndpoint.session).getCoreSession(); + WebSocketConnection connection = coreSession.getConnection(); + + assertThat(connection.getInputBufferSize(), is(inputBufferSize)); + + clientEndpoint.session.close(); + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertNull(clientEndpoint.error); + + assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(serverSocket.statusCode, is(StatusCode.NO_CODE)); + } + + @ParameterizedTest + @MethodSource("data") + public void testMaxBinaryMessageSize(String param) throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/"); + EventSocket clientEndpoint = getClientSocket(param); + CompletableFuture connect = client.connect(clientEndpoint, uri); + + connect.get(5, TimeUnit.SECONDS); + clientEndpoint.session.getRemote().sendBytes(BufferUtil.toBuffer(message)); + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + + assertThat(clientEndpoint.error, instanceOf(MessageTooLargeException.class)); + + assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(serverSocket.statusCode, is(StatusCode.MESSAGE_TOO_LARGE)); + } + + @ParameterizedTest + @MethodSource("data") + public void testIdleTimeout(String param) throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/"); + EventSocket clientEndpoint = getClientSocket(param); + CompletableFuture connect = client.connect(clientEndpoint, uri); + + connect.get(5, TimeUnit.SECONDS); + clientEndpoint.session.getRemote().sendString("hello world"); + Thread.sleep(idleTimeout + 500); + + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientEndpoint.error, instanceOf(WebSocketTimeoutException.class)); + + assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(serverSocket.statusCode, is(StatusCode.SHUTDOWN)); + } + + @ParameterizedTest + @MethodSource("data") + public void testMaxTextMessageSize(String param) throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/"); + EventSocket clientEndpoint = getClientSocket(param); + CompletableFuture connect = client.connect(clientEndpoint, uri); + + connect.get(5, TimeUnit.SECONDS); + clientEndpoint.session.getRemote().sendString(message); + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + + assertThat(clientEndpoint.error, instanceOf(MessageTooLargeException.class)); + + assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(serverSocket.statusCode, is(StatusCode.MESSAGE_TOO_LARGE)); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientOpenSessionTracker.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientOpenSessionTracker.java index 9f2471cba94..59c8bc53b17 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientOpenSessionTracker.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientOpenSessionTracker.java @@ -22,7 +22,7 @@ import java.util.concurrent.CountDownLatch; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.eclipse.jetty.websocket.common.WebSocketSessionImpl; +import org.eclipse.jetty.websocket.common.WebSocketSession; import org.eclipse.jetty.websocket.common.WebSocketSessionListener; import static java.util.concurrent.TimeUnit.SECONDS; @@ -64,12 +64,12 @@ public class ClientOpenSessionTracker implements Connection.Listener, WebSocketS } @Override - public void onWebSocketSessionOpened(WebSocketSessionImpl session) + public void onWebSocketSessionOpened(WebSocketSession session) { } @Override - public void onWebSocketSessionClosed(WebSocketSessionImpl session) + public void onWebSocketSessionClosed(WebSocketSession session) { this.closeSessionLatch.countDown(); } diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientSessionsTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientSessionsTest.java index 8b3b7bf29c0..7054af27dfd 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientSessionsTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientSessionsTest.java @@ -37,16 +37,15 @@ import org.eclipse.jetty.websocket.api.StatusCode; import org.eclipse.jetty.websocket.api.util.WSURI; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.eclipse.jetty.websocket.common.WebSocketSessionImpl; +import org.eclipse.jetty.websocket.common.WebSocketSession; import org.eclipse.jetty.websocket.common.WebSocketSessionListener; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; import org.eclipse.jetty.websocket.tests.EchoCreator; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -55,7 +54,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertTrue; -@Disabled("Needs triage") public class ClientSessionsTest { private Server server; @@ -70,12 +68,11 @@ public class ClientSessionsTest server.addConnector(connector); ServletContextHandler context = new ServletContextHandler(); - JettyWebSocketServletContainerInitializer.configure(context); context.setContextPath("/"); - ServletHolder holder = new ServletHolder(new WebSocketServlet() + ServletHolder holder = new ServletHolder(new JettyWebSocketServlet() { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { factory.setIdleTimeout(Duration.ofSeconds(10)); factory.setMaxTextMessageSize(1024 * 1024 * 2); @@ -88,6 +85,7 @@ public class ClientSessionsTest handlers.addHandler(context); handlers.addHandler(new DefaultHandler()); server.setHandler(handlers); + JettyWebSocketServletContainerInitializer.configureContext(context); server.start(); } @@ -107,12 +105,12 @@ public class ClientSessionsTest client.addSessionListener(new WebSocketSessionListener() { @Override - public void onWebSocketSessionOpened(WebSocketSessionImpl session) + public void onWebSocketSessionOpened(WebSocketSession session) { } @Override - public void onWebSocketSessionClosed(WebSocketSessionImpl session) + public void onWebSocketSessionClosed(WebSocketSession session) { onSessionCloseLatch.countDown(); } @@ -147,6 +145,8 @@ public class ClientSessionsTest String received = cliSock.messageQueue.poll(5, TimeUnit.SECONDS); assertThat("Message", received, containsString("Hello World!")); + + sess.close(StatusCode.NORMAL, null); } cliSock.assertReceivedCloseEvent(30000, is(StatusCode.NORMAL)); diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/SlowClientTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/SlowClientTest.java index 929a5a7481b..d373e1c13cb 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/SlowClientTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/SlowClientTest.java @@ -32,9 +32,9 @@ import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.StatusCode; import org.eclipse.jetty.websocket.api.util.WSURI; import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; import org.eclipse.jetty.websocket.tests.EchoSocket; import org.junit.jupiter.api.AfterEach; @@ -72,22 +72,22 @@ public class SlowClientTest ServletContextHandler context = new ServletContextHandler(); context.setContextPath("/"); - ServletHolder websocket = new ServletHolder(new WebSocketServlet() + ServletHolder websocket = new ServletHolder(new JettyWebSocketServlet() { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { factory.register(EchoSocket.class); } }); context.addServlet(websocket, "/ws"); - JettyWebSocketServletContainerInitializer.configure(context); HandlerList handlers = new HandlerList(); handlers.addHandler(context); handlers.addHandler(new DefaultHandler()); server.setHandler(handlers); + JettyWebSocketServletContainerInitializer.configureContext(context); server.start(); } diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java index 00b43224cd9..6e87475cddb 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoCreator.java @@ -18,11 +18,11 @@ package org.eclipse.jetty.websocket.tests.examples; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; -public class MyAdvancedEchoCreator implements WebSocketCreator +public class MyAdvancedEchoCreator implements JettyWebSocketCreator { private MyBinaryEchoSocket binaryEcho; private MyEchoSocket textEcho; @@ -35,7 +35,7 @@ public class MyAdvancedEchoCreator implements WebSocketCreator } @Override - public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) + public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) { for (String subprotocol : req.getSubProtocols()) { diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoServlet.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoServlet.java index d0dcb0a9c23..a0f7fb723ff 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoServlet.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAdvancedEchoServlet.java @@ -22,15 +22,15 @@ import java.time.Duration; import javax.servlet.annotation.WebServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; @SuppressWarnings("serial") @WebServlet(name = "MyAdvanced Echo WebSocket Servlet", urlPatterns = { "/advecho" }) -public class MyAdvancedEchoServlet extends WebSocketServlet +public class MyAdvancedEchoServlet extends JettyWebSocketServlet { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { // set a 10 second timeout factory.setIdleTimeout(Duration.ofSeconds(10)); diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAuthedCreator.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAuthedCreator.java index 424dc83d7bc..c5f15c69a60 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAuthedCreator.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAuthedCreator.java @@ -21,14 +21,14 @@ package org.eclipse.jetty.websocket.tests.examples; import java.io.IOException; import java.security.Principal; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; -public class MyAuthedCreator implements WebSocketCreator +public class MyAuthedCreator implements JettyWebSocketCreator { @Override - public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) + public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) { try { diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAuthedServlet.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAuthedServlet.java index e1b8511e8e9..bd3304d8446 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAuthedServlet.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyAuthedServlet.java @@ -18,14 +18,14 @@ package org.eclipse.jetty.websocket.tests.examples; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; @SuppressWarnings("serial") -public class MyAuthedServlet extends WebSocketServlet +public class MyAuthedServlet extends JettyWebSocketServlet { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { factory.setCreator(new MyAuthedCreator()); } diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyEchoServlet.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyEchoServlet.java index 4bbd1e7044d..f403968d149 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyEchoServlet.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/examples/MyEchoServlet.java @@ -22,15 +22,15 @@ import java.time.Duration; import javax.servlet.annotation.WebServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; @SuppressWarnings("serial") @WebServlet(name = "MyEcho WebSocket Servlet", urlPatterns = { "/echo" }) -public class MyEchoServlet extends WebSocketServlet +public class MyEchoServlet extends JettyWebSocketServlet { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { // set a 10 second timeout factory.setIdleTimeout(Duration.ofSeconds(10)); diff --git a/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/extensions/ExtensionConfigTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/extensions/ExtensionConfigTest.java similarity index 97% rename from jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/extensions/ExtensionConfigTest.java rename to jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/extensions/ExtensionConfigTest.java index f8001a93e72..df84a613b97 100644 --- a/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/extensions/ExtensionConfigTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/extensions/ExtensionConfigTest.java @@ -16,13 +16,14 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.api.extensions; - -import org.junit.jupiter.api.Test; +package org.eclipse.jetty.websocket.tests.extensions; import java.util.HashMap; import java.util.Map; +import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/AbstractCloseEndpoint.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/AbstractCloseEndpoint.java index ccd271410c3..6d8007f53d6 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/AbstractCloseEndpoint.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/AbstractCloseEndpoint.java @@ -56,6 +56,7 @@ public abstract class AbstractCloseEndpoint extends WebSocketAdapter @Override public void onWebSocketError(Throwable cause) { + LOG.debug("onWebSocketError({})", cause.getClass().getSimpleName()); errors.offer(cause); } diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerCloseCreator.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerCloseCreator.java index fd507d2a5fa..6f89b87402b 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerCloseCreator.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerCloseCreator.java @@ -19,29 +19,30 @@ package org.eclipse.jetty.websocket.tests.server; import java.util.concurrent.LinkedBlockingQueue; + import javax.servlet.ServletContext; import org.eclipse.jetty.websocket.common.WebSocketContainer; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.eclipse.jetty.websocket.server.JettyWebSocketCreator; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.tests.EchoSocket; import static java.util.concurrent.TimeUnit.SECONDS; -public class ServerCloseCreator implements WebSocketCreator +public class ServerCloseCreator implements JettyWebSocketCreator { - private final WebSocketServletFactory serverFactory; + private final JettyWebSocketServletFactory serverFactory; private LinkedBlockingQueue createdSocketQueue = new LinkedBlockingQueue<>(); - public ServerCloseCreator(WebSocketServletFactory serverFactory) + public ServerCloseCreator(JettyWebSocketServletFactory serverFactory) { this.serverFactory = serverFactory; } @Override - public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) + public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) { AbstractCloseEndpoint closeSocket = null; diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerCloseTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerCloseTest.java index 33696ccc34b..33b3e9a3ce4 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerCloseTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerCloseTest.java @@ -35,14 +35,14 @@ import org.eclipse.jetty.websocket.api.StatusCode; import org.eclipse.jetty.websocket.api.util.WSURI; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; -import org.eclipse.jetty.websocket.common.WebSocketSessionImpl; +import org.eclipse.jetty.websocket.common.WebSocketSession; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static java.util.concurrent.TimeUnit.SECONDS; @@ -55,7 +55,6 @@ import static org.hamcrest.Matchers.is; /** * Tests various close scenarios */ -@Disabled("Needs triage") public class ServerCloseTest { private WebSocketClient client; @@ -74,10 +73,10 @@ public class ServerCloseTest ServletContextHandler context = new ServletContextHandler(); context.setContextPath("/"); - ServletHolder closeEndpoint = new ServletHolder(new WebSocketServlet() + ServletHolder closeEndpoint = new ServletHolder(new JettyWebSocketServlet() { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { factory.setIdleTimeout(Duration.ofSeconds(2)); serverEndpointCreator = new ServerCloseCreator(factory); @@ -85,13 +84,13 @@ public class ServerCloseTest } }); context.addServlet(closeEndpoint, "/ws"); - JettyWebSocketServletContainerInitializer.configure(context); HandlerList handlers = new HandlerList(); handlers.addHandler(context); handlers.addHandler(new DefaultHandler()); server.setHandler(handlers); + JettyWebSocketServletContainerInitializer.configureContext(context); server.start(); } @@ -150,7 +149,7 @@ public class ServerCloseTest // Verify that server socket got close event AbstractCloseEndpoint serverEndpoint = serverEndpointCreator.pollLastCreated(); assertThat("Fast Close Latch", serverEndpoint.closeLatch.await(5, SECONDS), is(true)); - assertThat("Fast Close.statusCode", serverEndpoint.closeStatusCode, is(StatusCode.ABNORMAL)); + assertThat("Fast Close.statusCode", serverEndpoint.closeStatusCode, is(StatusCode.NORMAL)); } finally { @@ -174,7 +173,7 @@ public class ServerCloseTest Future futSession = client.connect(clientEndpoint, wsUri, request); Session session = null; - try(StacklessLogging ignore = new StacklessLogging(FastFailEndpoint.class, WebSocketSessionImpl.class)) + try(StacklessLogging ignore = new StacklessLogging(WebSocketCoreSession.class)) { session = futSession.get(5, SECONDS); @@ -213,7 +212,7 @@ public class ServerCloseTest Future futSession = client.connect(clientEndpoint, wsUri, request); Session session = null; - try(StacklessLogging ignore = new StacklessLogging(WebSocketSessionImpl.class)) + try(StacklessLogging ignore = new StacklessLogging(WebSocketSession.class)) { session = futSession.get(5, SECONDS); @@ -221,12 +220,13 @@ public class ServerCloseTest clientEndpoint.getEndPoint().close(); // Verify that client got close - clientEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.ABNORMAL), containsString("Disconnected")); + clientEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.ABNORMAL), containsString("Session Closed")); // Verify that server socket got close event AbstractCloseEndpoint serverEndpoint = serverEndpointCreator.pollLastCreated(); - serverEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.ABNORMAL), containsString("Disconnected")); - } finally + serverEndpoint.assertReceivedCloseEvent(5000, is(StatusCode.ABNORMAL), containsString("Session Closed")); + } + finally { close(session); } @@ -241,9 +241,9 @@ public class ServerCloseTest @Test public void testOpenSessionCleanup() throws Exception { - fastFail(); - fastClose(); - dropConnection(); + //fastFail(); + //fastClose(); + //dropConnection(); ClientUpgradeRequest request = new ClientUpgradeRequest(); request.setSubProtocols("container"); @@ -253,7 +253,7 @@ public class ServerCloseTest Future futSession = client.connect(clientEndpoint, wsUri, request); Session session = null; - try(StacklessLogging ignore = new StacklessLogging(WebSocketSessionImpl.class)) + try(StacklessLogging ignore = new StacklessLogging(WebSocketSession.class)) { session = futSession.get(5, SECONDS); diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerConfigTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerConfigTest.java new file mode 100644 index 00000000000..bcee42f5f8c --- /dev/null +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/ServerConfigTest.java @@ -0,0 +1,302 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.tests.server; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.eclipse.jetty.io.Connection; +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.util.BufferUtil; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.MessageTooLargeException; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.WebSocketTimeoutException; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.common.WebSocketSession; +import org.eclipse.jetty.websocket.core.internal.WebSocketConnection; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; +import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; +import org.eclipse.jetty.websocket.tests.EventSocket; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ServerConfigTest +{ + private Server server; + private WebSocketClient client; + private ServerConnector connector; + private ConnectionListener listener = new ConnectionListener(); + + private static String message = "this message is over 20 characters long"; + private final static int inputBufferSize = 200; + private final static int maxMessageSize = 20; + private final static int idleTimeout = 500; + + private EventSocket annotatedEndpoint = new AnnotatedConfigEndpoint(); + private EventSocket sessionConfigEndpoint = new SessionConfigEndpoint(); + private EventSocket standardEndpoint = new EventSocket(); + + private EventSocket getServerEndpoint(String path) + { + switch (path) + { + case "servletConfig": + case "containerConfig": + return standardEndpoint; + case "annotatedConfig": + return annotatedEndpoint; + case "sessionConfig": + return sessionConfigEndpoint; + default: + throw new IllegalStateException(); + } + } + + public static Stream data() + { + return Stream.of("servletConfig", "annotatedConfig", "containerConfig", "sessionConfig").map(Arguments::of); + } + + @WebSocket(idleTimeout=idleTimeout, maxTextMessageSize=maxMessageSize, maxBinaryMessageSize=maxMessageSize, inputBufferSize=inputBufferSize, batchMode=BatchMode.ON) + public static class AnnotatedConfigEndpoint extends EventSocket + { + } + + @WebSocket + public static class SessionConfigEndpoint extends EventSocket + { + @Override + public void onOpen(Session session) + { + session.setIdleTimeout(Duration.ofMillis(idleTimeout)); + session.setMaxTextMessageSize(maxMessageSize); + session.setMaxBinaryMessageSize(maxMessageSize); + session.setInputBufferSize(inputBufferSize); + super.onOpen(session); + } + } + + public class WebSocketFactoryConfigServlet extends JettyWebSocketServlet + { + @Override + public void configure(JettyWebSocketServletFactory factory) + { + factory.setIdleTimeout(Duration.ofMillis(idleTimeout)); + factory.setMaxTextMessageSize(maxMessageSize); + factory.setMaxBinaryMessageSize(maxMessageSize); + factory.setInputBufferSize(inputBufferSize); + factory.addMapping("/",(req, resp)->standardEndpoint); + } + } + + public class WebSocketAnnotatedConfigServlet extends JettyWebSocketServlet + { + @Override + public void configure(JettyWebSocketServletFactory factory) + { + factory.addMapping("/",(req, resp)->annotatedEndpoint); + } + } + + public class WebSocketSessionConfigServlet extends JettyWebSocketServlet + { + @Override + public void configure(JettyWebSocketServletFactory factory) + { + factory.addMapping("/",(req, resp)->sessionConfigEndpoint); + } + } + + public class ConnectionListener implements Connection.Listener + { + private AtomicInteger opened = new AtomicInteger(0); + private CountDownLatch closed = new CountDownLatch(1); + + @Override + public void onOpened(Connection connection) + { + if (connection instanceof WebSocketConnection) + opened.incrementAndGet(); + } + + @Override + public void onClosed(Connection connection) + { + if (connection instanceof WebSocketConnection) + closed.countDown(); + } + + public void assertClosed() throws Exception + { + assertTrue(closed.await(5, TimeUnit.SECONDS)); + assertThat(opened.get(), is(1)); + } + } + + @BeforeEach + public void start() throws Exception + { + server = new Server(); + connector = new ServerConnector(server); + connector.addBean(listener); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + contextHandler.setContextPath("/"); + contextHandler.addServlet(new ServletHolder(new WebSocketFactoryConfigServlet()), "/servletConfig"); + contextHandler.addServlet(new ServletHolder(new WebSocketAnnotatedConfigServlet()), "/annotatedConfig"); + contextHandler.addServlet(new ServletHolder(new WebSocketSessionConfigServlet()), "/sessionConfig"); + server.setHandler(contextHandler); + + JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.configureContext(contextHandler); + container.setIdleTimeout(Duration.ofMillis(idleTimeout)); + container.setMaxTextMessageSize(maxMessageSize); + container.setMaxBinaryMessageSize(maxMessageSize); + container.setInputBufferSize(inputBufferSize); + container.addMapping("/containerConfig", (req, resp)->standardEndpoint); + server.start(); + + client = new WebSocketClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + client.stop(); + server.stop(); + } + + + @ParameterizedTest + @MethodSource("data") + public void testInputBufferSize(String path) throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/" + path); + EventSocket clientEndpoint = new EventSocket(); + EventSocket serverEndpoint = getServerEndpoint(path); + CompletableFuture connect = client.connect(clientEndpoint, uri); + + connect.get(5, TimeUnit.SECONDS); + + assertTrue(serverEndpoint.openLatch.await(5, TimeUnit.SECONDS)); + WebSocketCoreSession coreSession = (WebSocketCoreSession)((WebSocketSession)serverEndpoint.session).getCoreSession(); + WebSocketConnection connection = coreSession.getConnection(); + + assertThat(connection.getInputBufferSize(), is(inputBufferSize)); + + serverEndpoint.session.close(); + assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertNull(serverEndpoint.error); + + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientEndpoint.statusCode, is(StatusCode.NO_CODE)); + + listener.assertClosed(); + } + + @ParameterizedTest + @MethodSource("data") + public void testMaxBinaryMessageSize(String path) throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/" + path); + EventSocket clientEndpoint = new EventSocket(); + EventSocket serverEndpoint = getServerEndpoint(path); + CompletableFuture connect = client.connect(clientEndpoint, uri); + + connect.get(5, TimeUnit.SECONDS); + clientEndpoint.session.getRemote().sendBytes(BufferUtil.toBuffer(message)); + assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + + assertThat(serverEndpoint.error, instanceOf(MessageTooLargeException.class)); + + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientEndpoint.statusCode, is(StatusCode.MESSAGE_TOO_LARGE)); + + listener.assertClosed(); + } + + @ParameterizedTest + @MethodSource("data") + public void testIdleTimeout(String path) throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/" + path); + EventSocket clientEndpoint = new EventSocket(); + EventSocket serverEndpoint = getServerEndpoint(path); + CompletableFuture connect = client.connect(clientEndpoint, uri); + + connect.get(5, TimeUnit.SECONDS); + clientEndpoint.session.getRemote().sendString("hello world"); + String msg = serverEndpoint.messageQueue.poll(500, TimeUnit.MILLISECONDS); + assertThat(msg, is("hello world")); + Thread.sleep(idleTimeout + 500); + + assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(serverEndpoint.error, instanceOf(WebSocketTimeoutException.class)); + + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientEndpoint.statusCode, is(StatusCode.SHUTDOWN)); + + listener.assertClosed(); + } + + @ParameterizedTest + @MethodSource("data") + public void testMaxTextMessageSize(String path) throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/" + path); + EventSocket clientEndpoint = new EventSocket(); + EventSocket serverEndpoint = getServerEndpoint(path); + CompletableFuture connect = client.connect(clientEndpoint, uri); + + connect.get(5, TimeUnit.SECONDS); + clientEndpoint.session.getRemote().sendString(message); + assertTrue(serverEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + + assertThat(serverEndpoint.error, instanceOf(MessageTooLargeException.class)); + + assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientEndpoint.statusCode, is(StatusCode.MESSAGE_TOO_LARGE)); + + listener.assertClosed(); + } +} diff --git a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/SlowServerTest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/SlowServerTest.java index 68676886977..20508c90a18 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/SlowServerTest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/SlowServerTest.java @@ -32,9 +32,9 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.util.WSURI; import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -72,22 +72,22 @@ public class SlowServerTest ServletContextHandler context = new ServletContextHandler(); context.setContextPath("/"); - ServletHolder websocket = new ServletHolder(new WebSocketServlet() + ServletHolder websocket = new ServletHolder(new JettyWebSocketServlet() { @Override - public void configure(WebSocketServletFactory factory) + public void configure(JettyWebSocketServletFactory factory) { factory.register(SlowServerEndpoint.class); } }); context.addServlet(websocket, "/ws"); - JettyWebSocketServletContainerInitializer.configure(context); HandlerList handlers = new HandlerList(); handlers.addHandler(context); handlers.addHandler(new DefaultHandler()); server.setHandler(handlers); + JettyWebSocketServletContainerInitializer.configureContext(context); server.start(); } diff --git a/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/WSURITest.java b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/util/WSURITest.java similarity index 96% rename from jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/WSURITest.java rename to jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/util/WSURITest.java index 9bf1370c6e5..281c838deb2 100644 --- a/jetty-websocket/jetty-websocket-api/src/test/java/org/eclipse/jetty/websocket/api/util/WSURITest.java +++ b/jetty-websocket/jetty-websocket-tests/src/test/java/org/eclipse/jetty/websocket/tests/util/WSURITest.java @@ -16,13 +16,14 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.api.util; - -import org.junit.jupiter.api.Test; +package org.eclipse.jetty.websocket.tests.util; import java.net.URI; import java.net.URISyntaxException; +import org.eclipse.jetty.websocket.api.util.WSURI; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; diff --git a/jetty-websocket/jetty-websocket-tests/src/test/resources/jetty-logging.properties b/jetty-websocket/jetty-websocket-tests/src/test/resources/jetty-logging.properties index d51a5031cea..8806e105177 100644 --- a/jetty-websocket/jetty-websocket-tests/src/test/resources/jetty-logging.properties +++ b/jetty-websocket/jetty-websocket-tests/src/test/resources/jetty-logging.properties @@ -20,6 +20,7 @@ # org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.Slf4jLog org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog org.eclipse.jetty.LEVEL=WARN +# org.eclipse.jetty.websocket.tests.LEVEL=DEBUG # org.eclipse.jetty.util.log.stderr.LONG=true # org.eclipse.jetty.server.AbstractConnector.LEVEL=DEBUG # org.eclipse.jetty.io.WriteFlusher.LEVEL=DEBUG @@ -28,7 +29,7 @@ org.eclipse.jetty.LEVEL=WARN # org.eclipse.jetty.io.LEVEL=DEBUG # org.eclipse.jetty.io.ManagedSelector.LEVEL=INFO # org.eclipse.jetty.websocket.LEVEL=DEBUG -# org.eclipse.jetty.websocket.core.internal.WebSocketChannel.LEVEL=DEBUG +# org.eclipse.jetty.websocket.core.internal.WebSocketCoreSessionsion.LEVEL=DEBUG # org.eclipse.jetty.websocket.jsr356.tests.LEVEL=DEBUG # org.eclipse.jetty.websocket.LEVEL=INFO # org.eclipse.jetty.websocket.jsr356.messages.LEVEL=DEBUG diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java new file mode 100644 index 00000000000..f8a5ae08d4a --- /dev/null +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionTracker.java @@ -0,0 +1,58 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.common; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; + +public class SessionTracker extends AbstractLifeCycle implements WebSocketSessionListener +{ + private CopyOnWriteArraySet sessions = new CopyOnWriteArraySet<>(); + + public Set getSessions() + { + return Collections.unmodifiableSet(sessions); + } + + @Override + public void onSessionOpened(WebSocketSession session) + { + sessions.add(session); + } + + @Override + public void onSessionClosed(WebSocketSession session) + { + sessions.remove(session); + } + + @Override + protected void doStop() throws Exception + { + for (WebSocketSession session : sessions) + { + LifeCycle.stop(session); + } + super.doStop(); + } +} \ No newline at end of file diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/AbstractExtension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/AbstractExtension.java index 5df0d0d6394..9e5c0ff9b4d 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/AbstractExtension.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/AbstractExtension.java @@ -18,16 +18,13 @@ package org.eclipse.jetty.websocket.core; -import java.io.IOException; - import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; -import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; @ManagedObject("Abstract Extension") public abstract class AbstractExtension implements Extension @@ -37,7 +34,7 @@ public abstract class AbstractExtension implements Extension private ExtensionConfig config; private OutgoingFrames nextOutgoing; private IncomingFrames nextIncoming; - private WebSocketChannel channel; + private WebSocketCoreSession coreSession; public AbstractExtension() { @@ -144,14 +141,14 @@ public abstract class AbstractExtension implements Extension } @Override - public void setWebSocketChannel(WebSocketChannel webSocketChannel) + public void setWebSocketCoreSession(WebSocketCoreSession coreSession) { - channel = webSocketChannel; + this.coreSession = coreSession; } - protected WebSocketChannel getWebSocketChannel() + protected WebSocketCoreSession getWebSocketCoreSession() { - return channel; + return coreSession; } @Override diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/CloseStatus.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/CloseStatus.java index dc49b31f1dd..3655cc286b3 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/CloseStatus.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/CloseStatus.java @@ -172,19 +172,11 @@ public class CloseStatus return null; } - // TODO consider defining a precedence for every CloseStatus, and change ChannelState only if higher precedence + // TODO consider defining a precedence for every CloseStatus, and change SessionState only if higher precedence public static boolean isOrdinary(CloseStatus closeStatus) { - switch (closeStatus.getCode()) - { - case NORMAL: - case SHUTDOWN: - case NO_CODE: - return true; - - default: - return false; - } + int code = closeStatus.getCode(); + return (code == NORMAL || code == NO_CODE || code >= 3000); } public int getCode() @@ -291,8 +283,8 @@ public class CloseStatus public Frame toFrame() { if (isTransmittableStatusCode(code)) - return new CloseFrame(this, OpCode.CLOSE, true, asPayloadBuffer(code, reason)); - return new CloseFrame(this, OpCode.CLOSE); + return new CloseFrame(OpCode.CLOSE, true, asPayloadBuffer(code, reason)); + return new CloseFrame(OpCode.CLOSE); } public static Frame toFrame(int closeStatus) @@ -356,12 +348,12 @@ public class CloseStatus class CloseFrame extends Frame implements CloseStatus.Supplier { - public CloseFrame(CloseStatus closeStatus, byte opcode) + public CloseFrame(byte opcode) { super(opcode); } - public CloseFrame(CloseStatus closeStatus, byte opCode, boolean fin, ByteBuffer payload) + public CloseFrame(byte opCode, boolean fin, ByteBuffer payload) { super(opCode, fin, payload); } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/Extension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/Extension.java index 38f7c715c78..8ee9987577c 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/Extension.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/Extension.java @@ -19,7 +19,7 @@ package org.eclipse.jetty.websocket.core; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; /** * Interface for WebSocket Extensions. @@ -89,9 +89,9 @@ public interface Extension extends IncomingFrames, OutgoingFrames void setNextOutgoingFrames(OutgoingFrames nextOutgoing); /** - * Set the {@link WebSocketChannel} for this Extension + * Set the {@link WebSocketCoreSession} for this Extension * - * @param webSocketChannel + * @param coreSession */ - void setWebSocketChannel(WebSocketChannel webSocketChannel); + void setWebSocketCoreSession(WebSocketCoreSession coreSession); } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/FrameHandler.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/FrameHandler.java index b856b46999b..4e87007d594 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/FrameHandler.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/FrameHandler.java @@ -75,7 +75,7 @@ public interface FrameHandler extends IncomingFrames * the connection will be closed.
    *

    * - * @param coreSession the channel associated with this connection. + * @param coreSession the session associated with this connection. * @param callback the callback to indicate success in processing (or failure) */ void onOpen(CoreSession coreSession, Callback callback); @@ -141,13 +141,27 @@ public interface FrameHandler extends IncomingFrames */ Duration getIdleTimeout(); + /** + * Get the Write Timeout + * + * @return the write timeout + */ + Duration getWriteTimeout(); + /** * Set the Idle Timeout. * - * @param timeout the timeout duration + * @param timeout the timeout duration (timeout <= 0 implies an infinite timeout) */ void setIdleTimeout(Duration timeout); + /** + * Set the Write Timeout. + * + * @param timeout the timeout duration (timeout <= 0 implies an infinite timeout) + */ + void setWriteTimeout(Duration timeout); + boolean isAutoFragment(); void setAutoFragment(boolean autoFragment); @@ -180,21 +194,21 @@ public interface FrameHandler extends IncomingFrames interface CoreSession extends OutgoingFrames, Configuration { /** - * The negotiated WebSocket Sub-Protocol for this channel. + * The negotiated WebSocket Sub-Protocol for this session. * - * @return the negotiated WebSocket Sub-Protocol for this channel. + * @return the negotiated WebSocket Sub-Protocol for this session. */ String getNegotiatedSubProtocol(); /** - * The negotiated WebSocket Extension Configurations for this channel. + * The negotiated WebSocket Extension Configurations for this session. * - * @return the list of Negotiated Extension Configurations for this channel. + * @return the list of Negotiated Extension Configurations for this session. */ List getNegotiatedExtensions(); /** - * The parameter map (from URI Query) for the active channel. + * The parameter map (from URI Query) for the active session. * * @return the immutable map of parameters */ @@ -241,7 +255,7 @@ public interface FrameHandler extends IncomingFrames * without supporting {@link InetSocketAddress} *

    * - * @return the SocketAddress for the local connection, or null if not supported by Channel + * @return the SocketAddress for the local connection, or null if not supported by Session */ SocketAddress getLocalAddress(); @@ -253,7 +267,7 @@ public interface FrameHandler extends IncomingFrames * without supporting {@link InetSocketAddress} *

    * - * @return the SocketAddress for the remote connection, or null if not supported by Channel + * @return the SocketAddress for the remote connection, or null if not supported by Session */ SocketAddress getRemoteAddress(); @@ -391,11 +405,22 @@ public interface FrameHandler extends IncomingFrames return Duration.ZERO; } + @Override + public Duration getWriteTimeout() + { + return Duration.ZERO; + } + @Override public void setIdleTimeout(Duration timeout) { } + @Override + public void setWriteTimeout(Duration timeout) + { + } + @Override public void flush(Callback callback) { @@ -491,12 +516,13 @@ public interface FrameHandler extends IncomingFrames interface Customizer { - void customize(CoreSession session); + void customize(Configuration configurable); } class ConfigurationCustomizer implements Customizer, Configuration { - private Duration timeout; + private Duration idleTimeout; + private Duration writeTimeout; private Boolean autoFragment; private Long maxFrameSize; private Integer outputBufferSize; @@ -507,13 +533,25 @@ public interface FrameHandler extends IncomingFrames @Override public Duration getIdleTimeout() { - return timeout==null ? Duration.ZERO : timeout; + return idleTimeout==null ? WebSocketConstants.DEFAULT_IDLE_TIMEOUT : idleTimeout; + } + + @Override + public Duration getWriteTimeout() + { + return writeTimeout==null ? WebSocketConstants.DEFAULT_WRITE_TIMEOUT : writeTimeout; } @Override public void setIdleTimeout(Duration timeout) { - this.timeout = timeout; + this.idleTimeout = timeout; + } + + @Override + public void setWriteTimeout(Duration timeout) + { + this.writeTimeout = timeout; } @Override @@ -589,22 +627,32 @@ public interface FrameHandler extends IncomingFrames } @Override - public void customize(CoreSession session) + public void customize(Configuration configurable) { - if (timeout!=null) - session.setIdleTimeout(timeout); + if (idleTimeout !=null) + configurable.setIdleTimeout(idleTimeout); + if (writeTimeout!=null) + configurable.setWriteTimeout(idleTimeout); if (autoFragment!=null) - session.setAutoFragment(autoFragment); + configurable.setAutoFragment(autoFragment); if (maxFrameSize!=null) - session.setMaxFrameSize(maxFrameSize); + configurable.setMaxFrameSize(maxFrameSize); if (inputBufferSize!=null) - session.setInputBufferSize(inputBufferSize); + configurable.setInputBufferSize(inputBufferSize); if (outputBufferSize!=null) - session.setOutputBufferSize(outputBufferSize); + configurable.setOutputBufferSize(outputBufferSize); if (maxBinaryMessageSize!=null) - session.setMaxBinaryMessageSize(maxBinaryMessageSize); + configurable.setMaxBinaryMessageSize(maxBinaryMessageSize); if (maxTextMessageSize!=null) - session.setMaxTextMessageSize(maxTextMessageSize); + configurable.setMaxTextMessageSize(maxTextMessageSize); + } + + public static ConfigurationCustomizer from(ConfigurationCustomizer parent, ConfigurationCustomizer child) + { + ConfigurationCustomizer customizer = new ConfigurationCustomizer(); + parent.customize(customizer); + child.customize(customizer); + return customizer; } } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketComponents.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketComponents.java index ec450ba54fd..e7fba03c394 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketComponents.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketComponents.java @@ -22,7 +22,6 @@ import javax.servlet.ServletContext; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; -import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.DecoratedObjectFactory; /** @@ -34,16 +33,16 @@ import org.eclipse.jetty.util.DecoratedObjectFactory; */ public class WebSocketComponents { + public static final String WEBSOCKET_COMPONENTS_ATTRIBUTE = WebSocketComponents.class.getName(); + public static WebSocketComponents ensureWebSocketComponents(ServletContext servletContext) { - ContextHandler contextHandler = ContextHandler.getContextHandler(servletContext); - // Ensure a mapping exists - WebSocketComponents components = contextHandler.getBean(WebSocketComponents.class); + WebSocketComponents components = (WebSocketComponents)servletContext.getAttribute(WEBSOCKET_COMPONENTS_ATTRIBUTE); if (components == null) { components = new WebSocketComponents(); - contextHandler.addBean(components); + servletContext.setAttribute(WEBSOCKET_COMPONENTS_ATTRIBUTE, components); } return components; diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConstants.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConstants.java index 543fc1d6d3c..632eef926c4 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConstants.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketConstants.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.websocket.core; import java.nio.charset.StandardCharsets; +import java.time.Duration; public final class WebSocketConstants { @@ -32,6 +33,8 @@ public final class WebSocketConstants public static final int DEFAULT_INPUT_BUFFER_SIZE = 4 * 1024; public static final int DEFAULT_OUTPUT_BUFFER_SIZE = 4 * 1024; public static final boolean DEFAULT_AUTO_FRAGMENT = true; + public static final Duration DEFAULT_IDLE_TIMEOUT = Duration.ofSeconds(30); + public static final Duration DEFAULT_WRITE_TIMEOUT = Duration.ZERO; /** * Globally Unique Identifier for use in WebSocket handshake within {@code Sec-WebSocket-Accept} and Sec-WebSocket-Key http headers. diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketExtensionRegistry.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketExtensionRegistry.java index 0ac25c8ae82..49459e90d8e 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketExtensionRegistry.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/WebSocketExtensionRegistry.java @@ -18,16 +18,17 @@ package org.eclipse.jetty.websocket.core; -import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.util.DecoratedObjectFactory; -import org.eclipse.jetty.util.StringUtil; - import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.util.DecoratedObjectFactory; +import org.eclipse.jetty.util.StringUtil; + public class WebSocketExtensionRegistry implements Iterable> { private Map> availableExtensions; @@ -100,7 +101,7 @@ public class WebSocketExtensionRegistry implements Iterable futureCoreSession; private final WebSocketCoreClient wsClient; + private FrameHandler frameHandler; + private FrameHandler.ConfigurationCustomizer customizer = new FrameHandler.ConfigurationCustomizer(); private List upgradeListeners = new ArrayList<>(); - /** - * Offered Extensions - */ - private List extensions = new ArrayList<>(); - /** - * Offered SubProtocols - */ - private List subProtocols = new ArrayList<>(); public ClientUpgradeRequest(WebSocketCoreClient webSocketClient, URI requestURI) { @@ -126,6 +122,11 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon getConversation().setAttribute(HttpConnectionUpgrader.class.getName(), this); } + public void setConfiguration(FrameHandler.ConfigurationCustomizer config) + { + config.customize(customizer); + } + public void addListener(UpgradeListener listener) { upgradeListeners.add(listener); @@ -133,52 +134,72 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon public void addExtensions(ExtensionConfig... configs) { + HttpFields headers = getHeaders(); for (ExtensionConfig config : configs) - { - this.extensions.add(config); - } - updateWebSocketExtensionHeader(); + headers.add(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, config.getParameterizedName()); } public void addExtensions(String... configs) { - this.extensions.addAll(ExtensionConfig.parseList(configs)); - updateWebSocketExtensionHeader(); + HttpFields headers = getHeaders(); + for (String config : configs) + headers.add(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, ExtensionConfig.parse(config).getParameterizedName()); } public List getExtensions() { + List extensions = getHeaders().getCSV(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, true) + .stream() + .map(ExtensionConfig::parse) + .collect(Collectors.toList()); + return extensions; } public void setExtensions(List configs) { - this.extensions = configs; - updateWebSocketExtensionHeader(); + HttpFields headers = getHeaders(); + headers.remove(HttpHeader.SEC_WEBSOCKET_EXTENSIONS); + for (ExtensionConfig config : configs) + headers.add(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, config.getParameterizedName()); } public List getSubProtocols() { - return this.subProtocols; + List subProtocols = getHeaders().getCSV(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, true); + return subProtocols; } public void setSubProtocols(String... protocols) { - this.subProtocols.clear(); - this.subProtocols.addAll(Arrays.asList(protocols)); - updateWebSocketSubProtocolHeader(); + HttpFields headers = getHeaders(); + headers.remove(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); + for (String protocol : protocols) + headers.add(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, protocol); } public void setSubProtocols(List protocols) { - this.subProtocols.clear(); - this.subProtocols.addAll(protocols); - updateWebSocketSubProtocolHeader(); + HttpFields headers = getHeaders(); + headers.remove(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); + for (String protocol : protocols) + headers.add(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, protocol); } @Override public void send(final Response.CompleteListener listener) { + try + { + frameHandler = getFrameHandler(); + if (frameHandler == null) + throw new IllegalArgumentException("FrameHandler could not be created"); + } + catch (Throwable t) + { + throw new IllegalArgumentException("FrameHandler could not be created", t); + } + initWebSocketHeaders(); super.send(listener); } @@ -216,19 +237,13 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon } Throwable failure = result.getFailure(); - if ((failure instanceof java.net.SocketException) || - (failure instanceof java.io.InterruptedIOException) || - (failure instanceof HttpResponseException) || - (failure instanceof UpgradeException)) - { - // handle as-is - handleException(failure); - } - else - { - // wrap in UpgradeException - handleException(new UpgradeException(requestURI, responseStatusCode, responseLine, failure)); - } + boolean wrapFailure = !((failure instanceof java.net.SocketException) || + (failure instanceof java.io.InterruptedIOException) || + (failure instanceof HttpResponseException) || + (failure instanceof UpgradeException)); + if (wrapFailure) + failure = new UpgradeException(requestURI, responseStatusCode, responseLine, failure); + handleException(failure); } if (responseStatusCode != HttpStatus.SWITCHING_PROTOCOLS_101) @@ -242,6 +257,17 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon protected void handleException(Throwable failure) { futureCoreSession.completeExceptionally(failure); + if (frameHandler != null) + { + try + { + frameHandler.onError(failure, Callback.NOOP); + } + catch (Throwable t) + { + LOG.warn("FrameHandler onError threw", t); + } + } } @SuppressWarnings("Duplicates") @@ -249,26 +275,17 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon public void upgrade(HttpResponse response, HttpConnectionOverHTTP httpConnection) { if (!this.getHeaders().get(HttpHeader.UPGRADE).equalsIgnoreCase("websocket")) - { - // Not my upgrade throw new HttpResponseException("Not a WebSocket Upgrade", response); - } - - HttpClient httpClient = wsClient.getHttpClient(); // Check the Accept hash String reqKey = this.getHeaders().get(HttpHeader.SEC_WEBSOCKET_KEY); String expectedHash = WebSocketCore.hashKey(reqKey); String respHash = response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_ACCEPT); - if (expectedHash.equalsIgnoreCase(respHash) == false) - { throw new HttpResponseException("Invalid Sec-WebSocket-Accept hash (was:" + respHash + ", expected:" + expectedHash + ")", response); - } - // Verify the Negotiated Extensions - ExtensionStack extensionStack = new ExtensionStack(wsClient.getExtensionRegistry()); - List extensions = new ArrayList<>(); + // Parse the Negotiated Extensions + List negotiatedExtensions = new ArrayList<>(); HttpField extField = response.getHeaders().getField(HttpHeader.SEC_WEBSOCKET_EXTENSIONS); if (extField != null) { @@ -280,15 +297,34 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon QuotedStringTokenizer tok = new QuotedStringTokenizer(extVal, ","); while (tok.hasMoreTokens()) { - extensions.add(ExtensionConfig.parse(tok.nextToken())); + negotiatedExtensions.add(ExtensionConfig.parse(tok.nextToken())); } } } } - extensionStack.negotiate(wsClient.getObjectFactory(), httpClient.getByteBufferPool(), extensions); + // Verify the Negotiated Extensions + List offeredExtensions = getExtensions(); + for (ExtensionConfig config : negotiatedExtensions) + { + if (config.getName().startsWith("@")) + continue; - // Check the negotiated subprotocol + long numMatch = offeredExtensions.stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); + if (numMatch < 1) + throw new WebSocketException("Upgrade failed: Sec-WebSocket-Extensions contained extension not requested"); + + numMatch = negotiatedExtensions.stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); + if (numMatch > 1) + throw new WebSocketException("Upgrade failed: Sec-WebSocket-Extensions contained more than one extension of the same name"); + } + + // Negotiate the extension stack + HttpClient httpClient = wsClient.getHttpClient(); + ExtensionStack extensionStack = new ExtensionStack(wsClient.getExtensionRegistry(), Behavior.CLIENT); + extensionStack.negotiate(wsClient.getObjectFactory(), httpClient.getByteBufferPool(), offeredExtensions, negotiatedExtensions); + + // Get the negotiated subprotocol String negotiatedSubProtocol = null; HttpField subProtocolField = response.getHeaders().getField(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); if (subProtocolField != null) @@ -297,38 +333,23 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon if (values != null) { if (values.length > 1) - { - throw new WebSocketException("Too many WebSocket subprotocol's in response: " + values); - } + throw new WebSocketException("Upgrade failed: Too many WebSocket subprotocol's in response: " + values); else if (values.length == 1) - { negotiatedSubProtocol = values[0]; - } } } - if (!subProtocols.isEmpty() && !subProtocols.contains(negotiatedSubProtocol)) - { - throw new WebSocketException("Upgrade failed: subprotocol [" + negotiatedSubProtocol + "] not found in offered subprotocols " + subProtocols); - } + // Verify the negotiated subprotocol + List offeredSubProtocols = getSubProtocols(); + if (negotiatedSubProtocol == null && !offeredSubProtocols.isEmpty()) + throw new WebSocketException("Upgrade failed: no subprotocol selected from offered subprotocols "); + if (negotiatedSubProtocol != null && !offeredSubProtocols.contains(negotiatedSubProtocol)) + throw new WebSocketException("Upgrade failed: subprotocol [" + negotiatedSubProtocol + "] not found in offered subprotocols " + offeredSubProtocols); // We can upgrade EndPoint endp = httpConnection.getEndPoint(); customize(endp); - FrameHandler frameHandler = getFrameHandler(wsClient, response); - - if (frameHandler == null) - { - StringBuilder err = new StringBuilder(); - err.append("FrameHandler is null for request ").append(this.getURI().toASCIIString()); - if (negotiatedSubProtocol != null) - { - err.append(" [subprotocol: ").append(negotiatedSubProtocol).append("]"); - } - throw new WebSocketException(err.toString()); - } - Request request = response.getRequest(); Negotiated negotiated = new Negotiated( request.getURI(), @@ -337,15 +358,15 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon extensionStack, WebSocketConstants.SPEC_VERSION_STRING); - WebSocketChannel wsChannel = newWebSocketChannel(frameHandler, negotiated); - wsClient.customize(wsChannel); + WebSocketCoreSession coreSession = newWebSocketCoreSession(frameHandler, negotiated); + customizer.customize(coreSession); - WebSocketConnection wsConnection = newWebSocketConnection(endp, httpClient.getExecutor(), httpClient.getByteBufferPool(), wsChannel); + WebSocketConnection wsConnection = newWebSocketConnection(endp, httpClient.getExecutor(), httpClient.getScheduler(), httpClient.getByteBufferPool(), coreSession); for (Connection.Listener listener : wsClient.getBeans(Connection.Listener.class)) wsConnection.addListener(listener); - wsChannel.setWebSocketConnection(wsConnection); + coreSession.setWebSocketConnection(wsConnection); notifyUpgradeListeners((listener) -> listener.onHandshakeResponse(this, response)); @@ -353,7 +374,7 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon try { endp.upgrade(wsConnection); - futureCoreSession.complete(wsChannel); + futureCoreSession.complete(coreSession); } catch (Throwable t) { @@ -370,17 +391,17 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon { } - protected WebSocketConnection newWebSocketConnection(EndPoint endp, Executor executor, ByteBufferPool byteBufferPool, WebSocketChannel wsChannel) + protected WebSocketConnection newWebSocketConnection(EndPoint endp, Executor executor, Scheduler scheduler, ByteBufferPool byteBufferPool, WebSocketCoreSession coreSession) { - return new WebSocketConnection(endp, executor, byteBufferPool, wsChannel); + return new WebSocketConnection(endp, executor, scheduler, byteBufferPool, coreSession); } - protected WebSocketChannel newWebSocketChannel(FrameHandler handler, Negotiated negotiated) + protected WebSocketCoreSession newWebSocketCoreSession(FrameHandler handler, Negotiated negotiated) { - return new WebSocketChannel(handler, Behavior.CLIENT, negotiated); + return new WebSocketCoreSession(handler, Behavior.CLIENT, negotiated); } - public abstract FrameHandler getFrameHandler(WebSocketCoreClient coreClient, HttpResponse response); + public abstract FrameHandler getFrameHandler(); private final String genRandomKey() { @@ -435,24 +456,4 @@ public abstract class ClientUpgradeRequest extends HttpRequest implements Respon } } } - - private void updateWebSocketExtensionHeader() - { - HttpFields headers = getHeaders(); - headers.remove(HttpHeader.SEC_WEBSOCKET_EXTENSIONS); - for (ExtensionConfig config : extensions) - { - headers.add(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, config.getParameterizedName()); - } - } - - private void updateWebSocketSubProtocolHeader() - { - HttpFields headers = getHeaders(); - headers.remove(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL); - for (String protocol : subProtocols) - { - headers.add(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL, protocol); - } - } } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpClientProvider.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpClientProvider.java index a9f7e965404..224e10268d1 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpClientProvider.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpClientProvider.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.websocket.core.client; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; public interface HttpClientProvider @@ -34,9 +33,9 @@ public interface HttpClientProvider if (client != null) return client; } - catch (Throwable ignore) + catch (Throwable x) { - Log.getLogger(HttpClientProvider.class).ignore(ignore); + Log.getLogger(HttpClientProvider.class).ignore(x); } return HttpClientProvider.newDefaultHttpClient(); @@ -44,7 +43,7 @@ public interface HttpClientProvider private static HttpClient newDefaultHttpClient() { - HttpClient client = new HttpClient(new SslContextFactory()); + HttpClient client = new HttpClient(); QueuedThreadPool threadPool = new QueuedThreadPool(); threadPool.setName("WebSocketClient@" + client.hashCode()); client.setExecutor(threadPool); diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/UpgradeListener.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/UpgradeListener.java index a72ee823265..4e70b198ab7 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/UpgradeListener.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/UpgradeListener.java @@ -28,7 +28,8 @@ public interface UpgradeListener * * @param request the request */ - void onHandshakeRequest(HttpRequest request); + default void onHandshakeRequest(HttpRequest request) + {} /** * Event that triggers after the Handshake response has been received. @@ -36,5 +37,6 @@ public interface UpgradeListener * @param request the request that was used * @param response the response that was received */ - void onHandshakeResponse(HttpRequest request, HttpResponse response); + default void onHandshakeResponse(HttpRequest request, HttpResponse response) + {} } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java index 759232bf107..2d13e76fcc3 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/WebSocketCoreClient.java @@ -31,16 +31,16 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.ShutdownThread; import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.FrameHandler; +import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; -public class WebSocketCoreClient extends ContainerLifeCycle implements FrameHandler.Customizer +public class WebSocketCoreClient extends ContainerLifeCycle { + public static final String WEBSOCKET_CORECLIENT_ATTRIBUTE = WebSocketCoreClient.class.getName(); private static final Logger LOG = Log.getLogger(WebSocketCoreClient.class); private final HttpClient httpClient; - private WebSocketExtensionRegistry extensionRegistry; - private DecoratedObjectFactory objectFactory; - private final FrameHandler.Customizer customizer; + private WebSocketComponents components; // TODO: Things to consider for inclusion in this class (or removal if they can be set elsewhere, like HttpClient) // - AsyncWrite Idle Timeout @@ -51,33 +51,24 @@ public class WebSocketCoreClient extends ContainerLifeCycle implements FrameHand public WebSocketCoreClient() { - this(null,null); + this(null, new WebSocketComponents()); } - public WebSocketCoreClient(HttpClient httpClient) + public WebSocketCoreClient(WebSocketComponents webSocketComponents) { - this(httpClient, null); + this(null, webSocketComponents); } - public WebSocketCoreClient(HttpClient httpClient, FrameHandler.Customizer customizer) + public WebSocketCoreClient(HttpClient httpClient, WebSocketComponents webSocketComponents) { if (httpClient == null) httpClient = Objects.requireNonNull(HttpClientProvider.get()); this.httpClient = httpClient; - this.extensionRegistry = new WebSocketExtensionRegistry(); - this.objectFactory = new DecoratedObjectFactory(); - this.customizer = customizer; + this.components = webSocketComponents; addBean(httpClient); } - @Override - public void customize(FrameHandler.CoreSession session) - { - if (customizer != null) - customizer.customize(session); - } - public CompletableFuture connect(FrameHandler frameHandler, URI wsUri) throws IOException { ClientUpgradeRequest request = ClientUpgradeRequest.from(this, wsUri, frameHandler); @@ -87,16 +78,12 @@ public class WebSocketCoreClient extends ContainerLifeCycle implements FrameHand public CompletableFuture connect(ClientUpgradeRequest request) throws IOException { if (!isStarted()) - { throw new IllegalStateException(WebSocketCoreClient.class.getSimpleName() + "@" + this.hashCode() + " is not started"); - } - - // TODO: add HttpClient delayed/on-demand start - See Issue #1516 // Validate Requested Extensions for (ExtensionConfig reqExt : request.getExtensions()) { - if (!extensionRegistry.isAvailable(reqExt.getName())) + if (!components.getExtensionRegistry().isAvailable(reqExt.getName())) { throw new IllegalArgumentException("Requested extension [" + reqExt.getName() + "] is not installed"); } @@ -121,7 +108,7 @@ public class WebSocketCoreClient extends ContainerLifeCycle implements FrameHand public WebSocketExtensionRegistry getExtensionRegistry() { - return extensionRegistry; + return components.getExtensionRegistry(); } public HttpClient getHttpClient() @@ -131,6 +118,11 @@ public class WebSocketCoreClient extends ContainerLifeCycle implements FrameHand public DecoratedObjectFactory getObjectFactory() { - return objectFactory; + return components.getObjectFactory(); + } + + public WebSocketComponents getWebSocketComponents() + { + return components; } } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/XmlHttpClientProvider.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/XmlHttpClientProvider.java index dafd14d5bd5..e0539fc51d8 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/XmlHttpClientProvider.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/XmlHttpClientProvider.java @@ -18,11 +18,11 @@ package org.eclipse.jetty.websocket.core.client; -import java.io.InputStream; import java.net.URL; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlConfiguration; class XmlHttpClientProvider implements HttpClientProvider @@ -35,11 +35,11 @@ class XmlHttpClientProvider implements HttpClientProvider { return null; } - - try (InputStream in = resource.openStream()) + + try { - XmlConfiguration configuration = new XmlConfiguration(in); - return (HttpClient)configuration.configure(); + XmlConfiguration configuration = new XmlConfiguration(Resource.newResource(resource)); + return (HttpClient) configuration.configure(); } catch (Throwable t) { diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/ExtensionStack.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/ExtensionStack.java index fa903261f14..97c1a20d2c3 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/ExtensionStack.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/ExtensionStack.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.ListIterator; import java.util.stream.Collectors; +import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.DecoratedObjectFactory; @@ -33,11 +34,13 @@ import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.Behavior; import org.eclipse.jetty.websocket.core.Extension; import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.IncomingFrames; import org.eclipse.jetty.websocket.core.OutgoingFrames; +import org.eclipse.jetty.websocket.core.WebSocketException; import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; /** @@ -49,13 +52,15 @@ public class ExtensionStack implements IncomingFrames, OutgoingFrames, Dumpable private static final Logger LOG = Log.getLogger(ExtensionStack.class); private final WebSocketExtensionRegistry factory; + private final Behavior behavior; private List extensions; private IncomingFrames incoming; private OutgoingFrames outgoing; - public ExtensionStack(WebSocketExtensionRegistry factory) + public ExtensionStack(WebSocketExtensionRegistry factory, Behavior behavior) { this.factory = factory; + this.behavior = behavior; } @ManagedAttribute(name = "Extension List", readonly = true) @@ -107,20 +112,60 @@ public class ExtensionStack implements IncomingFrames, OutgoingFrames, Dumpable *

    * For the list of negotiated extensions, use {@link #getNegotiatedExtensions()} * - * @param configs the configurations being requested + * @param offeredConfigs the configurations being requested by the client + * @param negotiatedConfigs the configurations accepted by the server */ - public void negotiate(DecoratedObjectFactory objectFactory, ByteBufferPool bufferPool, List configs) + public void negotiate(DecoratedObjectFactory objectFactory, ByteBufferPool bufferPool, List offeredConfigs, List negotiatedConfigs) { if (LOG.isDebugEnabled()) - LOG.debug("Extension Configs={}", configs); + LOG.debug("Extension Configs={}", negotiatedConfigs); this.extensions = new ArrayList<>(); String rsvClaims[] = new String[3]; - for (ExtensionConfig config : configs) + for (ExtensionConfig config : negotiatedConfigs) { - Extension ext = factory.newInstance(objectFactory, bufferPool, config); + Extension ext; + + try + { + ext = factory.newInstance(objectFactory, bufferPool, config); + } + catch (Throwable t) + { + /* If there was an error creating the extension we need to differentiate between a + bad ExtensionConfig offered by the client and a bad ExtensionConfig negotiated by the server. + + When deciding whether to throw a BadMessageException and send a 400 response or a WebSocketException + and send a 500 response it depends on whether this is running on the client or the server. */ + switch (behavior) + { + case SERVER: + { + String parameterizedName = config.getParameterizedName(); + for (ExtensionConfig offered : offeredConfigs) + { + if (offered.getParameterizedName().equals(parameterizedName)) + throw new BadMessageException("could not instantiate offered extension", t); + } + throw new WebSocketException("could not instantiate negotiated extension", t); + } + case CLIENT: + { + String parameterizedName = config.getParameterizedName(); + for (ExtensionConfig offered : offeredConfigs) + { + if (offered.getParameterizedName().equals(parameterizedName)) + throw new WebSocketException("could not instantiate offered extension", t); + } + throw new BadMessageException("could not instantiate negotiated extension", t); + } + default: + throw new IllegalStateException(); + } + } + if (ext == null) { // Extension not present on this side @@ -198,7 +243,7 @@ public class ExtensionStack implements IncomingFrames, OutgoingFrames, Dumpable outgoing.sendFrame(frame, callback, batch); } - public void initialize(IncomingFrames incoming, OutgoingFrames outgoing, WebSocketChannel webSocketChannel) + public void initialize(IncomingFrames incoming, OutgoingFrames outgoing, WebSocketCoreSession coreSession) { if (extensions == null) throw new IllegalStateException(); @@ -214,7 +259,7 @@ public class ExtensionStack implements IncomingFrames, OutgoingFrames, Dumpable } for (Extension extension : extensions) - extension.setWebSocketChannel(webSocketChannel); + extension.setWebSocketCoreSession(coreSession); } @Override diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/FrameFlusher.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/FrameFlusher.java index 409e07bfafc..4f8cb909f66 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/FrameFlusher.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/FrameFlusher.java @@ -23,8 +23,10 @@ import java.nio.channels.ClosedChannelException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; +import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.LongAdder; import org.eclipse.jetty.io.ByteBufferPool; @@ -32,10 +34,17 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IteratingCallback; +import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Scheduler; +import org.eclipse.jetty.websocket.core.CloseStatus; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; +import org.eclipse.jetty.websocket.core.WebSocketException; +import org.eclipse.jetty.websocket.core.WebSocketWriteTimeoutException; + +import static org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession.AbnormalCloseStatus; public class FrameFlusher extends IteratingCallback { @@ -49,15 +58,21 @@ public class FrameFlusher extends IteratingCallback private final Generator generator; private final int maxGather; private final Deque queue = new ArrayDeque<>(); - private final List entries; private final List buffers; + private final Scheduler timeoutScheduler; + private final List entries; + private final List previousEntries; + private final List failedEntries; + private ByteBuffer batchBuffer = null; private boolean canEnqueue = true; + private boolean flushed = true; private Throwable closedCause; private LongAdder messagesOut = new LongAdder(); private LongAdder bytesOut = new LongAdder(); + private long idleTimeout = 0; - public FrameFlusher(ByteBufferPool bufferPool, Generator generator, EndPoint endPoint, int bufferSize, int maxGather) + public FrameFlusher(ByteBufferPool bufferPool, Scheduler scheduler, Generator generator, EndPoint endPoint, int bufferSize, int maxGather) { this.bufferPool = bufferPool; this.endPoint = endPoint; @@ -65,10 +80,12 @@ public class FrameFlusher extends IteratingCallback this.generator = Objects.requireNonNull(generator); this.maxGather = maxGather; this.entries = new ArrayList<>(maxGather); + this.previousEntries = new ArrayList<>(maxGather); + this.failedEntries = new ArrayList<>(maxGather); this.buffers = new ArrayList<>((maxGather * 2) + 1); + this.timeoutScheduler = scheduler; } - /** * Enqueue a Frame to be written to the endpoint. * @param frame The frame to queue @@ -83,6 +100,8 @@ public class FrameFlusher extends IteratingCallback byte opCode = frame.getOpCode(); Throwable dead; + List failedEntries = null; + CloseStatus closeStatus = null; synchronized (this) { @@ -91,19 +110,36 @@ public class FrameFlusher extends IteratingCallback dead = closedCause; if (dead == null) { - if (opCode == OpCode.PING || opCode == OpCode.PONG) + switch (opCode) { - queue.offerFirst(entry); - } - else - { - queue.offerLast(entry); + case OpCode.CLOSE: + closeStatus = CloseStatus.getCloseStatus(frame); + if (!CloseStatus.isOrdinary(closeStatus)) + { + //fail all existing entries in the queue, and enqueue the error close + failedEntries = new ArrayList<>(queue); + queue.clear(); + } + queue.offerLast(entry); + this.canEnqueue = false; + break; + + case OpCode.PING: + case OpCode.PONG: + queue.offerFirst(entry); + break; + + default: + queue.offerLast(entry); + break; } - if (opCode == OpCode.CLOSE) - { - this.canEnqueue = false; - } + /* If the queue was empty then no timeout has been set, so we set a timeout to check the current + entry when it expires. When the timeout expires we will go over entries in the queue and + entries list to see if any of them have expired, it will then reset the timeout for the frame + with the soonest expiry time. */ + if ((idleTimeout > 0) && (queue.size()==1) && entries.isEmpty()) + timeoutScheduler.schedule(this::timeoutExpired, idleTimeout, TimeUnit.MILLISECONDS); } } else @@ -112,12 +148,26 @@ public class FrameFlusher extends IteratingCallback } } + if (failedEntries != null) + { + WebSocketException failure = new WebSocketException("Flusher received abnormal CloseFrame: " + CloseStatus.codeString(closeStatus.getCode())); + if (closeStatus instanceof AbnormalCloseStatus) + { + Throwable cause = ((AbnormalCloseStatus)closeStatus).getCause(); + failure.initCause(cause); + } + + for (Entry e : failedEntries) + { + notifyCallbackFailure(e.callback, failure); + } + } + if (dead == null) { if (LOG.isDebugEnabled()) - { LOG.debug("Enqueued {} to {}", entry, this); - } + return true; } @@ -147,9 +197,11 @@ public class FrameFlusher extends IteratingCallback if (closedCause != null) throw closedCause; - // Succeed entries from previous call to process - // and clear batchBuffer if we wrote it. - if (succeedEntries() && batchBuffer != null) + // Remember entries to succeed from previous process + previousEntries.addAll(entries); + entries.clear(); + + if (flushed && batchBuffer!=null) BufferUtil.clear(batchBuffer); while (!queue.isEmpty() && entries.size() <= maxGather) @@ -195,10 +247,7 @@ public class FrameFlusher extends IteratingCallback // Add the payload to the list of buffers ByteBuffer payload = entry.frame.getPayload(); if (BufferUtil.hasContent(payload)) - { buffers.add(payload); - break; - } } else { @@ -209,6 +258,8 @@ public class FrameFlusher extends IteratingCallback if (BufferUtil.hasContent(payload)) buffers.add(payload); } + + flushed = flush; } } @@ -220,10 +271,20 @@ public class FrameFlusher extends IteratingCallback BufferUtil.toDetailString(batchBuffer), entries); + // succeed previous entries + for (Entry entry : previousEntries) + { + if (entry.frame.getOpCode() == OpCode.CLOSE) + endPoint.shutdownOutput(); + notifyCallbackSuccess(entry.callback); + entry.release(); + } + previousEntries.clear(); + + // If we did not get any new entries go to IDLE state if (entries.isEmpty()) { releaseAggregate(); - succeedEntries(); return Action.IDLE; } @@ -248,7 +309,6 @@ public class FrameFlusher extends IteratingCallback } return Action.SCHEDULED; - } private int getQueueSize() @@ -259,26 +319,53 @@ public class FrameFlusher extends IteratingCallback } } - @Override - public void succeeded() + public void timeoutExpired() { - succeedEntries(); - super.succeeded(); - } - - private boolean succeedEntries() - { - boolean hadEntries = false; - for (Entry entry : entries) + boolean failed = false; + synchronized (FrameFlusher.this) { - hadEntries = true; - if (entry.frame.getOpCode() == OpCode.CLOSE) - endPoint.shutdownOutput(); - notifyCallbackSuccess(entry.callback); - entry.release(); + if (closedCause != null) + return; + + long currentTime = System.currentTimeMillis(); + long expiredIfCreatedBefore = currentTime - idleTimeout; + long earliestEntry = currentTime; + + /* Iterate through entries in both the queue and entries list. + If any entry has expired then we fail the FrameFlusher. + Otherwise we will try to schedule a new timeout. */ + Iterator iterator = TypeUtil.concat(entries.iterator(), queue.iterator()); + while (iterator.hasNext()) + { + Entry entry = iterator.next(); + + if (entry.getTimeOfCreation() <= expiredIfCreatedBefore) + { + LOG.warn("FrameFlusher write timeout on entry: {}", entry); + failed = true; + canEnqueue = false; + closedCause = new WebSocketWriteTimeoutException("FrameFlusher Write Timeout"); + failedEntries.addAll(entries); + failedEntries.addAll(queue); + entries.clear(); + queue.clear(); + break; + } + + if (entry.getTimeOfCreation() < earliestEntry) + earliestEntry = entry.getTimeOfCreation(); + } + + // if a timeout is set schedule a new timeout if we haven't failed and still have entries + if (!failed && idleTimeout>0 && !(entries.isEmpty() && queue.isEmpty())) + { + long nextTimeout = earliestEntry + idleTimeout - currentTime; + timeoutScheduler.schedule(this::timeoutExpired, nextTimeout, TimeUnit.MILLISECONDS); + } } - entries.clear(); - return hadEntries; + + if (failed) + this.iterate(); } @Override @@ -288,22 +375,26 @@ public class FrameFlusher extends IteratingCallback releaseAggregate(); synchronized (this) { - entries.addAll(queue); + failedEntries.addAll(queue); queue.clear(); + failedEntries.addAll(entries); + entries.clear(); + if (closedCause == null) closedCause = failure; else if (closedCause != failure) closedCause.addSuppressed(failure); } - for (Entry entry : entries) + for (Entry entry : failedEntries) { notifyCallbackFailure(entry.callback, failure); entry.release(); } - entries.clear(); - endPoint.close(failure); + + failedEntries.clear(); + endPoint.close(closedCause); } private void releaseAggregate() @@ -347,6 +438,16 @@ public class FrameFlusher extends IteratingCallback } } + public void setIdleTimeout(long idleTimeout) + { + this.idleTimeout = idleTimeout; + } + + public long getIdleTimeout() + { + return idleTimeout; + } + public long getMessagesOut() { return messagesOut.longValue(); @@ -369,6 +470,7 @@ public class FrameFlusher extends IteratingCallback private class Entry extends FrameEntry { private ByteBuffer headerBuffer; + private long timeOfCreation = System.currentTimeMillis(); private Entry(Frame frame, Callback callback, boolean batch) { @@ -396,6 +498,11 @@ public class FrameFlusher extends IteratingCallback } } + private long getTimeOfCreation() + { + return timeOfCreation; + } + @Override public String toString() { diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/ValidationExtension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/ValidationExtension.java index 11ab8bbeb3a..425bbcc9d34 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/ValidationExtension.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/ValidationExtension.java @@ -18,6 +18,8 @@ package org.eclipse.jetty.websocket.core.internal; +import java.util.Map; + import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.log.Log; @@ -28,8 +30,6 @@ import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.NullAppendable; import org.eclipse.jetty.websocket.core.ProtocolException; -import java.util.Map; - import static org.eclipse.jetty.websocket.core.OpCode.CONTINUATION; import static org.eclipse.jetty.websocket.core.OpCode.TEXT; import static org.eclipse.jetty.websocket.core.OpCode.UNDEFINED; @@ -62,7 +62,7 @@ public class ValidationExtension extends AbstractExtension incomingSequence.check(frame.getOpCode(), frame.isFin()); if (incomingFrameValidation) - getWebSocketChannel().assertValidIncoming(frame); + getWebSocketCoreSession().assertValidIncoming(frame); if (incomingUtf8Validation != null) validateUTF8(frame, incomingUtf8Validation, continuedInOpCode); @@ -85,7 +85,7 @@ public class ValidationExtension extends AbstractExtension outgoingSequence.check(frame.getOpCode(), frame.isFin()); if (outgoingFrameValidation) - getWebSocketChannel().assertValidOutgoing(frame); + getWebSocketCoreSession().assertValidOutgoing(frame); if (outgoingUtf8Validation != null) validateUTF8(frame, outgoingUtf8Validation, continuedOutOpCode); diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketConnection.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketConnection.java index ad48861a46b..c3b86eae260 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketConnection.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketConnection.java @@ -36,6 +36,7 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Scheduler; import org.eclipse.jetty.websocket.core.Behavior; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.MessageTooLargeException; @@ -47,7 +48,7 @@ import org.eclipse.jetty.websocket.core.WebSocketTimeoutException; */ public class WebSocketConnection extends AbstractConnection implements Connection.UpgradeTo, Dumpable, Runnable { - private final Logger LOG = Log.getLogger(this.getClass()); + private static final Logger LOG = Log.getLogger(WebSocketConnection.class); /** * Minimum size of a buffer is the determined to be what would be the maximum framing header size (not including payload) @@ -57,7 +58,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio private final ByteBufferPool bufferPool; private final Generator generator; private final Parser parser; - private final WebSocketChannel channel; + private final WebSocketCoreSession coreSession; private final Flusher flusher; private final Random random; @@ -79,10 +80,11 @@ public class WebSocketConnection extends AbstractConnection implements Connectio */ public WebSocketConnection(EndPoint endp, Executor executor, + Scheduler scheduler, ByteBufferPool bufferPool, - WebSocketChannel channel) + WebSocketCoreSession coreSession) { - this(endp, executor, bufferPool, channel, true); + this(endp, executor, scheduler, bufferPool, coreSession, true); } /** @@ -94,38 +96,39 @@ public class WebSocketConnection extends AbstractConnection implements Connectio */ public WebSocketConnection(EndPoint endp, Executor executor, + Scheduler scheduler, ByteBufferPool bufferPool, - WebSocketChannel channel, + WebSocketCoreSession coreSession, boolean validating) { super(endp, executor); Objects.requireNonNull(endp, "EndPoint"); - Objects.requireNonNull(channel, "Channel"); + Objects.requireNonNull(coreSession, "Session"); Objects.requireNonNull(executor, "Executor"); Objects.requireNonNull(bufferPool, "ByteBufferPool"); this.bufferPool = bufferPool; - this.channel = channel; + this.coreSession = coreSession; this.generator = new Generator(bufferPool); - this.parser = new Parser(bufferPool, channel.isAutoFragment()) + this.parser = new Parser(bufferPool, coreSession.isAutoFragment()) { @Override protected void checkFrameSize(byte opcode, int payloadLength) throws MessageTooLargeException, ProtocolException { super.checkFrameSize(opcode, payloadLength); - if (!channel.isAutoFragment() && channel.getMaxFrameSize() > 0 && payloadLength > channel.getMaxFrameSize()) - throw new MessageTooLargeException("Cannot handle payload lengths larger than " + channel.getMaxFrameSize()); + if (!coreSession.isAutoFragment() && coreSession.getMaxFrameSize() > 0 && payloadLength > coreSession.getMaxFrameSize()) + throw new MessageTooLargeException("Cannot handle payload lengths larger than " + coreSession.getMaxFrameSize()); } }; - this.flusher = new Flusher(channel.getOutputBufferSize(), generator, endp); - this.setInputBufferSize(channel.getInputBufferSize()); + this.flusher = new Flusher(scheduler, coreSession.getOutputBufferSize(), generator, endp); + this.setInputBufferSize(coreSession.getInputBufferSize()); - this.random = this.channel.getBehavior() == Behavior.CLIENT?new Random(endp.hashCode()):null; + this.random = this.coreSession.getBehavior() == Behavior.CLIENT?new Random(endp.hashCode()):null; } @Override @@ -170,8 +173,8 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (LOG.isDebugEnabled()) LOG.debug("onClose() of physical connection"); - if (!channel.isClosed()) - channel.onEof(); + if (!coreSession.isClosed()) + coreSession.onEof(); flusher.onClose(cause); super.onClose(cause); } @@ -184,7 +187,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio LOG.debug("onIdleExpired()"); // treat as a handler error because socket is still open - channel.processHandlerError(new WebSocketTimeoutException("Connection Idle Timeout"),Callback.NOOP); + coreSession.processHandlerError(new WebSocketTimeoutException("Connection Idle Timeout"),Callback.NOOP); return true; } @@ -200,7 +203,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio LOG.debug("onReadTimeout()"); // treat as a handler error because socket is still open - channel.processHandlerError(new WebSocketTimeoutException("Timeout on Read", timeout),Callback.NOOP); + coreSession.processHandlerError(new WebSocketTimeoutException("Timeout on Read", timeout),Callback.NOOP); return false; } @@ -213,7 +216,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (referenced != null) referenced.retain(); - channel.onFrame(frame, new Callback() + coreSession.onFrame(frame, new Callback() { @Override public void succeeded() @@ -225,7 +228,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (referenced != null) referenced.release(); - if (!channel.isDemanding()) + if (!coreSession.isDemanding()) demand(1); } @@ -240,7 +243,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio referenced.release(); // notify session & endpoint - channel.processHandlerError(cause,NOOP); + coreSession.processHandlerError(cause,NOOP); } }); } @@ -432,7 +435,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (filled < 0) { releaseNetworkBuffer(); - channel.onEof(); + coreSession.onEof(); return; } @@ -451,7 +454,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio LOG.warn(t.toString()); BufferUtil.clear(networkBuffer.getBuffer()); releaseNetworkBuffer(); - channel.processConnectionError(t,Callback.NOOP); + coreSession.processConnectionError(t,Callback.NOOP); } } @@ -491,9 +494,9 @@ public class WebSocketConnection extends AbstractConnection implements Connectio if (LOG.isDebugEnabled()) LOG.debug("onOpen() {}", this); - // Open Channel + // Open Session super.onOpen(); - channel.onOpen(); + coreSession.onOpen(); } @Override @@ -524,7 +527,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio return String.format("%s@%x[%s,p=%s,f=%s,g=%s]", getClass().getSimpleName(), hashCode(), - channel.getBehavior(), + coreSession.getBehavior(), parser, flusher, generator); @@ -546,6 +549,11 @@ public class WebSocketConnection extends AbstractConnection implements Connectio setInitialBuffer(prefilled); } + public FrameFlusher getFrameFlusher() + { + return flusher; + } + @Override public long getMessagesIn() { @@ -578,7 +586,7 @@ public class WebSocketConnection extends AbstractConnection implements Connectio */ void enqueueFrame(Frame frame, Callback callback, boolean batch) { - if (channel.getBehavior() == Behavior.CLIENT) + if (coreSession.getBehavior() == Behavior.CLIENT) { byte[] mask = new byte[4]; random.nextBytes(mask); @@ -591,16 +599,16 @@ public class WebSocketConnection extends AbstractConnection implements Connectio private class Flusher extends FrameFlusher { - private Flusher(int bufferSize, Generator generator, EndPoint endpoint) + private Flusher(Scheduler scheduler, int bufferSize, Generator generator, EndPoint endpoint) { - super(bufferPool, generator, endpoint, bufferSize, 8); + super(bufferPool, scheduler, generator, endpoint, bufferSize, 8); } @Override public void onCompleteFailure(Throwable x) { + coreSession.processConnectionError(x, NOOP); super.onCompleteFailure(x); - channel.processConnectionError(x, NOOP); } } } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketChannel.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketCoreSession.java similarity index 85% rename from jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketChannel.java rename to jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketCoreSession.java index 066fc6ce36f..a0917bf8652 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketChannel.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketCoreSession.java @@ -51,6 +51,7 @@ import org.eclipse.jetty.websocket.core.OutgoingFrames; import org.eclipse.jetty.websocket.core.ProtocolException; import org.eclipse.jetty.websocket.core.WebSocketConstants; import org.eclipse.jetty.websocket.core.WebSocketTimeoutException; +import org.eclipse.jetty.websocket.core.WebSocketWriteTimeoutException; import org.eclipse.jetty.websocket.core.internal.Parser.ParsedFrame; import static org.eclipse.jetty.util.Callback.NOOP; @@ -58,13 +59,13 @@ import static org.eclipse.jetty.util.Callback.NOOP; /** * The Core WebSocket Session. */ -public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSession, Dumpable +public class WebSocketCoreSession implements IncomingFrames, FrameHandler.CoreSession, Dumpable { - private Logger LOG = Log.getLogger(this.getClass()); + private static final Logger LOG = Log.getLogger(WebSocketCoreSession.class); private final static CloseStatus NO_CODE = new CloseStatus(CloseStatus.NO_CODE); private final Behavior behavior; - private final WebSocketChannelState channelState = new WebSocketChannelState(); + private final WebSocketSessionState sessionState = new WebSocketSessionState(); private final FrameHandler handler; private final Negotiated negotiated; private final boolean demanding; @@ -77,11 +78,12 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio private int outputBufferSize = WebSocketConstants.DEFAULT_OUTPUT_BUFFER_SIZE; private long maxBinaryMessageSize = WebSocketConstants.DEFAULT_MAX_BINARY_MESSAGE_SIZE; private long maxTextMessageSize = WebSocketConstants.DEFAULT_MAX_TEXT_MESSAGE_SIZE; - private Duration idleTimeout; + private Duration idleTimeout = WebSocketConstants.DEFAULT_IDLE_TIMEOUT; + private Duration writeTimeout = WebSocketConstants.DEFAULT_WRITE_TIMEOUT; - public WebSocketChannel(FrameHandler handler, - Behavior behavior, - Negotiated negotiated) + public WebSocketCoreSession(FrameHandler handler, + Behavior behavior, + Negotiated negotiated) { this.handler = handler; this.behavior = behavior; @@ -91,7 +93,7 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio } /** - * @return True if the channels handling is demanding. + * @return True if the sessions handling is demanding. */ public boolean isDemanding() { @@ -223,19 +225,29 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio @Override public Duration getIdleTimeout() { - if (getConnection() == null) - return idleTimeout; - else - return Duration.ofMillis(getConnection().getEndPoint().getIdleTimeout()); + return idleTimeout; } @Override public void setIdleTimeout(Duration timeout) { - if (getConnection() == null) - idleTimeout = timeout; - else - getConnection().getEndPoint().setIdleTimeout(timeout.toMillis()); + idleTimeout = timeout; + if (connection != null) + connection.getEndPoint().setIdleTimeout(timeout.toMillis()); + } + + @Override + public Duration getWriteTimeout() + { + return writeTimeout; + } + + @Override + public void setWriteTimeout(Duration timeout) + { + writeTimeout = timeout; + if (getConnection() != null) + getConnection().getFrameFlusher().setIdleTimeout(timeout.toMillis()); } public SocketAddress getLocalAddress() @@ -251,22 +263,19 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio @Override public boolean isOutputOpen() { - return channelState.isOutputOpen(); + return sessionState.isOutputOpen(); } public boolean isClosed() { - return channelState.isClosed(); + return sessionState.isClosed(); } public void setWebSocketConnection(WebSocketConnection connection) { + connection.getEndPoint().setIdleTimeout(idleTimeout.toMillis()); + connection.getFrameFlusher().setIdleTimeout(writeTimeout.toMillis()); this.connection = connection; - if (idleTimeout != null) - { - getConnection().getEndPoint().setIdleTimeout(idleTimeout.toMillis()); - idleTimeout = null; - } } /** @@ -306,12 +315,18 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio public void onEof() { - if (channelState.onEof()) - closeConnection(new ClosedChannelException(), channelState.getCloseStatus(), Callback.NOOP); + if (LOG.isDebugEnabled()) + LOG.debug("onEof() {}", this); + + if (sessionState.onEof()) + closeConnection(new ClosedChannelException(), sessionState.getCloseStatus(), Callback.NOOP); } public void closeConnection(Throwable cause, CloseStatus closeStatus, Callback callback) { + if (LOG.isDebugEnabled()) + LOG.debug("closeConnection() {} {} {}", closeStatus, this, cause); + connection.cancelDemand(); if (connection.getEndPoint().isOpen()) connection.close(); @@ -356,27 +371,6 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio callback.failed(e); } } - - - } - - AbnormalCloseStatus abnormalCloseStatusFor(Throwable cause) - { - int code; - if (cause instanceof ProtocolException) - code = CloseStatus.PROTOCOL; - else if (cause instanceof CloseException) - code = ((CloseException)cause).getStatusCode(); - else if (cause instanceof Utf8Appendable.NotUtf8Exception) - code = CloseStatus.BAD_PAYLOAD; - else if (cause instanceof WebSocketTimeoutException || cause instanceof TimeoutException || cause instanceof SocketTimeoutException) - code = CloseStatus.SHUTDOWN; - else if (behavior == Behavior.CLIENT) - code = CloseStatus.POLICY_VIOLATION; - else - code = CloseStatus.SERVER_ERROR; - - return new AbnormalCloseStatus(code, cause); } /** @@ -392,14 +386,26 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio if (LOG.isDebugEnabled()) LOG.debug("processConnectionError {} {}", this, cause); - CloseStatus closeStatus = abnormalCloseStatusFor(cause); - - if (closeStatus.getCode() == CloseStatus.PROTOCOL) - close(closeStatus, callback); - else if (channelState.onClosed(closeStatus)) - closeConnection(cause, closeStatus, callback); + int code; + if (cause instanceof CloseException) + code = ((CloseException)cause).getStatusCode(); + else if (cause instanceof Utf8Appendable.NotUtf8Exception) + code = CloseStatus.BAD_PAYLOAD; + else if (cause instanceof WebSocketWriteTimeoutException) + code = CloseStatus.NO_CLOSE; + else if (cause instanceof WebSocketTimeoutException || cause instanceof TimeoutException || cause instanceof SocketTimeoutException) + code = CloseStatus.SHUTDOWN; else - callback.failed(cause); + code = CloseStatus.NO_CLOSE; + + AbnormalCloseStatus closeStatus = new AbnormalCloseStatus(code, cause); + if (CloseStatus.isTransmittableStatusCode(code)) + close(closeStatus, callback); + else + { + if (sessionState.onClosed(closeStatus)) + closeConnection(cause, closeStatus, callback); + } } /** @@ -414,7 +420,19 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio if (LOG.isDebugEnabled()) LOG.debug("processHandlerError {} {}", this, cause); - close(abnormalCloseStatusFor(cause), callback); + int code; + if (cause instanceof CloseException) + code = ((CloseException)cause).getStatusCode(); + else if (cause instanceof Utf8Appendable.NotUtf8Exception) + code = CloseStatus.BAD_PAYLOAD; + else if (cause instanceof WebSocketTimeoutException || cause instanceof TimeoutException || cause instanceof SocketTimeoutException) + code = CloseStatus.SHUTDOWN; + else if (behavior == Behavior.CLIENT) + code = CloseStatus.POLICY_VIOLATION; + else + code = CloseStatus.SERVER_ERROR; + + close(new AbnormalCloseStatus(code, cause), callback); } /** @@ -426,13 +444,13 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio LOG.debug("onOpen() {}", this); // Upgrade success - channelState.onConnected(); + sessionState.onConnected(); if (LOG.isDebugEnabled()) LOG.debug("ConnectionState: Transition to CONNECTED"); Callback openCallback = Callback.from(()-> { - channelState.onOpen(); + sessionState.onOpen(); if (!demanding) connection.demand(1); if (LOG.isDebugEnabled()) @@ -466,7 +484,7 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio { if (!demanding) throw new IllegalStateException("FrameHandler is not demanding: " + this); - if (!channelState.isInputOpen()) + if (!sessionState.isInputOpen()) throw new IllegalStateException("FrameHandler input not open: " + this); // TODO Perhaps this is a NOOP? connection.demand(n); } @@ -484,6 +502,9 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio @Override public void onFrame(Frame frame, Callback callback) { + if (LOG.isDebugEnabled()) + LOG.debug("onFrame({})", frame); + try { assertValidIncoming(frame); @@ -506,6 +527,9 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio } catch (Throwable t) { + if (LOG.isDebugEnabled()) + LOG.warn("Invalid outgoing frame: {}", frame); + callback.failed(t); return; } @@ -514,17 +538,17 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio { synchronized(flusher) { - boolean closeConnection = channelState.onOutgoingFrame(frame); if (LOG.isDebugEnabled()) - LOG.debug("sendFrame({}, {}, {}) {}", frame, callback, batch, closeConnection); + LOG.debug("sendFrame({}, {}, {})", frame, callback, batch); + boolean closeConnection = sessionState.onOutgoingFrame(frame); if (closeConnection) { Throwable cause = AbnormalCloseStatus.getCause(CloseStatus.getCloseStatus(frame)); Callback closeConnectionCallback = Callback.from( - ()->closeConnection(cause, channelState.getCloseStatus(), callback), - t->closeConnection(cause, channelState.getCloseStatus(), Callback.from(callback, t))); + ()->closeConnection(cause, sessionState.getCloseStatus(), callback), + t->closeConnection(cause, sessionState.getCloseStatus(), Callback.from(callback, t))); flusher.queue.offer(new FrameEntry(frame, closeConnectionCallback, false)); } @@ -537,10 +561,13 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio } catch (Throwable t) { + if (LOG.isDebugEnabled()) + LOG.debug("Failed sendFrame()", t); + if (frame.getOpCode() == OpCode.CLOSE) { CloseStatus closeStatus = CloseStatus.getCloseStatus(frame); - if (closeStatus instanceof AbnormalCloseStatus && channelState.onClosed(closeStatus)) + if (closeStatus instanceof AbnormalCloseStatus && sessionState.onClosed(closeStatus)) closeConnection(AbnormalCloseStatus.getCause(closeStatus), closeStatus, Callback.from(callback, t)); else callback.failed(t); @@ -563,6 +590,9 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio @Override public void abort() { + if (LOG.isDebugEnabled()) + LOG.debug("abort(): {}", this); + connection.cancelDemand(); connection.getEndPoint().close(); } @@ -613,6 +643,8 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio public void setInputBufferSize(int inputBufferSize) { this.inputBufferSize = inputBufferSize; + if (connection != null) + connection.setInputBufferSize(inputBufferSize); } @Override @@ -647,10 +679,9 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio try { if (LOG.isDebugEnabled()) - LOG.debug("receiveFrame({}, {}) - connectionState={}, handler={}", - frame, callback, channelState, handler); + LOG.debug("receiveFrame({}, {}) - connectionState={}, handler={}", frame, callback, sessionState, handler); - boolean closeConnection = channelState.onIncomingFrame(frame); + boolean closeConnection = sessionState.onIncomingFrame(frame); // Handle inbound frame if (frame.getOpCode() != OpCode.CLOSE) @@ -665,13 +696,13 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio if (closeConnection) { - closeCallback = Callback.from(()-> closeConnection(null, channelState.getCloseStatus(), callback)); + closeCallback = Callback.from(()-> closeConnection(null, sessionState.getCloseStatus(), callback)); } else { closeCallback = Callback.from(()-> { - if (channelState.isOutputOpen()) + if (sessionState.isOutputOpen()) { CloseStatus closeStatus = CloseStatus.getCloseStatus(frame); if (LOG.isDebugEnabled()) @@ -765,10 +796,10 @@ public class WebSocketChannel implements IncomingFrames, FrameHandler.CoreSessio @Override public String toString() { - return String.format("WSChannel@%x{%s,%s,%s,af=%b,i/o=%d/%d,fs=%d}->%s", + return String.format("WSCoreSession@%x{%s,%s,%s,af=%b,i/o=%d/%d,fs=%d}->%s", hashCode(), behavior, - channelState, + sessionState, negotiated, autoFragment, inputBufferSize, diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketChannelState.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketSessionState.java similarity index 80% rename from jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketChannelState.java rename to jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketSessionState.java index 821f5c289fc..e1204170abb 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketChannelState.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/WebSocketSessionState.java @@ -26,7 +26,7 @@ import org.eclipse.jetty.websocket.core.ProtocolException; /** * Atomic Connection State */ -public class WebSocketChannelState +public class WebSocketSessionState { enum State { @@ -38,7 +38,7 @@ public class WebSocketChannelState CLOSED } - private State _channelState = State.CONNECTING; + private State _sessionState = State.CONNECTING; private byte _incomingContinuation = OpCode.UNDEFINED; private byte _outgoingContinuation = OpCode.UNDEFINED; CloseStatus _closeStatus = null; @@ -47,10 +47,10 @@ public class WebSocketChannelState { synchronized (this) { - if (_channelState != State.CONNECTING) - throw new IllegalStateException(_channelState.toString()); + if (_sessionState != State.CONNECTING) + throw new IllegalStateException(_sessionState.toString()); - _channelState = State.CONNECTED; + _sessionState = State.CONNECTED; } } @@ -58,10 +58,10 @@ public class WebSocketChannelState { synchronized (this) { - switch(_channelState) + switch(_sessionState) { case CONNECTED: - _channelState = State.OPEN; + _sessionState = State.OPEN; break; case OSHUT: @@ -70,7 +70,7 @@ public class WebSocketChannelState break; default: - throw new IllegalStateException(_channelState.toString()); + throw new IllegalStateException(_sessionState.toString()); } } } @@ -80,7 +80,7 @@ public class WebSocketChannelState { synchronized (this) { - return _channelState; + return _sessionState; } } @@ -113,11 +113,11 @@ public class WebSocketChannelState { synchronized (this) { - if (_channelState == State.CLOSED) + if (_sessionState == State.CLOSED) return false; _closeStatus = closeStatus; - _channelState = State.CLOSED; + _sessionState = State.CLOSED; return true; } } @@ -126,7 +126,7 @@ public class WebSocketChannelState { synchronized (this) { - switch (_channelState) + switch (_sessionState) { case CLOSED: case ISHUT: @@ -134,8 +134,8 @@ public class WebSocketChannelState default: if (_closeStatus == null || CloseStatus.isOrdinary(_closeStatus)) - _closeStatus = CloseStatus.NO_CLOSE_STATUS; - _channelState = State.CLOSED; + _closeStatus = new CloseStatus(CloseStatus.NO_CLOSE, "Session Closed"); + _sessionState = State.CLOSED; return true; } } @@ -149,30 +149,30 @@ public class WebSocketChannelState synchronized (this) { if (!isOutputOpen()) - throw new IllegalStateException(_channelState.toString()); + throw new IllegalStateException(_sessionState.toString()); if (opcode == OpCode.CLOSE) { _closeStatus = CloseStatus.getCloseStatus(frame); - if (_closeStatus instanceof WebSocketChannel.AbnormalCloseStatus) + if (_closeStatus instanceof WebSocketCoreSession.AbnormalCloseStatus) { - _channelState = State.CLOSED; + _sessionState = State.CLOSED; return true; } - switch (_channelState) + switch (_sessionState) { case CONNECTED: case OPEN: - _channelState = State.OSHUT; + _sessionState = State.OSHUT; return false; case ISHUT: - _channelState = State.CLOSED; + _sessionState = State.CLOSED; return true; default: - throw new IllegalStateException(_channelState.toString()); + throw new IllegalStateException(_sessionState.toString()); } } else if (frame.isDataFrame()) @@ -192,22 +192,22 @@ public class WebSocketChannelState synchronized (this) { if (!isInputOpen()) - throw new IllegalStateException(_channelState.toString()); + throw new IllegalStateException(_sessionState.toString()); if (opcode == OpCode.CLOSE) { _closeStatus = CloseStatus.getCloseStatus(frame); - switch (_channelState) + switch (_sessionState) { case OPEN: - _channelState = State.ISHUT; + _sessionState = State.ISHUT; return false; case OSHUT: - _channelState = State.CLOSED; + _sessionState = State.CLOSED; return true; default: - throw new IllegalStateException(_channelState.toString()); + throw new IllegalStateException(_sessionState.toString()); } } else if (frame.isDataFrame()) @@ -224,7 +224,7 @@ public class WebSocketChannelState public String toString() { return String.format("%s@%x{%s,i=%s,o=%s,c=%s}",getClass().getSimpleName(),hashCode(), - _channelState, + _sessionState, OpCode.name(_incomingContinuation), OpCode.name(_outgoingContinuation), _closeStatus); diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/CompressExtension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/CompressExtension.java index 16b9138830c..ff6e2657748 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/CompressExtension.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/CompressExtension.java @@ -37,7 +37,7 @@ import org.eclipse.jetty.websocket.core.AbstractExtension; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.internal.FrameEntry; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; public abstract class CompressExtension extends AbstractExtension { @@ -101,9 +101,9 @@ public abstract class CompressExtension extends AbstractExtension } @Override - public void setWebSocketChannel(WebSocketChannel webSocketChannel) + public void setWebSocketCoreSession(WebSocketCoreSession coreSession) { - super.setWebSocketChannel(webSocketChannel); + super.setWebSocketCoreSession(coreSession); } public Deflater getDeflater() diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/DeflateFrameExtension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/DeflateFrameExtension.java index b935c146162..275a59b66dc 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/DeflateFrameExtension.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/DeflateFrameExtension.java @@ -66,9 +66,9 @@ public class DeflateFrameExtension extends CompressExtension try { //TODO fix this to use long instead of int - if (getWebSocketChannel().getMaxFrameSize() > Integer.MAX_VALUE) + if (getWebSocketCoreSession().getMaxFrameSize() > Integer.MAX_VALUE) throw new IllegalArgumentException("maxFrameSize too large for ByteAccumulator"); - ByteAccumulator accumulator = new ByteAccumulator((int)getWebSocketChannel().getMaxFrameSize()); + ByteAccumulator accumulator = new ByteAccumulator((int)getWebSocketCoreSession().getMaxFrameSize()); decompress(accumulator, frame.getPayload()); decompress(accumulator, TAIL_BYTES_BUF.slice()); forwardIncoming(frame, callback, accumulator); diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/PerMessageDeflateExtension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/PerMessageDeflateExtension.java index e5f47588fc8..82e87543eec 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/PerMessageDeflateExtension.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/internal/compress/PerMessageDeflateExtension.java @@ -83,10 +83,10 @@ public class PerMessageDeflateExtension extends CompressExtension } //TODO fix this to use long instead of int - if (getWebSocketChannel().getMaxFrameSize() > Integer.MAX_VALUE) + if (getWebSocketCoreSession().getMaxFrameSize() > Integer.MAX_VALUE) throw new IllegalArgumentException("maxFrameSize too large for ByteAccumulator"); - ByteAccumulator accumulator = new ByteAccumulator((int)getWebSocketChannel().getMaxFrameSize()); + ByteAccumulator accumulator = new ByteAccumulator((int)getWebSocketCoreSession().getMaxFrameSize()); try { @@ -169,7 +169,7 @@ public class PerMessageDeflateExtension extends CompressExtension } case "server_no_context_takeover": { - params_negotiated.put("client_no_context_takeover", null); + params_negotiated.put("server_no_context_takeover", null); outgoingContextTakeover = false; break; } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/Negotiation.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/Negotiation.java index 22389e485d9..f139a376850 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/Negotiation.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/Negotiation.java @@ -18,25 +18,27 @@ package org.eclipse.jetty.websocket.core.server; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.QuotedCSV; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.DecoratedObjectFactory; +import org.eclipse.jetty.websocket.core.Behavior; import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; import org.eclipse.jetty.websocket.core.internal.ExtensionStack; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.ListIterator; -import java.util.Set; -import java.util.stream.Collectors; - public class Negotiation { private final Request baseRequest; @@ -55,13 +57,16 @@ public class Negotiation private String subprotocol; private ExtensionStack extensionStack; + /** + * @throws BadMessageException if there is any errors parsing the upgrade request + */ public Negotiation( Request baseRequest, HttpServletRequest request, HttpServletResponse response, WebSocketExtensionRegistry registry, DecoratedObjectFactory objectFactory, - ByteBufferPool bufferPool) + ByteBufferPool bufferPool) throws BadMessageException { this.baseRequest = baseRequest; this.request = request; @@ -77,65 +82,81 @@ public class Negotiation QuotedCSV extensions = null; QuotedCSV subprotocols = null; - for (HttpField field : baseRequest.getHttpFields()) + try { - if (field.getHeader() != null) + for (HttpField field : baseRequest.getHttpFields()) { - switch (field.getHeader()) + if (field.getHeader() != null) { - case UPGRADE: - if (upgrade == null && "websocket".equalsIgnoreCase(field.getValue())) - upgrade = Boolean.TRUE; - break; + switch (field.getHeader()) + { + case UPGRADE: + if (upgrade == null && "websocket".equalsIgnoreCase(field.getValue())) + upgrade = Boolean.TRUE; + break; - case CONNECTION: - if (connectionCSVs == null) - connectionCSVs = new QuotedCSV(); - connectionCSVs.addValue(field.getValue()); - break; + case CONNECTION: + if (connectionCSVs == null) + connectionCSVs = new QuotedCSV(); + connectionCSVs.addValue(field.getValue()); + break; - case SEC_WEBSOCKET_KEY: - key = field.getValue(); - break; + case SEC_WEBSOCKET_KEY: + key = field.getValue(); + break; - case SEC_WEBSOCKET_VERSION: - version = field.getValue(); - break; + case SEC_WEBSOCKET_VERSION: + version = field.getValue(); + break; - case SEC_WEBSOCKET_EXTENSIONS: - if (extensions == null) - extensions = new QuotedCSV(field.getValue()); - else - extensions.addValue(field.getValue()); - break; + case SEC_WEBSOCKET_EXTENSIONS: + if (extensions == null) + extensions = new QuotedCSV(field.getValue()); + else + extensions.addValue(field.getValue()); + break; - case SEC_WEBSOCKET_SUBPROTOCOL: - if (subprotocols == null) - subprotocols = new QuotedCSV(field.getValue()); - else - subprotocols.addValue(field.getValue()); - break; + case SEC_WEBSOCKET_SUBPROTOCOL: + if (subprotocols == null) + subprotocols = new QuotedCSV(field.getValue()); + else + subprotocols.addValue(field.getValue()); + break; - default: + default: + } } } + + this.version = version; + this.key = key; + this.upgrade = upgrade != null && connectionCSVs != null && connectionCSVs.getValues().stream().anyMatch(s -> s.equalsIgnoreCase("Upgrade")); + + Set available = registry.getAvailableExtensionNames(); + offeredExtensions = extensions == null + ? Collections.emptyList() + : extensions.getValues().stream() + .map(ExtensionConfig::parse) + .filter(ec -> available.contains(ec.getName().toLowerCase()) && !ec.getName().startsWith("@")) + .collect(Collectors.toList()); + + offeredSubprotocols = subprotocols == null + ? Collections.emptyList() + : subprotocols.getValues(); + + negotiatedExtensions = new ArrayList<>(); + for (ExtensionConfig config : offeredExtensions) + { + long matches = negotiatedExtensions.stream() + .filter(negotiatedConfig -> negotiatedConfig.getName().equals(config.getName())).count(); + if (matches == 0) + negotiatedExtensions.add(config); + } + } + catch (Throwable t) + { + throw new BadMessageException("Invalid Handshake Request", t); } - - this.version = version; - this.key = key; - this.upgrade = upgrade != null && connectionCSVs != null && connectionCSVs.getValues().stream().anyMatch(s -> s.equalsIgnoreCase("Upgrade")); - - Set available = registry.getAvailableExtensionNames(); - offeredExtensions = extensions == null - ?Collections.emptyList() - :extensions.getValues().stream() - .map(ExtensionConfig::parse) - .filter(ec -> available.contains(ec.getName().toLowerCase())) - .collect(Collectors.toList()); - - offeredSubprotocols = subprotocols == null - ?Collections.emptyList() - :subprotocols.getValues(); } public String getKey() @@ -158,8 +179,6 @@ public class Negotiation public List getNegotiatedExtensions() { - if (negotiatedExtensions == null) - return offeredExtensions; return negotiatedExtensions; } @@ -208,51 +227,18 @@ public class Negotiation { if (extensionStack == null) { - extensionStack = new ExtensionStack(registry); - boolean configsFromApplication = true; - - if (negotiatedExtensions == null) - { - // Has the header been set directly? - List extensions = baseRequest.getResponse().getHttpFields() - .getCSV(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, true); - - if (extensions.isEmpty()) - { - // If the negotiatedExtensions has not been set, just use the offered extensions - negotiatedExtensions = new ArrayList(offeredExtensions); - configsFromApplication = false; - } - else - { - negotiatedExtensions = extensions - .stream() - .map(ExtensionConfig::parse) - .collect(Collectors.toList()); - } - } - - if (configsFromApplication) - { - // TODO is this really necessary? - // Replace any configuration in the negotiated extensions with the offered extensions config - for (ListIterator i = negotiatedExtensions.listIterator(); i.hasNext(); ) - { - ExtensionConfig config = i.next(); - offeredExtensions.stream().filter(c -> c.getName().equalsIgnoreCase(config.getName())) - .findFirst() - .ifPresent(i::set); - } - } - - extensionStack.negotiate(objectFactory, bufferPool, negotiatedExtensions); + // Extension stack can decide to drop any of these extensions or their parameters + extensionStack = new ExtensionStack(registry, Behavior.SERVER); + extensionStack.negotiate(objectFactory, bufferPool, offeredExtensions, negotiatedExtensions); negotiatedExtensions = extensionStack.getNegotiatedExtensions(); + if (extensionStack.hasNegotiatedExtensions()) baseRequest.getResponse().setHeader(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, ExtensionConfig.toHeaderValue(negotiatedExtensions)); else baseRequest.getResponse().setHeader(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, null); } + return extensionStack; } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketNegotiator.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketNegotiator.java index 65272a26112..d7154306fae 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketNegotiator.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/WebSocketNegotiator.java @@ -18,14 +18,14 @@ package org.eclipse.jetty.websocket.core.server; +import java.io.IOException; +import java.util.function.Function; + import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; -import java.io.IOException; -import java.util.function.Function; - public interface WebSocketNegotiator extends FrameHandler.Customizer { FrameHandler negotiate(Negotiation negotiation) throws IOException; @@ -102,10 +102,10 @@ public interface WebSocketNegotiator extends FrameHandler.Customizer } @Override - public void customize(FrameHandler.CoreSession session) + public void customize(FrameHandler.Configuration configurable) { if (customizer != null) - customizer.customize(session); + customizer.customize(configurable); } @Override diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java index 43437645fa0..74e606c6964 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/server/internal/RFC6455Handshaker.java @@ -24,6 +24,7 @@ import java.util.concurrent.Executor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; @@ -42,13 +43,17 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Scheduler; import org.eclipse.jetty.websocket.core.Behavior; +import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.WebSocketConstants; +import org.eclipse.jetty.websocket.core.WebSocketException; +import org.eclipse.jetty.websocket.core.internal.ExtensionStack; import org.eclipse.jetty.websocket.core.internal.Negotiated; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; import org.eclipse.jetty.websocket.core.internal.WebSocketConnection; import org.eclipse.jetty.websocket.core.internal.WebSocketCore; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.eclipse.jetty.websocket.core.server.Handshaker; import org.eclipse.jetty.websocket.core.server.Negotiation; import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; @@ -60,9 +65,8 @@ public final class RFC6455Handshaker implements Handshaker private static final HttpField CONNECTION_UPGRADE = new PreEncodedHttpField(HttpHeader.CONNECTION, HttpHeader.UPGRADE.asString()); private static final HttpField SERVER_VERSION = new PreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION); - public boolean upgradeRequest(WebSocketNegotiator negotiator, HttpServletRequest request, - HttpServletResponse response, - FrameHandler.Customizer defaultCustomizer) throws IOException + public boolean upgradeRequest(WebSocketNegotiator negotiator, HttpServletRequest request, HttpServletResponse response, + FrameHandler.Customizer defaultCustomizer) throws IOException { Request baseRequest = Request.getBaseRequest(request); HttpChannel httpChannel = baseRequest.getHttpChannel(); @@ -118,11 +122,7 @@ public final class RFC6455Handshaker implements Handshaker } if (negotiation.getKey() == null) - { - if (LOG.isDebugEnabled()) - LOG.debug("not upgraded no key {}", baseRequest); - return false; - } + throw new BadMessageException("Missing request header 'Sec-WebSocket-Key'"); // Negotiate the FrameHandler FrameHandler handler = negotiator.negotiate(negotiation); @@ -149,60 +149,69 @@ public final class RFC6455Handshaker implements Handshaker // Check for handler if (handler == null) { - LOG.warn("not upgraded: no frame handler provided {}", baseRequest); + if (LOG.isDebugEnabled()) + LOG.debug("not upgraded: no frame handler provided {}", baseRequest); return false; } - // Check if subprotocol negotiated + // validate negotiated subprotocol String subprotocol = negotiation.getSubprotocol(); - if (negotiation.getOfferedSubprotocols().size() > 0) + if (subprotocol != null) { - if (subprotocol == null) - { - // TODO: this message needs to be returned to Http Client - LOG.warn("not upgraded: no subprotocol selected from offered subprotocols {}: {}", negotiation.getOfferedSubprotocols(), baseRequest); - return false; - } - if (!negotiation.getOfferedSubprotocols().contains(subprotocol)) - { - // TODO: this message needs to be returned to Http Client - LOG.warn("not upgraded: selected subprotocol {} not present in offered subprotocols {}: {}", subprotocol, negotiation.getOfferedSubprotocols(), - baseRequest); - return false; - } + throw new WebSocketException("not upgraded: selected a subprotocol not present in offered subprotocols"); } + else + { + if (!negotiation.getOfferedSubprotocols().isEmpty()) + throw new WebSocketException("not upgraded: no subprotocol selected from offered subprotocols"); + } + + // validate negotiated extensions + for (ExtensionConfig config : negotiation.getNegotiatedExtensions()) + { + if (config.getName().startsWith("@")) + continue; + + long matches = negotiation.getOfferedExtensions().stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); + if (matches < 1) + throw new WebSocketException("Upgrade failed: negotiated extension not requested"); + + matches = negotiation.getNegotiatedExtensions().stream().filter(c -> config.getName().equalsIgnoreCase(c.getName())).count(); + if (matches > 1) + throw new WebSocketException("Upgrade failed: multiple negotiated extensions of the same name"); + } + + // Create and Negotiate the ExtensionStack + ExtensionStack extensionStack = negotiation.getExtensionStack(); Negotiated negotiated = new Negotiated( baseRequest.getHttpURI().toURI(), subprotocol, baseRequest.isSecure(), - negotiation.getExtensionStack(), + extensionStack, WebSocketConstants.SPEC_VERSION_STRING); - // Create the Channel - WebSocketChannel channel = newWebSocketChannel(handler, negotiated); + // Create the Session + WebSocketCoreSession coreSession = newWebSocketCoreSession(handler, negotiated); if (defaultCustomizer!=null) - defaultCustomizer.customize(channel); - negotiator.customize(channel); + defaultCustomizer.customize(coreSession); + negotiator.customize(coreSession); if (LOG.isDebugEnabled()) - LOG.debug("channel {}", channel); + LOG.debug("session {}", coreSession); // Create a connection - WebSocketConnection connection = newWebSocketConnection(httpChannel.getEndPoint(), connector.getExecutor(), connector.getByteBufferPool(), channel); + WebSocketConnection connection = newWebSocketConnection(httpChannel.getEndPoint(), connector.getExecutor(), connector.getScheduler(), connector.getByteBufferPool(), coreSession); if (LOG.isDebugEnabled()) LOG.debug("connection {}", connection); if (connection == null) - { - LOG.warn("not upgraded: no connection {}", baseRequest); - return false; - } + throw new WebSocketException("not upgraded: no connection"); for (Connection.Listener listener : connector.getBeans(Connection.Listener.class)) connection.addListener(listener); - channel.setWebSocketConnection(connection); + coreSession.setWebSocketConnection(connection); // send upgrade response Response baseResponse = baseRequest.getResponse(); @@ -222,20 +231,20 @@ public final class RFC6455Handshaker implements Handshaker // upgrade if (LOG.isDebugEnabled()) - LOG.debug("upgrade connection={} session={}", connection, channel); + LOG.debug("upgrade connection={} session={}", connection, coreSession); baseRequest.setAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, connection); return true; } - protected WebSocketChannel newWebSocketChannel(FrameHandler handler, Negotiated negotiated) + protected WebSocketCoreSession newWebSocketCoreSession(FrameHandler handler, Negotiated negotiated) { - return new WebSocketChannel(handler, Behavior.SERVER, negotiated); + return new WebSocketCoreSession(handler, Behavior.SERVER, negotiated); } - protected WebSocketConnection newWebSocketConnection(EndPoint endPoint, Executor executor, ByteBufferPool byteBufferPool, WebSocketChannel wsChannel) + protected WebSocketConnection newWebSocketConnection(EndPoint endPoint, Executor executor, Scheduler scheduler, ByteBufferPool byteBufferPool, WebSocketCoreSession coreSession) { - return new WebSocketConnection(endPoint, executor, byteBufferPool, wsChannel); + return new WebSocketConnection(endPoint, executor, scheduler, byteBufferPool, coreSession); } private boolean getSendServerVersion(Connector connector) diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/AbstractTestFrameHandler.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/AbstractTestFrameHandler.java index b8d53db1e31..cb3bdecc797 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/AbstractTestFrameHandler.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/AbstractTestFrameHandler.java @@ -41,17 +41,17 @@ public class AbstractTestFrameHandler implements SynchronousFrameHandler private byte partial = OpCode.UNDEFINED; private Utf8StringBuilder utf8; private ByteBuffer byteBuffer; - private FrameHandler.CoreSession channel; + private FrameHandler.CoreSession coreSession; public FrameHandler.CoreSession getCoreSession() { - return channel; + return coreSession; } @Override public void onOpen(CoreSession coreSession) { - this.channel = coreSession; + this.coreSession = coreSession; onOpen(); } @@ -121,7 +121,7 @@ public class AbstractTestFrameHandler implements SynchronousFrameHandler try { - channel.sendFrame(new Frame(PONG).setPayload(pongBuf), callback, false); + coreSession.sendFrame(new Frame(PONG).setPayload(pongBuf), callback, false); } catch (Throwable t) { @@ -338,7 +338,7 @@ public class AbstractTestFrameHandler implements SynchronousFrameHandler } if (respond > 0) - channel.close(respond, reason, callback); + coreSession.close(respond, reason, callback); else callback.succeeded(); } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/GeneratorFrameFlagsTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/GeneratorFrameFlagsTest.java index bcc57892434..74d5dbf65de 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/GeneratorFrameFlagsTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/GeneratorFrameFlagsTest.java @@ -18,21 +18,21 @@ package org.eclipse.jetty.websocket.core; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.stream.Stream; + import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.websocket.core.internal.ExtensionStack; import org.eclipse.jetty.websocket.core.internal.Generator; import org.eclipse.jetty.websocket.core.internal.Negotiated; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.stream.Stream; - import static org.junit.jupiter.api.Assertions.assertThrows; /** @@ -41,7 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class GeneratorFrameFlagsTest { private static ByteBufferPool bufferPool = new MappedByteBufferPool(); - private WebSocketChannel channel; + private WebSocketCoreSession coreSession; public static Stream data() { @@ -63,9 +63,9 @@ public class GeneratorFrameFlagsTest public void setup(Frame invalidFrame) { - ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry()); - exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>()); - this.channel = new WebSocketChannel(new AbstractTestFrameHandler(), Behavior.CLIENT, Negotiated.from(exStack)); + ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry(), Behavior.SERVER); + exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>(), new LinkedList<>()); + this.coreSession = new WebSocketCoreSession(new AbstractTestFrameHandler(), Behavior.CLIENT, Negotiated.from(exStack)); } @ParameterizedTest @@ -76,6 +76,6 @@ public class GeneratorFrameFlagsTest ByteBuffer buffer = ByteBuffer.allocate(100); new Generator(bufferPool).generateWholeFrame(invalidFrame, buffer); - assertThrows(ProtocolException.class, () -> channel.assertValidOutgoing(invalidFrame)); + assertThrows(ProtocolException.class, () -> coreSession.assertValidOutgoing(invalidFrame)); } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/GeneratorTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/GeneratorTest.java index 052d650c02c..83d494daa55 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/GeneratorTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/GeneratorTest.java @@ -37,7 +37,7 @@ import org.eclipse.jetty.websocket.core.internal.ExtensionStack; import org.eclipse.jetty.websocket.core.internal.Generator; import org.eclipse.jetty.websocket.core.internal.Negotiated; import org.eclipse.jetty.websocket.core.internal.Parser; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; @@ -50,14 +50,14 @@ public class GeneratorTest private static final Logger LOG = Log.getLogger(Helper.class); private static Generator generator = new Generator(new MappedByteBufferPool()); - private static WebSocketChannel channel = newChannel(Behavior.SERVER); + private static WebSocketCoreSession coreSession = newWebSocketCoreSession(Behavior.SERVER); - private static WebSocketChannel newChannel(Behavior behavior) + private static WebSocketCoreSession newWebSocketCoreSession(Behavior behavior) { ByteBufferPool bufferPool = new MappedByteBufferPool(); - ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry()); - exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>()); - return new WebSocketChannel(new AbstractTestFrameHandler(), behavior, Negotiated.from(exStack)); + ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry(), Behavior.SERVER); + exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>(), new LinkedList<>()); + return new WebSocketCoreSession(new AbstractTestFrameHandler(), behavior, Negotiated.from(exStack)); } /** @@ -410,7 +410,7 @@ public class GeneratorTest BufferUtil.flipToFlush(bb, 0); closeFrame.setPayload(bb); - assertThrows(ProtocolException.class, () -> channel.assertValidOutgoing(closeFrame)); + assertThrows(ProtocolException.class, () -> coreSession.assertValidOutgoing(closeFrame)); } /** @@ -651,7 +651,7 @@ public class GeneratorTest Frame pingFrame = new Frame(OpCode.PING); pingFrame.setPayload(ByteBuffer.wrap(bytes)); - assertThrows(WebSocketException.class, () -> channel.assertValidOutgoing(pingFrame)); + assertThrows(WebSocketException.class, () -> coreSession.assertValidOutgoing(pingFrame)); } /** @@ -665,7 +665,7 @@ public class GeneratorTest Frame pongFrame = new Frame(OpCode.PONG); pongFrame.setPayload(ByteBuffer.wrap(bytes)); - assertThrows(WebSocketException.class, () -> channel.assertValidOutgoing(pongFrame)); + assertThrows(WebSocketException.class, () -> coreSession.assertValidOutgoing(pongFrame)); } /** diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/MessageHandlerTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/MessageHandlerTest.java index a8b002ae6bb..b947c9590e5 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/MessageHandlerTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/MessageHandlerTest.java @@ -50,7 +50,7 @@ public class MessageHandlerTest boolean demanding; int demand; - CoreSession session; + CoreSession coreSession; List textMessages = new ArrayList<>(); List binaryMessages = new ArrayList<>(); List callbacks = new ArrayList<>(); @@ -63,7 +63,7 @@ public class MessageHandlerTest demanding = false; demand = 0; - session = new CoreSession.Empty() + coreSession = new CoreSession.Empty() { private ByteBufferPool byteBufferPool = new MappedByteBufferPool(); @@ -110,7 +110,7 @@ public class MessageHandlerTest } }; - handler.onOpen(session, NOOP); + handler.onOpen(coreSession, NOOP); } @Test @@ -349,7 +349,7 @@ public class MessageHandlerTest FutureCallback callback; handler.setMaxTextMessageSize(4); - handler.onOpen(session, NOOP); + handler.onOpen(coreSession, NOOP); callback = new FutureCallback(); handler.onFrame(new Frame(OpCode.TEXT, true, "Testing"), callback); @@ -368,7 +368,7 @@ public class MessageHandlerTest FutureCallback callback; handler.setMaxTextMessageSize(4); - handler.onOpen(session, NOOP); + handler.onOpen(coreSession, NOOP); callback = new FutureCallback(); handler.onFrame(new Frame(OpCode.TEXT, false, "123"), callback); @@ -569,7 +569,7 @@ public class MessageHandlerTest FutureCallback callback; handler.setMaxBinaryMessageSize(4); - handler.onOpen(session, NOOP); + handler.onOpen(coreSession, NOOP); callback = new FutureCallback(); handler.onFrame(new Frame(OpCode.BINARY, true, "Testing"), callback); @@ -588,7 +588,7 @@ public class MessageHandlerTest FutureCallback callback; handler.setMaxBinaryMessageSize(4); - handler.onOpen(session, NOOP); + handler.onOpen(coreSession, NOOP); callback = new FutureCallback(); handler.onFrame(new Frame(OpCode.BINARY, false, "123"), callback); @@ -652,7 +652,7 @@ public class MessageHandlerTest } }; - handler.onOpen(session, NOOP); + handler.onOpen(coreSession, NOOP); FutureCallback callback; @@ -680,7 +680,7 @@ public class MessageHandlerTest } }; - handler.onOpen(session, NOOP); + handler.onOpen(coreSession, NOOP); FutureCallback callback; diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserCapture.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserCapture.java index 9339bdb469f..dd69132ca64 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserCapture.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/ParserCapture.java @@ -29,15 +29,15 @@ import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.websocket.core.internal.ExtensionStack; import org.eclipse.jetty.websocket.core.internal.Negotiated; import org.eclipse.jetty.websocket.core.internal.Parser; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class ParserCapture { private final Parser parser; - private final WebSocketChannel channel; + private final WebSocketCoreSession coreSession; public BlockingQueue framesQueue = new LinkedBlockingDeque<>(); public boolean closed = false; public boolean copy; @@ -58,9 +58,9 @@ public class ParserCapture this.copy = copy; ByteBufferPool bufferPool = new MappedByteBufferPool(); - ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry()); - exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>()); - this.channel = new WebSocketChannel(new AbstractTestFrameHandler(), behavior, Negotiated.from(exStack)); + ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry(), Behavior.SERVER); + exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>(), new LinkedList<>()); + this.coreSession = new WebSocketCoreSession(new AbstractTestFrameHandler(), behavior, Negotiated.from(exStack)); } public void parse(ByteBuffer buffer) @@ -71,7 +71,7 @@ public class ParserCapture if (frame == null) break; - channel.assertValidIncoming(frame); + coreSession.assertValidIncoming(frame); if (!onFrame(frame)) break; diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java index 3c2fb899dc8..6f8b70202b3 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestFrameHandler.java @@ -31,7 +31,7 @@ public class TestFrameHandler implements SynchronousFrameHandler { private static Logger LOG = Log.getLogger(TestFrameHandler.class); - protected CoreSession session; + protected CoreSession coreSession; public BlockingQueue receivedFrames = new BlockingArrayQueue<>(); protected Throwable failure; @@ -41,7 +41,7 @@ public class TestFrameHandler implements SynchronousFrameHandler public CoreSession getCoreSession() { - return session; + return coreSession; } public BlockingQueue getFrames() @@ -58,7 +58,7 @@ public class TestFrameHandler implements SynchronousFrameHandler public void onOpen(CoreSession coreSession) { LOG.info("onOpen {}", coreSession); - this.session = coreSession; + this.coreSession = coreSession; open.countDown(); } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestWebSocketNegotiator.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestWebSocketNegotiator.java index f7a29a3a453..1c170fec6f2 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestWebSocketNegotiator.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/TestWebSocketNegotiator.java @@ -21,7 +21,6 @@ package org.eclipse.jetty.websocket.core; import java.io.IOException; import java.util.List; -import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.util.DecoratedObjectFactory; @@ -44,7 +43,7 @@ public class TestWebSocketNegotiator implements WebSocketNegotiator } public TestWebSocketNegotiator(DecoratedObjectFactory objectFactory, WebSocketExtensionRegistry extensionRegistry, ByteBufferPool bufferPool, - FrameHandler frameHandler) + FrameHandler frameHandler) { this.objectFactory = objectFactory; this.extensionRegistry = extensionRegistry; @@ -56,18 +55,14 @@ public class TestWebSocketNegotiator implements WebSocketNegotiator public FrameHandler negotiate(Negotiation negotiation) throws IOException { List offeredSubprotocols = negotiation.getOfferedSubprotocols(); - if (!offeredSubprotocols.contains("test")) - return null; - negotiation.setSubprotocol("test"); + if (!offeredSubprotocols.isEmpty()) + negotiation.setSubprotocol(offeredSubprotocols.get(0)); - // TODO better to call negotiation.setNegotiatedExtensions(); - negotiation.getResponse().addHeader(HttpHeader.SEC_WEBSOCKET_EXTENSIONS.asString(), - "@validation; outgoing-sequence; incoming-sequence; outgoing-frame; incoming-frame; incoming-utf8; outgoing-utf8"); return frameHandler; } @Override - public void customize(FrameHandler.CoreSession session) + public void customize(FrameHandler.Configuration configurable) { } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketCloseTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketCloseTest.java index 827b1d3a192..a20d67be8fc 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketCloseTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketCloseTest.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.websocket.core; import java.net.Socket; +import java.time.Duration; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -37,7 +38,7 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; import org.eclipse.jetty.websocket.core.server.WebSocketUpgradeHandler; import org.eclipse.jetty.websocket.core.server.internal.RFC6455Handshaker; @@ -52,6 +53,7 @@ import static org.hamcrest.Matchers.is; 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.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -231,7 +233,7 @@ public class WebSocketCloseTest extends WebSocketTester @ValueSource(strings = {WS_SCHEME, WSS_SCHEME}) public void clientCloseServerFailClose_OSHUT(String scheme) throws Exception { - try (StacklessLogging stackless = new StacklessLogging(WebSocketChannel.class)) + try (StacklessLogging stackless = new StacklessLogging(WebSocketCoreSession.class)) { setup(State.OSHUT, scheme); server.handler.getCoreSession().demand(1); @@ -315,21 +317,22 @@ public class WebSocketCloseTest extends WebSocketTester client.close(); assertFalse(server.handler.closed.await(250, TimeUnit.MILLISECONDS)); - while(true) - { - if (!server.isOpen()) - break; - - server.sendFrame(new Frame(OpCode.TEXT, BufferUtil.toBuffer("frame after close")), Callback.NOOP); - } + assertTimeoutPreemptively(Duration.ofSeconds(1), ()->{ + while(true) + { + if (!server.isOpen()) + break; + server.sendFrame(new Frame(OpCode.TEXT, BufferUtil.toBuffer("frame after close")), Callback.NOOP); + Thread.sleep(100); + } + }); assertTrue(server.handler.closed.await(5, TimeUnit.SECONDS)); assertNotNull(server.handler.error); - assertThat(server.handler.closeStatus.getCode(), is(CloseStatus.SERVER_ERROR)); + assertThat(server.handler.closeStatus.getCode(), is(CloseStatus.NO_CLOSE)); Callback callback = server.handler.receivedCallback.poll(5, TimeUnit.SECONDS); callback.succeeded(); - assertThat(server.handler.closeStatus.getCode(), is(CloseStatus.SERVER_ERROR)); } @ParameterizedTest @@ -379,7 +382,7 @@ public class WebSocketCloseTest extends WebSocketTester client.getOutputStream().write(RawFrameBuilder.buildFrame(OpCode.BINARY, "binary", true)); - try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketChannel.class)) + try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketCoreSession.class)) { server.handler.getCoreSession().demand(1); assertTrue(server.handler.closed.await(5, TimeUnit.SECONDS)); @@ -397,7 +400,7 @@ public class WebSocketCloseTest extends WebSocketTester client.getOutputStream().write(RawFrameBuilder.buildFrame(OpCode.BINARY, "binary", true)); - try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketChannel.class)) + try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketCoreSession.class)) { server.handler.getCoreSession().demand(1); assertTrue(server.handler.closed.await(5, TimeUnit.SECONDS)); @@ -410,7 +413,7 @@ public class WebSocketCloseTest extends WebSocketTester static class DemandingTestFrameHandler implements SynchronousFrameHandler { - private CoreSession session; + private CoreSession coreSession; String state; protected BlockingQueue receivedFrames = new BlockingArrayQueue<>(); @@ -422,7 +425,7 @@ public class WebSocketCloseTest extends WebSocketTester public CoreSession getCoreSession() { - return session; + return coreSession; } public BlockingQueue getFrames() @@ -433,17 +436,17 @@ public class WebSocketCloseTest extends WebSocketTester @Override public void onOpen(CoreSession coreSession) { - LOG.info("onOpen {}", coreSession); - session = coreSession; - state = session.toString(); + LOG.debug("onOpen {}", coreSession); + this.coreSession = coreSession; + state = this.coreSession.toString(); opened.countDown(); } @Override public void onFrame(Frame frame, Callback callback) { - LOG.info("onFrame: " + BufferUtil.toDetailString(frame.getPayload())); - state = session.toString(); + LOG.debug("onFrame: " + BufferUtil.toDetailString(frame.getPayload())); + state = coreSession.toString(); receivedCallback.offer(callback); receivedFrames.offer(Frame.copy(frame)); @@ -454,8 +457,8 @@ public class WebSocketCloseTest extends WebSocketTester @Override public void onClosed(CloseStatus closeStatus) { - LOG.info("onClosed {}", closeStatus); - state = session.toString(); + LOG.debug("onClosed {}", closeStatus); + state = coreSession.toString(); this.closeStatus = closeStatus; closed.countDown(); } @@ -463,9 +466,9 @@ public class WebSocketCloseTest extends WebSocketTester @Override public void onError(Throwable cause) { - LOG.info("onError {} ", cause == null?null:cause.toString()); + LOG.debug("onError {} ", cause); error = cause; - state = session.toString(); + state = coreSession.toString(); } @Override @@ -481,7 +484,7 @@ public class WebSocketCloseTest extends WebSocketTester frame.setPayload(text); getCoreSession().sendFrame(frame, NOOP, false); - state = session.toString(); + state = coreSession.toString(); } } @@ -506,9 +509,9 @@ public class WebSocketCloseTest extends WebSocketTester return server.getBean(NetworkConnector.class).getLocalPort(); } - private SslContextFactory createSslContextFactory() + private SslContextFactory.Server createServerSslContextFactory() { - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); return sslContextFactory; @@ -522,7 +525,7 @@ public class WebSocketCloseTest extends WebSocketTester ServerConnector connector; if (tls) - connector = new ServerConnector(server, createSslContextFactory()); + connector = new ServerConnector(server, createServerSslContextFactory()); else connector = new ServerConnector(server); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java new file mode 100644 index 00000000000..3068a0b8f26 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java @@ -0,0 +1,339 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.HttpResponse; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.StacklessLogging; +import org.eclipse.jetty.websocket.core.FrameHandler.CoreSession; +import org.eclipse.jetty.websocket.core.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.core.client.UpgradeListener; +import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; +import org.eclipse.jetty.websocket.core.server.Negotiation; +import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class WebSocketNegotiationTest extends WebSocketTester +{ + public static class EchoFrameHandler extends TestFrameHandler + { + @Override + public void onFrame(Frame frame) + { + super.onFrame(frame); + Frame echo = new Frame(frame.getOpCode(), frame.getPayloadAsUTF8()); + getCoreSession().sendFrame(echo, Callback.NOOP, false); + } + } + + private WebSocketServer server; + private WebSocketCoreClient client; + + @BeforeEach + public void startup() throws Exception + { + WebSocketNegotiator negotiator = new WebSocketNegotiator.AbstractNegotiator() + { + @Override + public FrameHandler negotiate(Negotiation negotiation) throws IOException + { + if (negotiation.getOfferedSubprotocols().isEmpty()) + { + negotiation.setSubprotocol("NotOffered"); + return new EchoFrameHandler(); + } + + String subprotocol = negotiation.getOfferedSubprotocols().get(0); + negotiation.setSubprotocol(subprotocol); + switch (subprotocol) + { + case "testExtensionSelection": + negotiation.setNegotiatedExtensions(List.of(ExtensionConfig.parse("permessage-deflate;client_no_context_takeover"))); + break; + + case "testNotOfferedParameter": + negotiation.setNegotiatedExtensions(List.of(ExtensionConfig.parse("permessage-deflate;server_no_context_takeover"))); + break; + + case "testNotAcceptingExtensions": + negotiation.setNegotiatedExtensions(Collections.EMPTY_LIST); + break; + + case "testNoSubProtocolSelected": + negotiation.setSubprotocol(null); + break; + + case "test": + case "testInvalidExtensionParameter": + case "testAcceptTwoExtensionsOfSameName": + case "testInvalidUpgradeRequest": + break; + + default: + return null; + } + + return new EchoFrameHandler(); + } + }; + + server = new WebSocketServer(negotiator); + client = new WebSocketCoreClient(); + + server.start(); + client.start(); + } + + @AfterEach + public void shutdown() throws Exception + { + server.start(); + client.start(); + } + + @Test + public void testExtensionSelection() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + + ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, server.getUri(), clientHandler); + upgradeRequest.setSubProtocols("testExtensionSelection"); + upgradeRequest.addExtensions("permessage-deflate;server_no_context_takeover", "permessage-deflate;client_no_context_takeover"); + + CompletableFuture extensionHeader = new CompletableFuture<>(); + upgradeRequest.addListener(new UpgradeListener() + { + @Override + public void onHandshakeResponse(HttpRequest request, HttpResponse response) + { + extensionHeader.complete(response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_EXTENSIONS)); + } + }); + + CompletableFuture connect = client.connect(upgradeRequest); + connect.get(5, TimeUnit.SECONDS); + + clientHandler.sendText("hello world"); + clientHandler.sendClose(); + assertTrue(clientHandler.closed.await(5, TimeUnit.SECONDS)); + assertThat(clientHandler.receivedFrames.size(), is(2)); + assertNull(clientHandler.getError()); + + assertThat(extensionHeader.get(5, TimeUnit.SECONDS), is("permessage-deflate;client_no_context_takeover")); + } + + @Test + public void testNotOfferedParameter() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + + ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, server.getUri(), clientHandler); + upgradeRequest.setSubProtocols("testNotOfferedParameter"); + upgradeRequest.addExtensions("permessage-deflate;client_no_context_takeover"); + + CompletableFuture extensionHeader = new CompletableFuture<>(); + upgradeRequest.addListener(new UpgradeListener() + { + @Override + public void onHandshakeResponse(HttpRequest request, HttpResponse response) + { + extensionHeader.complete(response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_EXTENSIONS)); + } + }); + + CompletableFuture connect = client.connect(upgradeRequest); + connect.get(5, TimeUnit.SECONDS); + clientHandler.sendText("hello world"); + clientHandler.sendClose(); + assertTrue(clientHandler.closed.await(5, TimeUnit.SECONDS)); + assertThat(clientHandler.receivedFrames.size(), is(2)); + assertNull(clientHandler.getError()); + + assertThat(extensionHeader.get(5, TimeUnit.SECONDS), is("permessage-deflate;server_no_context_takeover")); + } + + @Test + public void testInvalidExtensionParameter() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + + ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, server.getUri(), clientHandler); + upgradeRequest.setSubProtocols("testInvalidExtensionParameter"); + upgradeRequest.addExtensions("permessage-deflate;invalid_parameter"); + + CompletableFuture connect = client.connect(upgradeRequest); + + Throwable t = assertThrows(ExecutionException.class, ()->connect.get(5, TimeUnit.SECONDS)); + assertThat(t.getMessage(), containsString("Failed to upgrade to websocket:")); + assertThat(t.getMessage(), containsString("400 Bad Request")); + } + + + @Test + public void testNotAcceptingExtensions() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + + ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, server.getUri(), clientHandler); + upgradeRequest.setSubProtocols("testNotAcceptingExtensions"); + upgradeRequest.addExtensions("permessage-deflate;server_no_context_takeover", "permessage-deflate;client_no_context_takeover"); + + CompletableFuture extensionHeader = new CompletableFuture<>(); + upgradeRequest.addListener(new UpgradeListener() + { + @Override + public void onHandshakeResponse(HttpRequest request, HttpResponse response) + { + extensionHeader.complete(response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_EXTENSIONS)); + } + }); + + CompletableFuture connect = client.connect(upgradeRequest); + connect.get(5, TimeUnit.SECONDS); + + clientHandler.sendText("hello world"); + clientHandler.sendClose(); + assertTrue(clientHandler.closed.await(5, TimeUnit.SECONDS)); + assertThat(clientHandler.receivedFrames.size(), is(2)); + assertNull(clientHandler.getError()); + + assertNull(extensionHeader.get(5, TimeUnit.SECONDS)); + } + + + @Test + public void testAcceptTwoExtensionsOfSameName() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + + ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, server.getUri(), clientHandler); + upgradeRequest.setSubProtocols("testAcceptTwoExtensionsOfSameName"); + upgradeRequest.addExtensions("permessage-deflate;server_no_context_takeover", "permessage-deflate;client_no_context_takeover"); + + CompletableFuture extensionHeader = new CompletableFuture<>(); + upgradeRequest.addListener(new UpgradeListener() + { + @Override + public void onHandshakeResponse(HttpRequest request, HttpResponse response) + { + extensionHeader.complete(response.getHeaders().get(HttpHeader.SEC_WEBSOCKET_EXTENSIONS)); + } + }); + + CompletableFuture connect = client.connect(upgradeRequest); + connect.get(5, TimeUnit.SECONDS); + + clientHandler.sendText("hello world"); + clientHandler.sendClose(); + assertTrue(clientHandler.closed.await(5, TimeUnit.SECONDS)); + assertThat(clientHandler.receivedFrames.size(), is(2)); + assertNull(clientHandler.getError()); + + assertThat(extensionHeader.get(5, TimeUnit.SECONDS), is("permessage-deflate;server_no_context_takeover")); + } + + @Test + public void testSubProtocolNotOffered() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + + ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, server.getUri(), clientHandler); + + try (StacklessLogging stacklessLogging = new StacklessLogging(HttpChannel.class)) + { + CompletableFuture connect = client.connect(upgradeRequest); + Throwable t = assertThrows(ExecutionException.class, () -> connect.get(5, TimeUnit.SECONDS)); + assertThat(t.getMessage(), containsString("Failed to upgrade to websocket:")); + assertThat(t.getMessage(), containsString("500 Server Error")); + } + } + + @Test + public void testNoSubProtocolSelected() throws Exception + { + TestFrameHandler clientHandler = new TestFrameHandler(); + + ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, server.getUri(), clientHandler); + upgradeRequest.setSubProtocols("testNoSubProtocolSelected"); + + try (StacklessLogging stacklessLogging = new StacklessLogging(HttpChannel.class)) + { + CompletableFuture connect = client.connect(upgradeRequest); + Throwable t = assertThrows(ExecutionException.class, () -> connect.get(5, TimeUnit.SECONDS)); + assertThat(t.getMessage(), containsString("Failed to upgrade to websocket:")); + assertThat(t.getMessage(), containsString("500 Server Error")); + } + } + + @Test + public void testValidUpgradeRequest() throws Exception + { + Socket client = new Socket(); + client.connect(new InetSocketAddress("127.0.0.1", server.getLocalPort())); + + HttpFields httpFields = newUpgradeRequest(null); + String upgradeRequest = "GET / HTTP/1.1\r\n" + httpFields; + client.getOutputStream().write(upgradeRequest.getBytes(StandardCharsets.ISO_8859_1)); + String response = getUpgradeResponse(client.getInputStream()); + + assertThat(response, startsWith("HTTP/1.1 101 Switching Protocols")); + assertThat(response, containsString("Sec-WebSocket-Protocol: test")); + assertThat(response, containsString("Sec-WebSocket-Accept: +WahVcVmeMLKQUMm0fvPrjSjwzI=")); + } + + @Test + public void testInvalidUpgradeRequestNoKey() throws Exception + { + Socket client = new Socket(); + client.connect(new InetSocketAddress("127.0.0.1", server.getLocalPort())); + + HttpFields httpFields = newUpgradeRequest(null); + httpFields.remove(HttpHeader.SEC_WEBSOCKET_KEY); + + String upgradeRequest = "GET / HTTP/1.1\r\n" + httpFields; + client.getOutputStream().write(upgradeRequest.getBytes(StandardCharsets.ISO_8859_1)); + String response = getUpgradeResponse(client.getInputStream()); + + assertThat(response, containsString("400 Bad Request")); + } +} \ No newline at end of file diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketOpenTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketOpenTest.java index 36e79ee3c14..72a418cff99 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketOpenTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketOpenTest.java @@ -30,7 +30,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.websocket.core.internal.Parser; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -95,7 +95,7 @@ public class WebSocketOpenTest extends WebSocketTester @Test public void testFailureInOnOpen() throws Exception { - try(StacklessLogging stackless = new StacklessLogging(WebSocketChannel.class)) + try(StacklessLogging stackless = new StacklessLogging(WebSocketCoreSession.class)) { setup((s, c) -> { @@ -158,12 +158,12 @@ public class WebSocketOpenTest extends WebSocketTester return null; }); - FrameHandler.CoreSession session = sx.exchange(null); + FrameHandler.CoreSession coreSession = sx.exchange(null); Callback onOpenCallback = cx.exchange(null); Thread.sleep(100); // Can send while onOpen is active - WebSocketOpenTest.TestFrameHandler.sendText(session,"Hello", NOOP); + WebSocketOpenTest.TestFrameHandler.sendText(coreSession,"Hello", NOOP); Parser.ParsedFrame frame = receiveFrame(client.getInputStream()); assertThat(frame.getPayloadAsUTF8(),is("Hello")); @@ -172,16 +172,16 @@ public class WebSocketOpenTest extends WebSocketTester assertFalse(serverHandler.onClosed.await(1, TimeUnit.SECONDS)); // Can't demand until open - assertThrows(Throwable.class, () -> session.demand(1)); + assertThrows(Throwable.class, () -> coreSession.demand(1)); client.getOutputStream().write(RawFrameBuilder.buildClose(new CloseStatus(CloseStatus.NORMAL), true)); assertFalse(serverHandler.onClosed.await(1, TimeUnit.SECONDS)); // Succeeded moves to OPEN state and still does not read CLOSE frame onOpenCallback.succeeded(); - assertThat(session.toString(),containsString("OPEN")); + assertThat(coreSession.toString(),containsString("OPEN")); // Demand start receiving frames - session.demand(1); + coreSession.demand(1); client.getOutputStream().write(RawFrameBuilder.buildClose(new CloseStatus(CloseStatus.NORMAL), true)); assertTrue(serverHandler.onClosed.await(5, TimeUnit.SECONDS)); @@ -198,7 +198,7 @@ public class WebSocketOpenTest extends WebSocketTester static class TestFrameHandler implements SynchronousFrameHandler { - private CoreSession session; + private CoreSession coreSession; private BiFunction onOpen; private CloseStatus closeStatus; private CountDownLatch onClosed = new CountDownLatch(1); @@ -211,7 +211,7 @@ public class WebSocketOpenTest extends WebSocketTester { synchronized (this) { - return session; + return coreSession; } } @@ -226,7 +226,7 @@ public class WebSocketOpenTest extends WebSocketTester LOG.info("onOpen {}", coreSession); synchronized (this) { - session = coreSession; + this.coreSession = coreSession; } onOpen.apply(coreSession, callback); } @@ -271,25 +271,25 @@ public class WebSocketOpenTest extends WebSocketTester public void sendText(String text) { - sendText(session, text); + sendText(coreSession, text); } public void sendText(String text, Callback callback) { - sendText(session, text, callback); + sendText(coreSession, text, callback); } - static void sendText(FrameHandler.CoreSession session, String text) + static void sendText(FrameHandler.CoreSession coreSession, String text) { - sendText(session, text, NOOP); + sendText(coreSession, text, NOOP); } - static void sendText(FrameHandler.CoreSession session, String text, Callback callback) + static void sendText(FrameHandler.CoreSession coreSession, String text, Callback callback) { Frame frame = new Frame(OpCode.TEXT); frame.setFin(true); frame.setPayload(text); - session.sendFrame(frame, callback, false); + coreSession.sendFrame(frame, callback, false); } } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketServer.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketServer.java index cee66497201..fbdad8ebbf9 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketServer.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketServer.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.websocket.core; import java.io.IOException; +import java.net.URI; import java.util.List; import org.eclipse.jetty.server.NetworkConnector; @@ -35,10 +36,12 @@ public class WebSocketServer { private static Logger LOG = Log.getLogger(WebSocketServer.class); private final Server server; + private URI serverUri; public void start() throws Exception { server.start(); + serverUri = new URI("ws://localhost:" + getLocalPort()); } public void stop() throws Exception @@ -56,12 +59,12 @@ public class WebSocketServer return server; } - public WebSocketServer(FrameHandler frameHandler) + public WebSocketServer(FrameHandler frameHandler) throws Exception { this(new DefaultNegotiator(frameHandler)); } - public WebSocketServer(WebSocketNegotiator negotiator) + public WebSocketServer(WebSocketNegotiator negotiator) throws Exception { server = new Server(); ServerConnector connector = new ServerConnector(server); @@ -75,6 +78,11 @@ public class WebSocketServer context.setHandler(upgradeHandler); } + public URI getUri() + { + return serverUri; + } + private static class DefaultNegotiator extends WebSocketNegotiator.AbstractNegotiator { private final FrameHandler frameHandler; diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketTester.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketTester.java index 7f302536ec9..27cdf501763 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketTester.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketTester.java @@ -45,14 +45,14 @@ import static org.hamcrest.Matchers.startsWith; public class WebSocketTester { private static String NON_RANDOM_KEY = new String(B64Code.encode("0123456701234567".getBytes())); - private static SslContextFactory sslContextFactory; + private static SslContextFactory.Client sslContextFactory; protected ByteBufferPool bufferPool; protected Parser parser; @BeforeAll public static void startSslContextFactory() throws Exception { - sslContextFactory = new SslContextFactory(true); + sslContextFactory = new SslContextFactory.Client(true); sslContextFactory.setEndpointIdentificationAlgorithm(""); sslContextFactory.start(); } @@ -84,17 +84,9 @@ public class WebSocketTester { return newClient(port, false, extensions); } - - protected Socket newClient(int port, boolean tls, String extensions) throws Exception + + protected static HttpFields newUpgradeRequest(String extensions) { - Socket client; - if (!tls) - client = new Socket(); - else - client = sslContextFactory.newSslSocket(); - - client.connect(new InetSocketAddress("127.0.0.1", port)); - HttpFields fields = new HttpFields(); fields.add(HttpHeader.HOST, "127.0.0.1"); fields.add(HttpHeader.UPGRADE, "websocket"); @@ -107,10 +99,11 @@ public class WebSocketTester if (extensions != null) fields.add(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, extensions); - client.getOutputStream().write(("GET / HTTP/1.1\r\n" + fields.toString()).getBytes(StandardCharsets.ISO_8859_1)); - - InputStream in = client.getInputStream(); + return fields; + } + protected static String getUpgradeResponse(InputStream in) throws IOException + { int state = 0; StringBuilder buffer = new StringBuilder(); while (state < 4) @@ -139,7 +132,23 @@ public class WebSocketTester } } - String response = buffer.toString(); + return buffer.toString(); + } + + protected Socket newClient(int port, boolean tls, String extensions) throws Exception + { + Socket client; + if (!tls) + client = new Socket(); + else + client = sslContextFactory.newSslSocket(); + + client.connect(new InetSocketAddress("127.0.0.1", port)); + + String upgradeRequest = "GET / HTTP/1.1\r\n" + newUpgradeRequest(extensions); + client.getOutputStream().write(upgradeRequest.getBytes(StandardCharsets.ISO_8859_1)); + String response = getUpgradeResponse(client.getInputStream()); + assertThat(response, startsWith("HTTP/1.1 101 Switching Protocols")); assertThat(response, containsString("Sec-WebSocket-Protocol: test")); assertThat(response, containsString("Sec-WebSocket-Accept: +WahVcVmeMLKQUMm0fvPrjSjwzI=")); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnWebSocketNegotiator.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnWebSocketNegotiator.java index 7b81b881390..da6432f36c5 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnWebSocketNegotiator.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/autobahn/AutobahnWebSocketNegotiator.java @@ -82,12 +82,12 @@ class AutobahnWebSocketNegotiator implements WebSocketNegotiator } @Override - public void customize(FrameHandler.CoreSession session) + public void customize(FrameHandler.Configuration configurable) { - session.setIdleTimeout(Duration.ofMillis(10000)); - session.setMaxTextMessageSize(Integer.MAX_VALUE); - session.setMaxBinaryMessageSize(Integer.MAX_VALUE); - session.setMaxFrameSize(65536*2); + configurable.setIdleTimeout(Duration.ofMillis(10000)); + configurable.setMaxTextMessageSize(Integer.MAX_VALUE); + configurable.setMaxBinaryMessageSize(Integer.MAX_VALUE); + configurable.setMaxFrameSize(65536*2); } @Override diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/client/WebSocketClientServerTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/client/WebSocketClientServerTest.java index 22c2bea9c6f..36d460f3d06 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/client/WebSocketClientServerTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/client/WebSocketClientServerTest.java @@ -31,7 +31,7 @@ import org.eclipse.jetty.websocket.core.FrameHandler.CoreSession; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.TestFrameHandler; import org.eclipse.jetty.websocket.core.WebSocketServer; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -102,7 +102,7 @@ public class WebSocketClientServerTest super.receivedFrames.offer(Frame.copy(frame)); if (frame.getOpCode() == OpCode.CLOSE) { - LOG.info("channel aborted"); + LOG.info("session aborted"); getCoreSession().abort(); callback.failed(new Exception()); } @@ -140,7 +140,7 @@ public class WebSocketClientServerTest assertNotNull(recv); assertThat(recv.getPayloadAsUTF8(), Matchers.equalTo(message)); - ((WebSocketChannel)clientHandler.getCoreSession()).getConnection().getEndPoint().close(); + ((WebSocketCoreSession)clientHandler.getCoreSession()).getConnection().getEndPoint().close(); assertTrue(clientHandler.closed.await(5, TimeUnit.SECONDS)); assertTrue(serverHandler.closed.await(5, TimeUnit.SECONDS)); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/DeflateFrameExtensionTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/DeflateFrameExtensionTest.java index c62a6eac28b..73091a5311c 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/DeflateFrameExtensionTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/DeflateFrameExtensionTest.java @@ -52,15 +52,15 @@ import org.eclipse.jetty.websocket.core.internal.ExtensionStack; import org.eclipse.jetty.websocket.core.internal.Generator; import org.eclipse.jetty.websocket.core.internal.Negotiated; import org.eclipse.jetty.websocket.core.internal.Parser; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.eclipse.jetty.websocket.core.internal.compress.DeflateFrameExtension; import org.junit.jupiter.api.Test; import static org.eclipse.jetty.websocket.core.OpCode.TEXT; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertArrayEquals; public class DeflateFrameExtensionTest extends AbstractExtensionTest @@ -237,7 +237,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest private void init(DeflateFrameExtension ext) { - ext.setWebSocketChannel(channelWithMaxMessageSize(20 * 1024 * 1024)); + ext.setWebSocketCoreSession(sessionWithMaxMessageSize(20 * 1024 * 1024)); ext.init(new ExtensionConfig(ext.getName()), bufferPool); } @@ -379,11 +379,11 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest DeflateFrameExtension clientExtension = new DeflateFrameExtension(); init(clientExtension); - clientExtension.setWebSocketChannel(channelWithMaxMessageSize(maxMessageSize)); + clientExtension.setWebSocketCoreSession(sessionWithMaxMessageSize(maxMessageSize)); final DeflateFrameExtension serverExtension = new DeflateFrameExtension(); init(serverExtension); - serverExtension.setWebSocketChannel(channelWithMaxMessageSize(maxMessageSize)); + serverExtension.setWebSocketCoreSession(sessionWithMaxMessageSize(maxMessageSize)); // Chain the next element to decompress. clientExtension.setNextOutgoingFrames((frame, callback, batch) -> @@ -416,14 +416,14 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest } - private WebSocketChannel channelWithMaxMessageSize(int maxMessageSize) + private WebSocketCoreSession sessionWithMaxMessageSize(int maxMessageSize) { ByteBufferPool bufferPool = new MappedByteBufferPool(); - ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry()); - exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>()); + ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry(), Behavior.SERVER); + exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>(), new LinkedList<>()); - WebSocketChannel channel = new WebSocketChannel(new AbstractTestFrameHandler(), Behavior.SERVER, Negotiated.from(exStack)); - channel.setMaxFrameSize(maxMessageSize); - return channel; + WebSocketCoreSession coreSession = new WebSocketCoreSession(new AbstractTestFrameHandler(), Behavior.SERVER, Negotiated.from(exStack)); + coreSession.setMaxFrameSize(maxMessageSize); + return coreSession; } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionStackTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionStackTest.java index d5276089157..8e8ee53a057 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionStackTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionStackTest.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.Behavior; import org.eclipse.jetty.websocket.core.Extension; import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.IncomingFrames; @@ -55,7 +56,7 @@ public class ExtensionStackTest { objectFactory = new DecoratedObjectFactory(); bufferPool = new MappedByteBufferPool(); - stack = new ExtensionStack(new WebSocketExtensionRegistry()); + stack = new ExtensionStack(new WebSocketExtensionRegistry(), Behavior.SERVER); } @SuppressWarnings("unchecked") @@ -75,7 +76,7 @@ public class ExtensionStackTest // 1 extension List configs = new ArrayList<>(); configs.add(ExtensionConfig.parse("identity")); - stack.negotiate(objectFactory, bufferPool, configs); + stack.negotiate(objectFactory, bufferPool, configs, configs); // Setup Listeners IncomingFrames session = new IncomingFramesCapture(); @@ -99,7 +100,7 @@ public class ExtensionStackTest List configs = new ArrayList<>(); configs.add(ExtensionConfig.parse("identity; id=A")); configs.add(ExtensionConfig.parse("identity; id=B")); - stack.negotiate(objectFactory, bufferPool, configs); + stack.negotiate(objectFactory, bufferPool, configs, configs); // Setup Listeners IncomingFrames session = new IncomingFramesCapture(); @@ -130,7 +131,7 @@ public class ExtensionStackTest { String chromeRequest = "permessage-deflate; client_max_window_bits, x-webkit-deflate-frame"; List requestedConfigs = ExtensionConfig.parseList(chromeRequest); - stack.negotiate(objectFactory, bufferPool, requestedConfigs); + stack.negotiate(objectFactory, bufferPool, requestedConfigs, requestedConfigs); List negotiated = stack.getNegotiatedExtensions(); String response = ExtensionConfig.toHeaderValue(negotiated); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionTool.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionTool.java index 519b4d8b437..4862ec9e12f 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionTool.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ExtensionTool.java @@ -39,7 +39,7 @@ import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; import org.eclipse.jetty.websocket.core.internal.ExtensionStack; import org.eclipse.jetty.websocket.core.internal.Negotiated; import org.eclipse.jetty.websocket.core.internal.Parser; -import org.eclipse.jetty.websocket.core.internal.WebSocketChannel; +import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; @@ -77,7 +77,7 @@ public class ExtensionTool { this.ext = factory.newInstance(objectFactory, bufferPool, extConfig); this.ext.setNextIncomingFrames(capture); - this.ext.setWebSocketChannel(newWebsocketChannel()); + this.ext.setWebSocketCoreSession(newWebSocketCoreSession()); } public void parseIncomingHex(String... rawhex) @@ -150,12 +150,12 @@ public class ExtensionTool return new Tester(parameterizedExtension); } - private WebSocketChannel newWebsocketChannel() + private WebSocketCoreSession newWebSocketCoreSession() { ByteBufferPool bufferPool = new MappedByteBufferPool(); - ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry()); - exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>()); - WebSocketChannel channel = new WebSocketChannel(new AbstractTestFrameHandler(), Behavior.SERVER, Negotiated.from(exStack)); - return channel; + ExtensionStack exStack = new ExtensionStack(new WebSocketExtensionRegistry(), Behavior.SERVER); + exStack.negotiate(new DecoratedObjectFactory(), bufferPool, new LinkedList<>(), new LinkedList<>()); + WebSocketCoreSession coreSession = new WebSocketCoreSession(new AbstractTestFrameHandler(), Behavior.SERVER, Negotiated.from(exStack)); + return coreSession; } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ValidationExtensionTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ValidationExtensionTest.java index b837dda4c06..f2cf77e7fa4 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ValidationExtensionTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/ValidationExtensionTest.java @@ -18,17 +18,23 @@ package org.eclipse.jetty.websocket.core.extensions; +import java.io.IOException; import java.net.Socket; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.websocket.core.CloseStatus; +import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.Frame; +import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.RawFrameBuilder; import org.eclipse.jetty.websocket.core.TestFrameHandler; import org.eclipse.jetty.websocket.core.TestWebSocketNegotiator; import org.eclipse.jetty.websocket.core.WebSocketServer; import org.eclipse.jetty.websocket.core.WebSocketTester; +import org.eclipse.jetty.websocket.core.server.Negotiation; import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -48,7 +54,19 @@ public class ValidationExtensionTest extends WebSocketTester public void start() throws Exception { serverHandler = new TestFrameHandler(); - WebSocketNegotiator negotiator = new TestWebSocketNegotiator(serverHandler); + WebSocketNegotiator negotiator = new TestWebSocketNegotiator(serverHandler) + { + @Override + public FrameHandler negotiate(Negotiation negotiation) throws IOException + { + List negotiatedExtensions = new ArrayList<>(); + negotiatedExtensions.add(ExtensionConfig.parse( + "@validation; outgoing-sequence; incoming-sequence; outgoing-frame; incoming-frame; incoming-utf8; outgoing-utf8")); + negotiation.setNegotiatedExtensions(negotiatedExtensions); + + return super.negotiate(negotiation); + } + }; server = new WebSocketServer(negotiator); server.start(); } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/internal/FrameFlusherTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/internal/FrameFlusherTest.java index f07c6e2ec27..dee3c6860b8 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/internal/FrameFlusherTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/internal/FrameFlusherTest.java @@ -24,19 +24,28 @@ import java.nio.channels.WritePendingException; import java.util.Arrays; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; +import org.eclipse.jetty.util.thread.Scheduler; import org.eclipse.jetty.websocket.core.CloseStatus; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.WebSocketConstants; +import org.eclipse.jetty.websocket.core.WebSocketWriteTimeoutException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static java.nio.charset.StandardCharsets.UTF_8; @@ -48,7 +57,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class FrameFlusherTest { - public ByteBufferPool bufferPool = new MappedByteBufferPool(); + private ByteBufferPool bufferPool = new MappedByteBufferPool(); + private Scheduler scheduler; + + @BeforeEach + public void start() throws Exception + { + scheduler = new ScheduledExecutorScheduler(); + scheduler.start(); + } + + @AfterEach + public void stop() throws Exception + { + scheduler.stop(); + } /** * Ensure post-close frames have their associated callbacks properly notified. @@ -60,7 +83,7 @@ public class FrameFlusherTest CapturingEndPoint endPoint = new CapturingEndPoint(bufferPool); int bufferSize = WebSocketConstants.DEFAULT_MAX_TEXT_MESSAGE_SIZE; int maxGather = 1; - FrameFlusher frameFlusher = new FrameFlusher(bufferPool, generator, endPoint, bufferSize, maxGather); + FrameFlusher frameFlusher = new FrameFlusher(bufferPool, scheduler, generator, endPoint, bufferSize, maxGather); Frame closeFrame = new Frame(OpCode.CLOSE).setPayload(CloseStatus.asPayloadBuffer(CloseStatus.MESSAGE_TOO_LARGE, "Message be to big")); Frame textFrame = new Frame(OpCode.TEXT).setPayload("Hello").setFin(true); @@ -91,7 +114,7 @@ public class FrameFlusherTest CapturingEndPoint endPoint = new CapturingEndPoint(bufferPool); int bufferSize = WebSocketConstants.DEFAULT_MAX_TEXT_MESSAGE_SIZE; int maxGather = 8; - FrameFlusher frameFlusher = new FrameFlusher(bufferPool, generator, endPoint, bufferSize, maxGather); + FrameFlusher frameFlusher = new FrameFlusher(bufferPool, scheduler, generator, endPoint, bufferSize, maxGather); int largeMessageSize = 60000; byte[] buf = new byte[largeMessageSize]; @@ -134,6 +157,73 @@ public class FrameFlusherTest serverTask.get(); } + @Test + public void testWriteTimeout() throws Exception + { + Generator generator = new Generator(bufferPool); + BlockingEndpoint endPoint = new BlockingEndpoint(bufferPool); + int bufferSize = WebSocketConstants.DEFAULT_MAX_TEXT_MESSAGE_SIZE; + int maxGather = 8; + + CountDownLatch flusherFailure = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + FrameFlusher frameFlusher = new FrameFlusher(bufferPool, scheduler, generator, endPoint, bufferSize, maxGather) + { + @Override + public void onCompleteFailure(Throwable failure) + { + error.set(failure); + flusherFailure.countDown(); + super.onCompleteFailure(failure); + } + }; + + frameFlusher.setIdleTimeout(100); + endPoint.setBlockTime(200); + + Frame frame = new Frame(OpCode.TEXT).setPayload("message").setFin(true); + frameFlusher.enqueue(frame, Callback.NOOP, false); + frameFlusher.iterate(); + + assertTrue(flusherFailure.await(2, TimeUnit.SECONDS)); + assertThat(error.get(), instanceOf(WebSocketWriteTimeoutException.class)); + } + + @Test + public void testErrorClose() throws Exception + { + Generator generator = new Generator(bufferPool); + BlockingEndpoint endPoint = new BlockingEndpoint(bufferPool); + endPoint.setBlockTime(100); + int bufferSize = WebSocketConstants.DEFAULT_MAX_TEXT_MESSAGE_SIZE; + int maxGather = 8; + FrameFlusher frameFlusher = new FrameFlusher(bufferPool, scheduler, generator, endPoint, bufferSize, maxGather); + + // Enqueue message before the error close. + Frame frame1 = new Frame(OpCode.TEXT).setPayload("message before close").setFin(true); + CountDownLatch failedFrame1 = new CountDownLatch(1); + Callback callbackFrame1 = Callback.from(()->{}, t->failedFrame1.countDown()); + assertTrue(frameFlusher.enqueue(frame1, callbackFrame1, false)); + + // Enqueue the close frame which should fail the previous frame as it is still in the queue. + Frame closeFrame = new CloseStatus(CloseStatus.MESSAGE_TOO_LARGE).toFrame(); + CountDownLatch succeededCloseFrame = new CountDownLatch(1); + Callback closeFrameCallback = Callback.from(succeededCloseFrame::countDown, t->{}); + assertTrue(frameFlusher.enqueue(closeFrame, closeFrameCallback, false)); + assertTrue(failedFrame1.await(1, TimeUnit.SECONDS)); + + // Any frames enqueued after this should fail. + Frame frame2 = new Frame(OpCode.TEXT).setPayload("message after close").setFin(true); + CountDownLatch failedFrame2 = new CountDownLatch(1); + Callback callbackFrame2 = Callback.from(()->{}, t->failedFrame2.countDown()); + assertFalse(frameFlusher.enqueue(frame2, callbackFrame2, false)); + assertTrue(failedFrame2.await(1, TimeUnit.SECONDS)); + + // Iterating should succeed the close callback. + frameFlusher.iterate(); + assertTrue(succeededCloseFrame.await(1, TimeUnit.SECONDS)); + } + public static class CapturingEndPoint extends MockEndpoint { public Parser parser; @@ -176,4 +266,43 @@ public class FrameFlusherTest } } } + + public static class BlockingEndpoint extends CapturingEndPoint + { + private static final Logger LOG = Log.getLogger(BlockingEndpoint.class); + + private long blockTime = 0; + public CountDownLatch closeLatch = new CountDownLatch(1); + public volatile Throwable error; + + public void setBlockTime(int time) + { + blockTime = time; + } + + public BlockingEndpoint(ByteBufferPool bufferPool) + { + super(bufferPool); + } + + @Override + public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException + { + try + { + Thread.sleep(blockTime); + super.write(callback, buffers); + } + catch (InterruptedException e) + { + callback.failed(e); + } + } + + @Override + public void close(Throwable cause) + { + //ignore + } + } } diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java index b073df0ef18..409fc21b066 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java @@ -22,7 +22,6 @@ import java.net.HttpCookie; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; -import java.net.URISyntaxException; import java.security.Principal; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -38,6 +37,7 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; +import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.WebSocketConstants; @@ -57,21 +57,28 @@ public class ServletUpgradeRequest private List cookies; private Map> parameterMap; - public ServletUpgradeRequest(Negotiation negotiation) throws URISyntaxException + public ServletUpgradeRequest(Negotiation negotiation) throws BadMessageException { this.negotiation = negotiation; HttpServletRequest httpRequest = negotiation.getRequest(); this.queryString = httpRequest.getQueryString(); this.secure = httpRequest.isSecure(); - // TODO why is this URL and not URI? - StringBuffer uri = httpRequest.getRequestURL(); - // WHY? - if (this.queryString != null) - uri.append("?").append(this.queryString); - uri.replace(0, uri.indexOf(":"), secure?"wss":"ws"); - this.requestURI = new URI(uri.toString()); - this.request = new UpgradeHttpServletRequest(httpRequest); + try + { + // TODO why is this URL and not URI? + StringBuffer uri = httpRequest.getRequestURL(); + // WHY? + if (this.queryString != null) + uri.append("?").append(this.queryString); + uri.replace(0, uri.indexOf(":"), secure ? "wss" : "ws"); + this.requestURI = new URI(uri.toString()); + this.request = new UpgradeHttpServletRequest(httpRequest); + } + catch (Throwable t) + { + throw new BadMessageException("Bad WebSocket UpgradeRequest", t); + } } /** diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java index d3621c8adf8..68b34cb54aa 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java @@ -18,11 +18,6 @@ package org.eclipse.jetty.websocket.servlet; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.websocket.core.ExtensionConfig; -import org.eclipse.jetty.websocket.core.server.Negotiation; - -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -33,6 +28,12 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.websocket.core.ExtensionConfig; +import org.eclipse.jetty.websocket.core.server.Negotiation; + /** * Servlet Specific UpgradeResponse implementation. */ @@ -166,6 +167,21 @@ public class ServletUpgradeResponse public void setExtensions(List configs) { + // This validation is also done later in RFC6455Handshaker but it is better to fail earlier + for (ExtensionConfig config : configs) + { + if (config.getName().startsWith("@")) + continue; + + long matches = negotiation.getOfferedExtensions().stream().filter(e -> e.getName().equals(config.getName())).count(); + if (matches < 1) + throw new IllegalArgumentException("Extension not a requested extension"); + + matches = negotiation.getNegotiatedExtensions().stream().filter(e -> e.getName().equals(config.getName())).count(); + if (matches > 1) + throw new IllegalArgumentException("Multiple extensions of the same name"); + } + negotiation.setNegotiatedExtensions(configs); } diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketMapping.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketMapping.java index 69dcffa1149..cc49610726d 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketMapping.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketMapping.java @@ -19,7 +19,6 @@ package org.eclipse.jetty.websocket.servlet; import java.io.IOException; -import java.net.URISyntaxException; import java.util.function.Consumer; import javax.servlet.ServletContext; @@ -32,7 +31,6 @@ import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.RegexPathSpec; import org.eclipse.jetty.http.pathmap.ServletPathSpec; import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; -import org.eclipse.jetty.io.RuntimeIOException; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.LifeCycle; @@ -61,11 +59,10 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener { private static final Logger LOG = Log.getLogger(WebSocketMapping.class); - public static WebSocketMapping ensureMapping(ServletContext servletContext, String mappingKey) + public static WebSocketMapping getMapping(ServletContext servletContext, String mappingKey) { - ContextHandler contextHandler = ContextHandler.getContextHandler(servletContext); + Object mappingObject = servletContext.getAttribute(mappingKey); - Object mappingObject = contextHandler.getAttribute(mappingKey); if (mappingObject!=null) { if (WebSocketMapping.class.isInstance(mappingObject)) @@ -73,104 +70,23 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener else throw new IllegalStateException( String.format("ContextHandler attribute %s is not of type WebSocketMapping: {%s}", - mappingKey, mappingObject.toString())); - } - else - { - WebSocketMapping mapping = new WebSocketMapping(WebSocketComponents.ensureWebSocketComponents(servletContext)); - contextHandler.setAttribute(mappingKey, mapping); - return mapping; + mappingKey, mappingObject.toString())); } + + return null; } - public static final String DEFAULT_KEY = "org.eclipse.jetty.websocket.servlet.WebSocketMapping"; - - private final PathMappings mappings = new PathMappings<>(); - private final WebSocketComponents components; - private final Handshaker handshaker = Handshaker.newInstance(); - - public WebSocketMapping() + public static WebSocketMapping ensureMapping(ServletContext servletContext, String mappingKey) { - this(new WebSocketComponents()); - } + WebSocketMapping mapping = getMapping(servletContext, mappingKey); - public WebSocketMapping(WebSocketComponents components) - { - this.components = components; - } - - @Override - public void lifeCycleStopping(LifeCycle context) - { - ContextHandler contextHandler = (ContextHandler) context; - WebSocketMapping mapping = contextHandler.getBean(WebSocketMapping.class); - if (mapping == this) - { - contextHandler.removeBean(mapping); - mappings.reset(); - } - } - - /** - * Manually add a WebSocket mapping. - *

    - * If mapping is added before this configuration is started, then it is persisted through - * stop/start of this configuration's lifecycle. Otherwise it will be removed when - * this configuration is stopped. - *

    - * - * @param pathSpec the pathspec to respond on - * @param creator the websocket creator to activate on the provided mapping. - * @param factory the factory to use to create a FrameHandler for the websocket - * @param customizer the customizer to use to customize the WebSocket session. - */ - public void addMapping(PathSpec pathSpec, WebSocketCreator creator, FrameHandlerFactory factory, FrameHandler.Customizer customizer) - throws WebSocketException - { - // TODO evaluate why this can't be done - //if (getMapping(pathSpec) != null) - // throw new WebSocketException("Duplicate WebSocket Mapping for PathSpec"); - - mappings.put(pathSpec, new Negotiator(creator, factory, customizer)); - } - - public WebSocketCreator getMapping(PathSpec pathSpec) - { - Negotiator cn = mappings.get(pathSpec); - return cn == null?null:cn.getWebSocketCreator(); - } - - public boolean removeMapping(PathSpec pathSpec) - { - return mappings.remove(pathSpec); - } - - @Override - public String dump() - { - return Dumpable.dump(this); - } - - @Override - public void dump(Appendable out, String indent) throws IOException - { - Dumpable.dumpObjects(out, indent, this, mappings); - } - - /** - * Get the matching {@link MappedResource} for the provided target. - * - * @param target the target path - * @return the matching resource, or null if no match. - */ - public WebSocketNegotiator getMatchedNegotiator(String target, Consumer pathSpecConsumer) - { - MappedResource mapping = this.mappings.getMatch(target); if (mapping == null) - return null; + { + mapping = new WebSocketMapping(WebSocketComponents.ensureWebSocketComponents(servletContext)); + servletContext.setAttribute(mappingKey, mapping); + } - pathSpecConsumer.accept(mapping.getPathSpec()); - return mapping.getResource(); + return mapping; } /** @@ -212,40 +128,115 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener throw new IllegalArgumentException("Unrecognized path spec syntax [" + rawSpec + "]"); } - public boolean upgrade(HttpServletRequest request, HttpServletResponse response, FrameHandler.Customizer defaultCustomizer) + public static final String DEFAULT_KEY = "org.eclipse.jetty.websocket.servlet.WebSocketMapping"; + + private final PathMappings mappings = new PathMappings<>(); + private final WebSocketComponents components; + private final Handshaker handshaker = Handshaker.newInstance(); + + public WebSocketMapping() { - try + this(new WebSocketComponents()); + } + + public WebSocketMapping(WebSocketComponents components) + { + this.components = components; + } + + @Override + public void lifeCycleStopping(LifeCycle context) + { + ContextHandler contextHandler = (ContextHandler) context; + WebSocketMapping mapping = contextHandler.getBean(WebSocketMapping.class); + if (mapping == this) { - // Since this may be a filter, we need to be smart about determining the target path. - // We should rely on the Container for stripping path parameters and its ilk before - // attempting to match a specific mapped websocket creator. - String target = request.getServletPath(); - if (request.getPathInfo() != null) - target = target + request.getPathInfo(); - - WebSocketNegotiator negotiator = getMatchedNegotiator(target, pathSpec -> - { - // Store PathSpec resource mapping as request attribute, for WebSocketCreator - // implementors to use later if they wish - request.setAttribute(PathSpec.class.getName(), pathSpec); - }); - - if (negotiator == null) - return false; - - if (LOG.isDebugEnabled()) - LOG.debug("WebSocket Negotiated detected on {} for endpoint {}", target, negotiator); - - // We have an upgrade request - return handshaker.upgradeRequest(negotiator, request, response, defaultCustomizer); + contextHandler.removeBean(mapping); + mappings.reset(); } - catch (Exception e) + } + + @Override + public String dump() + { + return Dumpable.dump(this); + } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + Dumpable.dumpObjects(out, indent, this, mappings); + } + + /** + * Manually add a WebSocket mapping. + *

    + * If mapping is added before this configuration is started, then it is persisted through + * stop/start of this configuration's lifecycle. Otherwise it will be removed when + * this configuration is stopped. + *

    + * + * @param pathSpec the pathspec to respond on + * @param creator the websocket creator to activate on the provided mapping. + * @param factory the factory to use to create a FrameHandler for the websocket + * @param customizer the customizer to use to customize the WebSocket session. + */ + public void addMapping(PathSpec pathSpec, WebSocketCreator creator, FrameHandlerFactory factory, FrameHandler.Customizer customizer) throws WebSocketException + { + mappings.put(pathSpec, new Negotiator(creator, factory, customizer)); + } + + public WebSocketCreator getMapping(PathSpec pathSpec) + { + Negotiator cn = mappings.get(pathSpec); + return cn == null?null:cn.getWebSocketCreator(); + } + + public boolean removeMapping(PathSpec pathSpec) + { + return mappings.remove(pathSpec); + } + + /** + * Get the matching {@link MappedResource} for the provided target. + * + * @param target the target path + * @return the matching resource, or null if no match. + */ + public WebSocketNegotiator getMatchedNegotiator(String target, Consumer pathSpecConsumer) + { + MappedResource mapping = this.mappings.getMatch(target); + if (mapping == null) + return null; + + pathSpecConsumer.accept(mapping.getPathSpec()); + return mapping.getResource(); + } + + public boolean upgrade(HttpServletRequest request, HttpServletResponse response, FrameHandler.Customizer defaultCustomizer) throws IOException + { + // Since this may be a filter, we need to be smart about determining the target path. + // We should rely on the Container for stripping path parameters and its ilk before + // attempting to match a specific mapped websocket creator. + String target = request.getServletPath(); + if (request.getPathInfo() != null) + target = target + request.getPathInfo(); + + WebSocketNegotiator negotiator = getMatchedNegotiator(target, pathSpec -> { - if (LOG.isDebugEnabled()) - LOG.debug("Unable to upgrade: "+e); - LOG.ignore(e); - } - return false; + // Store PathSpec resource mapping as request attribute, for WebSocketCreator + // implementors to use later if they wish + request.setAttribute(PathSpec.class.getName(), pathSpec); + }); + + if (negotiator == null) + return false; + + if (LOG.isDebugEnabled()) + LOG.debug("WebSocket Negotiated detected on {} for endpoint {}", target, negotiator); + + // We have an upgrade request + return handshaker.upgradeRequest(negotiator, request, response, defaultCustomizer); } private class Negotiator extends WebSocketNegotiator.AbstractNegotiator @@ -267,7 +258,7 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener @Override - public FrameHandler negotiate(Negotiation negotiation) + public FrameHandler negotiate(Negotiation negotiation) throws IOException { ServletContext servletContext = negotiation.getRequest().getServletContext(); if (servletContext == null) @@ -302,14 +293,6 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener return null; } - catch (IOException e) - { - throw new RuntimeIOException(e); - } - catch (URISyntaxException e) - { - throw new RuntimeIOException("Unable to negotiate websocket due to mangled request URI", e); - } finally { Thread.currentThread().setContextClassLoader(old); diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java index ddd8f85b6e9..0e7d7ba0714 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java @@ -30,7 +30,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.core.FrameHandler; @@ -72,7 +71,7 @@ import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; * Configuration / Init-Parameters: *

    *
    - *
    maxIdleTime
    + *
    idleTimeout
    *
    set the time in ms that a websocket may be idle before closing
    *
    maxTextMessageSize
    *
    set the size in UTF-8 bytes that a websocket may be accept as a Text Message before closing
    @@ -90,8 +89,6 @@ import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; @SuppressWarnings("serial") public abstract class WebSocketServlet extends HttpServlet { - // TODO This servlet should be split into an API neutral version and a Jetty API specific one. - private static final Logger LOG = Log.getLogger(WebSocketServlet.class); private final CustomizedWebSocketServletFactory customizer = new CustomizedWebSocketServletFactory(); @@ -105,7 +102,12 @@ public abstract class WebSocketServlet extends HttpServlet * {@link ContextHandler}, which in practise will mostly the the Jetty WebSocket API factory. * @param factory the WebSocketServletFactory */ - public abstract void configure(WebSocketServletFactory factory); + protected abstract void configure(WebSocketServletFactory factory); + + /** + * @return the instance of {@link FrameHandlerFactory} to be used to create the FrameHandler + */ + public abstract FrameHandlerFactory getFactory(); @Override public void init() throws ServletException @@ -117,7 +119,13 @@ public abstract class WebSocketServlet extends HttpServlet components = WebSocketComponents.ensureWebSocketComponents(servletContext); mapping = new WebSocketMapping(components); - String max = getInitParameter("maxIdleTime"); + String max = getInitParameter("idleTimeout"); + if (max == null) + { + max = getInitParameter("maxIdleTime"); + if (max != null) + LOG.warn("'maxIdleTime' init param is deprecated, use 'idleTimeout' instead"); + } if (max != null) customizer.setIdleTimeout(Duration.ofMillis(Long.parseLong(max))); @@ -159,7 +167,8 @@ public abstract class WebSocketServlet extends HttpServlet protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - if (mapping.upgrade(req, resp, customizer)) + // provide a null default customizer the customizer will be on the negotiator in the mapping + if (mapping.upgrade(req, resp, null)) return; // If we reach this point, it means we had an incoming request to upgrade @@ -175,6 +184,7 @@ public abstract class WebSocketServlet extends HttpServlet private class CustomizedWebSocketServletFactory extends FrameHandler.ConfigurationCustomizer implements WebSocketServletFactory { + @Override public WebSocketExtensionRegistry getExtensionRegistry() { return components.getExtensionRegistry(); @@ -189,15 +199,7 @@ public abstract class WebSocketServlet extends HttpServlet @Override public void addMapping(PathSpec pathSpec, WebSocketCreator creator) { - // TODO a bit fragile. This code knows that only the JettyFHF is added directly as a been - ServletContext servletContext = getServletContext(); - ContextHandler contextHandler = ServletContextHandler.getServletContextHandler(servletContext, "WebSocketServlet"); - FrameHandlerFactory frameHandlerFactory = contextHandler.getBean(FrameHandlerFactory.class); - - if (frameHandlerFactory==null) - throw new IllegalStateException("No known FrameHandlerFactory"); - - mapping.addMapping(pathSpec, creator, frameHandlerFactory, this); + mapping.addMapping(pathSpec, creator, getFactory(), this); } @Override diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java index d3b20236d9e..338798f3f86 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java @@ -21,39 +21,60 @@ package org.eclipse.jetty.websocket.servlet; import java.time.Duration; import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; -public interface WebSocketServletFactory +public interface WebSocketServletFactory extends FrameHandler.Configuration { WebSocketExtensionRegistry getExtensionRegistry(); + @Override Duration getIdleTimeout(); + @Override void setIdleTimeout(Duration duration); + @Override + Duration getWriteTimeout(); + + @Override + void setWriteTimeout(Duration duration); + + @Override int getInputBufferSize(); + @Override void setInputBufferSize(int bufferSize); + @Override long getMaxFrameSize(); + @Override void setMaxFrameSize(long maxFrameSize); + @Override long getMaxBinaryMessageSize(); + @Override void setMaxBinaryMessageSize(long bufferSize); + @Override long getMaxTextMessageSize(); + @Override void setMaxTextMessageSize(long bufferSize); + @Override int getOutputBufferSize(); + @Override void setOutputBufferSize(int bufferSize); + @Override boolean isAutoFragment(); + @Override void setAutoFragment(boolean autoFragment); void addMapping(String pathSpec, WebSocketCreator creator); diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketUpgradeFilter.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketUpgradeFilter.java index cca2d9400d3..368a4b3b47d 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketUpgradeFilter.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketUpgradeFilter.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.websocket.servlet; import java.io.IOException; import java.time.Duration; import java.util.EnumSet; + import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -54,7 +55,7 @@ import org.eclipse.jetty.websocket.core.WebSocketComponents; * Configuration / Init-Parameters: *

    *
    - *
    maxIdleTime
    + *
    idleTimeout
    *
    set the time in ms that a websocket may be idle before closing
    *
    maxTextMessageSize
    *
    set the size in UTF-8 bytes that a websocket may be accept as a Text Message before closing
    @@ -82,7 +83,7 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable for (FilterHolder holder : servletHandler.getFilters()) { - if (holder.getInitParameter(MAPPING_INIT_PARAM) != null) + if (holder.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM) != null) return holder; } @@ -100,7 +101,7 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable EnumSet dispatcherTypes = EnumSet.of(DispatcherType.REQUEST); FilterHolder holder = new FilterHolder(new WebSocketUpgradeFilter()); holder.setName(name); - holder.setInitParameter(MAPPING_INIT_PARAM, WebSocketMapping.DEFAULT_KEY); + holder.setInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM, WebSocketMapping.DEFAULT_KEY); holder.setAsyncSupported(true); ServletHandler servletHandler = ContextHandler.getContextHandler(servletContext).getChildHandlerByClass(ServletHandler.class); @@ -110,7 +111,7 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable return holder; } - public final static String MAPPING_INIT_PARAM = "org.eclipse.jetty.websocket.servlet.WebSocketMapping.key"; + public final static String MAPPING_ATTRIBUTE_INIT_PARAM = "org.eclipse.jetty.websocket.servlet.WebSocketMapping.key"; private final FrameHandler.ConfigurationCustomizer defaultCustomizer = new FrameHandler.ConfigurationCustomizer(); private WebSocketMapping mapping; @@ -157,13 +158,19 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable { final ServletContext context = config.getServletContext(); - String mappingKey = config.getInitParameter(MAPPING_INIT_PARAM); + String mappingKey = config.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM); if (mappingKey != null) mapping = WebSocketMapping.ensureMapping(context, mappingKey); else mapping = new WebSocketMapping(WebSocketComponents.ensureWebSocketComponents(context)); - String max = config.getInitParameter("maxIdleTime"); + String max = config.getInitParameter("idleTimeout"); + if (max == null) + { + max = config.getInitParameter("maxIdleTime"); + if (max != null) + LOG.warn("'maxIdleTime' init param is deprecated, use 'idleTimeout' instead"); + } if (max != null) defaultCustomizer.setIdleTimeout(Duration.ofMillis(Long.parseLong(max))); @@ -193,4 +200,9 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable if (autoFragment != null) defaultCustomizer.setAutoFragment(Boolean.parseBoolean(autoFragment)); } + + @Override + public void destroy() + { + } } diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/internal/UpgradeHttpServletRequest.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/internal/UpgradeHttpServletRequest.java index 50c3611c3a9..f80ec447d9f 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/internal/UpgradeHttpServletRequest.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/internal/UpgradeHttpServletRequest.java @@ -130,6 +130,12 @@ public class UpgradeHttpServletRequest implements HttpServletRequest attributes.put(name, httpRequest.getAttribute(name)); } + Enumeration localeElements = httpRequest.getLocales(); + while (localeElements.hasMoreElements()) + { + locales.add(localeElements.nextElement()); + } + localAddress = InetSocketAddress.createUnresolved(httpRequest.getLocalAddr(), httpRequest.getLocalPort()); localName = httpRequest.getLocalName(); remoteAddress = InetSocketAddress.createUnresolved(httpRequest.getRemoteAddr(), httpRequest.getRemotePort()); diff --git a/jetty-xml/src/main/java/org/eclipse/jetty/xml/ConfigurationProcessor.java b/jetty-xml/src/main/java/org/eclipse/jetty/xml/ConfigurationProcessor.java index c67b25899dc..1262e44df63 100644 --- a/jetty-xml/src/main/java/org/eclipse/jetty/xml/ConfigurationProcessor.java +++ b/jetty-xml/src/main/java/org/eclipse/jetty/xml/ConfigurationProcessor.java @@ -18,21 +18,28 @@ package org.eclipse.jetty.xml; -import java.net.URL; +import org.eclipse.jetty.util.resource.Resource; /** * A ConfigurationProcessor for non XmlConfiguration format files. *

    * A file in non-XmlConfiguration file format may be processed by a {@link ConfigurationProcessor} * instance that is returned from a {@link ConfigurationProcessorFactory} instance discovered by the - * ServiceLoader mechanism. This is used to allow spring configuration files to be used instead of - * jetty.xml - * + * {@code ServiceLoader} mechanism. This is used to allow spring configuration files to be used instead of + * {@code jetty.xml} */ public interface ConfigurationProcessor { - public void init(URL url, XmlParser.Node root, XmlConfiguration configuration); - - public Object configure( Object obj) throws Exception; - public Object configure() throws Exception; + /** + * Initialize a ConfigurationProcessor from provided Resource and XML + * + * @param resource the resource being read + * @param root the parsed XML root node for the resource + * @param configuration the configuration being used (typically for ref IDs) + */ + void init(Resource resource, XmlParser.Node root, XmlConfiguration configuration); + + Object configure(Object obj) throws Exception; + + Object configure() throws Exception; } diff --git a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java index d9061ec3aa5..ce780e55e3a 100644 --- a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java +++ b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java @@ -20,7 +20,7 @@ package org.eclipse.jetty.xml; import java.io.IOException; import java.io.InputStream; -import java.io.StringReader; +import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -29,7 +29,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.InetAddress; import java.net.MalformedURLException; -import java.net.URI; import java.net.URL; import java.net.UnknownHostException; import java.nio.file.Path; @@ -38,27 +37,30 @@ import java.security.AccessController; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.Queue; import java.util.ServiceLoader; import java.util.Set; +import org.eclipse.jetty.util.ArrayUtil; import org.eclipse.jetty.util.LazyList; import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.MultiException; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.annotation.Name; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; -import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** @@ -69,29 +71,35 @@ import org.xml.sax.SAXException; * {@link ConfigurationProcessorFactory} interface to be found by the * {@link ServiceLoader} by using the DTD and first tag element in the file. * Note that DTD will be null if validation is off.

    - *

    - * The configuration can be parameterised with properties that are looked up via the - * Property XML element and set on the configuration via the map returned from + *

    The configuration can be parameterised with properties that are looked up via the + * Property XML element and set on the configuration via the map returned from * {@link #getProperties()}

    - *

    - * The configuration can create and lookup beans by ID. If multiple configurations are used, then it - * is good practise to copy the entries from the {@link #getIdMap()} of a configuration to the next + *

    The configuration can create and lookup beans by ID. If multiple configurations are used, then it + * is good practise to copy the entries from the {@link #getIdMap()} of a configuration to the next * configuration so that they can share an ID space for beans.

    */ public class XmlConfiguration { private static final Logger LOG = Log.getLogger(XmlConfiguration.class); private static final Class[] __primitives = - {Boolean.TYPE, Character.TYPE, Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE, Void.TYPE}; + { + Boolean.TYPE, Character.TYPE, Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE, Void.TYPE + }; private static final Class[] __boxedPrimitives = - {Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Void.class}; + { + Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Void.class + }; private static final Class[] __supportedCollections = - {ArrayList.class, HashSet.class, Queue.class, List.class, Set.class, Collection.class}; + { + ArrayList.class, HashSet.class, Queue.class, List.class, Set.class, Collection.class + }; private static final Iterable __factoryLoader = ServiceLoader.load(ConfigurationProcessorFactory.class); private static final XmlParser __parser = initParser(); + private static XmlParser initParser() { XmlParser parser = new XmlParser(); + Class klass = XmlConfiguration.class; URL config60 = klass.getResource("configure_6_0.dtd"); URL config76 = klass.getResource("configure_7_6.dtd"); @@ -111,15 +119,17 @@ public class XmlConfiguration parser.redirectEntity("http://jetty.mortbay.org/configure_9_3.dtd",config93); parser.redirectEntity("http://jetty.eclipse.org/configure.dtd",config93); parser.redirectEntity("http://www.eclipse.org/jetty/configure.dtd",config93); + parser.redirectEntity("https://www.eclipse.org/jetty/configure.dtd",config93); parser.redirectEntity("http://www.eclipse.org/jetty/configure_9_3.dtd",config93); - + parser.redirectEntity("https://www.eclipse.org/jetty/configure_9_3.dtd",config93); + parser.redirectEntity("-//Mort Bay Consulting//DTD Configure//EN",config93); parser.redirectEntity("-//Jetty//Configure//EN",config93); return parser; } - /** + /** * Set the standard IDs and properties expected in a jetty XML file: *
      *
    • RefId Server
    • @@ -130,6 +140,7 @@ public class XmlConfiguration *
    • Property jetty.webapps
    • *
    • Property jetty.webapps.uri
    • *
    + * * @param server The Server object to set * @param webapp The webapps Resource */ @@ -137,155 +148,111 @@ public class XmlConfiguration { try { - if (server!=null) + if (server != null) getIdMap().put("Server", server); Path home = Paths.get(System.getProperty("jetty.home", ".")); - getProperties().put("jetty.home",home.toString()); - getProperties().put("jetty.home.uri",normalizeURI(home.toUri().toASCIIString())); + getProperties().put("jetty.home", home.toString()); + getProperties().put("jetty.home.uri", normalizeURI(home.toUri().toASCIIString())); Path base = Paths.get(System.getProperty("jetty.base", home.toString())); - getProperties().put("jetty.base",base.toString()); - getProperties().put("jetty.base.uri",normalizeURI(base.toUri().toASCIIString())); + getProperties().put("jetty.base", base.toString()); + getProperties().put("jetty.base.uri", normalizeURI(base.toUri().toASCIIString())); - if (webapp!=null) + if (webapp != null) { Path webappPath = webapp.getFile().toPath().toAbsolutePath(); getProperties().put("jetty.webapp", webappPath.toString()); getProperties().put("jetty.webapps", webappPath.getParent().toString()); - getProperties().put("jetty.webapps.uri", normalizeURI(webapp.getURI().toString())); + getProperties().put("jetty.webapps.uri", normalizeURI(webappPath.getParent().toUri().toString())); } } - catch(Exception e) + catch (Exception e) { LOG.warn(e); } } - + public static String normalizeURI(String uri) { if (uri.endsWith("/")) - return uri.substring(0,uri.length()-1); + return uri.substring(0, uri.length() - 1); return uri; } - + private final Map _idMap = new HashMap<>(); private final Map _propertyMap = new HashMap<>(); - private final URL _url; + private final Resource _location; private final String _dtd; private ConfigurationProcessor _processor; /** * Reads and parses the XML configuration file. * - * @param configuration the URL of the XML configuration + * @param resource the Resource to the XML configuration * @throws IOException if the configuration could not be read * @throws SAXException if the configuration could not be parsed */ - public XmlConfiguration(URL configuration) throws SAXException, IOException + public XmlConfiguration(Resource resource) throws SAXException, IOException { synchronized (__parser) { - _url=configuration; - setConfig(__parser.parse(configuration.toString())); - _dtd=__parser.getDTD(); + _location = resource; + try(InputStream inputStream = resource.getInputStream()) + { + setConfig(__parser.parse(inputStream)); + } + _dtd = __parser.getDTD(); } } - /** - * Reads and parses the XML configuration file. - * - * @param configuration the URI of the XML configuration - * @throws IOException if the configuration could not be read - * @throws SAXException if the configuration could not be parsed - */ - public XmlConfiguration(URI configuration) throws SAXException, IOException + @Override + public String toString() { - synchronized (__parser) + if (_location == null) { - _url=configuration.toURL(); - setConfig(__parser.parse(configuration.toString())); - _dtd=__parser.getDTD(); - } - } - - /** - * Reads and parses the XML configuration string. - * - * @param configuration String of XML configuration commands excluding the normal XML preamble. - * The String should start with a "<Configure ....>" element. - * @throws IOException if the configuration could not be read - * @throws SAXException if the configuration could not be parsed - */ - public XmlConfiguration(String configuration) throws SAXException, IOException - { - configuration = "\n" - + configuration; - InputSource source = new InputSource(new StringReader(configuration)); - synchronized (__parser) - { - _url=null; - setConfig( __parser.parse(source)); - _dtd=__parser.getDTD(); - } - } - - /** - * Reads and parses the XML configuration stream. - * - * @param configuration An input stream containing a complete configuration file - * @throws IOException if the configuration could not be read - * @throws SAXException if the configuration could not be parsed - */ - public XmlConfiguration(InputStream configuration) throws SAXException, IOException - { - InputSource source = new InputSource(configuration); - synchronized (__parser) - { - _url=null; - setConfig(__parser.parse(source)); - _dtd=__parser.getDTD(); + return "UNKNOWN-LOCATION"; } + return _location.toString(); } private void setConfig(XmlParser.Node config) { if ("Configure".equals(config.getTag())) { - _processor=new JettyXmlConfiguration(); + _processor = new JettyXmlConfiguration(); } - else if (__factoryLoader!=null) + else if (__factoryLoader != null) { for (ConfigurationProcessorFactory factory : __factoryLoader) { _processor = factory.getConfigurationProcessor(_dtd, config.getTag()); - if (_processor!=null) + if (_processor != null) break; } - - if (_processor==null) - throw new IllegalStateException("Unknown configuration type: "+config.getTag()+" in "+this); + if (_processor == null) + throw new IllegalStateException("Unknown configuration type: " + config.getTag() + " in " + this); } else { - throw new IllegalArgumentException("Unknown XML tag:"+config.getTag()); + throw new IllegalArgumentException("Unknown XML tag:" + config.getTag()); } - _processor.init(_url,config,this); + _processor.init(_location, config, this); } - /* ------------------------------------------------------------ */ - /** Get the map of ID String to Objects that is used to hold - * and lookup any objects by ID. + /** + * Get the map of ID String to Objects that is used to hold + * and lookup any objects by ID. *

    * A New, Get or Call XML element may have an * id attribute which will cause the resulting object to be placed into * this map. A Ref XML element will lookup an object from this map.

    *

    - * When chaining configuration files, it is good practise to copy the + * When chaining configuration files, it is good practise to copy the * ID entries from the ID map to the map of the next configuration, so * that they may share an ID space *

    - * + * * @return A modifiable map of ID strings to Objects */ public Map getIdMap() @@ -293,10 +260,10 @@ public class XmlConfiguration return _idMap; } - /* ------------------------------------------------------------ */ /** * Get the map of properties used by the Property XML element - * to parameterise configuration. + * to parametrize configuration. + * * @return A modifiable map of properties. */ public Map getProperties() @@ -309,8 +276,8 @@ public class XmlConfiguration * * @param obj The object to be configured, which must be of a type or super type * of the class attribute of the <Configure> element. - * @throws Exception if the configuration fails * @return the configured object + * @throws Exception if the configuration fails */ public Object configure(Object obj) throws Exception { @@ -330,32 +297,29 @@ public class XmlConfiguration { return _processor.configure(); } - - /* ------------------------------------------------------------ */ - /** Initialize a new Object defaults. - *

    This method must be called by any {@link ConfigurationProcessor} when it - * creates a new instance of an object before configuring it, so that a derived + + /** + * Initialize a new Object defaults. + *

    This method must be called by any {@link ConfigurationProcessor} when it + * creates a new instance of an object before configuring it, so that a derived * XmlConfiguration class may inject default values. + * * @param object the object to initialize defaults on */ public void initializeDefaults(Object object) { } - private static class JettyXmlConfiguration implements ConfigurationProcessor { - - private String _url; XmlParser.Node _root; XmlConfiguration _configuration; @Override - public void init(URL url, XmlParser.Node root, XmlConfiguration configuration) + public void init(Resource resource, XmlParser.Node root, XmlConfiguration configuration) { - _url=url==null?null:url.toString(); - _root=root; - _configuration=configuration; + _root = root; + _configuration = configuration; } @Override @@ -365,13 +329,13 @@ public class XmlConfiguration Class oClass = nodeClass(_root); if (oClass != null && !oClass.isInstance(obj)) { - String loaders = (oClass.getClassLoader()==obj.getClass().getClassLoader())?"":"Object Class and type Class are from different loaders."; - throw new IllegalArgumentException("Object of class '"+obj.getClass().getCanonicalName()+"' is not of type '" + oClass.getCanonicalName()+"'. "+loaders+" in "+_url); + String loaders = (oClass.getClassLoader() == obj.getClass().getClassLoader()) ? "" : "Object Class and type Class are from different loaders."; + throw new IllegalArgumentException("Object of class '" + obj.getClass().getCanonicalName() + "' is not of type '" + oClass.getCanonicalName() + "'. " + loaders + " in " + _configuration); } - String id=_root.getAttribute("id"); - if (id!=null) - _configuration.getIdMap().put(id,obj); - configure(obj,_root,0); + String id = _root.getAttribute("id"); + if (id != null) + _configuration.getIdMap().put(id, obj); + configure(obj, _root, 0); return obj; } @@ -381,7 +345,7 @@ public class XmlConfiguration Class oClass = nodeClass(_root); String id = _root.getAttribute("id"); - Object obj = id == null?null:_configuration.getIdMap().get(id); + Object obj = id == null ? null : _configuration.getIdMap().get(id); int index = 0; if (obj == null && oClass != null) @@ -394,41 +358,36 @@ public class XmlConfiguration { Object o = _root.get(i); if (o instanceof String) - { continue; - } - XmlParser.Node node = (XmlParser.Node)o; - if (!(node.getTag().equals("Arg"))) + XmlParser.Node node = (XmlParser.Node)o; + if (node.getTag().equals("Arg")) { - index = i; - break; + String namedAttribute = node.getAttribute("name"); + Object value = value(null, (XmlParser.Node)o); + if (namedAttribute != null) + namedArgMap.put(namedAttribute, value); + arguments.add(value); } else { - String namedAttribute = node.getAttribute("name"); - Object value=value(obj,(XmlParser.Node)o); - if (namedAttribute != null) - namedArgMap.put(namedAttribute,value); - arguments.add(value); + index = i; + break; } } try { - if (namedArgMap.size() > 0) - obj = TypeUtil.construct(oClass, arguments.toArray(), namedArgMap); - else - obj = TypeUtil.construct(oClass, arguments.toArray()); + obj = construct(oClass, arguments.toArray(), namedArgMap); } catch (NoSuchMethodException x) { - throw new IllegalStateException(String.format("No constructor %s(%s,%s) in %s",oClass,arguments,namedArgMap,_url)); + throw new IllegalStateException(String.format("No constructor %s(%s,%s) in %s", oClass, arguments, namedArgMap, _configuration)); } } - if (id!=null) - _configuration.getIdMap().put(id,obj); - + if (id != null) + _configuration.getIdMap().put(id, obj); + _configuration.initializeDefaults(obj); configure(obj, _root, index); return obj; @@ -439,7 +398,6 @@ public class XmlConfiguration String className = node.getAttribute("class"); if (className == null) return null; - return Loader.loadClass(className); } @@ -463,12 +421,12 @@ public class XmlConfiguration XmlParser.Node node = (XmlParser.Node)o; if ("Arg".equals(node.getTag())) { - LOG.warn("Ignored arg: "+node); + LOG.warn("Ignored arg: " + node); continue; } break; } - + // Process real arguments for (; i < cfg.size(); i++) { @@ -501,10 +459,10 @@ public class XmlConfiguration newArray(obj, node); break; case "Map": - newMap(obj,node); + newMap(obj, node); break; case "Ref": - refObj(obj, node); + refObj(node); break; case "Property": propertyObj(node); @@ -516,31 +474,37 @@ public class XmlConfiguration envObj(node); break; default: - throw new IllegalStateException("Unknown tag: " + tag + " in " + _url); + throw new IllegalStateException("Unknown tag: " + tag + " in " + _configuration); } } catch (Exception e) { - LOG.warn("Config error at " + node,e.toString()+" in "+_url); + LOG.warn("Config error at " + node, e.toString() + " in " + _configuration); throw e; } } } - /* - * Call a set method. This method makes a best effort to find a matching set method. The type of the value is used to find a suitable set method by 1. - * Trying for a trivial type match. 2. Looking for a native type match. 3. Trying all correctly named methods for an auto conversion. 4. Attempting to - * construct a suitable value from original value. @param obj + /** + *

    Call a setter method.

    + *

    This method makes a best effort to find a matching set method. + * The type of the value is used to find a suitable set method by:

    + *
      + *
    1. Trying for a trivial type match
    2. + *
    3. Looking for a native type match
    4. + *
    5. Trying all correctly named methods for an auto conversion
    6. + *
    7. Attempting to construct a suitable value from original value
    8. + *
    * - * @param node + * @param obj the enclosing object + * @param node the <Set> XML node */ private void set(Object obj, XmlParser.Node node) throws Exception { String attr = node.getAttribute("name"); - String name = "set" + attr.substring(0,1).toUpperCase(Locale.ENGLISH) + attr.substring(1); - Object value = value(obj,node); - Object[] arg = - { value }; + String name = "set" + attr.substring(0, 1).toUpperCase(Locale.ENGLISH) + attr.substring(1); + Object value = value(obj, node); + Object[] arg = {value}; Class oClass = nodeClass(node); if (oClass != null) @@ -548,21 +512,20 @@ public class XmlConfiguration else oClass = obj.getClass(); - Class[] vClass = - { Object.class }; + Class[] vClass = {Object.class}; if (value != null) vClass[0] = value.getClass(); if (LOG.isDebugEnabled()) - LOG.debug("XML " + (obj != null?obj.toString():oClass.getName()) + "." + name + "(" + value + ")"); + LOG.debug("XML " + (obj != null ? obj.toString() : oClass.getName()) + "." + name + "(" + value + ")"); MultiException me = new MultiException(); - + // Try for trivial match try { - Method set = oClass.getMethod(name,vClass); - set.invoke(obj,arg); + Method set = oClass.getMethod(name, vClass); + invokeMethod(set, obj, arg); return; } catch (IllegalArgumentException | IllegalAccessException | NoSuchMethodException e) @@ -576,8 +539,8 @@ public class XmlConfiguration { Field type = vClass[0].getField("TYPE"); vClass[0] = (Class)type.get(null); - Method set = oClass.getMethod(name,vClass); - set.invoke(obj,arg); + Method set = oClass.getMethod(name, vClass); + invokeMethod(set, obj, arg); return; } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException e) @@ -592,7 +555,7 @@ public class XmlConfiguration Field field = oClass.getField(attr); if (Modifier.isPublic(field.getModifiers())) { - field.set(obj,value); + setField(field, obj, value); return; } } @@ -606,19 +569,19 @@ public class XmlConfiguration Method[] sets = oClass.getMethods(); Method set = null; String types = null; - for (int s = 0; sets != null && s < sets.length; s++) + for (Method setter : sets) { - if (sets[s].getParameterCount()!=1) + if (setter.getParameterCount() != 1) continue; - Class[] paramTypes = sets[s].getParameterTypes(); - if (name.equals(sets[s].getName())) + Class[] paramTypes = setter.getParameterTypes(); + if (name.equals(setter.getName())) { - types = types==null?paramTypes[0].getName():(types+","+paramTypes[0].getName()); + types = types == null ? paramTypes[0].getName() : (types + "," + paramTypes[0].getName()); // lets try it try { - set = sets[s]; - sets[s].invoke(obj,arg); + set = setter; + invokeMethod(set, obj, arg); return; } catch (IllegalArgumentException | IllegalAccessException e) @@ -630,11 +593,13 @@ public class XmlConfiguration try { for (Class c : __supportedCollections) + { if (paramTypes[0].isAssignableFrom(c)) { - sets[s].invoke(obj,convertArrayToCollection(value,c)); + invokeMethod(setter, obj, convertArrayToCollection(value, c)); return; } + } } catch (IllegalAccessException e) { @@ -664,7 +629,7 @@ public class XmlConfiguration Constructor cons = sClass.getConstructor(vClass); arg[0] = cons.newInstance(arg); _configuration.initializeDefaults(arg[0]); - set.invoke(obj,arg); + invokeMethod(set, obj, arg); return; } catch (NoSuchMethodException | IllegalAccessException | InstantiationException e) @@ -676,15 +641,45 @@ public class XmlConfiguration // No Joy String message = oClass + "." + name + "(" + vClass[0] + ")"; - if (types!=null) - message += ". Found setters for "+types; - throw new NoSuchMethodException(message) + if (types != null) + message += ". Found setters for " + types; + NoSuchMethodException failure = new NoSuchMethodException(message); + for (int i = 0; i < me.size(); i++) { - { - for (int i=0; i constructor, Object... args) throws IllegalAccessException, InvocationTargetException, InstantiationException + { + Object result = constructor.newInstance(args); + if (constructor.getAnnotation(Deprecated.class) != null) + LOG.warn("Deprecated constructor {} in {}", constructor, _configuration); + return result; + } + + private Object invokeMethod(Method method, Object obj, Object... args) throws IllegalAccessException, InvocationTargetException + { + Object result = method.invoke(obj, args); + if (method.getAnnotation(Deprecated.class) != null) + LOG.warn("Deprecated method {} in {}", method, _configuration); + return result; + } + + private Object getField(Field field, Object object) throws IllegalAccessException + { + Object result = field.get(object); + if (field.getAnnotation(Deprecated.class) != null) + LOG.warn("Deprecated field {} in {}", field, _configuration); + return result; + } + + private void setField(Field field, Object obj, Object arg) throws IllegalAccessException + { + field.set(obj, arg); + if (field.getAnnotation(Deprecated.class) != null) + LOG.warn("Deprecated field {} in {}", field, _configuration); } /** @@ -694,6 +689,8 @@ public class XmlConfiguration */ private static Collection convertArrayToCollection(Object array, Class collectionType) { + if (array == null) + return null; Collection collection = null; if (array.getClass().isArray()) { @@ -702,7 +699,7 @@ public class XmlConfiguration else if (collectionType.isAssignableFrom(HashSet.class)) collection = new HashSet<>(convertArrayToArrayList(array)); } - if (collection==null) + if (collection == null) throw new IllegalArgumentException("Can't convert \"" + array.getClass() + "\" to " + collectionType); return collection; } @@ -712,14 +709,17 @@ public class XmlConfiguration int length = Array.getLength(array); ArrayList list = new ArrayList<>(length); for (int i = 0; i < length; i++) - list.add(Array.get(array,i)); + { + list.add(Array.get(array, i)); + } return list; } - /* - * Call a put method. + /** + *

    Calls a put method.

    * - * @param obj @param node + * @param obj the enclosing map object + * @param node the <Put> XML node */ private void put(Object obj, XmlParser.Node node) throws Exception { @@ -730,15 +730,19 @@ public class XmlConfiguration String name = node.getAttribute("name"); Object value = value(obj, node); - map.put(name,value); + map.put(name, value); if (LOG.isDebugEnabled()) LOG.debug("XML " + obj + ".put(" + name + "," + value + ")"); } - /* - * Call a get method. Any object returned from the call is passed to the configure method to consume the remaining elements. @param obj @param node - * If class attribute is given and the name is "class", then the class instance itself is returned. - * @return @exception Exception + /** + *

    Calls a getter method.

    + *

    Any object returned from the call is passed to the configure method to consume the remaining elements.

    + *

    If the "class" attribute is present and its value is "class", then the class instance itself is returned.

    + * + * @param obj the enclosing object + * @param node the <Get> XML node + * @return the result of the getter invocation */ private Object get(Object obj, XmlParser.Node node) throws Exception { @@ -757,24 +761,27 @@ public class XmlConfiguration { // Handle getClass explicitly if ("class".equalsIgnoreCase(name)) - obj=oClass; + { + obj = oClass; + } else { - // try calling a getXxx method. - Method method = oClass.getMethod("get" + name.substring(0,1).toUpperCase(Locale.ENGLISH) + name.substring(1),(java.lang.Class[])null); - obj = method.invoke(obj,(java.lang.Object[])null); + // Try calling a getXxx method. + Method method = oClass.getMethod("get" + name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1)); + obj = invokeMethod(method, obj); } - if (id!=null) - _configuration.getIdMap().put(id,obj); - configure(obj,node,0); + if (id != null) + _configuration.getIdMap().put(id, obj); + configure(obj, node, 0); } catch (NoSuchMethodException nsme) { try { + // Try the field. Field field = oClass.getField(name); - obj = field.get(obj); - configure(obj,node,0); + obj = getField(field, obj); + configure(obj, node, 0); } catch (NoSuchFieldException nsfe) { @@ -784,86 +791,145 @@ public class XmlConfiguration return obj; } - /* - * Call a method. A method is selected by trying all methods with matching names and number of arguments. Any object returned from the call is passed to - * the configure method to consume the remaining elements. Note that if this is a static call we consider only methods declared directly in the given - * class. i.e. we ignore any static methods in superclasses. @param obj + /** + *

    Calls a method.

    + *

    A method is selected by trying all methods with matching names and number of arguments. + * Any object returned from the call is passed to the configure method to consume the remaining elements. + * Note that if this is a static call we consider only methods declared directly in the given class, + * i.e. we ignore any static methods in superclasses. * - * @param node @return @exception Exception + * @param obj the enclosing object + * @param node the <Call> XML node + * @return the result of the method invocation */ private Object call(Object obj, XmlParser.Node node) throws Exception { - AttrOrElementNode aoeNode=new AttrOrElementNode(obj,node,"Id","Name","Class","Arg"); + AttrOrElementNode aoeNode = new AttrOrElementNode(obj, node, "Id", "Name", "Class", "Arg"); String id = aoeNode.getString("Id"); String name = aoeNode.getString("Name"); String clazz = aoeNode.getString("Class"); List args = aoeNode.getList("Arg"); - - + Class oClass; - if (clazz!=null) + if (clazz != null) { // static call - oClass=Loader.loadClass(clazz); - obj=null; + oClass = Loader.loadClass(clazz); + obj = null; } - else if (obj!=null) + else if (obj != null) { oClass = obj.getClass(); } else throw new IllegalArgumentException(node.toString()); - + if (LOG.isDebugEnabled()) LOG.debug("XML call " + name); try { - Object nobj= TypeUtil.call(oClass,name,obj,args.toArray(new Object[args.size()])); + Object nobj = call(oClass, name, obj, args.toArray(new Object[0])); if (id != null) - _configuration.getIdMap().put(id,nobj); - configure(nobj,node,aoeNode.getNext()); + _configuration.getIdMap().put(id, nobj); + configure(nobj, node, aoeNode.getNext()); return nobj; } catch (NoSuchMethodException e) { - IllegalStateException ise = new IllegalStateException("No Method: " + node + " on " + oClass); - ise.initCause(e); - throw ise; + throw new IllegalStateException("No Method: " + node + " on " + oClass, e); } } - /* - * Create a new value object. + private Object call(Class oClass, String methodName, Object obj, Object[] arg) throws InvocationTargetException, NoSuchMethodException + { + Objects.requireNonNull(oClass, "Class cannot be null"); + Objects.requireNonNull(methodName, "Method name cannot be null"); + if (StringUtil.isBlank(methodName)) + throw new IllegalArgumentException("Method name cannot be blank"); + + // Lets just try all methods for now + for (Method method : oClass.getMethods()) + { + if (!method.getName().equals(methodName)) + continue; + if (method.getParameterCount() != arg.length) + continue; + if (Modifier.isStatic(method.getModifiers()) != (obj == null)) + continue; + if ((obj == null) && method.getDeclaringClass() != oClass) + continue; + + try + { + return invokeMethod(method, obj, arg); + } + catch (IllegalAccessException | IllegalArgumentException e) + { + LOG.ignore(e); + } + } + + // Lets look for a method with varargs arguments + Object[] argsWithVarargs = null; + for (Method method : oClass.getMethods()) + { + if (!method.getName().equals(methodName)) + continue; + if (method.getParameterCount() != arg.length + 1) + continue; + if (!method.getParameterTypes()[arg.length].isArray()) + continue; + if (Modifier.isStatic(method.getModifiers()) != (obj == null)) + continue; + if ((obj == null) && method.getDeclaringClass() != oClass) + continue; + + if (argsWithVarargs == null) + argsWithVarargs = ArrayUtil.addToArray(arg, new Object[0], Object.class); + try + { + return invokeMethod(method, obj, argsWithVarargs); + } + catch (IllegalAccessException | IllegalArgumentException e) + { + LOG.ignore(e); + } + } + + throw new NoSuchMethodException(methodName); + } + + /** + *

    Creates a new value object.

    * - * @param obj - * @param node - * - * @return @exception Exception + * @param obj the enclosing object + * @param node the <New> XML node + * @return the result of the constructor invocation */ private Object newObj(Object obj, XmlParser.Node node) throws Exception { - AttrOrElementNode aoeNode=new AttrOrElementNode(obj,node,"Id","Class","Arg"); + AttrOrElementNode aoeNode = new AttrOrElementNode(obj, node, "Id", "Class", "Arg"); String id = aoeNode.getString("Id"); String clazz = aoeNode.getString("Class"); List argNodes = aoeNode.getNodes("Arg"); if (LOG.isDebugEnabled()) LOG.debug("XML new " + clazz); - + Class oClass = Loader.loadClass(clazz); - + // Find the elements Map namedArgMap = new HashMap<>(); List arguments = new LinkedList<>(); for (XmlParser.Node child : argNodes) { String namedAttribute = child.getAttribute("name"); - Object value=value(obj,child); + Object value = value(obj, child); if (namedAttribute != null) { // named arguments - namedArgMap.put(namedAttribute,value); + namedArgMap.put(namedAttribute, value); } // raw arguments arguments.add(value); @@ -872,16 +938,7 @@ public class XmlConfiguration Object nobj; try { - if (namedArgMap.size() > 0) - { - LOG.debug("using named mapping"); - nobj = TypeUtil.construct(oClass, arguments.toArray(), namedArgMap); - } - else - { - LOG.debug("using normal mapping"); - nobj = TypeUtil.construct(oClass, arguments.toArray()); - } + nobj = construct(oClass, arguments.toArray(), namedArgMap); } catch (NoSuchMethodException e) { @@ -890,39 +947,127 @@ public class XmlConfiguration if (id != null) _configuration.getIdMap().put(id, nobj); - + _configuration.initializeDefaults(nobj); - configure(nobj,node,aoeNode.getNext()); + configure(nobj, node, aoeNode.getNext()); return nobj; } - /* - * Reference an id value object. + private Object construct(Class klass, Object[] arguments, Map namedArgMap) throws InvocationTargetException, NoSuchMethodException + { + Objects.requireNonNull(klass, "Class cannot be null"); + Objects.requireNonNull(namedArgMap, "Named Argument Map cannot be null"); + + for (Constructor constructor : klass.getConstructors()) + { + if (arguments == null) + { + // null arguments in .newInstance() is allowed + if (constructor.getParameterCount() != 0) + continue; + } + else if (constructor.getParameterCount() != arguments.length) + { + continue; + } + + try + { + if (arguments == null || arguments.length == 0) + { + if (LOG.isDebugEnabled()) + LOG.debug("Invoking constructor, no arguments"); + return invokeConstructor(constructor); + } + + if (namedArgMap.isEmpty()) + { + if (LOG.isDebugEnabled()) + LOG.debug("Invoking constructor, no XML parameter mapping"); + return invokeConstructor(constructor, arguments); + } + + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + if (parameterAnnotations == null || parameterAnnotations.length == 0) + { + if (LOG.isDebugEnabled()) + LOG.debug("Invoking constructor, no parameter annotations"); + return invokeConstructor(constructor, arguments); + } + + int count = 0; + Object[] swizzled = new Object[arguments.length]; + for (Annotation[] annotations : parameterAnnotations) + { + for (Annotation annotation : annotations) + { + if (annotation instanceof Name) + { + Name param = (Name)annotation; + if (namedArgMap.containsKey(param.value())) + { + if (LOG.isDebugEnabled()) + LOG.debug("Mapping named parameter {} in position {}", param.value(), count); + swizzled[count] = namedArgMap.get(param.value()); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("Mapping argument {} in position {}", arguments[count], count); + swizzled[count] = arguments[count]; + } + ++count; + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("Skipping parameter annotated with {}", annotation); + } + } + } + + return invokeConstructor(constructor, swizzled); + } + catch (InstantiationException | IllegalAccessException | IllegalArgumentException e) + { + LOG.ignore(e); + } + } + throw new NoSuchMethodException(""); + } + + /** + *

    Returns a reference object mapped to an id.

    * - * @param obj @param node @return @exception NoSuchMethodException @exception ClassNotFoundException @exception InvocationTargetException + * @param node the <Ref> XML node + * @return the result of the reference invocation */ - private Object refObj(Object obj, XmlParser.Node node) throws Exception + private Object refObj(XmlParser.Node node) throws Exception { String refid = node.getAttribute("refid"); - if (refid==null) + if (refid == null) refid = node.getAttribute("id"); - obj = _configuration.getIdMap().get(refid); - if (obj == null && node.size()>0) + Object obj = _configuration.getIdMap().get(refid); + if (obj == null && node.size() > 0) throw new IllegalStateException("No object for refid=" + refid); - configure(obj,node,0); + configure(obj, node, 0); return obj; } - /* - * Create a new array object. + /** + *

    Creates a new array object.

    + * + * @param obj the enclosing object + * @param node the <Array> XML node + * @return the newly created array */ private Object newArray(Object obj, XmlParser.Node node) throws Exception { - AttrOrElementNode aoeNode=new AttrOrElementNode(obj,node,"Id","Type","Item"); + AttrOrElementNode aoeNode = new AttrOrElementNode(obj, node, "Id", "Type", "Item"); String id = aoeNode.getString("Id"); String type = aoeNode.getString("Type"); List items = aoeNode.getNodes("Item"); - + // Get the type Class aClass = java.lang.Object.class; if (type != null) @@ -947,30 +1092,33 @@ public class XmlConfiguration } } } - - Object al = null; + Object al = null; for (XmlParser.Node item : items) { String nid = item.getAttribute("id"); - Object v = value(obj,item); - al = LazyList.add(al,(v == null && aClass.isPrimitive())?0:v); + Object v = value(obj, item); + al = LazyList.add(al, (v == null && aClass.isPrimitive()) ? 0 : v); if (nid != null) - _configuration.getIdMap().put(nid,v); + _configuration.getIdMap().put(nid, v); } - Object array = LazyList.toArray(al,aClass); + Object array = LazyList.toArray(al, aClass); if (id != null) - _configuration.getIdMap().put(id,array); + _configuration.getIdMap().put(id, array); return array; } - /* - * Create a new map object. + /** + *

    Creates a new map object.

    + * + * @param obj the enclosing object + * @param node the <Map> XML node + * @return the newly created map */ private Object newMap(Object obj, XmlParser.Node node) throws Exception { - AttrOrElementNode aoeNode=new AttrOrElementNode(node,"Id","Entry"); + AttrOrElementNode aoeNode = new AttrOrElementNode(node, "Id", "Entry"); String id = aoeNode.getString("Id"); List entries = aoeNode.getNodes("Entry"); @@ -1004,65 +1152,63 @@ public class XmlConfiguration String kid = key.getAttribute("id"); String vid = value.getAttribute("id"); - Object k = value(obj,key); - Object v = value(obj,value); - map.put(k,v); + Object k = value(obj, key); + Object v = value(obj, value); + map.put(k, v); if (kid != null) - _configuration.getIdMap().put(kid,k); + _configuration.getIdMap().put(kid, k); if (vid != null) - _configuration.getIdMap().put(vid,v); + _configuration.getIdMap().put(vid, v); } return map; } - /* - * Get a Property. + /** + *

    Returns the value of a property.

    * - * @param node - * @return - * @exception Exception + * @param node the <Property> XML node + * @return the property value */ private Object propertyObj(XmlParser.Node node) throws Exception { - AttrOrElementNode aoeNode=new AttrOrElementNode(node,"Id","Name","Deprecated","Default"); + AttrOrElementNode aoeNode = new AttrOrElementNode(node, "Id", "Name", "Deprecated", "Default"); String id = aoeNode.getString("Id"); - String name = aoeNode.getString("Name",true); + String name = aoeNode.getString("Name", true); List deprecated = aoeNode.getList("Deprecated"); String dftValue = aoeNode.getString("Default"); // Look for a value - Map properties = _configuration.getProperties(); + Map properties = _configuration.getProperties(); String value = properties.get(name); - - // Look for a deprecated name value - String alternate=null; + // Look for a deprecated name value + String alternate = null; if (!deprecated.isEmpty()) { for (Object d : deprecated) - { + { String v = properties.get(StringUtil.valueOf(d)); - if (v!=null) + if (v != null) { - if (value==null) + if (value == null) LOG.warn("Property '{}' is deprecated, use '{}' instead", d, name); else LOG.warn("Property '{}' is deprecated, value from '{}' used", d, name); } - if (alternate==null) - alternate=v;; + if (alternate == null) + alternate = v; } } // use alternate from deprecated - if (value==null) - value=alternate; - + if (value == null) + value = alternate; + // use default value - if (value==null) - value=dftValue; + if (value == null) + value = dftValue; // Set value if ID set if (id != null) @@ -1070,50 +1216,51 @@ public class XmlConfiguration return value; } - /* - * Get a SystemProperty. + /** + *

    Returns the value of a system property.

    * - * @param node - * @return - * @exception Exception + * @param node the <SystemProperty> XML node + * @return the property value */ private Object systemPropertyObj(XmlParser.Node node) throws Exception { - AttrOrElementNode aoeNode=new AttrOrElementNode(node,"Id","Name","Deprecated","Default"); + AttrOrElementNode aoeNode = new AttrOrElementNode(node, "Id", "Name", "Deprecated", "Default"); String id = aoeNode.getString("Id"); - String name = aoeNode.getString("Name",true); + String name = aoeNode.getString("Name", true); List deprecated = aoeNode.getList("Deprecated"); String dftValue = aoeNode.getString("Default"); // Look for a value String value = System.getProperty(name); - + // Look for a deprecated name value - String alternate=null; + String alternate = null; if (!deprecated.isEmpty()) { for (Object d : deprecated) - { - String v = System.getProperty(StringUtil.valueOf(d)); - if (v!=null) + { + if (d == null) + continue; + String v = System.getProperty(d.toString()); + if (v != null) { - if (value==null) + if (value == null) LOG.warn("SystemProperty '{}' is deprecated, use '{}' instead", d, name); else LOG.warn("SystemProperty '{}' is deprecated, value from '{}' used", d, name); } - if (alternate==null) - alternate=v;; + if (alternate == null) + alternate = v; } } // use alternate from deprecated - if (value==null) - value=alternate; - + if (value == null) + value = alternate; + // use default value - if (value==null) - value=dftValue; + if (value == null) + value = dftValue; // Set value if ID set if (id != null) @@ -1121,42 +1268,41 @@ public class XmlConfiguration return value; } - - /* - * Get a Environment Property. + + /** + *

    Returns the value of an environment property.

    * - * @param node - * @return - * @exception Exception + * @param node the <Env> XML node + * @return the environment property value */ private Object envObj(XmlParser.Node node) throws Exception { - AttrOrElementNode aoeNode=new AttrOrElementNode(node,"Id","Name","Deprecated","Default"); + AttrOrElementNode aoeNode = new AttrOrElementNode(node, "Id", "Name", "Deprecated", "Default"); String id = aoeNode.getString("Id"); - String name = aoeNode.getString("Name",true); + String name = aoeNode.getString("Name", true); List deprecated = aoeNode.getList("Deprecated"); String dftValue = aoeNode.getString("Default"); // Look for a value String value = System.getenv(name); - + // Look for a deprecated name value - if (value==null && !deprecated.isEmpty()) + if (value == null && !deprecated.isEmpty()) { for (Object d : deprecated) { value = System.getenv(StringUtil.valueOf(d)); - if (value!=null) + if (value != null) { LOG.warn("Property '{}' is deprecated, use '{}' instead", d, name); break; } } } - + // use default value - if (value==null) - value=dftValue; + if (value == null) + value = dftValue; // Set value if ID set if (id != null) @@ -1165,18 +1311,23 @@ public class XmlConfiguration return value; } - /* - * Get the value of an element. If no value type is specified, then white space is trimmed out of the value. If it contains multiple value elements they - * are added as strings before being converted to any specified type. @param node + /** + *

    Returns the scalar value of an element

    . + *

    If no value type is specified, then white space is trimmed out of the value. + * If it contains multiple value elements they are added as strings before being + * converted to any specified type.

    + * + * @param obj the enclosing object + * @param node the XML node + * @return the value of the XML node */ private Object value(Object obj, XmlParser.Node node) throws Exception { - Object value; - // Get the type String type = node.getAttribute("type"); // Try a ref lookup + Object value; String ref = node.getAttribute("ref"); if (ref != null) { @@ -1197,7 +1348,7 @@ public class XmlConfiguration int last = node.size() - 1; // Handle default trim type - if (type == null || !"String".equals(type)) + if (!"String".equals(type)) { // Skip leading white Object item; @@ -1230,8 +1381,10 @@ public class XmlConfiguration } if (first == last) + { // Single Item value - value = itemValue(obj,node.get(first)); + value = itemValue(obj, node.get(first)); + } else { // Get the multiple items as a single string @@ -1239,7 +1392,7 @@ public class XmlConfiguration for (int i = first; i <= last; i++) { Object item = node.get(i); - buf.append(itemValue(obj,item)); + buf.append(itemValue(obj, item)); } value = buf.toString(); } @@ -1261,14 +1414,14 @@ public class XmlConfiguration return value; } - if (isTypeMatchingClass(type,String.class)) + if (isTypeMatchingClass(type, String.class)) return value.toString(); Class pClass = TypeUtil.fromName(type); if (pClass != null) - return TypeUtil.valueOf(pClass,value.toString()); + return TypeUtil.valueOf(pClass, value.toString()); - if (isTypeMatchingClass(type,URL.class)) + if (isTypeMatchingClass(type, URL.class)) { if (value instanceof URL) return value; @@ -1282,7 +1435,7 @@ public class XmlConfiguration } } - if (isTypeMatchingClass(type,InetAddress.class)) + if (isTypeMatchingClass(type, InetAddress.class)) { if (value instanceof InetAddress) return value; @@ -1298,8 +1451,8 @@ public class XmlConfiguration for (Class collectionClass : __supportedCollections) { - if (isTypeMatchingClass(type,collectionClass)) - return convertArrayToCollection(value,collectionClass); + if (isTypeMatchingClass(type, collectionClass)) + return convertArrayToCollection(value, collectionClass); } throw new IllegalStateException("Unknown type " + type); @@ -1310,8 +1463,12 @@ public class XmlConfiguration return classToMatch.getSimpleName().equalsIgnoreCase(type) || classToMatch.getName().equals(type); } - /* - * Get the value of a single element. @param obj @param item @return @exception Exception + /** + *

    Returns recursively the value of an element.

    + * + * @param obj the enclosing object + * @param item the initial element value + * @return the recursive value of the element */ private Object itemValue(Object obj, Object item) throws Exception { @@ -1322,17 +1479,17 @@ public class XmlConfiguration XmlParser.Node node = (XmlParser.Node)item; String tag = node.getTag(); if ("Call".equals(tag)) - return call(obj,node); + return call(obj, node); if ("Get".equals(tag)) - return get(obj,node); + return get(obj, node); if ("New".equals(tag)) - return newObj(obj,node); + return newObj(obj, node); if ("Ref".equals(tag)) - return refObj(obj,node); + return refObj(node); if ("Array".equals(tag)) - return newArray(obj,node); + return newArray(obj, node); if ("Map".equals(tag)) - return newMap(obj,node); + return newMap(obj, node); if ("Property".equals(tag)) return propertyObj(node); if ("SystemProperty".equals(tag)) @@ -1340,10 +1497,9 @@ public class XmlConfiguration if ("Env".equals(tag)) return envObj(node); - LOG.warn("Unknown value tag: " + node,new Throwable()); + LOG.warn("Unknown value tag: " + node + " in " + _configuration, new Throwable()); return null; } - private class AttrOrElementNode { @@ -1352,41 +1508,40 @@ public class XmlConfiguration final Set _elements = new HashSet<>(); final int _next; - AttrOrElementNode(XmlParser.Node node,String... elements ) + AttrOrElementNode(XmlParser.Node node, String... elements) { - this(null,node,elements); + this(null, node, elements); } - - AttrOrElementNode(Object obj, XmlParser.Node node,String... elements ) + + AttrOrElementNode(Object obj, XmlParser.Node node, String... elements) { - _obj=obj; - _node=node; - for (String e:elements) - _elements.add(e); - - int next=0; - for (Object o: _node) + _obj = obj; + _node = node; + Collections.addAll(_elements, elements); + + int next = 0; + for (Object o : _node) { if (o instanceof String) { - if (((String)o).trim().length()==0) + if (((String)o).trim().length() == 0) { next++; continue; } break; } - + if (!(o instanceof XmlParser.Node)) break; - + XmlParser.Node n = (XmlParser.Node)o; if (!_elements.contains(n.getTag())) break; - + next++; } - _next=next; + _next = next; } public int getNext() @@ -1396,21 +1551,21 @@ public class XmlConfiguration public String getString(String elementName) throws Exception { - return StringUtil.valueOf(get(elementName,false)); + return StringUtil.valueOf(get(elementName, false)); } - - public String getString(String elementName, boolean manditory) throws Exception + + public String getString(String elementName, boolean mandatory) throws Exception { - return StringUtil.valueOf(get(elementName,manditory)); + return StringUtil.valueOf(get(elementName, mandatory)); } - - public Object get(String elementName, boolean manditory) throws Exception + + public Object get(String elementName, boolean mandatory) throws Exception { - String attrName=StringUtil.asciiToLowerCase(elementName); + String attrName = StringUtil.asciiToLowerCase(elementName); String attr = _node.getAttribute(attrName); - Object value=attr; - - for (int i=0;i<_next;i++) + Object value = attr; + + for (int i = 0; i < _next; i++) { Object o = _node.get(i); if (!(o instanceof XmlParser.Node)) @@ -1418,85 +1573,84 @@ public class XmlConfiguration XmlParser.Node n = (XmlParser.Node)o; if (elementName.equals(n.getTag())) { - if (attr!=null) - throw new IllegalStateException("Cannot have attr '"+attrName+"' and element '"+elementName+"'"); + if (attr != null) + throw new IllegalStateException("Cannot have attr '" + attrName + "' and element '" + elementName + "'"); - value=value(_obj,n); + value = value(_obj, n); break; } } - - if (manditory && value==null) - throw new IllegalStateException("Must have attr '"+attrName+"' or element '"+elementName+"'"); - + + if (mandatory && value == null) + throw new IllegalStateException("Must have attr '" + attrName + "' or element '" + elementName + "'"); + return value; } public List getList(String elementName) throws Exception { - return getList(elementName,false); + return getList(elementName, false); } - + public List getList(String elementName, boolean manditory) throws Exception { - String attrName=StringUtil.asciiToLowerCase(elementName); - final List values=new ArrayList<>(); - + String attrName = StringUtil.asciiToLowerCase(elementName); + final List values = new ArrayList<>(); + String attr = _node.getAttribute(attrName); - if (attr!=null) - values.addAll(StringUtil.csvSplit(null,attr,0,attr.length())); + if (attr != null) + values.addAll(StringUtil.csvSplit(null, attr, 0, attr.length())); - - for (int i=0;i<_next;i++) + for (int i = 0; i < _next; i++) { Object o = _node.get(i); if (!(o instanceof XmlParser.Node)) continue; XmlParser.Node n = (XmlParser.Node)o; - + if (elementName.equals(n.getTag())) { - if (attr!=null) - throw new IllegalStateException("Cannot have attr '"+attrName+"' and element '"+elementName+"'"); + if (attr != null) + throw new IllegalStateException("Cannot have attr '" + attrName + "' and element '" + elementName + "'"); - values.add(value(_obj,n)); + values.add(value(_obj, n)); } } - + if (manditory && values.isEmpty()) - throw new IllegalStateException("Must have attr '"+attrName+"' or element '"+elementName+"'"); + throw new IllegalStateException("Must have attr '" + attrName + "' or element '" + elementName + "'"); return values; } - - public List getNodes(String elementName) throws Exception + + public List getNodes(String elementName) { - String attrName=StringUtil.asciiToLowerCase(elementName); - final List values=new ArrayList<>(); - + String attrName = StringUtil.asciiToLowerCase(elementName); + final List values = new ArrayList<>(); + String attr = _node.getAttribute(attrName); - if (attr!=null) + if (attr != null) { - for (String a : StringUtil.csvSplit(null,attr,0,attr.length())) + for (String a : StringUtil.csvSplit(null, attr, 0, attr.length())) { // create a fake node - XmlParser.Node n = new XmlParser.Node(null,elementName,null); + XmlParser.Node n = new XmlParser.Node(null, elementName, null); n.add(a); values.add(n); } } - for (int i=0;i<_next;i++) + for (int i = 0; i < _next; i++) { Object o = _node.get(i); if (!(o instanceof XmlParser.Node)) continue; XmlParser.Node n = (XmlParser.Node)o; - + if (elementName.equals(n.getTag())) { - if (attr!=null) - throw new IllegalStateException("Cannot have attr '"+attrName+"' and element '"+elementName+"'"); + if (attr != null) + throw new IllegalStateException("Cannot have attr '" + attrName + "' and element '" + elementName + "'"); values.add(n); } @@ -1508,96 +1662,85 @@ public class XmlConfiguration } /** - * Run the XML configurations as a main application. The command line is used to obtain properties files (must be named '*.properties') and XmlConfiguration - * files. + * Runs the XML configurations as a main application. + * The command line is used to obtain properties files (must be named '*.properties') and XmlConfiguration files. *

    * Any property file on the command line is added to a combined Property instance that is passed to each configuration file via * {@link XmlConfiguration#getProperties()}. *

    - * Each configuration file on the command line is used to create a new XmlConfiguration instance and the {@link XmlConfiguration#configure()} method is used - * to create the configured object. If the resulting object is an instance of {@link LifeCycle}, then it is started. + * Each configuration file on the command line is used to create a new XmlConfiguration instance and the + * {@link XmlConfiguration#configure()} method is used to create the configured object. + * If the resulting object is an instance of {@link LifeCycle}, then it is started. *

    * Any IDs created in a configuration are passed to the next configuration file on the command line using {@link #getIdMap()}. * This allows objects with IDs created in one config file to be referenced in subsequent config files on the command line. * - * @param args - * array of property and xml configuration filenames or {@link Resource}s. + * @param args array of property and xml configuration filenames or {@link Resource}s. * @throws Exception if the XML configurations cannot be run */ public static void main(final String... args) throws Exception { try { - AccessController.doPrivileged(new PrivilegedExceptionAction() + AccessController.doPrivileged((PrivilegedExceptionAction)() -> { - @Override - public Void run() throws Exception + Properties properties = new Properties(); + properties.putAll(System.getProperties()); + + // For all arguments, load properties + for (String arg : args) { - Properties properties = null; - - // If no start.config properties, use clean slate - if (properties == null) + if (arg.indexOf('=') >= 0) { - // Add System Properties - properties = new Properties(); - properties.putAll(System.getProperties()); + int i = arg.indexOf('='); + properties.put(arg.substring(0, i), arg.substring(i + 1)); } - - // For all arguments, load properties - for (String arg : args) - { - if (arg.indexOf('=')>=0) - { - int i=arg.indexOf('='); - properties.put(arg.substring(0,i),arg.substring(i+1)); - } - else if (arg.toLowerCase(Locale.ENGLISH).endsWith(".properties")) - properties.load(Resource.newResource(arg).getInputStream()); - } - - // For all arguments, parse XMLs - XmlConfiguration last = null; - List objects = new ArrayList<>(args.length); - for (int i = 0; i < args.length; i++) - { - if (!args[i].toLowerCase(Locale.ENGLISH).endsWith(".properties") && (args[i].indexOf('=')<0)) - { - XmlConfiguration configuration = new XmlConfiguration(Resource.newResource(args[i]).getURI().toURL()); - if (last != null) - configuration.getIdMap().putAll(last.getIdMap()); - if (properties.size() > 0) - { - Map props = new HashMap<>(); - for (Object key : properties.keySet()) - { - props.put(key.toString(),String.valueOf(properties.get(key))); - } - configuration.getProperties().putAll(props); - } - - Object obj = configuration.configure(); - if (obj!=null && !objects.contains(obj)) - objects.add(obj); - last = configuration; - } - } - - // For all objects created by XmlConfigurations, start them if they are lifecycles. - for (Object obj : objects) - { - if (obj instanceof LifeCycle) - { - LifeCycle lc = (LifeCycle)obj; - if (!lc.isRunning()) - lc.start(); - } - } - - return null; + else if (arg.toLowerCase(Locale.ENGLISH).endsWith(".properties")) + properties.load(Resource.newResource(arg).getInputStream()); } + + // For all arguments, parse XMLs + XmlConfiguration last = null; + List objects = new ArrayList<>(args.length); + for (String arg : args) + { + if (!arg.toLowerCase(Locale.ENGLISH).endsWith(".properties") && (arg.indexOf('=') < 0)) + { + XmlConfiguration configuration = new XmlConfiguration(Resource.newResource(arg)); + if (last != null) + configuration.getIdMap().putAll(last.getIdMap()); + if (properties.size() > 0) + { + Map props = new HashMap<>(); + for (Object key : properties.keySet()) + { + props.put(key.toString(), String.valueOf(properties.get(key))); + } + configuration.getProperties().putAll(props); + } + + Object obj = configuration.configure(); + if (obj != null && !objects.contains(obj)) + objects.add(obj); + last = configuration; + } + } + + // For all objects created by XmlConfigurations, start them if they are lifecycles. + for (Object obj : objects) + { + if (obj instanceof LifeCycle) + { + LifeCycle lc = (LifeCycle)obj; + if (!lc.isRunning()) + lc.start(); + } + } + + return null; }); - } - catch (Error|Exception e) + } + catch (Error | Exception e) { LOG.warn(e); throw e; diff --git a/jetty-xml/src/test/java/org/eclipse/jetty/xml/AnnotatedTestConfiguration.java b/jetty-xml/src/test/java/org/eclipse/jetty/xml/AnnotatedTestConfiguration.java index f694d176792..9d1ff60db16 100644 --- a/jetty-xml/src/test/java/org/eclipse/jetty/xml/AnnotatedTestConfiguration.java +++ b/jetty-xml/src/test/java/org/eclipse/jetty/xml/AnnotatedTestConfiguration.java @@ -25,9 +25,18 @@ public class AnnotatedTestConfiguration private String first; private String second; private String third; - - AnnotatedTestConfiguration nested; - + private String deprecated; + private AnnotatedTestConfiguration nested; + // Do not remove deprecation, used in tests. + @Deprecated + public String obsolete; + + // Do not remove deprecation, used in tests. + @Deprecated + public AnnotatedTestConfiguration() + { + } + public AnnotatedTestConfiguration(@Name("first") String first, @Name("second") String second, @Name("third") String third) { this.first = first; @@ -74,5 +83,18 @@ public class AnnotatedTestConfiguration { this.nested = nested; } - + + // Do not remove deprecation, used in tests. + @Deprecated + public void setDeprecated(String value) + { + this.deprecated = value; + } + + // Do not remove deprecation, used in tests. + @Deprecated + public String getDeprecated() + { + return deprecated; + } } diff --git a/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java b/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java index bcc3798f7fa..70d86d773ec 100644 --- a/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java +++ b/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java @@ -18,23 +18,41 @@ package org.eclipse.jetty.xml; -import java.io.ByteArrayInputStream; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.net.URL; -import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.log.StdErrLog; +import org.eclipse.jetty.util.resource.PathResource; +import org.eclipse.jetty.util.resource.Resource; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xml.sax.SAXException; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -42,8 +60,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +@ExtendWith(WorkDirExtension.class) public class XmlConfigurationTest { + public WorkDir workDir; + protected String[] _configure=new String [] {"org/eclipse/jetty/xml/configureWithAttr.xml","org/eclipse/jetty/xml/configureWithElements.xml"}; private static final String STRING_ARRAY_XML = "String1String2"; @@ -53,7 +74,8 @@ public class XmlConfigurationTest public void testMortBay() throws Exception { URL url = XmlConfigurationTest.class.getResource("mortbay.xml"); - XmlConfiguration configuration = new XmlConfiguration(url); + Resource resource = Resource.newResource(url); + XmlConfiguration configuration = new XmlConfiguration(resource); configuration.configure(); } @@ -66,7 +88,7 @@ public class XmlConfigurationTest properties.put("whatever", "xxx"); TestConfiguration.VALUE=77; URL url = XmlConfigurationTest.class.getClassLoader().getResource(configure); - XmlConfiguration configuration = new XmlConfiguration(url); + XmlConfiguration configuration = new XmlConfiguration(Resource.newResource(url)); TestConfiguration tc = new TestConfiguration("tc"); configuration.getProperties().putAll(properties); configuration.configure(tc); @@ -145,7 +167,7 @@ public class XmlConfigurationTest URL url = XmlConfigurationTest.class.getClassLoader().getResource(configure); final AtomicInteger count = new AtomicInteger(0); - XmlConfiguration configuration = new XmlConfiguration(url) + XmlConfiguration configuration = new XmlConfiguration(Resource.newResource(url)) { @Override public void initializeDefaults(Object object) @@ -225,18 +247,26 @@ public class XmlConfigurationTest } } + public XmlConfiguration asXmlConfiguration(String rawXml) throws IOException, SAXException + { + Path testFile = workDir.getEmptyPathDir().resolve("raw.xml"); + try(BufferedWriter writer = Files.newBufferedWriter(testFile, UTF_8)) + { + writer.write(rawXml); + } + return new XmlConfiguration(new PathResource(testFile)); + } @Test public void testGetClass() throws Exception { - XmlConfiguration configuration = - new XmlConfiguration(""); + XmlConfiguration configuration = asXmlConfiguration(""); TestConfiguration tc = new TestConfiguration(); configuration.configure(tc); assertEquals(TestConfiguration.class,tc.testObject); configuration = - new XmlConfiguration(""); + asXmlConfiguration(""); configuration.configure(tc); assertEquals(String.class,tc.testObject); assertEquals("String",configuration.getIdMap().get("simple")); @@ -245,8 +275,7 @@ public class XmlConfigurationTest @Test public void testStringConfiguration() throws Exception { - XmlConfiguration configuration = - new XmlConfiguration("SetValue2"); + XmlConfiguration configuration = asXmlConfiguration("SetValue2"); TestConfiguration tc = new TestConfiguration(); configuration.configure(tc); assertEquals("SetValue", tc.testObject, "Set String 3"); @@ -256,8 +285,7 @@ public class XmlConfigurationTest @Test public void testMeaningfullSetException() throws Exception { - XmlConfiguration configuration = - new XmlConfiguration(""); + XmlConfiguration configuration = asXmlConfiguration(""); TestConfiguration tc = new TestConfiguration(); NoSuchMethodException e = assertThrows(NoSuchMethodException.class, () -> { @@ -270,7 +298,7 @@ public class XmlConfigurationTest @Test public void testListConstructorArg() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + STRING_ARRAY_XML + ""); TestConfiguration tc = new TestConfiguration(); @@ -283,7 +311,7 @@ public class XmlConfigurationTest @Test public void testTwoArgumentListConstructorArg() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + "" + STRING_ARRAY_XML + "" + "" + STRING_ARRAY_XML + "" @@ -298,7 +326,7 @@ public class XmlConfigurationTest @Test public void testListNotContainingArray() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "Some String"); TestConfiguration tc = new TestConfiguration(); @@ -310,7 +338,7 @@ public class XmlConfigurationTest @Test public void testSetConstructorArg() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + STRING_ARRAY_XML + ""); TestConfiguration tc = new TestConfiguration(); @@ -323,7 +351,7 @@ public class XmlConfigurationTest @Test public void testSetNotContainingArray() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "Some String"); TestConfiguration tc = new TestConfiguration(); assertThrows(IllegalArgumentException.class, ()->{ @@ -334,7 +362,7 @@ public class XmlConfigurationTest @Test public void testListSetterWithStringArray() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + STRING_ARRAY_XML + ""); TestConfiguration tc = new TestConfiguration(); assertThat("tc.getList() returns null as it's not configured yet",tc.getList(),is(nullValue())); @@ -345,7 +373,7 @@ public class XmlConfigurationTest @Test public void testListSetterWithPrimitiveArray() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + INT_ARRAY_XML + ""); TestConfiguration tc = new TestConfiguration(); assertThat("tc.getList() returns null as it's not configured yet",tc.getList(),is(nullValue())); @@ -356,7 +384,7 @@ public class XmlConfigurationTest @Test public void testNotSupportedLinkedListSetter() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + INT_ARRAY_XML + ""); TestConfiguration tc = new TestConfiguration(); assertThat("tc.getSet() returns null as it's not configured yet", tc.getList(), is(nullValue())); @@ -368,7 +396,7 @@ public class XmlConfigurationTest @Test public void testArrayListSetter() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + INT_ARRAY_XML + ""); TestConfiguration tc = new TestConfiguration(); assertThat("tc.getSet() returns null as it's not configured yet", tc.getList(), is(nullValue())); @@ -379,7 +407,7 @@ public class XmlConfigurationTest @Test public void testSetSetter() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + STRING_ARRAY_XML + ""); TestConfiguration tc = new TestConfiguration(); assertThat("tc.getSet() returns null as it's not configured yet", tc.getSet(), is(nullValue())); @@ -390,7 +418,7 @@ public class XmlConfigurationTest @Test public void testSetSetterWithPrimitiveArray() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + INT_ARRAY_XML + ""); TestConfiguration tc = new TestConfiguration(); assertThat("tc.getSet() returns null as it's not configured yet", tc.getSet(), is(nullValue())); @@ -401,7 +429,7 @@ public class XmlConfigurationTest @Test public void testMap() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " " + " " + @@ -425,7 +453,7 @@ public class XmlConfigurationTest @Test public void testConstructorNamedInjection() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg2 " + @@ -442,7 +470,7 @@ public class XmlConfigurationTest @Test public void testConstructorNamedInjectionOrdered() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg2 " + @@ -459,7 +487,7 @@ public class XmlConfigurationTest @Test public void testConstructorNamedInjectionUnOrdered() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg3 " + @@ -476,7 +504,7 @@ public class XmlConfigurationTest @Test public void testConstructorNamedInjectionOrderedMixed() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg2 " + @@ -493,7 +521,7 @@ public class XmlConfigurationTest @Test public void testConstructorNamedInjectionUnorderedMixed() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg3 " + " arg2 " + @@ -510,7 +538,7 @@ public class XmlConfigurationTest @Test public void testNestedConstructorNamedInjection() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg2 " + @@ -538,7 +566,7 @@ public class XmlConfigurationTest @Test public void testNestedConstructorNamedInjectionOrdered() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg2 " + @@ -565,7 +593,7 @@ public class XmlConfigurationTest @Test public void testNestedConstructorNamedInjectionUnOrdered() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg3 " + @@ -592,7 +620,7 @@ public class XmlConfigurationTest @Test public void testNestedConstructorNamedInjectionOrderedMixed() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg2 " + @@ -619,7 +647,7 @@ public class XmlConfigurationTest @Test public void testArgumentsGetIgnoredMissingDTD() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration(new ByteArrayInputStream(("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg2 " + @@ -631,8 +659,7 @@ public class XmlConfigurationTest " arg3\n" + " " + " " + - "").getBytes(StandardCharsets.ISO_8859_1))); -// XmlConfiguration xmlConfiguration = new XmlConfiguration(url); + ""); AnnotatedTestConfiguration atc = (AnnotatedTestConfiguration)xmlConfiguration.configure(); @@ -647,7 +674,7 @@ public class XmlConfigurationTest @Test public void testSetGetIgnoredMissingDTD() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration(new ByteArrayInputStream(("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg1 " + " arg2 " + @@ -659,8 +686,7 @@ public class XmlConfigurationTest " arg3 " + " " + " " + - "").getBytes(StandardCharsets.UTF_8))); -// XmlConfiguration xmlConfiguration = new XmlConfiguration(url); + ""); DefaultTestConfiguration atc = (DefaultTestConfiguration)xmlConfiguration.configure(); @@ -675,7 +701,7 @@ public class XmlConfigurationTest @Test public void testNestedConstructorNamedInjectionUnorderedMixed() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " arg3 " + " arg2 " + @@ -740,7 +766,7 @@ public class XmlConfigurationTest @Test public void testSetBooleanTrue() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " true" + ""); @@ -752,7 +778,7 @@ public class XmlConfigurationTest @Test public void testSetBooleanFalse() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " false" + ""); @@ -765,7 +791,7 @@ public class XmlConfigurationTest @Disabled public void testSetBadBoolean() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " tru" + ""); @@ -777,7 +803,7 @@ public class XmlConfigurationTest @Test public void testSetBadInteger() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " bad" + ""); @@ -790,7 +816,7 @@ public class XmlConfigurationTest @Test public void testSetBadExtraInteger() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " 100 bas" + ""); @@ -803,7 +829,7 @@ public class XmlConfigurationTest @Test public void testSetBadFloatInteger() throws Exception { - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " 1.5" + ""); @@ -818,7 +844,7 @@ public class XmlConfigurationTest { // No properties String defolt = "baz"; - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " " + ""); @@ -831,7 +857,7 @@ public class XmlConfigurationTest { String name = "foo"; String value = "foo"; - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " " + ""); @@ -845,7 +871,7 @@ public class XmlConfigurationTest { String name = "bar"; String value = "bar"; - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " " + ""); @@ -859,7 +885,7 @@ public class XmlConfigurationTest { String name = "bar"; String value = "bar"; - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " " + ""); @@ -873,7 +899,7 @@ public class XmlConfigurationTest { String name = "bar"; String value = "bar"; - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " " + " " + @@ -896,7 +922,7 @@ public class XmlConfigurationTest String value = "bar"; String defaultValue = "___"; String expectedValue = "_" + value + "_" + value + "_"; - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " " + " " + @@ -914,7 +940,7 @@ public class XmlConfigurationTest public void testPropertyNotFoundWithPropertyInDefaultValueNotFoundWithDefault() throws Exception { String value = "bar"; - XmlConfiguration xmlConfiguration = new XmlConfiguration("" + + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + "" + " " + " " + @@ -937,7 +963,7 @@ public class XmlConfigurationTest for(String propName: propNames) { XmlConfiguration configuration = - new XmlConfiguration("" + + asXmlConfiguration("" + "" + " " + " " + @@ -965,7 +991,7 @@ public class XmlConfigurationTest for(String propName: propNames) { XmlConfiguration configuration = - new XmlConfiguration("" + + asXmlConfiguration("" + "" + " " + " " + @@ -981,4 +1007,65 @@ public class XmlConfigurationTest assertThat(propName, tc.getTestString(), startsWith("file:")); } } + + @Test + public void testJettyStandardIdsAndProperties_JettyWebappsUri() throws Exception + { + Path war = MavenTestingUtils.getTargetPath("no.war"); + XmlConfiguration configuration = + asXmlConfiguration("" + + "" + + " " + + " " + + " " + + ""); + + configuration.setJettyStandardIdsAndProperties(null, Resource.newResource(war)); + + TestConfiguration tc = new TestConfiguration(); + configuration.configure(tc); + + assertThat("jetty.webapps.uri", tc.getTestString(), is(XmlConfiguration.normalizeURI(war.getParent().toUri().toString()))); + } + + @Test + public void testDeprecated() throws Exception + { + Class testClass = AnnotatedTestConfiguration.class; + XmlConfiguration xmlConfiguration = asXmlConfiguration("" + + "" + + " foo" + + " " + + " " + + " " + + " " + + ""); + + ByteArrayOutputStream logBytes = null; + Logger logger = Log.getLogger(XmlConfiguration.class); + if (logger instanceof StdErrLog) + { + StdErrLog stdErrLog = (StdErrLog)logger; + logBytes = new ByteArrayOutputStream(); + stdErrLog.setStdErrStream(new PrintStream(logBytes)); + } + + xmlConfiguration.configure(); + + if (logBytes != null) + { + String[] lines = logBytes.toString("UTF-8").split(System.lineSeparator()); + List warnings = Arrays.stream(lines) + .filter(line -> line.contains(":WARN:")) + .filter(line -> line.contains(testClass.getSimpleName())) + .collect(Collectors.toList()); + // 1. Deprecated constructor + // 2. Deprecated method + // 3. Deprecated method + // 4. Deprecated method + // 5. Deprecated field + // 6. Deprecated field + assertEquals(6, warnings.size()); + } + } } diff --git a/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithAttr.xml b/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithAttr.xml index 8377987bc89..832f244da82 100644 --- a/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithAttr.xml +++ b/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithAttr.xml @@ -1,5 +1,5 @@ - + name diff --git a/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithElements.xml b/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithElements.xml index e4fa88d41b8..f0d1b8d1a1f 100644 --- a/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithElements.xml +++ b/jetty-xml/src/test/resources/org/eclipse/jetty/xml/configureWithElements.xml @@ -1,5 +1,5 @@ - + name diff --git a/pom.xml b/pom.xml index 18ef9f01ec2..3aabf82bbbc 100644 --- a/pom.xml +++ b/pom.xml @@ -21,21 +21,22 @@ UTF-8 1.4 1.8.0-beta2 - 2.11.1 + 2.11.2 1.3.0-alpha4 5.1.1.RELEASE 1.2 4.0.2 - 9.0.14.1 + 9.0.19 + 9.4.8.Final undefined - 1.4.1 - 7.0 + 2.1.0 + 7.1 1.21 benchmarks 1.2.0 1.1.5 - 5.3.1 + 5.4.0 3.6.0 1.3.1 2.4.5.Final @@ -46,8 +47,8 @@ -Dfile.encoding=UTF-8 -Duser.language=en -Duser.region=US -showversion -Xmx1g -Xms1g -Xlog:gc:stderr:time,level,tags - 3.0.0-M2 - 3.8.0 + 3.0.0-M3 + 3.8.1 3.1.1 3.1.0 3.0.1 @@ -56,7 +57,7 @@ false - 5.2 + 5.3 2.1.1.RELEASE @@ -426,6 +427,11 @@ + + org.apache.maven.plugins + maven-clean-plugin + 3.1.0 + org.apache.maven.plugins maven-compiler-plugin @@ -493,7 +499,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.1.1 + 3.1.2 ${project.build.outputDirectory}/META-INF/MANIFEST.MF @@ -517,6 +523,8 @@ + 11 + 11 UTF-8 UTF-8 UTF-8 @@ -891,9 +899,8 @@ - org.junit.jupiter - junit-jupiter-engine - ${junit.version} + org.eclipse.jetty.toolchain + jetty-test-helper test @@ -906,9 +913,9 @@ ${servlet.api.version} - javax.websocket - javax.websocket-api - 1.1 + org.eclipse.jetty.toolchain + jetty-javax-websocket-api + 1.1.1 jakarta.annotation @@ -1031,26 +1038,16 @@ slf4j-simple ${slf4j.version} + + org.jboss.logging + jboss-logging + 3.3.2.Final + com.github.jnr jnr-unixsocket 0.22 - - junit - junit - 4.12 - - - org.hamcrest - hamcrest-core - 1.3 - - - org.hamcrest - hamcrest-library - 1.3 - @@ -1197,9 +1194,17 @@ ci - - aggregates/jetty-all - + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.version} + + + + update-version @@ -1314,7 +1319,7 @@ true - + oss.snapshots OSS Snapshots @@ -1335,17 +1340,6 @@ true - - jetty.snapshots - Jetty Snapshots - https://oss.sonatype.org/content/repositories/jetty-snapshots - - false - - - true - - @@ -1477,6 +1471,7 @@ + apache.snapshots diff --git a/scripts/release-jetty.sh b/scripts/release-jetty.sh index a9f7f5ea6b9..57ae4c9afbd 100755 --- a/scripts/release-jetty.sh +++ b/scripts/release-jetty.sh @@ -69,8 +69,6 @@ VER_CURRENT=`sed -e "s/xmlns/ignore/" pom.xml | xmllint --xpath "/project/versio echo "Current pom.xml Version: ${VER_CURRENT}" read -e -p "Release Version ? " VER_RELEASE read -e -p "Next Dev Version ? " VER_NEXT -# VER_RELEASE=9.3.5.v20151012 -# VER_NEXT=9.3.6-SNAPSHOT TAG_NAME="jetty-$VER_RELEASE" # Ensure tag doesn't exist (yet) @@ -89,9 +87,12 @@ if [ ! -d "$ALT_DEPLOY_DIR" ] ; then fi # DEPLOY_OPTS="-Dmaven.test.failure.ignore=true" -DEPLOY_OPTS="-Dtest=None" +DEPLOY_OPTS="-DskipTests -Dtest=None" # DEPLOY_OPTS="$DEPLOY_OPTS -DaltDeploymentRepository=intarget::default::file://$ALT_DEPLOY_DIR/" +# Uncomment for Java 1.7 +export MAVEN_OPTS="-Xmx1g -XX:MaxPermSize=128m" + echo "" echo "-----------------------------------------------" echo " Release Plan Review" @@ -103,6 +104,7 @@ echo "Current Version : $VER_CURRENT" echo "Release Version : $VER_RELEASE" echo "Next Dev Version : $VER_NEXT" echo "Tag name : $TAG_NAME" +echo "MAVEN_OPTS : $MAVEN_OPTS" echo "Maven Deploy Opts: $DEPLOY_OPTS" reportMavenTestFailures() { @@ -130,18 +132,25 @@ echo "" if proceedyn "Are you sure you want to release using above? (y/N)" n; then echo "" if proceedyn "Update VERSION.txt for $VER_RELEASE? (Y/n)" y; then + # Uncomment alternate JVM for jetty 9.2 builds + # JAVA_HOME_ORIG=$JAVA_HOME + # PATH_ORIG=$PATH + # JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home + # PATH=$JAVA_HOME/bin:$PATH mvn -N -Pupdate-version generate-resources cp VERSION.txt VERSION.txt.backup cat VERSION.txt.backup | sed -e "s/$VER_CURRENT/$VER_RELEASE/" > VERSION.txt rm VERSION.txt.backup + # JAVA_HOME=$JAVA_HOME_ORIG + # PATH=$PATH_ORIG echo "VERIFY the following files (in a different console window) before continuing." echo " VERSION.txt - top section" - echo " target/vers-tag.txt - for the tag commit message" + echo " target/version-tag.txt - for the tag commit message" fi # This is equivalent to 'mvn release:prepare' if proceedyn "Update project.versions for $VER_RELEASE? (Y/n)" y; then - mvn org.codehaus.mojo:versions-maven-plugin:2.5:set \ + mvn org.codehaus.mojo:versions-maven-plugin:2.7:set \ -DoldVersion="$VER_CURRENT" \ -DnewVersion="$VER_RELEASE" \ -DprocessAllModules=true @@ -151,14 +160,14 @@ if proceedyn "Are you sure you want to release using above? (y/N)" n; then fi if proceedyn "Create Tag $TAG_NAME? (Y/n)" y; then echo "TODO: Sign tags with GIT_USER_EMAIL=$GIT_USER_EMAIL" - echo "Using target/vers-tag.txt as tag text" - git tag --file=target/vers-tag.txt $TAG_NAME + echo "Using target/version-tag.txt as tag text" + git tag --file=target/version-tag.txt $TAG_NAME fi # This is equivalent to 'mvn release:perform' if proceedyn "Build/Deploy from tag $TAG_NAME? (Y/n)" y; then git checkout $TAG_NAME - mvn clean package source:jar javadoc:jar gpg:sign deploy \ + mvn clean package source:jar javadoc:jar gpg:sign javadoc:aggregate-jar deploy \ -Peclipse-release $DEPLOY_OPTS reportMavenTestFailures git checkout $GIT_BRANCH_ID @@ -170,9 +179,10 @@ if proceedyn "Are you sure you want to release using above? (y/N)" n; then echo "" >> VERSION.txt cat VERSION.txt.backup >> VERSION.txt echo "Update project.versions for $VER_NEXT" - mvn org.codehaus.mojo:versions-maven-plugin:2.1:set \ + mvn org.codehaus.mojo:versions-maven-plugin:2.7:set \ -DoldVersion="$VER_RELEASE" \ - -DnewVersion="$VER_NEXT" + -DnewVersion="$VER_NEXT" \ + -DprocessAllModules=true echo "Commit $VER_NEXT" if proceedyn "Commit updates in working directory for $VER_NEXT? (Y/n)" y; then git commit -a -m "Updating to version $VER_NEXT" diff --git a/tests/jetty-http-tools/pom.xml b/tests/jetty-http-tools/pom.xml index 8f8ca6a4f0a..12a4f6c63da 100644 --- a/tests/jetty-http-tools/pom.xml +++ b/tests/jetty-http-tools/pom.xml @@ -20,13 +20,8 @@ org.hamcrest - hamcrest-core - - - - org.hamcrest - hamcrest-library - test + hamcrest + 2.1 diff --git a/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/HttpTester.java b/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/HttpTester.java index 27441fb2b53..8eace85d3d3 100644 --- a/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/HttpTester.java +++ b/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/HttpTester.java @@ -512,6 +512,12 @@ public class HttpTester { return 0; } + + @Override + public boolean isHeaderCacheCaseSensitive() + { + return false; + } } public static class Request extends Message implements HttpParser.RequestHandler diff --git a/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/matchers/HttpFieldsContainsHeaderKey.java b/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/matchers/HttpFieldsContainsHeaderKey.java index eeec5e4e3b8..38e448e687f 100644 --- a/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/matchers/HttpFieldsContainsHeaderKey.java +++ b/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/matchers/HttpFieldsContainsHeaderKey.java @@ -21,7 +21,6 @@ package org.eclipse.jetty.http.tools.matchers; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.hamcrest.Description; -import org.hamcrest.Factory; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; @@ -51,7 +50,6 @@ public class HttpFieldsContainsHeaderKey extends TypeSafeMatcher return fields.containsKey(this.keyName); } - @Factory public static Matcher containsKey(String keyName) { return new HttpFieldsContainsHeaderKey(keyName); } diff --git a/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/matchers/HttpFieldsMatchers.java b/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/matchers/HttpFieldsMatchers.java index 710916d041f..879f06f7a4d 100644 --- a/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/matchers/HttpFieldsMatchers.java +++ b/tests/jetty-http-tools/src/main/java/org/eclipse/jetty/http/tools/matchers/HttpFieldsMatchers.java @@ -20,27 +20,22 @@ package org.eclipse.jetty.http.tools.matchers; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; -import org.hamcrest.Factory; import org.hamcrest.Matcher; public class HttpFieldsMatchers { - @Factory public static Matcher containsHeader(String keyName) { return new HttpFieldsContainsHeaderKey(keyName); } - @Factory public static Matcher containsHeader(HttpHeader header) { return new HttpFieldsContainsHeaderKey(header); } - @Factory public static Matcher containsHeaderValue(String keyName, String value) { return new HttpFieldsContainsHeaderValue(keyName, value); } - @Factory public static Matcher containsHeaderValue(HttpHeader header, String value) { return new HttpFieldsContainsHeaderValue(header, value); } diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/server/jmh/ListVsMapBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/server/jmh/ListVsMapBenchmark.java new file mode 100644 index 00000000000..6311b79562f --- /dev/null +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/server/jmh/ListVsMapBenchmark.java @@ -0,0 +1,341 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server.jmh; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@State(Scope.Benchmark) +@Threads(1) +@Warmup(iterations = 6, time = 2000, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 3, time = 2000, timeUnit = TimeUnit.MILLISECONDS) +public class ListVsMapBenchmark +{ + @Param({ "12" }) // Chrome has 12 for HTTP/1.1 and 16 for HTTP/2 (including meta headers) + public static int size; + + @Param({ "11" }) // average length of known headers in HttpHeader + public static int length; + + @Param({"1", "10", "20", "30" }) + public static int lookups; + + @Param({"hits", "misses", "iterate" }) + public static String mostly; + + static final String base = "This-is-the-base-of-All-key-names-and-is-long".substring(0,length); + static final String miss = "X-" + base; + static final List trials = new ArrayList<>(); + static final Random random = new Random(); + + @Setup(Level.Trial) + public void setup() + { + int hits = 1; + int misses = 1; + switch(mostly) + { + case "hits" : hits = lookups; break; + case "misses" : misses = lookups; break; + case "iterate" : hits = lookups/2; misses=lookups-hits; break; + default : throw new IllegalStateException(); + } + + for (int h = hits; h-->0;) + trials.add(base + "-" + (h % size)); + + for (int m = misses; m-->0; ) + trials.add(miss); + + Collections.shuffle(trials); + } + + + static class Pair + { + final String key; + final String value; + + public Pair(String key, String value) + { + this.key = key; + this.value = value; + } + } + + interface Fill + { + void put(Pair p); + } + + interface Lookup + { + Pair get(String key); + Iterator iterate(); + } + + private void fill(Fill fill) + { + for (int i=0; i t = trials.iterator(); + while(t.hasNext()) + { + // Look for 4 headers at once because that is what the common case of a + // ResourceService does + String one = t.hasNext() ? t.next() : null; + String two = t.hasNext() ? t.next() : null; + String three = t.hasNext() ? t.next() : null; + String four = t.hasNext() ? t.next() : null; + + Iterator i = lookup.iterate(); + while (i.hasNext()) + { + Pair p = i.next(); + String k = p.key; + if (one != null && one.equals(k)) + result ^= p.value.hashCode(); + else if (two != null && two.equals(k)) + result ^= p.value.hashCode(); + else if (three != null && three.equals(k)) + result ^= p.value.hashCode(); + else if (four != null && four.equals(k)) + result ^= p.value.hashCode(); + } + } + } + else + { + for (String t : trials) + { + Pair p = lookup.get(t); + if (p != null) + result ^= p.value.hashCode(); + } + } + + return result; + } + + private long listLookup(List list) + { + return test(new Lookup() { + @Override + public Pair get(String k) + { + for (int i = 0; i iterate() + { + return list.iterator(); + } + }); + } + + @Benchmark + @BenchmarkMode({Mode.Throughput}) + public long testArrayList() throws Exception + { + List list = new ArrayList<>(size); + fill(list::add); + return listLookup(list); + } + + @Benchmark + @BenchmarkMode({Mode.Throughput}) + public long testLinkedList() throws Exception + { + List list = new LinkedList<>(); + fill(list::add); + return listLookup(list); + } + + @Benchmark + @BenchmarkMode({Mode.Throughput}) + public long testLinkedHashMap() throws Exception + { + // This loses the true ordering of fields + Map> map = new LinkedHashMap<>(size); + fill(p-> + { + List list = new LinkedList<>(); + list.add(p); + map.put(p.key.toLowerCase(),list); + }); + return test(new Lookup() + { + @Override + public Pair get(String k) + { + List list = map.get(k.toLowerCase()); + if (list == null || list.isEmpty()) + return null; + return list.get(0); + } + + @Override + public Iterator iterate() + { + Iterator> iter = map.values().iterator(); + + return new Iterator() { + Iterator current; + @Override + public boolean hasNext() + { + if (( current==null || !current.hasNext() ) && iter.hasNext()) + current=iter.next().iterator(); + return current!=null && current.hasNext(); + } + + @Override + public Pair next() + { + if (hasNext()) + return current.next(); + throw new NoSuchElementException(); + } + }; + } + }); + } + + + @Benchmark + @BenchmarkMode({Mode.Throughput}) + public long testHashMapAndLinkedList() throws Exception + { + // This keeps the true ordering of fields + Map> map = new HashMap<>(size); + List order = new LinkedList<>(); + + fill(p-> + { + List list = new LinkedList<>(); + list.add(p); + map.put(p.key.toLowerCase(),list); + order.add(p); + }); + return mapLookup(map, order); + } + + + + @Benchmark + @BenchmarkMode({Mode.Throughput}) + public long testHashMapAndArrayList() throws Exception + { + // This keeps the true ordering of fields + Map> map = new HashMap<>(size); + List order = new ArrayList<>(); + + fill(p-> + { + List list = new ArrayList<>(2); + list.add(p); + map.put(p.key.toLowerCase(),list); + order.add(p); + }); + return mapLookup(map, order); + } + + private long mapLookup(Map> map, List order) + { + return test(new Lookup() + { + @Override + public Pair get(String k) + { + List list = map.get(k.toLowerCase()); + if (list == null || list.isEmpty()) + return null; + return list.get(0); + } + + @Override + public Iterator iterate() + { + return order.iterator(); + } + }); + } + + + public static void main(String[] args) throws RunnerException + { + Options opt = new OptionsBuilder() + .include(ListVsMapBenchmark.class.getSimpleName()) + // .addProfiler(GCProfiler.class) + .forks(1) + .build(); + + new Runner(opt).run(); + } +} + + diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/StringIsEmptyBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/StringIsEmptyBenchmark.java new file mode 100644 index 00000000000..42e51760e8f --- /dev/null +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/StringIsEmptyBenchmark.java @@ -0,0 +1,66 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +@State( Scope.Benchmark) +@Threads(4) +@Warmup(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 9, time = 800, timeUnit = TimeUnit.MILLISECONDS) +public class StringIsEmptyBenchmark +{ + + private static final String SHORT = "beer.com/foo"; + + private static final String MEDIUM = "beer.com/foobarbeeerbe/bebbebbebebbe/bebbeghdegde/foobarbeeerbe/bebbebbebebbe/bebbeghdegde"; + + private static final String LONG = "beer.com/foobarbeeerbe/bebbebbebebbe/bebbeghdegde/foobarbeeerbe/bebbebbebebbe/bebbeghdegde/foobarbeeerbe/bebbebbebebbe/bebbeghdegde/foobarbeeerbe/bebbebbebebbe/bebbeghdegde/foobarbeeerbe/bebbebbebebbe/bebbeghdegde/foobarbeeerbe/bebbebbebebbe/bebbeghdegde/foobarbeeerbe/bebbebbebebbe/bebbeghdegde"; + + + @Benchmark + @BenchmarkMode( Mode.Throughput) + public void shortIsEmpty() + { + StringUtil.isEmpty(SHORT); + } + + @Benchmark + @BenchmarkMode( Mode.Throughput) + public void mediumIsEmpty() + { + StringUtil.isEmpty(MEDIUM); + } + + @Benchmark + @BenchmarkMode( Mode.Throughput) + public void longIsEmpty() + { + StringUtil.isEmpty(LONG); + } +} diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/logs/LogCondensePackageStringBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/logs/LogCondensePackageStringBenchmark.java new file mode 100644 index 00000000000..c956f2e9e53 --- /dev/null +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/logs/LogCondensePackageStringBenchmark.java @@ -0,0 +1,64 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.log; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.profile.GCProfiler; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@Fork(value = 5) +@State(Scope.Benchmark) + +@Warmup(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS) +public class LogCondensePackageStringBenchmark +{ + @Param({"com.acme.Dump", + "org.eclipse.jetty.websocket.common.extensions.compress.DeflateFrameExtension$Pool" + }) + String fqClassName; + + @Benchmark + public void testCondensePackage(Blackhole blackhole) + { + blackhole.consume(AbstractLogger.condensePackageString(fqClassName)); + } + + public static void main(String[] args) throws RunnerException + { + Options opt = new OptionsBuilder() + .include(LogCondensePackageStringBenchmark.class.getSimpleName()) + .addProfiler(GCProfiler.class) + .build(); + + new Runner(opt).run(); + } +} diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ThreadPoolBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ThreadPoolBenchmark.java index f986a6217a9..396dec710eb 100644 --- a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ThreadPoolBenchmark.java +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/jmh/ThreadPoolBenchmark.java @@ -18,11 +18,12 @@ package org.eclipse.jetty.util.thread.jmh; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.thread.ExecutorThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -38,31 +39,25 @@ import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; -import org.openjdk.jmh.runner.options.TimeValue; @State(Scope.Benchmark) -@Threads(4) -@Warmup(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS) -@Measurement(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Warmup(iterations = 8, time = 1000, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 3, time = 1000, timeUnit = TimeUnit.MILLISECONDS) public class ThreadPoolBenchmark { public enum Type { - QTP, ETP; + QTP, ETP, LQTP, LETP, AQTP, AETP; } - @Param({ "QTP", "ETP"}) + @Param({ "QTP", "ETP" /*, "LQTP", "LETP", "AQTP", "AETP" */ }) Type type; - @Param({ "50"}) - int tasks; - - @Param({ "200", "2000"}) + @Param({ "200" }) int size; ThreadPool pool; @@ -73,11 +68,39 @@ public class ThreadPoolBenchmark switch(type) { case QTP: - pool = new QueuedThreadPool(size); + { + QueuedThreadPool qtp = new QueuedThreadPool(size, size, new BlockingArrayQueue<>(32768, 32768)); + qtp.setReservedThreads(0); + pool = qtp; break; + } case ETP: - pool = new ExecutorThreadPool(size); + pool = new ExecutorThreadPool(size, size, new BlockingArrayQueue<>(32768, 32768)); + break; + + case LQTP: + { + QueuedThreadPool qtp = new QueuedThreadPool(size, size, new LinkedBlockingQueue<>()); + qtp.setReservedThreads(0); + pool = qtp; + break; + } + + case LETP: + pool = new ExecutorThreadPool(size, size, new LinkedBlockingQueue<>()); + break; + + case AQTP: + { + QueuedThreadPool qtp = new QueuedThreadPool(size, size, new ArrayBlockingQueue<>(32768)); + qtp.setReservedThreads(0); + pool = qtp; + break; + } + + case AETP: + pool = new ExecutorThreadPool(size, size, new ArrayBlockingQueue<>(32768)); break; } LifeCycle.start(pool); @@ -85,9 +108,27 @@ public class ThreadPoolBenchmark @Benchmark @BenchmarkMode(Mode.Throughput) - public void testPool() + @Threads(1) + public void testFew() throws Exception { - doWork().join(); + doJob(); + } + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Threads(4) + public void testSome() throws Exception + { + doJob(); + } + + + @Benchmark + @BenchmarkMode(Mode.Throughput) + @Threads(200) + public void testMany() throws Exception + { + doJob(); } @TearDown // (Level.Iteration) @@ -97,33 +138,20 @@ public class ThreadPoolBenchmark pool = null; } - CompletableFuture doWork() + void doJob() throws Exception { - List> futures = new ArrayList<>(); - for (int i = 0; i < tasks; i++) - { - final CompletableFuture f = new CompletableFuture(); - futures.add(f); - pool.execute(() -> { - Blackhole.consumeCPU(64); - f.complete(null); - }); - } - - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CountDownLatch latch = new CountDownLatch(1); + pool.execute(latch::countDown); + latch.await(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(ThreadPoolBenchmark.class.getSimpleName()) - .warmupIterations(2) - .measurementIterations(3) .forks(1) - .threads(400) + // .threads(400) // .syncIterations(true) // Don't start all threads at same time - .warmupTime(new TimeValue(10000,TimeUnit.MILLISECONDS)) - .measurementTime(new TimeValue(10000,TimeUnit.MILLISECONDS)) // .addProfiler(CompilerProfiler.class) // .addProfiler(LinuxPerfProfiler.class) // .addProfiler(LinuxPerfNormProfiler.class) diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/strategy/jmh/EWYKBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/strategy/jmh/EWYKBenchmark.java index 7c81298cc83..2292281ede2 100644 --- a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/strategy/jmh/EWYKBenchmark.java +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/strategy/jmh/EWYKBenchmark.java @@ -19,11 +19,16 @@ package org.eclipse.jetty.util.thread.strategy.jmh; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.thread.ExecutionStrategy; import org.eclipse.jetty.util.thread.Invocable; @@ -51,7 +56,7 @@ public class EWYKBenchmark { static TestServer server; static ReservedThreadExecutor reserved; - static File directory; + static Path directory; @Param({"PC","PEC","EWYK"}) public static String strategyName; @@ -66,21 +71,15 @@ public class EWYKBenchmark public static void setupServer() throws Exception { // Make a test directory - directory = File.createTempFile("ewyk","dir"); - if (directory.exists()) - directory.delete(); - directory.mkdir(); - directory.deleteOnExit(); - + directory = Files.createTempDirectory("ewyk"); + // Make some test files for (int i=0;i<75;i++) { - File file =new File(directory,i+".txt"); - file.createNewFile(); - file.deleteOnExit(); + File.createTempFile( "ewyk_benchmark", i + ".txt" , directory.toFile()); } - server=new TestServer(directory); + server=new TestServer(directory.toFile()); server.start(); reserved = new ReservedThreadExecutor(server,20); reserved.start(); @@ -89,6 +88,14 @@ public class EWYKBenchmark @TearDown(Level.Trial) public static void stopServer() throws Exception { + try + { + IO.delete(directory.toFile()); + } + catch ( Exception e ) + { + System.out.println("cannot delete directory:"+directory); + } reserved.stop(); server.stop(); } diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/strategy/jmh/TestServer.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/strategy/jmh/TestServer.java index ab99582cad6..3f05463f2ab 100644 --- a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/strategy/jmh/TestServer.java +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/thread/strategy/jmh/TestServer.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.util.thread.strategy.jmh; import java.io.File; +import java.nio.file.Files; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -44,7 +45,7 @@ public class TestServer implements Executor, TryExecutor TestServer() { - this(new File("/tmp/")); + this(new File(System.getProperty("java.io.tmpdir"))); } diff --git a/tests/test-distribution/pom.xml b/tests/test-distribution/pom.xml index 091418d18cb..a6cf1ff70dc 100644 --- a/tests/test-distribution/pom.xml +++ b/tests/test-distribution/pom.xml @@ -78,6 +78,17 @@ org.eclipse.jetty.toolchain jetty-test-helper + + + org.eclipse.jetty + jetty-unixsocket-server + ${project.version} + test + + + org.eclipse.jetty + jetty-unixsocket-client + ${project.version} test diff --git a/tests/test-distribution/src/main/java/org/eclipse/jetty/tests/distribution/DistributionTester.java b/tests/test-distribution/src/main/java/org/eclipse/jetty/tests/distribution/DistributionTester.java index 44a2a9edda9..27724f8b436 100644 --- a/tests/test-distribution/src/main/java/org/eclipse/jetty/tests/distribution/DistributionTester.java +++ b/tests/test-distribution/src/main/java/org/eclipse/jetty/tests/distribution/DistributionTester.java @@ -62,6 +62,8 @@ import org.eclipse.aether.spi.connector.transport.TransporterFactory; import org.eclipse.aether.transfer.AbstractTransferListener; import org.eclipse.aether.transport.file.FileTransporterFactory; import org.eclipse.aether.transport.http.HttpTransporterFactory; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -140,6 +142,9 @@ public class DistributionTester commands.add("-Djava.io.tmpdir=" + workDir.toAbsolutePath().toString()); commands.add("-jar"); commands.add(config.jettyHome.toAbsolutePath() + "/start.jar"); + // we get artifacts from local repo first + args = new ArrayList<>(args); + args.add("maven.local.repo=" + System.getProperty("mavenRepoPath")); commands.addAll(args); LOGGER.info("Executing: {}", commands); @@ -166,6 +171,21 @@ public class DistributionTester } } + /** + * Installs content from {@code src/test/resources/} into {@code ${jetty.base}/} + * + * @param testResourcePath the location of the source file in {@code src/test/resources} + * @param baseResourcePath the location of the destination file in {@code ${jetty.base}} + * @throws IOException if unable to copy file + */ + public void installBaseResource(String testResourcePath, String baseResourcePath) throws IOException + { + Path srcFile = MavenTestingUtils.getTestResourcePath(testResourcePath); + Path destFile = config.jettyBase.resolve(baseResourcePath); + + Files.copy(srcFile, destFile); + } + /** * Installs in {@code ${jetty.base}/webapps} the given war file under the given context path. * @@ -176,7 +196,7 @@ public class DistributionTester public void installWarFile(File warFile, String context) throws IOException { //webapps - Path webapps = Paths.get(config.jettyBase.toString(), "webapps", context); + Path webapps = config.jettyBase.resolve("webapps").resolve(context); if (!Files.exists(webapps)) Files.createDirectories(webapps); unzip(warFile, webapps.toFile()); @@ -213,7 +233,9 @@ public class DistributionTester if (config.jettyBase == null) { - config.jettyBase = Files.createTempDirectory("jetty_base_"); + Path bases = MavenTestingUtils.getTargetTestingPath("bases"); + FS.ensureDirExists(bases); + config.jettyBase = Files.createTempDirectory(bases, "jetty_base_"); } else { @@ -225,12 +247,12 @@ public class DistributionTester private String getJavaExecutable() { String[] javaExecutables = new String[]{"java", "java.exe"}; - File javaHomeDir = new File(System.getProperty("java.home")); + Path javaBinDir = Paths.get(System.getProperty("java.home")).resolve("bin"); for (String javaExecutable : javaExecutables) { - File javaFile = new File(javaHomeDir, "bin" + File.separator + javaExecutable); - if (javaFile.exists() && javaFile.isFile()) - return javaFile.getAbsolutePath(); + Path javaFile = javaBinDir.resolve(javaExecutable); + if (Files.exists(javaFile) && Files.isRegularFile(javaFile)) + return javaFile.toAbsolutePath().toString(); } return "java"; } @@ -451,7 +473,7 @@ public class DistributionTester @Override public void close() { - destroy(); + stop(); } /** diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java new file mode 100644 index 00000000000..b2cfa38b9e8 --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/BadAppTests.java @@ -0,0 +1,156 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests where the server is started with a Bad App that will fail in its init phase. + */ +public class BadAppTests extends AbstractDistributionTest +{ + /** + * Start a server where a bad webapp is being deployed. + * The badapp.war will throw a ServletException during its deploy/init. + * The badapp.xml contains a {@code true} + * + * It is expected that the server does not start and exits with an error code + */ + @Test + public void testXml_ThrowOnUnavailable_True() throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + DistributionTester distribution = DistributionTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + try (DistributionTester.Run run1 = distribution.start("--add-to-start=http,deploy")) + { + assertTrue(run1.awaitFor(5, TimeUnit.SECONDS)); + assertThat(run1.getExitValue(), is(0)); + + // Setup webapps directory + distribution.installBaseResource("badapp/badapp.war", + "webapps/badapp.war"); + distribution.installBaseResource("badapp/badapp_throwonunavailable_true.xml", + "webapps/badapp.xml"); + + int port = distribution.freePort(); + try (DistributionTester.Run run2 = distribution.start("jetty.http.port=" + port)) + { + assertTrue(run2.awaitFor(5, TimeUnit.SECONDS), "Should have exited"); + assertThat("Should have gotten a non-zero exit code", run2.getExitValue(), not(is(0))); + } + } + } + + /** + * Start a server where a bad webapp is being deployed. + * The badapp.war will throw a ServletException during its deploy/init. + * The badapp.xml contains a {@code false} + * + * It is expected that the server does start and attempts to access the /badapp/ report + * that it is unavailable. + */ + @Test + public void testXml_ThrowOnUnavailable_False() throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + DistributionTester distribution = DistributionTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + try (DistributionTester.Run run1 = distribution.start("--add-to-start=http,deploy")) + { + assertTrue(run1.awaitFor(5, TimeUnit.SECONDS)); + assertThat(run1.getExitValue(), is(0)); + + // Setup webapps directory + distribution.installBaseResource("badapp/badapp.war", + "webapps/badapp.war"); + distribution.installBaseResource("badapp/badapp_throwonunavailable_false.xml", + "webapps/badapp.xml"); + + int port = distribution.freePort(); + try (DistributionTester.Run run2 = distribution.start("jetty.http.port=" + port)) + { + assertTrue(run2.awaitConsoleLogsFor("Started @", 10, TimeUnit.SECONDS)); + + startHttpClient(); + ContentResponse response = client.GET("http://localhost:" + port + "/badapp/"); + assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); + assertThat(response.getContentAsString(), containsString("Unavailable")); + assertThat(response.getContentAsString(), containsString("Problem accessing /badapp/")); + } + } + } + + /** + * Start a server where a bad webapp is being deployed. + * The badapp.war will throw a ServletException during its deploy/init. + * No badapp.xml is used, relying on default values for {@code throwUnavailableOnStartupException} + * + * It is expected that the server does start and attempts to access the /badapp/ report + * that it is unavailable. + */ + @Test + public void testNoXml_ThrowOnUnavailable_Default() throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + DistributionTester distribution = DistributionTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + try (DistributionTester.Run run1 = distribution.start("--add-to-start=http,deploy")) + { + assertTrue(run1.awaitFor(5, TimeUnit.SECONDS)); + assertThat(run1.getExitValue(), is(0)); + + // Setup webapps directory + distribution.installBaseResource("badapp/badapp.war", + "webapps/badapp.war"); + + int port = distribution.freePort(); + try (DistributionTester.Run run2 = distribution.start("jetty.http.port=" + port)) + { + assertTrue(run2.awaitConsoleLogsFor("Started @", 10, TimeUnit.SECONDS)); + + startHttpClient(); + ContentResponse response = client.GET("http://localhost:" + port + "/badapp/"); + assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); + assertThat(response.getContentAsString(), containsString("Unavailable")); + assertThat(response.getContentAsString(), containsString("Problem accessing /badapp/")); + } + } + } +} diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DemoBaseTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DemoBaseTests.java new file mode 100644 index 00000000000..40828383857 --- /dev/null +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DemoBaseTests.java @@ -0,0 +1,145 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.tests.distribution; + +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DemoBaseTests extends AbstractDistributionTest +{ + @Test + public void testJspDump() throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + DistributionTester distribution = DistributionTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .jettyBase(Paths.get("demo-base")) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + int httpPort = distribution.freePort(); + int httpsPort = distribution.freePort(); + assertThat("httpPort != httpsPort", httpPort, is(not(httpsPort))); + + String[] args = { + "jetty.http.port=" + httpPort, + "jetty.httpConfig.port=" + httpsPort, + "jetty.ssl.port=" + httpsPort + }; + + try (DistributionTester.Run run1 = distribution.start(args)) + { + assertTrue(run1.awaitConsoleLogsFor("Started @", 20, TimeUnit.SECONDS)); + + startHttpClient(); + ContentResponse response = client.GET("http://localhost:" + httpPort + "/test/jsp/dump.jsp"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContentAsString(), containsString("PathInfo")); + assertThat(response.getContentAsString(), not(containsString("<%"))); + } + } + + @Test + public void testAsyncRest() throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + DistributionTester distribution = DistributionTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .jettyBase(Paths.get("demo-base")) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + int httpPort = distribution.freePort(); + int httpsPort = distribution.freePort(); + assertThat("httpPort != httpsPort", httpPort, is(not(httpsPort))); + + String[] args = { + "jetty.http.port=" + httpPort, + "jetty.httpConfig.port=" + httpsPort, + "jetty.ssl.port=" + httpsPort + }; + + try (DistributionTester.Run run1 = distribution.start(args)) + { + assertTrue(run1.awaitConsoleLogsFor("Started @", 20, TimeUnit.SECONDS)); + + startHttpClient(); + ContentResponse response; + + response = client.GET("http://localhost:" + httpPort + "/async-rest/testSerial?items=kayak"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContentAsString(), containsString("Blocking: kayak")); + + response = client.GET("http://localhost:" + httpPort + "/async-rest/testSerial?items=mouse,beer,gnome"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContentAsString(), containsString("Blocking: mouse,beer,gnome")); + + response = client.GET("http://localhost:" + httpPort + "/async-rest/testAsync?items=kayak"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContentAsString(), containsString("Asynchronous: kayak")); + + response = client.GET("http://localhost:" + httpPort + "/async-rest/testAsync?items=mouse,beer,gnome"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContentAsString(), containsString("Asynchronous: mouse,beer,gnome")); + } + } + + @Test + public void testSpec() throws Exception + { + String jettyVersion = System.getProperty("jettyVersion"); + DistributionTester distribution = DistributionTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .jettyBase(Paths.get("demo-base")) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + int httpPort = distribution.freePort(); + int httpsPort = distribution.freePort(); + assertThat("httpPort != httpsPort", httpPort, is(not(httpsPort))); + + String[] args = { + "jetty.http.port=" + httpPort, + "jetty.httpConfig.port=" + httpsPort, + "jetty.ssl.port=" + httpsPort + }; + + try (DistributionTester.Run run1 = distribution.start(args)) + { + assertTrue(run1.awaitConsoleLogsFor("Started @", 20, TimeUnit.SECONDS)); + + startHttpClient(); + ContentResponse response = client.POST("http://localhost:" + httpPort + "/test-spec/asy/xx").send(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContentAsString(), containsString("PASS")); + assertThat(response.getContentAsString(), not(containsString("FAIL"))); + } + } +} diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 11718fed86f..3de8eb47ed3 100644 --- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -19,6 +19,8 @@ package org.eclipse.jetty.tests.distribution; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.TimeUnit; @@ -27,6 +29,10 @@ import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.unixsocket.client.HttpClientTransportOverUnixSockets; +import org.eclipse.jetty.unixsocket.server.UnixSocketConnector; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.StringUtil; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.JRE; @@ -36,6 +42,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; public class DistributionTests extends AbstractDistributionTest { @@ -173,7 +180,7 @@ public class DistributionTests extends AbstractDistributionTest assertTrue(run2.awaitConsoleLogsFor("Started @", 10, TimeUnit.SECONDS)); HTTP2Client h2Client = new HTTP2Client(); - startHttpClient(() -> new HttpClient(new HttpClientTransportOverHTTP2(h2Client), null)); + startHttpClient(() -> new HttpClient(new HttpClientTransportOverHTTP2(h2Client))); ContentResponse response = client.GET("http://localhost:" + port + "/test/index.jsp"); assertEquals(HttpStatus.OK_200, response.getStatus()); assertThat(response.getContentAsString(), containsString("Hello")); @@ -183,25 +190,100 @@ public class DistributionTests extends AbstractDistributionTest } @Test - public void testDemoBase() throws Exception + public void testUnixSocket() throws Exception { + Path tmpSockFile; + String unixSocketTmp = System.getProperty("unix.socket.tmp"); + if (StringUtil.isNotBlank(unixSocketTmp)) + tmpSockFile = Files.createTempFile(Paths.get(unixSocketTmp), "unix", ".sock"); + else + tmpSockFile = Files.createTempFile("unix", ".sock"); + if (tmpSockFile.toAbsolutePath().toString().length() > UnixSocketConnector.MAX_UNIX_SOCKET_PATH_LENGTH) + { + Path tmp = Paths.get("/tmp"); + assumeTrue(Files.exists(tmp) && Files.isDirectory(tmp)); + tmpSockFile = Files.createTempFile(tmp, "unix", ".sock"); + } + Path sockFile = tmpSockFile; + assertTrue(Files.deleteIfExists(sockFile), "temp sock file cannot be deleted"); + String jettyVersion = System.getProperty("jettyVersion"); DistributionTester distribution = DistributionTester.Builder.newInstance() - .jettyVersion(jettyVersion) - .jettyBase(Paths.get("demo-base")) - .mavenLocalRepository(System.getProperty("mavenRepoPath")) - .build(); + .jettyVersion(jettyVersion) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); - int port = distribution.freePort(); - try (DistributionTester.Run run1 = distribution.start("jetty.http.port=" + port)) + String[] args1 = { + "--create-startd", + "--add-to-start=unixsocket-http,deploy,jsp", + "--approve-all-licenses" + }; + try (DistributionTester.Run run1 = distribution.start(args1)) { - assertTrue(run1.awaitConsoleLogsFor("Started @", 20, TimeUnit.SECONDS)); + // Give it time to download the dependencies + assertTrue(run1.awaitFor(30, TimeUnit.SECONDS)); + assertEquals(0, run1.getExitValue()); - startHttpClient(); - ContentResponse response = client.GET("http://localhost:" + port + "/test/jsp/dump.jsp"); - assertEquals(HttpStatus.OK_200, response.getStatus()); - assertThat(response.getContentAsString(), containsString("PathInfo")); - assertThat(response.getContentAsString(), not(containsString("<%"))); + File war = distribution.resolveArtifact("org.eclipse.jetty.tests:test-simple-webapp:war:" + jettyVersion); + distribution.installWarFile(war, "test"); + + try (DistributionTester.Run run2 = distribution.start("jetty.unixsocket.path=" + sockFile.toString())) + { + assertTrue(run2.awaitConsoleLogsFor("Started @", 10, TimeUnit.SECONDS)); + + startHttpClient(() -> new HttpClient(new HttpClientTransportOverUnixSockets(sockFile.toString()))); + ContentResponse response = client.GET("http://localhost/test/index.jsp"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContentAsString(), containsString("Hello")); + assertThat(response.getContentAsString(), not(containsString("<%"))); + } + } + finally + { + Files.deleteIfExists(sockFile); + } + } + + @Test + public void testLog4j2ModuleWithSimpleWebAppWithJSP() throws Exception + { + Path jettyBase = Files.createTempDirectory( "jetty_base"); + String jettyVersion = System.getProperty("jettyVersion"); + DistributionTester distribution = DistributionTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .jettyBase(jettyBase) + .mavenLocalRepository(System.getProperty("mavenRepoPath")) + .build(); + + String[] args1 = { + "--create-startd", + "--approve-all-licenses", + "--add-to-start=resources,server,http,webapp,deploy,jsp,servlet,servlets,logging-log4j2" + }; + try (DistributionTester.Run run1 = distribution.start(args1)) + { + assertTrue(run1.awaitFor(5, TimeUnit.SECONDS)); + assertEquals(0, run1.getExitValue()); + assertTrue(Files.exists(jettyBase.resolve("resources/log4j2.xml"))); + + File war = distribution.resolveArtifact("org.eclipse.jetty.tests:test-simple-webapp:war:" + jettyVersion); + distribution.installWarFile(war, "test"); + + int port = distribution.freePort(); + try (DistributionTester.Run run2 = distribution.start("jetty.http.port=" + port)) + { + assertTrue(run2.awaitConsoleLogsFor("Started @", 10, TimeUnit.SECONDS)); + + startHttpClient(); + ContentResponse response = client.GET("http://localhost:" + port + "/test/index.jsp"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + assertThat(response.getContentAsString(), containsString("Hello")); + assertThat(response.getContentAsString(), not(containsString("<%"))); + assertTrue(Files.exists(jettyBase.resolve("resources/log4j2.xml"))); + } + } finally + { + IO.delete(jettyBase.toFile()); } } } diff --git a/tests/test-distribution/src/test/resources/badapp/badapp.war b/tests/test-distribution/src/test/resources/badapp/badapp.war new file mode 100644 index 00000000000..3fc1a60d5fe Binary files /dev/null and b/tests/test-distribution/src/test/resources/badapp/badapp.war differ diff --git a/tests/test-distribution/src/test/resources/badapp/badapp_throwonunavailable_false.xml b/tests/test-distribution/src/test/resources/badapp/badapp_throwonunavailable_false.xml new file mode 100644 index 00000000000..7e325b59625 --- /dev/null +++ b/tests/test-distribution/src/test/resources/badapp/badapp_throwonunavailable_false.xml @@ -0,0 +1,8 @@ + + + + + /badapp + /badapp.war + false + diff --git a/tests/test-distribution/src/test/resources/badapp/badapp_throwonunavailable_true.xml b/tests/test-distribution/src/test/resources/badapp/badapp_throwonunavailable_true.xml new file mode 100644 index 00000000000..952aab492d1 --- /dev/null +++ b/tests/test-distribution/src/test/resources/badapp/badapp_throwonunavailable_true.xml @@ -0,0 +1,8 @@ + + + + + /badapp + /badapp.war + true + diff --git a/tests/test-http-client-transport/pom.xml b/tests/test-http-client-transport/pom.xml index 3edc69bc67a..992a27d51f6 100644 --- a/tests/test-http-client-transport/pom.xml +++ b/tests/test-http-client-transport/pom.xml @@ -102,11 +102,6 @@ ${project.version} test - - junit - junit - test - org.eclipse.jetty.toolchain jetty-test-helper diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java index 373d92d3e05..e2e86ceb3b2 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java @@ -76,7 +76,6 @@ import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.log.StacklessLogging; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -394,12 +393,8 @@ public class AsyncIOServletTest extends AbstractTest { diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpChannelAssociationTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpChannelAssociationTest.java index 2f1898b16a8..ea12b8722d2 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpChannelAssociationTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpChannelAssociationTest.java @@ -40,10 +40,10 @@ import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.http.HttpChannelOverHTTP2; import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; import org.eclipse.jetty.http2.client.http.HttpConnectionOverHTTP2; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.unixsocket.client.HttpClientTransportOverUnixSockets; import org.eclipse.jetty.util.Promise; -import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -65,7 +65,7 @@ public class HttpChannelAssociationTest extends AbstractTest init(transport); scenario.startServer(new EmptyServerHandler()); - scenario.client = new HttpClient(newHttpClientTransport(scenario, exchange -> false), scenario.sslContextFactory); + scenario.client = new HttpClient(newHttpClientTransport(scenario, exchange -> false)); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); scenario.client.setExecutor(clientThreads); @@ -90,15 +90,13 @@ public class HttpChannelAssociationTest extends AbstractTest scenario.startServer(new EmptyServerHandler()); long idleTimeout = 1000; - SslContextFactory sslContextFactory = scenario.newSslContextFactory(); - sslContextFactory.setEndpointIdentificationAlgorithm(null); scenario.client = new HttpClient(newHttpClientTransport(scenario, exchange -> { // We idle timeout just before the association, // we must be able to send the request successfully. sleep(2 * idleTimeout); return true; - }), sslContextFactory); + })); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); scenario.client.setExecutor(clientThreads); @@ -123,7 +121,10 @@ public class HttpChannelAssociationTest extends AbstractTest case HTTP: case HTTPS: { - return new HttpClientTransportOverHTTP(1) + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(scenario.newClientSslContextFactory()); + return new HttpClientTransportOverHTTP(clientConnector) { @Override protected HttpConnectionOverHTTP newHttpConnection(EndPoint endPoint, HttpDestination destination, Promise promise) @@ -149,8 +150,10 @@ public class HttpChannelAssociationTest extends AbstractTest case H2C: case H2: { - HTTP2Client http2Client = new HTTP2Client(); - http2Client.setSelectors(1); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(scenario.newClientSslContextFactory()); + HTTP2Client http2Client = new HTTP2Client(clientConnector); return new HttpClientTransportOverHTTP2(http2Client) { @Override @@ -176,7 +179,10 @@ public class HttpChannelAssociationTest extends AbstractTest } case FCGI: { - return new HttpClientTransportOverFCGI(1, "") + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(scenario.newClientSslContextFactory()); + return new HttpClientTransportOverFCGI(clientConnector, "") { @Override protected HttpConnectionOverFCGI newHttpConnection(EndPoint endPoint, HttpDestination destination, Promise promise) @@ -201,7 +207,8 @@ public class HttpChannelAssociationTest extends AbstractTest } case UNIX_SOCKET: { - return new HttpClientTransportOverUnixSockets( scenario.sockFile.toString() ){ + return new HttpClientTransportOverUnixSockets(scenario.sockFile.toString()) + { @Override protected HttpConnectionOverHTTP newHttpConnection(EndPoint endPoint, HttpDestination destination, Promise promise) { diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java index 4210cdb487e..669e24b6eda 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java @@ -47,6 +47,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.LeakTrackingByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.server.Connector; @@ -60,6 +61,7 @@ import org.eclipse.jetty.util.LeakDetector; import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Scheduler; import org.hamcrest.Matchers; import org.junit.jupiter.params.ParameterizedTest; @@ -178,7 +180,7 @@ public class HttpClientLoadTest extends AbstractTest failures) @@ -364,31 +366,32 @@ public class HttpClientLoadTest extends AbstractTest new LeakTrackingConnectionPool(destination, client.getMaxConnectionsPerDestination(), destination) { @Override @@ -416,7 +419,7 @@ public class HttpClientLoadTest extends AbstractTest new LeakTrackingConnectionPool(destination, client.getMaxConnectionsPerDestination(), destination) { @Override @@ -430,7 +433,7 @@ public class HttpClientLoadTest extends AbstractTest scenario.startServer(new EmptyServerHandler()); // Use a default SslContextFactory, requests should fail because the server certificate is unknown. - scenario.client = scenario.newHttpClient(scenario.provideClientTransport(), new SslContextFactory()); + SslContextFactory.Client clientTLS = scenario.newClientSslContextFactory(); + clientTLS.setEndpointIdentificationAlgorithm("HTTPS"); + scenario.client = scenario.newHttpClient(scenario.provideClientTransport(transport, clientTLS)); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); scenario.client.setExecutor(clientThreads); diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java index 7aed306fd9e..9c14538fdd1 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTimeoutTest.java @@ -251,9 +251,8 @@ public class HttpClientTimeoutTest extends AbstractTest scenario.startServer(new TimeoutHandler(2 * timeout)); AtomicBoolean sslIdle = new AtomicBoolean(); - SslContextFactory sslContextFactory = scenario.newSslContextFactory(); - sslContextFactory.setEndpointIdentificationAlgorithm(null); - scenario.client = new HttpClient(scenario.provideClientTransport(), sslContextFactory) + SslContextFactory.Client sslContextFactory = scenario.newClientSslContextFactory(); + scenario.client = new HttpClient(scenario.provideClientTransport(transport, sslContextFactory)) { @Override public ClientConnectionFactory newSslClientConnectionFactory(ClientConnectionFactory connectionFactory) diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java new file mode 100644 index 00000000000..07bbfa5c263 --- /dev/null +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java @@ -0,0 +1,509 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http.client; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Function; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.HttpRequest; +import org.eclipse.jetty.client.MultiplexHttpDestination; +import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.client.proxy.ProxyProtocolClientConnectionFactory; +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.server.AbstractConnectionFactory; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.ProxyConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HttpClientTransportDynamicTest +{ + private Server server; + private ServerConnector connector; + private HttpClient client; + + private void startServer(Function connectorFn, Handler handler) throws Exception + { + prepareServer(connectorFn, handler); + server.start(); + } + + private void prepareServer(Function connectorFn, Handler handler) + { + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + serverThreads.setDetailedDump(true); + server = new Server(serverThreads); + connector = connectorFn.apply(server); + server.setHandler(handler); + } + + private ServerConnector h1(Server server) + { + HttpConfiguration httpConfiguration = new HttpConfiguration(); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpConfiguration); + ServerConnector connector = new ServerConnector(server, 1, 1, h1); + server.addConnector(connector); + return connector; + } + + private ServerConnector h1_h2c(Server server) + { + HttpConfiguration httpConfiguration = new HttpConfiguration(); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpConfiguration); + HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfiguration); + ServerConnector connector = new ServerConnector(server, 1, 1, h1, h2c); + server.addConnector(connector); + return connector; + } + + private ServerConnector ssl_alpn_h1(Server server) + { + HttpConfiguration httpConfiguration = new HttpConfiguration(); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpConfiguration); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + SslContextFactory.Server sslContextFactory = newServerSslContextFactory(); + ServerConnector connector = new ServerConnector(server, 1, 1, AbstractConnectionFactory.getFactories(sslContextFactory, alpn, h1)); + server.addConnector(connector); + return connector; + } + + private ServerConnector ssl_h1_h2c(Server server) + { + HttpConfiguration httpConfiguration = new HttpConfiguration(); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpConfiguration); + HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfiguration); + SslContextFactory.Server sslContextFactory = newServerSslContextFactory(); + // No ALPN. + ServerConnector connector = new ServerConnector(server, 1, 1, AbstractConnectionFactory.getFactories(sslContextFactory, h1, h2c)); + server.addConnector(connector); + return connector; + } + + private ServerConnector ssl_alpn_h1_h2(Server server) + { + HttpConfiguration httpConfiguration = new HttpConfiguration(); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpConfiguration); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfiguration); + SslContextFactory.Server sslContextFactory = newServerSslContextFactory(); + // Make explicitly h1 the default protocol (normally it would be h2). + ServerConnector connector = new ServerConnector(server, 1, 1, AbstractConnectionFactory.getFactories(sslContextFactory, alpn, h1, h2)); + server.addConnector(connector); + return connector; + } + + private ServerConnector ssl_alpn_h2_h1(Server server) + { + HttpConfiguration httpConfiguration = new HttpConfiguration(); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpConfiguration); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfiguration); + SslContextFactory.Server sslContextFactory = newServerSslContextFactory(); + ServerConnector connector = new ServerConnector(server, 1, 1, AbstractConnectionFactory.getFactories(sslContextFactory, alpn, h2, h1)); + server.addConnector(connector); + return connector; + } + + private ServerConnector proxy_h1_h2c(Server server) + { + HttpConfiguration httpConfiguration = new HttpConfiguration(); + HttpConnectionFactory h1 = new HttpConnectionFactory(httpConfiguration); + ProxyConnectionFactory proxy = new ProxyConnectionFactory(h1.getProtocol()); + ServerConnector connector = new ServerConnector(server, 1, 1, proxy, h1); + server.addConnector(connector); + return connector; + } + + @AfterEach + public void stop() throws Exception + { + if (server != null) + server.stop(); + if (client != null) + client.stop(); + } + + private SslContextFactory.Client newClientSslContextFactory() + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + configureSslContextFactory(sslContextFactory); + sslContextFactory.setEndpointIdentificationAlgorithm(null); + return sslContextFactory; + } + + private SslContextFactory.Server newServerSslContextFactory() + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + configureSslContextFactory(sslContextFactory); + return sslContextFactory; + } + + private void configureSslContextFactory(SslContextFactory sslContextFactory) + { + sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); + sslContextFactory.setKeyStorePassword("storepwd"); + // The mandatory HTTP/2 cipher. + sslContextFactory.setIncludeCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + } + + @Test + public void testClearTextHTTP1() throws Exception + { + startServer(this::h1_h2c, new EmptyServerHandler()); + + HttpClientConnectionFactory.Info h1c = HttpClientConnectionFactory.HTTP11; + client = new HttpClient(new HttpClientTransportDynamic(new ClientConnector(), h1c)); + client.start(); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .send(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + @Test + public void testClearTextHTTP2() throws Exception + { + startServer(this::h1_h2c, new EmptyServerHandler()); + + // TODO: why do we need HTTP2Client? we only use it for configuration, + // so the configuration can instead be moved to the CCF? + ClientConnector clientConnector = new ClientConnector(); + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h2c)); + client.addBean(http2Client); + client.start(); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) +// .version(HttpVersion.HTTP_2) + .send(); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + @Test + public void testClearTextProtocolSelection() throws Exception + { + startServer(this::h1_h2c, new EmptyServerHandler()); + testProtocolSelection(HttpScheme.HTTP); + } + + @Test + public void testEncryptedProtocolSelectionWithoutNegotiation() throws Exception + { + startServer(this::ssl_h1_h2c, new EmptyServerHandler()); + testProtocolSelection(HttpScheme.HTTPS); + } + + private void testProtocolSelection(HttpScheme scheme) throws Exception + { + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(newClientSslContextFactory()); + HttpClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(clientConnector, h1, h2c) + { + @Override + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) + { + // Use prior-knowledge, i.e. negotiate==false. + List protocols = HttpVersion.HTTP_2 == request.getVersion() ? h2c.getProtocols() : h1.getProtocols(); + return new HttpDestination.Key(origin, new HttpDestination.Protocol(protocols, false)); + } + }; + client = new HttpClient(transport); + client.addBean(http2Client); + client.start(); + + // Make a HTTP/1.1 request. + ContentResponse h1cResponse = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme.asString()) + .version(HttpVersion.HTTP_1_1) + .send(); + assertEquals(HttpStatus.OK_200, h1cResponse.getStatus()); + + // Make a HTTP/2 request. + ContentResponse h2cResponse = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme.asString()) + .version(HttpVersion.HTTP_2) + .send(); + assertEquals(HttpStatus.OK_200, h2cResponse.getStatus()); + + // We must have 2 different destinations with the same origin. + List destinations = client.getDestinations(); + assertEquals(2, destinations.size()); + assertEquals(1, destinations.stream() + .map(HttpDestination.class::cast) + .map(HttpDestination::getKey) + .map(HttpDestination.Key::getOrigin) + .distinct() + .count()); + } + + @Test + public void testEncryptedProtocolSelectionWithNegotiation() throws Exception + { + startServer(this::ssl_alpn_h1_h2, new EmptyServerHandler()); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(newClientSslContextFactory()); + HttpClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, h2)); + client.addBean(http2Client); + client.start(); + + // Make a request, should be HTTP/1.1 because of the order of protocols on server. + ContentResponse h1cResponse = client.newRequest("localhost", connector.getLocalPort()) + .scheme("https") + .send(); + assertEquals(HttpStatus.OK_200, h1cResponse.getStatus()); + + // Now clearly specify HTTP/2 in the client. + ContentResponse h2cResponse = client.newRequest("localhost", connector.getLocalPort()) + .scheme("https") + .version(HttpVersion.HTTP_2) + .send(); + assertEquals(HttpStatus.OK_200, h2cResponse.getStatus()); + + // We must have 2 different destinations with the same origin. + List destinations = client.getDestinations(); + assertEquals(2, destinations.size()); + assertEquals(1, destinations.stream() + .map(HttpDestination.class::cast) + .map(HttpDestination::getKey) + .map(HttpDestination.Key::getOrigin) + .distinct() + .count()); + } + + @Test + public void testServerOnlySpeaksEncryptedHTTP11ClientFallsBackToHTTP11() throws Exception + { + startServer(this::ssl_alpn_h1, new EmptyServerHandler()); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(newClientSslContextFactory()); + HttpClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h2, h1)); + client.addBean(http2Client); + client.start(); + + // The client prefers h2 over h1, and use of TLS and ALPN will allow the fallback to h1. + ContentResponse h1cResponse = client.newRequest("localhost", connector.getLocalPort()) + .scheme("https") + .send(); + assertEquals(HttpStatus.OK_200, h1cResponse.getStatus()); + } + + @Test + public void testServerOnlySpeaksClearTextHTTP11ClientFailsHTTP2() throws Exception + { + startServer(this::h1, new EmptyServerHandler()); + + ClientConnector clientConnector = new ClientConnector(); + HttpClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h1, h2c)); + client.addBean(http2Client); + client.start(); + + // The client forces HTTP/2, but the server cannot speak it, so the request fails. + // There is no fallback to HTTP/1 because the protocol version is set explicitly. + assertThrows(ExecutionException.class, () -> client.newRequest("localhost", connector.getLocalPort()) + .version(HttpVersion.HTTP_2) + .send()); + } + + @Test + public void testDestinationClientConnectionFactoryWrapped() throws Exception + { + // A more complicated test simulating a reverse proxy. + // + // If a reverse proxy is totally stateless, it can proxy multiple clients + // using the same connection pool (and hence just one destination). + // + // However, if we want to use the PROXY protocol, the proxy should have one + // destination per client IP:port, because we need to send the PROXY bytes + // with the client IP:port when opening a connection to the server. + // Note that if the client speaks HTTP/2 to the proxy, but the proxy speaks + // HTTP/1.1 to the server, there may be the need for the proxy to have + // multiple HTTP/1.1 connections for the same client IP:port. + + // client :1234 <-> :8888 proxy :5678 <-> server :8080 + // client :2345 <-> :8888 proxy :6789 <-> server :8080 + + startServer(this::proxy_h1_h2c, new EmptyServerHandler() + { + @Override + protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.setContentType(MimeTypes.Type.TEXT_PLAIN.asString()); + response.getOutputStream().print(request.getRemotePort()); + } + }); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(newClientSslContextFactory()); + ClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + + Map mapping = new ConcurrentHashMap<>(); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h1) + { + @Override + public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) + { + String kind = mapping.remove(request); + return new HttpDestination.Key(origin, new HttpDestination.Protocol(List.of("http/1.1"), false), kind); + } + + @Override + public HttpDestination newHttpDestination(HttpDestination.Key key) + { + // Here we want to wrap the destination with the PROXY + // protocol, for a specific remote client socket address. + return new MultiplexHttpDestination(client, key, factory -> new ProxyProtocolClientConnectionFactory(factory, () -> + { + String[] address = key.getKind().split(":"); + return new Origin.Address(address[0], Integer.parseInt(address[1])); + })); + } + }); + client.start(); + + // Simulate a proxy request to the server. + HttpRequest proxyRequest1 = (HttpRequest)client.newRequest("localhost", connector.getLocalPort()); + // Map the proxy request to client IP:port. + int clientPort1 = ThreadLocalRandom.current().nextInt(1024, 65536); + mapping.put(proxyRequest1, "localhost:" + clientPort1); + ContentResponse proxyResponse1 = proxyRequest1.send(); + assertEquals(String.valueOf(clientPort1), proxyResponse1.getContentAsString()); + + // Simulate another request to the server, from a different client port. + HttpRequest proxyRequest2 = (HttpRequest)client.newRequest("localhost", connector.getLocalPort()); + int clientPort2 = ThreadLocalRandom.current().nextInt(1024, 65536); + mapping.put(proxyRequest2, "localhost:" + clientPort2); + ContentResponse proxyResponse2 = proxyRequest2.send(); + assertEquals(String.valueOf(clientPort2), proxyResponse2.getContentAsString()); + + // We must have 2 different destinations with the same origin. + List destinations = client.getDestinations(); + assertEquals(2, destinations.size()); + assertEquals(1, destinations.stream() + .map(HttpDestination.class::cast) + .map(HttpDestination::getKey) + .map(HttpDestination.Key::getOrigin) + .distinct() + .count()); + } + + @Test + public void testClearTextAndEncryptedHTTP2() throws Exception + { + prepareServer(this::ssl_alpn_h2_h1, new EmptyServerHandler()); + ServerConnector clearConnector = h1_h2c(server); + server.start(); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(newClientSslContextFactory()); + HttpClientConnectionFactory.Info h1 = HttpClientConnectionFactory.HTTP11; + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactory.Info h2c = new ClientConnectionFactoryOverHTTP2.H2C(http2Client); + ClientConnectionFactory.Info h2 = new ClientConnectionFactoryOverHTTP2.H2(http2Client); + client = new HttpClient(new HttpClientTransportDynamic(clientConnector, h2, h1, h2c)); + client.addBean(http2Client); + client.start(); + + // Make a clear-text request using HTTP/1.1. + ContentResponse h1cResponse = client.newRequest("localhost", clearConnector.getLocalPort()) + .send(); + assertEquals(HttpStatus.OK_200, h1cResponse.getStatus()); + + // Make a clear-text request using HTTP/2. + ContentResponse h2cResponse = client.newRequest("localhost", clearConnector.getLocalPort()) + .version(HttpVersion.HTTP_2) + .send(); + assertEquals(HttpStatus.OK_200, h2cResponse.getStatus()); + + // Make an encrypted request without specifying the protocol. + // Because the server prefers h2, this request will be HTTP/2, but will + // generate a different destination than an explicit HTTP/2 request (like below). + ContentResponse h1Response = client.newRequest("localhost", connector.getLocalPort()) + .scheme("https") + .send(); + assertEquals(HttpStatus.OK_200, h1Response.getStatus()); + + // Make an encrypted request using explicitly HTTP/2. + ContentResponse h2Response = client.newRequest("localhost", connector.getLocalPort()) + .scheme("https") + .version(HttpVersion.HTTP_2) + .send(); + assertEquals(HttpStatus.OK_200, h2Response.getStatus()); + + // There should be 4 destinations with 2 origins. + List destinations = client.getDestinations(); + assertEquals(4, destinations.size()); + assertEquals(2, destinations.stream() + .map(HttpDestination.class::cast) + .map(HttpDestination::getKey) + .map(HttpDestination.Key::getOrigin) + .distinct() + .count()); + } +} diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/RoundRobinConnectionPoolTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/RoundRobinConnectionPoolTest.java index 740c38f5fd1..6f87ae0c3d8 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/RoundRobinConnectionPoolTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/RoundRobinConnectionPoolTest.java @@ -97,7 +97,7 @@ public class RoundRobinConnectionPoolTest extends AbstractTest 0) + if (transport != Transport.UNIX_SOCKET && i > 0) assertThat(remotePorts.get(i - 1), Matchers.not(Matchers.equalTo(candidate))); } } @@ -187,7 +187,7 @@ public class RoundRobinConnectionPoolTest extends AbstractTest 0) + if (transport != Transport.UNIX_SOCKET && i > 0) assertThat(remotePorts.get(i - 1), Matchers.not(Matchers.equalTo(candidate))); } } diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ServerTimeoutsTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ServerTimeoutsTest.java index 6724a583cb1..fd6cc2bf8cd 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ServerTimeoutsTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ServerTimeoutsTest.java @@ -58,10 +58,10 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import static org.eclipse.jetty.http.client.Transport.FCGI; import static org.eclipse.jetty.http.client.Transport.UNIX_SOCKET; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportProvider.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportProvider.java index fbd3dbf9e48..0e95658c71a 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportProvider.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportProvider.java @@ -20,10 +20,10 @@ package org.eclipse.jetty.http.client; import java.util.Arrays; import java.util.EnumSet; -import java.util.Set; import java.util.stream.Stream; import org.eclipse.jetty.util.StringUtil; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; @@ -35,21 +35,16 @@ public class TransportProvider implements ArgumentsProvider String transports = System.getProperty(Transport.class.getName()); if (!StringUtil.isBlank(transports)) - { - return Arrays.stream(transports.split("\\s*,\\s*")) - .map(Transport::valueOf); - } + return Arrays.stream(transports.split("\\s*,\\s*")).map(Transport::valueOf); - // TODO #2014 too many test failures, don't test unix socket client for now. - // if (OS.IS_UNIX) - // return Transport.values(); + if (OS.LINUX.isCurrentOs()) + return Arrays.stream(Transport.values()); - return EnumSet.complementOf(EnumSet.of(Transport.UNIX_SOCKET)) - .stream(); + return EnumSet.complementOf(EnumSet.of(Transport.UNIX_SOCKET)).stream(); } @Override - public Stream provideArguments(ExtensionContext context) throws Exception + public Stream provideArguments(ExtensionContext context) { return getActiveTransports().map(Arguments::of); } diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportScenario.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportScenario.java index c91d9a15e27..ac0af2d80b4 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportScenario.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportScenario.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.lang.management.ManagementFactory; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -41,6 +42,7 @@ import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.jmx.MBeanContainer; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; @@ -59,10 +61,15 @@ import org.eclipse.jetty.unixsocket.client.HttpClientTransportOverUnixSockets; import org.eclipse.jetty.unixsocket.server.UnixSocketConnector; import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.SocketAddressResolver; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.Assumptions; + +import static org.eclipse.jetty.http.client.Transport.UNIX_SOCKET; +import static org.junit.jupiter.api.Assumptions.assumeTrue; public class TransportScenario { @@ -70,32 +77,44 @@ public class TransportScenario protected final HttpConfiguration httpConfig = new HttpConfiguration(); protected final Transport transport; - protected SslContextFactory sslContextFactory; + protected SslContextFactory.Server sslContextFactory; protected Server server; protected Connector connector; protected ServletContextHandler context; protected String servletPath = "/servlet"; protected HttpClient client; protected Path sockFile; - protected final BlockingQueue requestLog= new BlockingArrayQueue<>(); + protected final BlockingQueue requestLog = new BlockingArrayQueue<>(); public TransportScenario(final Transport transport) throws IOException { this.transport = transport; - if(sockFile == null || !Files.exists( sockFile )) + Path unixSocketTmp; + String tmpProp = System.getProperty("unix.socket.tmp"); + if (StringUtil.isBlank(tmpProp)) + unixSocketTmp = MavenTestingUtils.getTargetPath(); + else + unixSocketTmp = Paths.get(tmpProp); + sockFile = Files.createTempFile(unixSocketTmp, "unix", ".sock"); + if (sockFile.toAbsolutePath().toString().length() > UnixSocketConnector.MAX_UNIX_SOCKET_PATH_LENGTH) { - Path target = MavenTestingUtils.getTargetPath(); - sockFile = Files.createTempFile(target,"unix", ".sock" ); - Files.delete( sockFile ); + Files.delete(sockFile); + Path tmp = Paths.get("/tmp"); + assumeTrue(Files.exists(tmp) && Files.isDirectory(tmp)); + sockFile = Files.createTempFile(tmp, "unix", ".sock"); } + Files.delete(sockFile); + + // Disable UNIX_SOCKET due to jnr/jnr-unixsocket#69. + Assumptions.assumeTrue(transport != UNIX_SOCKET); } public Optional getNetworkConnectorLocalPort() { if (connector instanceof ServerConnector) { - ServerConnector serverConnector = (ServerConnector) connector; + ServerConnector serverConnector = (ServerConnector)connector; return Optional.of(Integer.toString(serverConnector.getLocalPort())); } @@ -106,7 +125,7 @@ public class TransportScenario { if (connector instanceof ServerConnector) { - ServerConnector serverConnector = (ServerConnector) connector; + ServerConnector serverConnector = (ServerConnector)connector; return Optional.of(serverConnector.getLocalPort()); } @@ -118,24 +137,25 @@ public class TransportScenario return transport.isTlsBased() ? "https" : "http"; } - public HTTP2Client newHTTP2Client() + public HTTP2Client newHTTP2Client(SslContextFactory.Client sslContextFactory) { - HTTP2Client http2Client = new HTTP2Client(); - http2Client.setSelectors(1); - return http2Client; + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(sslContextFactory); + return new HTTP2Client(clientConnector); } - public HttpClient newHttpClient(HttpClientTransport transport, SslContextFactory sslContextFactory) + public HttpClient newHttpClient(HttpClientTransport transport) { - return new HttpClient(transport, sslContextFactory); + return new HttpClient(transport); } - public Connector newServerConnector(Server server) throws Exception + public Connector newServerConnector(Server server) { if (transport == Transport.UNIX_SOCKET) { - UnixSocketConnector unixSocketConnector = new UnixSocketConnector(server, provideServerConnectionFactory( transport )); - unixSocketConnector.setUnixSocket( sockFile.toString() ); + UnixSocketConnector unixSocketConnector = new UnixSocketConnector(server, provideServerConnectionFactory(transport)); + unixSocketConnector.setUnixSocket(sockFile.toString()); return unixSocketConnector; } return new ServerConnector(server, provideServerConnectionFactory(transport)); @@ -147,31 +167,26 @@ public class TransportScenario ret.append(getScheme()); ret.append("://localhost"); Optional localPort = getNetworkConnectorLocalPort(); - if (localPort.isPresent()) - { - ret.append(':').append(localPort.get()); - } + localPort.ifPresent(s -> ret.append(':').append(s)); return ret.toString(); } - public HttpClientTransport provideClientTransport() - { - return provideClientTransport(this.transport); - } - - public HttpClientTransport provideClientTransport(Transport transport) + public HttpClientTransport provideClientTransport(Transport transport, SslContextFactory.Client sslContextFactory) { switch (transport) { case HTTP: case HTTPS: { - return new HttpClientTransportOverHTTP(1); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSelectors(1); + clientConnector.setSslContextFactory(sslContextFactory); + return new HttpClientTransportOverHTTP(clientConnector); } case H2C: case H2: { - HTTP2Client http2Client = newHTTP2Client(); + HTTP2Client http2Client = newHTTP2Client(sslContextFactory); return new HttpClientTransportOverHTTP2(http2Client); } case FCGI: @@ -180,7 +195,7 @@ public class TransportScenario } case UNIX_SOCKET: { - return new HttpClientTransportOverUnixSockets( sockFile.toString() ); + return new HttpClientTransportOverUnixSockets(sockFile.toString()); } default: { @@ -235,13 +250,13 @@ public class TransportScenario throw new IllegalArgumentException(); } } - return result.toArray(new ConnectionFactory[result.size()]); + return result.toArray(new ConnectionFactory[0]); } public void setConnectionIdleTimeout(long idleTimeout) { if (connector instanceof AbstractConnector) - AbstractConnector.class.cast(connector).setIdleTimeout(idleTimeout); + ((AbstractConnector)connector).setIdleTimeout(idleTimeout); } public void setServerIdleTimeout(long idleTimeout) @@ -252,9 +267,10 @@ public class TransportScenario else setConnectionIdleTimeout(idleTimeout); } + public void start(Handler handler) throws Exception { - start(handler,null); + start(handler, null); } public void start(Handler handler, Consumer config) throws Exception @@ -279,13 +295,12 @@ public class TransportScenario QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); clientThreads.setDetailedDump(true); - SslContextFactory sslContextFactory = newSslContextFactory(); - sslContextFactory.setEndpointIdentificationAlgorithm(null); - client = newHttpClient(provideClientTransport(transport), sslContextFactory); + SslContextFactory.Client sslContextFactory = newClientSslContextFactory(); + client = newHttpClient(provideClientTransport(transport, sslContextFactory)); client.setExecutor(clientThreads); client.setSocketAddressResolver(new SocketAddressResolver.Sync()); - if (config!=null) + if (config != null) config.accept(client); client.start(); @@ -305,7 +320,7 @@ public class TransportScenario public void startServer(Handler handler) throws Exception { - sslContextFactory = newSslContextFactory(); + sslContextFactory = newServerSslContextFactory(); QueuedThreadPool serverThreads = new QueuedThreadPool(); serverThreads.setName("server"); serverThreads.setDetailedDump(true); @@ -318,7 +333,7 @@ public class TransportScenario server.setRequestLog((request, response) -> { int status = response.getCommittedMetaData().getStatus(); - requestLog.offer(String.format("%s %s %s %03d",request.getMethod(),request.getRequestURI(),request.getProtocol(),status)); + requestLog.offer(String.format("%s %s %s %03d", request.getMethod(), request.getRequestURI(), request.getProtocol(), status)); }); server.setHandler(handler); @@ -327,22 +342,35 @@ public class TransportScenario { server.start(); } - catch ( Exception e ) + catch (Exception e) { e.printStackTrace(); } } - protected SslContextFactory newSslContextFactory() + protected SslContextFactory.Server newServerSslContextFactory() + { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + configureSslContextFactory(sslContextFactory); + return sslContextFactory; + } + + protected SslContextFactory.Client newClientSslContextFactory() + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + configureSslContextFactory(sslContextFactory); + sslContextFactory.setEndpointIdentificationAlgorithm(null); + return sslContextFactory; + } + + private void configureSslContextFactory(SslContextFactory sslContextFactory) { - SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks"); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setTrustStorePath("src/test/resources/truststore.jks"); sslContextFactory.setTrustStorePassword("storepwd"); sslContextFactory.setUseCipherSuitesOrder(true); sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); - return sslContextFactory; } public void stopClient() throws Exception @@ -363,25 +391,25 @@ public class TransportScenario { stopClient(); } - catch (Exception ignore) + catch (Exception x) { - LOG.ignore(ignore); + LOG.ignore(x); } try { stopServer(); } - catch (Exception ignore) + catch (Exception x) { - LOG.ignore(ignore); + LOG.ignore(x); } - if (sockFile!=null) + if (sockFile != null) { try { - Files.deleteIfExists( sockFile ); + Files.deleteIfExists(sockFile); } catch (IOException e) { @@ -389,6 +417,4 @@ public class TransportScenario } } } - - } diff --git a/tests/test-integration/pom.xml b/tests/test-integration/pom.xml index 5fa756b46c7..8665ed2137f 100644 --- a/tests/test-integration/pom.xml +++ b/tests/test-integration/pom.xml @@ -30,11 +30,8 @@ copy-dependencies - org.eclipse.jetty.tests test war - true - true true ${project.build.directory}/webapps @@ -123,6 +120,36 @@ http2-server ${project.version} + + org.eclipse.jetty + jetty-annotations + ${project.version} + test + + + org.eclipse.jetty + jetty-servlets + ${project.version} + test + + + org.eclipse.jetty.websocket + jetty-websocket-server + ${project.version} + test + + + org.eclipse.jetty.websocket + jetty-websocket-client + ${project.version} + test + + + org.eclipse.jetty.websocket + javax-websocket-server + ${project.version} + test + org.eclipse.jetty.tests test-webapp-rfc2616 @@ -130,6 +157,13 @@ war test + + org.eclipse.jetty + test-jetty-webapp + ${project.version} + war + test + org.eclipse.jetty jetty-alpn-server diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/AnnotatedAsyncListenerTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/AnnotatedAsyncListenerTest.java new file mode 100644 index 00000000000..553af23cf41 --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/AnnotatedAsyncListenerTest.java @@ -0,0 +1,162 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.test; + +import java.io.BufferedWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import javax.annotation.Resource; +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.plus.jndi.EnvEntry; +import org.eclipse.jetty.plus.webapp.EnvConfiguration; +import org.eclipse.jetty.plus.webapp.PlusConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.FragmentConfiguration; +import org.eclipse.jetty.webapp.MetaInfConfiguration; +import org.eclipse.jetty.webapp.WebAppConfiguration; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.webapp.WebInfConfiguration; +import org.eclipse.jetty.webapp.WebXmlConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AnnotatedAsyncListenerTest +{ + private Server server; + private ServerConnector connector; + private HttpClient client; + + private void start(HttpServlet servlet) throws Exception + { + QueuedThreadPool serverExecutor = new QueuedThreadPool(); + serverExecutor.setName("server"); + server = new Server(serverExecutor); + connector = new ServerConnector(server, 1, 1); + server.addConnector(connector); + + Path webAppDir = MavenTestingUtils.getTargetTestingPath(AnnotatedAsyncListenerTest.class.getName() + "@" + servlet.hashCode()); + Path webInf = webAppDir.resolve("WEB-INF"); + Files.createDirectories(webInf); + + try (BufferedWriter writer = Files.newBufferedWriter(webInf.resolve("web.xml"), StandardCharsets.UTF_8, StandardOpenOption.CREATE)) + { + writer.write("" + + ""); + } + + WebAppContext context = new WebAppContext(webAppDir.toString(), "/"); + context.setConfigurations(new Configuration[] + { + new AnnotationConfiguration(), + new WebXmlConfiguration(), + new WebInfConfiguration(), + new MetaInfConfiguration(), + new FragmentConfiguration(), + new EnvConfiguration(), + new PlusConfiguration(), + new WebAppConfiguration() + }); + context.addServlet(new ServletHolder(servlet), "/*"); + new EnvEntry(context, "value", 1307D, false); + server.setHandler(context); + + QueuedThreadPool clientExecutor = new QueuedThreadPool(); + clientExecutor.setName("client"); + client = new HttpClient(); + client.setExecutor(clientExecutor); + server.addBean(client); + + server.start(); + } + + @AfterEach + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testAnnotatedAsyncListener() throws Exception + { + start(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException + { + AsyncContext asyncContext = request.startAsync(); + AnnotatedAsyncListener listener = asyncContext.createListener(AnnotatedAsyncListener.class); + assertEquals(listener.value, 1307D); + asyncContext.complete(); + } + }); + + ContentResponse response = client.GET("http://localhost:" + connector.getLocalPort() + "/test"); + assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + public static class AnnotatedAsyncListener implements AsyncListener + { + @Resource(mappedName = "value") + private Double value; + + @Override + public void onComplete(AsyncEvent event) + { + } + + @Override + public void onTimeout(AsyncEvent event) + { + } + + @Override + public void onError(AsyncEvent event) + { + } + + @Override + public void onStartAsync(AsyncEvent event) + { + } + } +} diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/DefaultHandlerTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/DefaultHandlerTest.java index 630ad8a75c8..6388bca0a63 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/DefaultHandlerTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/DefaultHandlerTest.java @@ -29,7 +29,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.test.support.TestableJettyServer; +import org.eclipse.jetty.test.support.XmlBasedJettyServer; import org.eclipse.jetty.test.support.rawhttp.HttpSocketImpl; import org.eclipse.jetty.test.support.rawhttp.HttpTesting; import org.eclipse.jetty.util.IO; @@ -50,16 +50,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; */ public class DefaultHandlerTest { - private static TestableJettyServer server; + private static XmlBasedJettyServer server; private int serverPort; @BeforeAll public static void setUpServer() throws Exception { - server = new TestableJettyServer(); + server = new XmlBasedJettyServer(); server.setScheme(HttpScheme.HTTP.asString()); - server.addConfiguration("DefaultHandler.xml"); - server.addConfiguration("NIOHttp.xml"); + server.addXmlConfiguration("DefaultHandler.xml"); + server.addXmlConfiguration("NIOHttp.xml"); server.load(); server.start(); diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputIntegrationTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputIntegrationTest.java index f718cd806bb..87bbab367e3 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputIntegrationTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/HttpInputIntegrationTest.java @@ -82,7 +82,7 @@ public class HttpInputIntegrationTest private static Server __server; private static HttpConfiguration __config; private static HttpConfiguration __sslConfig; - private static SslContextFactory __sslContextFactory; + private static SslContextFactory.Server __sslContextFactory; @BeforeAll public static void beforeClass() throws Exception @@ -101,11 +101,10 @@ public class HttpInputIntegrationTest // SSL Context Factory for HTTPS and HTTP/2 String jetty_distro = System.getProperty("jetty.distro","../../jetty-distribution/target/distribution"); - __sslContextFactory = new SslContextFactory(); + __sslContextFactory = new SslContextFactory.Server(); __sslContextFactory.setKeyStorePath(jetty_distro + "/../../../jetty-server/src/test/config/etc/keystore"); __sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); __sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); - __sslContextFactory.setEndpointIdentificationAlgorithm(null); // HTTPS Configuration __sslConfig = new HttpConfiguration(__config); diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616BaseTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616BaseTest.java index d0d58b46ce0..5569a2c8a0e 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616BaseTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616BaseTest.java @@ -33,7 +33,7 @@ import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.tools.HttpTester; import org.eclipse.jetty.test.support.StringUtil; -import org.eclipse.jetty.test.support.TestableJettyServer; +import org.eclipse.jetty.test.support.XmlBasedJettyServer; import org.eclipse.jetty.test.support.rawhttp.HttpSocket; import org.eclipse.jetty.test.support.rawhttp.HttpTesting; import org.eclipse.jetty.toolchain.test.FS; @@ -62,7 +62,7 @@ public abstract class RFC2616BaseTest private static final String ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n"; /** STRICT RFC TESTS */ private static final boolean STRICT = false; - private static TestableJettyServer server; + private static XmlBasedJettyServer server; private HttpTesting http; class TestFile @@ -88,7 +88,7 @@ public abstract class RFC2616BaseTest } } - public static void setUpServer(TestableJettyServer testableserver, Class testclazz) throws Exception + public static void setUpServer(XmlBasedJettyServer testableserver, Class testclazz) throws Exception { File testWorkDir = MavenTestingUtils.getTargetTestingDir(testclazz.getName()); FS.ensureDirExists(testWorkDir); diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616NIOHttpTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616NIOHttpTest.java index 4f3929142ae..472a7d20948 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616NIOHttpTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616NIOHttpTest.java @@ -19,7 +19,7 @@ package org.eclipse.jetty.test.rfcs; import org.eclipse.jetty.http.HttpScheme; -import org.eclipse.jetty.test.support.TestableJettyServer; +import org.eclipse.jetty.test.support.XmlBasedJettyServer; import org.eclipse.jetty.test.support.rawhttp.HttpSocket; import org.eclipse.jetty.test.support.rawhttp.HttpSocketImpl; import org.junit.jupiter.api.BeforeAll; @@ -32,12 +32,12 @@ public class RFC2616NIOHttpTest extends RFC2616BaseTest @BeforeAll public static void setupServer() throws Exception { - TestableJettyServer server = new TestableJettyServer(); + XmlBasedJettyServer server = new XmlBasedJettyServer(); server.setScheme(HttpScheme.HTTP.asString()); - server.addConfiguration("RFC2616Base.xml"); - server.addConfiguration("RFC2616_Redirects.xml"); - server.addConfiguration("RFC2616_Filters.xml"); - server.addConfiguration("NIOHttp.xml"); + server.addXmlConfiguration("RFC2616Base.xml"); + server.addXmlConfiguration("RFC2616_Redirects.xml"); + server.addXmlConfiguration("RFC2616_Filters.xml"); + server.addXmlConfiguration("NIOHttp.xml"); setUpServer(server, RFC2616NIOHttpTest.class); } diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616NIOHttpsTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616NIOHttpsTest.java index 34a98de8901..2a6d1c951fc 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616NIOHttpsTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/rfcs/RFC2616NIOHttpsTest.java @@ -19,7 +19,7 @@ package org.eclipse.jetty.test.rfcs; import org.eclipse.jetty.http.HttpScheme; -import org.eclipse.jetty.test.support.TestableJettyServer; +import org.eclipse.jetty.test.support.XmlBasedJettyServer; import org.eclipse.jetty.test.support.rawhttp.HttpSocket; import org.eclipse.jetty.test.support.rawhttp.HttpsSocketImpl; import org.junit.jupiter.api.BeforeAll; @@ -35,12 +35,12 @@ public class RFC2616NIOHttpsTest extends RFC2616BaseTest @BeforeAll public static void setupServer() throws Exception { - TestableJettyServer server = new TestableJettyServer(); + XmlBasedJettyServer server = new XmlBasedJettyServer(); server.setScheme(HttpScheme.HTTPS.asString()); - server.addConfiguration("RFC2616Base.xml"); - server.addConfiguration("RFC2616_Redirects.xml"); - server.addConfiguration("RFC2616_Filters.xml"); - server.addConfiguration("NIOHttps.xml"); + server.addXmlConfiguration("RFC2616Base.xml"); + server.addXmlConfiguration("RFC2616_Redirects.xml"); + server.addXmlConfiguration("RFC2616_Filters.xml"); + server.addXmlConfiguration("NIOHttps.xml"); setUpServer(server, RFC2616NIOHttpsTest.class); } diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/TestableJettyServer.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/XmlBasedJettyServer.java similarity index 86% rename from tests/test-integration/src/test/java/org/eclipse/jetty/test/support/TestableJettyServer.java rename to tests/test-integration/src/test/java/org/eclipse/jetty/test/support/XmlBasedJettyServer.java index a612e6592e7..1b471ec48ca 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/TestableJettyServer.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/XmlBasedJettyServer.java @@ -18,17 +18,12 @@ package org.eclipse.jetty.test.support; -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 java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URI; -import java.net.URL; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; @@ -39,16 +34,23 @@ import java.util.Properties; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.resource.PathResource; +import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlConfiguration; -import org.junit.jupiter.api.Disabled; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Allows for setting up a Jetty server for testing based on XML configuration files. */ -@Disabled -public class TestableJettyServer +public class XmlBasedJettyServer { - private List _xmlConfigurations; + private static final Logger LOG = Log.getLogger(XmlBasedJettyServer.class); + private List _xmlConfigurations; private final Map _properties = new HashMap<>(); private Server _server; private int _serverPort; @@ -58,7 +60,7 @@ public class TestableJettyServer private File baseDir; private File testResourcesDir; - public TestableJettyServer() throws IOException + public XmlBasedJettyServer() throws IOException { _xmlConfigurations = new ArrayList<>(); Properties properties = new Properties(); @@ -87,25 +89,25 @@ public class TestableJettyServer // Write out configuration for use by ConfigurationManager. File testConfig = new File(targetDir,"testable-jetty-server-config.properties"); FileOutputStream out = new FileOutputStream(testConfig); - properties.store(out,"Generated by " + TestableJettyServer.class.getName()); + properties.store(out,"Generated by " + XmlBasedJettyServer.class.getName()); for (Object key:properties.keySet()) _properties.put(String.valueOf(key),String.valueOf(properties.get(key))); } - public void addConfiguration(URL xmlConfig) + public void addXmlConfiguration(Resource xmlConfig) { _xmlConfigurations.add(xmlConfig); } - public void addConfiguration(File xmlConfigFile) throws MalformedURLException + public void addXmlConfiguration(File xmlConfigFile) { - _xmlConfigurations.add(xmlConfigFile.toURI().toURL()); + _xmlConfigurations.add(new PathResource(xmlConfigFile)); } - public void addConfiguration(String testConfigName) throws MalformedURLException + public void addXmlConfiguration(String testConfigName) throws MalformedURLException { - addConfiguration(new File(testResourcesDir,testConfigName)); + addXmlConfiguration(new File(testResourcesDir,testConfigName)); } public void setProperty(String key, String value) @@ -121,9 +123,9 @@ public class TestableJettyServer // Configure everything for (int i = 0; i < this._xmlConfigurations.size(); i++) { - URL configURL = this._xmlConfigurations.get(i); - System.err.println("configuring: "+configURL); - XmlConfiguration configuration = new XmlConfiguration(configURL); + Resource configResource = this._xmlConfigurations.get(i); + LOG.debug("configuring: "+configResource); + XmlConfiguration configuration = new XmlConfiguration(configResource); if (last != null) { configuration.getIdMap().putAll(last.getIdMap()); diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/rawhttp/HttpResponseTesterTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/rawhttp/HttpResponseTesterTest.java index 6bffef0bec2..d51526d426b 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/rawhttp/HttpResponseTesterTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/rawhttp/HttpResponseTesterTest.java @@ -26,10 +26,10 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.tools.HttpTester; import org.junit.jupiter.api.Test; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JavaxSimpleEchoSocket.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JavaxSimpleEchoSocket.java new file mode 100644 index 00000000000..edbbfb3f38a --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JavaxSimpleEchoSocket.java @@ -0,0 +1,72 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.test.websocket; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CountDownLatch; + +import javax.websocket.ClientEndpoint; +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +@ClientEndpoint( + subprotocols = {"chat"}) +public class JavaxSimpleEchoSocket +{ + private static final Logger LOG = Log.getLogger(JavaxSimpleEchoSocket.class); + private Session session; + public CountDownLatch messageLatch = new CountDownLatch(1); + public CountDownLatch closeLatch = new CountDownLatch(1); + + @OnError + public void onError(Throwable t) + { + LOG.warn(t); + fail(t.getMessage()); + } + + @OnClose + public void onClose(CloseReason close) + { + LOG.debug("Closed: {}, {}", close.getCloseCode().getCode(), close.getReasonPhrase()); + closeLatch.countDown(); + } + + @OnMessage + public void onMessage(String message) + { + LOG.debug("Received: {}", message); + messageLatch.countDown(); + } + + @OnOpen + public void onOpen(Session session) + { + LOG.debug("Opened"); + this.session = session; + } +} diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JavaxWebSocketTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JavaxWebSocketTest.java new file mode 100644 index 00000000000..3dfa2e5a99c --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JavaxWebSocketTest.java @@ -0,0 +1,89 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.test.websocket; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import javax.websocket.ContainerProvider; +import javax.websocket.RemoteEndpoint; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; + +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.test.support.XmlBasedJettyServer; +import org.eclipse.jetty.websocket.api.util.WSURI; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class JavaxWebSocketTest +{ + private static XmlBasedJettyServer server; + + @BeforeAll + public static void setUpServer() throws Exception + { + server = new XmlBasedJettyServer(); + server.setScheme(HttpScheme.HTTP.asString()); + server.addXmlConfiguration("basic-server.xml"); + server.addXmlConfiguration("login-service.xml"); + // server.addXmlConfiguration("configurations-addknown-all.xml"); + server.addXmlConfiguration("deploy.xml"); + server.addXmlConfiguration("NIOHttp.xml"); + + server.load(); + // server.getServer().setDumpAfterStart(true); + server.start(); + } + + @AfterAll + public static void tearDownServer() throws Exception + { + server.stop(); + } + + @Test + public void testChatEndpoint() throws Exception + { + URI uri = WSURI.toWebsocket(server.getServerURI().resolve("/test-jetty-webapp/javax.websocket")); + + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + + // to encourage client container to shutdown with server ... + server.getServer().addBean(container, true); + + JavaxSimpleEchoSocket socket = new JavaxSimpleEchoSocket(); + Session session = container.connectToServer(socket, uri); + try + { + RemoteEndpoint.Basic remote = session.getBasicRemote(); + String msg = "Foo"; + remote.sendText(msg); + assertTrue(socket.messageLatch.await(5, TimeUnit.SECONDS)); // give remote 1 second to respond + } + finally + { + session.close(); + assertTrue(socket.closeLatch.await(5, TimeUnit.SECONDS)); // give remote 1 second to acknowledge response + } + } +} diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JettySimpleEchoSocket.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JettySimpleEchoSocket.java new file mode 100644 index 00000000000..b5f43ebcf33 --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JettySimpleEchoSocket.java @@ -0,0 +1,84 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.test.websocket; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +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; + +/** + * Basic Echo Client Socket + */ +@WebSocket(maxTextMessageSize = 64 * 1024) +public class JettySimpleEchoSocket +{ + private static final Logger LOG = Log.getLogger(JettySimpleEchoSocket.class); + private final CountDownLatch closeLatch; + @SuppressWarnings("unused") + private Session session; + + public JettySimpleEchoSocket() + { + this.closeLatch = new CountDownLatch(1); + } + + public boolean awaitClose(int duration, TimeUnit unit) throws InterruptedException + { + return this.closeLatch.await(duration, unit); + } + + @OnWebSocketClose + public void onClose(int statusCode, String reason) + { + LOG.debug("Connection closed: {} - {}", statusCode, reason); + this.session = null; + this.closeLatch.countDown(); // trigger latch + } + + @OnWebSocketConnect + public void onConnect(Session session) + { + LOG.debug("Got connect: {}", session); + this.session = session; + try + { + session.getRemote().sendString("Foo"); + session.close(StatusCode.NORMAL, "I'm done"); + } + catch (Throwable t) + { + LOG.warn(t); + } + } + + @OnWebSocketMessage + public void onMessage(String msg) + { + LOG.debug("Got msg: {}", msg); + } +} diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JettyWebSocketTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JettyWebSocketTest.java new file mode 100644 index 00000000000..7eafa5352e8 --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/websocket/JettyWebSocketTest.java @@ -0,0 +1,85 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.test.websocket; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.test.support.XmlBasedJettyServer; +import org.eclipse.jetty.websocket.api.util.WSURI; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class JettyWebSocketTest +{ + private static XmlBasedJettyServer server; + + @BeforeAll + public static void setUpServer() throws Exception + { + server = new XmlBasedJettyServer(); + server.setScheme(HttpScheme.HTTP.asString()); + server.addXmlConfiguration("basic-server.xml"); + server.addXmlConfiguration("login-service.xml"); + // server.addXmlConfiguration("configurations-addknown-all.xml"); + server.addXmlConfiguration("deploy.xml"); + server.addXmlConfiguration("NIOHttp.xml"); + + server.load(); + // server.getServer().setDumpAfterStart(true); + server.start(); + } + + @AfterAll + public static void tearDownServer() throws Exception + { + server.stop(); + } + + @Test + public void testChatEndpoint() throws Exception + { + URI uri = WSURI.toWebsocket(server.getServerURI().resolve("/test-jetty-webapp/ws/foo")); + + WebSocketClient client = new WebSocketClient(); + + try + { + JettySimpleEchoSocket socket = new JettySimpleEchoSocket(); + + client.start(); + + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols("chat"); + client.connect(socket,uri,request); + // wait for closed socket connection. + assertTrue(socket.awaitClose(5, TimeUnit.SECONDS)); + } + finally + { + client.stop(); + } + } +} diff --git a/tests/test-integration/src/test/resources/DefaultHandler.xml b/tests/test-integration/src/test/resources/DefaultHandler.xml index a438ef02119..d7fab77a311 100644 --- a/tests/test-integration/src/test/resources/DefaultHandler.xml +++ b/tests/test-integration/src/test/resources/DefaultHandler.xml @@ -1,59 +1,51 @@ - - - - - - - - - - + - - https - - 32768 - 8192 - 8192 - true - false - 4096 - - - - - - - - - - - - - - - /tests - /default - - default - - - - - + + https + + + 32768 + 8192 + 8192 + true + false + 4096 + - - - - true - 1000 + + + + + + + + + + + + + /tests + /default + + + + + default + + + + + + + + + + + true + 1000 diff --git a/tests/test-integration/src/test/resources/NIOHttp.xml b/tests/test-integration/src/test/resources/NIOHttp.xml index d58befb1b94..6f64037beb1 100644 --- a/tests/test-integration/src/test/resources/NIOHttp.xml +++ b/tests/test-integration/src/test/resources/NIOHttp.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-integration/src/test/resources/NIOHttps.xml b/tests/test-integration/src/test/resources/NIOHttps.xml index 5146752a77e..b08409060f8 100644 --- a/tests/test-integration/src/test/resources/NIOHttps.xml +++ b/tests/test-integration/src/test/resources/NIOHttps.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-integration/src/test/resources/RFC2616Base.xml b/tests/test-integration/src/test/resources/RFC2616Base.xml index b04737485aa..acb64a9c4ab 100644 --- a/tests/test-integration/src/test/resources/RFC2616Base.xml +++ b/tests/test-integration/src/test/resources/RFC2616Base.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-integration/src/test/resources/RFC2616_Filters.xml b/tests/test-integration/src/test/resources/RFC2616_Filters.xml index 73dbb385f87..f540bf0f7c9 100644 --- a/tests/test-integration/src/test/resources/RFC2616_Filters.xml +++ b/tests/test-integration/src/test/resources/RFC2616_Filters.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-integration/src/test/resources/RFC2616_Redirects.xml b/tests/test-integration/src/test/resources/RFC2616_Redirects.xml index 3be8ea63ba4..ab0c7c9da05 100644 --- a/tests/test-integration/src/test/resources/RFC2616_Redirects.xml +++ b/tests/test-integration/src/test/resources/RFC2616_Redirects.xml @@ -1,5 +1,5 @@ - + @@ -8,44 +8,44 @@ - - true - false - requestedPath + + true + false + requestedPath - - + + - - + + - - + + - - - - /redirect/(.*) - /tests/$1 - - - - + + + + /redirect/(.*) + /tests/$1 + + + + diff --git a/tests/test-integration/src/test/resources/add-jetty-test-webapp.xml b/tests/test-integration/src/test/resources/add-jetty-test-webapp.xml new file mode 100644 index 00000000000..07d1b2b74f0 --- /dev/null +++ b/tests/test-integration/src/test/resources/add-jetty-test-webapp.xml @@ -0,0 +1,16 @@ + + + + + + + /test-jetty-webapp + /test-jetty-webapp.war + + + 1024 + + + + + diff --git a/tests/test-integration/src/test/resources/basic-server.xml b/tests/test-integration/src/test/resources/basic-server.xml new file mode 100644 index 00000000000..fefca7e39e8 --- /dev/null +++ b/tests/test-integration/src/test/resources/basic-server.xml @@ -0,0 +1,42 @@ + + + + + + https + + + + 32768 + 8192 + 8192 + true + false + 4096 + + + + + + + + + + + + + + + + + + + + + + + + true + 1000 + + diff --git a/tests/test-integration/src/test/resources/configurations-addknown-all.xml b/tests/test-integration/src/test/resources/configurations-addknown-all.xml new file mode 100644 index 00000000000..d0a48f8dc9b --- /dev/null +++ b/tests/test-integration/src/test/resources/configurations-addknown-all.xml @@ -0,0 +1,25 @@ + + + + + + + + org.eclipse.jetty.webapp.FragmentConfiguration + org.eclipse.jetty.webapp.JettyWebXmlConfiguration + org.eclipse.jetty.webapp.WebXmlConfiguration + org.eclipse.jetty.webapp.WebAppConfiguration + org.eclipse.jetty.webapp.ServletsConfiguration + org.eclipse.jetty.webapp.JspConfiguration + org.eclipse.jetty.webapp.JaasConfiguration + org.eclipse.jetty.webapp.JndiConfiguration + org.eclipse.jetty.plus.webapp.PlusConfiguration + org.eclipse.jetty.plus.webapp.EnvConfiguration + org.eclipse.jetty.webapp.JmxConfiguration + org.eclipse.jetty.annotations.AnnotationConfiguration + org.eclipse.jetty.websocket.server.JettyWebSocketConfiguration + org.eclipse.jetty.websocket.jsr356.server.JavaxWebSocketConfiguration + + + + diff --git a/tests/test-integration/src/test/resources/deploy.xml b/tests/test-integration/src/test/resources/deploy.xml new file mode 100644 index 00000000000..21a9bc9b187 --- /dev/null +++ b/tests/test-integration/src/test/resources/deploy.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern + .*/[^/]*servlet-api-[^/]*\.jar$ + + + + + + 1 + true + + + /testable-jetty-server-config.properties + + + + + + + + + diff --git a/tests/test-integration/src/test/resources/docroots/deployerror/badapp-unavailable-false.xml b/tests/test-integration/src/test/resources/docroots/deployerror/badapp-unavailable-false.xml index 162802692d5..c51104ddc6b 100644 --- a/tests/test-integration/src/test/resources/docroots/deployerror/badapp-unavailable-false.xml +++ b/tests/test-integration/src/test/resources/docroots/deployerror/badapp-unavailable-false.xml @@ -1,5 +1,5 @@ - + /badapp-uaf @@ -10,4 +10,4 @@ org.eclipse.jetty.containerInitializerExclusionPattern org.jboss.* - \ No newline at end of file + diff --git a/tests/test-integration/src/test/resources/docroots/deployerror/badapp.xml b/tests/test-integration/src/test/resources/docroots/deployerror/badapp.xml index 529424cc124..5c45d195481 100644 --- a/tests/test-integration/src/test/resources/docroots/deployerror/badapp.xml +++ b/tests/test-integration/src/test/resources/docroots/deployerror/badapp.xml @@ -1,5 +1,5 @@ - + /badapp @@ -10,4 +10,4 @@ org.eclipse.jetty.containerInitializerExclusionPattern org.jboss.* - \ No newline at end of file + diff --git a/tests/test-integration/src/test/resources/login-service.xml b/tests/test-integration/src/test/resources/login-service.xml new file mode 100644 index 00000000000..64bdfabd3a7 --- /dev/null +++ b/tests/test-integration/src/test/resources/login-service.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + Test Realm + + false + + + + diff --git a/tests/test-integration/src/test/resources/ssl.xml b/tests/test-integration/src/test/resources/ssl.xml index eecff79a0c1..b85cff469c4 100644 --- a/tests/test-integration/src/test/resources/ssl.xml +++ b/tests/test-integration/src/test/resources/ssl.xml @@ -1,10 +1,9 @@ - + / / - SSL_RSA_WITH_DES_CBC_SHA diff --git a/tests/test-integration/src/test/resources/webapp-contexts/RFC2616/rfc2616-webapp.xml b/tests/test-integration/src/test/resources/webapp-contexts/RFC2616/rfc2616-webapp.xml index 36e624f56db..f7c83e419f1 100644 --- a/tests/test-integration/src/test/resources/webapp-contexts/RFC2616/rfc2616-webapp.xml +++ b/tests/test-integration/src/test/resources/webapp-contexts/RFC2616/rfc2616-webapp.xml @@ -1,5 +1,5 @@ - + /rfc2616-webapp /test-webapp-rfc2616.war diff --git a/tests/test-quickstart/src/test/java/org/eclipse/jetty/quickstart/QuickStartTest.java b/tests/test-quickstart/src/test/java/org/eclipse/jetty/quickstart/QuickStartTest.java index b5d378ad0f3..971662cc225 100644 --- a/tests/test-quickstart/src/test/java/org/eclipse/jetty/quickstart/QuickStartTest.java +++ b/tests/test-quickstart/src/test/java/org/eclipse/jetty/quickstart/QuickStartTest.java @@ -74,7 +74,7 @@ public class QuickStartTest if (contextXml != null) { // System.err.println("Applying "+contextXml); - XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXml.getURI()); + XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXml); xmlConfiguration.configure(webapp); } @@ -123,7 +123,7 @@ public class QuickStartTest if (contextXml != null) { // System.err.println("Applying "+contextXml); - XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXml.getURI()); + XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXml); xmlConfiguration.configure(webapp); } @@ -169,7 +169,7 @@ public class QuickStartTest if (contextXml != null) { // System.err.println("Applying "+contextXml); - XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXml.getURI().toURL()); + XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXml); xmlConfiguration.configure(webapp); } diff --git a/tests/test-quickstart/src/test/java/org/eclipse/jetty/quickstart/Quickstart.java b/tests/test-quickstart/src/test/java/org/eclipse/jetty/quickstart/Quickstart.java index c6c2a0a22fe..7b8cfcb6ac2 100644 --- a/tests/test-quickstart/src/test/java/org/eclipse/jetty/quickstart/Quickstart.java +++ b/tests/test-quickstart/src/test/java/org/eclipse/jetty/quickstart/Quickstart.java @@ -48,8 +48,7 @@ public class Quickstart //apply context xml file if (contextXml != null) { - // System.err.println("Applying "+contextXml); - XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXml.getURI()); + XmlConfiguration xmlConfiguration = new XmlConfiguration(contextXml); xmlConfiguration.configure(webapp); } diff --git a/tests/test-quickstart/src/test/resources/test-jndi.xml b/tests/test-quickstart/src/test/resources/test-jndi.xml index 5f4ab4fa2da..a683d5e5fe0 100644 --- a/tests/test-quickstart/src/test/resources/test-jndi.xml +++ b/tests/test-quickstart/src/test/resources/test-jndi.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-quickstart/src/test/resources/test-spec.xml b/tests/test-quickstart/src/test/resources/test-spec.xml index a136d28395f..9e6b25b2ed8 100644 --- a/tests/test-quickstart/src/test/resources/test-spec.xml +++ b/tests/test-quickstart/src/test/resources/test-spec.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-quickstart/src/test/resources/test.xml b/tests/test-quickstart/src/test/resources/test.xml index 85156563b4d..39e94e5a433 100644 --- a/tests/test-quickstart/src/test/resources/test.xml +++ b/tests/test-quickstart/src/test/resources/test.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-webapps/test-jaas-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/tests/test-webapps/test-jaas-webapp/src/main/webapp/WEB-INF/jetty-web.xml index ecd5e307bb0..3cbabe61ca3 100644 --- a/tests/test-webapps/test-jaas-webapp/src/main/webapp/WEB-INF/jetty-web.xml +++ b/tests/test-webapps/test-jaas-webapp/src/main/webapp/WEB-INF/jetty-web.xml @@ -1,8 +1,8 @@ - + - - test-jaas webapp is deployed. DO NOT USE IN PRODUCTION! + + The test-jaas webapp is deployed. DO NOT USE IN PRODUCTION! diff --git a/tests/test-webapps/test-jetty-webapp/pom.xml b/tests/test-webapps/test-jetty-webapp/pom.xml index 27b738317df..ef64ecfb3fe 100644 --- a/tests/test-webapps/test-jetty-webapp/pom.xml +++ b/tests/test-webapps/test-jetty-webapp/pom.xml @@ -141,6 +141,7 @@ org.eclipse.jetty jetty-servlets ${project.version} + provided org.eclipse.jetty.toolchain @@ -195,8 +196,8 @@ provided - javax.websocket - javax.websocket-api + org.eclipse.jetty.toolchain + jetty-javax-websocket-api provided @@ -207,7 +208,7 @@ org.eclipse.jetty.websocket - websocket-servlet + jetty-websocket-server ${project.version} provided @@ -217,12 +218,6 @@ ${project.version} test - - org.eclipse.jetty.websocket - jetty-websocket-server - ${project.version} - test - diff --git a/tests/test-webapps/test-jetty-webapp/src/main/assembly/embedded-jetty-web-for-webbundle.xml b/tests/test-webapps/test-jetty-webapp/src/main/assembly/embedded-jetty-web-for-webbundle.xml index eefda45fb50..0fd96f479be 100644 --- a/tests/test-webapps/test-jetty-webapp/src/main/assembly/embedded-jetty-web-for-webbundle.xml +++ b/tests/test-webapps/test-jetty-webapp/src/main/assembly/embedded-jetty-web-for-webbundle.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/etc/test-realm.xml b/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/etc/test-realm.xml index 72c6de06d68..f24b4f3e887 100644 --- a/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/etc/test-realm.xml +++ b/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/etc/test-realm.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/start.d/demo.ini b/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/modules/demo.mod similarity index 86% rename from tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/start.d/demo.ini rename to tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/modules/demo.mod index 5921b619377..8bb82d2cfcd 100644 --- a/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/start.d/demo.ini +++ b/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/modules/demo.mod @@ -4,20 +4,27 @@ # Additional ini files are in demo-base/start.d # -# Enable security via jaas, and configure it ---module=jaas -jetty.jaas.login.conf=etc/login.conf +[depends] +rewrite +jaas +test-keystore + +[xml] # Enable rewrite examples ---module=rewrite etc/demo-rewrite-rules.xml +# Add the test realm +etc/test-realm.xml + +[ini-template] +# Enable security via jaas, and configure it +jetty.jaas.login.conf=etc/login.conf + # Websocket chat examples needs websocket enabled # Don't start for all contexts (set to true in test.xml context) org.eclipse.jetty.websocket.jsr356=false ---module=websocket # Create and configure the test realm -etc/test-realm.xml jetty.demo.realm=etc/realm.properties diff --git a/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/webapps/test.xml b/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/webapps/test.xml index 57a03fb1e8f..95de38729a4 100644 --- a/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/webapps/test.xml +++ b/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/webapps/test.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml b/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml index bd97eb13606..20ba11f7391 100644 --- a/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml +++ b/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-env.xml b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-env.xml index adebfed771b..3ae653cd23c 100644 --- a/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-env.xml +++ b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-env.xml @@ -1,5 +1,5 @@ - + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-web.xml index 203f811885a..2755401a915 100644 --- a/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-web.xml +++ b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-web.xml @@ -1,8 +1,8 @@ - + - - test-jndi webapp is deployed. DO NOT USE IN PRODUCTION! + + The test-jndi webapp is deployed. DO NOT USE IN PRODUCTION! diff --git a/tests/test-webapps/test-proxy-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/tests/test-webapps/test-proxy-webapp/src/main/webapp/WEB-INF/jetty-web.xml index e2cb8ec33c4..1d81b215e87 100644 --- a/tests/test-webapps/test-proxy-webapp/src/main/webapp/WEB-INF/jetty-web.xml +++ b/tests/test-webapps/test-proxy-webapp/src/main/webapp/WEB-INF/jetty-web.xml @@ -1,6 +1,9 @@ - + + + The test-proxy webapp is deployed. DO NOT USE IN PRODUCTION! + /proxy diff --git a/tests/test-webapps/test-proxy-webapp/src/test/java/org/eclipse/jetty/TestTransparentProxyServer.java b/tests/test-webapps/test-proxy-webapp/src/test/java/org/eclipse/jetty/TestTransparentProxyServer.java index 002156a46fd..2a768019ef9 100644 --- a/tests/test-webapps/test-proxy-webapp/src/test/java/org/eclipse/jetty/TestTransparentProxyServer.java +++ b/tests/test-webapps/test-proxy-webapp/src/test/java/org/eclipse/jetty/TestTransparentProxyServer.java @@ -82,7 +82,7 @@ public class TestTransparentProxyServer // SSL configurations - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath(jetty_root + "/jetty-server/src/main/config/etc/keystore"); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); @@ -136,5 +136,4 @@ public class TestTransparentProxyServer server.start(); server.join(); } - } diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml index 4dd9b8fef0d..a47b61c39a3 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml @@ -73,6 +73,7 @@ org.eclipse.jetty.tests.test-spec-webapp Test Webapp for Servlet 3.1 Features + javax.transaction*;version="[1.1,1.3)", javax.servlet*;version="[2.6,4.1)", @@ -169,6 +170,27 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-java + + enforce + + + + + + org.eclipse.jetty:jetty-util + + + + + + + @@ -178,12 +200,6 @@ jakarta.transaction-api provided - - org.eclipse.jetty - jetty-server - ${project.version} - provided - org.eclipse.jetty.toolchain jetty-servlet-api @@ -194,20 +210,30 @@ jakarta.annotation-api provided - - org.eclipse.jetty.tests - test-web-fragment - ${project.version} - - - org.eclipse.jetty.tests - test-container-initializer - ${project.version} - - - org.eclipse.jetty - jetty-util - ${project.version} - + + org.eclipse.jetty.tests + test-web-fragment + ${project.version} + + + org.eclipse.jetty.tests + test-container-initializer + ${project.version} + + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/AnnotationTest.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/AnnotationTest.java index cf99b4b0daf..7ee2613e95a 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/AnnotationTest.java +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/AnnotationTest.java @@ -208,10 +208,10 @@ public class AnnotationTest extends HttpServlet boolean fragInitParamResult = "123".equals(config.getInitParameter("extra1")) && "345".equals(config.getInitParameter("extra2")); out.println("

    Result: "+(fragInitParamResult? "PASS": "FAIL")+"

    "); - __HandlesTypes = Arrays.asList( "javax.servlet.GenericServlet", "javax.servlet.http.HttpServlet", "com.acme.test.AsyncListenerServlet", + "com.acme.test.ClassLoaderServlet", "com.acme.test.AnnotationTest", "com.acme.test.RoleAnnotationTest", "com.acme.test.MultiPartTest", diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/ClassLoaderServlet.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/ClassLoaderServlet.java new file mode 100644 index 00000000000..0b7ce45b78b --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/ClassLoaderServlet.java @@ -0,0 +1,133 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package com.acme.test; + +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet(urlPatterns="/classloader") +public class ClassLoaderServlet extends HttpServlet +{ + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException + { + try + { + + PrintWriter writer = resp.getWriter(); + writer.println(""); + writer.println(""); + writer.println(""); + writer.println("

    ClassLoader Isolation Test

    "); + + // TODO uncomment the following once 9.4.19 is released with a fix for #3726 + /* + Class webappIO = IO.class; + URI webappURI = getLocationOfClass(webappIO); + String webappVersion = webappIO.getPackage().getImplementationVersion(); + Class serverIO = req.getServletContext().getClass().getClassLoader().loadClass("org.eclipse.jetty.util.IO"); + URI serverURI = getLocationOfClass(serverIO); + String serverVersion = serverIO.getPackage().getImplementationVersion(); + + writer.printf("

    Webapp loaded org.eclipse.jetty.util.IO(%s) from %s%n",webappVersion,webappURI); + writer.printf("
    Server loaded org.eclipse.jetty.util.IO(%s) from %s%n",serverVersion, serverURI); + if (webappVersion.equals(serverVersion)) + writer.println("
    Version Result: FAIL"); + else + writer.println("
    Version Result: PASS"); + if (webappURI.equals(serverURI)) + writer.println("
    URI Result: FAIL

    "); + else + writer.println("
    URI Result: PASS

    "); + */ + + writer.println(""); + writer.println(""); + writer.flush(); + writer.close(); + } + catch(Exception e) + { + throw new ServletException(e); + } + } + + /* ------------------------------------------------------------ */ + public static URI getLocationOfClass(Class clazz) + { + try + { + ProtectionDomain domain = clazz.getProtectionDomain(); + if (domain != null) + { + CodeSource source = domain.getCodeSource(); + if (source != null) + { + URL location = source.getLocation(); + + if (location != null) + return location.toURI(); + } + } + + String resourceName = clazz.getName().replace('.', '/') + ".class"; + ClassLoader loader = clazz.getClassLoader(); + URL url = (loader == null ? ClassLoader.getSystemClassLoader() : loader).getResource(resourceName); + if (url != null) + { + return getJarSource(url.toURI()); + } + } + catch (URISyntaxException e) + { + throw new RuntimeException(e); + } + return null; + } + + + public static URI getJarSource(URI uri) + { + try + { + if (!"jar".equals(uri.getScheme())) + return uri; + // Get SSP (retaining encoded form) + String s = uri.getRawSchemeSpecificPart(); + int bang_slash = s.indexOf("!/"); + if (bang_slash>=0) + s=s.substring(0,bang_slash); + return new URI(s); + } + catch(URISyntaxException e) + { + throw new IllegalArgumentException(e); + } + } +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/MultiPartTest.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/MultiPartTest.java index ad1b1cabdbc..e92abd8fa6b 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/MultiPartTest.java +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/MultiPartTest.java @@ -19,6 +19,8 @@ package com.acme.test; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.Collection; import javax.servlet.ServletConfig; @@ -30,7 +32,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; -import org.eclipse.jetty.util.IO; /** * MultiPartTest * @@ -58,7 +59,6 @@ public class MultiPartTest extends HttpServlet @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - try { response.setContentType("text/html"); @@ -78,7 +78,7 @@ public class MultiPartTest extends HttpServlet if (p.getContentType() == null || p.getContentType().startsWith("text/plain")) { out.println("

    "); - IO.copy(p.getInputStream(),out); + copy(p.getInputStream(),out); out.println("

    "); } } @@ -116,8 +116,21 @@ public class MultiPartTest extends HttpServlet throw new ServletException(e); } } - - - + // TODO remove inline once 9.4.19 is released with a fix for #3726 + public static void copy(InputStream in, + OutputStream out) + throws IOException + { + int bufferSize = 8192; + byte buffer[] = new byte[bufferSize]; + + while (true) + { + int len=in.read(buffer,0,bufferSize); + if (len<0 ) + break; + out.write(buffer,0,len); + } + } } diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/SecuredServlet.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/SecuredServlet.java index a98dfdbba6e..acfbe69a207 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/SecuredServlet.java +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/test/SecuredServlet.java @@ -42,8 +42,8 @@ public class SecuredServlet extends HttpServlet PrintWriter writer = resp.getWriter(); writer.println( ""); writer.println(""); - writer.println("

    @ServletSecurity

    "); writer.println(""); + writer.println("

    @ServletSecurity

    "); writer.println("
    ");
             writer.println("@ServletSecurity");
             writer.println("public class SecuredServlet");
    diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml
    index 217722556bf..dc0080cc9cb 100644
    --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml
    +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml
    @@ -1,5 +1,5 @@
     
    -
    +
     
     
     
    diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml
    index 36ff548a92b..ef5e00dfde6 100644
    --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml
    +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml
    @@ -1,5 +1,5 @@
     
    -
    +
     
     
     
    diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-env.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-env.xml
    index 909ac83052e..f0791e637cd 100644
    --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-env.xml
    +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-env.xml
    @@ -1,5 +1,5 @@
     
    -
    +
     
     
     
    diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-web.xml
    index e360f5c202a..4a0126dbd58 100644
    --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-web.xml
    +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-web.xml
    @@ -1,8 +1,13 @@
     
    -
    +
     
     
    -  
    -    test-spec webapp is deployed. DO NOT USE IN PRODUCTION!
    +  
    +    The test-spec webapp is deployed. DO NOT USE IN PRODUCTION!
       
    +  
     
    diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html
    index 2d50e5e7f6f..f7d94c3a675 100644
    --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html
    +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html
    @@ -24,7 +24,7 @@ This example tests some aspects of the servlet 3.1 specification:
      The source repository for this test is available here.

      -

      Test Servlet 2.5/3.0 Annotations, Fragments and Initializers

      +

      Test Servlet 2.5/3.1 Annotations, Fragments and Initializers

      @@ -65,10 +65,14 @@ Test of the annotation: -
      +

      Test ClassPath Isolation

      +

      Click the link to test classpath isolation of system and server classes

      +ClassPathServlet + +

      -
      + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/test/jetty-plugin-env.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/test/jetty-plugin-env.xml index 23c4b5b9cbc..bf3aef8c569 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/test/jetty-plugin-env.xml +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/test/jetty-plugin-env.xml @@ -1,5 +1,5 @@ - +