diff --git a/Jenkinsfile b/Jenkinsfile
index 0d9b2c54a3d..bfbf28a8a79 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -14,62 +14,63 @@ pipeline {
mavenBuild("jdk11", "-Pmongodb install", "maven3", true) // -Pautobahn
// Collect up the jacoco execution results (only on main build)
jacoco inclusionPattern: '**/org/eclipse/jetty/**/*.class',
- exclusionPattern: '' +
- // build tools
- '**/org/eclipse/jetty/ant/**' +
- ',**/org/eclipse/jetty/maven/**' +
- ',**/org/eclipse/jetty/jspc/**' +
- // example code / documentation
- ',**/org/eclipse/jetty/embedded/**' +
- ',**/org/eclipse/jetty/asyncrest/**' +
- ',**/org/eclipse/jetty/demo/**' +
- // special environments / late integrations
- ',**/org/eclipse/jetty/gcloud/**' +
- ',**/org/eclipse/jetty/infinispan/**' +
- ',**/org/eclipse/jetty/osgi/**' +
- ',**/org/eclipse/jetty/spring/**' +
- ',**/org/eclipse/jetty/http/spi/**' +
- // test classes
- ',**/org/eclipse/jetty/tests/**' +
- ',**/org/eclipse/jetty/test/**',
- execPattern: '**/target/jacoco.exec',
- classPattern: '**/target/classes',
- sourcePattern: '**/src/main/java'
+ exclusionPattern: '' +
+ // build tools
+ '**/org/eclipse/jetty/ant/**' +
+ ',**/org/eclipse/jetty/maven/**' +
+ ',**/org/eclipse/jetty/jspc/**' +
+ // example code / documentation
+ ',**/org/eclipse/jetty/embedded/**' +
+ ',**/org/eclipse/jetty/asyncrest/**' +
+ ',**/org/eclipse/jetty/demo/**' +
+ // special environments / late integrations
+ ',**/org/eclipse/jetty/gcloud/**' +
+ ',**/org/eclipse/jetty/infinispan/**' +
+ ',**/org/eclipse/jetty/osgi/**' +
+ ',**/org/eclipse/jetty/spring/**' +
+ ',**/org/eclipse/jetty/http/spi/**' +
+ // test classes
+ ',**/org/eclipse/jetty/tests/**' +
+ ',**/org/eclipse/jetty/test/**',
+ execPattern: '**/target/jacoco.exec',
+ classPattern: '**/target/classes',
+ sourcePattern: '**/src/main/java'
warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']]
junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml,**/target/autobahntestsuite-reports/*.xml'
}
}
-
+
stage("Build / Test - JDK12") {
agent { node { label 'linux' } }
- options { timeout(time: 120, unit: 'MINUTES') }
steps {
- mavenBuild("jdk12", "-Pmongodb install", "maven3", true)
- warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']]
- junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml'
+ timeout(time: 120, unit: 'MINUTES') {
+ mavenBuild("jdk12", "-Pmongodb install", "maven3", true)
+ warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']]
+ junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml'
+ }
}
}
stage("Build Javadoc") {
agent { node { label 'linux' } }
- options { timeout(time: 30, unit: 'MINUTES') }
steps {
- mavenBuild("jdk11", "install javadoc:javadoc -DskipTests", "maven3", true)
- warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'JavaDoc'], [parserName: 'Java']]
+ timeout(time: 30, unit: 'MINUTES') {
+ mavenBuild("jdk11", "install javadoc:javadoc -DskipTests", "maven3", true)
+ warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'JavaDoc'], [parserName: 'Java']]
+ }
}
}
stage("Checkstyle ") {
agent { node { label 'linux' } }
- options { timeout(time: 30, unit: 'MINUTES') }
steps {
- mavenBuild("jdk11", "install -DskipTests", "maven3", true)
- mavenBuild("jdk11", "install -f build-resources", "maven3", true)
- mavenBuild("jdk11", "install checkstyle:check -DskipTests", "maven3", true)
- recordIssues(
- enabledForFailure: true, aggregatingResults: true,
- tools: [java(), checkStyle(pattern: '**/target/checkstyle-result.xml', reportEncoding: 'UTF-8')]
- )
+ timeout(time: 30, unit: 'MINUTES') {
+ mavenBuild("jdk11", "install -f build-resources", "maven3", true)
+ mavenBuild("jdk11", "install checkstyle:check -DskipTests", "maven3", true)
+ recordIssues(
+ enabledForFailure: true, aggregatingResults: true,
+ tools: [java(), checkStyle(pattern: '**/target/checkstyle-result.xml', reportEncoding: 'UTF-8')])
+ }
}
}
}
@@ -88,28 +89,24 @@ pipeline {
}
}
-
def slackNotif() {
- script {
- try
- {
- 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}"
- }
- } catch (Exception e) {
- e.printStackTrace()
- echo "skip failure slack notification: " + e.getMessage()
+ script {
+ try {
+ 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}"
}
+ } catch (Exception e) {
+ e.printStackTrace()
+ echo "skip failure slack notification: " + e.getMessage()
}
+ }
}
-
/**
* To other developers, if you are using this method above, please use the following syntax.
*
@@ -125,15 +122,16 @@ def mavenBuild(jdk, cmdline, mvnName, junitPublishDisabled) {
def mavenOpts = '-Xms1g -Xmx4g -Djava.awt.headless=true'
withMaven(
- maven: mvnName,
- jdk: "$jdk",
- publisherStrategy: 'EXPLICIT',
- options: [junitPublisher(disabled: junitPublishDisabled),mavenLinkerPublisher(disabled: false),pipelineGraphPublisher(disabled: false)],
- mavenOpts: mavenOpts,
- mavenLocalRepo: localRepo) {
+ maven: mvnName,
+ jdk: "$jdk",
+ publisherStrategy: 'EXPLICIT',
+ options: [junitPublisher(disabled: junitPublishDisabled), mavenLinkerPublisher(disabled: false), pipelineGraphPublisher(disabled: false)],
+ mavenOpts: mavenOpts,
+ mavenLocalRepo: localRepo) {
// Some common Maven command line + provided command line
sh "mvn -Pci -V -B -T3 -e -fae -Dmaven.test.failure.ignore=true -Djetty.testtracker.log=true $cmdline -Dunix.socket.tmp=" + env.JENKINS_HOME
}
}
+
// vim: et:ts=2:sw=2:ft=groovy
diff --git a/KEYS.txt b/KEYS.txt
index acf04a8cc33..73b9fb49a4e 100644
--- a/KEYS.txt
+++ b/KEYS.txt
@@ -2,6 +2,6 @@
Jan Bartel
- * See exampleserver.xml
- *
- * See fileserver.xml - *
*/ public class FileServerXml { - public static void main(String[] args) throws Exception + public static Server createServer(int port, Path baseResource) throws Exception { + // Find Jetty XML (in classpath) that configures and starts Server. + // See src/main/resources/fileserver.xml Resource fileServerXml = Resource.newSystemResource("fileserver.xml"); XmlConfiguration configuration = new XmlConfiguration(fileServerXml); - Server server = (Server)configuration.configure(); + configuration.getProperties().put("http.port", Integer.toString(port)); + configuration.getProperties().put("fileserver.baseresource", baseResource.toAbsolutePath().toString()); + return (Server)configuration.configure(); + } + + public static void main(String[] args) throws Exception + { + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + Path userDir = Paths.get(System.getProperty("user.dir")); + Server server = createServer(port, userDir); server.start(); server.join(); } diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/HelloWorld.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/HelloWorld.java index 151432fe6b9..5218c5e9249 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/HelloWorld.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/HelloWorld.java @@ -51,7 +51,8 @@ public class HelloWorld extends AbstractHandler public static void main(String[] args) throws Exception { - Server server = new Server(8080); + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + Server server = new Server(port); server.setHandler(new HelloWorld()); server.start(); 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 c07590e1d37..0552725c53d 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 @@ -18,9 +18,12 @@ package org.eclipse.jetty.embedded; -import java.io.File; +import java.io.FileNotFoundException; 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.Arrays; import java.util.Date; import java.util.EnumSet; @@ -56,12 +59,15 @@ import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlets.PushCacheFilter; +import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.ssl.SslContextFactory; public class Http2Server { public static void main(String... args) throws Exception { + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + int securePort = ExampleUtil.getPort(args, "jetty.https.port", 8443); Server server = new Server(); MBeanContainer mbContainer = new MBeanContainer( @@ -69,10 +75,11 @@ public class Http2Server server.addBean(mbContainer); ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS); - String docroot = "src/main/resources/docroot"; - if (!new File(docroot).exists()) - docroot = "examples/embedded/src/main/resources/docroot"; - context.setResourceBase(docroot); + Path docroot = Paths.get("src/main/resources/docroot"); + if (!Files.exists(docroot)) + throw new FileNotFoundException(docroot.toString()); + + context.setBaseResource(new PathResource(docroot)); context.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); // context.addFilter(PushSessionCacheFilter.class,"/*",EnumSet.of(DispatcherType.REQUEST)); context.addFilter(PushedTilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); @@ -83,21 +90,21 @@ public class Http2Server // HTTP Configuration HttpConfiguration httpConfig = new HttpConfiguration(); httpConfig.setSecureScheme("https"); - httpConfig.setSecurePort(8443); + httpConfig.setSecurePort(securePort); httpConfig.setSendXPoweredBy(true); httpConfig.setSendServerVersion(true); // HTTP Connector ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig), new HTTP2CServerConnectionFactory(httpConfig)); - http.setPort(8080); + http.setPort(port); server.addConnector(http); // SSL Context Factory for HTTPS and HTTP/2 - String jettyDistro = System.getProperty("jetty.distro", "../../jetty-distribution/target/distribution"); - if (!new File(jettyDistro).exists()) - jettyDistro = "jetty-distribution/target/distribution"; + Path keystorePath = Paths.get("src/main/resources/etc/keystore").toAbsolutePath(); + if (!Files.exists(keystorePath)) + throw new FileNotFoundException(keystorePath.toString()); SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setKeyStorePath(jettyDistro + "/demo-base/etc/keystore"); + sslContextFactory.setKeyStorePath(keystorePath.toString()); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); @@ -119,7 +126,7 @@ public class Http2Server // HTTP/2 Connector ServerConnector http2Connector = new ServerConnector(server, ssl, alpn, h2, new HttpConnectionFactory(httpsConfig)); - http2Connector.setPort(8443); + http2Connector.setPort(securePort); server.addConnector(http2Connector); server.start(); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/JarServer.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/JarServer.java index c84ee9b3c33..027f5f922d6 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/JarServer.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/JarServer.java @@ -18,7 +18,11 @@ package org.eclipse.jetty.embedded; -import org.eclipse.jetty.server.Handler; +import java.io.FileNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerList; @@ -28,24 +32,36 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.resource.Resource; /** - * + * Example of serving content from a JAR file. + * The JAR file in this example does not belong to any Classpath. */ public class JarServer { - public static void main(String[] args) throws Exception + public static Server createServer(int port) throws Exception { - final Server server = new Server(8080); + Server server = new Server(port); + + Path jarFile = Paths.get("src/main/other/content.jar"); + if (!Files.exists(jarFile)) + throw new FileNotFoundException(jarFile.toString()); ServletContextHandler context = new ServletContextHandler(); Resource.setDefaultUseCaches(true); - Resource base = Resource.newResource("jar:file:src/main/resources/content.jar!/"); + Resource base = Resource.newResource("jar:" + jarFile.toAbsolutePath().toUri().toASCIIString() + "!/"); context.setBaseResource(base); context.addServlet(new ServletHolder(new DefaultServlet()), "/"); HandlerList handlers = new HandlerList(); - handlers.setHandlers(new Handler[]{context, new DefaultHandler()}); + handlers.addHandler(context); + handlers.addHandler(new DefaultHandler()); server.setHandler(handlers); + return server; + } + public static void main(String[] args) throws Exception + { + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + Server server = createServer(port); server.start(); server.join(); } diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/JettyDistribution.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/JettyDistribution.java index 5f707aca97f..32d09d17c99 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/JettyDistribution.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/JettyDistribution.java @@ -18,8 +18,9 @@ package org.eclipse.jetty.embedded; -import java.io.File; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; @@ -41,54 +42,80 @@ public class JettyDistribution static { Path distro = asJettyDistribution(System.getProperty("jetty.home")); + LOG.debug("JettyDistribution(prop(jetty.home)) = " + distro); if (distro == null) + { distro = asJettyDistribution(System.getenv().get("JETTY_HOME")); + LOG.debug("JettyDistribution(env(JETTY_HOME)) = " + distro); + } if (distro == null) { try { - Path working = new File(".").getAbsoluteFile().getCanonicalFile().toPath(); + Path working = Paths.get(System.getProperty("user.dir")); + LOG.debug("JettyDistribution(prop(user.dir)) = " + working); while (distro == null && working != null) { distro = asJettyDistribution(working.resolve("jetty-distribution/target/distribution").toString()); working = working.getParent(); } + LOG.debug("JettyDistribution(working.resolve(...)) = " + distro); } catch (Throwable th) { LOG.warn(th); } } + + if (distro == null) + { + LOG.info("JettyDistribution() FAILURE: NOT FOUND"); + } + else + { + LOG.debug("JettyDistribution() FOUND = " + distro); + } DISTRIBUTION = distro; } - private static Path asJettyDistribution(String test) + private static Path asJettyDistribution(String jettyHome) { try { - if (StringUtil.isBlank(test)) + if (jettyHome == null) { - LOG.info("asJettyDistribution {} is blank", test); return null; } - File dir = new File(test); - if (!dir.exists() || !dir.isDirectory()) + if (StringUtil.isBlank(jettyHome)) { - LOG.info("asJettyDistribution {} is not a directory", test); + LOG.debug("asJettyDistribution {} is blank", jettyHome); return null; } - File demoBase = new File(dir, "demo-base"); - if (!demoBase.exists() || !demoBase.isDirectory()) + Path dir = Paths.get(jettyHome); + if (!Files.exists(dir)) { - LOG.info("asJettyDistribution {} has no demo-base", test); + LOG.debug("asJettyDistribution {} does not exist", jettyHome); return null; } - LOG.info("asJettyDistribution {}", dir); - return dir.getAbsoluteFile().getCanonicalFile().toPath(); + if (!Files.isDirectory(dir)) + { + LOG.debug("asJettyDistribution {} is not a directory", jettyHome); + return null; + } + + Path demoBase = dir.resolve("demo-base"); + if (!Files.exists(demoBase) || !Files.isDirectory(demoBase)) + { + LOG.debug("asJettyDistribution {} has no demo-base", jettyHome); + return null; + } + + LOG.debug("asJettyDistribution {}", dir); + return dir.toAbsolutePath(); } catch (Exception e) { @@ -97,9 +124,16 @@ public class JettyDistribution return null; } + public static Path get() + { + if (DISTRIBUTION == null) + throw new RuntimeException("jetty-distribution not found"); + return DISTRIBUTION; + } + public static Path resolve(String path) { - return DISTRIBUTION.resolve(path); + return get().resolve(path); } public static void main(String... arg) 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 752d071f682..fa0b53a12d1 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 @@ -18,12 +18,16 @@ package org.eclipse.jetty.embedded; -import java.io.File; +import java.io.FileNotFoundException; import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.deploy.DeploymentManager; import org.eclipse.jetty.deploy.PropertiesConfigurationManager; +import org.eclipse.jetty.deploy.bindings.DebugListenerBinding; import org.eclipse.jetty.deploy.providers.WebAppProvider; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.jmx.MBeanContainer; @@ -35,6 +39,7 @@ import org.eclipse.jetty.rewrite.handler.ValidUrlRule; import org.eclipse.jetty.security.HashLoginService; import org.eclipse.jetty.server.AsyncRequestLogWriter; import org.eclipse.jetty.server.CustomRequestLog; +import org.eclipse.jetty.server.DebugListener; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -59,21 +64,21 @@ import org.eclipse.jetty.webapp.Configurations; */ public class LikeJettyXml { - public static void main(String[] args) throws Exception + public static Server createServer(int port, int securePort, boolean addDebugListener) throws Exception { // Path to as-built jetty-distribution directory - String jettyHomeBuild = JettyDistribution.DISTRIBUTION.toString(); + Path jettyHomeBuild = JettyDistribution.get(); // Find jetty home and base directories - String homePath = System.getProperty("jetty.home", jettyHomeBuild); - File homeDir = new File(homePath); + String homePath = System.getProperty("jetty.home", jettyHomeBuild.toString()); + Path homeDir = Paths.get(homePath); - String basePath = System.getProperty("jetty.base", homeDir + "/demo-base"); - File baseDir = new File(basePath); + String basePath = System.getProperty("jetty.base", homeDir.resolve("demo-base").toString()); + Path baseDir = Paths.get(basePath); // Configure jetty.home and jetty.base system properties - String jettyHome = homeDir.getAbsolutePath(); - String jettyBase = baseDir.getAbsolutePath(); + String jettyHome = homeDir.toAbsolutePath().toString(); + String jettyBase = baseDir.toAbsolutePath().toString(); System.setProperty("jetty.home", jettyHome); System.setProperty("jetty.base", jettyBase); @@ -91,7 +96,7 @@ public class LikeJettyXml // HTTP Configuration HttpConfiguration httpConfig = new HttpConfiguration(); httpConfig.setSecureScheme("https"); - httpConfig.setSecurePort(8443); + httpConfig.setSecurePort(securePort); httpConfig.setOutputBufferSize(32768); httpConfig.setRequestHeaderSize(8192); httpConfig.setResponseHeaderSize(8192); @@ -105,11 +110,6 @@ public class LikeJettyXml handlers.setHandlers(new Handler[]{contexts, new DefaultHandler()}); server.setHandler(handlers); - // Extra options - server.setDumpAfterStart(true); - server.setDumpBeforeStop(false); - server.setStopAtShutdown(true); - // === jetty-jmx.xml === MBeanContainer mbContainer = new MBeanContainer( ManagementFactory.getPlatformMBeanServer()); @@ -118,24 +118,21 @@ public class LikeJettyXml // === jetty-http.xml === ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); - http.setPort(8080); + http.setPort(port); http.setIdleTimeout(30000); server.addConnector(http); // === jetty-https.xml === // SSL Context Factory + Path keystorePath = Paths.get("src/main/resources/etc/keystore").toAbsolutePath(); + if (!Files.exists(keystorePath)) + throw new FileNotFoundException(keystorePath.toString()); SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setKeyStorePath(jettyHome + "/../../../jetty-server/src/test/config/etc/keystore"); + sslContextFactory.setKeyStorePath(keystorePath.toString()); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); - sslContextFactory.setTrustStorePath(jettyHome + "/../../../jetty-server/src/test/config/etc/keystore"); + sslContextFactory.setTrustStorePath(keystorePath.toString()); sslContextFactory.setTrustStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); - sslContextFactory.setExcludeCipherSuites("SSL_RSA_WITH_DES_CBC_SHA", - "SSL_DHE_RSA_WITH_DES_CBC_SHA", "SSL_DHE_DSS_WITH_DES_CBC_SHA", - "SSL_RSA_EXPORT_WITH_RC4_40_MD5", - "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", - "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", - "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA"); // SSL HTTP Configuration HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); @@ -145,14 +142,17 @@ public class LikeJettyXml ServerConnector sslConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConfig)); - sslConnector.setPort(8443); + sslConnector.setPort(securePort); server.addConnector(sslConnector); // === jetty-deploy.xml === DeploymentManager deployer = new DeploymentManager(); - //DebugListener debug = new DebugListener(System.out,true,true,true); - // server.addBean(debug); - // deployer.addLifeCycleBinding(new DebugListenerBinding(debug)); + if (addDebugListener) + { + DebugListener debug = new DebugListener(System.err, true, true, true); + server.addBean(debug); + deployer.addLifeCycleBinding(new DebugListenerBinding(debug)); + } deployer.setContexts(contexts); deployer.setContextAttribute( "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", @@ -208,6 +208,20 @@ public class LikeJettyXml login.setHotReload(false); server.addBean(login); + return server; + } + + public static void main(String[] args) throws Exception + { + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + int securePort = ExampleUtil.getPort(args, "jetty.https.port", 8443); + Server server = createServer(port, securePort, true); + + // Extra options + server.setDumpAfterStart(true); + server.setDumpBeforeStop(false); + server.setStopAtShutdown(true); + // Start the server server.start(); server.join(); 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 fa8f86f1c97..351d853c9fb 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 @@ -18,8 +18,10 @@ package org.eclipse.jetty.embedded; -import java.io.File; import java.io.FileNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.Connector; @@ -36,23 +38,13 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; */ public class ManyConnectors { - public static void main(String[] args) throws Exception + public static Server createServer(int plainPort, int securePort) throws Exception { // Since this example shows off SSL configuration, we need a keystore - // with the appropriate key. These lookup of jetty.home is purely a hack - // to get access to a keystore that we use in many unit tests and should - // probably be a direct path to your own keystore. - - String jettyDistKeystore = "../../jetty-distribution/target/distribution/demo-base/etc/test-keystore"; - String keystorePath = System.getProperty("example.keystore", jettyDistKeystore); - File keystoreFile = new File(keystorePath); - if (!keystoreFile.exists()) - { - keystorePath = "jetty-distribution/target/distribution/demo-base/etc/keystore"; - keystoreFile = new File(keystorePath); - if (!keystoreFile.exists()) - throw new FileNotFoundException(keystoreFile.getAbsolutePath()); - } + // with the appropriate key. + Path keystorePath = Paths.get("src/main/resources/etc/keystore").toAbsolutePath(); + if (!Files.exists(keystorePath)) + throw new FileNotFoundException(keystorePath.toString()); // Create a basic jetty server object without declaring the port. Since // we are configuring connectors directly we'll be setting ports on @@ -67,7 +59,7 @@ public class ManyConnectors // done. The port for secured communication is also set here. HttpConfiguration httpConfig = new HttpConfiguration(); httpConfig.setSecureScheme("https"); - httpConfig.setSecurePort(8443); + httpConfig.setSecurePort(securePort); httpConfig.setOutputBufferSize(32768); // HTTP connector @@ -77,7 +69,7 @@ public class ManyConnectors // configure an idle timeout. ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); - http.setPort(8080); + http.setPort(plainPort); http.setIdleTimeout(30000); // SSL Context Factory for HTTPS @@ -88,7 +80,7 @@ public class ManyConnectors // keystore to be used. SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setKeyStorePath(keystoreFile.getAbsolutePath()); + sslContextFactory.setKeyStorePath(keystorePath.toString()); sslContextFactory.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"); sslContextFactory.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g"); @@ -118,7 +110,7 @@ public class ManyConnectors ServerConnector https = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), new HttpConnectionFactory(httpsConfig)); - https.setPort(8443); + https.setPort(securePort); https.setIdleTimeout(500000); // Here you see the server having multiple connectors registered with @@ -132,7 +124,14 @@ public class ManyConnectors // Set a handler server.setHandler(new HelloHandler()); + return server; + } + public static void main(String[] args) throws Exception + { + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + int securePort = ExampleUtil.getPort(args, "jetty.https.port", 8443); + Server server = createServer(port, securePort); // Start the server server.start(); server.dumpStdErr(); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyContexts.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyContexts.java index 944cce31853..d64735e964f 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyContexts.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyContexts.java @@ -18,39 +18,42 @@ package org.eclipse.jetty.embedded; -import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; public class ManyContexts { - public static void main(String[] args) throws Exception + public static Server createServer(int port) { - final Server server = new Server(8080); + Server server = new Server(port); ContextHandler context = new ContextHandler("/"); context.setContextPath("/"); context.setHandler(new HelloHandler("Root Hello")); ContextHandler contextFR = new ContextHandler("/fr"); - contextFR.setHandler(new HelloHandler("Bonjoir")); + contextFR.setHandler(new HelloHandler("Bonjour")); ContextHandler contextIT = new ContextHandler("/it"); - contextIT.setHandler(new HelloHandler("Bongiorno")); + contextIT.setHandler(new HelloHandler("Buongiorno")); ContextHandler contextV = new ContextHandler("/"); contextV.setVirtualHosts(new String[]{"127.0.0.2"}); contextV.setHandler(new HelloHandler("Virtual Hello")); - ContextHandlerCollection contexts = new ContextHandlerCollection(); - contexts.setHandlers(new Handler[]{ - context, contextFR, contextIT, - contextV - }); + ContextHandlerCollection contexts = new ContextHandlerCollection( + context, contextFR, contextIT, contextV + ); server.setHandler(contexts); + return server; + } + public static void main(String[] args) throws Exception + { + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + Server server = createServer(port); server.start(); server.dumpStdErr(); server.join(); 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 1cbc13d0d4f..e6dfce9a1b9 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 @@ -30,6 +30,8 @@ import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ContextHandler; +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.server.handler.HandlerList; @@ -99,20 +101,23 @@ public class ManyHandlers HttpServletResponse response) throws IOException, ServletException { - request.setAttribute("welcome", "Hello"); + response.setHeader("X-Welcome", "Greetings from WelcomeWrapHandler"); super.handle(target, baseRequest, request, response); } } - public static void main(String[] args) throws Exception + public static Server createServer(int port) throws IOException { - final Server server = new Server(8080); + Server server = new Server(port); // create the handlers - final Handler param = new ParamHandler(); - final HandlerWrapper wrapper = new WelcomeWrapHandler(); - final Handler hello = new HelloHandler(); - final Handler dft = new DefaultHandler(); + Handler param = new ParamHandler(); + HandlerWrapper wrapper = new WelcomeWrapHandler(); + Handler hello = new HelloHandler(); + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setMinGzipSize(10); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.addIncludedMimeTypes("text/html"); // configure request logging File requestLogFile = File.createTempFile("demo", "log"); @@ -120,16 +125,47 @@ public class ManyHandlers server.setRequestLog(ncsaLog); // create the handler collections - HandlerCollection handlers = new HandlerCollection(); - HandlerList list = new HandlerList(); + HandlerList handlers = new HandlerList(); - // link them all together + // wrap contexts around specific handlers wrapper.setHandler(hello); - list.setHandlers(new Handler[]{param, new GzipHandler()}); - handlers.setHandlers(new Handler[]{list, dft}); + ContextHandler helloContext = new ContextHandler("/hello"); + helloContext.setHandler(wrapper); + ContextHandler paramContext = new ContextHandler("/params"); + paramContext.setHandler(param); + + ContextHandlerCollection contexts = new ContextHandlerCollection(helloContext, paramContext); + + // Wrap Contexts with GZIP + gzipHandler.setHandler(contexts); + + // Set the top level Handler List + handlers.addHandler(gzipHandler); + handlers.addHandler(new DefaultHandler()); server.setHandler(handlers); + /* At this point you have the following handler hierarchy. + * + * Server.handler: + * HandlerList + * \- GzipHandler + * | \- ContextHandlerCollection + * | \- ContextHandler ("/hello") + * | | \- WelcomeWrapHandler + * | | \- HelloHandler + * | \- ContextHandler ("/params") + * | \- ParamHandler + * \- DefaultHandler + */ + + return server; + } + + public static void main(String[] args) throws Exception + { + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + Server server = createServer(port); server.start(); server.join(); } diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyServletContexts.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyServletContexts.java index 53fa7317c04..d1adc4be4c3 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyServletContexts.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/ManyServletContexts.java @@ -29,9 +29,9 @@ import org.eclipse.jetty.servlet.ServletHolder; public class ManyServletContexts { - public static void main(String[] args) throws Exception + public static Server createServer(int port) { - Server server = new Server(8080); + Server server = new Server(port); // Setup JMX MBeanContainer mbContainer = new MBeanContainer( @@ -48,7 +48,7 @@ public class ManyServletContexts // Add servlets to root context root.addServlet(new ServletHolder(new HelloServlet("Hello")), "/"); root.addServlet(new ServletHolder(new HelloServlet("Ciao")), "/it/*"); - root.addServlet(new ServletHolder(new HelloServlet("Bonjoir")), "/fr/*"); + root.addServlet(new ServletHolder(new HelloServlet("Bonjour")), "/fr/*"); // Configure context "/other" for servlets ServletContextHandler other = new ServletContextHandler(contexts, @@ -57,6 +57,13 @@ public class ManyServletContexts other.addServlet(DefaultServlet.class.getCanonicalName(), "/"); other.addServlet(new ServletHolder(new HelloServlet("YO!")), "*.yo"); + return server; + } + + public static void main(String[] args) throws Exception + { + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + Server server = createServer(port); server.start(); server.dumpStdErr(); server.join(); diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/MinimalServlets.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/MinimalServlets.java index 5ce9a24016c..76a9fe82cda 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/MinimalServlets.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/MinimalServlets.java @@ -19,7 +19,6 @@ package org.eclipse.jetty.embedded; import java.io.IOException; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -29,13 +28,13 @@ import org.eclipse.jetty.servlet.ServletHandler; public class MinimalServlets { - public static void main(String[] args) throws Exception + + public static Server createServer(int port) { - // Create a basic jetty server object that will listen on port 8080. // Note that if you set this to port 0 then a randomly available port // will be assigned that you can either look in the logs for the port, // or programmatically obtain it for use in test cases. - Server server = new Server(8080); + Server server = new Server(port); // The ServletHandler is a dead simple way to create a context handler // that is backed by an instance of a Servlet. @@ -51,13 +50,20 @@ public class MinimalServlets // through a web.xml @WebServlet annotation, or anything similar. handler.addServletWithMapping(HelloServlet.class, "/*"); + return server; + } + + public static void main(String[] args) throws Exception + { + // Create a basic jetty server object that will listen on port 8080. + int port = ExampleUtil.getPort(args, "jetty.http.port", 8080); + Server server = createServer(port); + // Start things up! server.start(); // The use of server.join() the will make the current thread join and - // wait until the server is done executing. - // See - // http://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#join() + // wait until the server thread is done executing. server.join(); } @@ -66,11 +72,11 @@ public class MinimalServlets { @Override protected void doGet(HttpServletRequest request, - HttpServletResponse response) throws ServletException, - IOException + HttpServletResponse response) throws IOException { - response.setContentType("text/html"); response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("text/html"); + response.setCharacterEncoding("utf-8"); response.getWriter().println("* While the {@link HttpClient} APIs define the HTTP semantic (request, response, headers, etc.) - * how a HTTP exchange is carried over the network depends on implementations of this class. + * how an HTTP exchange is carried over the network depends on implementations of this class. *
* The default implementation uses the HTTP protocol to carry over the network the HTTP exchange, * but the HTTP exchange may also be carried using the FCGI protocol, the HTTP/2 protocol or, diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java index efb5e983e4d..adb02176f0e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.log.Logger; /** * {@link HttpContent} is a stateful, linear representation of the request content provided * by a {@link ContentProvider} that can be traversed one-way to obtain content buffers to - * send to a HTTP server. + * send to an HTTP server. *
* {@link HttpContent} offers the notion of a one-way cursor to traverse the content. * The cursor starts in a virtual "before" position and can be advanced using {@link #advance()} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index daf6a32dca0..1f073351179 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -50,7 +50,7 @@ import org.eclipse.jetty.util.log.Logger; *
{@link Request} represents a HTTP request, and offers a fluent interface to customize + *
{@link Request} represents an HTTP request, and offers a fluent interface to customize * various attributes such as the path, the headers, the content, etc.
*You can create {@link Request} objects via {@link HttpClient#newRequest(String)} and * you can send them using either {@link #send()} for a blocking semantic, or diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java index 95605aad404..07e4d9b9a35 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java @@ -29,7 +29,7 @@ import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.util.Callback; /** - *
{@link Response} represents a HTTP response and offers methods to retrieve status code, HTTP version + *
{@link Response} represents an HTTP response and offers methods to retrieve status code, HTTP version * and headers.
*{@link Response} objects are passed as parameters to {@link Response.Listener} callbacks, or as * future result of {@link Request#send()}.
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 9febba733de..310b0db9e8e 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 @@ -161,7 +161,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res } /** - * Parses a HTTP response in the receivers buffer. + * Parses an HTTP response in the receivers buffer. * * @return true to indicate that parsing should be interrupted (and will be resumed by another thread). */ @@ -173,10 +173,10 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res boolean complete = this.complete; this.complete = false; if (LOG.isDebugEnabled()) - LOG.debug("Parsed {}, remaining {} {}", handle, buffer.remaining(), parser); + LOG.debug("Parsed {}, remaining {} {}", handle, BufferUtil.length(buffer), parser); if (handle) return true; - if (!buffer.hasRemaining()) + if (!BufferUtil.hasContent(buffer)) return false; if (complete) { 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 99614a56d2d..50befdceabf 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 @@ -106,7 +106,7 @@ public class HostnameVerificationTest /** * This test is supposed to verify that hostname verification works as described in: * http://www.ietf.org/rfc/rfc2818.txt section 3.1. It uses a certificate with a common name different to localhost - * and sends a request to localhost. This should fail with a SSLHandshakeException. + * and sends a request to localhost. This should fail with an SSLHandshakeException. * * @throws Exception on test failure */ 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 2a732d2b92f..c6b0dd263dc 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 @@ -657,10 +657,10 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest assertEquals("1523430383", headerInfo.getParameter("nonce")); // test multiple authentications - ListInformational
message category as defined in the http://user@host:port/path/info;param?query#fragment
* this class will split it into the following undecoded optional elements:A HttpField that will be cached and used many times can be created as + *
An HttpField that will be cached and used many times can be created as
* a {@link PreEncodedHttpField}, which will use the {@link HttpFieldPreEncoder}
* instances discovered by the {@link ServiceLoader} to pre-encode the header
* for each version of HTTP in use. This will save garbage
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 aea328cca0a..8f570f63b81 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
@@ -20,9 +20,14 @@ package org.eclipse.jetty.http.pathmap;
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;
public class ServletPathSpec extends PathSpec
{
+
+ private static final Logger LOG = Log.getLogger(ServletPathSpec.class);
+
/**
* If a servlet or filter path mapping isn't a suffix mapping, ensure
* it starts with '/'
@@ -213,13 +218,13 @@ public class ServletPathSpec extends PathSpec
super.pathDepth = 0;
char lastChar = servletPathSpec.charAt(specLength - 1);
// prefix based
- if ((servletPathSpec.charAt(0) == '/') && (specLength > 1) && (lastChar == '*'))
+ if (servletPathSpec.charAt(0) == '/' && servletPathSpec.endsWith("/*"))
{
this.group = PathSpecGroup.PREFIX_GLOB;
this.prefix = servletPathSpec.substring(0, specLength - 2);
}
// suffix based
- else if (servletPathSpec.charAt(0) == '*')
+ else if (servletPathSpec.charAt(0) == '*' && servletPathSpec.length() > 1)
{
this.group = PathSpecGroup.SUFFIX_GLOB;
this.suffix = servletPathSpec.substring(2, specLength);
@@ -228,6 +233,11 @@ public class ServletPathSpec extends PathSpec
{
this.group = PathSpecGroup.EXACT;
this.prefix = servletPathSpec;
+ if (servletPathSpec.endsWith("*"))
+ {
+ LOG.warn("Suspicious URL pattern: '{}'; see sections 12.1 and 12.2 of the Servlet specification",
+ servletPathSpec);
+ }
}
for (int i = 0; i < specLength; i++)
@@ -276,11 +286,6 @@ public class ServletPathSpec extends PathSpec
{
throw new IllegalArgumentException("Servlet Spec 12.2 violation: glob '*' can only exist at end of prefix based matches: bad spec \"" + servletPathSpec + "\"");
}
-
- if (idx < 1 || servletPathSpec.charAt(idx - 1) != '/')
- {
- throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix glob '*' can only exist after '/': bad spec \"" + servletPathSpec + "\"");
- }
}
else if (servletPathSpec.startsWith("*."))
{
diff --git a/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties b/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties
index b4ad431489b..fe10cfdba3d 100644
--- a/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties
+++ b/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties
@@ -167,6 +167,7 @@ ustar=application/x-ustar
vcd=application/x-cdlink
vrml=model/vrml
vxml=application/voicexml+xml
+wasm=application/wasm
wav=audio/x-wav
wbmp=image/vnd.wap.wbmp
wml=text/vnd.wap.wml
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 e5bf0672a16..d60c7b9c46c 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
@@ -40,6 +40,7 @@ public class CookieCutterLenientTest
{
return Stream.of(
// Simple test to verify behavior
+ Arguments.of("x=y", "x", "y"),
Arguments.of("key=value", "key", "value"),
// Tests that conform to RFC2109
@@ -62,12 +63,17 @@ public class CookieCutterLenientTest
// quoted-string = ( <"> *(qdtext) <"> )
// qdtext =
The SPI interface for implementing a HTTP/2 session.
+ *The SPI interface for implementing an HTTP/2 session.
*This class extends {@link Session} by adding the methods required to * implement the HTTP/2 session functionalities.
*/ diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java index cca02086afb..b59d09c3238 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java @@ -25,7 +25,7 @@ import org.eclipse.jetty.http2.frames.Frame; import org.eclipse.jetty.util.Callback; /** - *The SPI interface for implementing a HTTP/2 stream.
+ *The SPI interface for implementing an HTTP/2 stream.
*This class extends {@link Stream} by adding the methods required to * implement the HTTP/2 stream functionalities.
*/ diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java index 91a492ea1b3..d5d63a95477 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java @@ -33,7 +33,7 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; /** - *A {@link Session} represents the client-side endpoint of a HTTP/2 connection to a single origin server.
+ *A {@link Session} represents the client-side endpoint of an HTTP/2 connection to a single origin server.
*Once a {@link Session} has been obtained, it can be used to open HTTP/2 streams:
** Session session = ...; @@ -140,7 +140,7 @@ public interface Session /** *A {@link Listener} is the passive counterpart of a {@link Session} and - * receives events happening on a HTTP/2 connection.
+ * receives events happening on an HTTP/2 connection. * * @see Session */ @@ -164,9 +164,9 @@ public interface Session /** *Callback method invoked when a new stream is being created upon - * receiving a HEADERS frame representing a HTTP request.
+ * receiving a HEADERS frame representing an HTTP request. *Applications should implement this method to process HTTP requests, - * typically providing a HTTP response via + * typically providing an HTTP response via * {@link Stream#headers(HeadersFrame, Callback)}.
*Applications can detect whether request DATA frames will be arriving * by testing {@link HeadersFrame#isEndStream()}. If the application is diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java index 913cd294029..d2b5e9ed5f4 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java @@ -29,8 +29,8 @@ import org.eclipse.jetty.util.Promise; *
A {@link Stream} represents a bidirectional exchange of data on top of a {@link Session}.
*Differently from socket streams, where the input and output streams are permanently associated * with the socket (and hence with the connection that the socket represents), there can be multiple - * HTTP/2 streams present concurrent for a HTTP/2 session.
- *A {@link Stream} maps to a HTTP request/response cycle, and after the request/response cycle is + * HTTP/2 streams present concurrent for an HTTP/2 session.
+ *A {@link Stream} maps to an HTTP request/response cycle, and after the request/response cycle is * completed, the stream is closed and removed from the session.
*Like {@link Session}, {@link Stream} is the active part and by calling its API applications * can generate events on the stream; conversely, {@link Stream.Listener} is the passive part, and @@ -51,7 +51,7 @@ public interface Stream public Session getSession(); /** - *
Sends the given HEADERS {@code frame} representing a HTTP response.
+ *Sends the given HEADERS {@code frame} representing an HTTP response.
* * @param frame the HEADERS frame to send * @param callback the callback that gets notified when the frame has been sent @@ -131,7 +131,7 @@ public interface Stream /** *A {@link Stream.Listener} is the passive counterpart of a {@link Stream} and receives - * events happening on a HTTP/2 stream.
+ * events happening on an HTTP/2 stream. * * @see Stream */ diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/ContinuationFrame.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/ContinuationFrame.java new file mode 100644 index 00000000000..39d69310ce8 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/ContinuationFrame.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.http2.frames; + +public class ContinuationFrame extends Frame +{ + private final int streamId; + private final boolean endHeaders; + + public ContinuationFrame(int streamId, boolean endHeaders) + { + super(FrameType.CONTINUATION); + this.streamId = streamId; + this.endHeaders = endHeaders; + } + + public int getStreamId() + { + return streamId; + } + + public boolean isEndHeaders() + { + return endHeaders; + } + + @Override + public String toString() + { + return String.format("%s#%d{end=%b}", super.toString(), getStreamId(), isEndHeaders()); + } +} diff --git a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/UnknownFrame.java similarity index 70% rename from jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java rename to jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/UnknownFrame.java index c39c5169bea..ffd88682b2a 100644 --- a/jetty-websocket/javax-websocket-server/src/test/java/org/eclipse/jetty/websocket/javax/server/DummyServerContainer.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/UnknownFrame.java @@ -16,14 +16,21 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.javax.server; +package org.eclipse.jetty.http2.frames; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; - -public class DummyServerContainer extends JavaxWebSocketServerContainer +public class UnknownFrame extends Frame { - public DummyServerContainer() + private final int frameType; + + public UnknownFrame(int frameType) { - super(new WebSocketMapping()); + super(null); + this.frameType = frameType; + } + + @Override + public String toString() + { + return String.format("%s,t=%d", super.toString(), frameType); } } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java index 82d18f96a84..01c6d29e570 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/BodyParser.java @@ -96,6 +96,11 @@ public abstract class BodyParser return headerParser.getLength(); } + protected int getFrameType() + { + return headerParser.getFrameType(); + } + protected void notifyData(DataFrame frame) { try @@ -223,9 +228,10 @@ public abstract class BodyParser } } - protected void streamFailure(int streamId, int error, String reason) + protected boolean streamFailure(int streamId, int error, String reason) { notifyStreamFailure(streamId, error, reason); + return false; } private void notifyStreamFailure(int streamId, int error, String reason) @@ -239,4 +245,9 @@ public abstract class BodyParser LOG.info("Failure while notifying listener " + listener, x); } } + + protected boolean rateControlOnEvent(Object o) + { + return headerParser.getRateControl().onEvent(o); + } } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ContinuationBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ContinuationBodyParser.java index 65e47d1c827..fc2e03e97a0 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ContinuationBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ContinuationBodyParser.java @@ -23,6 +23,7 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.ErrorCode; import org.eclipse.jetty.http2.Flags; +import org.eclipse.jetty.http2.frames.ContinuationFrame; import org.eclipse.jetty.http2.frames.HeadersFrame; public class ContinuationBodyParser extends BodyParser @@ -43,7 +44,15 @@ public class ContinuationBodyParser extends BodyParser protected void emptyBody(ByteBuffer buffer) { if (hasFlag(Flags.END_HEADERS)) - onHeaders(); + { + onHeaders(buffer); + } + else + { + ContinuationFrame frame = new ContinuationFrame(getStreamId(), hasFlag(Flags.END_HEADERS)); + if (!rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_continuation_frame_rate"); + } } @Override @@ -81,7 +90,7 @@ public class ContinuationBodyParser extends BodyParser headerBlockFragments.storeFragment(buffer, length, last); reset(); if (last) - return onHeaders(); + return onHeaders(buffer); return true; } } @@ -94,15 +103,20 @@ public class ContinuationBodyParser extends BodyParser return false; } - private boolean onHeaders() + private boolean onHeaders(ByteBuffer buffer) { ByteBuffer headerBlock = headerBlockFragments.complete(); MetaData metaData = headerBlockParser.parse(headerBlock, headerBlock.remaining()); + if (metaData == null) + return true; if (metaData == HeaderBlockParser.SESSION_FAILURE) return false; - if (metaData == null || metaData == HeaderBlockParser.STREAM_FAILURE) - return true; HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, headerBlockFragments.getPriorityFrame(), headerBlockFragments.isEndStream()); + if (metaData == HeaderBlockParser.STREAM_FAILURE) + { + if (!rateControlOnEvent(frame)) + return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_continuation_frame_rate"); + } notifyHeaders(frame); return true; } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java index ac9e7bab991..be0ede0a5cb 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/DataBodyParser.java @@ -48,9 +48,17 @@ public class DataBodyParser extends BodyParser protected void emptyBody(ByteBuffer buffer) { if (isPadding()) + { connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_data_frame"); + } else - onData(BufferUtil.EMPTY_BUFFER, false, 0); + { + DataFrame frame = new DataFrame(getStreamId(), BufferUtil.EMPTY_BUFFER, isEndStream()); + if (!isEndStream() && !rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_data_frame_rate"); + else + onData(frame); + } } @Override @@ -134,7 +142,11 @@ public class DataBodyParser extends BodyParser private void onData(ByteBuffer buffer, boolean fragment, int padding) { - DataFrame frame = new DataFrame(getStreamId(), buffer, !fragment && isEndStream(), padding); + onData(new DataFrame(getStreamId(), buffer, !fragment && isEndStream(), padding)); + } + + private void onData(DataFrame frame) + { notifyData(frame); } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java index 0e4d2dc5e9e..e735d9ada3e 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeaderParser.java @@ -30,14 +30,24 @@ import org.eclipse.jetty.http2.frames.FrameType; */ public class HeaderParser { + private final RateControl rateControl; private State state = State.LENGTH; private int cursor; - private int length; private int type; private int flags; private int streamId; + public HeaderParser(RateControl rateControl) + { + this.rateControl = rateControl; + } + + public RateControl getRateControl() + { + return rateControl; + } + protected void reset() { state = State.LENGTH; diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeadersBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeadersBodyParser.java index febdefb6c25..3054d5ee439 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeadersBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/HeadersBodyParser.java @@ -61,17 +61,23 @@ public class HeadersBodyParser extends BodyParser @Override protected void emptyBody(ByteBuffer buffer) { - if (hasFlag(Flags.END_HEADERS)) + if (hasFlag(Flags.PRIORITY)) + { + connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_headers_priority_frame"); + } + else if (hasFlag(Flags.END_HEADERS)) { MetaData metaData = headerBlockParser.parse(BufferUtil.EMPTY_BUFFER, 0); - onHeaders(0, 0, false, metaData); + HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, null, isEndStream()); + if (!rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_headers_frame_rate"); + else + onHeaders(frame); } else { headerBlockFragments.setStreamId(getStreamId()); headerBlockFragments.setEndStream(isEndStream()); - if (hasFlag(Flags.PRIORITY)) - connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_headers_priority_frame"); } } @@ -179,7 +185,15 @@ public class HeadersBodyParser extends BodyParser state = State.PADDING; loop = paddingLength == 0; if (metaData != HeaderBlockParser.STREAM_FAILURE) + { onHeaders(parentStreamId, weight, exclusive, metaData); + } + else + { + HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, null, isEndStream()); + if (!rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_headers_frame_rate"); + } } } else @@ -230,6 +244,11 @@ public class HeadersBodyParser extends BodyParser if (hasFlag(Flags.PRIORITY)) priorityFrame = new PriorityFrame(getStreamId(), parentStreamId, weight, exclusive); HeadersFrame frame = new HeadersFrame(getStreamId(), metaData, priorityFrame, isEndStream()); + onHeaders(frame); + } + + private void onHeaders(HeadersFrame frame) + { notifyHeaders(frame); } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/Parser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/Parser.java index 6ac14471695..62df89ef818 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/Parser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/Parser.java @@ -54,18 +54,22 @@ public class Parser private final HpackDecoder hpackDecoder; private final BodyParser[] bodyParsers; private UnknownBodyParser unknownBodyParser; - private int maxFrameLength; + private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH; private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS; private boolean continuation; private State state = State.HEADER; public Parser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize) + { + this(byteBufferPool, listener, maxDynamicTableSize, maxHeaderSize, RateControl.NO_RATE_CONTROL); + } + + public Parser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize, RateControl rateControl) { this.byteBufferPool = byteBufferPool; this.listener = listener; - this.headerParser = new HeaderParser(); + this.headerParser = new HeaderParser(rateControl == null ? RateControl.NO_RATE_CONTROL : rateControl); this.hpackDecoder = new HpackDecoder(maxDynamicTableSize, maxHeaderSize); - this.maxFrameLength = Frame.DEFAULT_MAX_LENGTH; this.bodyParsers = new BodyParser[FrameType.values().length]; } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java index 8cee350e91e..e56e573236e 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PingBodyParser.java @@ -66,7 +66,7 @@ public class PingBodyParser extends BodyParser if (buffer.remaining() >= 8) { buffer.get(payload); - return onPing(payload); + return onPing(buffer, payload); } else { @@ -80,7 +80,7 @@ public class PingBodyParser extends BodyParser payload[8 - cursor] = buffer.get(); --cursor; if (cursor == 0) - return onPing(payload); + return onPing(buffer, payload); break; } default: @@ -92,9 +92,11 @@ public class PingBodyParser extends BodyParser return false; } - private boolean onPing(byte[] payload) + private boolean onPing(ByteBuffer buffer, byte[] payload) { PingFrame frame = new PingFrame(payload, hasFlag(Flags.ACK)); + if (!rateControlOnEvent(frame)) + return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_ping_frame_rate"); reset(); notifyPing(frame); return true; diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java index 3a6058878a4..741bba433a7 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java @@ -42,7 +42,7 @@ public class PrefaceParser *Advances this parser after the {@link PrefaceFrame#PREFACE_PREAMBLE_BYTES}.
*This allows the HTTP/1.1 parser to parse the preamble of the preface, * which is a legal HTTP/1.1 request, and this parser will parse the remaining - * bytes, that are not parseable by a HTTP/1.1 parser.
+ * bytes, that are not parseable by an HTTP/1.1 parser. */ protected void directUpgrade() { diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java index a9d11398987..914e9387b30 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PriorityBodyParser.java @@ -103,7 +103,7 @@ public class PriorityBodyParser extends BodyParser if (getStreamId() == parentStreamId) return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_priority_frame"); int weight = (buffer.get() & 0xFF) + 1; - return onPriority(parentStreamId, weight, exclusive); + return onPriority(buffer, parentStreamId, weight, exclusive); } default: { @@ -114,9 +114,11 @@ public class PriorityBodyParser extends BodyParser return false; } - private boolean onPriority(int parentStreamId, int weight, boolean exclusive) + private boolean onPriority(ByteBuffer buffer, int parentStreamId, int weight, boolean exclusive) { PriorityFrame frame = new PriorityFrame(getStreamId(), parentStreamId, weight, exclusive); + if (!rateControlOnEvent(frame)) + return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_priority_frame_rate"); reset(); notifyPriority(frame); return true; diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/RateControl.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/RateControl.java new file mode 100644 index 00000000000..be8f82a7cf2 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/RateControl.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.http2.parser; + +/** + * Controls rate of events via {@link #onEvent(Object)}. + */ +public interface RateControl +{ + public static final RateControl NO_RATE_CONTROL = event -> true; + + /** + *Applications should call this method when they want to signal an + * event that is subject to rate control.
+ *Implementations should return true if the event does not exceed + * the desired rate, or false to signal that the event exceeded the + * desired rate.
+ * + * @param event the event subject to rate control. + * @return true IFF the rate is within limits + */ + public boolean onEvent(Object event); +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ServerParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ServerParser.java index 1b17ddfa73c..0f1715e6219 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ServerParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/ServerParser.java @@ -37,9 +37,9 @@ public class ServerParser extends Parser private State state = State.PREFACE; private boolean notifyPreface = true; - public ServerParser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize) + public ServerParser(ByteBufferPool byteBufferPool, Listener listener, int maxDynamicTableSize, int maxHeaderSize, RateControl rateControl) { - super(byteBufferPool, listener, maxDynamicTableSize, maxHeaderSize); + super(byteBufferPool, listener, maxDynamicTableSize, maxHeaderSize, rateControl); this.listener = listener; this.prefaceParser = new PrefaceParser(listener); } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java index 741dd95981d..b58fa8a8b51 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/SettingsBodyParser.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.http2.parser; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -72,7 +73,12 @@ public class SettingsBodyParser extends BodyParser @Override protected void emptyBody(ByteBuffer buffer) { - onSettings(buffer, new HashMap<>()); + boolean isReply = hasFlag(Flags.ACK); + SettingsFrame frame = new SettingsFrame(Collections.emptyMap(), isReply); + if (!isReply && !rateControlOnEvent(frame)) + connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_settings_frame_rate"); + else + onSettings(frame); } @Override @@ -200,6 +206,11 @@ public class SettingsBodyParser extends BodyParser return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_settings_max_frame_size"); SettingsFrame frame = new SettingsFrame(settings, hasFlag(Flags.ACK)); + return onSettings(frame); + } + + private boolean onSettings(SettingsFrame frame) + { reset(); notifySettings(frame); return true; @@ -207,40 +218,25 @@ public class SettingsBodyParser extends BodyParser public static SettingsFrame parseBody(final ByteBuffer buffer) { - final int bodyLength = buffer.remaining(); - final AtomicReferenceframeRef = new AtomicReference<>(); - SettingsBodyParser parser = new SettingsBodyParser(null, null) + AtomicReference frameRef = new AtomicReference<>(); + SettingsBodyParser parser = new SettingsBodyParser(new HeaderParser(RateControl.NO_RATE_CONTROL), new Parser.Listener.Adapter() { @Override - protected int getStreamId() + public void onSettings(SettingsFrame frame) { - return 0; + frameRef.set(frame); } @Override - protected int getBodyLength() - { - return bodyLength; - } - - @Override - protected boolean onSettings(ByteBuffer buffer, Map settings) - { - frameRef.set(new SettingsFrame(settings, false)); - return true; - } - - @Override - protected boolean connectionFailure(ByteBuffer buffer, int error, String reason) + public void onConnectionFailure(int error, String reason) { frameRef.set(null); - return false; } - }; - if (bodyLength == 0) - parser.emptyBody(buffer); - else + }); + if (buffer.hasRemaining()) parser.parse(buffer); + else + parser.emptyBody(buffer); return frameRef.get(); } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/UnknownBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/UnknownBodyParser.java index dbb29bfede2..90293fee9db 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/UnknownBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/UnknownBodyParser.java @@ -20,6 +20,9 @@ package org.eclipse.jetty.http2.parser; import java.nio.ByteBuffer; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.frames.UnknownFrame; + public class UnknownBodyParser extends BodyParser { private int cursor; @@ -34,7 +37,11 @@ public class UnknownBodyParser extends BodyParser { int length = cursor == 0 ? getBodyLength() : cursor; cursor = consume(buffer, length); - return cursor == 0; + boolean parsed = cursor == 0; + if (parsed && !rateControlOnEvent(new UnknownFrame(getFrameType()))) + return connectionFailure(buffer, ErrorCode.ENHANCE_YOUR_CALM_ERROR.code, "invalid_unknown_frame_rate"); + + return parsed; } private int consume(ByteBuffer buffer, int length) diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowRateControl.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowRateControl.java new file mode 100644 index 00000000000..00a2c769737 --- /dev/null +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowRateControl.java @@ -0,0 +1,65 @@ +// +// ======================================================================== +// 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.parser; + +import java.time.Duration; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An implementation of {@link RateControl} that limits the number of + * events within a time period.
+ *Events are kept in a queue and for each event the queue is first + * drained of the old events outside the time window, and then the new + * event is added to the queue. The size of the queue is maintained + * separately in an AtomicInteger and if it exceeds the max + * number of events then {@link #onEvent(Object)} returns {@code false}.
+ */ +public class WindowRateControl implements RateControl +{ + private final Queueevents = new ConcurrentLinkedQueue<>(); + private final AtomicInteger size = new AtomicInteger(); + private final int maxEvents; + private final long window; + + public WindowRateControl(int maxEvents, Duration window) + { + this.maxEvents = maxEvents; + this.window = window.toNanos(); + } + + @Override + public boolean onEvent(Object event) + { + long now = System.nanoTime(); + while (true) + { + Long time = events.peek(); + if (time == null) + break; + if (now < time) + break; + if (events.remove(time)) + size.decrementAndGet(); + } + events.add(now + window); + return size.incrementAndGet() <= maxEvents; + } +} diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java index 78505540939..2c3ec6b736c 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/WindowUpdateBodyParser.java @@ -61,7 +61,7 @@ public class WindowUpdateBodyParser extends BodyParser if (buffer.remaining() >= 4) { windowDelta = buffer.getInt() & 0x7F_FF_FF_FF; - return onWindowUpdate(windowDelta); + return onWindowUpdate(buffer, windowDelta); } else { @@ -78,7 +78,7 @@ public class WindowUpdateBodyParser extends BodyParser if (cursor == 0) { windowDelta &= 0x7F_FF_FF_FF; - return onWindowUpdate(windowDelta); + return onWindowUpdate(buffer, windowDelta); } break; } @@ -91,9 +91,17 @@ public class WindowUpdateBodyParser extends BodyParser return false; } - private boolean onWindowUpdate(int windowDelta) + private boolean onWindowUpdate(ByteBuffer buffer, int windowDelta) { - WindowUpdateFrame frame = new WindowUpdateFrame(getStreamId(), windowDelta); + int streamId = getStreamId(); + if (windowDelta == 0) + { + if (streamId == 0) + return connectionFailure(buffer, ErrorCode.PROTOCOL_ERROR.code, "invalid_window_update_frame"); + else + return streamFailure(streamId, ErrorCode.PROTOCOL_ERROR.code, "invalid_window_update_frame"); + } + WindowUpdateFrame frame = new WindowUpdateFrame(streamId, windowDelta); reset(); notifyWindowUpdate(frame); return true; diff --git a/jetty-http2/http2-common/src/test/java/org/eclipse/jetty/http2/frames/FrameFloodTest.java b/jetty-http2/http2-common/src/test/java/org/eclipse/jetty/http2/frames/FrameFloodTest.java new file mode 100644 index 00000000000..04685dcb743 --- /dev/null +++ b/jetty-http2/http2-common/src/test/java/org/eclipse/jetty/http2/frames/FrameFloodTest.java @@ -0,0 +1,161 @@ +// +// ======================================================================== +// 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.frames; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.UnaryOperator; + +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.Flags; +import org.eclipse.jetty.http2.hpack.HpackEncoder; +import org.eclipse.jetty.http2.parser.Parser; +import org.eclipse.jetty.http2.parser.WindowRateControl; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.MappedByteBufferPool; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.lessThan; + +public class FrameFloodTest +{ + private final ByteBufferPool byteBufferPool = new MappedByteBufferPool(); + + // Frame structure: + // | Len0 | Len1 | Len2 | Type | Flags | StreamID0 |StreamID1 |StreamID2 |StreamID3 | Payload... | + + private byte[] frameFrom(int length, int frameType, int flags, int streamId, byte[] payload) + { + byte[] result = new byte[3 + 1 + 1 + 4 + payload.length]; + result[0] = (byte)((length >>> 16) & 0xFF); + result[1] = (byte)((length >>> 8) & 0xFF); + result[2] = (byte)(length & 0xFF); + result[3] = (byte)frameType; + result[4] = (byte)flags; + result[5] = (byte)((streamId >>> 24) & 0xFF); + result[6] = (byte)((streamId >>> 16) & 0xFF); + result[7] = (byte)((streamId >>> 8) & 0xFF); + result[8] = (byte)(streamId & 0xFF); + System.arraycopy(payload, 0, result, 9, payload.length); + return result; + } + + @Test + public void testDataFrameFlood() + { + byte[] payload = new byte[0]; + testFrameFlood(null, frameFrom(payload.length, FrameType.DATA.getType(), 0, 13, payload)); + } + + @Test + public void testHeadersFrameFlood() + { + byte[] payload = new byte[0]; + testFrameFlood(null, frameFrom(payload.length, FrameType.HEADERS.getType(), Flags.END_HEADERS, 13, payload)); + } + + @Test + public void testInvalidHeadersFrameFlood() + { + // Invalid MetaData (no method, no scheme, etc). + MetaData.Request metadata = new MetaData.Request(null, (String)null, null, null, HttpVersion.HTTP_2, null, -1); + HpackEncoder encoder = new HpackEncoder(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + encoder.encode(buffer, metadata); + buffer.flip(); + byte[] payload = new byte[buffer.remaining()]; + buffer.get(payload); + testFrameFlood(null, frameFrom(payload.length, FrameType.HEADERS.getType(), Flags.END_HEADERS, 13, payload)); + } + + @Test + public void testPriorityFrameFlood() + { + byte[] payload = new byte[]{0, 0, 0, 7, 0}; + testFrameFlood(null, frameFrom(payload.length, FrameType.PRIORITY.getType(), 0, 13, payload)); + } + + @Test + public void testSettingsFrameFlood() + { + byte[] payload = new byte[0]; + testFrameFlood(null, frameFrom(payload.length, FrameType.SETTINGS.getType(), 0, 0, payload)); + } + + @Test + public void testPingFrameFlood() + { + byte[] payload = {0, 0, 0, 0, 0, 0, 0, 0}; + testFrameFlood(null, frameFrom(payload.length, FrameType.PING.getType(), 0, 0, payload)); + } + + @Test + public void testContinuationFrameFlood() + { + int streamId = 13; + byte[] headersPayload = new byte[0]; + byte[] headersBytes = frameFrom(headersPayload.length, FrameType.HEADERS.getType(), 0, streamId, headersPayload); + byte[] continuationPayload = new byte[0]; + testFrameFlood(headersBytes, frameFrom(continuationPayload.length, FrameType.CONTINUATION.getType(), 0, streamId, continuationPayload)); + } + + @Test + public void testUnknownFrameFlood() + { + byte[] payload = {0, 0, 0, 0}; + testFrameFlood(null, frameFrom(payload.length, 64, 0, 0, payload)); + } + + private void testFrameFlood(byte[] preamble, byte[] bytes) + { + AtomicBoolean failed = new AtomicBoolean(); + Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter() + { + @Override + public void onConnectionFailure(int error, String reason) + { + failed.set(true); + } + }, 4096, 8192, new WindowRateControl(8, Duration.ofSeconds(1))); + parser.init(UnaryOperator.identity()); + + if (preamble != null) + { + ByteBuffer buffer = ByteBuffer.wrap(preamble); + while (buffer.hasRemaining()) + { + parser.parse(buffer); + } + } + + int count = 0; + while (!failed.get()) + { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + while (buffer.hasRemaining()) + { + parser.parse(buffer); + } + assertThat("too many frames allowed", ++count, lessThan(1024)); + } + } +} diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java index 675d8269e94..9943f463c7b 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java @@ -461,6 +461,8 @@ public class HpackContext if (value != null && value.length() > 0) { int huffmanLen = Huffman.octetsNeeded(value); + if (huffmanLen < 0) + throw new IllegalStateException("bad value"); int lenLen = NBitInteger.octectsNeeded(7, huffmanLen); _huffmanValue = new byte[1 + lenLen + huffmanLen]; ByteBuffer buffer = ByteBuffer.wrap(_huffmanValue); diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java index ae10f58711f..db4eaeb892c 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java @@ -22,6 +22,7 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpTokens; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.util.BufferUtil; @@ -170,14 +171,35 @@ public class HpackDecoder name = Huffman.decode(buffer, length); else name = toASCIIString(buffer, length); - for (int i = 0; i < name.length(); i++) + check: + for (int i = name.length(); i-- > 0; ) { char c = name.charAt(i); - if (c >= 'A' && c <= 'Z') + if (c > 0xff) { - _builder.streamException("Uppercase header name %s", name); + _builder.streamException("Illegal header name %s", name); break; } + HttpTokens.Token token = HttpTokens.TOKENS[0xFF & c]; + switch (token.getType()) + { + case ALPHA: + if (c >= 'A' && c <= 'Z') + { + _builder.streamException("Uppercase header name %s", name); + break check; + } + break; + + case COLON: + case TCHAR: + case DIGIT: + break; + + default: + _builder.streamException("Illegal header name %s", name); + break check; + } } header = HttpHeader.CACHE.get(name); } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java index ebba4623667..04734317942 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java @@ -19,9 +19,10 @@ package org.eclipse.jetty.http2.hpack; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.EnumSet; +import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; @@ -34,24 +35,23 @@ import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry; -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.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; public class HpackEncoder { - public static final Logger LOG = Log.getLogger(HpackEncoder.class); - private static final HttpField[] __status = new HttpField[599]; - static final EnumSet __DO_NOT_HUFFMAN = + private static final Logger LOG = Log.getLogger(HpackEncoder.class); + private static final HttpField[] STATUSES = new HttpField[599]; + static final EnumSet DO_NOT_HUFFMAN = EnumSet.of( HttpHeader.AUTHORIZATION, HttpHeader.CONTENT_MD5, HttpHeader.PROXY_AUTHENTICATE, HttpHeader.PROXY_AUTHORIZATION); - static final EnumSet __DO_NOT_INDEX = + static final EnumSet DO_NOT_INDEX = EnumSet.of( // HttpHeader.C_PATH, // TODO more data needed // HttpHeader.DATE, // TODO more data needed @@ -71,23 +71,21 @@ public class HpackEncoder HttpHeader.LAST_MODIFIED, HttpHeader.SET_COOKIE, HttpHeader.SET_COOKIE2); - static final EnumSet __NEVER_INDEX = + static final EnumSet NEVER_INDEX = EnumSet.of( HttpHeader.AUTHORIZATION, HttpHeader.SET_COOKIE, HttpHeader.SET_COOKIE2); - private static final PreEncodedHttpField CONNECTION_TE = new PreEncodedHttpField(HttpHeader.CONNECTION, "te"); + private static final EnumSet IGNORED_HEADERS = EnumSet.of(HttpHeader.CONNECTION, HttpHeader.KEEP_ALIVE, + HttpHeader.PROXY_CONNECTION, HttpHeader.TRANSFER_ENCODING, HttpHeader.UPGRADE); private static final PreEncodedHttpField TE_TRAILERS = new PreEncodedHttpField(HttpHeader.TE, "trailers"); - private static final Trie specialHopHeaders = new ArrayTrie<>(6); static { for (HttpStatus.Code code : HttpStatus.Code.values()) { - __status[code.getCode()] = new PreEncodedHttpField(HttpHeader.C_STATUS, Integer.toString(code.getCode())); + STATUSES[code.getCode()] = new PreEncodedHttpField(HttpHeader.C_STATUS, Integer.toString(code.getCode())); } - specialHopHeaders.put("close", true); - specialHopHeaders.put("te", true); } private final HpackContext _context; @@ -182,33 +180,37 @@ public class HpackEncoder { MetaData.Response response = (MetaData.Response)metadata; int code = response.getStatus(); - HttpField status = code < __status.length ? __status[code] : null; + HttpField status = code < STATUSES.length ? STATUSES[code] : null; if (status == null) status = new HttpField.IntValueHttpField(HttpHeader.C_STATUS, code); encode(buffer, status); } - // Add all non-connection fields. + // Remove fields as specified in RFC 7540, 8.1.2.2. HttpFields fields = metadata.getFields(); if (fields != null) { - Set hopHeaders = fields.getCSV(HttpHeader.CONNECTION, false).stream() - .filter(v -> specialHopHeaders.get(v) == Boolean.TRUE) - .map(StringUtil::asciiToLowerCase) - .collect(Collectors.toSet()); + // For example: Connection: Close, TE, Upgrade, Custom. + Set hopHeaders = null; + for (String value : fields.getCSV(HttpHeader.CONNECTION, false)) + { + if (hopHeaders == null) + hopHeaders = new HashSet<>(); + hopHeaders.add(StringUtil.asciiToLowerCase(value)); + } for (HttpField field : fields) { - if (field.getHeader() == HttpHeader.CONNECTION) + HttpHeader header = field.getHeader(); + if (header != null && IGNORED_HEADERS.contains(header)) continue; - if (!hopHeaders.isEmpty() && hopHeaders.contains(StringUtil.asciiToLowerCase(field.getName()))) - continue; - if (field.getHeader() == HttpHeader.TE) + if (header == HttpHeader.TE) { - if (!field.contains("trailers")) - continue; - encode(buffer, CONNECTION_TE); - encode(buffer, TE_TRAILERS); + if (field.contains("trailers")) + encode(buffer, TE_TRAILERS); + continue; } + if (hopHeaders != null && hopHeaders.contains(StringUtil.asciiToLowerCase(field.getName()))) + continue; encode(buffer, field); } } @@ -318,12 +320,12 @@ public class HpackEncoder if (_debug) encoding = indexed ? "PreEncodedIdx" : "PreEncoded"; } - else if (__DO_NOT_INDEX.contains(header)) + else if (DO_NOT_INDEX.contains(header)) { // Non indexed field indexed = false; - boolean neverIndex = __NEVER_INDEX.contains(header); - boolean huffman = !__DO_NOT_HUFFMAN.contains(header); + boolean neverIndex = NEVER_INDEX.contains(header); + boolean huffman = !DO_NOT_HUFFMAN.contains(header); encodeName(buffer, neverIndex ? (byte)0x10 : (byte)0x00, 4, header.asString(), name); encodeValue(buffer, huffman, field.getValue()); @@ -346,7 +348,7 @@ public class HpackEncoder { // indexed indexed = true; - boolean huffman = !__DO_NOT_HUFFMAN.contains(header); + boolean huffman = !DO_NOT_HUFFMAN.contains(header); encodeName(buffer, (byte)0x40, 6, header.asString(), name); encodeValue(buffer, huffman, field.getValue()); if (_debug) @@ -399,19 +401,38 @@ public class HpackEncoder { // huffman literal value buffer.put((byte)0x80); - NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(value)); - Huffman.encode(buffer, value); + + int needed = Huffman.octetsNeeded(value); + if (needed >= 0) + { + NBitInteger.encode(buffer, 7, needed); + Huffman.encode(buffer, value); + } + else + { + // Not iso_8859_1 + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(bytes)); + Huffman.encode(buffer, bytes); + } } else { // add literal assuming iso_8859_1 - buffer.put((byte)0x00); + buffer.put((byte)0x00).mark(); NBitInteger.encode(buffer, 7, value.length()); for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (c < ' ' || c > 127) - throw new IllegalArgumentException(); + { + // Not iso_8859_1, so re-encode as UTF-8 + buffer.reset(); + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + NBitInteger.encode(buffer, 7, bytes.length); + buffer.put(bytes, 0, bytes.length); + return; + } buffer.put((byte)c); } } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java index 2b3d30dc345..ba54ecb2dad 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java @@ -46,7 +46,7 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder @Override public byte[] getEncodedField(HttpHeader header, String name, String value) { - boolean notIndexed = HpackEncoder.__DO_NOT_INDEX.contains(header); + boolean notIndexed = HpackEncoder.DO_NOT_INDEX.contains(header); ByteBuffer buffer = BufferUtil.allocate(name.length() + value.length() + 10); BufferUtil.clearToFill(buffer); @@ -56,8 +56,8 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder if (notIndexed) { // Non indexed field - boolean neverIndex = HpackEncoder.__NEVER_INDEX.contains(header); - huffman = !HpackEncoder.__DO_NOT_HUFFMAN.contains(header); + boolean neverIndex = HpackEncoder.NEVER_INDEX.contains(header); + huffman = !HpackEncoder.DO_NOT_HUFFMAN.contains(header); buffer.put(neverIndex ? (byte)0x10 : (byte)0x00); bits = 4; } @@ -72,7 +72,7 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder { // indexed buffer.put((byte)0x40); - huffman = !HpackEncoder.__DO_NOT_HUFFMAN.contains(header); + huffman = !HpackEncoder.DO_NOT_HUFFMAN.contains(header); bits = 6; } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java index 0bbe4f661d4..9edd8a0fd7f 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java @@ -20,6 +20,8 @@ package org.eclipse.jetty.http2.hpack; import java.nio.ByteBuffer; +import org.eclipse.jetty.util.Utf8StringBuilder; + public class Huffman { @@ -358,7 +360,7 @@ public class Huffman public static String decode(ByteBuffer buffer, int length) throws HpackException.CompressionException { - StringBuilder out = new StringBuilder(length * 2); + Utf8StringBuilder utf8 = new Utf8StringBuilder(length * 2); int node = 0; int current = 0; int bits = 0; @@ -378,7 +380,7 @@ public class Huffman throw new HpackException.CompressionException("EOS in content"); // terminal node - out.append(rowsym[node]); + utf8.append((byte)(0xFF & rowsym[node])); bits -= rowbits[node]; node = 0; } @@ -411,7 +413,7 @@ public class Huffman break; } - out.append(rowsym[node]); + utf8.append((byte)(0xFF & rowsym[node])); bits -= rowbits[node]; node = 0; } @@ -419,7 +421,27 @@ public class Huffman if (node != 0) throw new HpackException.CompressionException("Bad termination"); - return out.toString(); + return utf8.toString(); + } + + public static int octetsNeeded(String s) + { + return octetsNeeded(CODES, s); + } + + public static int octetsNeeded(byte[] b) + { + return octetsNeeded(CODES, b); + } + + public static void encode(ByteBuffer buffer, String s) + { + encode(CODES, buffer, s); + } + + public static void encode(ByteBuffer buffer, byte[] b) + { + encode(CODES, buffer, b); } public static int octetsNeededLC(String s) @@ -432,11 +454,6 @@ public class Huffman encode(LCCODES, buffer, s); } - public static int octetsNeeded(String s) - { - return octetsNeeded(CODES, s); - } - private static int octetsNeeded(final int[][] table, String s) { int needed = 0; @@ -445,18 +462,30 @@ public class Huffman { char c = s.charAt(i); if (c >= 128 || c < ' ') - throw new IllegalArgumentException(); + return -1; needed += table[c][1]; } return (needed + 7) / 8; } - public static void encode(ByteBuffer buffer, String s) + private static int octetsNeeded(final int[][] table, byte[] b) { - encode(CODES, buffer, s); + int needed = 0; + int len = b.length; + for (int i = 0; i < len; i++) + { + int c = 0xFF & b[i]; + needed += table[c][1]; + } + return (needed + 7) / 8; } + /** + * @param table The table to encode by + * @param buffer The buffer to encode to + * @param s The string to encode + */ private static void encode(final int[][] table, ByteBuffer buffer, String s) { long current = 0; @@ -488,4 +517,35 @@ public class Huffman buffer.put((byte)(current)); } } + + private static void encode(final int[][] table, ByteBuffer buffer, byte[] b) + { + long current = 0; + int n = 0; + + int len = b.length; + for (int i = 0; i < len; i++) + { + int c = 0xFF & b[i]; + int code = table[c][0]; + int bits = table[c][1]; + + current <<= bits; + current |= code; + n += bits; + + while (n >= 8) + { + n -= 8; + buffer.put((byte)(current >> n)); + } + } + + if (n > 0) + { + current <<= (8 - n); + current |= (0xFF >>> n); + buffer.put((byte)(current)); + } + } } 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 cad98d49c59..5db1f03ccae 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 @@ -76,11 +76,13 @@ public class MetaDataBuilder { HttpHeader header = field.getHeader(); String name = field.getName(); + if (name == null || name.length() == 0) + throw new HpackException.SessionException("Header size 0"); String value = field.getValue(); int fieldSize = name.length() + (value == null ? 0 : value.length()); _size += fieldSize + 32; if (_size > _maxSize) - throw new HpackException.SessionException("Header Size %d > %d", _size, _maxSize); + throw new HpackException.SessionException("Header size %d > %d", _size, _maxSize); if (field instanceof StaticTableHttpField) { diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java index ed4ac09d86a..a24de7a396a 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java @@ -104,7 +104,7 @@ public class HpackContextTest new HttpField("name", "v3"), new HttpField("name", "v4"), new HttpField("name", "v5"), - }; + }; Entry[] entry = new Entry[field.length]; @@ -197,7 +197,7 @@ public class HpackContextTest new HttpField("fo8", "b8r"), new HttpField("fo9", "b9r"), new HttpField("foA", "bAr"), - }; + }; Entry[] entry = new Entry[100]; @@ -324,7 +324,7 @@ public class HpackContextTest new HttpField("fo8", "b8r"), new HttpField("fo9", "b9r"), new HttpField("foA", "bAr"), - }; + }; Entry[] entry = new Entry[field.length]; // Add 5 entries diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java index 12526186674..85067ec768c 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.hpack.HpackException.CompressionException; +import org.eclipse.jetty.http2.hpack.HpackException.SessionException; import org.eclipse.jetty.http2.hpack.HpackException.StreamException; import org.eclipse.jetty.util.TypeUtil; import org.hamcrest.Matchers; @@ -43,6 +44,21 @@ import static org.junit.jupiter.api.Assertions.fail; public class HpackDecoderTest { + /* + 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 0 | 0 | 0 | 0 | + +---+---+-----------------------+ + | H | Name Length (7+) | + +---+---------------------------+ + | Name String (Length octets) | + +---+---------------------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length octets) | + +-------------------------------+ + */ + @Test public void testDecodeD_3() throws Exception { @@ -253,14 +269,14 @@ public class HpackDecoderTest decoder.decode(buffer); fail(); } - catch (HpackException.SessionException e) + catch (SessionException e) { assertThat(e.getMessage(), Matchers.startsWith("Unknown index")); } } /* 8.1.2.1. Pseudo-Header Fields */ - @Test() + @Test public void test8_1_2_1_PsuedoHeaderFields() throws Exception { // 1:Sends a HEADERS frame that contains a unknown pseudo-header field @@ -312,7 +328,7 @@ public class HpackDecoderTest } } - @Test() + @Test public void test8_1_2_2_ConnectionSpecificHeaderFields() throws Exception { MetaDataBuilder mdb; @@ -349,7 +365,7 @@ public class HpackDecoderTest assertNotNull(mdb.build()); } - @Test() + @Test public void test8_1_2_3_RequestPseudoHeaderFields() throws Exception { { @@ -368,7 +384,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("No Path")); } @@ -378,7 +394,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("No Method")); } @@ -388,7 +404,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("No Scheme")); } @@ -398,7 +414,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_METHOD, "GET")); mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("No Path")); } @@ -410,7 +426,7 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_SCHEME, "http")); mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("Duplicate")); } @@ -423,12 +439,12 @@ public class HpackDecoderTest mdb.emit(new HttpField(HttpHeader.C_AUTHORITY, "localhost:8080")); mdb.emit(new HttpField(HttpHeader.C_PATH, "/")); - StreamException ex = assertThrows(StreamException.class, () -> mdb.build()); + StreamException ex = assertThrows(StreamException.class, mdb::build); assertThat(ex.getMessage(), Matchers.containsString("Duplicate")); } } - @Test() + @Test public void testHuffmanEncodedStandard() throws Exception { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -446,8 +462,8 @@ public class HpackDecoderTest } /* 5.2.1: Sends a Huffman-encoded string literal representation with padding longer than 7 bits */ - @Test() - public void testHuffmanEncodedExtraPadding() throws Exception + @Test + public void testHuffmanEncodedExtraPadding() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -458,8 +474,8 @@ public class HpackDecoderTest } /* 5.2.2: Sends a Huffman-encoded string literal representation padded by zero */ - @Test() - public void testHuffmanEncodedZeroPadding() throws Exception + @Test + public void testHuffmanEncodedZeroPadding() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -471,8 +487,8 @@ public class HpackDecoderTest } /* 5.2.3: Sends a Huffman-encoded string literal representation containing the EOS symbol */ - @Test() - public void testHuffmanEncodedWithEOS() throws Exception + @Test + public void testHuffmanEncodedWithEOS() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -483,8 +499,8 @@ public class HpackDecoderTest assertThat(ex.getMessage(), Matchers.containsString("EOS in content")); } - @Test() - public void testHuffmanEncodedOneIncompleteOctet() throws Exception + @Test + public void testHuffmanEncodedOneIncompleteOctet() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -495,8 +511,8 @@ public class HpackDecoderTest assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); } - @Test() - public void testHuffmanEncodedTwoIncompleteOctet() throws Exception + @Test + public void testHuffmanEncodedTwoIncompleteOctet() { HpackDecoder decoder = new HpackDecoder(4096, 8192); @@ -506,4 +522,49 @@ public class HpackDecoderTest CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); } + + @Test + public void testZeroLengthName() + { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "00000130"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); + SessionException ex = assertThrows(SessionException.class, () -> decoder.decode(buffer)); + assertThat(ex.getMessage(), Matchers.containsString("Header size 0")); + } + + @Test + public void testZeroLengthValue() throws Exception + { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "00016800"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); + MetaData metaData = decoder.decode(buffer); + assertThat(metaData.getFields().size(), is(1)); + assertThat(metaData.getFields().get("h"), is("")); + } + + @Test + public void testUpperCaseName() + { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "0001480130"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); + StreamException ex = assertThrows(StreamException.class, () -> decoder.decode(buffer)); + assertThat(ex.getMessage(), Matchers.containsString("Uppercase header")); + } + + @Test + public void testWhiteSpaceName() + { + HpackDecoder decoder = new HpackDecoder(4096, 8192); + + String encoded = "0001200130"; + ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); + StreamException ex = assertThrows(StreamException.class, () -> decoder.decode(buffer)); + assertThat(ex.getMessage(), Matchers.containsString("Illegal header")); + } } diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackEncoderTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackEncoderTest.java index d9a59639950..2fada9a66c5 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackEncoderTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackEncoderTest.java @@ -56,7 +56,7 @@ public class HpackEncoderTest new HttpField("fo8", "b8r"), new HttpField("fo9", "b9r"), new HttpField("foA", "bAr"), - }; + }; // Add 4 entries for (int i = 0; i <= 3; i++) 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 7b9218d3585..245f925cd2f 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 @@ -67,7 +67,7 @@ public class HpackTest BufferUtil.flipToFlush(buffer, 0); Response decoded0 = (Response)decoder.decode(buffer); original0.getFields().put(new HttpField(HttpHeader.CONTENT_ENCODING, "")); - assertMetadataSame(original0, decoded0); + assertMetaDataResponseSame(original0, decoded0); // Same again? BufferUtil.clearToFill(buffer); @@ -75,7 +75,7 @@ public class HpackTest BufferUtil.flipToFlush(buffer, 0); Response decoded0b = (Response)decoder.decode(buffer); - assertMetadataSame(original0, decoded0b); + assertMetaDataResponseSame(original0, decoded0b); HttpFields fields1 = new HttpFields(); fields1.add(HttpHeader.CONTENT_TYPE, "text/plain"); @@ -93,7 +93,7 @@ public class HpackTest BufferUtil.flipToFlush(buffer, 0); Response decoded1 = (Response)decoder.decode(buffer); - assertMetadataSame(original1, decoded1); + assertMetaDataResponseSame(original1, decoded1); assertEquals("custom-key", decoded1.getFields().getField("Custom-Key").getName()); } @@ -106,19 +106,19 @@ public class HpackTest HttpFields fields0 = new HttpFields(); fields0.add("1234567890", "1234567890123456789012345678901234567890"); - fields0.add("Cookie", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"); + fields0.add("Cookie", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"); MetaData original0 = new MetaData(HttpVersion.HTTP_2, fields0); BufferUtil.clearToFill(buffer); encoder.encode(buffer, original0); BufferUtil.flipToFlush(buffer, 0); - MetaData decoded0 = (MetaData)decoder.decode(buffer); + MetaData decoded0 = decoder.decode(buffer); - assertMetadataSame(original0, decoded0); + assertMetaDataSame(original0, decoded0); HttpFields fields1 = new HttpFields(); fields1.add("1234567890", "1234567890123456789012345678901234567890"); - fields1.add("Cookie", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"); + fields1.add("Cookie", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"); fields1.add("x", "y"); MetaData original1 = new MetaData(HttpVersion.HTTP_2, fields1); @@ -136,6 +136,26 @@ public class HpackTest } } + @Test + public void encodeDecodeNonAscii() throws Exception + { + HpackEncoder encoder = new HpackEncoder(); + HpackDecoder decoder = new HpackDecoder(4096, 8192); + ByteBuffer buffer = BufferUtil.allocate(16 * 1024); + + HttpFields fields0 = new HttpFields(); + fields0.add("Cookie", "[\uD842\uDF9F]"); + fields0.add("custom-key", "[\uD842\uDF9F]"); + Response original0 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields0); + + BufferUtil.clearToFill(buffer); + encoder.encode(buffer, original0); + BufferUtil.flipToFlush(buffer, 0); + Response decoded0 = (Response)decoder.decode(buffer); + + assertMetaDataSame(original0, decoded0); + } + @Test public void evictReferencedFieldTest() throws Exception { @@ -143,57 +163,111 @@ public class HpackTest HpackDecoder decoder = new HpackDecoder(200, 1024); ByteBuffer buffer = BufferUtil.allocateDirect(16 * 1024); + String longEnoughToBeEvicted = "012345678901234567890123456789012345678901234567890"; + HttpFields fields0 = new HttpFields(); - fields0.add("123456789012345678901234567890123456788901234567890", "value"); - fields0.add("foo", "abcdeffhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"); + fields0.add(longEnoughToBeEvicted, "value"); + fields0.add("foo", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); MetaData original0 = new MetaData(HttpVersion.HTTP_2, fields0); BufferUtil.clearToFill(buffer); encoder.encode(buffer, original0); BufferUtil.flipToFlush(buffer, 0); - MetaData decoded0 = (MetaData)decoder.decode(buffer); + MetaData decoded0 = decoder.decode(buffer); assertEquals(2, encoder.getHpackContext().size()); assertEquals(2, decoder.getHpackContext().size()); - assertEquals("123456789012345678901234567890123456788901234567890", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 1).getHttpField().getName()); - assertEquals("foo", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 0).getHttpField().getName()); + assertEquals(longEnoughToBeEvicted, encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 1).getHttpField().getName()); + assertEquals("foo", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length).getHttpField().getName()); - assertMetadataSame(original0, decoded0); + assertMetaDataSame(original0, decoded0); HttpFields fields1 = new HttpFields(); - fields1.add("123456789012345678901234567890123456788901234567890", "other_value"); + fields1.add(longEnoughToBeEvicted, "other_value"); fields1.add("x", "y"); MetaData original1 = new MetaData(HttpVersion.HTTP_2, fields1); BufferUtil.clearToFill(buffer); encoder.encode(buffer, original1); BufferUtil.flipToFlush(buffer, 0); - MetaData decoded1 = (MetaData)decoder.decode(buffer); - assertMetadataSame(original1, decoded1); + MetaData decoded1 = decoder.decode(buffer); + assertMetaDataSame(original1, decoded1); assertEquals(2, encoder.getHpackContext().size()); assertEquals(2, decoder.getHpackContext().size()); - assertEquals("x", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 0).getHttpField().getName()); + assertEquals("x", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length).getHttpField().getName()); assertEquals("foo", encoder.getHpackContext().get(HpackContext.STATIC_TABLE.length + 1).getHttpField().getName()); } - private void assertMetadataSame(MetaData.Response expected, MetaData.Response actual) + @Test + public void testHopHeadersAreRemoved() throws Exception + { + HpackEncoder encoder = new HpackEncoder(); + HpackDecoder decoder = new HpackDecoder(4096, 16384); + + HttpFields input = new HttpFields(); + input.put(HttpHeader.ACCEPT, "*"); + input.put(HttpHeader.CONNECTION, "TE, Upgrade, Custom"); + input.put("Custom", "Pizza"); + input.put(HttpHeader.KEEP_ALIVE, "true"); + input.put(HttpHeader.PROXY_CONNECTION, "foo"); + input.put(HttpHeader.TE, "1234567890abcdef"); + input.put(HttpHeader.TRANSFER_ENCODING, "chunked"); + input.put(HttpHeader.UPGRADE, "gold"); + + ByteBuffer buffer = BufferUtil.allocate(2048); + BufferUtil.clearToFill(buffer); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, input)); + BufferUtil.flipToFlush(buffer, 0); + MetaData metaData = decoder.decode(buffer); + HttpFields output = metaData.getFields(); + + assertEquals(1, output.size()); + assertEquals("*", output.get(HttpHeader.ACCEPT)); + } + + @Test + public void testTETrailers() throws Exception + { + HpackEncoder encoder = new HpackEncoder(); + HpackDecoder decoder = new HpackDecoder(4096, 16384); + + HttpFields input = new HttpFields(); + input.put(HttpHeader.CONNECTION, "TE"); + String teValue = "trailers"; + input.put(HttpHeader.TE, teValue); + String trailerValue = "Custom"; + input.put(HttpHeader.TRAILER, trailerValue); + + ByteBuffer buffer = BufferUtil.allocate(2048); + BufferUtil.clearToFill(buffer); + encoder.encode(buffer, new MetaData(HttpVersion.HTTP_2, input)); + BufferUtil.flipToFlush(buffer, 0); + MetaData metaData = decoder.decode(buffer); + HttpFields output = metaData.getFields(); + + assertEquals(2, output.size()); + assertEquals(teValue, output.get(HttpHeader.TE)); + assertEquals(trailerValue, output.get(HttpHeader.TRAILER)); + } + + private void assertMetaDataResponseSame(MetaData.Response expected, MetaData.Response actual) { assertThat("Response.status", actual.getStatus(), is(expected.getStatus())); assertThat("Response.reason", actual.getReason(), is(expected.getReason())); - assertMetadataSame((MetaData)expected, (MetaData)actual); + assertMetaDataSame(expected, actual); } - private void assertMetadataSame(MetaData expected, MetaData actual) + private void assertMetaDataSame(MetaData expected, MetaData actual) { assertThat("Metadata.contentLength", actual.getContentLength(), is(expected.getContentLength())); assertThat("Metadata.version" + ".version", actual.getHttpVersion(), is(expected.getHttpVersion())); - assertHttpFieldsSame("Metadata.fields", expected.getFields(), actual.getFields()); + assertHttpFieldsSame(expected.getFields(), actual.getFields()); } - private void assertHttpFieldsSame(String msg, HttpFields expected, HttpFields actual) + private void assertHttpFieldsSame(HttpFields expected, HttpFields actual) { - assertThat(msg + ".size", actual.size(), is(expected.size())); + assertThat("metaData.fields.size", actual.size(), is(expected.size())); for (HttpField actualField : actual) { @@ -203,7 +277,7 @@ public class HpackTest // during testing. continue; } - assertThat(msg + ".contains(" + actualField + ")", expected.contains(actualField), is(true)); + assertThat("metaData.fields.contains(" + actualField + ")", expected.contains(actualField), is(true)); } } } diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java index 802427f1866..5a947b8fd78 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java @@ -25,11 +25,13 @@ import java.util.stream.Stream; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; +import org.hamcrest.Matchers; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -77,8 +79,7 @@ public class HuffmanTest { String s = "bad '" + bad + "'"; - assertThrows(IllegalArgumentException.class, - () -> Huffman.octetsNeeded(s)); + assertThat(Huffman.octetsNeeded(s), Matchers.is(-1)); assertThrows(BufferOverflowException.class, () -> Huffman.encode(BufferUtil.allocate(32), s)); 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 a067f510fcd..e115d254d91 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 @@ -62,6 +62,7 @@ import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.http2.frames.SettingsFrame; import org.eclipse.jetty.http2.generator.Generator; +import org.eclipse.jetty.http2.parser.RateControl; import org.eclipse.jetty.http2.parser.ServerParser; import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory; import org.eclipse.jetty.io.ByteBufferPool; @@ -195,7 +196,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest .onRequestBegin(request -> { if (request.getVersion() != HttpVersion.HTTP_2) - request.abort(new Exception("Not a HTTP/2 request")); + request.abort(new Exception("Not an HTTP/2 request")); }) .send(); @@ -495,7 +496,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest x.printStackTrace(); } } - }, 4096, 8192); + }, 4096, 8192, RateControl.NO_RATE_CONTROL); parser.init(UnaryOperator.identity()); byte[] bytes = new byte[1024]; 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 16984b9bbdf..ad51713fd9c 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 @@ -2,7 +2,7 @@ - + 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 08e5d62911f..2f5398b4d97 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 @@ -2,7 +2,7 @@ - + diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java index f5e7bcd209a..417ca7aefed 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/AbstractHTTP2ServerConnectionFactory.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.http2.server; import java.io.IOException; +import java.time.Duration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -34,7 +35,9 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener; import org.eclipse.jetty.http2.frames.Frame; import org.eclipse.jetty.http2.frames.SettingsFrame; import org.eclipse.jetty.http2.generator.Generator; +import org.eclipse.jetty.http2.parser.RateControl; import org.eclipse.jetty.http2.parser.ServerParser; +import org.eclipse.jetty.http2.parser.WindowRateControl; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.AbstractConnectionFactory; @@ -58,6 +61,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne private int maxHeaderBlockFragment = 0; private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH; private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS; + private RateControl rateControl = new WindowRateControl(20, Duration.ofSeconds(1)); private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F); private long streamIdleTimeout; private boolean _useInputDirectBuffers; @@ -182,6 +186,16 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne this.maxSettingsKeys = maxSettingsKeys; } + public RateControl getRateControl() + { + return rateControl; + } + + public void setRateControl(RateControl rateControl) + { + this.rateControl = rateControl; + } + public boolean isUseInputDirectByteBuffers() { return _useInputDirectBuffers; @@ -239,7 +253,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne session.setInitialSessionRecvWindow(getInitialSessionRecvWindow()); session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize()); - ServerParser parser = newServerParser(connector, session); + ServerParser parser = newServerParser(connector, session, getRateControl()); parser.setMaxFrameLength(getMaxFrameLength()); parser.setMaxSettingsKeys(getMaxSettingsKeys()); @@ -253,9 +267,9 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne protected abstract ServerSessionListener newSessionListener(Connector connector, EndPoint endPoint); - protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener) + protected ServerParser newServerParser(Connector connector, ServerParser.Listener listener, RateControl rateControl) { - return new ServerParser(connector.getByteBufferPool(), listener, getMaxDynamicTableSize(), getHttpConfiguration().getRequestHeaderSize()); + return new ServerParser(connector.getByteBufferPool(), listener, getMaxDynamicTableSize(), getHttpConfiguration().getRequestHeaderSize(), rateControl); } @ManagedObject("The container of HTTP/2 sessions") diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java index cc0b9925311..4ed23cf2fea 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java @@ -39,8 +39,8 @@ import org.eclipse.jetty.util.log.Logger; * * If used in combination with a {@link HttpConnectionFactory} as the * default protocol, this factory can support the non-standard direct - * update mechanism, where a HTTP1 request of the form "PRI * HTTP/2.0" - * is used to trigger a switch to a HTTP2 connection. This approach + * update mechanism, where an HTTP1 request of the form "PRI * HTTP/2.0" + * is used to trigger a switch to an HTTP2 connection. This approach * allows a single port to accept either HTTP/1 or HTTP/2 direct * connections. */ 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 aa027a6151b..5dee8dab902 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 @@ -66,7 +66,7 @@ import org.eclipse.jetty.util.TypeUtil; public class HTTP2ServerConnection extends HTTP2Connection implements Connection.UpgradeTo { /** - * @param protocol A HTTP2 protocol variant + * @param protocol An HTTP2 protocol variant * @return True if the protocol version is supported */ public static boolean isSupportedProtocol(String protocol) @@ -377,10 +377,9 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection } @Override - protected void checkAndPrepareUpgrade() + protected boolean checkAndPrepareUpgrade() { - if (isTunnel()) - getHttpTransport().prepareUpgrade(); + return isTunnel() && getHttpTransport().prepareUpgrade(); } @Override diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index 68b19d878eb..3a99edb6a5e 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -314,7 +314,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport return transportCallback.onIdleTimeout(failure); } - void prepareUpgrade() + boolean prepareUpgrade() { HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); Request request = channel.getRequest(); @@ -323,7 +323,8 @@ public class HttpTransportOverHTTP2 implements HttpTransport endPoint.upgrade(connection); stream.setAttachment(endPoint); if (request.getHttpInput().hasContent()) - channel.sendErrorOrAbort("Unexpected content in CONNECT request"); + return channel.sendErrorOrAbort("Unexpected content in CONNECT request"); + return false; } @Override diff --git a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java index 134639d9429..ff8a8c482af 100644 --- a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java +++ b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java @@ -188,7 +188,7 @@ public class HTTP2CServerTest extends AbstractServerTest assertThat(content, containsString("Hello from Jetty using HTTP/1.1")); assertThat(content, containsString("uri=/one")); - // Send a HTTP/2 request. + // Send an HTTP/2 request. headersRef.set(null); dataRef.set(null); latchRef.set(new CountDownLatch(2)); @@ -319,7 +319,7 @@ public class HTTP2CServerTest extends AbstractServerTest connector.setDefaultProtocol(connectionFactory.getProtocol()); connector.start(); - // Now send a HTTP/2 direct request, which + // Now send an HTTP/2 direct request, which // will have the PRI * HTTP/2.0 preface. byteBufferPool = new MappedByteBufferPool(); @@ -336,7 +336,7 @@ public class HTTP2CServerTest extends AbstractServerTest output.write(BufferUtil.toArray(buffer)); } - // We sent a HTTP/2 preface, but the server has no "h2c" connection + // We sent an HTTP/2 preface, but the server has no "h2c" connection // factory so it does not know how to handle this request. InputStream input = client.getInputStream(); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java index 14948563718..cdfd2fac200 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java @@ -36,7 +36,7 @@ import java.util.concurrent.locks.Condition; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Locker; +import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.Scheduler; /** @@ -68,24 +68,13 @@ public class ByteArrayEndPoint extends AbstractEndPoint private static final ByteBuffer EOF = BufferUtil.allocate(0); - private final Runnable _runFillable = new Runnable() - { - @Override - public void run() - { - getFillInterest().fillable(); - } - }; - - private final Locker _locker = new Locker(); - private final Condition _hasOutput = _locker.newCondition(); + private final Runnable _runFillable = () -> getFillInterest().fillable(); + private final AutoLock _lock = new AutoLock(); + private final Condition _hasOutput = _lock.newCondition(); private final Queue
- *_inQ = new ArrayDeque<>(); private ByteBuffer _out; private boolean _growOutput; - /** - * - */ public ByteArrayEndPoint() { this(null, 0, null, null); @@ -138,7 +127,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint public void doShutdownOutput() { super.doShutdownOutput(); - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { _hasOutput.signalAll(); } @@ -148,7 +137,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint public void doClose() { super.doClose(); - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { _hasOutput.signalAll(); } @@ -180,7 +169,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint @Override protected void needsFillInterest() throws IOException { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { if (!isOpen()) throw new ClosedChannelException(); @@ -205,7 +194,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint public void addInput(ByteBuffer in) { boolean fillable = false; - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { if (isEOF(_inQ.peek())) throw new RuntimeIOException(new EOFException()); @@ -238,7 +227,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint public void addInputAndExecute(ByteBuffer in) { boolean fillable = false; - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { if (isEOF(_inQ.peek())) throw new RuntimeIOException(new EOFException()); @@ -263,7 +252,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint */ public ByteBuffer getOutput() { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { return _out; } @@ -293,7 +282,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint { ByteBuffer b; - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { b = _out; _out = BufferUtil.allocate(b.capacity()); @@ -314,7 +303,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint { ByteBuffer b; - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { while (BufferUtil.isEmpty(_out) && !isOutputShutdown()) { @@ -351,7 +340,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint */ public void setOutput(ByteBuffer out) { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { _out = out; } @@ -359,7 +348,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint } /** - * @return true
if there are bytes remaining to be read from the encoded input + * @return {@code true} if there are bytes remaining to be read from the encoded input */ public boolean hasMore() { @@ -373,7 +362,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint public int fill(ByteBuffer buffer) throws IOException { int filled = 0; - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { while (true) { @@ -418,7 +407,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint public boolean flush(ByteBuffer... buffers) throws IOException { boolean flushed = true; - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { if (!isOpen()) throw new IOException("CLOSED"); @@ -467,7 +456,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint @Override public void reset() { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { _inQ.clear(); _hasOutput.signalAll(); @@ -507,7 +496,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint int q; ByteBuffer b; String o; - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { q = _inQ.size(); b = _inQ.peek(); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java new file mode 100644 index 00000000000..fc68e4714b9 --- /dev/null +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java @@ -0,0 +1,62 @@ +// +// ======================================================================== +// 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.io; + +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.eclipse.jetty.util.BufferUtil; + +/** + * Simple wrapper of a ByteBuffer as an OutputStream. + * The buffer does not grow and this class will throw an + * {@link java.nio.BufferOverflowException} if the buffer capacity is exceeded. + */ +public class ByteBufferOutputStream extends OutputStream +{ + final ByteBuffer _buffer; + + public ByteBufferOutputStream(ByteBuffer buffer) + { + _buffer = buffer; + } + + public void close() + { + } + + public void flush() + { + } + + public void write(byte[] b) + { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) + { + BufferUtil.append(_buffer, b, off, len); + } + + public void write(int b) + { + BufferUtil.append(_buffer, (byte)b); + } +} 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 da76d773ed9..36057b2c9cb 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 @@ -43,6 +43,7 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.DumpableCollection; @@ -122,12 +123,20 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable start._started.await(); } + protected void onSelectFailed(Throwable cause) + { + // override to change behavior + } + public int size() { Selector s = _selector; if (s == null) return 0; - return s.keys().size(); + Setkeys = s.keys(); + if (keys == null) + return 0; + return keys.size(); } @Override @@ -135,7 +144,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { // doStop might be called for a failed managedSelector, // We do not want to wait twice, so we only stop once for each start - if (_started.compareAndSet(true, false)) + if (_started.compareAndSet(true, false) && _selector != null) { // Close connections, but only wait a single selector cycle for it to take effect CloseConnections closeConnections = new CloseConnections(); @@ -210,7 +219,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable catch (RejectedExecutionException x) { if (task instanceof Closeable) - closeNoExceptions((Closeable)task); + IO.close((Closeable)task); } } @@ -246,17 +255,14 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } } - private static void closeNoExceptions(Closeable closeable) + protected void endPointOpened(EndPoint endPoint) { - try - { - if (closeable != null) - closeable.close(); - } - catch (Throwable x) - { - LOG.ignore(x); - } + _selectorManager.endPointOpened(endPoint); + } + + protected void endPointClosed(EndPoint endPoint) + { + _selectorManager.endPointClosed(endPoint); } private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException @@ -266,7 +272,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable endPoint.setConnection(connection); selectionKey.attach(endPoint); endPoint.onOpen(); - _selectorManager.endPointOpened(endPoint); + endPointOpened(endPoint); _selectorManager.connectionOpened(connection); if (LOG.isDebugEnabled()) LOG.debug("Created {}", endPoint); @@ -496,15 +502,19 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { + IO.close(_selector); _selector = null; + if (isRunning()) - LOG.warn(x); + { + LOG.warn("Fatal select() failure", x); + onSelectFailed(x); + } else { LOG.warn(x.toString()); LOG.debug(x); } - closeNoExceptions(_selector); } return false; } @@ -541,13 +551,13 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { LOG.debug("Ignoring cancelled key for channel {}", key.channel()); if (attachment instanceof EndPoint) - closeNoExceptions((EndPoint)attachment); + IO.close((EndPoint)attachment); } catch (Throwable x) { LOG.warn("Could not process key for channel " + key.channel(), x); if (attachment instanceof EndPoint) - closeNoExceptions((EndPoint)attachment); + IO.close((EndPoint)attachment); } } else @@ -556,7 +566,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable LOG.debug("Selector loop ignoring invalid key for channel {}", key.channel()); Object attachment = key.attachment(); if (attachment instanceof EndPoint) - closeNoExceptions((EndPoint)attachment); + IO.close((EndPoint)attachment); } } return null; @@ -661,7 +671,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { - closeNoExceptions(_channel); + IO.close(_channel); LOG.warn(x); } } @@ -683,7 +693,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { - closeNoExceptions(channel); + IO.close(channel); LOG.warn("Accept failed for channel " + channel, x); } @@ -722,7 +732,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable public void close() { LOG.debug("closed accept of {}", channel); - closeNoExceptions(channel); + IO.close(channel); } @Override @@ -735,7 +745,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { - closeNoExceptions(channel); + IO.close(channel); _selectorManager.onAcceptFailed(channel, x); LOG.debug(x); } @@ -758,7 +768,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable protected void failed(Throwable failure) { - closeNoExceptions(channel); + IO.close(channel); LOG.warn(String.valueOf(failure)); LOG.debug(failure); _selectorManager.onAcceptFailed(channel, failure); @@ -808,7 +818,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable if (failed.compareAndSet(false, true)) { timeout.cancel(); - closeNoExceptions(channel); + IO.close(channel); ManagedSelector.this._selectorManager.connectionFailed(channel, failure, attachment); } } @@ -864,12 +874,12 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { if (_closed == null) { - closeNoExceptions(closeable); + IO.close(closeable); } else if (!_closed.contains(closeable)) { _closed.add(closeable); - closeNoExceptions(closeable); + IO.close(closeable); } } } @@ -894,12 +904,12 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { Object attachment = key.attachment(); if (attachment instanceof EndPoint) - closeNoExceptions((EndPoint)attachment); + IO.close((EndPoint)attachment); } } _selector = null; - closeNoExceptions(selector); + IO.close(selector); _stopped.countDown(); } } @@ -924,7 +934,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable failure) { - closeNoExceptions(_connect.channel); + IO.close(_connect.channel); LOG.warn(String.valueOf(failure)); LOG.debug(failure); _connect.failed(failure); @@ -957,7 +967,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable Connection connection = endPoint.getConnection(); if (connection != null) _selectorManager.connectionClosed(connection, cause); - _selectorManager.endPointClosed(endPoint); + ManagedSelector.this.endPointClosed(endPoint); } @Override diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java index 22c5924c9ac..470acc8f59c 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslHandshakeListener.java @@ -35,6 +35,7 @@ public interface SslHandshakeListener extends EventListener * Callback method invoked when the TLS handshake succeeds.
* * @param event the event object carrying information about the TLS handshake event + * @throws SSLException if any error happen during handshake */ default void handshakeSucceeded(Event event) throws SSLException { diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/IOTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/IOTest.java index a9b198eaced..0463922154f 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/IOTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/IOTest.java @@ -43,6 +43,7 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.IO; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.OS; @@ -93,14 +94,8 @@ public class IOTest assertEquals(-1, server.getInputStream().read()); // but cannot write - try - { - client.getOutputStream().write(1); - fail("exception expected"); - } - catch (SocketException expected) - { - } + Assertions.assertThrows(SocketException.class, () -> client.getOutputStream().write(1)); + // but can still write in opposite direction. server.getOutputStream().write(1); @@ -110,14 +105,7 @@ public class IOTest server.shutdownInput(); // now we EOF instead of reading -1 - try - { - server.getInputStream().read(); - fail("exception expected"); - } - catch (SocketException expected) - { - } + Assertions.assertThrows(SocketException.class, () -> server.getInputStream().read()); // but can still write in opposite direction. server.getOutputStream().write(1); @@ -127,14 +115,7 @@ public class IOTest client.shutdownInput(); // now we EOF instead of reading -1 - try - { - client.getInputStream().read(); - fail("exception expected"); - } - catch (SocketException expected) - { - } + Assertions.assertThrows(SocketException.class, () -> client.getInputStream().read()); // But we can still write at the server (data which will never be read) server.getOutputStream().write(1); @@ -146,14 +127,7 @@ public class IOTest server.shutdownOutput(); // and now we can't write - try - { - server.getOutputStream().write(1); - fail("exception expected"); - } - catch (SocketException expected) - { - } + Assertions.assertThrows(SocketException.class, () -> server.getOutputStream().write(1)); // but the sockets are still open assertFalse(client.isClosed()); 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 92395418ceb..8805895a1cd 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 @@ -460,6 +460,7 @@ public class SslConnectionTest } catch (SocketTimeoutException e) { + // no op } assertTrue(__onIncompleteFlush.get()); @@ -506,6 +507,7 @@ public class SslConnectionTest } catch (SocketTimeoutException e) { + // no op } __blockFor.set(0); diff --git a/jetty-jaas/src/test/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModuleTest.java b/jetty-jaas/src/test/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModuleTest.java index 7f9cd1cafc5..e62320ea54a 100644 --- a/jetty-jaas/src/test/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModuleTest.java +++ b/jetty-jaas/src/test/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModuleTest.java @@ -18,12 +18,6 @@ package org.eclipse.jetty.jaas.spi; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.io.File; import java.util.HashMap; @@ -33,6 +27,12 @@ import org.eclipse.jetty.jaas.callback.DefaultCallbackHandler; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class PropertyFileLoginModuleTest { @Test 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 0b18009b0c0..af44682b588 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 @@ -472,9 +472,9 @@ public class TestJNDI ((Context)zzz.lookup("java:comp")).bind("crud2", "xxx2"); fail("Should not be able to write to locked context"); } - catch (NamingException ne) + catch (NamingException e) { - assertThat(ne.getMessage(), Matchers.containsString("immutable")); + assertThat(e.getMessage(), Matchers.containsString("immutable")); } finally { @@ -492,9 +492,9 @@ public class TestJNDI ((Context)zzz.lookup("java:comp")).bind("foo", "bar"); fail("Should not be able to write to locked context"); } - catch (NamingException ne) + catch (NamingException e) { - assertThat(ne.getMessage(), Matchers.containsString("immutable")); + assertThat(e.getMessage(), Matchers.containsString("immutable")); } finally { diff --git a/jetty-maven-plugin/.gitignore b/jetty-maven-plugin/.gitignore deleted file mode 100644 index 929d903c7d3..00000000000 --- a/jetty-maven-plugin/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.classpath -.project -.settings -target -*.swp diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyEffectiveWebXml.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyEffectiveWebXml.java index 9749cb16c04..ca2f83df9aa 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyEffectiveWebXml.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyEffectiveWebXml.java @@ -28,8 +28,9 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.quickstart.QuickStartConfiguration; +import org.eclipse.jetty.quickstart.QuickStartConfiguration.Mode; import org.eclipse.jetty.util.IO; -import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -92,30 +93,23 @@ public class JettyEffectiveWebXml extends JettyRunMojo configureWebApplication(); //set the webapp up to do very little other than generate the quickstart-web.xml + if (effectiveWebXml == null) + { + deleteOnExit = true; + effectiveWebXml = new File(target, "effective-web.xml"); + effectiveWebXml.deleteOnExit(); + } + Resource descriptor = Resource.newResource(effectiveWebXml); + if (!effectiveWebXml.getParentFile().exists()) + effectiveWebXml.getParentFile().mkdirs(); + if (!effectiveWebXml.exists()) + effectiveWebXml.createNewFile(); + webApp.setCopyWebDir(false); webApp.setCopyWebInf(false); - webApp.setGenerateQuickStart(true); - - //if the user didn't nominate a file to generate into, pick the name and - //make sure that it is deleted on exit - if (webApp.getQuickStartWebDescriptor() == null) - { - if (effectiveWebXml == null) - { - deleteOnExit = true; - effectiveWebXml = new File(target, "effective-web.xml"); - effectiveWebXml.deleteOnExit(); - } - - Resource descriptor = Resource.newResource(effectiveWebXml); - - if (!effectiveWebXml.getParentFile().exists()) - effectiveWebXml.getParentFile().mkdirs(); - if (!effectiveWebXml.exists()) - effectiveWebXml.createNewFile(); - - webApp.setQuickStartWebDescriptor(descriptor); - } + webApp.addConfiguration(new QuickStartConfiguration()); + webApp.setAttribute(QuickStartConfiguration.MODE, Mode.GENERATE); + webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, descriptor); ServerSupport.addWebApplication(server, webApp); @@ -158,7 +152,7 @@ public class JettyEffectiveWebXml extends JettyRunMojo try { //just show the result in the log - getLog().info(IO.toString(webApp.getQuickStartWebDescriptor().getInputStream())); + getLog().info(IO.toString(((Resource)webApp.getAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML)).getInputStream())); } catch (Exception e) { diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java index cd255b6962c..feada7f24e0 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java @@ -43,6 +43,8 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.quickstart.QuickStartConfiguration; +import org.eclipse.jetty.quickstart.QuickStartConfiguration.Mode; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.resource.Resource; @@ -209,22 +211,19 @@ public class JettyRunForkedMojo extends JettyRunMojo configureWebApplication(); //set the webapp up to do very little other than generate the quickstart-web.xml + if (forkWebXml == null) + forkWebXml = new File(target, "fork-web.xml"); + + if (!forkWebXml.getParentFile().exists()) + forkWebXml.getParentFile().mkdirs(); + if (!forkWebXml.exists()) + forkWebXml.createNewFile(); + + webApp.addConfiguration(new MavenQuickStartConfiguration()); + webApp.setAttribute(QuickStartConfiguration.MODE, Mode.GENERATE); + webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, Resource.newResource(forkWebXml)); webApp.setCopyWebDir(false); - webApp.setCopyWebInf(false); - webApp.setGenerateQuickStart(true); - - if (webApp.getQuickStartWebDescriptor() == null) - { - if (forkWebXml == null) - forkWebXml = new File(target, "fork-web.xml"); - - if (!forkWebXml.getParentFile().exists()) - forkWebXml.getParentFile().mkdirs(); - if (!forkWebXml.exists()) - forkWebXml.createNewFile(); - - webApp.setQuickStartWebDescriptor(Resource.newResource(forkWebXml)); - } + webApp.setCopyWebInf(false); //add webapp to our fake server instance ServerSupport.addWebApplication(server, webApp); @@ -240,11 +239,10 @@ public class JettyRunForkedMojo extends JettyRunMojo //leave everything unpacked for the forked process to use webApp.setPersistTempDirectory(true); + File props = null; webApp.start(); //just enough to generate the quickstart - //save config of the webapp BEFORE we stop - final File props = prepareConfiguration(); - + props = prepareConfiguration(); webApp.stop(); if (tpool != null) @@ -311,7 +309,7 @@ public class JettyRunForkedMojo extends JettyRunMojo if (PluginLog.getLog().isDebugEnabled()) PluginLog.getLog().debug("Forked cli:" + Arrays.toString(cmd.toArray())); - + PluginLog.getLog().info("Forked process starting"); //set up extra environment vars if there are any 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 79c8509ad70..8e765504b1e 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 @@ -34,7 +34,6 @@ import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.plus.webapp.EnvConfiguration; import org.eclipse.jetty.plus.webapp.PlusConfiguration; import org.eclipse.jetty.quickstart.QuickStartConfiguration; -import org.eclipse.jetty.quickstart.QuickStartConfiguration.Mode; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterMapping; import org.eclipse.jetty.servlet.ServletHolder; @@ -105,8 +104,6 @@ public class JettyWebAppContext extends WebAppContext */ private boolean _baseAppFirst = true; - private boolean _isGenerateQuickStart; - public JettyWebAppContext() throws Exception { super(); @@ -117,6 +114,8 @@ public class JettyWebAppContext extends WebAppContext addConfiguration(new EnvConfiguration()); addConfiguration(new PlusConfiguration()); addConfiguration(new AnnotationConfiguration()); + + setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, "origin"); } public void setContainerIncludeJarPattern(String pattern) @@ -210,27 +209,6 @@ public class JettyWebAppContext extends WebAppContext return attr == null ? null : attr.toString(); } - /** - * Toggle whether or not the origin attribute will be generated into the - * xml. - * - * @param generateOrigin if true then the origin of each xml element is - * added, otherwise it is omitted. - */ - public void setGenerateOrigin(boolean generateOrigin) - { - setAttribute(QuickStartConfiguration.GENERATE_ORIGIN, generateOrigin); - } - - /** - * @return true if the origin attribute will be generated, false otherwise - */ - public boolean isGenerateOrigin() - { - Object attr = getAttribute(QuickStartConfiguration.GENERATE_ORIGIN); - return attr == null ? false : Boolean.valueOf(attr.toString()); - } - public ListgetOverlays() { return _overlays; @@ -246,35 +224,6 @@ public class JettyWebAppContext extends WebAppContext return _baseAppFirst; } - /** - * Set the file to use into which to generate the quickstart output. - * - * @param quickStartWebXml the full path to the file to use - */ - public void setQuickStartWebDescriptor(String quickStartWebXml) throws Exception - { - setQuickStartWebDescriptor(Resource.newResource(quickStartWebXml)); - } - - /** - * Set the Resource to use into which to generate the quickstart output. - */ - protected void setQuickStartWebDescriptor(Resource quickStartWebXml) - { - setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, quickStartWebXml.toString()); - } - - public Resource getQuickStartWebDescriptor() throws Exception - { - Object o = getAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML); - if (o == null) - return null; - else if (o instanceof Resource) - return (Resource)o; - else - return Resource.newResource((String)o); - } - /** * This method is provided as a convenience for jetty maven plugin * configuration @@ -307,41 +256,9 @@ public class JettyWebAppContext extends WebAppContext return _webInfClasses; } - /** - * If true, a quickstart for the webapp is generated. - * - * @param quickStart if true the quickstart is generated, false otherwise - */ - public void setGenerateQuickStart(boolean quickStart) - { - _isGenerateQuickStart = quickStart; - } - - public boolean isGenerateQuickStart() - { - return _isGenerateQuickStart; - } - @Override public void doStart() throws Exception { - - // choose if this will be a quickstart or normal start - if (!isGenerateQuickStart() && getQuickStartWebDescriptor() != null) - { - MavenQuickStartConfiguration quickStart = new MavenQuickStartConfiguration(); - quickStart.setMode(Mode.QUICKSTART); - quickStart.setQuickStartWebXml(getQuickStartWebDescriptor()); - addConfiguration(quickStart); - } - else if (isGenerateQuickStart()) - { - MavenQuickStartConfiguration quickStart = new MavenQuickStartConfiguration(); - quickStart.setMode(Mode.GENERATE); - quickStart.setQuickStartWebXml(getQuickStartWebDescriptor()); - addConfiguration(quickStart); - } - // Set up the pattern that tells us where the jars are that need // scanning diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenQuickStartConfiguration.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenQuickStartConfiguration.java index 597c313555e..63d643a7a24 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenQuickStartConfiguration.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenQuickStartConfiguration.java @@ -18,15 +18,12 @@ package org.eclipse.jetty.maven.plugin; -import java.io.File; - import org.eclipse.jetty.quickstart.QuickStartConfiguration; import org.eclipse.jetty.util.IO; 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.util.resource.ResourceCollection; -import org.eclipse.jetty.webapp.WebAppClassLoader; import org.eclipse.jetty.webapp.WebAppContext; /** @@ -36,56 +33,6 @@ public class MavenQuickStartConfiguration extends QuickStartConfiguration { private static final Logger LOG = Log.getLogger(QuickStartConfiguration.class); - private Resource _quickStartWebXml; //the descriptor to use for starting/generating quickstart - - public void setQuickStartWebXml(Resource quickStartWebXml) - { - _quickStartWebXml = quickStartWebXml; - } - - @Override - public Resource getQuickStartWebXml(WebAppContext context) throws Exception - { - if (_quickStartWebXml == null) - return super.getQuickStartWebXml(context); - - return _quickStartWebXml; - } - - @Override - public void preConfigure(WebAppContext context) throws Exception - { - //check that webapp is suitable for quick start - if (context.getBaseResource() == null) - throw new IllegalStateException("No location for webapp"); - - //look for quickstart-web.xml in WEB-INF of webapp - Resource quickStartWebXml = getQuickStartWebXml(context); - if (LOG.isDebugEnabled()) - LOG.debug("quickStartWebXml={}", quickStartWebXml); - super.preConfigure(context); - } - - @Override - public void configure(WebAppContext context) throws Exception - { - JettyWebAppContext jwac = (JettyWebAppContext)context; - - //put the classes dir and all dependencies into the classpath - if (jwac.getClassPathFiles() != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Setting up classpath ..."); - for (File classPathFile : jwac.getClassPathFiles()) - { - ((WebAppClassLoader)context.getClassLoader()).addClassPath(classPathFile.getCanonicalPath()); - } - } - - //Set up the quickstart environment for the context - super.configure(context); - } - @Override public void deconfigure(WebAppContext context) throws Exception { diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java index 7d76d8124a5..9c25a5c8328 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.eclipse.jetty.quickstart.QuickStartConfiguration; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ShutdownMonitor; @@ -71,14 +72,9 @@ public class Starter //configure webapp from properties file describing unassembled webapp configureWebApp(); - - //make it a quickstart if the quickstart-web.xml file exists - if (webApp.getTempDirectory() != null) - { - File qs = new File(webApp.getTempDirectory(), "quickstart-web.xml"); - if (qs.exists() && qs.isFile()) - webApp.setQuickStartWebDescriptor(Resource.newResource(qs)); - } + + webApp.addConfiguration(new QuickStartConfiguration()); + webApp.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.QUICKSTART); ServerSupport.addWebApplication(server, webApp); @@ -232,7 +228,9 @@ public class Starter public static final void main(String[] args) { if (args == null) + { System.exit(1); + } Starter starter = null; try 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 d5ca0c745e6..fed62949ce8 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 @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import org.eclipse.jetty.quickstart.QuickStartConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.resource.Resource; @@ -71,9 +72,10 @@ public class WebAppPropertyConverter props.put("web.xml", webApp.getDescriptor()); } - if (webApp.getQuickStartWebDescriptor() != null) + Object tmp = webApp.getAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML); + if (tmp != null) { - props.put("quickstart.web.xml", webApp.getQuickStartWebDescriptor().getFile().getAbsolutePath()); + props.put("quickstart.web.xml", tmp.toString()); } //sort out the context path @@ -183,11 +185,10 @@ public class WebAppPropertyConverter if (!StringUtil.isBlank(str)) webApp.setDescriptor(str); - //TODO the WebAppStarter class doesn't set up the QUICKSTART_CONFIGURATION_CLASSES, but the Starter class does!!! str = props.getProperty("quickstart.web.xml"); if (!StringUtil.isBlank(str)) { - webApp.setQuickStartWebDescriptor(Resource.newResource(new File(str))); + webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, Resource.newResource(str)); } // - the tmp directory 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 2a09f98bc73..1e3f34f8f80 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 @@ -73,8 +73,8 @@ public class TestGetContent url += pathToCheck; } String response = httpClient.GET(url).getContentAsString(); - assertTrue(response.contains(contentCheck), "it test " + System.getProperty("maven.it.name") - + ", response not contentCheck: " + contentCheck + ", response:" + response); + assertTrue(response.contains(contentCheck), "it test " + System.getProperty("maven.it.name") + + ", response not contentCheck: " + contentCheck + ", response:" + response); System.out.println("contentCheck"); } if (Boolean.getBoolean("helloTestServlet")) diff --git a/jetty-openid/pom.xml b/jetty-openid/pom.xml new file mode 100644 index 00000000000..48dcc6adaf1 --- /dev/null +++ b/jetty-openid/pom.xml @@ -0,0 +1,64 @@ + + diff --git a/jetty-openid/src/main/config/etc/jetty-openid.xml b/jetty-openid/src/main/config/etc/jetty-openid.xml new file mode 100644 index 00000000000..0cb6d538849 --- /dev/null +++ b/jetty-openid/src/main/config/etc/jetty-openid.xml @@ -0,0 +1,29 @@ + + ++ + +org.eclipse.jetty +jetty-project +10.0.0-SNAPSHOT +4.0.0 +jetty-openid +Jetty :: OpenID +Jetty OpenID Connect infrastructure +http://www.eclipse.org/jetty + ++ + +${project.groupId}.openid ++ + ++ ++ +org.codehaus.mojo +findbugs-maven-plugin ++ +org.eclipse.jetty.security.openid.* ++ ++ +org.eclipse.jetty +jetty-server +${project.version} ++ +org.eclipse.jetty +jetty-security +${project.version} ++ +org.eclipse.jetty +jetty-util-ajax +${project.version} ++ +org.eclipse.jetty +jetty-servlet +${project.version} +test ++ +org.eclipse.jetty.toolchain +jetty-test-helper +test ++ +org.eclipse.jetty +jetty-client +${project.version} +test ++ \ No newline at end of file diff --git a/jetty-openid/src/main/config/modules/openid.mod b/jetty-openid/src/main/config/modules/openid.mod new file mode 100644 index 00000000000..c9a4cc7476e --- /dev/null +++ b/jetty-openid/src/main/config/modules/openid.mod @@ -0,0 +1,34 @@ +DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html + +[description] +Adds OpenId Connect authentication. + +[depend] +security + +[lib] +lib/jetty-openid-${jetty.version}.jar +lib/jetty-util-ajax-${jetty.version}.jar + +[files] +basehome:modules/openid/openid-baseloginservice.xml|etc/openid-baseloginservice.xml + +[xml] +etc/openid-baseloginservice.xml +etc/jetty-openid.xml + +[ini-template] +## The OpenID Identity Provider +# jetty.openid.openIdProvider=https://accounts.google.com/ + +## The Client Identifier +# jetty.openid.clientId=test1234.apps.googleusercontent.com + +## The Client Secret +# jetty.openid.clientSecret=XT_Mafv_aUCGheuCaKY8P + +## Additional Scopes to Request +# jetty.openid.scopes=email,profile + +## Whether to Authenticate users not found by base LoginService +# jetty.openid.authenticateNewUsers=false \ No newline at end of file diff --git a/jetty-openid/src/main/config/modules/openid/openid-baseloginservice.xml b/jetty-openid/src/main/config/modules/openid/openid-baseloginservice.xml new file mode 100644 index 00000000000..d87b88f7016 --- /dev/null +++ b/jetty-openid/src/main/config/modules/openid/openid-baseloginservice.xml @@ -0,0 +1,10 @@ + + ++ ++ + + + ++ ++ ++ + ++ ++ ++ + + ++ ++ + + \ No newline at end of file diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java new file mode 100644 index 00000000000..41fce0be5ee --- /dev/null +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java @@ -0,0 +1,491 @@ +// +// ======================================================================== +// 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.openid; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.security.LoginService; +import org.eclipse.jetty.security.ServerAuthException; +import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.security.authentication.DeferredAuthentication; +import org.eclipse.jetty.security.authentication.LoginAuthenticator; +import org.eclipse.jetty.security.authentication.SessionAuthentication; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.Authentication.User; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.UrlEncoded; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.security.Constraint; + +/** + *Implements authentication using OpenId Connect on top of OAuth 2.0. + * + *
The OpenIdAuthenticator redirects unauthenticated requests to the OpenID Connect Provider. The End-User is + * eventually redirected back with an Authorization Code to the /j_security_check URI within the context. + * The Authorization Code is then used to authenticate the user through the {@link OpenIdCredentials} and {@link OpenIdLoginService}. + *
+ *+ * Once a user is authenticated the OpenID Claims can be retrieved through an attribute on the session with the key {@link #CLAIMS}. + * The full response containing the OAuth 2.0 Access Token can be obtained with the session attribute {@link #RESPONSE}. + *
+ *{@link SessionAuthentication} is then used to wrap Authentication results so that they are associated with the session.
+ */ +public class OpenIdAuthenticator extends LoginAuthenticator +{ + private static final Logger LOG = Log.getLogger(OpenIdAuthenticator.class); + + public static final String CLAIMS = "org.eclipse.jetty.security.openid.claims"; + public static final String RESPONSE = "org.eclipse.jetty.security.openid.response"; + public static final String ERROR_PAGE = "org.eclipse.jetty.security.openid.error_page"; + public static final String J_URI = "org.eclipse.jetty.security.openid.URI"; + public static final String J_POST = "org.eclipse.jetty.security.openid.POST"; + public static final String J_METHOD = "org.eclipse.jetty.security.openid.METHOD"; + public static final String CSRF_TOKEN = "org.eclipse.jetty.security.openid.csrf_token"; + public static final String J_SECURITY_CHECK = "/j_security_check"; + + private OpenIdConfiguration _configuration; + private String _errorPage; + private String _errorPath; + private boolean _alwaysSaveUri; + + public OpenIdAuthenticator() + { + } + + public OpenIdAuthenticator(OpenIdConfiguration configuration, String errorPage) + { + this._configuration = configuration; + if (errorPage != null) + setErrorPage(errorPage); + } + + @Override + public void setConfiguration(AuthConfiguration configuration) + { + super.setConfiguration(configuration); + + String error = configuration.getInitParameter(ERROR_PAGE); + if (error != null) + setErrorPage(error); + + if (_configuration != null) + return; + + LoginService loginService = configuration.getLoginService(); + if (!(loginService instanceof OpenIdLoginService)) + throw new IllegalArgumentException("invalid LoginService"); + this._configuration = ((OpenIdLoginService)loginService).getConfiguration(); + } + + @Override + public String getAuthMethod() + { + return Constraint.__OPENID_AUTH; + } + + /** + * If true, uris that cause a redirect to a login page will always + * be remembered. If false, only the first uri that leads to a login + * page redirect is remembered. + * + * @param alwaysSave true to always save the uri + */ + public void setAlwaysSaveUri(boolean alwaysSave) + { + _alwaysSaveUri = alwaysSave; + } + + public boolean isAlwaysSaveUri() + { + return _alwaysSaveUri; + } + + private void setErrorPage(String path) + { + if (path == null || path.trim().length() == 0) + { + _errorPath = null; + _errorPage = null; + } + else + { + if (!path.startsWith("/")) + { + LOG.warn("error-page must start with /"); + path = "/" + path; + } + _errorPage = path; + _errorPath = path; + + if (_errorPath.indexOf('?') > 0) + _errorPath = _errorPath.substring(0, _errorPath.indexOf('?')); + } + } + + @Override + public UserIdentity login(String username, Object credentials, ServletRequest request) + { + if (LOG.isDebugEnabled()) + LOG.debug("login {} {} {}", username, credentials, request); + + UserIdentity user = super.login(username, credentials, request); + if (user != null) + { + HttpSession session = ((HttpServletRequest)request).getSession(); + Authentication cached = new SessionAuthentication(getAuthMethod(), user, credentials); + session.setAttribute(SessionAuthentication.__J_AUTHENTICATED, cached); + session.setAttribute(CLAIMS, ((OpenIdCredentials)credentials).getClaims()); + session.setAttribute(RESPONSE, ((OpenIdCredentials)credentials).getResponse()); + } + 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); + session.removeAttribute(CLAIMS); + session.removeAttribute(RESPONSE); + } + + @Override + public void prepareRequest(ServletRequest request) + { + //if this is a request resulting from a redirect after auth is complete + //(ie its from a redirect to the original request uri) then due to + //browser handling of 302 redirects, the method may not be the same as + //that of the original request. Replace the method and original post + //params (if it was a post). + // + //See Servlet Spec 3.1 sec 13.6.3 + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpSession session = httpRequest.getSession(false); + if (session == null || session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null) + return; //not authenticated yet + + String juri = (String)session.getAttribute(J_URI); + if (juri == null || juri.length() == 0) + return; //no original uri saved + + String method = (String)session.getAttribute(J_METHOD); + if (method == null || method.length() == 0) + return; //didn't save original request method + + StringBuffer buf = httpRequest.getRequestURL(); + if (httpRequest.getQueryString() != null) + buf.append("?").append(httpRequest.getQueryString()); + + if (!juri.equals(buf.toString())) + return; //this request is not for the same url as the original + + //restore the original request's method on this request + if (LOG.isDebugEnabled()) + LOG.debug("Restoring original method {} for {} with method {}", method, juri, httpRequest.getMethod()); + Request baseRequest = Request.getBaseRequest(request); + baseRequest.setMethod(method); + } + + @Override + public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException + { + final HttpServletRequest request = (HttpServletRequest)req; + final HttpServletResponse response = (HttpServletResponse)res; + final Request baseRequest = Request.getBaseRequest(request); + final Response baseResponse = baseRequest.getResponse(); + + String uri = request.getRequestURI(); + if (uri == null) + uri = URIUtil.SLASH; + + mandatory |= isJSecurityCheck(uri); + if (!mandatory) + return new DeferredAuthentication(this); + + if (isErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response)) + return new DeferredAuthentication(this); + + try + { + // Handle a request for authentication. + if (isJSecurityCheck(uri)) + { + String authCode = request.getParameter("code"); + if (authCode != null) + { + // Verify anti-forgery state token + String state = request.getParameter("state"); + String antiForgeryToken = (String)request.getSession().getAttribute(CSRF_TOKEN); + if (antiForgeryToken == null || !antiForgeryToken.equals(state)) + { + LOG.warn("auth failed 403: invalid state parameter"); + if (response != null) + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return Authentication.SEND_FAILURE; + } + + // Attempt to login with the provided authCode + OpenIdCredentials credentials = new OpenIdCredentials(authCode, getRedirectUri(request), _configuration); + UserIdentity user = login(null, credentials, request); + HttpSession session = request.getSession(false); + if (user != null) + { + // Redirect to original request + String nuri; + synchronized (session) + { + nuri = (String)session.getAttribute(J_URI); + + if (nuri == null || nuri.length() == 0) + { + nuri = request.getContextPath(); + if (nuri.length() == 0) + nuri = URIUtil.SLASH; + } + } + OpenIdAuthentication openIdAuth = new OpenIdAuthentication(getAuthMethod(), user); + if (LOG.isDebugEnabled()) + LOG.debug("authenticated {}->{}", openIdAuth, nuri); + + response.setContentLength(0); + int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); + baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(nuri)); + return openIdAuth; + } + } + + // not authenticated + if (LOG.isDebugEnabled()) + LOG.debug("OpenId authentication FAILED"); + if (_errorPage == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("auth failed 403"); + if (response != null) + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("auth failed {}", _errorPage); + int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); + baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), _errorPage))); + } + + return Authentication.SEND_FAILURE; + } + + // Look for cached authentication + HttpSession session = request.getSession(false); + Authentication authentication = session == null ? null : (Authentication)session.getAttribute(SessionAuthentication.__J_AUTHENTICATED); + if (authentication != null) + { + // Has authentication been revoked? + if (authentication instanceof Authentication.User && _loginService != null && + !_loginService.validate(((Authentication.User)authentication).getUserIdentity())) + { + if (LOG.isDebugEnabled()) + LOG.debug("auth revoked {}", authentication); + session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED); + } + else + { + synchronized (session) + { + String jUri = (String)session.getAttribute(J_URI); + if (jUri != null) + { + //check if the request is for the same url as the original and restore + //params if it was a post + if (LOG.isDebugEnabled()) + LOG.debug("auth retry {}->{}", authentication, jUri); + StringBuffer buf = request.getRequestURL(); + if (request.getQueryString() != null) + buf.append("?").append(request.getQueryString()); + + if (jUri.equals(buf.toString())) + { + @SuppressWarnings("unchecked") + MultiMapjPost = (MultiMap )session.getAttribute(J_POST); + if (jPost != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("auth rePOST {}->{}", authentication, jUri); + baseRequest.setContentParameters(jPost); + } + session.removeAttribute(J_URI); + session.removeAttribute(J_METHOD); + session.removeAttribute(J_POST); + } + } + } + if (LOG.isDebugEnabled()) + LOG.debug("auth {}", authentication); + return authentication; + } + } + + // if we can't send challenge + if (DeferredAuthentication.isDeferred(response)) + { + if (LOG.isDebugEnabled()) + LOG.debug("auth deferred {}", session == null ? null : session.getId()); + return Authentication.UNAUTHENTICATED; + } + + // remember the current URI + session = (session != null ? session : request.getSession(true)); + synchronized (session) + { + // But only if it is not set already, or we save every uri that leads to a login redirect + if (session.getAttribute(J_URI) == null || isAlwaysSaveUri()) + { + StringBuffer buf = request.getRequestURL(); + if (request.getQueryString() != null) + buf.append("?").append(request.getQueryString()); + session.setAttribute(J_URI, buf.toString()); + session.setAttribute(J_METHOD, request.getMethod()); + + if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod())) + { + MultiMap formParameters = new MultiMap<>(); + baseRequest.extractFormParameters(formParameters); + session.setAttribute(J_POST, formParameters); + } + } + } + + // send the the challenge + String challengeUri = getChallengeUri(request); + if (LOG.isDebugEnabled()) + LOG.debug("challenge {}->{}", session.getId(), challengeUri); + int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); + baseResponse.sendRedirect(redirectCode, response.encodeRedirectURL(challengeUri)); + + return Authentication.SEND_CONTINUE; + } + catch (IOException e) + { + throw new ServerAuthException(e); + } + } + + public boolean isJSecurityCheck(String uri) + { + int jsc = uri.indexOf(J_SECURITY_CHECK); + + if (jsc < 0) + return false; + int e = jsc + J_SECURITY_CHECK.length(); + if (e == uri.length()) + return true; + char c = uri.charAt(e); + return c == ';' || c == '#' || c == '/' || c == '?'; + } + + public boolean isErrorPage(String pathInContext) + { + return pathInContext != null && (pathInContext.equals(_errorPath)); + } + + private String getRedirectUri(HttpServletRequest request) + { + final StringBuffer redirectUri = new StringBuffer(128); + URIUtil.appendSchemeHostPort(redirectUri, request.getScheme(), + request.getServerName(), request.getServerPort()); + redirectUri.append(request.getContextPath()); + redirectUri.append(J_SECURITY_CHECK); + return redirectUri.toString(); + } + + protected String getChallengeUri(HttpServletRequest request) + { + HttpSession session = request.getSession(); + String antiForgeryToken; + synchronized (session) + { + antiForgeryToken = (session.getAttribute(CSRF_TOKEN) == null) + ? new BigInteger(130, new SecureRandom()).toString(32) + : (String)session.getAttribute(CSRF_TOKEN); + session.setAttribute(CSRF_TOKEN, antiForgeryToken); + } + + // any custom scopes requested from configuration + StringBuilder scopes = new StringBuilder(); + for (String s : _configuration.getScopes()) + { + scopes.append(" ").append(s); + } + + return _configuration.getAuthEndpoint() + + "?client_id=" + UrlEncoded.encodeString(_configuration.getClientId(), StandardCharsets.UTF_8) + + "&redirect_uri=" + UrlEncoded.encodeString(getRedirectUri(request), StandardCharsets.UTF_8) + + "&scope=openid" + UrlEncoded.encodeString(scopes.toString(), StandardCharsets.UTF_8) + + "&state=" + antiForgeryToken + + "&response_type=code"; + } + + @Override + public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) + { + return true; + } + + /** + * This Authentication represents a just completed OpenId Connect authentication. + * Subsequent requests from the same user are authenticated by the presents + * of a {@link SessionAuthentication} instance in their session. + */ + public static class OpenIdAuthentication extends UserAuthentication implements Authentication.ResponseSent + { + public OpenIdAuthentication(String method, UserIdentity userIdentity) + { + super(method, userIdentity); + } + + @Override + public String toString() + { + return "OpenId" + super.toString(); + } + } +} diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorFactory.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorFactory.java new file mode 100644 index 00000000000..86eea6cdbde --- /dev/null +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticatorFactory.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// 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.openid; + +import javax.servlet.ServletContext; + +import org.eclipse.jetty.security.Authenticator; +import org.eclipse.jetty.security.DefaultAuthenticatorFactory; +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.LoginService; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.security.Constraint; + +public class OpenIdAuthenticatorFactory extends DefaultAuthenticatorFactory +{ + @Override + public Authenticator getAuthenticator(Server server, ServletContext context, Authenticator.AuthConfiguration configuration, IdentityService identityService, LoginService loginService) + { + String auth = configuration.getAuthMethod(); + if (Constraint.__OPENID_AUTH.equalsIgnoreCase(auth)) + return new OpenIdAuthenticator(); + return super.getAuthenticator(server, context, configuration, identityService, loginService); + } +} \ No newline at end of file diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java new file mode 100644 index 00000000000..3d14aa2d36c --- /dev/null +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdConfiguration.java @@ -0,0 +1,141 @@ +// +// ======================================================================== +// 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.openid; + +import java.io.InputStream; +import java.io.Serializable; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.ajax.JSON; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * Holds the configuration for an OpenID Connect service. + * + * This uses the OpenID Provider URL with the path {@link #CONFIG_PATH} to discover + * the required information about the OIDC service. + */ +public class OpenIdConfiguration implements Serializable +{ + private static final Logger LOG = Log.getLogger(OpenIdConfiguration.class); + private static final long serialVersionUID = 2227941990601349102L; + private static final String CONFIG_PATH = "/.well-known/openid-configuration"; + + private final String openIdProvider; + private final String issuer; + private final String authEndpoint; + private final String tokenEndpoint; + private final String clientId; + private final String clientSecret; + private final Map discoveryDocument; + private final List scopes = new ArrayList<>(); + + /** + * Create an OpenID configuration for a specific OIDC provider. + * @param provider The URL of the OpenID provider. + * @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server. + * @param clientSecret The client secret known only by the Client and the Authorization Server. + */ + public OpenIdConfiguration(String provider, String clientId, String clientSecret) + { + this.openIdProvider = provider; + this.clientId = clientId; + this.clientSecret = clientSecret; + + try + { + if (provider.endsWith("/")) + provider = provider.substring(0, provider.length() - 1); + + URI providerUri = URI.create(provider + CONFIG_PATH); + InputStream inputStream = providerUri.toURL().openConnection().getInputStream(); + String content = IO.toString(inputStream); + discoveryDocument = (Map)JSON.parse(content); + if (LOG.isDebugEnabled()) + LOG.debug("discovery document {}", discoveryDocument); + } + catch (Throwable e) + { + throw new IllegalArgumentException("invalid identity provider", e); + } + + issuer = (String)discoveryDocument.get("issuer"); + if (issuer == null) + throw new IllegalArgumentException(); + + authEndpoint = (String)discoveryDocument.get("authorization_endpoint"); + if (authEndpoint == null) + throw new IllegalArgumentException("authorization_endpoint"); + + tokenEndpoint = (String)discoveryDocument.get("token_endpoint"); + if (tokenEndpoint == null) + throw new IllegalArgumentException("token_endpoint"); + } + + public Map getDiscoveryDocument() + { + return discoveryDocument; + } + + public String getAuthEndpoint() + { + return authEndpoint; + } + + public String getClientId() + { + return clientId; + } + + public String getClientSecret() + { + return clientSecret; + } + + public String getIssuer() + { + return issuer; + } + + public String getOpenIdProvider() + { + return openIdProvider; + } + + public String getTokenEndpoint() + { + return tokenEndpoint; + } + + public void addScopes(String... scopes) + { + Collections.addAll(this.scopes, scopes); + } + + public List getScopes() + { + return scopes; + } +} diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java new file mode 100644 index 00000000000..85df9e28b04 --- /dev/null +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdCredentials.java @@ -0,0 +1,214 @@ +// +// ======================================================================== +// 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.openid; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.UrlEncoded; +import org.eclipse.jetty.util.ajax.JSON; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * The credentials of an user to be authenticated with OpenID Connect. This will contain + * the OpenID ID Token and the OAuth 2.0 Access Token.
+ * + *+ * This is constructed with an authorization code from the authentication request. This authorization code + * is then exchanged using {@link #redeemAuthCode()} for a response containing the ID Token and Access Token. + * The response is then validated against the {@link OpenIdConfiguration}. + *
+ */ +public class OpenIdCredentials implements Serializable +{ + private static final Logger LOG = Log.getLogger(OpenIdCredentials.class); + private static final long serialVersionUID = 4766053233370044796L; + + private final String redirectUri; + private final OpenIdConfiguration configuration; + private String authCode; + private Mapresponse; + private Map claims; + + public OpenIdCredentials(String authCode, String redirectUri, OpenIdConfiguration configuration) + { + this.authCode = authCode; + this.redirectUri = redirectUri; + this.configuration = configuration; + } + + public String getUserId() + { + return (String)claims.get("sub"); + } + + public Map getClaims() + { + return claims; + } + + public Map getResponse() + { + return response; + } + + public void redeemAuthCode() throws IOException + { + if (LOG.isDebugEnabled()) + LOG.debug("redeemAuthCode() {}", this); + + if (authCode != null) + { + try + { + response = claimAuthCode(authCode); + if (LOG.isDebugEnabled()) + LOG.debug("response: {}", response); + + String idToken = (String)response.get("id_token"); + if (idToken == null) + throw new IllegalArgumentException("no id_token"); + + String accessToken = (String)response.get("access_token"); + if (accessToken == null) + throw new IllegalArgumentException("no access_token"); + + String tokenType = (String)response.get("token_type"); + if (!"Bearer".equalsIgnoreCase(tokenType)) + throw new IllegalArgumentException("invalid token_type"); + + claims = decodeJWT(idToken); + if (LOG.isDebugEnabled()) + LOG.debug("claims {}", claims); + validateClaims(); + } + finally + { + // reset authCode as it can only be used once + authCode = null; + } + } + } + + private void validateClaims() + { + // Issuer Identifier for the OpenID Provider MUST exactly match the value of the iss (issuer) Claim. + if (!configuration.getIssuer().equals(claims.get("iss"))) + throw new IllegalArgumentException("Issuer Identifier MUST exactly match the iss Claim"); + + // The aud (audience) Claim MUST contain the client_id value. + if (!configuration.getClientId().equals(claims.get("aud"))) + throw new IllegalArgumentException("Audience Claim MUST contain the client_id value"); + + // If an azp (authorized party) Claim is present, verify that its client_id is the Claim Value. + Object azp = claims.get("azp"); + if (azp != null && !configuration.getClientId().equals(azp)) + throw new IllegalArgumentException("Authorized party claim value should be the client_id"); + } + + public boolean isExpired() + { + if (authCode != null || claims == null) + return true; + + // Check expiry + long expiry = (Long)claims.get("exp"); + long currentTimeSeconds = (long)(System.currentTimeMillis() / 1000F); + if (currentTimeSeconds > expiry) + { + if (LOG.isDebugEnabled()) + LOG.debug("OpenId Credentials expired {}", this); + return true; + } + + return false; + } + + protected Map decodeJWT(String jwt) throws IOException + { + if (LOG.isDebugEnabled()) + LOG.debug("decodeJWT {}", jwt); + + String[] sections = jwt.split("\\."); + if (sections.length != 3) + throw new IllegalArgumentException("JWT does not contain 3 sections"); + + Base64.Decoder decoder = Base64.getDecoder(); + String jwtHeaderString = new String(decoder.decode(sections[0]), StandardCharsets.UTF_8); + String jwtClaimString = new String(decoder.decode(sections[1]), StandardCharsets.UTF_8); + String jwtSignature = sections[2]; + + Map jwtHeader = (Map)JSON.parse(jwtHeaderString); + LOG.debug("JWT Header: {}", jwtHeader); + + /* If the ID Token is received via direct communication between the Client + and the Token Endpoint (which it is in this flow), the TLS server validation + MAY be used to validate the issuer in place of checking the token signature. */ + if (LOG.isDebugEnabled()) + LOG.debug("JWT signature not validated {}", jwtSignature); + + return (Map)JSON.parse(jwtClaimString); + } + + private Map claimAuthCode(String authCode) throws IOException + { + if (LOG.isDebugEnabled()) + LOG.debug("claimAuthCode {}", authCode); + + // Use the authorization code to get the id_token from the OpenID Provider + String urlParameters = "code=" + authCode + + "&client_id=" + UrlEncoded.encodeString(configuration.getClientId(), StandardCharsets.UTF_8) + + "&client_secret=" + UrlEncoded.encodeString(configuration.getClientSecret(), StandardCharsets.UTF_8) + + "&redirect_uri=" + UrlEncoded.encodeString(redirectUri, StandardCharsets.UTF_8) + + "&grant_type=authorization_code"; + + URL url = new URL(configuration.getTokenEndpoint()); + HttpURLConnection connection = (HttpURLConnection)url.openConnection(); + try + { + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Host", configuration.getOpenIdProvider()); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + + try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) + { + wr.write(urlParameters.getBytes(StandardCharsets.UTF_8)); + } + + try (InputStream content = (InputStream)connection.getContent()) + { + return (Map)JSON.parse(IO.toString(content)); + } + } + finally + { + connection.disconnect(); + } + } +} diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java new file mode 100644 index 00000000000..512ff474b71 --- /dev/null +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdLoginService.java @@ -0,0 +1,170 @@ +// +// ======================================================================== +// 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.openid; + +import java.security.Principal; +import javax.security.auth.Subject; +import javax.servlet.ServletRequest; + +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.LoginService; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * The implementation of {@link LoginService} required to use OpenID Connect. + * + * + * Can contain an optional wrapped {@link LoginService} which is used to store role information about users. + *
+ */ +public class OpenIdLoginService extends ContainerLifeCycle implements LoginService +{ + private static final Logger LOG = Log.getLogger(OpenIdLoginService.class); + + private final OpenIdConfiguration _configuration; + private final LoginService loginService; + private IdentityService identityService; + private boolean authenticateNewUsers; + + public OpenIdLoginService(OpenIdConfiguration configuration) + { + this(configuration, null); + } + + /** + * Use a wrapped {@link LoginService} to store information about user roles. + * Users in the wrapped loginService must be stored with their username as + * the value of the sub (subject) Claim, and a credentials value of the empty string. + * @param configuration the OpenID configuration to use. + * @param loginService the wrapped LoginService to defer to for user roles. + */ + public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService) + { + _configuration = configuration; + this.loginService = loginService; + addBean(this.loginService); + } + + @Override + public String getName() + { + return _configuration.getOpenIdProvider(); + } + + public OpenIdConfiguration getConfiguration() + { + return _configuration; + } + + @Override + public UserIdentity login(String identifier, Object credentials, ServletRequest req) + { + if (LOG.isDebugEnabled()) + LOG.debug("login({}, {}, {})", identifier, credentials, req); + + OpenIdCredentials openIdCredentials = (OpenIdCredentials)credentials; + try + { + openIdCredentials.redeemAuthCode(); + if (openIdCredentials.isExpired()) + return null; + } + catch (Throwable e) + { + LOG.warn(e); + return null; + } + + OpenIdUserPrincipal userPrincipal = new OpenIdUserPrincipal(openIdCredentials); + Subject subject = new Subject(); + subject.getPrincipals().add(userPrincipal); + subject.getPrivateCredentials().add(credentials); + subject.setReadOnly(); + + if (loginService != null) + { + UserIdentity userIdentity = loginService.login(openIdCredentials.getUserId(), "", req); + if (userIdentity == null) + { + if (isAuthenticateNewUsers()) + return getIdentityService().newUserIdentity(subject, userPrincipal, new String[0]); + return null; + } + return new OpenIdUserIdentity(subject, userPrincipal, userIdentity); + } + + return identityService.newUserIdentity(subject, userPrincipal, new String[0]); + } + + public boolean isAuthenticateNewUsers() + { + return authenticateNewUsers; + } + + /** + * This setting is only meaningful if a wrapped {@link LoginService} has been set. + *+ * If set to true, any users not found by the wrapped {@link LoginService} will still + * be authenticated but with no roles, if set to false users will not be + * authenticated unless they are discovered by the wrapped {@link LoginService}. + *
+ * @param authenticateNewUsers whether to authenticate users not found by a wrapping LoginService + */ + public void setAuthenticateNewUsers(boolean authenticateNewUsers) + { + this.authenticateNewUsers = authenticateNewUsers; + } + + @Override + public boolean validate(UserIdentity user) + { + Principal userPrincipal = user.getUserPrincipal(); + if (!(userPrincipal instanceof OpenIdUserPrincipal)) + return false; + + OpenIdCredentials credentials = ((OpenIdUserPrincipal)userPrincipal).getCredentials(); + return !credentials.isExpired(); + } + + @Override + public IdentityService getIdentityService() + { + return loginService == null ? identityService : loginService.getIdentityService(); + } + + @Override + public void setIdentityService(IdentityService service) + { + if (isRunning()) + throw new IllegalStateException("Running"); + + if (loginService != null) + loginService.setIdentityService(service); + else + identityService = service; + } + + @Override + public void logout(UserIdentity user) + { + } +} diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserIdentity.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserIdentity.java new file mode 100644 index 00000000000..f375e138c50 --- /dev/null +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserIdentity.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 org.eclipse.jetty.security.openid; + +import java.security.Principal; +import javax.security.auth.Subject; + +import org.eclipse.jetty.server.UserIdentity; + +public class OpenIdUserIdentity implements UserIdentity +{ + private final Subject subject; + private final Principal userPrincipal; + private final UserIdentity userIdentity; + + public OpenIdUserIdentity(Subject subject, Principal userPrincipal, UserIdentity userIdentity) + { + this.subject = subject; + this.userPrincipal = userPrincipal; + this.userIdentity = userIdentity; + } + + @Override + public Subject getSubject() + { + return subject; + } + + @Override + public Principal getUserPrincipal() + { + return userPrincipal; + } + + @Override + public boolean isUserInRole(String role, Scope scope) + { + return userIdentity != null && userIdentity.isUserInRole(role, scope); + } +} diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java new file mode 100644 index 00000000000..6ebb46df2a2 --- /dev/null +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdUserPrincipal.java @@ -0,0 +1,50 @@ +// +// ======================================================================== +// 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.openid; + +import java.io.Serializable; +import java.security.Principal; + +public class OpenIdUserPrincipal implements Principal, Serializable +{ + private static final long serialVersionUID = 1521094652756670469L; + private final OpenIdCredentials _credentials; + + public OpenIdUserPrincipal(OpenIdCredentials credentials) + { + _credentials = credentials; + } + + public OpenIdCredentials getCredentials() + { + return _credentials; + } + + @Override + public String getName() + { + return _credentials.getUserId(); + } + + @Override + public String toString() + { + return _credentials.getUserId(); + } +} \ No newline at end of file diff --git a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java new file mode 100644 index 00000000000..54dd613c52d --- /dev/null +++ b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdAuthenticationTest.java @@ -0,0 +1,226 @@ +// +// ======================================================================== +// 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.openid; + +import java.io.IOException; +import java.security.Principal; +import java.util.Map; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.security.Authenticator; +import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.security.Constraint; +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; + +public class OpenIdAuthenticationTest +{ + public static final String CLIENT_ID = "testClient101"; + public static final String CLIENT_SECRET = "secret37989798"; + + private OpenIdProvider openIdProvider; + private Server server; + private ServerConnector connector; + private HttpClient client; + + @BeforeEach + public void setup() throws Exception + { + openIdProvider = new OpenIdProvider(CLIENT_ID, CLIENT_SECRET); + openIdProvider.start(); + + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS); + + // Add servlets + context.addServlet(LoginPage.class, "/login"); + context.addServlet(LogoutPage.class, "/logout"); + context.addServlet(HomePage.class, "/*"); + context.addServlet(ErrorPage.class, "/error"); + + // configure security constraints + Constraint constraint = new Constraint(); + constraint.setName(Constraint.__OPENID_AUTH); + constraint.setRoles(new String[]{"**"}); + constraint.setAuthenticate(true); + + Constraint adminConstraint = new Constraint(); + adminConstraint.setName(Constraint.__OPENID_AUTH); + adminConstraint.setRoles(new String[]{"admin"}); + adminConstraint.setAuthenticate(true); + + // constraint mappings + ConstraintMapping profileMapping = new ConstraintMapping(); + profileMapping.setConstraint(constraint); + profileMapping.setPathSpec("/profile"); + ConstraintMapping loginMapping = new ConstraintMapping(); + loginMapping.setConstraint(constraint); + loginMapping.setPathSpec("/login"); + ConstraintMapping adminMapping = new ConstraintMapping(); + adminMapping.setConstraint(adminConstraint); + adminMapping.setPathSpec("/admin"); + + // security handler + ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + securityHandler.setRealmName("OpenID Connect Authentication"); + securityHandler.addConstraintMapping(profileMapping); + securityHandler.addConstraintMapping(loginMapping); + securityHandler.addConstraintMapping(adminMapping); + + // Authentication using local OIDC Provider + OpenIdConfiguration configuration = new OpenIdConfiguration(openIdProvider.getProvider(), CLIENT_ID, CLIENT_SECRET); + + // Configure OpenIdLoginService optionally providing a base LoginService to provide user roles + OpenIdLoginService loginService = new OpenIdLoginService(configuration);//, hashLoginService); + securityHandler.setLoginService(loginService); + + Authenticator authenticator = new OpenIdAuthenticator(configuration, "/error"); + securityHandler.setAuthenticator(authenticator); + context.setSecurityHandler(securityHandler); + + server.start(); + String redirectUri = "http://localhost:"+connector.getLocalPort() + "/j_security_check"; + openIdProvider.addRedirectUri(redirectUri); + + client = new HttpClient(); + client.start(); + } + + @AfterEach + public void stop() throws Exception + { + openIdProvider.stop(); + server.stop(); + } + + @Test + public void testLoginLogout() throws Exception + { + String appUriString = "http://localhost:"+connector.getLocalPort(); + + // Initially not authenticated + ContentResponse response = client.GET(appUriString + "/"); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + String[] content = response.getContentAsString().split("\n"); + assertThat(content.length, is(1)); + assertThat(content[0], is("not authenticated")); + + // Request to login is success + response = client.GET(appUriString + "/login"); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + content = response.getContentAsString().split("\n"); + assertThat(content.length, is(1)); + assertThat(content[0], is("success")); + + // Now authenticated we can get info + response = client.GET(appUriString + "/"); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + content = response.getContentAsString().split("\n"); + assertThat(content.length, is(3)); + assertThat(content[0], is("userId: 123456789")); + assertThat(content[1], is("name: FirstName LastName")); + assertThat(content[2], is("email: FirstName@fake-email.com")); + + // Request to admin page gives 403 as we do not have admin role + response = client.GET(appUriString + "/admin"); + assertThat(response.getStatus(), is(HttpStatus.FORBIDDEN_403)); + + // We are no longer authenticated after logging out + response = client.GET(appUriString + "/logout"); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + content = response.getContentAsString().split("\n"); + assertThat(content.length, is(1)); + assertThat(content[0], is("not authenticated")); + } + + public static class LoginPage extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.getWriter().println("success"); + } + } + + public static class LogoutPage extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + request.getSession().invalidate(); + response.sendRedirect("/"); + } + } + + public static class AdminPage extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + MapuserInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS); + response.getWriter().println(userInfo.get("sub") + ": success"); + } + } + + public static class HomePage extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.setContentType("text/plain"); + Principal userPrincipal = request.getUserPrincipal(); + if (userPrincipal != null) + { + Map userInfo = (Map)request.getSession().getAttribute(OpenIdAuthenticator.CLAIMS); + response.getWriter().println("userId: " + userInfo.get("sub")); + response.getWriter().println("name: " + userInfo.get("name")); + response.getWriter().println("email: " + userInfo.get("email")); + } + else + { + response.getWriter().println("not authenticated"); + } + } + } + + public static class ErrorPage extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.setContentType("text/plain"); + response.getWriter().println("not authorized"); + } + } +} diff --git a/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java new file mode 100644 index 00000000000..83bfb3441b7 --- /dev/null +++ b/jetty-openid/src/test/java/org/eclipse/jetty/security/openid/OpenIdProvider.java @@ -0,0 +1,254 @@ +// +// ======================================================================== +// 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.openid; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +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.StringUtil; +import org.eclipse.jetty.util.component.ContainerLifeCycle; + +public class OpenIdProvider extends ContainerLifeCycle +{ + private static final String CONFIG_PATH = "/.well-known/openid-configuration"; + private static final String AUTH_PATH = "/auth"; + private static final String TOKEN_PATH = "/token"; + private final Map issuedAuthCodes = new HashMap<>(); + + protected final String clientId; + protected final String clientSecret; + protected final List redirectUris = new ArrayList<>(); + + private String provider; + private Server server; + private ServerConnector connector; + + public OpenIdProvider(String clientId, String clientSecret) + { + this.clientId = clientId; + this.clientSecret = clientSecret; + + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/"); + contextHandler.addServlet(new ServletHolder(new OpenIdConfigServlet()), CONFIG_PATH); + contextHandler.addServlet(new ServletHolder(new OpenIdAuthEndpoint()), AUTH_PATH); + contextHandler.addServlet(new ServletHolder(new OpenIdTokenEndpoint()), TOKEN_PATH); + server.setHandler(contextHandler); + + addBean(server); + } + + @Override + protected void doStart() throws Exception + { + super.doStart(); + provider = "http://localhost:" + connector.getLocalPort(); + } + + public String getProvider() + { + if (!isStarted()) + throw new IllegalStateException(); + return provider; + } + + public void addRedirectUri(String uri) + { + redirectUris.add(uri); + } + + public class OpenIdAuthEndpoint extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (!clientId.equals(req.getParameter("client_id"))) + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid client_id"); + return; + } + + String redirectUri = req.getParameter("redirect_uri"); + if (!redirectUris.contains(redirectUri)) + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid redirect_uri"); + return; + } + + String scopeString = req.getParameter("scope"); + List scopes = (scopeString == null) ? Collections.emptyList() : Arrays.asList(StringUtil.csvSplit(scopeString)); + if (!scopes.contains("openid")) + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no openid scope"); + return; + } + + if (!"code".equals(req.getParameter("response_type"))) + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "response_type must be code"); + return; + } + + String state = req.getParameter("state"); + if (state == null) + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "no state param"); + return; + } + + String authCode = UUID.randomUUID().toString().replace("-", ""); + User user = new User(123456789, "FirstName", "LastName"); + issuedAuthCodes.put(authCode, user); + + final Request baseRequest = Request.getBaseRequest(req); + final Response baseResponse = baseRequest.getResponse(); + redirectUri += "?code=" + authCode + "&state=" + state; + int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? + HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER); + baseResponse.sendRedirect(redirectCode, resp.encodeRedirectURL(redirectUri)); + } + } + + public class OpenIdTokenEndpoint extends HttpServlet + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + String code = req.getParameter("code"); + + if (!clientId.equals(req.getParameter("client_id")) || + !clientSecret.equals(req.getParameter("client_secret")) || + !redirectUris.contains(req.getParameter("redirect_uri")) || + !"authorization_code".equals(req.getParameter("grant_type")) || + code == null) + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "bad auth request"); + return; + } + + User user = issuedAuthCodes.remove(code); + if (user == null) + { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "invalid auth code"); + return; + } + + String jwtHeader = "{\"INFO\": \"this is not used or checked in our implementation\"}"; + String jwtBody = user.getIdToken(); + String jwtSignature = "we do not validate signature as we use the authorization code flow"; + + Base64.Encoder encoder = Base64.getEncoder(); + String jwt = encoder.encodeToString(jwtHeader.getBytes()) + "." + + encoder.encodeToString(jwtBody.getBytes()) + "." + + encoder.encodeToString(jwtSignature.getBytes()); + + String accessToken = "ABCDEFG"; + long expiry = System.currentTimeMillis() + Duration.ofMinutes(10).toMillis(); + String response = "{" + + "\"access_token\": \"" + accessToken + "\"," + + "\"id_token\": \"" + jwt + "\"," + + "\"expires_in\": " + expiry + "," + + "\"token_type\": \"Bearer\"" + + "}"; + + resp.setContentType("text/plain"); + resp.getWriter().print(response); + } + } + + public class OpenIdConfigServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + String discoveryDocument = "{" + + "\"issuer\": \"" + provider + "\"," + + "\"authorization_endpoint\": \"" + provider + AUTH_PATH + "\"," + + "\"token_endpoint\": \"" + provider + TOKEN_PATH + "\"," + + "}"; + + resp.getWriter().write(discoveryDocument); + } + } + + public class User + { + private long subject; + private String firstName; + private String lastName; + + public User(String firstName, String lastName) + { + this(new Random().nextLong(), firstName, lastName); + } + + public User(long subject, String firstName, String lastName) + { + this.subject = subject; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() + { + return firstName; + } + + public String getLastName() + { + return lastName; + } + + public String getIdToken() + { + return "{" + + "\"iss\": \"" + provider + "\"," + + "\"sub\": \"" + subject + "\"," + + "\"aud\": \"" + clientId + "\"," + + "\"exp\": " + System.currentTimeMillis() + Duration.ofMinutes(1).toMillis() + "," + + "\"name\": \"" + firstName + " " + lastName + "\"," + + "\"email\": \"" + firstName + "@fake-email.com" + "\"" + + "}"; + } + } +} diff --git a/jetty-openid/src/test/resources/jetty-logging.properties b/jetty-openid/src/test/resources/jetty-logging.properties new file mode 100755 index 00000000000..c73ac07f8ac --- /dev/null +++ b/jetty-openid/src/test/resources/jetty-logging.properties @@ -0,0 +1,3 @@ +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +# org.eclipse.jetty.LEVEL=DEBUG +# org.eclipse.jetty.security.openid.LEVEL=DEBUG \ No newline at end of file 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 c5925ef6b77..303ada127b6 100644 --- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml +++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml @@ -3,13 +3,13 @@ - + - + diff --git a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml index 17f38f2776d..70c174072ab 100644 --- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml +++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml @@ -96,7 +96,7 @@ - org.eclipse.jetty.webapp.JmxConfiguration
- org.eclipse.jetty.osgi.annotations.AnnotationConfiguration
- org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration
-- org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration
+- org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration
- org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration
- org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration
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 cd907934b86..e5baeae8106 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 @@ -3,13 +3,13 @@ - +- + 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 df4f9a29512..3f034f1334b 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 @@ -3,13 +3,13 @@ - + - + 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 fafa0ecdd18..ccb14c09de9 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 @@ -3,13 +3,13 @@ - + - + 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 856a577d329..6f28312891c 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 @@ -3,13 +3,13 @@ - + - + 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 aeda97879e8..5ae769fca57 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 @@ -3,13 +3,13 @@ - + - + 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 3c45c45a368..20bb83b2765 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 @@ -3,13 +3,13 @@ - + - + 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 6384118ee3b..8995bfc70ce 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 @@ -3,13 +3,13 @@ - + - + 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 16984b9bbdf..ad51713fd9c 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 @@ -2,7 +2,7 @@ - + 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 a48b216e4ff..2bf9d1051b1 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 @@ -2,7 +2,7 @@ - + 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 7b408b6dc4d..41fb957e7f0 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 @@ -2,7 +2,7 @@ - + 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 077cc7f66cd..ed177d89c37 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 @@ -9,7 +9,7 @@ - + - + diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java index ab3008edd5c..c5040271992 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java @@ -51,7 +51,7 @@ import org.ietf.jgss.Oid; * of the {@link #getServiceName() service name} and the {@link #getHostName() host name}, * for example {@code HTTP/wonder.com}, using a {@code keyTab} file as the service principal * credentials. 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 9cc5f6f72e7..6c2b9d68082 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 @@ -82,7 +82,7 @@ - org.eclipse.jetty.plus.webapp.EnvConfiguration
- org.eclipse.jetty.webapp.JmxConfiguration
- org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration
-- org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration
+- org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration
- org.eclipse.jetty.osgi.annotations.AnnotationConfiguration
- org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration
- org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration
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 291fccd1873..ad2b008c33a 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 @@ -85,7 +85,7 @@- org.eclipse.jetty.webapp.JmxConfiguration
- org.eclipse.jetty.osgi.annotations.AnnotationConfiguration
- org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration
-- org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration
+- org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration
- org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration
- org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration
diff --git a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java index 5c6339b9021..0652508ffbb 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java +++ b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; + import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -116,15 +117,14 @@ public class TestOSGiUtil if (!StringUtil.isBlank(mavenRepoPath)) { res.add(systemProperty("org.ops4j.pax.url.mvn.localRepository").value(mavenRepoPath)); - res.add( systemProperty( "org.ops4j.pax.url.mvn.localRepository" ).value( mavenRepoPath ) ); - res.add( systemProperty( "org.ops4j.pax.url.mvn.defaultRepositories" ).value( "file://" + mavenRepoPath + "@id=local.repo") ); - res.add( systemProperty( "org.ops4j.pax.url.mvn.useFallbackRepositories").value( Boolean.FALSE.toString() ) ); - res.add( systemProperty( "org.ops4j.pax.url.mvn.repositories").value( "+https://repo1.maven.org/maven2@id=maven.central.repo" ) ); + res.add(systemProperty("org.ops4j.pax.url.mvn.defaultRepositories").value("file://" + mavenRepoPath + "@id=local.repo")); + res.add(systemProperty("org.ops4j.pax.url.mvn.useFallbackRepositories").value(Boolean.FALSE.toString())); + res.add(systemProperty("org.ops4j.pax.url.mvn.repositories").value("+https://repo1.maven.org/maven2@id=maven.central.repo")); } String settingsFilePath = System.getProperty("settingsFilePath"); if (!StringUtil.isBlank(settingsFilePath)) { - res.add( systemProperty( "org.ops4j.pax.url.mvn.settings" ).value( System.getProperty( "settingsFilePath" ) ) ); + res.add(systemProperty("org.ops4j.pax.url.mvn.settings").value(System.getProperty("settingsFilePath"))); } res.add(mavenBundle().groupId("org.eclipse.jetty.toolchain").artifactId("jetty-servlet-api").versionAsInProject().start()); res.add(mavenBundle().groupId("org.ow2.asm").artifactId("asm").versionAsInProject().start()); @@ -199,11 +199,11 @@ public class TestOSGiUtil for (Bundle b : bundleContext.getBundles()) { Bundle prevBundle = _bundles.put(b.getSymbolicName(), b); - String err = prevBundle != null ? "2 versions of the bundle " + b.getSymbolicName() - + " " - + b.getHeaders().get("Bundle-Version") - + " and " - + prevBundle.getHeaders().get("Bundle-Version") : ""; + String err = prevBundle != null ? "2 versions of the bundle " + b.getSymbolicName() + + " " + + b.getHeaders().get("Bundle-Version") + + " and " + + prevBundle.getHeaders().get("Bundle-Version") : ""; assertNull(err, prevBundle); } return _bundles.get(symbolicName); @@ -234,16 +234,16 @@ public class TestOSGiUtil { diagnoseNonActiveOrNonResolvedBundle(b); } - assertTrue("Bundle: " + b - + " (state should be " - + "ACTIVE[" - + Bundle.ACTIVE - + "] or RESOLVED[" - + Bundle.RESOLVED - + "]" - + ", but was [" - + b.getState() - + "])", (b.getState() == Bundle.ACTIVE) || (b.getState() == Bundle.RESOLVED)); + assertTrue("Bundle: " + b + + " (state should be " + + "ACTIVE[" + + Bundle.ACTIVE + + "] or RESOLVED[" + + Bundle.RESOLVED + + "]" + + ", but was [" + + b.getState() + + "])", (b.getState() == Bundle.ACTIVE) || (b.getState() == Bundle.RESOLVED)); } } diff --git a/jetty-plus/src/test/java/org/eclipse/jetty/plus/annotation/LifeCycleCallbackCollectionTest.java b/jetty-plus/src/test/java/org/eclipse/jetty/plus/annotation/LifeCycleCallbackCollectionTest.java index a9369c4357f..1d43f750170 100644 --- a/jetty-plus/src/test/java/org/eclipse/jetty/plus/annotation/LifeCycleCallbackCollectionTest.java +++ b/jetty-plus/src/test/java/org/eclipse/jetty/plus/annotation/LifeCycleCallbackCollectionTest.java @@ -18,20 +18,15 @@ package org.eclipse.jetty.plus.annotation; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anEmptyMap; -import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.fail; - import java.lang.reflect.Method; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.fail; + public class LifeCycleCallbackCollectionTest { diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index bf7dd3cec3a..a50db15003c 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -693,6 +693,14 @@ public abstract class AbstractProxyServlet extends HttpServlet catch (Exception e) { _log.ignore(e); + try + { + proxyResponse.sendError(-1); + } + catch (Exception e2) + { + _log.ignore(e2); + } } finally { diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java index c64d64fb571..a9dbb178f84 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java @@ -78,6 +78,7 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.StacklessLogging; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.OS; @@ -88,6 +89,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +@Disabled("See issue #3974") public class AsyncMiddleManServletTest { private static final Logger LOG = Log.getLogger(AsyncMiddleManServletTest.class); diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ReverseProxyTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ReverseProxyTest.java index 5a032128670..fc596ba7a99 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ReverseProxyTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ReverseProxyTest.java @@ -141,9 +141,11 @@ public class ReverseProxyTest } }); startProxy(new HashMap() - {{ - put("preserveHost", "true"); - }}); + { + { + put("preserveHost", "true"); + } + }); startClient(); ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort()).send(); diff --git a/jetty-quickstart/src/main/config/etc/example-quickstart.xml b/jetty-quickstart/src/main/config/etc/example-quickstart.xml deleted file mode 100644 index c042d89d724..00000000000 --- a/jetty-quickstart/src/main/config/etc/example-quickstart.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/jetty-quickstart/src/main/config/etc/jetty-quickstart.xml b/jetty-quickstart/src/main/config/etc/jetty-quickstart.xml new file mode 100644 index 00000000000..84d030260b0 --- /dev/null +++ b/jetty-quickstart/src/main/config/etc/jetty-quickstart.xml @@ -0,0 +1,31 @@ + + + +true -/ -- - /application.war + diff --git a/jetty-quickstart/src/main/config/modules/jetty-quickstart.d/quickstart-webapp.xml b/jetty-quickstart/src/main/config/modules/jetty-quickstart.d/quickstart-webapp.xml new file mode 100644 index 00000000000..b0f07545e95 --- /dev/null +++ b/jetty-quickstart/src/main/config/modules/jetty-quickstart.d/quickstart-webapp.xml @@ -0,0 +1,28 @@ + + + ++ + + ++ + + + + + ++ ++ + + + + ++ ++ ++ +/etc/quickstart-webapp.xml + + diff --git a/jetty-quickstart/src/main/config/modules/quickstart.mod b/jetty-quickstart/src/main/config/modules/quickstart.mod index 102801714b6..c531ea648d0 100644 --- a/jetty-quickstart/src/main/config/modules/quickstart.mod +++ b/jetty-quickstart/src/main/config/modules/quickstart.mod @@ -6,8 +6,21 @@ deployment of preconfigured webapplications. [depend] server -plus -annotations +deploy [lib] lib/jetty-quickstart-${jetty.version}.jar + +[xml] +etc/jetty-quickstart.xml + +[files] +basehome:modules/jetty-quickstart.d/quickstart-webapp.xml|etc/quickstart-webapp.xml + + +[ini-template] + +# Modes are AUTO, GENERATE, QUICKSTART +# jetty.quickstart.mode=AUTO +# jetty.quickstart.origin=origin +# jetty.quickstart.xml= diff --git a/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/PreconfigureQuickStartWar.java b/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/PreconfigureQuickStartWar.java index 627f676588a..92814695c33 100644 --- a/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/PreconfigureQuickStartWar.java +++ b/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/PreconfigureQuickStartWar.java @@ -20,11 +20,15 @@ package org.eclipse.jetty.quickstart; import java.util.Locale; +import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.plus.webapp.EnvConfiguration; +import org.eclipse.jetty.plus.webapp.PlusConfiguration; 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.JarResource; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.xml.XmlConfiguration; public class PreconfigureQuickStartWar @@ -98,7 +102,13 @@ public class PreconfigureQuickStartWar final Server server = new Server(); - QuickStartWebApp webapp = new QuickStartWebApp(); + WebAppContext webapp = new WebAppContext(); + webapp.addConfiguration(new QuickStartConfiguration(), + new EnvConfiguration(), + new PlusConfiguration(), + new AnnotationConfiguration()); + webapp.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.GENERATE); + webapp.setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, ""); if (xml != null) { @@ -108,10 +118,21 @@ public class PreconfigureQuickStartWar xmlConfiguration.configure(webapp); } webapp.setResourceBase(dir.getFile().getAbsolutePath()); - webapp.setMode(QuickStartConfiguration.Mode.GENERATE); server.setHandler(webapp); - server.start(); - server.stop(); + try + { + server.setDryRun(true); + server.start(); + } + catch (Exception e) + { + throw e; + } + finally + { + if (!server.isStopped()) + server.stop(); + } } private static void error(String message) diff --git a/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartConfiguration.java b/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartConfiguration.java index 2462ad4e326..2dc417e42d8 100644 --- a/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartConfiguration.java +++ b/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartConfiguration.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.quickstart; +import java.io.File; import java.io.IOException; import java.util.HashSet; import java.util.Set; @@ -26,6 +27,8 @@ import java.util.stream.Collectors; import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.annotations.AnnotationDecorator; import org.eclipse.jetty.annotations.ServletContainerInitializersStarter; +import org.eclipse.jetty.server.Server; +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; @@ -39,17 +42,16 @@ import org.eclipse.jetty.webapp.WebXmlConfiguration; /** * QuickStartConfiguration *+ + +org.eclipse.jetty.quickstart.origin ++ + + +org.eclipse.jetty.quickstart.xml ++ + + +org.eclipse.jetty.quickstart.mode ++ ++ ++ true +false +false + +- * Re-inflate a deployable webapp from a saved effective-web.xml - * which combines all pre-parsed web xml descriptors and annotations. + * Prepare for quickstart generation, or usage. */ public class QuickStartConfiguration extends AbstractConfiguration { private static final Logger LOG = Log.getLogger(QuickStartConfiguration.class); public static final Set
> __replacedConfigurations = new HashSet<>(); - public static final String ORIGIN_ATTRIBUTE = "org.eclipse.jetty.quickstart.ORIGIN_ATTRIBUTE"; - public static final String GENERATE_ORIGIN = "org.eclipse.jetty.quickstart.GENERATE_ORIGIN"; - public static final String QUICKSTART_WEB_XML = "org.eclipse.jetty.quickstart.QUICKSTART_WEB_XML"; + public static final String ORIGIN_ATTRIBUTE = "org.eclipse.jetty.quickstart.origin"; + public static final String QUICKSTART_WEB_XML = "org.eclipse.jetty.quickstart.xml"; + public static final String MODE = "org.eclipse.jetty.quickstart.mode"; static { @@ -59,41 +61,35 @@ public class QuickStartConfiguration extends AbstractConfiguration __replacedConfigurations.add(org.eclipse.jetty.annotations.AnnotationConfiguration.class); } - ; + /** Configure the server for the quickstart mode. + * In practise this means calling
+ * @see Server#setDryRun(boolean) + * @param server The server to configure + * @param mode The quickstart mode + */ + public static void configureMode(Server server, String mode) + { + if (mode != null && Mode.valueOf(mode) == Mode.GENERATE) + server.setDryRun(true); + } public enum Mode { - DISABLED, // No Quick start GENERATE, // Generate quickstart-web.xml and then stop AUTO, // use or generate depending on the existance of quickstart-web.xml QUICKSTART // Use quickstart-web.xml } - ; - private Mode _mode = Mode.AUTO; private boolean _quickStart; public QuickStartConfiguration() { - super(true); + super(false); addDependencies(WebInfConfiguration.class); addDependents(WebXmlConfiguration.class); } - public void setMode(Mode mode) - { - _mode = mode; - } - - public Mode getMode() - { - return _mode; - } - - /** - * @see org.eclipse.jetty.webapp.AbstractConfiguration#preConfigure(org.eclipse.jetty.webapp.WebAppContext) - */ @Override public void preConfigure(WebAppContext context) throws Exception { @@ -106,39 +102,46 @@ public class QuickStartConfiguration extends AbstractConfiguration Resource quickStartWebXml = getQuickStartWebXml(context); LOG.debug("quickStartWebXml={} exists={}", quickStartWebXml, quickStartWebXml.exists()); + //Get the mode + Mode mode = (Mode)context.getAttribute(MODE); + if (mode != null) + _mode = mode; + _quickStart = false; + switch (_mode) { - case DISABLED: - super.preConfigure(context); - break; - case GENERATE: { + if (quickStartWebXml.exists()) + LOG.info("Regenerating {}", quickStartWebXml); + else + LOG.info("Generating {}", quickStartWebXml); + super.preConfigure(context); + //generate the quickstart file then abort QuickStartGeneratorConfiguration generator = new QuickStartGeneratorConfiguration(true); configure(generator, context); context.addConfiguration(generator); break; } - case AUTO: { if (quickStartWebXml.exists()) - quickStart(context, quickStartWebXml); + { + quickStart(context); + } else { + if (LOG.isDebugEnabled()) + LOG.debug("No quickstart xml file, starting webapp {} normally", context); super.preConfigure(context); - QuickStartGeneratorConfiguration generator = new QuickStartGeneratorConfiguration(false); - configure(generator, context); - context.addConfiguration(generator); } break; } - case QUICKSTART: if (quickStartWebXml.exists()) - quickStart(context, quickStartWebXml); + quickStart(context); else throw new IllegalStateException("No " + quickStartWebXml); break; @@ -151,27 +154,20 @@ public class QuickStartConfiguration extends AbstractConfiguration protected void configure(QuickStartGeneratorConfiguration generator, WebAppContext context) throws IOException { Object attr; - attr = context.getAttribute(GENERATE_ORIGIN); - if (attr != null) - generator.setGenerateOrigin(Boolean.valueOf(attr.toString())); attr = context.getAttribute(ORIGIN_ATTRIBUTE); if (attr != null) generator.setOriginAttribute(attr.toString()); - attr = context.getAttribute(QUICKSTART_WEB_XML); - if (attr instanceof Resource) - generator.setQuickStartWebXml((Resource)attr); - else if (attr != null) - generator.setQuickStartWebXml(Resource.newResource(attr.toString())); + + generator.setQuickStartWebXml((Resource)context.getAttribute(QUICKSTART_WEB_XML)); } - /** - * @see org.eclipse.jetty.webapp.AbstractConfiguration#configure(org.eclipse.jetty.webapp.WebAppContext) - */ @Override public void configure(WebAppContext context) throws Exception { if (!_quickStart) + { super.configure(context); + } else { //add the processor to handle normal web.xml content @@ -195,14 +191,27 @@ public class QuickStartConfiguration extends AbstractConfiguration } } - protected void quickStart(WebAppContext context, Resource quickStartWebXml) + @Override + public void postConfigure(WebAppContext context) throws Exception + { + super.postConfigure(context); + ServletContainerInitializersStarter starter = (ServletContainerInitializersStarter)context.getAttribute(AnnotationConfiguration.CONTAINER_INITIALIZER_STARTER); + if (starter != null) + { + context.removeBean(starter); + context.removeAttribute(AnnotationConfiguration.CONTAINER_INITIALIZER_STARTER); + } + } + + protected void quickStart(WebAppContext context) throws Exception { + LOG.info("Quickstarting {}", context); _quickStart = true; context.setConfigurations(context.getWebAppConfigurations().stream() .filter(c -> !__replacedConfigurations.contains(c.replaces()) && !__replacedConfigurations.contains(c.getClass())) .collect(Collectors.toList()).toArray(new Configuration[]{})); - context.getMetaData().setWebXml(quickStartWebXml); + context.getMetaData().setWebXml((Resource)context.getAttribute(QUICKSTART_WEB_XML)); context.getServletContext().setEffectiveMajorVersion(context.getMetaData().getWebXml().getMajorVersion()); context.getServletContext().setEffectiveMinorVersion(context.getMetaData().getWebXml().getMinorVersion()); } @@ -216,12 +225,38 @@ public class QuickStartConfiguration extends AbstractConfiguration */ public Resource getQuickStartWebXml(WebAppContext context) throws Exception { + Object attr = context.getAttribute(QUICKSTART_WEB_XML); + if (attr instanceof Resource) + return (Resource)attr; + Resource webInf = context.getWebInf(); if (webInf == null || !webInf.exists()) - throw new IllegalStateException("No WEB-INF"); - LOG.debug("webinf={}", webInf); + { + File tmp = new File(context.getBaseResource().getFile(), "WEB-INF"); + tmp.mkdirs(); + webInf = context.getWebInf(); + } - Resource quickStartWebXml = webInf.addPath("quickstart-web.xml"); - return quickStartWebXml; + Resource qstart; + if (attr == null || StringUtil.isBlank(attr.toString())) + { + qstart = webInf.addPath("quickstart-web.xml"); + } + else + { + try + { + // Try a relative resolution + qstart = Resource.newResource(webInf.getFile().toPath().resolve(attr.toString())); + } + catch (Throwable th) + { + // try as a resource + qstart = (Resource.newResource(attr.toString())); + } + context.setAttribute(QUICKSTART_WEB_XML, qstart); + } + context.setAttribute(QUICKSTART_WEB_XML, qstart); + return qstart; } } diff --git a/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartGeneratorConfiguration.java b/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartGeneratorConfiguration.java index 20cfab4535b..753f32dce0d 100644 --- a/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartGeneratorConfiguration.java +++ b/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartGeneratorConfiguration.java @@ -52,6 +52,7 @@ import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; import org.eclipse.jetty.util.QuotedStringTokenizer; +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; @@ -61,10 +62,11 @@ import org.eclipse.jetty.webapp.MetaData; import org.eclipse.jetty.webapp.MetaData.OriginInfo; import org.eclipse.jetty.webapp.MetaInfConfiguration; import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.webapp.WebInfConfiguration; import org.eclipse.jetty.xml.XmlAppendable; /** - * QuickStartDescriptorGenerator + * QuickStartGeneratorConfiguration *server.setDryRun(true)
for GENERATE mode* Generate an effective web.xml from a WebAppContext, including all components * from web.xml, web-fragment.xmls annotations etc. @@ -81,10 +83,9 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration protected final boolean _abort; protected String _originAttribute; - protected boolean _generateOrigin; protected int _count; protected Resource _quickStartWebXml; - + public QuickStartGeneratorConfiguration() { this(false); @@ -116,22 +117,6 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration return _originAttribute; } - /** - * @return the generateOrigin - */ - public boolean isGenerateOrigin() - { - return _generateOrigin; - } - - /** - * @param generateOrigin the generateOrigin to set - */ - public void setGenerateOrigin(boolean generateOrigin) - { - _generateOrigin = generateOrigin; - } - public Resource getQuickStartWebXml() { return _quickStartWebXml; @@ -163,8 +148,6 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration if (context.getBaseResource() == null) throw new IllegalArgumentException("No base resource for " + this); - LOG.info("Quickstart generating"); - MetaData md = context.getMetaData(); Map
webappAttr = new HashMap<>(); @@ -195,13 +178,13 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration //the META-INF/resources discovered addContextParamFromAttribute(context, out, MetaInfConfiguration.METAINF_RESOURCES, normalizer); - // the default-context-path, if presernt + // the default-context-path, if present String defaultContextPath = (String)context.getAttribute("default-context-path"); if (defaultContextPath != null) out.tag("default-context-path", defaultContextPath); //add the name of the origin attribute, if it is being used - if (_generateOrigin) + if (StringUtil.isNotBlank(_originAttribute)) { out.openTag("context-param") .tag("param-name", ORIGIN) @@ -766,7 +749,7 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration */ public Map origin(MetaData md, String name) { - if (!(_generateOrigin || LOG.isDebugEnabled())) + if (StringUtil.isBlank(_originAttribute)) return Collections.emptyMap(); if (name == null) return Collections.emptyMap(); @@ -792,13 +775,19 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration { MetaData metadata = context.getMetaData(); metadata.resolve(context); - - Resource quickStartWebXml = _quickStartWebXml; - if (_quickStartWebXml == null) - quickStartWebXml = context.getWebInf().addPath("/quickstart-web.xml"); - try (FileOutputStream fos = new FileOutputStream(quickStartWebXml.getFile(), false)) + try (FileOutputStream fos = new FileOutputStream(_quickStartWebXml.getFile(), false)) { generateQuickStartWebXml(context, fos); + LOG.info("Generated {}", _quickStartWebXml); + if (context.getAttribute(WebInfConfiguration.TEMPORARY_RESOURCE_BASE) != null && !context.isPersistTempDirectory()) + LOG.warn("Generated to non persistent location: " + _quickStartWebXml); } } + + @Override + public void deconfigure(WebAppContext context) throws Exception + { + super.deconfigure(context); + } + } diff --git a/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartWebApp.java b/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartWebApp.java deleted file mode 100644 index 0cf95f8f1b5..00000000000 --- a/jetty-quickstart/src/main/java/org/eclipse/jetty/quickstart/QuickStartWebApp.java +++ /dev/null @@ -1,90 +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.quickstart; - -import org.eclipse.jetty.annotations.AnnotationConfiguration; -import org.eclipse.jetty.plus.webapp.EnvConfiguration; -import org.eclipse.jetty.plus.webapp.PlusConfiguration; -import org.eclipse.jetty.quickstart.QuickStartConfiguration.Mode; -import org.eclipse.jetty.webapp.WebAppContext; - -/** - * QuickStartWar - */ -public class QuickStartWebApp extends WebAppContext -{ - private final QuickStartConfiguration _quickStartConfiguration; - - private String _originAttribute; - private boolean _generateOrigin; - - public QuickStartWebApp() - { - super(); - addConfiguration( - _quickStartConfiguration = new QuickStartConfiguration(), - new EnvConfiguration(), - new PlusConfiguration(), - new AnnotationConfiguration()); - setExtractWAR(true); - setCopyWebDir(false); - setCopyWebInf(false); - } - - public void setOriginAttribute(String name) - { - setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, name); - } - - /** - * @return the originAttribute - */ - public String getOriginAttribute() - { - Object attr = getAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE); - return attr == null ? null : attr.toString(); - } - - /** - * @param generateOrigin the generateOrigin to set - */ - public void setGenerateOrigin(boolean generateOrigin) - { - setAttribute(QuickStartConfiguration.GENERATE_ORIGIN, generateOrigin); - } - - /** - * @return the generateOrigin - */ - public boolean isGenerateOrigin() - { - Object attr = getAttribute(QuickStartConfiguration.GENERATE_ORIGIN); - return attr == null ? false : Boolean.valueOf(attr.toString()); - } - - public Mode getMode() - { - return _quickStartConfiguration.getMode(); - } - - public void setMode(Mode mode) - { - _quickStartConfiguration.setMode(mode); - } -} diff --git a/jetty-quickstart/src/test/java/org/eclipse/jetty/quickstart/TestQuickStart.java b/jetty-quickstart/src/test/java/org/eclipse/jetty/quickstart/TestQuickStart.java index 6e7b3ed3834..82e0c376f8f 100644 --- a/jetty-quickstart/src/test/java/org/eclipse/jetty/quickstart/TestQuickStart.java +++ b/jetty-quickstart/src/test/java/org/eclipse/jetty/quickstart/TestQuickStart.java @@ -27,6 +27,7 @@ import org.eclipse.jetty.servlet.ListenerHolder; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.webapp.WebAppContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -61,10 +62,11 @@ public class TestQuickStart Server server = new Server(); //generate a quickstart-web.xml - QuickStartWebApp quickstart = new QuickStartWebApp(); + WebAppContext quickstart = new WebAppContext(); + quickstart.addConfiguration(new QuickStartConfiguration()); + quickstart.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.GENERATE); + quickstart.setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, "origin"); quickstart.setResourceBase(testDir.getAbsolutePath()); - quickstart.setMode(QuickStartConfiguration.Mode.GENERATE); - quickstart.setGenerateOrigin(true); ServletHolder fooHolder = new ServletHolder(); fooHolder.setServlet(new FooServlet()); fooHolder.setName("foo"); @@ -73,19 +75,22 @@ public class TestQuickStart lholder.setListener(new FooContextListener()); quickstart.getServletHandler().addListener(lholder); server.setHandler(quickstart); + server.setDryRun(true); server.start(); - server.stop(); assertTrue(quickstartXml.exists()); //now run the webapp again purely from the generated quickstart - QuickStartWebApp webapp = new QuickStartWebApp(); + WebAppContext webapp = new WebAppContext(); webapp.setResourceBase(testDir.getAbsolutePath()); - webapp.setMode(QuickStartConfiguration.Mode.QUICKSTART); + webapp.addConfiguration(new QuickStartConfiguration()); + webapp.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.QUICKSTART); webapp.setClassLoader(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader())); server.setHandler(webapp); + server.setDryRun(false); server.start(); + server.dumpStdErr(); //verify that FooServlet is now mapped to / and not the DefaultServlet ServletHolder sh = webapp.getServletHandler().getMappedServlet("/").getResource(); @@ -104,28 +109,30 @@ public class TestQuickStart Server server = new Server(); // generate a quickstart-web.xml - QuickStartWebApp quickstart = new QuickStartWebApp(); + WebAppContext quickstart = new WebAppContext(); quickstart.setResourceBase(testDir.getAbsolutePath()); - quickstart.setMode(QuickStartConfiguration.Mode.GENERATE); - quickstart.setGenerateOrigin(true); + quickstart.addConfiguration(new QuickStartConfiguration()); + quickstart.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.GENERATE); + quickstart.setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, "origin"); quickstart.setDescriptor(MavenTestingUtils.getTestResourceFile("web.xml").getAbsolutePath()); quickstart.setContextPath("/foo"); server.setHandler(quickstart); + server.setDryRun(true); server.start(); - assertEquals("/foo", quickstart.getContextPath()); assertFalse(quickstart.isContextPathDefault()); - server.stop(); assertTrue(quickstartXml.exists()); // quick start - QuickStartWebApp webapp = new QuickStartWebApp(); + WebAppContext webapp = new WebAppContext(); + webapp.addConfiguration(new QuickStartConfiguration()); + quickstart.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.QUICKSTART); webapp.setResourceBase(testDir.getAbsolutePath()); - webapp.setMode(QuickStartConfiguration.Mode.QUICKSTART); webapp.setClassLoader(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader())); server.setHandler(webapp); + server.setDryRun(false); server.start(); // verify the context path is the default-context-path diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java index 86abf843ef4..79d80dfaab2 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java @@ -22,6 +22,8 @@ import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.Name; /** diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRuleTest.java index 49136242fde..53d969875e1 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRuleTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRuleTest.java @@ -42,7 +42,7 @@ public class HeaderPatternRuleTest extends AbstractRuleTestCase public void testHeaderWithTextValues() throws IOException { // different keys - String headers[][] = { + String[][] headers = { {"hnum#1", "test1"}, {"hnum#2", "2test2"}, {"hnum#3", "test3"} @@ -53,7 +53,7 @@ public class HeaderPatternRuleTest extends AbstractRuleTestCase @Test public void testHeaderWithNumberValues() throws IOException { - String headers[][] = { + String[][] headers = { {"hello", "1"}, {"hello", "-1"}, {"hello", "100"}, @@ -70,7 +70,7 @@ public class HeaderPatternRuleTest extends AbstractRuleTestCase @Test public void testHeaderOverwriteValues() throws IOException { - String headers[][] = { + String[][] headers = { {"size", "100"}, {"size", "200"}, {"size", "300"}, @@ -100,7 +100,7 @@ public class HeaderPatternRuleTest extends AbstractRuleTestCase assertEquals("abba1", _response.getHeader("title1")); } - private void assertHeaders(String headers[][]) throws IOException + private void assertHeaders(String[][] headers) throws IOException { for (String[] header : headers) { diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRuleTest.java index 52851f5a92a..194646f6c6f 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRuleTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRuleTest.java @@ -43,7 +43,7 @@ public class HeaderRegexRuleTest extends AbstractRuleTestCase public void testHeaderWithTextValues() throws IOException { // different keys - String headers[][] = + String[][] headers = { {"hnum#1", "test1"}, {"hnum#2", "2test2"}, @@ -55,7 +55,7 @@ public class HeaderRegexRuleTest extends AbstractRuleTestCase @Test public void testHeaderWithNumberValues() throws IOException { - String headers[][] = + String[][] headers = { {"hello", "1"}, {"hello", "-1"}, @@ -72,7 +72,7 @@ public class HeaderRegexRuleTest extends AbstractRuleTestCase @Test public void testHeaderOverwriteValues() throws IOException { - String headers[][] = + String[][] headers = { {"size", "100"}, {"size", "200"}, @@ -122,7 +122,7 @@ public class HeaderRegexRuleTest extends AbstractRuleTestCase assertEquals(null, _response.getHeader("cache-control")); } - private void assertHeaders(String headers[][]) throws IOException + private void assertHeaders(String[][] headers) throws IOException { for (String[] header : headers) { 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 4977d6a5425..0f817b14e56 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 @@ -37,7 +37,7 @@ public class RewritePatternRuleTest extends AbstractRuleTestCase {"/foo/bar", "/foo/bar", "/replace"}, {"/foo/bar.txt", "*.txt", "/replace"}, {"/foo/bar/%20x", "/foo/*", "/replace/bar/%20x"}, - }; + }; private RewritePatternRule _rule; @BeforeEach diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java index 2eef7648bca..7afc9f9bd31 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/ValidUrlRuleTest.java @@ -120,7 +120,9 @@ public class ValidUrlRuleTest extends AbstractRuleTestCase // space assertTrue(_rule.isValidChar("\u0020".charAt(0))); // form feed + //@checkstyle-disable-check : IllegalTokenText assertFalse(_rule.isValidChar("\u000c".charAt(0))); + //@checkstyle-enable-check : IllegalTokenText } } 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 90dc198b4f3..dedac35bc32 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 @@ -43,7 +43,7 @@ Upon receiving a HTTP request, the server tries to authenticate the client + *
Upon receiving an HTTP request, the server tries to authenticate the client * calling {@link #login(String, Object, ServletRequest)} where the GSS APIs are used to * verify client tokens and (perhaps after a few round-trips) a {@code GSSContext} is * established.
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java index 26aede1f640..411594f32f1 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java @@ -814,7 +814,7 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr { //an exact method name if (!hasOmissions) - //a http-method does not have http-method-omission to cover the other method names + //an http-method does not have http-method-omission to cover the other method names uncoveredPaths.add(path); } } 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 eaa66a0d81a..8b99979b18b 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 @@ -39,7 +39,7 @@ import org.eclipse.jetty.util.log.Logger; * * 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. + * into an HttpSession to remember that the user is authenticated. */ public class SessionAuthentication extends AbstractUserAuthentication implements Serializable, HttpSessionActivationListener, HttpSessionBindingListener 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 9fdc629230d..84c81f922dd 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 @@ -423,7 +423,7 @@ public class ConstraintTest assertEquals(1, uncoveredPaths.size()); assertThat("/user/*", is(in(uncoveredPaths))); - //Test an explicitly named method with a http-method-omission to cover all other methods + //Test an explicitly named method with an http-method-omission to cover all other methods Constraint constraint2a = new Constraint(); constraint2a.setAuthenticate(true); constraint2a.setName("forbid constraint"); @@ -437,7 +437,7 @@ public class ConstraintTest assertNotNull(uncoveredPaths); assertEquals(0, uncoveredPaths.size()); - //Test a http-method-omission only + //Test an http-method-omission only Constraint constraint3 = new Constraint(); constraint3.setAuthenticate(true); constraint3.setName("omit constraint"); @@ -1708,7 +1708,7 @@ public class ConstraintTest { request.login("admin", "fail"); } - catch (ServletException se) + catch (ServletException e) { request.login("admin", "password"); } diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/SessionAuthenticationTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/SessionAuthenticationTest.java new file mode 100644 index 00000000000..a30d3337524 --- /dev/null +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/SessionAuthenticationTest.java @@ -0,0 +1,93 @@ +// +// ======================================================================== +// 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 java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.eclipse.jetty.security.authentication.SessionAuthentication; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.security.Password; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * SessionAuthenticationTest + * + */ +public class SessionAuthenticationTest +{ + /** + * Check that a SessionAuthenticator is serializable, and that + * the deserialized SessionAuthenticator contains the same authentication + * and authorization information. + */ + @Test + public void testSessionAuthenticationSerialization() + throws Exception + { + + ContextHandler contextHandler = new ContextHandler(); + SecurityHandler securityHandler = new ConstraintSecurityHandler(); + contextHandler.setHandler(securityHandler); + TestLoginService loginService = new TestLoginService("SessionAuthTest"); + Password pwd = new Password("foo"); + loginService.putUser("foo", pwd, new String[]{"boss", "worker"}); + securityHandler.setLoginService(loginService); + securityHandler.setAuthMethod("FORM"); + UserIdentity user = loginService.login("foo", pwd, null); + assertNotNull(user); + assertNotNull(user.getUserPrincipal()); + assertEquals("foo", user.getUserPrincipal().getName()); + SessionAuthentication sessionAuth = new SessionAuthentication("FORM", user, pwd); + assertTrue(sessionAuth.isUserInRole(null, "boss")); + contextHandler.handle(new Runnable() + { + public void run() + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(sessionAuth); + oos.close(); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())); + SessionAuthentication reactivatedSessionAuth = (SessionAuthentication)ois.readObject(); + assertNotNull(reactivatedSessionAuth); + assertNotNull(reactivatedSessionAuth.getUserIdentity()); + assertNotNull(reactivatedSessionAuth.getUserIdentity().getUserPrincipal()); + assertEquals("foo", reactivatedSessionAuth.getUserIdentity().getUserPrincipal().getName()); + assertNotNull(reactivatedSessionAuth.getUserIdentity().getSubject()); + assertTrue(reactivatedSessionAuth.isUserInRole(null, "boss")); + } + catch (Exception e) + { + fail(e); + } + } + }); + } +} 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 234646c807f..2a483729ed1 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 @@ -44,6 +44,7 @@ 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.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -322,7 +323,8 @@ public class SpecExampleConstraintTest response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + "Authorization: Basic " + encodedChris + "\r\n" + "\r\n"); - assertThat(response, startsWith("HTTP/1.1 403 ")); + assertThat(response, startsWith("HTTP/1.1 403 Forbidden")); + assertThat(response, containsString("!Secure")); //a user in role HOMEOWNER can do a GET response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" + diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java index 60f7fe510cd..25b6dbe6235 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java @@ -18,14 +18,17 @@ package org.eclipse.jetty.security.authentication; +import java.io.IOException; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpOutput; import org.eclipse.jetty.server.Request; @@ -34,6 +37,8 @@ import org.eclipse.jetty.server.Server; 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; public class SpnegoAuthenticatorTest @@ -49,27 +54,34 @@ public class SpnegoAuthenticatorTest @Test public void testChallengeSentWithNoAuthorization() throws Exception { - HttpChannel channel = new HttpChannel(null, new HttpConfiguration(), null, null) + HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public Server getServer() { return null; } - }; - Request req = new Request(channel, null); - HttpOutput out = new HttpOutput(channel) - { + @Override - public void close() + protected HttpOutput newHttpOutput() { + return new HttpOutput(this) + { + @Override + public void close() {} + + @Override + public void flush() throws IOException {} + }; } }; - Response res = new Response(channel, out); + Request req = channel.getRequest(); + Response res = channel.getResponse(); MetaData.Request metadata = new MetaData.Request(new HttpFields()); metadata.setURI(new HttpURI("http://localhost")); req.setMetaData(metadata); + assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); @@ -78,23 +90,29 @@ public class SpnegoAuthenticatorTest @Test public void testChallengeSentWithUnhandledAuthorization() throws Exception { - HttpChannel channel = new HttpChannel(null, new HttpConfiguration(), null, null) + HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public Server getServer() { return null; } - }; - Request req = new Request(channel, null); - HttpOutput out = new HttpOutput(channel) - { + @Override - public void close() + protected HttpOutput newHttpOutput() { + return new HttpOutput(this) + { + @Override + public void close() {} + + @Override + public void flush() throws IOException {} + }; } }; - Response res = new Response(channel, out); + Request req = channel.getRequest(); + Response res = channel.getResponse(); HttpFields http_fields = new HttpFields(); // Create a bogus Authorization header. We don't care about the actual credentials. http_fields.add(HttpHeader.AUTHORIZATION, "Basic asdf"); @@ -102,8 +120,34 @@ public class SpnegoAuthenticatorTest metadata.setURI(new HttpURI("http://localhost")); req.setMetaData(metadata); + assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); } + } + + class MockConnector extends AbstractConnector + { + public MockConnector() + { + super(new Server() , null, null, null, 0); + } + + @Override + protected void accept(int acceptorID) throws IOException, InterruptedException + { + } + + @Override + public Object getTransport() + { + return null; + } + + @Override + public String dumpSelf() + { + return null; + } } diff --git a/jetty-server/src/main/config/etc/jetty-http.xml b/jetty-server/src/main/config/etc/jetty-http.xml index ccf02559439..17aa2453587 100644 --- a/jetty-server/src/main/config/etc/jetty-http.xml +++ b/jetty-server/src/main/config/etc/jetty-http.xml @@ -3,13 +3,13 @@ - +- + diff --git a/jetty-server/src/main/config/etc/jetty-https.xml b/jetty-server/src/main/config/etc/jetty-https.xml index bbeaa5a6cc7..a71de579531 100644 --- a/jetty-server/src/main/config/etc/jetty-https.xml +++ b/jetty-server/src/main/config/etc/jetty-https.xml @@ -2,7 +2,7 @@ - + diff --git a/jetty-server/src/main/config/etc/jetty-ssl.xml b/jetty-server/src/main/config/etc/jetty-ssl.xml index 2401454bfce..e829d919b66 100644 --- a/jetty-server/src/main/config/etc/jetty-ssl.xml +++ b/jetty-server/src/main/config/etc/jetty-ssl.xml @@ -9,7 +9,7 @@ - + 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 c40486e363e..35d7bfb28f0 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 @@ -5,15 +5,16 @@ - + 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 84d26c24ef7..7de90393a52 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 @@ -10,8 +10,9 @@ - - - - + + + + + diff --git a/jetty-server/src/main/config/modules/http.mod b/jetty-server/src/main/config/modules/http.mod index e1f89b00dbe..a0b26e12663 100644 --- a/jetty-server/src/main/config/modules/http.mod +++ b/jetty-server/src/main/config/modules/http.mod @@ -1,7 +1,7 @@ DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html [description] -Enables a HTTP connector on the server. +Enables an HTTP connector on the server. By default HTTP/1 is support, but HTTP2C can be added to the connector with the http2c module. diff --git a/jetty-server/src/main/config/modules/session-cache-hash.mod b/jetty-server/src/main/config/modules/session-cache-hash.mod index 32ab705c7a2..2d336bc1d99 100644 --- a/jetty-server/src/main/config/modules/session-cache-hash.mod +++ b/jetty-server/src/main/config/modules/session-cache-hash.mod @@ -1,10 +1,9 @@ DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html [description] -Enable first level session cache in ConcurrentHashMap. -If not enabled, sessions will use a HashSessionCache by default, so enabling -via this module is only needed if the configuration properties need to be -changed. +Enable first level session cache. If this module is not enabled, sessions will +use the DefaultSessionCache by default, so enabling via this module is only needed +if the configuration properties need to be changed from their defaults. [tags] session @@ -23,3 +22,4 @@ etc/sessions/session-cache-hash.xml #jetty.session.saveOnInactiveEvict=false #jetty.session.saveOnCreate=false #jetty.session.removeUnloadableSessions=false +#jetty.session.flushOnResponseCommit=false diff --git a/jetty-server/src/main/config/modules/session-cache-null.mod b/jetty-server/src/main/config/modules/session-cache-null.mod index abdf2d7e076..2a94f59cb82 100644 --- a/jetty-server/src/main/config/modules/session-cache-null.mod +++ b/jetty-server/src/main/config/modules/session-cache-null.mod @@ -18,3 +18,4 @@ etc/sessions/session-cache-null.xml [ini-template] #jetty.session.saveOnCreate=false #jetty.session.removeUnloadableSessions=false +#jetty.session.flushOnResponseCommit=false 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 46c18701b7e..d076742d121 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 @@ -45,13 +45,14 @@ import org.eclipse.jetty.util.ProcessorUtils; 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.component.Container; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Graceful; 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.Locker; +import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; import org.eclipse.jetty.util.thread.Scheduler; import org.eclipse.jetty.util.thread.ThreadPoolBudget; @@ -115,7 +116,7 @@ import org.eclipse.jetty.util.thread.ThreadPoolBudget; * {@link ConnectionFactory}s may also create temporary {@link org.eclipse.jetty.io.Connection} instances that will exchange bytes * over the connection to determine what is the next protocol to use. For example the ALPN protocol is an extension * of SSL to allow a protocol to be specified during the SSL handshake. ALPN is used by the HTTP/2 protocol to - * negotiate the protocol that the client and server will speak. Thus to accept a HTTP/2 connection, the + * negotiate the protocol that the client and server will speak. Thus to accept an HTTP/2 connection, the * connector will be configured with {@link ConnectionFactory}s for "SSL-ALPN", "h2", "http/1.1" * with the default protocol being "SSL-ALPN". Thus a newly accepted connection uses "SSL-ALPN", which specifies a * SSLConnectionFactory with "ALPN" as the next protocol. Thus an SSL connection instance is created chained to an ALPN @@ -143,8 +144,8 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co { protected static final Logger LOG = Log.getLogger(AbstractConnector.class); - private final Locker _locker = new Locker(); - private final Condition _setAccepting = _locker.newCondition(); + private final AutoLock _lock = new AutoLock(); + private final Condition _setAccepting = _lock.newCondition(); private final Map - - + + + _factories = new LinkedHashMap<>(); // Order is important on server side, so we use a LinkedHashMap private final Server _server; private final Executor _executor; @@ -154,6 +155,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co private final Set _endpoints = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set _immutableEndPoints = Collections.unmodifiableSet(_endpoints); private final Graceful.Shutdown _shutdown = new Graceful.Shutdown(); + private HttpChannel.Listener _httpChannelListeners = HttpChannel.NOOP_LISTENER; private CountDownLatch _stopping; private long _idleTimeout = 30000; private String _defaultProtocol; @@ -188,6 +190,23 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co pool = _server.getBean(ByteBufferPool.class); _byteBufferPool = pool != null ? pool : new ArrayByteBufferPool(); + addEventListener(new Container.Listener() + { + @Override + public void beanAdded(Container parent, Object bean) + { + if (bean instanceof HttpChannel.Listener) + _httpChannelListeners = new HttpChannelListeners(getBeans(HttpChannel.Listener.class)); + } + + @Override + public void beanRemoved(Container parent, Object bean) + { + if (bean instanceof HttpChannel.Listener) + _httpChannelListeners = new HttpChannelListeners(getBeans(HttpChannel.Listener.class)); + } + }); + addBean(_server, false); addBean(_executor); if (executor == null) @@ -208,6 +227,24 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co _acceptors = new Thread[acceptors]; } + /** + * Get the {@link HttpChannel.Listener}s added to the connector + * as a single combined Listener. + * This is equivalent to a listener that iterates over the individual + * listeners returned from {@code getBeans(HttpChannel.Listener.class);}, + * except that: + *
+ * @see #getBeans(Class) + * @return An unmodifiable list of EventListener beans + */ + public HttpChannel.Listener getHttpChannelListeners() + { + return _httpChannelListeners; + } + @Override public Server getServer() { @@ -295,7 +332,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co protected void interruptAcceptors() { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { for (Thread thread : _acceptors) { @@ -350,7 +387,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co public void join(long timeout) throws InterruptedException { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { for (Thread thread : _acceptors) { @@ -367,7 +404,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co */ public boolean isAccepting() { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { return _accepting; } @@ -375,7 +412,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co public void setAccepting(boolean accepting) { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { _accepting = accepting; _setAccepting.signalAll(); @@ -385,7 +422,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co @Override public ConnectionFactory getConnectionFactory(String protocol) { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { return _factories.get(StringUtil.asciiToLowerCase(protocol)); } @@ -394,7 +431,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co @Override public- The result is precomputed, so it is more efficient
+ *- The result is ordered by the order added.
+ *- The result is immutable.
+ *T getConnectionFactory(Class factoryType) { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { for (ConnectionFactory f : _factories.values()) { @@ -646,7 +683,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co { while (isRunning()) { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { if (!_accepting && isRunning()) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java index 0cd93ff67be..52520ffd71f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java @@ -160,7 +160,7 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable Scheduler.Task task = _timeoutTask; _timeoutTask = null; if (task != null) - _state.getHttpChannel().execute(() -> _state.onTimeout()); + _state.timeout(); } public void addThrowable(Throwable e) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java index ffc6153bc12..0a1d87bc37b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java @@ -35,11 +35,11 @@ import org.eclipse.jetty.io.EndPoint; * A ConnectionFactory has a protocol name that represents the protocol of the Connections * created. Example of protocol names include: * - *
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index 9d8088b4ef1..9a474a30fc0 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -33,6 +33,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.DebugHandler; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.log.Log; @@ -42,8 +43,6 @@ public class Dispatcher implements RequestDispatcher { private static final Logger LOG = Log.getLogger(Dispatcher.class); - public static final String __ERROR_DISPATCH = "org.eclipse.jetty.server.Dispatcher.ERROR"; - /** * Dispatch include attribute names */ @@ -77,15 +76,7 @@ public class Dispatcher implements RequestDispatcher public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException { - try - { - request.setAttribute(__ERROR_DISPATCH, Boolean.TRUE); - forward(request, response, DispatcherType.ERROR); - } - finally - { - request.setAttribute(__ERROR_DISPATCH, null); - } + forward(request, response, DispatcherType.ERROR); } @Override 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 92e7a1a52fc..e11ea6f23e9 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 @@ -35,8 +35,6 @@ 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; @@ -63,8 +61,6 @@ import static java.lang.invoke.MethodType.methodType; */ public class ForwardedRequestCustomizer implements Customizer { - private static final Logger LOG = Log.getLogger(ForwardedRequestCustomizer.class); - private HostPortHttpField _forcedHost; private boolean _proxyAsAuthority = false; private boolean _forwardedPortAsAuthority = true; @@ -236,7 +232,7 @@ public class ForwardedRequestCustomizer implements Customizer public String getForwardedPortHeader() { - return _forwardedHostHeader; + return _forwardedPortHeader; } /** @@ -244,9 +240,9 @@ public class ForwardedRequestCustomizer implements Customizer */ public void setForwardedPortHeader(String forwardedPortHeader) { - if (_forwardedHostHeader == null || !_forwardedHostHeader.equalsIgnoreCase(forwardedPortHeader)) + if (_forwardedPortHeader == null || !_forwardedPortHeader.equalsIgnoreCase(forwardedPortHeader)) { - _forwardedHostHeader = forwardedPortHeader; + _forwardedPortHeader = forwardedPortHeader; updateHandles(); } } @@ -356,7 +352,7 @@ public class ForwardedRequestCustomizer implements Customizer } /** - * @return true if the presence of a SSL session or certificate header is sufficient + * @return true if the presence of an SSL session or certificate header is sufficient * to indicate a secure request (default is true) */ public boolean isSslIsSecure() @@ -365,7 +361,7 @@ public class ForwardedRequestCustomizer implements Customizer } /** - * @param sslIsSecure true if the presence of a SSL session or certificate header is sufficient + * @param sslIsSecure true if the presence of an SSL session or certificate header is sufficient * to indicate a secure request (default is true) */ public void setSslIsSecure(boolean sslIsSecure) @@ -456,32 +452,32 @@ public class ForwardedRequestCustomizer implements Customizer { int size = 0; MethodHandles.Lookup lookup = MethodHandles.lookup(); - MethodType type = methodType(Void.TYPE, HttpField.class); + // Loop to grow capacity of ArrayTrie for all headers while (true) { try { - size += 128; + size += 128; // experimented good baseline size _handles = new ArrayTrie<>(size); - if (_forwardedCipherSuiteHeader != null && !_handles.put(_forwardedCipherSuiteHeader, lookup.findVirtual(Forwarded.class, "handleCipherSuite", type))) + if (updateForwardedHandle(lookup, getForwardedCipherSuiteHeader(), "handleCipherSuite")) continue; - if (_forwardedSslSessionIdHeader != null && !_handles.put(_forwardedSslSessionIdHeader, lookup.findVirtual(Forwarded.class, "handleSslSessionId", type))) + if (updateForwardedHandle(lookup, getForwardedSslSessionIdHeader(), "handleSslSessionId")) continue; - if (_forwardedHeader != null && !_handles.put(_forwardedHeader, lookup.findVirtual(Forwarded.class, "handleRFC7239", type))) + if (updateForwardedHandle(lookup, getForwardedHeader(), "handleRFC7239")) continue; - if (_forwardedForHeader != null && !_handles.put(_forwardedForHeader, lookup.findVirtual(Forwarded.class, "handleFor", type))) + if (updateForwardedHandle(lookup, getForwardedForHeader(), "handleFor")) continue; - if (_forwardedPortHeader != null && !_handles.put(_forwardedPortHeader, lookup.findVirtual(Forwarded.class, "handlePort", type))) + if (updateForwardedHandle(lookup, getForwardedPortHeader(), "handlePort")) continue; - if (_forwardedHostHeader != null && !_handles.put(_forwardedHostHeader, lookup.findVirtual(Forwarded.class, "handleHost", type))) + if (updateForwardedHandle(lookup, getForwardedHostHeader(), "handleHost")) continue; - if (_forwardedProtoHeader != null && !_handles.put(_forwardedProtoHeader, lookup.findVirtual(Forwarded.class, "handleProto", type))) + if (updateForwardedHandle(lookup, getForwardedProtoHeader(), "handleProto")) continue; - if (_forwardedHttpsHeader != null && !_handles.put(_forwardedHttpsHeader, lookup.findVirtual(Forwarded.class, "handleHttps", type))) + if (updateForwardedHandle(lookup, getForwardedHttpsHeader(), "handleHttps")) continue; - if (_forwardedServerHeader != null && !_handles.put(_forwardedServerHeader, lookup.findVirtual(Forwarded.class, "handleServer", type))) + if (updateForwardedHandle(lookup, getForwardedServerHeader(), "handleServer")) continue; break; } @@ -492,6 +488,16 @@ public class ForwardedRequestCustomizer implements Customizer } } + private boolean updateForwardedHandle(MethodHandles.Lookup lookup, String headerName, String forwardedMethodName) throws NoSuchMethodException, IllegalAccessException + { + final MethodType type = methodType(void.class, HttpField.class); + + if (StringUtil.isBlank(headerName)) + return false; + + return !_handles.put(headerName, lookup.findVirtual(Forwarded.class, forwardedMethodName, type)); + } + private static class ForcedHostPort extends HostPort { ForcedHostPort(String authority) @@ -548,6 +554,7 @@ public class ForwardedRequestCustomizer implements Customizer _host = _forcedHost.getHostPort(); } + @SuppressWarnings("unused") public void handleCipherSuite(HttpField field) { _request.setAttribute("javax.servlet.request.cipher_suite", field.getValue()); @@ -558,6 +565,7 @@ public class ForwardedRequestCustomizer implements Customizer } } + @SuppressWarnings("unused") public void handleSslSessionId(HttpField field) { _request.setAttribute("javax.servlet.request.ssl_session_id", field.getValue()); @@ -570,7 +578,7 @@ public class ForwardedRequestCustomizer implements Customizer public void handleHost(HttpField field) { - if (_forwardedPortAsAuthority && !StringUtil.isEmpty(_forwardedPortHeader)) + if (getForwardedPortAsAuthority() && !StringUtil.isEmpty(getForwardedPortHeader())) { if (_host == null) _host = new PossiblyPartialHostPort(getLeftMost(field.getValue())); @@ -583,22 +591,25 @@ public class ForwardedRequestCustomizer implements Customizer } } + @SuppressWarnings("unused") public void handleServer(HttpField field) { - if (_proxyAsAuthority) + if (getProxyAsAuthority()) return; handleHost(field); } + @SuppressWarnings("unused") public void handleProto(HttpField field) { if (_proto == null) _proto = getLeftMost(field.getValue()); } + @SuppressWarnings("unused") public void handleFor(HttpField field) { - if (!_forwardedPortAsAuthority && !StringUtil.isEmpty(_forwardedPortHeader)) + if (!getForwardedPortAsAuthority() && !StringUtil.isEmpty(getForwardedPortHeader())) { if (_for == null) _for = new PossiblyPartialHostPort(getLeftMost(field.getValue())); @@ -611,30 +622,33 @@ public class ForwardedRequestCustomizer implements Customizer } } + @SuppressWarnings("unused") public void handlePort(HttpField field) { - if (!_forwardedPortAsAuthority) + if (!getForwardedPortAsAuthority()) { if (_for == null) - _for = new PortSetHostPort(_request.getRemoteHost(), field.getIntValue()); + _for = new PortSetHostPort(_request.getRemoteHost(), Integer.parseInt(getLeftMost(field.getValue()))); else if (_for instanceof PossiblyPartialHostPort && _for.getPort() <= 0) - _for = new HostPort(HostPort.normalizeHost(_for.getHost()), field.getIntValue()); + _for = new HostPort(HostPort.normalizeHost(_for.getHost()), Integer.parseInt(getLeftMost(field.getValue()))); } else { if (_host == null) - _host = new PortSetHostPort(_request.getServerName(), field.getIntValue()); + _host = new PortSetHostPort(_request.getServerName(), Integer.parseInt(getLeftMost(field.getValue()))); else if (_host instanceof PossiblyPartialHostPort && _host.getPort() <= 0) - _host = new HostPort(HostPort.normalizeHost(_host.getHost()), field.getIntValue()); + _host = new HostPort(HostPort.normalizeHost(_host.getHost()), Integer.parseInt(getLeftMost(field.getValue()))); } } + @SuppressWarnings("unused") public void handleHttps(HttpField field) { if (_proto == null && ("on".equalsIgnoreCase(field.getValue()) || "true".equalsIgnoreCase(field.getValue()))) _proto = HttpScheme.HTTPS.asString(); } + @SuppressWarnings("unused") public void handleRFC7239(HttpField field) { addValue(field.getValue()); @@ -650,11 +664,11 @@ public class ForwardedRequestCustomizer implements Customizer switch (name) { case "by": - if (!_proxyAsAuthority) + if (!getProxyAsAuthority()) break; if (value.startsWith("_") || "unknown".equals(value)) break; - if (_proxyAsAuthority && (_host == null || !(_host instanceof Rfc7239HostPort))) + if (_host == null || !(_host instanceof Rfc7239HostPort)) _host = new Rfc7239HostPort(value); break; case "for": 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 78bd72fa20d..82801769c1a 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 @@ -22,10 +22,10 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.EventListener; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -33,12 +33,12 @@ import java.util.function.Function; import java.util.function.Supplier; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; @@ -50,6 +50,7 @@ import org.eclipse.jetty.io.QuietException; import org.eclipse.jetty.server.HttpChannelState.Action; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.server.handler.ErrorHandler.ErrorPageMapper; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.SharedBlockingCallback.Blocker; @@ -59,7 +60,7 @@ import org.eclipse.jetty.util.thread.Scheduler; /** * HttpChannel represents a single endpoint for HTTP semantic processing. - * The HttpChannel is both a HttpParser.RequestHandler, where it passively receives events from + * The HttpChannel is both an HttpParser.RequestHandler, where it passively receives events from * an incoming HTTP request, and a Runnable, where it actively takes control of the request/response * life cycle and calls the application (perhaps suspending and resuming with multiple calls to run). * The HttpChannel signals the switch from passive mode to active mode by returning true to one of the @@ -68,10 +69,9 @@ import org.eclipse.jetty.util.thread.Scheduler; */ public class HttpChannel implements Runnable, HttpOutput.Interceptor { + public static Listener NOOP_LISTENER = new Listener(){}; private static final Logger LOG = Log.getLogger(HttpChannel.class); - private final AtomicBoolean _committed = new AtomicBoolean(); - private final AtomicBoolean _responseCompleted = new AtomicBoolean(); private final AtomicLong _requests = new AtomicLong(); private final Connector _connector; private final Executor _executor; @@ -81,9 +81,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private final HttpChannelState _state; private final Request _request; private final Response _response; + private final HttpChannel.Listener _combinedListener; + @Deprecated + private final List- http
- Creates a HTTP connection that can handle multiple versions of HTTP from 0.9 to 1.1
- *- h2
- Creates a HTTP/2 connection that handles the HTTP/2 protocol
+ *- http
- Creates an HTTP connection that can handle multiple versions of HTTP from 0.9 to 1.1
+ *- h2
- Creates an HTTP/2 connection that handles the HTTP/2 protocol
*- SSL-XYZ
- Create an SSL connection chained to a connection obtained from a connection factory * with a protocol "XYZ".
- *- SSL-http
- Create an SSL connection chained to a HTTP connection (aka https)
+ *- SSL-http
- Create an SSL connection chained to an HTTP connection (aka https)
*- SSL-ALPN
- Create an SSL connection chained to a ALPN connection, that uses a negotiation with * the client to determine the next protocol.
*_transientListeners = new ArrayList<>(); private HttpFields _trailers; private final Supplier _trailerSupplier = () -> _trailers; - private final List _listeners; private MetaData.Response _committedMetaData; private RequestLog _requestLog; private long _oldIdleTimeout; @@ -104,13 +106,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request = new Request(this, newHttpInput(_state)); _response = new Response(this, newHttpOutput()); - _executor = connector == null ? null : connector.getServer().getThreadPool(); - _requestLog = connector == null ? null : connector.getServer().getRequestLog(); - - List listeners = new ArrayList<>(); - if (connector != null) - listeners.addAll(connector.getBeans(Listener.class)); - _listeners = listeners; + _executor = connector.getServer().getThreadPool(); + _requestLog = connector.getServer().getRequestLog(); + _combinedListener = (connector instanceof AbstractConnector) + ? ((AbstractConnector)connector).getHttpChannelListeners() + : NOOP_LISTENER; if (LOG.isDebugEnabled()) LOG.debug("new {} -> {},{},{}", @@ -120,6 +120,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _state); } + public boolean isSendError() + { + return _state.isSendError(); + } + protected HttpInput newHttpInput(HttpChannelState state) { return new HttpInput(state); @@ -135,14 +140,32 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor return _state; } + /** + * Add a transient Listener to the HttpChannel. + * Listeners added by this method will only be notified + * if the HttpChannel has been constructed with an instance of + * {@link TransientListeners} as an {@link AbstractConnector} + * provided listener
+ *Transient listeners are removed after every request cycle
+ * @param listener + * @return true if the listener was added. + */ + @Deprecated public boolean addListener(Listener listener) { - return _listeners.add(listener); + return _transientListeners.add(listener); } + @Deprecated public boolean removeListener(Listener listener) { - return _listeners.remove(listener); + return _transientListeners.remove(listener); + } + + @Deprecated + public ListgetTransientListeners() + { + return _transientListeners; } public long getBytesWritten() @@ -277,8 +300,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void recycle() { - _committed.set(false); - _responseCompleted.set(false); _request.recycle(); _response.recycle(); _committedMetaData = null; @@ -286,6 +307,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _written = 0; _trailers = null; _oldIdleTimeout = 0; + _transientListeners.clear(); } public void onAsyncWaitForContent() @@ -313,7 +335,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean handle() { if (LOG.isDebugEnabled()) - LOG.debug("{} handle {} ", this, _request.getHttpURI()); + LOG.debug("handle {} {} ", _request.getHttpURI(), this); HttpChannelState.Action action = _state.handling(); @@ -327,19 +349,18 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor try { if (LOG.isDebugEnabled()) - LOG.debug("{} action {}", this, action); + LOG.debug("action {} {}", action, this); switch (action) { case TERMINATED: + onCompleted(); + break loop; + case WAIT: // break loop without calling unhandle break loop; - case NOOP: - // do nothing other than call unhandle - break; - case DISPATCH: { if (!_request.hasMetaData()) @@ -347,35 +368,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setHandled(false); _response.getHttpOutput().reopen(); - try + dispatch(DispatcherType.REQUEST, () -> { - _request.setDispatcherType(DispatcherType.REQUEST); - notifyBeforeDispatch(_request); - - List customizers = _configuration.getCustomizers(); - if (!customizers.isEmpty()) + for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers()) { - for (HttpConfiguration.Customizer customizer : customizers) - { - customizer.customize(getConnector(), _configuration, _request); - if (_request.isHandled()) - break; - } + customizer.customize(getConnector(), _configuration, _request); + if (_request.isHandled()) + return; } + getServer().handle(HttpChannel.this); + }); - if (!_request.isHandled()) - getServer().handle(this); - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } break; } @@ -384,70 +387,70 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setHandled(false); _response.getHttpOutput().reopen(); - try - { - _request.setDispatcherType(DispatcherType.ASYNC); - notifyBeforeDispatch(_request); - getServer().handleAsync(this); - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } + dispatch(DispatcherType.ASYNC,() -> getServer().handleAsync(this)); break; } - case ERROR_DISPATCH: + case ASYNC_TIMEOUT: + _state.onTimeout(); + break; + + case SEND_ERROR: { try { - _response.reset(true); - Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500; - _response.setStatus(code); - _request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); + // Get ready to send an error response _request.setHandled(false); + _response.resetContent(); _response.getHttpOutput().reopen(); - try + // the following is needed as you cannot trust the response code and reason + // as those could have been modified after calling sendError + Integer code = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + _response.setStatus(code != null ? code : HttpStatus.INTERNAL_SERVER_ERROR_500); + + ContextHandler.Context context = (ContextHandler.Context)_request.getAttribute(ErrorHandler.ERROR_CONTEXT); + ErrorHandler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler()); + + // If we can't have a body, then create a minimal error response. + if (HttpStatus.hasNoBody(_response.getStatus()) || errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod())) { - _request.setDispatcherType(DispatcherType.ERROR); - notifyBeforeDispatch(_request); - getServer().handle(this); + sendResponseAndComplete(); + break; } - catch (Throwable x) + + // Look for an error page dispatcher + String errorPage = (errorHandler instanceof ErrorPageMapper) ? ((ErrorPageMapper)errorHandler).getErrorPage(_request) : null; + Dispatcher errorDispatcher = errorPage != null ? (Dispatcher)context.getRequestDispatcher(errorPage) : null; + if (errorDispatcher == null) { - notifyDispatchFailure(_request, x); - throw x; + // Allow ErrorHandler to generate response + errorHandler.handle(null, _request, _request, _response); + _request.setHandled(true); } - finally + else { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); + // Do the error page dispatch + dispatch(DispatcherType.ERROR,() -> errorDispatcher.error(_request, _response)); } } catch (Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("Could not perform ERROR dispatch, aborting", x); - Throwable failure = (Throwable)_request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); - if (failure == null) - { - minimalErrorResponse(x); - } + if (_state.isResponseCommitted()) + abort(x); else { - if (x != failure) - failure.addSuppressed(x); - minimalErrorResponse(failure); + _response.resetContent(); + sendResponseAndComplete(); } } + finally + { + // clean up the context that was set in Response.sendError + _request.removeAttribute(ErrorHandler.ERROR_CONTEXT); + } break; } @@ -456,6 +459,12 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor throw _state.getAsyncContextEvent().getThrowable(); } + case READ_REGISTER: + { + onAsyncWaitForContent(); + break; + } + case READ_PRODUCE: { _request.getHttpInput().asyncReadProduce(); @@ -484,41 +493,36 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor case COMPLETE: { - try + if (!_response.isCommitted() && !_request.isHandled() && !_response.getHttpOutput().isClosed()) { - if (!_response.isCommitted() && !_request.isHandled()) - { - _response.sendError(HttpStatus.NOT_FOUND_404); - } - else - { - // RFC 7230, section 3.3. - int status = _response.getStatus(); - boolean hasContent = !(_request.isHead() || - HttpMethod.CONNECT.is(_request.getMethod()) && status == HttpStatus.OK_200 || - HttpStatus.isInformational(status) || - status == HttpStatus.NO_CONTENT_204 || - status == HttpStatus.NOT_MODIFIED_304); - if (hasContent && !_response.isContentComplete(_response.getHttpOutput().getWritten())) - sendErrorOrAbort("Insufficient content written"); - } - checkAndPrepareUpgrade(); - _response.closeOutput(); - } - finally - { - _request.setHandled(true); - _state.onComplete(); - onCompleted(); + _response.sendError(HttpStatus.NOT_FOUND_404); + break; } - break loop; + // RFC 7230, section 3.3. + if (!_request.isHead() && !_response.isContentComplete(_response.getHttpOutput().getWritten())) + { + if (sendErrorOrAbort("Insufficient content written")) + break; + } + + // Check if an update is done (if so, do not close) + if (checkAndPrepareUpgrade()) + break; + + // TODO Currently a blocking/aborting consumeAll is done in the handling of the TERMINATED + // TODO Action triggered by the completed callback below. It would be possible to modify the + // TODO callback to do a non-blocking consumeAll at this point and only call completed when + // TODO that is done. + + // Set a close callback on the HttpOutput to make it an async callback + _response.closeOutput(Callback.from(_state::completed)); + + break; } default: - { - throw new IllegalStateException("state=" + _state); - } + throw new IllegalStateException(this.toString()); } } catch (Throwable failure) @@ -533,42 +537,50 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor } if (LOG.isDebugEnabled()) - LOG.debug("{} handle exit, result {}", this, action); + LOG.debug("!handle {} {}", action, this); boolean suspended = action == Action.WAIT; return !suspended; } - public void sendErrorOrAbort(String message) + public boolean sendErrorOrAbort(String message) { try { if (isCommitted()) + { abort(new IOException(message)); - else - _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); + return false; + } + + _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); + return true; } catch (Throwable x) { LOG.ignore(x); abort(x); } + return false; } - protected void sendError(int code, String reason) + private void dispatch(DispatcherType type, Dispatchable dispatchable) throws IOException, ServletException { try { - _response.sendError(code, reason); + _request.setDispatcherType(type); + _combinedListener.onBeforeDispatch(_request); + dispatchable.dispatch(); } catch (Throwable x) { - if (LOG.isDebugEnabled()) - LOG.debug("Could not send error " + code + " " + reason, x); + _combinedListener.onDispatchFailure(_request, x); + throw x; } finally { - _state.errorComplete(); + _combinedListener.onAfterDispatch(_request); + _request.setDispatcherType(null); } } @@ -596,27 +608,19 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { // No stack trace unless there is debug turned on if (LOG.isDebugEnabled()) - LOG.debug(_request.getRequestURI(), failure); + LOG.warn("handleException " + _request.getRequestURI(), failure); else - LOG.warn("{} {}", _request.getRequestURI(), noStack.toString()); + LOG.warn("handleException {} {}", _request.getRequestURI(), noStack.toString()); } else { LOG.warn(_request.getRequestURI(), failure); } - try - { + if (isCommitted()) + abort(failure); + else _state.onError(failure); - } - catch (Throwable e) - { - if (e != failure) - failure.addSuppressed(e); - LOG.warn("ERROR dispatch failed", failure); - // Try to send a minimal response. - minimalErrorResponse(failure); - } } /** @@ -640,32 +644,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor return null; } - private void minimalErrorResponse(Throwable failure) + public void sendResponseAndComplete() { try { - int code = 500; - Integer status = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - if (status != null) - { - code = status.intValue(); - } - else - { - Throwable cause = unwrap(failure, BadMessageException.class); - if (cause instanceof BadMessageException) - code = ((BadMessageException)cause).getCode(); - } - - _response.reset(true); - _response.setStatus(code); - _response.flushBuffer(); + _request.setHandled(true); + _state.completing(); + sendResponse(null, _response.getHttpOutput().getBuffer(), true, Callback.from(_state::completed)); } catch (Throwable x) { - if (x != failure) - failure.addSuppressed(x); - abort(failure); + abort(x); } } @@ -683,11 +672,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public String toString() { long timeStamp = _request.getTimeStamp(); - return String.format("%s@%x{r=%s,c=%b,c=%b/%b,a=%s,uri=%s,age=%d}", + return String.format("%s@%x{s=%s,r=%s,c=%b/%b,a=%s,uri=%s,age=%d}", getClass().getSimpleName(), hashCode(), + _state, _requests, - _committed.get(), isRequestCompleted(), isResponseCompleted(), _state.getState(), @@ -713,7 +702,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setSecure(HttpScheme.HTTPS.is(request.getURI().getScheme())); - notifyRequestBegin(_request); + _combinedListener.onRequestBegin(_request); if (LOG.isDebugEnabled()) LOG.debug("REQUEST for {} on {}{}{} {} {}{}{}", request.getURIString(), this, System.lineSeparator(), @@ -724,33 +713,33 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean onContent(HttpInput.Content content) { if (LOG.isDebugEnabled()) - LOG.debug("{} onContent {}", this, content); - notifyRequestContent(_request, content.getByteBuffer()); + LOG.debug("onContent {} {}", this, content); + _combinedListener.onRequestContent(_request, content.getByteBuffer()); return _request.getHttpInput().addContent(content); } public boolean onContentComplete() { if (LOG.isDebugEnabled()) - LOG.debug("{} onContentComplete", this); - notifyRequestContentEnd(_request); + LOG.debug("onContentComplete {}", this); + _combinedListener.onRequestContentEnd(_request); return false; } public void onTrailers(HttpFields trailers) { if (LOG.isDebugEnabled()) - LOG.debug("{} onTrailers {}", this, trailers); + LOG.debug("onTrailers {} {}", this, trailers); _trailers = trailers; - notifyRequestTrailers(_request); + _combinedListener.onRequestTrailers(_request); } public boolean onRequestComplete() { if (LOG.isDebugEnabled()) - LOG.debug("{} onRequestComplete", this); + LOG.debug("onRequestComplete {}", this); boolean result = _request.getHttpInput().eof(); - notifyRequestEnd(_request); + _combinedListener.onRequestEnd(_request); return result; } @@ -760,15 +749,18 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * response is sent back to the client. * This avoids a race where the server is unprepared if the client sends * data immediately after having received the upgrade response.
+ * @return true if the channel is not complete and more processing is required, + * typically because sendError has been called. */ - protected void checkAndPrepareUpgrade() + protected boolean checkAndPrepareUpgrade() { + return false; } public void onCompleted() { if (LOG.isDebugEnabled()) - LOG.debug("COMPLETE for {} written={}", getRequest().getRequestURI(), getBytesWritten()); + LOG.debug("onCompleted for {} written={}", getRequest().getRequestURI(), getBytesWritten()); if (_requestLog != null) _requestLog.log(_request, _response); @@ -777,8 +769,8 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor if (idleTO >= 0 && getIdleTimeout() != _oldIdleTimeout) setIdleTimeout(_oldIdleTimeout); - notifyComplete(_request); - + _request.onCompleted(); + _combinedListener.onComplete(_request); _transport.onCompleted(); } @@ -790,11 +782,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void onBadMessage(BadMessageException failure) { int status = failure.getCode(); - String message = failure.getReason(); - if (status < 400 || status > 599) - failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, message, failure); + String reason = failure.getReason(); + if (status < HttpStatus.BAD_REQUEST_400 || status > 599) + failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, reason, failure); - notifyRequestFailure(_request, failure); + _combinedListener.onRequestFailure(_request, failure); Action action; try @@ -818,7 +810,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor ErrorHandler handler = getServer().getBean(ErrorHandler.class); if (handler != null) - content = handler.badMessageError(status, message, fields); + content = handler.badMessageError(status, reason, fields); sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1, status, null, fields, BufferUtil.length(content)), content, true); } @@ -843,7 +835,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor protected boolean sendResponse(MetaData.Response response, ByteBuffer content, boolean complete, final Callback callback) { - boolean committing = _committed.compareAndSet(false, true); + boolean committing = _state.commitResponse(); if (LOG.isDebugEnabled()) LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}", @@ -859,12 +851,14 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor if (response == null) response = _response.newResponseMetaData(); commit(response); - - // Wrap the callback to process 1xx responses. - Callback committed = HttpStatus.isInformational(response.getStatus()) - ? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); - - notifyResponseBegin(_request); + _combinedListener.onResponseBegin(_request); + _request.onResponseCommit(); + + // wrap callback to process 100 responses + final int status = response.getStatus(); + final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100) + ? new Send100Callback(callback) + : new SendCallback(callback, content, true, complete); // committing write _transport.send(_request.getMetaData(), response, content, complete, committed); @@ -909,7 +903,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean isCommitted() { - return _committed.get(); + return _state.isResponseCommitted(); } /** @@ -925,7 +919,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor */ public boolean isResponseCompleted() { - return _responseCompleted.get(); + return _state.isResponseCompleted(); } public boolean isPersistent() @@ -985,8 +979,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor */ public void abort(Throwable failure) { - notifyResponseFailure(_request, failure); - _transport.abort(failure); + if (_state.abortResponse()) + { + _combinedListener.onResponseFailure(_request, failure); + _transport.abort(failure); + } } public boolean isTunnellingSupported() @@ -999,84 +996,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor throw new UnsupportedOperationException("Tunnelling not supported"); } - private void notifyRequestBegin(Request request) - { - notifyEvent1(listener -> listener::onRequestBegin, request); - } - - private void notifyBeforeDispatch(Request request) - { - notifyEvent1(listener -> listener::onBeforeDispatch, request); - } - - private void notifyDispatchFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onDispatchFailure, request, failure); - } - - private void notifyAfterDispatch(Request request) - { - notifyEvent1(listener -> listener::onAfterDispatch, request); - } - - private void notifyRequestContent(Request request, ByteBuffer content) - { - notifyEvent2(listener -> listener::onRequestContent, request, content); - } - - private void notifyRequestContentEnd(Request request) - { - notifyEvent1(listener -> listener::onRequestContentEnd, request); - } - - private void notifyRequestTrailers(Request request) - { - notifyEvent1(listener -> listener::onRequestTrailers, request); - } - - private void notifyRequestEnd(Request request) - { - notifyEvent1(listener -> listener::onRequestEnd, request); - } - - private void notifyRequestFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onRequestFailure, request, failure); - } - - private void notifyResponseBegin(Request request) - { - notifyEvent1(listener -> listener::onResponseBegin, request); - } - - private void notifyResponseCommit(Request request) - { - notifyEvent1(listener -> listener::onResponseCommit, request); - } - - private void notifyResponseContent(Request request, ByteBuffer content) - { - notifyEvent2(listener -> listener::onResponseContent, request, content); - } - - private void notifyResponseEnd(Request request) - { - notifyEvent1(listener -> listener::onResponseEnd, request); - } - - private void notifyResponseFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onResponseFailure, request, failure); - } - - private void notifyComplete(Request request) - { - notifyEvent1(listener -> listener::onComplete, request); - } - private void notifyEvent1(Function> function, Request request) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { try { @@ -1091,7 +1013,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private void notifyEvent2(Function > function, Request request, ByteBuffer content) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { ByteBuffer view = content.slice(); try @@ -1107,7 +1029,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private void notifyEvent2(Function > function, Request request, Throwable failure) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { try { @@ -1120,10 +1042,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor } } + interface Dispatchable + { + void dispatch() throws IOException, ServletException; + } + /** * Listener for {@link HttpChannel} events.
*HttpChannel will emit events for the various phases it goes through while - * processing a HTTP request and response.
+ * processing an HTTP request and response. *Implementations of this interface may listen to those events to track * timing and/or other values such as request URI, etc.
*The events parameters, especially the {@link Request} object, may be @@ -1138,15 +1065,20 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor *
Listener methods are invoked synchronously from the thread that is * performing the request processing, and they should not call blocking code * (otherwise the request processing will be blocked as well).
+ *Listener instances that are set as a bean on the {@link Connector} are + * efficiently added to {@link HttpChannel}. If additional listeners are added + * using the deprecated {@link HttpChannel#addListener(Listener)}
method, + * then an instance of {@link TransientListeners} must be added to the connector + * in order for them to be invoked. */ - public interface Listener + public interface Listener extends EventListener { /** * Invoked just after the HTTP request line and headers have been parsed. * * @param request the request object */ - public default void onRequestBegin(Request request) + default void onRequestBegin(Request request) { } @@ -1155,7 +1087,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onBeforeDispatch(Request request) + default void onBeforeDispatch(Request request) { } @@ -1165,7 +1097,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the exception thrown by the application */ - public default void onDispatchFailure(Request request, Throwable failure) + default void onDispatchFailure(Request request, Throwable failure) { } @@ -1174,7 +1106,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onAfterDispatch(Request request) + default void onAfterDispatch(Request request) { } @@ -1185,7 +1117,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param content a {@link ByteBuffer#slice() slice} of the request content chunk */ - public default void onRequestContent(Request request, ByteBuffer content) + default void onRequestContent(Request request, ByteBuffer content) { } @@ -1194,7 +1126,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestContentEnd(Request request) + default void onRequestContentEnd(Request request) { } @@ -1203,7 +1135,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestTrailers(Request request) + default void onRequestTrailers(Request request) { } @@ -1212,7 +1144,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestEnd(Request request) + default void onRequestEnd(Request request) { } @@ -1222,7 +1154,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the request failure */ - public default void onRequestFailure(Request request, Throwable failure) + default void onRequestFailure(Request request, Throwable failure) { } @@ -1231,7 +1163,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseBegin(Request request) + default void onResponseBegin(Request request) { } @@ -1242,7 +1174,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseCommit(Request request) + default void onResponseCommit(Request request) { } @@ -1252,7 +1184,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param content a {@link ByteBuffer#slice() slice} of the response content chunk */ - public default void onResponseContent(Request request, ByteBuffer content) + default void onResponseContent(Request request, ByteBuffer content) { } @@ -1261,7 +1193,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseEnd(Request request) + default void onResponseEnd(Request request) { } @@ -1271,7 +1203,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the response failure */ - public default void onResponseFailure(Request request, Throwable failure) + default void onResponseFailure(Request request, Throwable failure) { } @@ -1280,7 +1212,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onComplete(Request request) + default void onComplete(Request request) { } } @@ -1305,16 +1237,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void succeeded() { _written += _length; + if (_complete) + _response.getHttpOutput().closed(); super.succeeded(); if (_commit) - notifyResponseCommit(_request); + _combinedListener.onResponseCommit(_request); if (_length > 0) - notifyResponseContent(_request, _content); - if (_complete) - { - _responseCompleted.set(true); - notifyResponseEnd(_request); - } + _combinedListener.onResponseContent(_request, _content); + if (_complete && _state.completeResponse()) + _combinedListener.onResponseEnd(_request); } @Override @@ -1330,13 +1261,14 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor @Override public void succeeded() { - super.failed(x); _response.getHttpOutput().closed(); + super.failed(x); } @Override public void failed(Throwable th) { + _response.getHttpOutput().closed(); abort(x); super.failed(x); } @@ -1360,10 +1292,108 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor @Override public void succeeded() { - if (_committed.compareAndSet(true, false)) + if (_state.partialResponse()) super.succeeded(); else super.failed(new IllegalStateException()); } } + + /** + * A Listener instance that can be added as a bean to {@link AbstractConnector} so that + * the listeners obtained from HttpChannel{@link #getTransientListeners()} + */ + @Deprecated + public static class TransientListeners implements Listener + { + @Override + public void onRequestBegin(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestBegin, request); + } + + @Override + public void onBeforeDispatch(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onBeforeDispatch, request); + } + + @Override + public void onDispatchFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onDispatchFailure, request, failure); + } + + @Override + public void onAfterDispatch(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onAfterDispatch, request); + } + + @Override + public void onRequestContent(Request request, ByteBuffer content) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onRequestContent, request, content); + } + + @Override + public void onRequestContentEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestContentEnd, request); + } + + @Override + public void onRequestTrailers(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestTrailers, request); + } + + @Override + public void onRequestEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestEnd, request); + } + + @Override + public void onRequestFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onRequestFailure, request, failure); + } + + @Override + public void onResponseBegin(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseBegin, request); + } + + @Override + public void onResponseCommit(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseCommit, request); + } + + @Override + public void onResponseContent(Request request, ByteBuffer content) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onResponseContent, request, content); + } + + @Override + public void onResponseEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseEnd, request); + } + + @Override + public void onResponseFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onResponseFailure, request, failure); + } + + @Override + public void onComplete(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onComplete, request); + } + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java new file mode 100644 index 00000000000..281c7f5eb04 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java @@ -0,0 +1,286 @@ +// +// ======================================================================== +// 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; + +import java.nio.ByteBuffer; +import java.util.Collection; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * A {@link HttpChannel.Listener} that holds a collection of + * other {@link HttpChannel.Listener} instances that are efficiently + * invoked without iteration. + * @see AbstractConnector + */ +public class HttpChannelListeners implements HttpChannel.Listener +{ + static final Logger LOG = Log.getLogger(HttpChannel.class); + public static HttpChannel.Listener NOOP = new HttpChannel.Listener() {}; + + private final NotifyRequest onRequestBegin; + private final NotifyRequest onBeforeDispatch; + private final NotifyFailure onDispatchFailure; + private final NotifyRequest onAfterDispatch; + private final NotifyContent onRequestContent; + private final NotifyRequest onRequestContentEnd; + private final NotifyRequest onRequestTrailers; + private final NotifyRequest onRequestEnd; + private final NotifyFailure onRequestFailure; + private final NotifyRequest onResponseBegin; + private final NotifyRequest onResponseCommit; + private final NotifyContent onResponseContent; + private final NotifyRequest onResponseEnd; + private final NotifyFailure onResponseFailure; + private final NotifyRequest onComplete; + + public HttpChannelListeners(Collectionlisteners) + { + try + { + NotifyRequest onRequestBegin = NotifyRequest.NOOP; + NotifyRequest onBeforeDispatch = NotifyRequest.NOOP; + NotifyFailure onDispatchFailure = NotifyFailure.NOOP; + NotifyRequest onAfterDispatch = NotifyRequest.NOOP; + NotifyContent onRequestContent = NotifyContent.NOOP; + NotifyRequest onRequestContentEnd = NotifyRequest.NOOP; + NotifyRequest onRequestTrailers = NotifyRequest.NOOP; + NotifyRequest onRequestEnd = NotifyRequest.NOOP; + NotifyFailure onRequestFailure = NotifyFailure.NOOP; + NotifyRequest onResponseBegin = NotifyRequest.NOOP; + NotifyRequest onResponseCommit = NotifyRequest.NOOP; + NotifyContent onResponseContent = NotifyContent.NOOP; + NotifyRequest onResponseEnd = NotifyRequest.NOOP; + NotifyFailure onResponseFailure = NotifyFailure.NOOP; + NotifyRequest onComplete = NotifyRequest.NOOP; + + for (HttpChannel.Listener listener : listeners) + { + if (!listener.getClass().getMethod("onRequestBegin", Request.class).isDefault()) + onRequestBegin = combine(onRequestBegin, listener::onRequestBegin); + if (!listener.getClass().getMethod("onBeforeDispatch", Request.class).isDefault()) + onBeforeDispatch = combine(onBeforeDispatch, listener::onBeforeDispatch); + if (!listener.getClass().getMethod("onDispatchFailure", Request.class, Throwable.class).isDefault()) + onDispatchFailure = combine(onDispatchFailure, listener::onDispatchFailure); + if (!listener.getClass().getMethod("onAfterDispatch", Request.class).isDefault()) + onAfterDispatch = combine(onAfterDispatch, listener::onAfterDispatch); + if (!listener.getClass().getMethod("onRequestContent", Request.class, ByteBuffer.class).isDefault()) + onRequestContent = combine(onRequestContent, listener::onRequestContent); + if (!listener.getClass().getMethod("onRequestContentEnd", Request.class).isDefault()) + onRequestContentEnd = combine(onRequestContentEnd, listener::onRequestContentEnd); + if (!listener.getClass().getMethod("onRequestTrailers", Request.class).isDefault()) + onRequestTrailers = combine(onRequestTrailers, listener::onRequestTrailers); + if (!listener.getClass().getMethod("onRequestEnd", Request.class).isDefault()) + onRequestEnd = combine(onRequestEnd, listener::onRequestEnd); + if (!listener.getClass().getMethod("onRequestFailure", Request.class, Throwable.class).isDefault()) + onRequestFailure = combine(onRequestFailure, listener::onRequestFailure); + if (!listener.getClass().getMethod("onResponseBegin", Request.class).isDefault()) + onResponseBegin = combine(onResponseBegin, listener::onResponseBegin); + if (!listener.getClass().getMethod("onResponseCommit", Request.class).isDefault()) + onResponseCommit = combine(onResponseCommit, listener::onResponseCommit); + if (!listener.getClass().getMethod("onResponseContent", Request.class, ByteBuffer.class).isDefault()) + onResponseContent = combine(onResponseContent, listener::onResponseContent); + if (!listener.getClass().getMethod("onResponseEnd", Request.class).isDefault()) + onResponseEnd = combine(onResponseEnd, listener::onResponseEnd); + if (!listener.getClass().getMethod("onResponseFailure", Request.class, Throwable.class).isDefault()) + onResponseFailure = combine(onResponseFailure, listener::onResponseFailure); + if (!listener.getClass().getMethod("onComplete", Request.class).isDefault()) + onComplete = combine(onComplete, listener::onComplete); + } + + this.onRequestBegin = onRequestBegin; + this.onBeforeDispatch = onBeforeDispatch; + this.onDispatchFailure = onDispatchFailure; + this.onAfterDispatch = onAfterDispatch; + this.onRequestContent = onRequestContent; + this.onRequestContentEnd = onRequestContentEnd; + this.onRequestTrailers = onRequestTrailers; + this.onRequestEnd = onRequestEnd; + this.onRequestFailure = onRequestFailure; + this.onResponseBegin = onResponseBegin; + this.onResponseCommit = onResponseCommit; + this.onResponseContent = onResponseContent; + this.onResponseEnd = onResponseEnd; + this.onResponseFailure = onResponseFailure; + this.onComplete = onComplete; + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + @Override + public void onRequestBegin(Request request) + { + onRequestBegin.onRequest(request); + } + + @Override + public void onBeforeDispatch(Request request) + { + onBeforeDispatch.onRequest(request); + } + + @Override + public void onDispatchFailure(Request request, Throwable failure) + { + onDispatchFailure.onFailure(request, failure); + } + + @Override + public void onAfterDispatch(Request request) + { + onAfterDispatch.onRequest(request); + } + + @Override + public void onRequestContent(Request request, ByteBuffer content) + { + onRequestContent.onContent(request, content); + } + + @Override + public void onRequestContentEnd(Request request) + { + onRequestContentEnd.onRequest(request); + } + + @Override + public void onRequestTrailers(Request request) + { + onRequestTrailers.onRequest(request); + } + + @Override + public void onRequestEnd(Request request) + { + onRequestEnd.onRequest(request); + } + + @Override + public void onRequestFailure(Request request, Throwable failure) + { + onRequestFailure.onFailure(request, failure); + } + + @Override + public void onResponseBegin(Request request) + { + onResponseBegin.onRequest(request); + } + + @Override + public void onResponseCommit(Request request) + { + onResponseCommit.onRequest(request); + } + + @Override + public void onResponseContent(Request request, ByteBuffer content) + { + onResponseContent.onContent(request, content); + } + + @Override + public void onResponseEnd(Request request) + { + onResponseEnd.onRequest(request); + } + + @Override + public void onResponseFailure(Request request, Throwable failure) + { + onResponseFailure.onFailure(request, failure); + } + + @Override + public void onComplete(Request request) + { + onComplete.onRequest(request); + } + + private interface NotifyRequest + { + void onRequest(Request request); + + NotifyRequest NOOP = request -> + { + }; + } + + private interface NotifyFailure + { + void onFailure(Request request, Throwable failure); + + NotifyFailure NOOP = (request, failure) -> + { + }; + } + + private interface NotifyContent + { + void onContent(Request request, ByteBuffer content); + + NotifyContent NOOP = (request, content) -> + { + }; + } + + private static NotifyRequest combine(NotifyRequest first, NotifyRequest second) + { + if (first == NotifyRequest.NOOP) + return second; + if (second == NotifyRequest.NOOP) + return first; + return request -> + { + first.onRequest(request); + second.onRequest(request); + }; + } + + private static NotifyFailure combine(NotifyFailure first, NotifyFailure second) + { + if (first == NotifyFailure.NOOP) + return second; + if (second == NotifyFailure.NOOP) + return first; + return (request, throwable) -> + { + first.onFailure(request, throwable); + second.onFailure(request, throwable); + }; + } + + private static NotifyContent combine(NotifyContent first, NotifyContent second) + { + if (first == NotifyContent.NOOP) + return (request, content) -> second.onContent(request, content.slice()); + if (second == NotifyContent.NOOP) + return (request, content) -> first.onContent(request, content.slice()); + return (request, content) -> + { + content = content.slice(); + first.onContent(request, content); + second.onContent(request, content); + }; + } +} 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 61d87a3b79f..7fd7799954f 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 @@ -44,7 +44,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** - * A HttpChannel customized to be transported over the HTTP/1 protocol + * An HttpChannel customized to be transported over the HTTP/1 protocol */ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandler, ComplianceViolation.Listener { @@ -414,7 +414,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque } /** - * Attempts to perform a HTTP/1.1 upgrade.
+ *Attempts to perform an HTTP/1.1 upgrade.
*The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector * matching the protocol specified in the {@code Upgrade} header.
*The upgrade may succeed, be ignored (which can allow a later handler to implement) 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 75e37a8763a..e55f1878498 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 @@ -21,9 +21,7 @@ package org.eclipse.jetty.server; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import javax.servlet.AsyncListener; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletResponse; import javax.servlet.UnavailableException; @@ -32,14 +30,16 @@ import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler.Context; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Locker; import org.eclipse.jetty.util.thread.Scheduler; import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION; import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION_TYPE; import static javax.servlet.RequestDispatcher.ERROR_MESSAGE; +import static javax.servlet.RequestDispatcher.ERROR_REQUEST_URI; +import static javax.servlet.RequestDispatcher.ERROR_SERVLET_NAME; import static javax.servlet.RequestDispatcher.ERROR_STATUS_CODE; /** @@ -51,21 +51,77 @@ public class HttpChannelState private static final long DEFAULT_TIMEOUT = Long.getLong("org.eclipse.jetty.server.HttpChannelState.DEFAULT_TIMEOUT", 30000L); - /** + /* * The state of the HttpChannel,used to control the overall lifecycle. + *
+ * IDLE <-----> HANDLING ----> WAITING + * | ^ / + * | \ / + * v \ v + * UPGRADED WOKEN + **/ public enum State { - IDLE, // Idle request - DISPATCHED, // Request dispatched to filter/servlet - THROWN, // Exception thrown while DISPATCHED - ASYNC_WAIT, // Suspended and waiting - ASYNC_WOKEN, // Dispatch to handle from ASYNC_WAIT - ASYNC_IO, // Dispatched for async IO - ASYNC_ERROR, // Async error from ASYNC_WAIT - COMPLETING, // Response is completable - COMPLETED, // Response is completed - UPGRADED // Request upgraded the connection + IDLE, // Idle request + HANDLING, // Request dispatched to filter/servlet or Async IO callback + WAITING, // Suspended and waiting + WOKEN, // Dispatch to handle from ASYNC_WAIT + UPGRADED // Request upgraded the connection + } + + /* + * The state of the request processing lifecycle. + *+ * BLOCKING <----> COMPLETING ---> COMPLETED + * ^ | ^ ^ + * / | \ | + * | | DISPATCH | + * | | ^ ^ | + * | v / | | + * | ASYNC -------> COMPLETE + * | | | ^ + * | v | | + * | EXPIRE | | + * \ | / | + * \ v / | + * EXPIRING ----------+ + *+ */ + private enum RequestState + { + BLOCKING, // Blocking request dispatched + ASYNC, // AsyncContext.startAsync() has been called + DISPATCH, // AsyncContext.dispatch() has been called + EXPIRE, // AsyncContext timeout has happened + EXPIRING, // AsyncListeners are being called + COMPLETE, // AsyncContext.complete() has been called + COMPLETING, // Request is being closed (maybe asynchronously) + COMPLETED // Response is completed + } + + /* + * The input readiness state, which works together with {@link HttpInput.State} + */ + private enum InputState + { + IDLE, // No isReady; No data + REGISTER, // isReady()==false handling; No data + REGISTERED, // isReady()==false !handling; No data + POSSIBLE, // isReady()==false async read callback called (http/1 only) + PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) + READY // isReady() was false, onContentAdded has been called + } + + /* + * The output committed state, which works together with {@link HttpOutput.State} + */ + private enum OutputState + { + OPEN, + COMMITTED, + COMPLETED, + ABORTED, } /** @@ -73,51 +129,28 @@ public class HttpChannelState */ public enum Action { - NOOP, // No action DISPATCH, // handle a normal request dispatch ASYNC_DISPATCH, // handle an async request dispatch - ERROR_DISPATCH, // handle a normal error + SEND_ERROR, // Generate an error page or error dispatch ASYNC_ERROR, // handle an async error + ASYNC_TIMEOUT, // call asyncContext onTimeout WRITE_CALLBACK, // handle an IO write callback + READ_REGISTER, // Register for fill interest READ_PRODUCE, // Check is a read is possible by parsing/filling READ_CALLBACK, // handle an IO read callback - COMPLETE, // Complete the response + COMPLETE, // Complete the response by closing output TERMINATED, // No further actions WAIT, // Wait for further events } - /** - * The state of the servlet async API. - */ - private enum Async - { - NOT_ASYNC, - STARTED, // AsyncContext.startAsync() has been called - DISPATCH, // AsyncContext.dispatch() has been called - COMPLETE, // AsyncContext.complete() has been called - EXPIRING, // AsyncContext timeout just happened - EXPIRED, // AsyncContext timeout has been processed - ERRORING, // An error just happened - ERRORED // The error has been processed - } - - private enum AsyncRead - { - IDLE, // No isReady; No data - REGISTER, // isReady()==false handling; No data - REGISTERED, // isReady()==false !handling; No data - POSSIBLE, // isReady()==false async read callback called (http/1 only) - PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) - READY // isReady() was false, onContentAdded has been called - } - - private final Locker _locker = new Locker(); private final HttpChannel _channel; private List_asyncListeners; - private State _state; - private Async _async; - private boolean _initial; - private AsyncRead _asyncRead = AsyncRead.IDLE; + private State _state = State.IDLE; + private RequestState _requestState = RequestState.BLOCKING; + private OutputState _outputState = OutputState.OPEN; + private InputState _inputState = InputState.IDLE; + private boolean _initial = true; + private boolean _sendError; private boolean _asyncWritePossible; private long _timeoutMs = DEFAULT_TIMEOUT; private AsyncContextEvent _event; @@ -125,14 +158,11 @@ public class HttpChannelState protected HttpChannelState(HttpChannel channel) { _channel = channel; - _state = State.IDLE; - _async = Async.NOT_ASYNC; - _initial = true; } public State getState() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _state; } @@ -140,7 +170,7 @@ public class HttpChannelState public void addListener(AsyncListener listener) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (_asyncListeners == null) _asyncListeners = new ArrayList<>(); @@ -150,7 +180,7 @@ public class HttpChannelState public boolean hasListener(AsyncListener listener) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (_asyncListeners == null) return false; @@ -167,9 +197,17 @@ public class HttpChannelState } } + public boolean isSendError() + { + synchronized (this) + { + return _sendError; + } + } + public void setTimeout(long ms) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { _timeoutMs = ms; } @@ -177,7 +215,7 @@ public class HttpChannelState public long getTimeout() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _timeoutMs; } @@ -185,7 +223,7 @@ public class HttpChannelState public AsyncContextEvent getAsyncContextEvent() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _event; } @@ -194,43 +232,139 @@ public class HttpChannelState @Override public String toString() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return toStringLocked(); } } - public String toStringLocked() + private String toStringLocked() { - return String.format("%s@%x{s=%s a=%s i=%b r=%s w=%b}", + return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), - _state, - _async, - _initial, - _asyncRead, - _asyncWritePossible); + getStatusStringLocked()); } private String getStatusStringLocked() { - return String.format("s=%s i=%b a=%s", _state, _initial, _async); + return String.format("s=%s rs=%s os=%s is=%s awp=%b se=%b i=%b al=%d", + _state, + _requestState, + _outputState, + _inputState, + _asyncWritePossible, + _sendError, + _initial, + _asyncListeners == null ? 0 : _asyncListeners.size()); } public String getStatusString() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return getStatusStringLocked(); } } + public boolean commitResponse() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + _outputState = OutputState.COMMITTED; + return true; + + default: + return false; + } + } + } + + public boolean partialResponse() + { + synchronized (this) + { + switch (_outputState) + { + case COMMITTED: + _outputState = OutputState.OPEN; + return true; + + default: + return false; + } + } + } + + public boolean completeResponse() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + case COMMITTED: + _outputState = OutputState.COMPLETED; + return true; + + default: + return false; + } + } + } + + public boolean isResponseCommitted() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + return false; + default: + return true; + } + } + } + + public boolean isResponseCompleted() + { + synchronized (this) + { + return _outputState == OutputState.COMPLETED; + } + } + + public boolean abortResponse() + { + synchronized (this) + { + switch (_outputState) + { + case ABORTED: + return false; + + case OPEN: + _channel.getResponse().setStatus(500); + _outputState = OutputState.ABORTED; + return true; + + default: + _outputState = OutputState.ABORTED; + return true; + } + } + } + /** * @return Next handling of the request should proceed */ - protected Action handling() + public Action handling() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("handling {}", toStringLocked()); @@ -238,90 +372,169 @@ public class HttpChannelState switch (_state) { case IDLE: + if (_requestState != RequestState.BLOCKING) + throw new IllegalStateException(getStatusStringLocked()); _initial = true; - _state = State.DISPATCHED; + _state = State.HANDLING; return Action.DISPATCH; - case COMPLETING: - case COMPLETED: - return Action.TERMINATED; - - case ASYNC_WOKEN: - switch (_asyncRead) + case WOKEN: + if (_event != null && _event.getThrowable() != null && !_sendError) { - case POSSIBLE: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.PRODUCING; - return Action.READ_PRODUCE; - case READY: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.IDLE; - return Action.READ_CALLBACK; - case REGISTER: - case PRODUCING: - case IDLE: - case REGISTERED: - break; - default: - throw new IllegalStateException(getStatusStringLocked()); + _state = State.HANDLING; + return Action.ASYNC_ERROR; } - if (_asyncWritePossible) - { - _state = State.ASYNC_IO; - _asyncWritePossible = false; - return Action.WRITE_CALLBACK; - } + Action action = nextAction(true); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(true) {} {}", action, toStringLocked()); + return action; - switch (_async) - { - case COMPLETE: - _state = State.COMPLETING; - return Action.COMPLETE; - case DISPATCH: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ASYNC_DISPATCH; - case EXPIRED: - case ERRORED: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - case STARTED: - case EXPIRING: - case ERRORING: - _state = State.ASYNC_WAIT; - return Action.NOOP; - case NOT_ASYNC: - default: - throw new IllegalStateException(getStatusStringLocked()); - } - - case ASYNC_ERROR: - return Action.ASYNC_ERROR; - - case ASYNC_IO: - case ASYNC_WAIT: - case DISPATCHED: - case UPGRADED: default: throw new IllegalStateException(getStatusStringLocked()); } } } + /** + * Signal that the HttpConnection has finished handling the request. + * For blocking connectors, this call may block if the request has + * been suspended (startAsync called). + * + * @return next actions + * be handled again (eg because of a resume that happened before unhandle was called) + */ + protected Action unhandle() + { + boolean readInterested = false; + + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("unhandle {}", toStringLocked()); + + if (_state != State.HANDLING) + throw new IllegalStateException(this.getStatusStringLocked()); + + _initial = false; + + Action action = nextAction(false); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(false) {} {}", action, toStringLocked()); + return action; + } + } + + private Action nextAction(boolean handling) + { + // Assume we can keep going, but exceptions are below + _state = State.HANDLING; + + if (_sendError) + { + switch (_requestState) + { + case BLOCKING: + case ASYNC: + case COMPLETE: + case DISPATCH: + case COMPLETING: + _requestState = RequestState.BLOCKING; + _sendError = false; + return Action.SEND_ERROR; + + default: + break; + } + } + + switch (_requestState) + { + case BLOCKING: + if (handling) + throw new IllegalStateException(getStatusStringLocked()); + _requestState = RequestState.COMPLETING; + return Action.COMPLETE; + + case ASYNC: + switch (_inputState) + { + case POSSIBLE: + _inputState = InputState.PRODUCING; + return Action.READ_PRODUCE; + case READY: + _inputState = InputState.IDLE; + return Action.READ_CALLBACK; + case REGISTER: + case PRODUCING: + _inputState = InputState.REGISTERED; + return Action.READ_REGISTER; + case IDLE: + case REGISTERED: + break; + + default: + throw new IllegalStateException(getStatusStringLocked()); + } + + if (_asyncWritePossible) + { + _asyncWritePossible = false; + return Action.WRITE_CALLBACK; + } + + Scheduler scheduler = _channel.getScheduler(); + if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) + _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); + _state = State.WAITING; + return Action.WAIT; + + case DISPATCH: + _requestState = RequestState.BLOCKING; + return Action.ASYNC_DISPATCH; + + case EXPIRE: + _requestState = RequestState.EXPIRING; + return Action.ASYNC_TIMEOUT; + + case EXPIRING: + if (handling) + throw new IllegalStateException(getStatusStringLocked()); + sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "AsyncContext timeout"); + // handle sendError immediately + _requestState = RequestState.BLOCKING; + _sendError = false; + return Action.SEND_ERROR; + + case COMPLETE: + _requestState = RequestState.COMPLETING; + return Action.COMPLETE; + + case COMPLETING: + _state = State.WAITING; + return Action.WAIT; + + case COMPLETED: + _state = State.IDLE; + return Action.TERMINATED; + + default: + throw new IllegalStateException(getStatusStringLocked()); + } + } + public void startAsync(AsyncContextEvent event) { final List lastAsyncListeners; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("startAsync {}", toStringLocked()); - if (_state != State.DISPATCHED || _async != Async.NOT_ASYNC) + if (_state != State.HANDLING || _requestState != RequestState.BLOCKING) throw new IllegalStateException(this.getStatusStringLocked()); - _async = Async.STARTED; + _requestState = RequestState.ASYNC; _event = event; lastAsyncListeners = _asyncListeners; _asyncListeners = null; @@ -359,219 +572,36 @@ public class HttpChannelState } } - public void asyncError(Throwable failure) - { - AsyncContextEvent event = null; - try (Locker.Lock lock = _locker.lock()) - { - switch (_state) - { - case IDLE: - case DISPATCHED: - case COMPLETING: - case COMPLETED: - case UPGRADED: - case ASYNC_IO: - case ASYNC_WOKEN: - case ASYNC_ERROR: - { - break; - } - case ASYNC_WAIT: - { - _event.addThrowable(failure); - _state = State.ASYNC_ERROR; - event = _event; - break; - } - default: - { - throw new IllegalStateException(getStatusStringLocked()); - } - } - } - - if (event != null) - { - cancelTimeout(event); - runInContext(event, _channel); - } - } - - /** - * Signal that the HttpConnection has finished handling the request. - * For blocking connectors, this call may block if the request has - * been suspended (startAsync called). - * - * @return next actions - * be handled again (eg because of a resume that happened before unhandle was called) - */ - protected Action unhandle() - { - boolean readInterested = false; - - try (Locker.Lock lock = _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("unhandle {}", toStringLocked()); - - switch (_state) - { - case COMPLETING: - case COMPLETED: - return Action.TERMINATED; - - case THROWN: - _state = State.DISPATCHED; - return Action.ERROR_DISPATCH; - - case DISPATCHED: - case ASYNC_IO: - case ASYNC_ERROR: - case ASYNC_WAIT: - break; - - default: - throw new IllegalStateException(this.getStatusStringLocked()); - } - - _initial = false; - switch (_async) - { - case COMPLETE: - _state = State.COMPLETING; - _async = Async.NOT_ASYNC; - return Action.COMPLETE; - - case DISPATCH: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ASYNC_DISPATCH; - - case STARTED: - switch (_asyncRead) - { - case READY: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.IDLE; - return Action.READ_CALLBACK; - - case POSSIBLE: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.PRODUCING; - return Action.READ_PRODUCE; - - case REGISTER: - case PRODUCING: - _asyncRead = AsyncRead.REGISTERED; - readInterested = true; - break; - - case IDLE: - case REGISTERED: - break; - - default: - throw new IllegalStateException(_asyncRead.toString()); - } - - if (_asyncWritePossible) - { - _state = State.ASYNC_IO; - _asyncWritePossible = false; - return Action.WRITE_CALLBACK; - } - else - { - _state = State.ASYNC_WAIT; - - Scheduler scheduler = _channel.getScheduler(); - if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) - _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); - - return Action.WAIT; - } - - case EXPIRING: - // onTimeout callbacks still being called, so just WAIT - _state = State.ASYNC_WAIT; - return Action.WAIT; - - case EXPIRED: - // onTimeout handling is complete, but did not dispatch as - // we were handling. So do the error dispatch here - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - - case ERRORED: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - - case NOT_ASYNC: - _state = State.COMPLETING; - return Action.COMPLETE; - - default: - _state = State.COMPLETING; - return Action.COMPLETE; - } - } - finally - { - if (readInterested) - _channel.onAsyncWaitForContent(); - } - } - public void dispatch(ServletContext context, String path) { boolean dispatch = false; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("dispatch {} -> {}", toStringLocked(), path); - boolean started = false; - event = _event; - switch (_async) + switch (_requestState) { - case STARTED: - started = true; - break; + case ASYNC: case EXPIRING: - case ERRORING: - case ERRORED: break; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _async = Async.DISPATCH; if (context != null) _event.setDispatchContext(context); if (path != null) _event.setDispatchPath(path); - if (started) + if (_requestState == RequestState.ASYNC && _state == State.WAITING) { - switch (_state) - { - case DISPATCHED: - case ASYNC_IO: - case ASYNC_WOKEN: - break; - case ASYNC_WAIT: - _state = State.ASYNC_WOKEN; - dispatch = true; - break; - default: - LOG.warn("async dispatched when complete {}", this); - break; - } + _state = State.WOKEN; + dispatch = true; } + _requestState = RequestState.DISPATCH; + event = _event; } cancelTimeout(event); @@ -579,23 +609,47 @@ public class HttpChannelState scheduleDispatch(); } + protected void timeout() + { + boolean dispatch = false; + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("Timeout {}", toStringLocked()); + + if (_requestState != RequestState.ASYNC) + return; + _requestState = RequestState.EXPIRE; + + if (_state == State.WAITING) + { + _state = State.WOKEN; + dispatch = true; + } + } + + if (dispatch) + { + if (LOG.isDebugEnabled()) + LOG.debug("Dispatch after async timeout {}", this); + scheduleDispatch(); + } + } + protected void onTimeout() { final List listeners; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onTimeout {}", toStringLocked()); - - if (_async != Async.STARTED) - return; - _async = Async.EXPIRING; + if (_requestState != RequestState.EXPIRING || _state != State.HANDLING) + throw new IllegalStateException(toStringLocked()); event = _event; listeners = _asyncListeners; } - final AtomicReference error = new AtomicReference<>(); if (listeners != null) { Runnable task = new Runnable() @@ -613,11 +667,6 @@ public class HttpChannelState { LOG.warn(x + " while invoking onTimeout listener " + listener); LOG.debug(x); - Throwable failure = error.get(); - if (failure == null) - error.set(x); - else if (x != failure) - failure.addSuppressed(x); } } } @@ -631,86 +680,34 @@ public class HttpChannelState runInContext(event, task); } - - Throwable th = error.get(); - boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) - { - switch (_async) - { - case EXPIRING: - _async = th == null ? Async.EXPIRED : Async.ERRORING; - break; - - case COMPLETE: - case DISPATCH: - if (th != null) - { - LOG.ignore(th); - th = null; - } - break; - - default: - throw new IllegalStateException(); - } - - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; - } - } - - if (th != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Error after async timeout {}", this, th); - onError(th); - } - - if (dispatch) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dispatch after async timeout {}", this); - scheduleDispatch(); - } } public void complete() { - - // just like resume, except don't set _dispatched=true; boolean handle = false; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("complete {}", toStringLocked()); - boolean started = false; event = _event; - - switch (_async) + switch (_requestState) { - case STARTED: - started = true; - break; case EXPIRING: - case ERRORING: - case ERRORED: + case ASYNC: + _requestState = _sendError ? RequestState.BLOCKING : RequestState.COMPLETE; break; + case COMPLETE: return; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _async = Async.COMPLETE; - - if (started && _state == State.ASYNC_WAIT) + if (_state == State.WAITING) { handle = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } } @@ -719,31 +716,132 @@ public class HttpChannelState runInContext(event, _channel); } - public void errorComplete() + public void asyncError(Throwable failure) { - try (Locker.Lock lock = _locker.lock()) + // This method is called when an failure occurs asynchronously to + // normal handling. If the request is async, we arrange for the + // exception to be thrown from the normal handling loop and then + // actually handled by #thrownException + + AsyncContextEvent event = null; + synchronized (this) { if (LOG.isDebugEnabled()) - LOG.debug("error complete {}", toStringLocked()); + LOG.debug("asyncError " + toStringLocked(), failure); - _async = Async.COMPLETE; - _event.setDispatchContext(null); - _event.setDispatchPath(null); + if (_state == State.WAITING && _requestState == RequestState.ASYNC) + { + _state = State.WOKEN; + _event.addThrowable(failure); + event = _event; + } + else + { + LOG.warn(failure.toString()); + LOG.debug(failure); + } } - cancelTimeout(); + if (event != null) + { + cancelTimeout(event); + runInContext(event, _channel); + } } protected void onError(Throwable th) { - final List listeners; - final AsyncContextEvent event; - final Request baseRequest = _channel.getRequest(); + final AsyncContextEvent asyncEvent; + final List asyncListeners; + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("thrownException " + getStatusStringLocked(), th); - int code = HttpStatus.INTERNAL_SERVER_ERROR_500; - String message = null; + // This can only be called from within the handle loop + if (_state != State.HANDLING) + throw new IllegalStateException(getStatusStringLocked()); + + // If sendError has already been called, we can only handle one failure at a time! + if (_sendError) + { + LOG.warn("unhandled due to prior sendError", th); + return; + } + + // Check async state to determine type of handling + switch (_requestState) + { + case BLOCKING: + // handle the exception with a sendError + sendError(th); + return; + + case DISPATCH: // Dispatch has already been called but we ignore and handle exception below + case COMPLETE: // Complete has already been called but we ignore and handle exception below + case ASYNC: + if (_asyncListeners == null || _asyncListeners.isEmpty()) + { + sendError(th); + return; + } + asyncEvent = _event; + asyncEvent.addThrowable(th); + asyncListeners = _asyncListeners; + break; + + default: + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); + return; + } + } + + // If we are async and have async listeners + // call onError + runInContext(asyncEvent, () -> + { + for (AsyncListener listener : asyncListeners) + { + try + { + listener.onError(asyncEvent); + } + catch (Throwable x) + { + LOG.warn(x + " while invoking onError listener " + listener); + LOG.debug(x); + } + } + }); + + // check the actions of the listeners + synchronized (this) + { + // If we are still async and nobody has called sendError + if (_requestState == RequestState.ASYNC && !_sendError) + // Then the listeners did not invoke API methods + // and the container must provide a default error dispatch. + sendError(th); + else + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); + } + } + + private void sendError(Throwable th) + { + // No sync as this is always called with lock held + + // Determine the actual details of the exception + final Request request = _channel.getRequest(); + final int code; + final String message; Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); - if (cause instanceof BadMessageException) + if (cause == null) + { + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = th.toString(); + } + else if (cause instanceof BadMessageException) { BadMessageException bme = (BadMessageException)cause; code = bme.getCode(); @@ -751,196 +849,175 @@ public class HttpChannelState } else if (cause instanceof UnavailableException) { + message = cause.toString(); if (((UnavailableException)cause).isPermanent()) code = HttpStatus.NOT_FOUND_404; else code = HttpStatus.SERVICE_UNAVAILABLE_503; } - - try (Locker.Lock lock = _locker.lock()) + else { - if (LOG.isDebugEnabled()) - LOG.debug("onError {} {}", toStringLocked(), th); - - // Set error on request. - if (_event != null) - { - _event.addThrowable(th); - _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, message); - } - else - { - Throwable error = (Throwable)baseRequest.getAttribute(ERROR_EXCEPTION); - if (error != null) - throw new IllegalStateException("Error already set", error); - 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, message); - } - - // Are we blocking? - if (_async == Async.NOT_ASYNC) - { - // Only called from within HttpChannel Handling, so much be dispatched, let's stay dispatched! - if (_state == State.DISPATCHED) - { - _state = State.THROWN; - return; - } - throw new IllegalStateException(this.getStatusStringLocked()); - } - - // We are Async - _async = Async.ERRORING; - listeners = _asyncListeners; - event = _event; + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = null; } - if (listeners != null) - { - Runnable task = new Runnable() - { - @Override - public void run() - { - for (AsyncListener listener : listeners) - { - try - { - listener.onError(event); - } - catch (Throwable x) - { - LOG.warn(x + " while invoking onError listener " + listener); - LOG.debug(x); - } - } - } + sendError(code, message); - @Override - public String toString() - { - return "onError"; - } - }; - runInContext(event, task); - } - - boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) - { - switch (_async) - { - case ERRORING: - { - // Still in this state ? The listeners did not invoke API methods - // and the container must provide a default error dispatch. - _async = Async.ERRORED; - break; - } - case DISPATCH: - case COMPLETE: - { - // The listeners called dispatch() or complete(). - break; - } - default: - { - throw new IllegalStateException(toString()); - } - } - - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; - } - } - - if (dispatch) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dispatch after error {}", this); - scheduleDispatch(); - } + // No ISE, so good to modify request/state + request.setAttribute(ERROR_EXCEPTION, th); + request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass()); + // Ensure any async lifecycle is ended! + _requestState = RequestState.BLOCKING; } - protected void onComplete() + public void sendError(int code, String message) { - final List aListeners; - final AsyncContextEvent event; + // This method is called by Response.sendError to organise for an error page to be generated when it is possible: + // + The response is reset and temporarily closed. + // + The details of the error are saved as request attributes + // + The _sendError boolean is set to true so that an ERROR_DISPATCH action will be generated: + // - after unhandle for sync + // - after both unhandle and complete for async - try (Locker.Lock lock = _locker.lock()) + final Request request = _channel.getRequest(); + final Response response = _channel.getResponse(); + if (message == null) + message = HttpStatus.getMessage(code); + + synchronized (this) { if (LOG.isDebugEnabled()) - LOG.debug("onComplete {}", toStringLocked()); + LOG.debug("sendError {}", toStringLocked()); switch (_state) { - case COMPLETING: - aListeners = _asyncListeners; - event = _event; - _state = State.COMPLETED; - _async = Async.NOT_ASYNC; + case HANDLING: + case WOKEN: + case WAITING: break; - default: - throw new IllegalStateException(this.getStatusStringLocked()); + throw new IllegalStateException(getStatusStringLocked()); + } + if (_outputState != OutputState.OPEN) + throw new IllegalStateException("Response is " + _outputState); + + response.getHttpOutput().closedBySendError(); + response.setStatus(code); + + request.setAttribute(ErrorHandler.ERROR_CONTEXT, request.getErrorContext()); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); + request.setAttribute(ERROR_STATUS_CODE, code); + request.setAttribute(ERROR_MESSAGE, message); + + _sendError = true; + if (_event != null) + { + Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION); + if (cause != null) + _event.addThrowable(cause); + } + } + } + + protected void completing() + { + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("completing {}", toStringLocked()); + + switch (_requestState) + { + case COMPLETED: + throw new IllegalStateException(getStatusStringLocked()); + default: + _requestState = RequestState.COMPLETING; + } + } + } + + protected void completed() + { + final List aListeners; + final AsyncContextEvent event; + boolean handle = false; + + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("completed {}", toStringLocked()); + + if (_requestState != RequestState.COMPLETING) + throw new IllegalStateException(this.getStatusStringLocked()); + + if (_event == null) + { + _requestState = RequestState.COMPLETED; + aListeners = null; + event = null; + if (_state == State.WAITING) + { + _state = State.WOKEN; + handle = true; + } + } + else + { + aListeners = _asyncListeners; + event = _event; } } if (event != null) { + cancelTimeout(event); if (aListeners != null) { - Runnable callback = new Runnable() + runInContext(event, () -> { - @Override - public void run() + for (AsyncListener listener : aListeners) { - for (AsyncListener listener : aListeners) + try { - try - { - listener.onComplete(event); - } - catch (Throwable e) - { - LOG.warn(e + " while invoking onComplete listener " + listener); - LOG.debug(e); - } + listener.onComplete(event); + } + catch (Throwable e) + { + LOG.warn(e + " while invoking onComplete listener " + listener); + LOG.debug(e); } } - - @Override - public String toString() - { - return "onComplete"; - } - }; - - runInContext(event, callback); + }); } event.completed(); + + synchronized (this) + { + _requestState = RequestState.COMPLETED; + if (_state == State.WAITING) + { + _state = State.WOKEN; + handle = true; + } + } } + + if (handle) + _channel.handle(); } protected void recycle() { cancelTimeout(); - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("recycle {}", toStringLocked()); switch (_state) { - case DISPATCHED: - case ASYNC_IO: + case HANDLING: throw new IllegalStateException(getStatusStringLocked()); case UPGRADED: return; @@ -949,9 +1026,10 @@ public class HttpChannelState } _asyncListeners = null; _state = State.IDLE; - _async = Async.NOT_ASYNC; + _requestState = RequestState.BLOCKING; + _outputState = OutputState.OPEN; _initial = true; - _asyncRead = AsyncRead.IDLE; + _inputState = InputState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -961,7 +1039,7 @@ public class HttpChannelState public void upgrade() { cancelTimeout(); - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("upgrade {}", toStringLocked()); @@ -969,16 +1047,15 @@ public class HttpChannelState switch (_state) { case IDLE: - case COMPLETED: break; default: throw new IllegalStateException(getStatusStringLocked()); } _asyncListeners = null; _state = State.UPGRADED; - _async = Async.NOT_ASYNC; + _requestState = RequestState.BLOCKING; _initial = true; - _asyncRead = AsyncRead.IDLE; + _inputState = InputState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -993,7 +1070,7 @@ public class HttpChannelState protected void cancelTimeout() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1008,7 +1085,7 @@ public class HttpChannelState public boolean isIdle() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _state == State.IDLE; } @@ -1016,15 +1093,16 @@ public class HttpChannelState public boolean isExpired() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _async == Async.EXPIRED; + // TODO review + return _requestState == RequestState.EXPIRE || _requestState == RequestState.EXPIRING; } } public boolean isInitial() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _initial; } @@ -1032,51 +1110,35 @@ public class HttpChannelState public boolean isSuspended() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _state == State.ASYNC_WAIT || _state == State.DISPATCHED && _async == Async.STARTED; - } - } - - boolean isCompleting() - { - try (Locker.Lock lock = _locker.lock()) - { - return _state == State.COMPLETING; + return _state == State.WAITING || _state == State.HANDLING && _requestState == RequestState.ASYNC; } } boolean isCompleted() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _state == State.COMPLETED; + return _requestState == RequestState.COMPLETED; } } public boolean isAsyncStarted() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - if (_state == State.DISPATCHED) - return _async != Async.NOT_ASYNC; - return _async == Async.STARTED || _async == Async.EXPIRING; - } - } - - public boolean isAsyncComplete() - { - try (Locker.Lock lock = _locker.lock()) - { - return _async == Async.COMPLETE; + if (_state == State.HANDLING) + return _requestState != RequestState.BLOCKING; + return _requestState == RequestState.ASYNC || _requestState == RequestState.EXPIRING; } } public boolean isAsync() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return !_initial || _async != Async.NOT_ASYNC; + return !_initial || _requestState != RequestState.BLOCKING; } } @@ -1093,7 +1155,7 @@ public class HttpChannelState public ContextHandler getContextHandler() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1114,7 +1176,7 @@ public class HttpChannelState public ServletResponse getServletResponse() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1162,23 +1224,23 @@ public class HttpChannelState public void onReadUnready() { boolean interested = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadUnready {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: case READY: - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { interested = true; - _asyncRead = AsyncRead.REGISTERED; + _inputState = InputState.REGISTERED; } else { - _asyncRead = AsyncRead.REGISTER; + _inputState = InputState.REGISTER; } break; @@ -1187,8 +1249,9 @@ public class HttpChannelState case POSSIBLE: case PRODUCING: break; + default: - throw new IllegalStateException(_asyncRead.toString()); + throw new IllegalStateException(toStringLocked()); } } @@ -1207,28 +1270,28 @@ public class HttpChannelState public boolean onContentAdded() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onContentAdded {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: case READY: break; case PRODUCING: - _asyncRead = AsyncRead.READY; + _inputState = InputState.READY; break; case REGISTER: case REGISTERED: - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1250,19 +1313,19 @@ public class HttpChannelState public boolean onReadReady() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadReady {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1283,19 +1346,19 @@ public class HttpChannelState public boolean onReadPossible() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadPossible {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case REGISTERED: - _asyncRead = AsyncRead.POSSIBLE; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.POSSIBLE; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1315,17 +1378,17 @@ public class HttpChannelState public boolean onReadEof() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onEof {}", toStringLocked()); // Force read ready so onAllDataRead can be called - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } } return woken; @@ -1335,15 +1398,15 @@ public class HttpChannelState { boolean wake = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onWritePossible {}", toStringLocked()); _asyncWritePossible = true; - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; wake = true; } } 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 acf95042a28..5452c7ffa8b 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 @@ -38,7 +38,7 @@ import org.eclipse.jetty.util.component.DumpableCollection; /** * HTTP Configuration. * This class is a holder of HTTP configuration for use by the - * {@link HttpChannel} class. Typically a HTTPConfiguration instance + * {@link HttpChannel} class. Typically an HTTPConfiguration instance * is instantiated and passed to a {@link ConnectionFactory} that can * create HTTP channels (e.g. HTTP, AJP or FCGI).
*The configuration held by this class is not for the wire protocol, @@ -185,19 +185,19 @@ public class HttpConfiguration implements Dumpable return _outputAggregationSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP request header") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP request header") public int getRequestHeaderSize() { return _requestHeaderSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP response header") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP response header") public int getResponseHeaderSize() { return _responseHeaderSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP header field cache") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP header field cache") public int getHeaderCacheSize() { return _headerCacheSize; @@ -228,20 +228,20 @@ public class HttpConfiguration implements Dumpable } /** - *
The max idle time is applied to a HTTP request for IO operations and + *
The max idle time is applied to an HTTP request for IO operations and * delayed dispatch.
* * @return the max idle time in ms or if == 0 implies an infinite timeout, <0 * implies no HTTP channel timeout and the connection timeout is used instead. */ - @ManagedAttribute("The idle timeout in ms for I/O operations during the handling of a HTTP request") + @ManagedAttribute("The idle timeout in ms for I/O operations during the handling of an HTTP request") public long getIdleTimeout() { return _idleTimeout; } /** - *The max idle time is applied to a HTTP request for IO operations and + *
The max idle time is applied to an HTTP request for IO operations and * delayed dispatch.
* * @param timeoutMs the max idle time in ms or if == 0 implies an infinite timeout, <0 diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index d93682242f8..a33d842188d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -293,18 +293,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http } else if (filled < 0) { - switch (_channel.getState().getState()) - { - case COMPLETING: - case COMPLETED: - case IDLE: - case THROWN: - case ASYNC_ERROR: - getEndPoint().shutdownOutput(); - break; - default: - break; - } + if (_channel.getState().isIdle()) + getEndPoint().shutdownOutput(); break; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java index 0b6a450d01c..151c1bc9467 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java @@ -274,7 +274,8 @@ public class HttpInput extends ServletInputStream implements Runnable { BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408, String.format("Request content data rate < %d B/s", minRequestDataRate)); - _channelState.getHttpChannel().abort(bad); + if (_channelState.isResponseCommitted()) + _channelState.getHttpChannel().abort(bad); throw bad; } } 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 6036807f978..0d5c5b8ac21 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 @@ -18,6 +18,7 @@ package org.eclipse.jetty.server; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -62,8 +63,31 @@ import org.eclipse.jetty.util.log.Logger; public class HttpOutput extends ServletOutputStream implements Runnable { private static final String LSTRING_FILE = "javax.servlet.LocalStrings"; + private static final Callback BLOCKING_CLOSE_CALLBACK = new Callback() {}; private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + /* + ACTION OPEN ASYNC READY PENDING UNREADY CLOSING CLOSED + -------------------------------------------------------------------------------------------------- + setWriteListener() READY->owp ise ise ise ise ise ise + write() OPEN ise PENDING wpe wpe eof eof + flush() OPEN ise PENDING wpe wpe eof eof + close() CLOSING CLOSING CLOSING CLOSED CLOSED CLOSING CLOSED + isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true CLOSED:true + write completed - - - ASYNC READY->owp CLOSED - + */ + enum State + { + OPEN, // Open in blocking mode + ASYNC, // Open in async mode + READY, // isReady() has returned true + PENDING, // write operating in progress + UNREADY, // write operating in progress, isReady has returned false + ERROR, // An error has occured + CLOSING, // Asynchronous close in progress + CLOSED // Closed + } + /** * The HttpOutput.Interceptor is a single intercept point for all * output written to the HttpOutput: via writer; via output stream; @@ -121,6 +145,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable private static Logger LOG = Log.getLogger(HttpOutput.class); private static final ThreadLocal_encoder = new ThreadLocal<>(); + private final AtomicReference _state = new AtomicReference<>(State.OPEN); private final HttpChannel _channel; private final SharedBlockingCallback _writeBlocker; private Interceptor _interceptor; @@ -132,23 +157,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable private int _commitSize; private WriteListener _writeListener; private volatile Throwable _onError; - - /* - ACTION OPEN ASYNC READY PENDING UNREADY CLOSED - ------------------------------------------------------------------------------------------- - setWriteListener() READY->owp ise ise ise ise ise - write() OPEN ise PENDING wpe wpe eof - flush() OPEN ise PENDING wpe wpe eof - close() CLOSED CLOSED CLOSED CLOSED CLOSED CLOSED - isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true - write completed - - - ASYNC READY->owp - - */ - private enum OutputState - { - OPEN, ASYNC, READY, PENDING, UNREADY, ERROR, CLOSED - } - - private final AtomicReference _state = new AtomicReference<>(OutputState.OPEN); + private Callback _closeCallback; public HttpOutput(HttpChannel channel) { @@ -192,7 +201,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable public void reopen() { - _state.set(OutputState.OPEN); + _state.set(State.OPEN); } private boolean isLastContentToWrite(int len) @@ -217,28 +226,78 @@ public class HttpOutput extends ServletOutputStream implements Runnable _channel.abort(failure); } - @Override - public void close() + public void closedBySendError() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { + case OPEN: + case READY: + case ASYNC: + if (!_state.compareAndSet(state, State.CLOSED)) + continue; + return; + + default: + throw new IllegalStateException(state.toString()); + } + } + } + + public void close(Closeable wrapper, Callback callback) + { + _closeCallback = callback; + try + { + if (wrapper != null) + wrapper.close(); + if (!isClosed()) + close(); + } + catch (Throwable th) + { + closed(); + if (_closeCallback == null) + LOG.ignore(th); + else + callback.failed(th); + } + finally + { + if (_closeCallback != null) + callback.succeeded(); + _closeCallback = null; + } + } + + @Override + public void close() + { + Callback closeCallback = _closeCallback == null ? BLOCKING_CLOSE_CALLBACK : _closeCallback; + + while (true) + { + State state = _state.get(); + switch (state) + { + case CLOSING: case CLOSED: { + _closeCallback = null; + closeCallback.succeeded(); return; } case ASYNC: { // A close call implies a write operation, thus in asynchronous mode // a call to isReady() that returned true should have been made. - // However it is desirable to allow a close at any time, specially if - // complete is called. Thus we simulate a call to isReady here, assuming - // that we can transition to READY. - if (!_state.compareAndSet(state, OutputState.READY)) - continue; - break; + // However it is desirable to allow a close at any time, specially if + // complete is called. Thus we simulate a call to isReady here, by + // trying to move to READY state. Either way we continue. + _state.compareAndSet(state, State.READY); + continue; } case UNREADY: case PENDING: @@ -249,34 +308,45 @@ public class HttpOutput extends ServletOutputStream implements Runnable // complete is called. Because the prior write has not yet completed // and/or isReady has not been called, this close is allowed, but will // abort the response. - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) continue; IOException ex = new IOException("Closed while Pending/Unready"); LOG.warn(ex.toString()); LOG.debug(ex); abort(ex); + _closeCallback = null; + closeCallback.failed(ex); return; } default: { - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSING)) continue; // Do a normal close by writing the aggregate buffer or an empty buffer. If we are // not including, then indicate this is the last write. try { - write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, !_channel.getResponse().isIncluding()); + ByteBuffer content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER; + if (closeCallback == BLOCKING_CLOSE_CALLBACK) + { + // Do a blocking close + write(content, !_channel.getResponse().isIncluding()); + _closeCallback = null; + closeCallback.succeeded(); + } + else + { + _closeCallback = null; + write(content, !_channel.getResponse().isIncluding(), closeCallback); + } } catch (IOException x) { LOG.ignore(x); // Ignore it, it's been already logged in write(). + _closeCallback = null; + closeCallback.failed(x); } - finally - { - releaseBuffer(); - } - // Return even if an exception is thrown by write(). return; } } @@ -287,11 +357,11 @@ public class HttpOutput extends ServletOutputStream implements Runnable * Called to indicate that the last write has been performed. * It updates the state and performs cleanup operations. */ - void closed() + public void closed() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case CLOSED: @@ -300,15 +370,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable } case UNREADY: { - if (_state.compareAndSet(state, OutputState.ERROR)) + if (_state.compareAndSet(state, State.ERROR)) _writeListener.onError(_onError == null ? new EofException("Async closed") : _onError); break; } default: { - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) break; + // Just make sure write and output stream really are closed try { _channel.getResponse().closeOutput(); @@ -330,6 +401,18 @@ public class HttpOutput extends ServletOutputStream implements Runnable } } + public ByteBuffer getBuffer() + { + return _aggregate; + } + + public ByteBuffer acquireBuffer() + { + if (_aggregate == null) + _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _channel.isUseOutputDirectByteBuffers()); + return _aggregate; + } + private void releaseBuffer() { if (_aggregate != null) @@ -341,7 +424,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable public boolean isClosed() { - return _state.get() == OutputState.CLOSED; + switch (_state.get()) + { + case CLOSING: + case CLOSED: + return true; + default: + return false; + } } public boolean isAsync() @@ -363,7 +453,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, false); @@ -373,25 +464,24 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; new AsyncFlush().iterate(); return; - case PENDING: - return; - case UNREADY: throw new WritePendingException(); case ERROR: throw new EofException(_onError); + case PENDING: + case CLOSING: case CLOSED: return; default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } } } @@ -433,7 +523,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // Async or Blocking ? while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: // process blocking below @@ -443,14 +534,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; // Should we aggregate? boolean last = isLastContentToWrite(len); if (!last && len <= _commitSize) { - ensureAggregate(); + acquireBuffer(); // YES - fill the aggregate with content from the buffer int filled = BufferUtil.fill(_aggregate, b, off, len); @@ -458,8 +549,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // return if we are not complete, not full and filled all the content if (filled == len && !BufferUtil.isFull(_aggregate)) { - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) - throw new IllegalStateException(); + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) + throw new IllegalStateException(_state.get().toString()); return; } @@ -479,11 +570,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } break; } @@ -494,7 +586,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable boolean last = isLastContentToWrite(len); if (!last && len <= _commitSize) { - ensureAggregate(); + acquireBuffer(); // YES - fill the aggregate with content from the buffer int filled = BufferUtil.fill(_aggregate, b, off, len); @@ -543,15 +635,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable { write(BufferUtil.EMPTY_BUFFER, true); } - - if (last) - closed(); - } - - private void ensureAggregate() - { - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _channel.isUseOutputDirectByteBuffers()); } public void write(ByteBuffer buffer) throws IOException @@ -561,7 +644,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // Async or Blocking ? while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: // process blocking below @@ -571,7 +655,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; // Do the asynchronous writing from the callback @@ -586,11 +670,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } break; } @@ -608,9 +693,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable write(buffer, last); else if (last) write(BufferUtil.EMPTY_BUFFER, true); - - if (last) - closed(); } @Override @@ -625,32 +707,28 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: - ensureAggregate(); + acquireBuffer(); BufferUtil.append(_aggregate, (byte)b); // Check if all written or full if (complete || BufferUtil.isFull(_aggregate)) - { write(_aggregate, complete); - if (complete) - closed(); - } break; case ASYNC: throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(State.READY, State.PENDING)) continue; - ensureAggregate(); + acquireBuffer(); BufferUtil.append(_aggregate, (byte)b); // Check if all written or full if (!complete && !BufferUtil.isFull(_aggregate)) { - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) throw new IllegalStateException(); return; } @@ -666,6 +744,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); @@ -803,7 +882,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable _written += content.remaining(); write(content, true); - closed(); } /** @@ -959,7 +1037,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: - if (!_state.compareAndSet(OutputState.OPEN, OutputState.PENDING)) + if (!_state.compareAndSet(State.OPEN, State.PENDING)) continue; break; @@ -967,6 +1045,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable callback.failed(new EofException(_onError)); return; + case CLOSING: case CLOSED: callback.failed(new EofException("Closed")); return; @@ -1066,6 +1145,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable _onError = null; _firstByteTimeStamp = -1; _flushed = 0; + _closeCallback = null; reopen(); } @@ -1075,7 +1155,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (BufferUtil.hasContent(_aggregate)) BufferUtil.clear(_aggregate); _written = 0; - reopen(); } @Override @@ -1084,7 +1163,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (!_channel.getState().isAsync()) throw new IllegalStateException("!ASYNC"); - if (_state.compareAndSet(OutputState.OPEN, OutputState.READY)) + if (_state.compareAndSet(State.OPEN, State.READY)) { _writeListener = writeListener; if (_channel.getState().onWritePossible()) @@ -1102,30 +1181,25 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: + case READY: + case ERROR: + case CLOSING: + case CLOSED: return true; case ASYNC: - if (!_state.compareAndSet(OutputState.ASYNC, OutputState.READY)) + if (!_state.compareAndSet(State.ASYNC, State.READY)) continue; return true; - case READY: - return true; - case PENDING: - if (!_state.compareAndSet(OutputState.PENDING, OutputState.UNREADY)) + if (!_state.compareAndSet(State.PENDING, State.UNREADY)) continue; return false; case UNREADY: return false; - case ERROR: - return true; - - case CLOSED: - return true; - default: throw new IllegalStateException(); } @@ -1137,12 +1211,13 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - OutputState state = _state.get(); + State state = _state.get(); if (_onError != null) { switch (state) { + case CLOSING: case CLOSED: case ERROR: { @@ -1151,7 +1226,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable } default: { - if (_state.compareAndSet(state, OutputState.ERROR)) + if (_state.compareAndSet(state, State.ERROR)) { Throwable th = _onError; _onError = null; @@ -1227,16 +1302,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - OutputState last = _state.get(); + State last = _state.get(); switch (last) { case PENDING: - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) continue; break; case UNREADY: - if (!_state.compareAndSet(OutputState.UNREADY, OutputState.READY)) + if (!_state.compareAndSet(State.UNREADY, State.READY)) continue; if (_last) closed(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java index 71b40c0fa82..41118eb081d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java @@ -95,7 +95,7 @@ public class OptionalSslConnectionFactory extends AbstractConnectionFactory int byte2 = buffer.get(1) & 0xFF; if (byte1 == 'G' && byte2 == 'E') { - // Plain text HTTP to a HTTPS port, + // Plain text HTTP to an HTTPS port, // write a minimal response. String body = "\r\n" + 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 eeca07a335c..4471850c5d6 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 @@ -212,6 +212,7 @@ public class Request implements HttpServletRequest private String _contentType; private String _characterEncoding; private ContextHandler.Context _context; + private ContextHandler.Context _errorContext; private Cookies _cookies; private DispatcherType _dispatcherType; private int _inputState = INPUT_NONE; @@ -229,6 +230,7 @@ public class Request implements HttpServletRequest private long _timeStamp; private MultiParts _multiParts; //if the request is a multi-part mime private AsyncContextState _async; + private List _sessions; //list of sessions used during lifetime of request public Request(HttpChannel channel, HttpInput input) { @@ -353,6 +355,48 @@ public class Request implements HttpServletRequest if (listener instanceof AsyncListener) throw new IllegalArgumentException(listener.getClass().toString()); } + + /** + * Remember a session that this request has just entered. + * + * @param s the session + */ + public void enterSession(HttpSession s) + { + if (!(s instanceof Session)) + return; + + if (_sessions == null) + _sessions = new ArrayList<>(); + if (LOG.isDebugEnabled()) + LOG.debug("Request {} entering session={}", this, s); + _sessions.add((Session)s); + } + + /** + * Complete this request's access to a session. + * + * @param session the session + */ + private void leaveSession(Session session) + { + if (LOG.isDebugEnabled()) + LOG.debug("Request {} leaving session {}", this, session); + session.getSessionHandler().complete(session); + } + + /** + * A response is being committed for a session, + * potentially write the session out before the + * client receives the response. + * @param session the session + */ + private void commitSession(Session session) + { + if (LOG.isDebugEnabled()) + LOG.debug("Response {} committing for session {}", this, session); + session.getSessionHandler().commit(session); + } private MultiMap getParameters() { @@ -725,6 +769,22 @@ public class Request implements HttpServletRequest return _context; } + /** + * @return The current {@link Context context} used for this error handling for this request. If the request is asynchronous, + * then it is the context that called async. Otherwise it is the last non-null context passed to #setContext + */ + public Context getErrorContext() + { + if (isAsyncStarted()) + { + ContextHandler handler = _channel.getState().getContextHandler(); + if (handler != null) + return handler.getServletContext(); + } + + return _errorContext; + } + /* * @see javax.servlet.http.HttpServletRequest#getContextPath() */ @@ -1432,6 +1492,59 @@ public class Request implements HttpServletRequest return session.getId(); } + /** + * Called when the request is fully finished being handled. + * For every session in any context that the session has + * accessed, ensure that the session is completed. + */ + public void onCompleted() + { + if (_sessions != null) + { + for (Session s:_sessions) + leaveSession(s); + } + } + + /** + * Called when a response is about to be committed, ie sent + * back to the client + */ + public void onResponseCommit() + { + if (_sessions != null) + { + for (Session s:_sessions) + commitSession(s); + } + } + + /** + * Find a session that this request has already entered for the + * given SessionHandler + * + * @param sessionHandler the SessionHandler (ie context) to check + * @return + */ + public HttpSession getSession(SessionHandler sessionHandler) + { + if (_sessions == null || _sessions.size() == 0 || sessionHandler == null) + return null; + + HttpSession session = null; + + for (HttpSession s:_sessions) + { + Session ss = Session.class.cast(s); + if (sessionHandler == ss.getSessionHandler()) + { + session = s; + break; + } + } + return session; + } + /* * @see javax.servlet.http.HttpServletRequest#getSession() */ @@ -1770,7 +1883,9 @@ public class Request implements HttpServletRequest _inputState = INPUT_NONE; _multiParts = null; _remote = null; + _sessions = null; _input.recycle(); + _requestAttributeListeners.clear(); } /* @@ -1908,6 +2023,7 @@ public class Request implements HttpServletRequest else { _context = context; + _errorContext = context; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java index 95a9d83e533..b6d58845de1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; /** - * A HttpContent.Factory for transient content (not cached). The HttpContent's created by + * An HttpContent.Factory for transient content (not cached). The HttpContent's created by * this factory are not intended to be cached, so memory limits for individual * HttpOutput streams are enforced. */ 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 10d9f338343..d0f1f98da23 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 @@ -18,19 +18,19 @@ package org.eclipse.jetty.server; +import java.io.Closeable; import java.io.IOException; import java.io.PrintWriter; import java.nio.channels.IllegalSelectorException; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.List; +import java.util.Iterator; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; import javax.servlet.ServletResponseWrapper; @@ -43,6 +43,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.SameSite; import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; @@ -57,9 +58,8 @@ import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.RuntimeIOException; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; @@ -86,12 +86,6 @@ public class Response implements HttpServletResponse */ public static final String SET_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include."; - /** - * If this string is found within the comment of a cookie added with {@link #addCookie(Cookie)}, then the cookie - * will be set as HTTP ONLY. - */ - public static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__"; - private final HttpChannel _channel; private final HttpFields _fields = new HttpFields(); private final AtomicInteger _include = new AtomicInteger(); @@ -186,19 +180,10 @@ public class Response implements HttpServletResponse throw new IllegalArgumentException("Cookie.name cannot be blank/null"); String comment = cookie.getComment(); - boolean httpOnly = cookie.isHttpOnly(); - - if (comment != null) - { - int i = comment.indexOf(HTTP_ONLY_COMMENT); - if (i >= 0) - { - httpOnly = true; - comment = StringUtil.strip(comment.trim(), HTTP_ONLY_COMMENT); - if (comment.length() == 0) - comment = null; - } - } + // HttpOnly was supported as a comment in cookie flags before the java.net.HttpCookie implementation so need to check that + boolean httpOnly = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment); + SameSite sameSite = HttpCookie.getSameSiteFromComment(comment); + comment = HttpCookie.getCommentWithoutAttributes(comment); addCookie(new HttpCookie( cookie.getName(), @@ -209,7 +194,8 @@ public class Response implements HttpServletResponse httpOnly, cookie.getSecure(), comment, - cookie.getVersion())); + cookie.getVersion(), + sameSite)); } /** @@ -392,71 +378,40 @@ public class Response implements HttpServletResponse sendError(sc, null); } + /** + * Send an error response. + * In addition to the servlet standard handling, this method supports some additional codes:
+ *+ *
+ * @param code The error code + * @param message The message + * @throws IOException If an IO problem occurred sending the error response. + */ @Override public void sendError(int code, String message) throws IOException { if (isIncluding()) return; - if (isCommitted()) - { - if (LOG.isDebugEnabled()) - LOG.debug("Aborting on sendError on committed response {} {}", code, message); - code = -1; - } - else - resetBuffer(); - switch (code) { case -1: - _channel.abort(new IOException()); - return; - case 102: + _channel.abort(new IOException(message)); + break; + case HttpStatus.PROCESSING_102: sendProcessing(); - return; + break; default: + _channel.getState().sendError(code, message); break; } - - _outputType = OutputType.NONE; - setContentType(null); - setCharacterEncoding(null); - setHeader(HttpHeader.EXPIRES, null); - setHeader(HttpHeader.LAST_MODIFIED, null); - setHeader(HttpHeader.CACHE_CONTROL, null); - setHeader(HttpHeader.CONTENT_TYPE, null); - setHeader(HttpHeader.CONTENT_LENGTH, null); - - setStatus(code); - - Request request = _channel.getRequest(); - Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); - _reason = HttpStatus.getMessage(code); - if (message == null) - message = cause == null ? _reason : cause.toString(); - - // If we are allowed to have a body, then produce the error page. - if (code != SC_NO_CONTENT && code != SC_NOT_MODIFIED && - code != SC_PARTIAL_CONTENT && code >= SC_OK) - { - ContextHandler.Context context = request.getContext(); - ContextHandler contextHandler = context == null ? _channel.getState().getContextHandler() : context.getContextHandler(); - request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); - request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); - request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, request.getServletName()); - ErrorHandler errorHandler = ErrorHandler.getErrorHandler(_channel.getServer(), contextHandler); - if (errorHandler != null) - errorHandler.handle(null, request, request, this); - } - if (!request.isAsyncStarted()) - closeOutput(); } /** * Sends a 102-Processing response. - * If the connection is a HTTP connection, the version is 1.1 and the + * If the connection is an HTTP connection, the version is 1.1 and the * request has a Expect header starting with 102, then a 102 response is * sent. This indicates that the request still be processed and real response * can still be sent. This method is called by sendError if it is passed 102. @@ -663,8 +618,11 @@ public class Response implements HttpServletResponse throw new IllegalArgumentException(); if (!isIncluding()) { + // Null the reason only if the status is different. This allows + // a specific reason to be sent with setStatusWithReason followed by sendError. + if (_status != sc) + _reason = null; _status = sc; - _reason = null; } } @@ -727,6 +685,11 @@ public class Response implements HttpServletResponse return _outputType == OutputType.STREAM; } + public boolean isWritingOrStreaming() + { + return isWriting() || isStreaming(); + } + @Override public PrintWriter getWriter() throws IOException { @@ -835,21 +798,15 @@ public class Response implements HttpServletResponse public void closeOutput() throws IOException { - switch (_outputType) - { - case WRITER: - _writer.close(); - if (!_out.isClosed()) - _out.close(); - break; - case STREAM: - if (!_out.isClosed()) - getOutputStream().close(); - break; - default: - if (!_out.isClosed()) - _out.close(); - } + if (_outputType == OutputType.WRITER) + _writer.close(); + if (!_out.isClosed()) + _out.close(); + } + + public void closeOutput(Callback callback) + { + _out.close((_outputType == OutputType.WRITER) ? _writer : _out, callback); } public long getLongContentLength() @@ -1042,19 +999,20 @@ public class Response implements HttpServletResponse @Override public void reset() { - reset(false); - } - - public void reset(boolean preserveCookies) - { - resetForForward(); _status = 200; _reason = null; + _out.resetBuffer(); + _outputType = OutputType.NONE; _contentLength = -1; + _contentType = null; + _mimeType = null; + _characterEncoding = null; + _encodingFrom = EncodingFrom.NOT_SET; - List- 102
- Send a partial PROCESSING response and allow additional responses
+ *- -1
- Abort the HttpChannel and close the connection/stream
+ *cookies = preserveCookies ? _fields.getFields(HttpHeader.SET_COOKIE) : null; + // Clear all response headers _fields.clear(); + // recreate necessary connection related fields for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false)) { HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value); @@ -1077,21 +1035,57 @@ public class Response implements HttpServletResponse } } - if (preserveCookies) - cookies.forEach(_fields::add); - else + // recreate session cookies + Request request = getHttpChannel().getRequest(); + HttpSession session = request.getSession(false); + if (session != null && session.isNew()) { - Request request = getHttpChannel().getRequest(); - HttpSession session = request.getSession(false); - if (session != null && session.isNew()) + SessionHandler sh = request.getSessionHandler(); + if (sh != null) { - SessionHandler sh = request.getSessionHandler(); - if (sh != null) - { - HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); - if (c != null) - addCookie(c); - } + HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); + if (c != null) + addCookie(c); + } + } + } + + public void resetContent() + { + _out.resetBuffer(); + _outputType = OutputType.NONE; + _contentLength = -1; + _contentType = null; + _mimeType = null; + _characterEncoding = null; + _encodingFrom = EncodingFrom.NOT_SET; + + // remove the content related response headers and keep all others + for (Iterator i = getHttpFields().iterator(); i.hasNext(); ) + { + HttpField field = i.next(); + if (field.getHeader() == null) + continue; + + switch (field.getHeader()) + { + case CONTENT_TYPE: + case CONTENT_LENGTH: + case CONTENT_ENCODING: + case CONTENT_LANGUAGE: + case CONTENT_RANGE: + case CONTENT_MD5: + case CONTENT_LOCATION: + case TRANSFER_ENCODING: + case CACHE_CONTROL: + case LAST_MODIFIED: + case EXPIRES: + case ETAG: + case DATE: + case VARY: + i.remove(); + continue; + default: } } } @@ -1106,6 +1100,7 @@ public class Response implements HttpServletResponse public void resetBuffer() { _out.resetBuffer(); + _out.reopen(); } @Override @@ -1156,6 +1151,9 @@ public class Response implements HttpServletResponse @Override public boolean isCommitted() { + // If we are in sendError state, we pretend to be committed + if (_channel.isSendError()) + return true; return _channel.isCommitted(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java new file mode 100644 index 00000000000..805620745c4 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java @@ -0,0 +1,78 @@ +// +// ======================================================================== +// 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; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck; +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; + +/** + * Alias checking for working with FileSystems that normalize access to the + * File System. + * + * The Java {@link Files#isSameFile(Path, Path)} method is used to determine + * if the requested file is the same as the alias file. + *
+ *+ * For File Systems that are case insensitive (eg: Microsoft Windows FAT32 and NTFS), + * the access to the file can be in any combination or style of upper and lowercase. + *
+ *+ * For File Systems that normalize UTF-8 access (eg: Mac OSX on HFS+ or APFS, + * or Linux on XFS) the the actual file could be stored using UTF-16, + * but be accessed using NFD UTF-8 or NFC UTF-8 for the same file. + *
+ */ +public class SameFileAliasChecker implements AliasCheck +{ + private static final Logger LOG = Log.getLogger(SameFileAliasChecker.class); + + @Override + public boolean check(String uri, Resource resource) + { + // Only support PathResource alias checking + if (!(resource instanceof PathResource)) + return false; + + try + { + PathResource pathResource = (PathResource)resource; + Path path = pathResource.getPath(); + Path alias = pathResource.getAliasPath(); + + if (Files.isSameFile(path, alias)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Allow alias to same file {} --> {}", path, alias); + return true; + } + } + catch (IOException e) + { + LOG.ignore(e); + } + return false; + } +} 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 7568fdf00b0..8456899589a 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 @@ -57,7 +57,7 @@ import org.eclipse.jetty.util.component.AttributeContainerMap; 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.thread.Locker; +import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ShutdownThread; import org.eclipse.jetty.util.thread.ThreadPool; @@ -79,12 +79,12 @@ public class Server extends HandlerWrapper implements Attributes private final List_connectors = new CopyOnWriteArrayList<>(); private SessionIdManager _sessionIdManager; private boolean _stopAtShutdown; - private boolean _dumpAfterStart = false; - private boolean _dumpBeforeStop = false; + private boolean _dumpAfterStart; + private boolean _dumpBeforeStop; private ErrorHandler _errorHandler; private RequestLog _requestLog; - - private final Locker _dateLocker = new Locker(); + private boolean _dryRun; + private final AutoLock _dateLock = new AutoLock(); private volatile DateField _dateField; public Server() @@ -131,6 +131,16 @@ public class Server extends HandlerWrapper implements Attributes setServer(this); } + public boolean isDryRun() + { + return _dryRun; + } + + public void setDryRun(boolean dryRun) + { + _dryRun = dryRun; + } + public RequestLog getRequestLog() { return _requestLog; @@ -315,7 +325,7 @@ public class Server extends HandlerWrapper implements Attributes if (df == null || df._seconds != seconds) { - try (Locker.Lock lock = _dateLocker.lock()) + try (AutoLock lock = _dateLock.lock()) { df = _dateField; if (df == null || df._seconds != seconds) @@ -367,25 +377,33 @@ public class Server extends HandlerWrapper implements Attributes MultiException mex = new MultiException(); // Open network connector to ensure ports are available - _connectors.stream().filter(NetworkConnector.class::isInstance).map(NetworkConnector.class::cast).forEach(connector -> + if (!_dryRun) { - try + _connectors.stream().filter(NetworkConnector.class::isInstance).map(NetworkConnector.class::cast).forEach(connector -> { - connector.open(); - } - catch (Throwable th) - { - mex.add(th); - } - }); - - // Throw now if verified start sequence and there was an open exception - mex.ifExceptionThrow(); + try + { + connector.open(); + } + catch (Throwable th) + { + mex.add(th); + } + }); + // Throw now if verified start sequence and there was an open exception + mex.ifExceptionThrow(); + } // Start the server and components, but not connectors! // #start(LifeCycle) is overridden so that connectors are not started super.doStart(); + if (_dryRun) + { + LOG.info(String.format("Started(dry run) %s @%dms", this, Uptime.getUptime())); + throw new StopException(); + } + // start connectors for (Connector connector : _connectors) { @@ -402,7 +420,7 @@ public class Server extends HandlerWrapper implements Attributes } mex.ifExceptionThrow(); - LOG.info(String.format("Started @%dms", Uptime.getUptime())); + LOG.info(String.format("Started %s @%dms", this, Uptime.getUptime())); } catch (Throwable th) { @@ -423,7 +441,7 @@ public class Server extends HandlerWrapper implements Attributes } finally { - if (isDumpAfterStart()) + if (isDumpAfterStart() && !(_dryRun && isDumpBeforeStop())) dumpStdErr(); } } @@ -442,6 +460,7 @@ public class Server extends HandlerWrapper implements Attributes if (isDumpBeforeStop()) dumpStdErr(); + LOG.info(String.format("Stopped %s", this)); if (LOG.isDebugEnabled()) LOG.debug("doStop {}", this); @@ -514,10 +533,16 @@ public class Server extends HandlerWrapper implements Attributes if (HttpMethod.OPTIONS.is(request.getMethod()) || "*".equals(target)) { if (!HttpMethod.OPTIONS.is(request.getMethod())) + { + request.setHandled(true); response.sendError(HttpStatus.BAD_REQUEST_400); - handleOptions(request, response); - if (!request.isHandled()) - handle(target, request, request, response); + } + else + { + handleOptions(request, response); + if (!request.isHandled()) + handle(target, request, request, response); + } } else handle(target, request, request, response); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java index c9a8ef3c92c..6e50690ef7a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java @@ -35,7 +35,7 @@ import javax.servlet.http.Part; /** * ServletRequestHttpWrapper * - * Class to tunnel a ServletRequest via a HttpServletRequest + * Class to tunnel a ServletRequest via an HttpServletRequest */ public class ServletRequestHttpWrapper extends ServletRequestWrapper implements HttpServletRequest { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java index e35689fb072..3db98034fed 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java @@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletResponse; /** * ServletResponseHttpWrapper * - * Wrapper to tunnel a ServletResponse via a HttpServletResponse + * Wrapper to tunnel a ServletResponse via an HttpServletResponse */ public class ServletResponseHttpWrapper extends ServletResponseWrapper implements HttpServletResponse { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java index cb2e2257eb7..b02064d54a3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java @@ -123,7 +123,7 @@ public abstract class AbstractHandler extends ContainerLifeCycle implements Hand if (_server == server) return; if (isStarted()) - throw new IllegalStateException(STARTED); + throw new IllegalStateException(getState()); _server = server; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java index e56645f5dcf..5f533d6f549 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java @@ -128,7 +128,7 @@ public abstract class AbstractHandlerContainer extends AbstractHandler implement return; if (isStarted()) - throw new IllegalStateException(STARTED); + throw new IllegalStateException(getState()); super.setServer(server); Handler[] handlers = getHandlers(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java index c6186885d9a..c1dc168f9fa 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java @@ -52,7 +52,7 @@ public class AllowSymLinkAliasChecker implements AliasCheck Path path = pathResource.getPath(); Path alias = pathResource.getAliasPath(); - if (path.equals(alias)) + if (PathResource.isSameName(alias, path)) return false; // Unknown why this is an alias if (hasSymbolicLink(path) && Files.isSameFile(path, alias)) 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 a9066b6535b..6f2b3c563aa 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 @@ -126,7 +126,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu public static final int DEFAULT_LISTENER_TYPE_INDEX = 1; public static final int EXTENDED_LISTENER_TYPE_INDEX = 0; - private static final String __unimplmented = "Unimplemented - use org.eclipse.jetty.servlet.ServletContextHandler"; + private static final String UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER = "Unimplemented {} - use org.eclipse.jetty.servlet.ServletContextHandler"; private static final Logger LOG = Log.getLogger(ContextHandler.class); @@ -864,7 +864,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu * insert additional handling (Eg configuration) before the call to super.doStart by this method will start contained handlers. * * @throws Exception if unable to start the context - * @see org.eclipse.jetty.server.handler.ContextHandler.Context + * @see ContextHandler.Context */ protected void startContext() throws Exception { @@ -889,9 +889,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu protected void stopContext() throws Exception { - // stop all the handler hierarchy - super.doStop(); - // Call the context listeners ServletContextEvent event = new ServletContextEvent(_scontext); Collections.reverse(_destroySerletContextListeners); @@ -907,6 +904,17 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu ex.add(x); } } + + // stop all the handler hierarchy + try + { + super.doStop(); + } + catch (Exception x) + { + ex.add(x); + } + ex.ifExceptionThrow(); } @@ -1103,7 +1111,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu case UNAVAILABLE: baseRequest.setHandled(true); response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - return true; + return false; default: if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled())) return false; @@ -1138,8 +1146,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (oldContext != _scontext) { // check the target. - if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch) || - DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isAsync()) + if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch)) { if (_compactPath) target = URIUtil.compactPath(target); @@ -1276,29 +1283,11 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (new_context) requestInitialized(baseRequest, request); - switch (dispatch) + if (dispatch == DispatcherType.REQUEST && isProtectedTarget(target)) { - case REQUEST: - if (isProtectedTarget(target)) - { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - baseRequest.setHandled(true); - return; - } - break; - - case ERROR: - // If this is already a dispatch to an error page, proceed normally - if (Boolean.TRUE.equals(baseRequest.getAttribute(Dispatcher.__ERROR_DISPATCH))) - break; - - // We can just call doError here. If there is no error page, then one will - // be generated. If there is an error page, then a RequestDispatcher will be - // used to route the request through appropriate filters etc. - doError(target, baseRequest, request, response); - return; - default: - break; + baseRequest.setHandled(true); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; } nextHandle(target, baseRequest, request, response); @@ -2250,7 +2239,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu @Override public void log(String message, Throwable throwable) { - _logger.warn(message, throwable); + if (throwable == null) + _logger.warn(message); + else + _logger.warn(message, throwable); } /* @@ -2510,7 +2502,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu @Override public JspConfigDescriptor getJspConfigDescriptor() { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getJspConfigDescriptor()"); return null; } @@ -2703,138 +2695,141 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu @Override public Dynamic addFilter(String filterName, Class extends Filter> filterClass) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addFilter(String, Class)"); return null; } @Override public Dynamic addFilter(String filterName, Filter filter) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addFilter(String, Filter)"); return null; } @Override public Dynamic addFilter(String filterName, String className) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addFilter(String, String)"); return null; } @Override public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, Class extends Servlet> servletClass) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addServlet(String, Class)"); return null; } @Override public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addServlet(String, Servlet)"); return null; } @Override public javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, String className) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addServlet(String, String)"); return null; } + /** + * @since Servlet 4.0 + */ @Override public ServletRegistration.Dynamic addJspFile(String servletName, String jspFile) { // TODO new in 4.0 - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addJspFile(String, String)"); return null; } @Override public T createFilter(Class c) throws ServletException { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "createFilter(Class)"); return null; } @Override public T createServlet(Class c) throws ServletException { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "createServlet(Class)"); return null; } @Override public Set getDefaultSessionTrackingModes() { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getDefaultSessionTrackingModes()"); return null; } @Override public Set getEffectiveSessionTrackingModes() { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getEffectiveSessionTrackingModes()"); return null; } @Override public FilterRegistration getFilterRegistration(String filterName) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getFilterRegistration(String)"); return null; } @Override public Map getFilterRegistrations() { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getFilterRegistrations()"); return null; } @Override public ServletRegistration getServletRegistration(String servletName) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getServletRegistration(String)"); return null; } @Override public Map getServletRegistrations() { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getServletRegistrations()"); return null; } @Override public SessionCookieConfig getSessionCookieConfig() { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getSessionCookieConfig()"); return null; } @Override public void setSessionTrackingModes(Set sessionTrackingModes) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "setSessionTrackingModes(Set )"); } @Override public void addListener(String className) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addListener(String)"); } @Override public void addListener(T t) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addListener(T)"); } @Override public void addListener(Class extends EventListener> listenerClass) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addListener(Class)"); } @Override @@ -2881,14 +2876,14 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu @Override public JspConfigDescriptor getJspConfigDescriptor() { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getJspConfigDescriptor()"); return null; } @Override public void declareRoles(String... roleNames) { - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "declareRoles(String...)"); } @Override @@ -2897,49 +2892,67 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu return null; } + /** + * @since Servlet 4.0 + */ @Override public int getSessionTimeout() { // TODO new in 4.0 - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getSessionTimeout()"); return 0; } + /** + * @since Servlet 4.0 + */ @Override public void setSessionTimeout(int sessionTimeout) { // TODO new in 4.0 - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "setSessionTimeout(int)"); } + /** + * @since Servlet 4.0 + */ @Override public String getRequestCharacterEncoding() { // TODO new in 4.0 - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getRequestCharacterEncoding()"); return null; } + /** + * @since Servlet 4.0 + */ @Override public void setRequestCharacterEncoding(String encoding) { // TODO new in 4.0 - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "setRequestCharacterEncoding(String)"); } + /** + * @since Servlet 4.0 + */ @Override public String getResponseCharacterEncoding() { // TODO new in 4.0 - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "getResponseCharacterEncoding()"); return null; } + /** + * @since Servlet 4.0 + */ @Override public void setResponseCharacterEncoding(String encoding) { // TODO new in 4.0 - LOG.warn(__unimplmented); + LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "setResponseCharacterEncoding(String)"); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index a1a560ec9c0..c46b31e342c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -19,28 +19,29 @@ package org.eclipse.jetty.server.handler; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.StringWriter; import java.io.Writer; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.QuotedQualityCSV; +import org.eclipse.jetty.io.ByteBufferOutputStream; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -49,15 +50,18 @@ import org.eclipse.jetty.util.log.Logger; * Handler for Error pages * An ErrorHandler is registered with {@link ContextHandler#setErrorHandler(ErrorHandler)} or * {@link Server#setErrorHandler(ErrorHandler)}. - * It is called by the HttpResponse.sendError method to write a error page via {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)} + * It is called by the HttpResponse.sendError method to write an error page via {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)} * or via {@link #badMessageError(int, String, HttpFields)} for bad requests for which a dispatch cannot be done. */ public class ErrorHandler extends AbstractHandler { + // TODO This classes API needs to be majorly refactored/cleanup in jetty-10 private static final Logger LOG = Log.getLogger(ErrorHandler.class); public static final String ERROR_PAGE = "org.eclipse.jetty.server.error_page"; + public static final String ERROR_CONTEXT = "org.eclipse.jetty.server.error_context"; boolean _showStacks = true; + boolean _disableStacks = false; boolean _showMessageInTitle = true; String _cacheControl = "must-revalidate,no-cache,no-store"; @@ -65,6 +69,19 @@ public class ErrorHandler extends AbstractHandler { } + public boolean errorPageForMethod(String method) + { + switch (method) + { + case "GET": + case "POST": + case "HEAD": + return true; + default: + return false; + } + } + /* * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) */ @@ -77,71 +94,13 @@ public class ErrorHandler extends AbstractHandler @Override public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - String method = request.getMethod(); - if (!HttpMethod.GET.is(method) && !HttpMethod.POST.is(method) && !HttpMethod.HEAD.is(method)) - { - baseRequest.setHandled(true); - return; - } + String cacheControl = getCacheControl(); + if (cacheControl != null) + response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl); - if (this instanceof ErrorPageMapper) - { - String errorPage = ((ErrorPageMapper)this).getErrorPage(request); - if (errorPage != null) - { - String oldErrorPage = (String)request.getAttribute(ERROR_PAGE); - ContextHandler.Context context = baseRequest.getContext(); - if (context == null) - context = ContextHandler.getCurrentContext(); - if (context == null) - { - LOG.warn("No ServletContext for error page {}", errorPage); - } - else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) - { - LOG.warn("Error page loop {}", errorPage); - } - else - { - request.setAttribute(ERROR_PAGE, errorPage); - - Dispatcher dispatcher = (Dispatcher)context.getRequestDispatcher(errorPage); - try - { - if (LOG.isDebugEnabled()) - LOG.debug("error page dispatch {}->{}", errorPage, dispatcher); - if (dispatcher != null) - { - dispatcher.error(request, response); - return; - } - LOG.warn("No error page found " + errorPage); - } - catch (ServletException e) - { - LOG.warn(Log.EXCEPTION, e); - return; - } - } - } - else - { - if (LOG.isDebugEnabled()) - { - LOG.debug("No Error Page mapping for request({} {}) (using default)", request.getMethod(), request.getRequestURI()); - } - } - } - - if (_cacheControl != null) - response.setHeader(HttpHeader.CACHE_CONTROL.asString(), _cacheControl); - - String message = (String)request.getAttribute(RequestDispatcher.ERROR_MESSAGE); + String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE); if (message == null) message = baseRequest.getResponse().getReason(); - if (message == null) - message = HttpStatus.getMessage(response.getStatus()); - generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message); } @@ -151,7 +110,7 @@ public class ErrorHandler extends AbstractHandler * acceptable to the user-agent. The Accept header is evaluated in * quality order and the method * {@link #generateAcceptableResponse(Request, HttpServletRequest, HttpServletResponse, int, String, String)} - * is called for each mimetype until {@link Request#isHandled()} is true. + * is called for each mimetype until the response is written to or committed. * * @param baseRequest The base request * @param request The servlet request (may be wrapped) @@ -174,48 +133,10 @@ public class ErrorHandler extends AbstractHandler for (String mimeType : acceptable) { generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); - if (response.isCommitted() || baseRequest.getResponse().isWriting() || baseRequest.getResponse().isStreaming()) + if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming()) break; } } - baseRequest.getResponse().closeOutput(); - } - - /** - * Generate an acceptable error response for a mime type. - * This method is called for each mime type in the users agent's - *
Accept
header, until {@link Request#isHandled()} is true and a - * response of the appropriate type is generated. - * - * @param baseRequest The base request - * @param request The servlet request (may be wrapped) - * @param response The response (may be wrapped) - * @param code the http error code - * @param message the http error message - * @param mimeType The mimetype to generate (may be */*or other wildcard) - * @throws IOException if a response cannot be generated - */ - protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String mimeType) - throws IOException - { - switch (mimeType) - { - case "text/html": - case "text/*": - case "*/*": - { - baseRequest.setHandled(true); - Writer writer = getAcceptableWriter(baseRequest, request, response); - if (writer != null) - { - response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); - handleErrorPage(request, writer, code, message); - } - break; - } - default: - break; - } } /** @@ -236,6 +157,7 @@ public class ErrorHandler extends AbstractHandler * @return A {@link Writer} if there is a known acceptable charset or null * @throws IOException if a Writer cannot be returned */ + @Deprecated protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -264,6 +186,139 @@ public class ErrorHandler extends AbstractHandler return null; } + /** + * Generate an acceptable error response for a mime type. + *This method is called for each mime type in the users agent's + *
+ *Accept
header, until {@link Request#isHandled()} is true and a + * response of the appropriate type is generated. + *The default implementation handles "text/html", "text/*" and "*/*". + * The method can be overridden to handle other types. Implementations must + * immediate produce a response and may not be async. + *
+ * + * @param baseRequest The base request + * @param request The servlet request (may be wrapped) + * @param response The response (may be wrapped) + * @param code the http error code + * @param message the http error message + * @param contentType The mimetype to generate (may be */*or other wildcard) + * @throws IOException if a response cannot be generated + */ + protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String contentType) + throws IOException + { + // We can generate an acceptable contentType, but can we generate an acceptable charset? + // TODO refactor this in jetty-10 to be done in the other calling loop + Charset charset = null; + Listacceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET); + if (!acceptable.isEmpty()) + { + for (String name : acceptable) + { + if ("*".equals(name)) + { + charset = StandardCharsets.UTF_8; + break; + } + + try + { + charset = Charset.forName(name); + } + catch (Exception e) + { + LOG.ignore(e); + } + } + if (charset == null) + return; + } + + MimeTypes.Type type; + switch (contentType) + { + case "text/html": + case "text/*": + case "*/*": + type = MimeTypes.Type.TEXT_HTML; + if (charset == null) + charset = StandardCharsets.ISO_8859_1; + break; + + case "text/json": + case "application/json": + type = MimeTypes.Type.TEXT_JSON; + if (charset == null) + charset = StandardCharsets.UTF_8; + break; + + case "text/plain": + type = MimeTypes.Type.TEXT_PLAIN; + if (charset == null) + charset = StandardCharsets.ISO_8859_1; + break; + + default: + return; + } + + // write into the response aggregate buffer and flush it asynchronously. + while (true) + { + try + { + // TODO currently the writer used here is of fixed size, so a large + // TODO error page may cause a BufferOverflow. In which case we try + // TODO again with stacks disabled. If it still overflows, it is + // TODO written without a body. + ByteBuffer buffer = baseRequest.getResponse().getHttpOutput().acquireBuffer(); + ByteBufferOutputStream out = new ByteBufferOutputStream(buffer); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset)); + + switch (type) + { + case TEXT_HTML: + response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); + response.setCharacterEncoding(charset.name()); + handleErrorPage(request, writer, code, message); + break; + case TEXT_JSON: + response.setContentType(contentType); + writeErrorJson(request, writer, code, message); + break; + case TEXT_PLAIN: + response.setContentType(MimeTypes.Type.TEXT_PLAIN.asString()); + response.setCharacterEncoding(charset.name()); + writeErrorPlain(request, writer, code, message); + break; + default: + throw new IllegalStateException(); + } + + writer.flush(); + break; + } + catch (BufferOverflowException e) + { + LOG.warn("Error page too large: {} {} {}", code, message, request); + if (LOG.isDebugEnabled()) + LOG.warn(e); + baseRequest.getResponse().resetContent(); + if (!_disableStacks) + { + LOG.info("Disabling showsStacks for " + this); + _disableStacks = true; + continue; + } + break; + } + } + + // Do an asynchronous completion. + baseRequest.getHttpChannel().sendResponseAndComplete(); + } + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { @@ -288,12 +343,13 @@ public class ErrorHandler extends AbstractHandler { writer.write("\n"); writer.write(" Error "); - writer.write(Integer.toString(code)); - - if (_showMessageInTitle) + // TODO this code is duplicated in writeErrorPageMessage + String status = Integer.toString(code); + writer.write(status); + if (message != null && !message.equals(status)) { writer.write(' '); - write(writer, message); + writer.write(StringUtil.sanitizeXmlString(message)); } writer.write(" \n"); } @@ -304,7 +360,7 @@ public class ErrorHandler extends AbstractHandler String uri = request.getRequestURI(); writeErrorPageMessage(request, writer, code, message, uri); - if (showStacks) + if (showStacks && !_disableStacks) writeErrorPageStacks(request, writer); Request.getBaseRequest(request).getHttpChannel().getHttpConfiguration() @@ -315,35 +371,103 @@ public class ErrorHandler extends AbstractHandler throws IOException { writer.write("HTTP ERROR "); + String status = Integer.toString(code); + writer.write(status); + if (message != null && !message.equals(status)) + { + writer.write(' '); + writer.write(StringUtil.sanitizeXmlString(message)); + } + writer.write("
\n"); + writer.write("\n"); + htmlRow(writer, "URI", uri); + htmlRow(writer, "STATUS", status); + htmlRow(writer, "MESSAGE", message); + htmlRow(writer, "SERVLET", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + while (cause != null) + { + htmlRow(writer, "CAUSED BY", cause); + cause = cause.getCause(); + } + writer.write("
\n"); + } + + private void htmlRow(Writer writer, String tag, Object value) + throws IOException + { + writer.write("\n"); + } + + private void writeErrorPlain(HttpServletRequest request, PrintWriter writer, int code, String message) + { + writer.write("HTTP ERROR "); writer.write(Integer.toString(code)); - writer.write("\n "); + writer.write(tag); + writer.write(": "); + if (value == null) + writer.write("-"); + else + writer.write(StringUtil.sanitizeXmlString(value.toString())); + writer.write(" Problem accessing "); - write(writer, uri); - writer.write(". Reason:\n
"); - write(writer, message); - writer.write(""); + writer.write(' '); + writer.write(StringUtil.sanitizeXmlString(message)); + writer.write("\n"); + writer.printf("URI: %s%n", request.getRequestURI()); + writer.printf("STATUS: %s%n", code); + writer.printf("MESSAGE: %s%n", message); + writer.printf("SERVLET: %s%n", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + while (cause != null) + { + writer.printf("CAUSED BY %s%n", cause); + if (_showStacks && !_disableStacks) + cause.printStackTrace(writer); + cause = cause.getCause(); + } + } + + private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int code, String message) + { + writer + .append("{\n") + .append(" url: \"").append(request.getRequestURI()).append("\",\n") + .append(" status: \"").append(Integer.toString(code)).append("\",\n") + .append(" message: ").append(QuotedStringTokenizer.quote(message)).append(",\n"); + Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME); + if (servlet != null) + writer.append("servlet: \"").append(servlet.toString()).append("\",\n"); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + int c = 0; + while (cause != null) + { + writer.append(" cause").append(Integer.toString(c++)).append(": ") + .append(QuotedStringTokenizer.quote(cause.toString())).append(",\n"); + cause = cause.getCause(); + } + writer.append("}"); } protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) throws IOException { Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); - while (th != null) + if (_showStacks && th != null) { - writer.write("Caused by:
"); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - th.printStackTrace(pw); - pw.flush(); - write(writer, sw.getBuffer().toString()); + PrintWriter pw = writer instanceof PrintWriter ? (PrintWriter)writer : new PrintWriter(writer); + pw.write(""); + while (th != null) + { + th.printStackTrace(pw); + th = th.getCause(); + } writer.write("\n"); - - th = th.getCause(); } } /** * Bad Message Error body - *Generate a error response body to be sent for a bad message. + *
Generate an error response body to be sent for a bad message. * In this case there is something wrong with the request, so either * a request cannot be built, or it is not safe to build a request. * This method allows for a simple error page body to be returned 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 dfea9fc93b7..9bbda66b116 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 @@ -82,7 +82,7 @@ public class HandlerCollection extends AbstractHandlerContainer public void setHandlers(Handler[] handlers) { if (!_mutableWhenRunning && isStarted()) - throw new IllegalStateException(STARTED); + throw new IllegalStateException(getState()); while (true) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java index 27a53fecf98..0c0897f5c1f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java @@ -74,7 +74,7 @@ public class HandlerWrapper extends AbstractHandlerContainer public void setHandler(Handler handler) { if (isStarted()) - throw new IllegalStateException(STARTED); + throw new IllegalStateException(getState()); // check for loops if (handler == this || (handler instanceof HandlerContainer && diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java index 947cf9ce5f2..f9baab903fa 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java @@ -38,7 +38,7 @@ import org.eclipse.jetty.util.log.Logger; * A handler that shuts the server down on a valid request. Used to do "soft" restarts from Java. * If _exitJvm is set to true a hard System.exit() call is being made. * If _sendShutdownAtStart is set to true, starting the server will try to shut down an existing server at the same port. - * If _sendShutdownAtStart is set to true, make a http call to + * If _sendShutdownAtStart is set to true, make an http call to * "http://localhost:" + port + "/shutdown?token=" + shutdownCookie * in order to shut down the server. * @@ -93,7 +93,7 @@ public class ShutdownHandler extends HandlerWrapper /** * @param shutdownToken a secret password to avoid unauthorized shutdown attempts * @param exitJVM If true, when the shutdown is executed, the handler class System.exit() - * @param sendShutdownAtStart If true, a shutdown is sent as a HTTP post + * @param sendShutdownAtStart If true, a shutdown is sent as an HTTP post * during startup, which will shutdown any previously running instances of * this server with an identically configured ShutdownHandler */ @@ -190,8 +190,9 @@ public class ShutdownHandler extends HandlerWrapper connector.shutdown(); } - response.sendError(200, "Connectors closed, commencing full shutdown"); baseRequest.setHandled(true); + response.setStatus(200); + response.flushBuffer(); final Server server = getServer(); new Thread() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java index 7666bcb0641..feab2f5644a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ThreadLimitHandler.java @@ -48,7 +48,7 @@ import org.eclipse.jetty.util.annotation.ManagedOperation; import org.eclipse.jetty.util.annotation.Name; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Locker; +import org.eclipse.jetty.util.thread.AutoLock; /** *
Handler to limit the threads per IP address for DOS protection
@@ -241,7 +241,7 @@ public class ThreadLimitHandler extends HandlerWrapper } } - protected Remote getRemote(Request baseRequest) + private Remote getRemote(Request baseRequest) { Remote remote = (Remote)baseRequest.getAttribute(REMOTE); if (remote != null) @@ -329,11 +329,11 @@ public class ThreadLimitHandler extends HandlerWrapper return (comma >= 0) ? forwardedFor.substring(comma + 1).trim() : forwardedFor; } - private final class Remote implements Closeable + private static final class Remote implements Closeable { private final String _ip; private final int _limit; - private final Locker _locker = new Locker(); + private final AutoLock _lock = new AutoLock(); private int _permits; private Deque> _queue = new ArrayDeque<>(); private final CompletableFuture _permitted = CompletableFuture.completedFuture(this); @@ -346,7 +346,7 @@ public class ThreadLimitHandler extends HandlerWrapper public CompletableFuture acquire() { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { // Do we have available passes? if (_permits < _limit) @@ -358,16 +358,16 @@ public class ThreadLimitHandler extends HandlerWrapper } // No pass available, so queue a new future - CompletableFuture pass = new CompletableFuture (); + CompletableFuture pass = new CompletableFuture<>(); _queue.addLast(pass); return pass; } } @Override - public void close() throws IOException + public void close() { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { // reduce the allocated passes _permits--; @@ -396,14 +396,14 @@ public class ThreadLimitHandler extends HandlerWrapper @Override public String toString() { - try (Locker.Lock lock = _locker.lock()) + try (AutoLock lock = _lock.lock()) { return String.format("R[ip=%s,p=%d,l=%d,q=%d]", _ip, _permits, _limit, _queue.size()); } } } - private final class RFC7239 extends QuotedCSV + private static final class RFC7239 extends QuotedCSV { String _for; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java index 5c590b2d3e5..bb1022bbb7b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java @@ -34,6 +34,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.CompressedContentFormat; import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; @@ -422,7 +423,8 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory @Override public Deflater getDeflater(Request request, long contentLength) { - String ua = request.getHttpFields().get(HttpHeader.USER_AGENT); + HttpFields httpFields = request.getHttpFields(); + String ua = httpFields.get(HttpHeader.USER_AGENT); if (ua != null && !isAgentGzipable(ua)) { LOG.debug("{} excluded user agent {}", this, request); @@ -436,16 +438,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory } // check the accept encoding header - HttpField accept = request.getHttpFields().getField(HttpHeader.ACCEPT_ENCODING); - - if (accept == null) - { - LOG.debug("{} excluded !accept {}", this, request); - return null; - } - boolean gzip = accept.contains("gzip"); - - if (!gzip) + if (!httpFields.contains(HttpHeader.ACCEPT_ENCODING, "gzip")) { LOG.debug("{} excluded not gzip accept {}", this, request); return null; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java index 9fedb2b3938..c099e09abfd 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java @@ -27,7 +27,7 @@ import org.eclipse.jetty.server.HttpInput.Content; import org.eclipse.jetty.util.component.Destroyable; /** - * A HttpInput Interceptor that inflates GZIP encoded request content. + * An HttpInput Interceptor that inflates GZIP encoded request content. */ public class GzipHttpInputInterceptor implements HttpInput.Interceptor, Destroyable { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCache.java index 62830cd60fe..329a44bec48 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCache.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCache.java @@ -29,7 +29,7 @@ import org.eclipse.jetty.util.annotation.ManagedObject; 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.thread.Locker.Lock; +import org.eclipse.jetty.util.thread.AutoLock; /** * AbstractSessionCache @@ -91,6 +91,12 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements * deleted from the SessionDataStore. */ protected boolean _removeUnloadableSessions; + + /** + * If true, when a response is about to be committed back to the client, + * a dirty session will be flushed to the session store. + */ + protected boolean _flushOnResponseCommit; /** * Create a new Session object from pre-existing session data @@ -110,9 +116,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements */ public abstract Session newSession(HttpServletRequest request, SessionData data); - /** - * @see org.eclipse.jetty.server.session.SessionCache#newSession(javax.servlet.http.HttpServletRequest, java.lang.String, long, long) - */ @Override public Session newSession(HttpServletRequest request, String id, long time, long maxInactiveMs) { @@ -133,12 +136,11 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements } /** - * Get the session matching the key * * @param id session id * @return the Session object matching the id */ - public abstract Session doGet(String id); + protected abstract Session doGet(String id); /** * Put the session into the map if it wasn't already there @@ -147,7 +149,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements * @param session the session object * @return null if the session wasn't already in the map, or the existing entry otherwise */ - public abstract Session doPutIfAbsent(String id, Session session); + protected abstract Session doPutIfAbsent(String id, Session session); /** * Replace the mapping from id to oldValue with newValue @@ -157,7 +159,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements * @param newValue the new value * @return true if replacement was done */ - public abstract boolean doReplace(String id, Session oldValue, Session newValue); + protected abstract boolean doReplace(String id, Session oldValue, Session newValue); /** * Remove the session with this identity from the store @@ -170,7 +172,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements /** * PlaceHolder */ - protected class PlaceHolderSession extends Session + protected static class PlaceHolderSession extends Session { /** @@ -200,9 +202,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements return _handler; } - /** - * @see org.eclipse.jetty.server.session.SessionCache#initialize(org.eclipse.jetty.server.session.SessionContext) - */ @Override public void initialize(SessionContext context) { @@ -249,9 +248,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements return _sessionDataStore; } - /** - * @see org.eclipse.jetty.server.session.SessionCache#setSessionDataStore(org.eclipse.jetty.server.session.SessionDataStore) - */ @Override public void setSessionDataStore(SessionDataStore sessionStore) { @@ -259,9 +255,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements _sessionDataStore = sessionStore; } - /** - * @see org.eclipse.jetty.server.session.SessionCache#getEvictionPolicy() - */ @ManagedAttribute(value = "session eviction policy", readonly = true) @Override public int getEvictionPolicy() @@ -273,8 +266,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements * -1 means we never evict inactive sessions. * 0 means we evict a session after the last request for it exits * >0 is the number of seconds after which we evict inactive sessions from the cache - * - * @see org.eclipse.jetty.server.session.SessionCache#setEvictionPolicy(int) */ @Override public void setEvictionPolicy(int evictionTimeout) @@ -309,7 +300,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements * If a session's data cannot be loaded from the store without error, remove * it from the persistent store. * - * @param removeUnloadableSessions if true
unloadable sessions will be removed from session store + * @param removeUnloadableSessions if {@code true} unloadable sessions will be removed from session store */ @Override public void setRemoveUnloadableSessions(boolean removeUnloadableSessions) @@ -317,17 +308,43 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements _removeUnloadableSessions = removeUnloadableSessions; } + @Override + public void setFlushOnResponseCommit(boolean flushOnResponseCommit) + { + _flushOnResponseCommit = flushOnResponseCommit; + } + + @Override + public boolean isFlushOnResponseCommit() + { + return _flushOnResponseCommit; + } + /** * Get a session object. * * If the session object is not in this session store, try getting * the data for it from a SessionDataStore associated with the - * session manager. - * - * @see org.eclipse.jetty.server.session.SessionCache#get(java.lang.String) + * session manager. The usage count of the session is incremented. */ @Override public Session get(String id) throws Exception + { + return getAndEnter(id, true); + } + + /** Get a session object. + * + * If the session object is not in this session store, try getting + * the data for it from a SessionDataStore associated with the + * session manager. + * + * @param id The session to retrieve + * @param enter if true, the usage count of the session will be incremented + * @return the Session object + * @throws Exception if the session cannot be retrieved + */ + protected Session getAndEnter(String id, boolean enter) throws Exception { Session session = null; Exception ex = null; @@ -342,11 +359,11 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements if (session == null) { if (LOG.isDebugEnabled()) - LOG.debug("Session {} not found locally, attempting to load", id); + LOG.debug("Session {} not found locally in {}, attempting to load", id, this); //didn't get a session, try and create one and put in a placeholder for it PlaceHolderSession phs = new PlaceHolderSession(_handler, new SessionData(id, null, null, 0, 0, 0, 0)); - Lock phsLock = phs.lock(); + AutoLock phsLock = phs.lock(); Session s = doPutIfAbsent(id, phs); if (s == null) { @@ -362,7 +379,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements break; } - try (Lock lock = session.lock()) + try (AutoLock lock = session.lock()) { //swap it in instead of the placeholder boolean success = doReplace(id, phs, session); @@ -379,6 +396,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements { //successfully swapped in the session session.setResident(true); + if (enter) + session.use(); phsLock.close(); break; } @@ -397,7 +416,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements { //my placeholder didn't win, check the session returned phsLock.close(); - try (Lock lock = s.lock()) + try (AutoLock lock = s.lock()) { //is it a placeholder? or is a non-resident session? In both cases, chuck it away and start again if (!s.isResident() || s instanceof PlaceHolderSession) @@ -405,7 +424,10 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements session = null; continue; } + //I will use this session too session = s; + if (enter) + session.use(); break; } } @@ -413,7 +435,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements else { //check the session returned - try (Lock lock = session.lock()) + try (AutoLock lock = session.lock()) { //is it a placeholder? or is it passivated? In both cases, chuck it away and start again if (!session.isResident() || session instanceof PlaceHolderSession) @@ -423,6 +445,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements } //got the session + if (enter) + session.use(); break; } } @@ -469,7 +493,80 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements } /** - * Put the Session object back into the session store. + * Add an entirely new session (created by the application calling Request.getSession(true)) + * to the cache. The usage count of the fresh session is incremented. + * + * @param id the session id + * @param session the new session to add + */ + @Override + public void add(String id, Session session) throws Exception + { + if (id == null || session == null) + throw new IllegalArgumentException("Add key=" + id + " session=" + (session == null ? "null" : session.getId())); + + try (AutoLock lock = session.lock()) + { + if (session.getSessionHandler() == null) + throw new IllegalStateException("Session " + id + " is not managed"); + + if (!session.isValid()) + throw new IllegalStateException("Session " + id + " is not valid"); + + if (doPutIfAbsent(id, session) == null) + { + session.setResident(true); //its in the cache + session.use(); //the request is using it + } + else + throw new IllegalStateException("Session " + id + " already in cache"); + } + } + + /** + * A response that has accessed this session is about to + * be returned to the client. Pass the session to the store + * to persist, so that any changes will be visible to + * subsequent requests on the same node (if using NullSessionCache), + * or on other nodes. + */ + @Override + public void commit(Session session) throws Exception + { + if (session == null) + return; + + try (AutoLock lock = session.lock()) + { + //only write the session out at this point if the attributes changed. If only + //the lastAccess/expiry time changed defer the write until the last request exits + if (session.getSessionData().isDirty() && _flushOnResponseCommit) + { + if (LOG.isDebugEnabled()) + LOG.debug("Flush session {} on response commit", session); + //save the session + if (!_sessionDataStore.isPassivating()) + { + _sessionDataStore.store(session.getId(), session.getSessionData()); + } + else + { + session.willPassivate(); + _sessionDataStore.store(session.getId(), session.getSessionData()); + session.didActivate(); + } + } + } + } + + @Override + public void put(String id, Session session) throws Exception + { + release(id, session); + } + + /** + * Finish using the Session object. * * This should be called when a request exists the session. Only when the last * simultaneous request exists the session will any action be taken. @@ -480,23 +577,23 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements * * If the evictionPolicy == SessionCache.EVICT_ON_SESSION_EXIT then after we have saved * the session, we evict it from the cache. - * - * @see org.eclipse.jetty.server.session.SessionCache#put(java.lang.String, org.eclipse.jetty.server.session.Session) */ @Override - public void put(String id, Session session) throws Exception + public void release(String id, Session session) throws Exception { if (id == null || session == null) throw new IllegalArgumentException("Put key=" + id + " session=" + (session == null ? "null" : session.getId())); - try (Lock lock = session.lock()) + try (AutoLock lock = session.lock()) { if (session.getSessionHandler() == null) throw new IllegalStateException("Session " + id + " is not managed"); - if (!session.isValid()) + if (session.isInvalid()) return; + session.complete(); + //don't do anything with the session until the last request for it has finished if ((session.getRequests() <= 0)) { @@ -566,7 +663,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements * it will check with the data store. * * @throws Exception the Exception - * @see org.eclipse.jetty.server.session.SessionCache#exists(java.lang.String) */ @Override public boolean exists(String id) throws Exception @@ -575,7 +671,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements Session s = doGet(id); if (s != null) { - try (Lock lock = s.lock()) + try (AutoLock lock = s.lock()) { //wait for the lock and check the validity of the session return s.isValid(); @@ -589,8 +685,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements /** * Check to see if this cache contains an entry for the session * corresponding to the session id. - * - * @see org.eclipse.jetty.server.session.SessionCache#contains(java.lang.String) */ @Override public boolean contains(String id) throws Exception @@ -601,14 +695,12 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements /** * Remove a session object from this store and from any backing store. - * - * @see org.eclipse.jetty.server.session.SessionCache#delete(java.lang.String) */ @Override public Session delete(String id) throws Exception { //get the session, if its not in memory, this will load it - Session session = get(id); + Session session = getAndEnter(id, false); //Always delete it from the backing data store if (_sessionDataStore != null) @@ -627,9 +719,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements return doDelete(id); } - /** - * @see org.eclipse.jetty.server.session.SessionCache#checkExpiration(Set) - */ @Override public SetcheckExpiration(Set candidates) { @@ -677,9 +766,10 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements if (LOG.isDebugEnabled()) LOG.debug("Checking for idle {}", session.getId()); - try (Lock s = session.lock()) + try (AutoLock lock = session.lock()) { - if (getEvictionPolicy() > 0 && session.isIdleLongerThan(getEvictionPolicy()) && session.isValid() && session.isResident() && session.getRequests() <= 0) + if (getEvictionPolicy() > 0 && session.isIdleLongerThan(getEvictionPolicy()) && + session.isValid() && session.isResident() && session.getRequests() <= 0) { //Be careful with saveOnInactiveEviction - you may be able to re-animate a session that was //being managed on another node and has expired. @@ -694,6 +784,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements if (_sessionDataStore.isPassivating()) session.willPassivate(); + //Fake being dirty to force the write + session.getSessionData().setDirty(true); _sessionDataStore.store(session.getId(), session.getSessionData()); } @@ -703,7 +795,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements catch (Exception e) { LOG.warn("Passivation of idle session {} failed", session.getId(), e); - //session.updateInactivityTimer(); } } } @@ -718,7 +809,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements if (StringUtil.isBlank(newId)) throw new IllegalArgumentException("New session id is null"); - Session session = get(oldId); + Session session = getAndEnter(oldId, true); renewSessionId(session, newId, newExtendedId); return session; @@ -738,7 +829,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements if (session == null) return; - try (Lock lock = session.lock()) + try (AutoLock lock = session.lock()) { final String oldId = session.getId(); session.checkValidForWrite(); //can't change id on invalid session @@ -761,9 +852,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements } } - /** - * @see org.eclipse.jetty.server.session.SessionCache#setSaveOnInactiveEviction(boolean) - */ @Override public void setSaveOnInactiveEviction(boolean saveOnEvict) { @@ -787,6 +875,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements public String toString() { return String.format("%s@%x[evict=%d,removeUnloadable=%b,saveOnCreate=%b,saveOnInactiveEvict=%b]", - this.getClass().getName(), this.hashCode(), _evictionPolicy, _removeUnloadableSessions, _saveOnCreate, _saveOnInactiveEviction); + this.getClass().getName(), this.hashCode(), _evictionPolicy, + _removeUnloadableSessions, _saveOnCreate, _saveOnInactiveEviction); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCacheFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCacheFactory.java new file mode 100644 index 00000000000..b29e5585a19 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCacheFactory.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.server.session; + +/** + * AbstractSessionCacheFactory + * + * Base class for SessionCacheFactories. + * + */ +public abstract class AbstractSessionCacheFactory implements SessionCacheFactory +{ + int _evictionPolicy; + boolean _saveOnInactiveEvict; + boolean _saveOnCreate; + boolean _removeUnloadableSessions; + boolean _flushOnResponseCommit; + + /** + * @return the flushOnResponseCommit + */ + public boolean isFlushOnResponseCommit() + { + return _flushOnResponseCommit; + } + + /** + * @param flushOnResponseCommit the flushOnResponseCommit to set + */ + public void setFlushOnResponseCommit(boolean flushOnResponseCommit) + { + _flushOnResponseCommit = flushOnResponseCommit; + } + + /** + * @return the saveOnCreate + */ + public boolean isSaveOnCreate() + { + return _saveOnCreate; + } + + /** + * @param saveOnCreate the saveOnCreate to set + */ + public void setSaveOnCreate(boolean saveOnCreate) + { + _saveOnCreate = saveOnCreate; + } + + /** + * @return the removeUnloadableSessions + */ + public boolean isRemoveUnloadableSessions() + { + return _removeUnloadableSessions; + } + + /** + * @param removeUnloadableSessions the removeUnloadableSessions to set + */ + public void setRemoveUnloadableSessions(boolean removeUnloadableSessions) + { + _removeUnloadableSessions = removeUnloadableSessions; + } + + /** + * @return the evictionPolicy + */ + public int getEvictionPolicy() + { + return _evictionPolicy; + } + + /** + * @param evictionPolicy the evictionPolicy to set + */ + public void setEvictionPolicy(int evictionPolicy) + { + _evictionPolicy = evictionPolicy; + } + + /** + * @return the saveOnInactiveEvict + */ + public boolean isSaveOnInactiveEvict() + { + return _saveOnInactiveEvict; + } + + /** + * @param saveOnInactiveEvict the saveOnInactiveEvict to set + */ + public void setSaveOnInactiveEvict(boolean saveOnInactiveEvict) + { + _saveOnInactiveEvict = saveOnInactiveEvict; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java index b89ba6c060b..813a661f2d5 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java @@ -80,22 +80,21 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public SessionData load(String id) throws Exception { + if (!isStarted()) + throw new IllegalStateException("Not started"); + final AtomicReference reference = new AtomicReference (); final AtomicReference exception = new AtomicReference (); - Runnable r = new Runnable() + Runnable r = () -> { - @Override - public void run() + try { - try - { - reference.set(doLoad(id)); - } - catch (Exception e) - { - exception.set(e); - } + reference.set(doLoad(id)); + } + catch (Exception e) + { + exception.set(e); } }; @@ -109,6 +108,9 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public void store(String id, SessionData data) throws Exception { + if (!isStarted()) + throw new IllegalStateException("Not started"); + if (data == null) return; @@ -123,10 +125,14 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem long savePeriodMs = (_savePeriodSec <= 0 ? 0 : TimeUnit.SECONDS.toMillis(_savePeriodSec)); if (LOG.isDebugEnabled()) - LOG.debug("Store: id={}, dirty={}, lsave={}, period={}, elapsed={}", id, data.isDirty(), data.getLastSaved(), savePeriodMs, (System.currentTimeMillis() - lastSave)); + { + LOG.debug("Store: id={}, mdirty={}, dirty={}, lsave={}, period={}, elapsed={}", id, data.isMetaDataDirty(), + data.isDirty(), data.getLastSaved(), savePeriodMs, (System.currentTimeMillis() - lastSave)); + } - //save session if attribute changed or never been saved or time between saves exceeds threshold - if (data.isDirty() || (lastSave <= 0) || ((System.currentTimeMillis() - lastSave) > savePeriodMs)) + //save session if attribute changed, never been saved or metadata changed (eg expiry time) and save interval exceeded + if (data.isDirty() || (lastSave <= 0) || + (data.isMetaDataDirty() && ((System.currentTimeMillis() - lastSave) >= savePeriodMs))) { //set the last saved time to now data.setLastSaved(System.currentTimeMillis()); @@ -134,7 +140,7 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem { //call the specific store method, passing in previous save time doStore(id, data, lastSave); - data.setDirty(false); //only undo the dirty setting if we saved it + data.clean(); //unset all dirty flags } catch (Exception e) { @@ -156,6 +162,9 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public Set getExpired(Set candidates) { + if (!isStarted()) + throw new IllegalStateException("Not started"); + try { return doGetExpired(candidates); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCacheFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCacheFactory.java index b1261647414..87b945e5a0b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCacheFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionCacheFactory.java @@ -23,80 +23,8 @@ package org.eclipse.jetty.server.session; * * Factory for creating new DefaultSessionCaches. */ -public class DefaultSessionCacheFactory implements SessionCacheFactory +public class DefaultSessionCacheFactory extends AbstractSessionCacheFactory { - int _evictionPolicy; - boolean _saveOnInactiveEvict; - boolean _saveOnCreate; - boolean _removeUnloadableSessions; - - /** - * @return the saveOnCreate - */ - public boolean isSaveOnCreate() - { - return _saveOnCreate; - } - - /** - * @param saveOnCreate the saveOnCreate to set - */ - public void setSaveOnCreate(boolean saveOnCreate) - { - _saveOnCreate = saveOnCreate; - } - - /** - * @return the removeUnloadableSessions - */ - public boolean isRemoveUnloadableSessions() - { - return _removeUnloadableSessions; - } - - /** - * @param removeUnloadableSessions the removeUnloadableSessions to set - */ - public void setRemoveUnloadableSessions(boolean removeUnloadableSessions) - { - _removeUnloadableSessions = removeUnloadableSessions; - } - - /** - * @return the evictionPolicy - */ - public int getEvictionPolicy() - { - return _evictionPolicy; - } - - /** - * @param evictionPolicy the evictionPolicy to set - */ - public void setEvictionPolicy(int evictionPolicy) - { - _evictionPolicy = evictionPolicy; - } - - /** - * @return the saveOnInactiveEvict - */ - public boolean isSaveOnInactiveEvict() - { - return _saveOnInactiveEvict; - } - - /** - * @param saveOnInactiveEvict the saveOnInactiveEvict to set - */ - public void setSaveOnInactiveEvict(boolean saveOnInactiveEvict) - { - _saveOnInactiveEvict = saveOnInactiveEvict; - } - - /** - * @see org.eclipse.jetty.server.session.SessionCacheFactory#getSessionCache(org.eclipse.jetty.server.session.SessionHandler) - */ @Override public SessionCache getSessionCache(SessionHandler handler) { @@ -105,6 +33,7 @@ public class DefaultSessionCacheFactory implements SessionCacheFactory cache.setSaveOnInactiveEviction(isSaveOnInactiveEvict()); cache.setSaveOnCreate(isSaveOnCreate()); cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions()); + cache.setFlushOnResponseCommit(isFlushOnResponseCommit()); return cache; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java index 688340ddc3b..6809c785ae4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java @@ -467,9 +467,9 @@ public class DefaultSessionIdManager extends ContainerLifeCycle implements Sessi } /** - * Get SessionManager for every context. + * Get SessionHandler for every context. * - * @return all session managers + * @return all SessionHandlers that are running */ @Override public Set getSessionHandlers() @@ -480,7 +480,8 @@ public class DefaultSessionIdManager extends ContainerLifeCycle implements Sessi { for (Handler h : tmp) { - handlers.add((SessionHandler)h); + if (h.isStarted()) + handlers.add((SessionHandler)h); } } return handlers; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java index 5d93456dd24..c589aab4bdb 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java @@ -30,7 +30,6 @@ import javax.servlet.http.HttpServletRequest; */ public class NullSessionCache extends AbstractSessionCache { - /** * @param handler The SessionHandler related to this SessionCache */ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java index 7d0886148d8..40bf5f45f9d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java @@ -18,57 +18,51 @@ package org.eclipse.jetty.server.session; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + /** * NullSessionCacheFactory * * Factory for NullSessionCaches. */ -public class NullSessionCacheFactory implements SessionCacheFactory +public class NullSessionCacheFactory extends AbstractSessionCacheFactory { - boolean _saveOnCreate; - boolean _removeUnloadableSessions; - - /** - * @return the saveOnCreate - */ - public boolean isSaveOnCreate() + private static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session"); + + @Override + public int getEvictionPolicy() { - return _saveOnCreate; + return SessionCache.EVICT_ON_SESSION_EXIT; //never actually stored } - /** - * @param saveOnCreate the saveOnCreate to set - */ - public void setSaveOnCreate(boolean saveOnCreate) + @Override + public void setEvictionPolicy(int evictionPolicy) { - _saveOnCreate = saveOnCreate; + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring eviction policy setting for NullSessionCaches"); } - /** - * @return the removeUnloadableSessions - */ - public boolean isRemoveUnloadableSessions() + @Override + public boolean isSaveOnInactiveEvict() { - return _removeUnloadableSessions; + return false; //never kept in cache } - /** - * @param removeUnloadableSessions the removeUnloadableSessions to set - */ - public void setRemoveUnloadableSessions(boolean removeUnloadableSessions) + @Override + public void setSaveOnInactiveEvict(boolean saveOnInactiveEvict) { - _removeUnloadableSessions = removeUnloadableSessions; + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring eviction policy setting for NullSessionCaches"); } - /** - * @see org.eclipse.jetty.server.session.SessionCacheFactory#getSessionCache(org.eclipse.jetty.server.session.SessionHandler) - */ @Override public SessionCache getSessionCache(SessionHandler handler) { NullSessionCache cache = new NullSessionCache(handler); cache.setSaveOnCreate(isSaveOnCreate()); cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions()); + cache.setFlushOnResponseCommit(isFlushOnResponseCommit()); return cache; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java index 61ccb44ecd7..50656e17f40 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java @@ -36,13 +36,12 @@ import javax.servlet.http.HttpSessionEvent; import org.eclipse.jetty.io.CyclicTimeout; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Locker; -import org.eclipse.jetty.util.thread.Locker.Lock; +import org.eclipse.jetty.util.thread.AutoLock; /** * Session * - * A heavy-weight Session object representing a HttpSession. Session objects + * A heavy-weight Session object representing an HttpSession. Session objects * relating to a context are kept in a {@link SessionCache}. The purpose of the * SessionCache is to keep the working set of Session objects in memory so that * they may be accessed quickly, and facilitate the sharing of a Session object @@ -73,21 +72,18 @@ public class Session implements SessionHandler.SessionIf VALID, INVALID, INVALIDATING, CHANGING } - ; - public enum IdState { SET, CHANGING } - ; - protected final SessionData _sessionData; // the actual data associated with // a session protected final SessionHandler _handler; // the manager of the session protected String _extendedId; // the _id plus the worker name + protected long _requests; protected boolean _idChanged; @@ -97,7 +93,7 @@ public class Session implements SessionHandler.SessionIf protected State _state = State.VALID; // state of the session:valid,invalid // or being invalidated - protected Locker _lock = new Locker(); // sync lock + protected AutoLock _lock = new AutoLock(); protected Condition _stateChangeCompleted = _lock.newCondition(); protected boolean _resident = false; protected final SessionInactivityTimer _sessionInactivityTimer; @@ -127,11 +123,12 @@ public class Session implements SessionHandler.SessionIf long now = System.currentTimeMillis(); //handle what to do with the session after the timer expired getSessionHandler().sessionInactivityTimerExpired(Session.this, now); - try (Lock lock = Session.this.lock()) + try (AutoLock lock = Session.this.lock()) { //grab the lock and check what happened to the session: if it didn't get evicted and //it hasn't expired, we need to reset the timer - if (Session.this.isResident() && Session.this.getRequests() <= 0 && Session.this.isValid() && !Session.this.isExpiredAt(now)) + if (Session.this.isResident() && Session.this.getRequests() <= 0 && Session.this.isValid() && + !Session.this.isExpiredAt(now)) { //session wasn't expired or evicted, we need to reset the timer SessionInactivityTimer.this.schedule(Session.this.calculateInactivityTimeout(now)); @@ -188,8 +185,6 @@ public class Session implements SessionHandler.SessionIf _sessionData = data; _newSession = true; _sessionData.setDirty(true); - _requests = 1; // access will not be called on this new session, but we - // are obviously in a request _sessionInactivityTimer = new SessionInactivityTimer(); } @@ -213,7 +208,7 @@ public class Session implements SessionHandler.SessionIf */ public long getRequests() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { return _requests; } @@ -226,17 +221,30 @@ public class Session implements SessionHandler.SessionIf protected void cookieSet() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { _sessionData.setCookieSet(_sessionData.getAccessed()); } } + protected void use() + { + try (AutoLock lock = _lock.lock()) + { + _requests++; + + // temporarily stop the idle timer + if (LOG.isDebugEnabled()) + LOG.debug("Session {} in use, stopping timer, active requests={}", getId(), _requests); + _sessionInactivityTimer.cancel(); + } + } + protected boolean access(long time) { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { - if (!isValid()) + if (!isValid() || !isResident()) return false; _newSession = false; long lastAccessed = _sessionData.getAccessed(); @@ -248,20 +256,13 @@ public class Session implements SessionHandler.SessionIf invalidate(); return false; } - _requests++; - - // temporarily stop the idle timer - if (LOG.isDebugEnabled()) - LOG.debug("Session {} accessed, stopping timer, active requests={}", getId(), _requests); - _sessionInactivityTimer.cancel(); - return true; } } protected void complete() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { _requests--; @@ -288,7 +289,7 @@ public class Session implements SessionHandler.SessionIf */ protected boolean isExpiredAt(long time) { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { return _sessionData.isExpiredAt(time); } @@ -303,7 +304,7 @@ public class Session implements SessionHandler.SessionIf protected boolean isIdleLongerThan(int sec) { long now = System.currentTimeMillis(); - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { return ((_sessionData.getAccessed() + (sec * 1000)) <= now); } @@ -344,7 +345,7 @@ public class Session implements SessionHandler.SessionIf */ public void unbindValue(java.lang.String name, Object value) { - if (value != null && value instanceof HttpSessionBindingListener) + if (value instanceof HttpSessionBindingListener) ((HttpSessionBindingListener)value).valueUnbound(new HttpSessionBindingEvent(this, name)); } @@ -357,7 +358,7 @@ public class Session implements SessionHandler.SessionIf */ public void bindValue(java.lang.String name, Object value) { - if (value != null && value instanceof HttpSessionBindingListener) + if (value instanceof HttpSessionBindingListener) ((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this, name)); } @@ -366,16 +367,31 @@ public class Session implements SessionHandler.SessionIf */ public void didActivate() { - HttpSessionEvent event = new HttpSessionEvent(this); - for (Iterator iter = _sessionData.getKeys().iterator(); iter.hasNext(); ) + //A passivate listener might remove a non-serializable attribute that + //the activate listener might put back in again, which would spuriously + //set the dirty bit to true, causing another round of passivate/activate + //when the request exits. The store clears the dirty bit if it does a + //save, so ensure dirty flag is set to the value determined by the store, + //not a passivation listener. + boolean dirty = getSessionData().isDirty(); + + try { - Object value = _sessionData.getAttribute(iter.next()); - if (value instanceof HttpSessionActivationListener) + HttpSessionEvent event = new HttpSessionEvent(this); + for (String name : _sessionData.getKeys()) { - HttpSessionActivationListener listener = (HttpSessionActivationListener)value; - listener.sessionDidActivate(event); + Object value = _sessionData.getAttribute(name); + if (value instanceof HttpSessionActivationListener) + { + HttpSessionActivationListener listener = (HttpSessionActivationListener)value; + listener.sessionDidActivate(event); + } } } + finally + { + getSessionData().setDirty(dirty); + } } /** @@ -384,9 +400,9 @@ public class Session implements SessionHandler.SessionIf public void willPassivate() { HttpSessionEvent event = new HttpSessionEvent(this); - for (Iterator iter = _sessionData.getKeys().iterator(); iter.hasNext(); ) + for (String name : _sessionData.getKeys()) { - Object value = _sessionData.getAttribute(iter.next()); + Object value = _sessionData.getAttribute(name); if (value instanceof HttpSessionActivationListener) { HttpSessionActivationListener listener = (HttpSessionActivationListener)value; @@ -397,21 +413,23 @@ public class Session implements SessionHandler.SessionIf public boolean isValid() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { return _state == State.VALID; } } - public boolean isChanging() + public boolean isInvalid() { - checkLocked(); - return _state == State.CHANGING; + try (AutoLock lock = _lock.lock()) + { + return _state == State.INVALID || _state == State.INVALIDATING; + } } public long getCookieSetTime() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { return _sessionData.getCookieSet(); } @@ -420,7 +438,7 @@ public class Session implements SessionHandler.SessionIf @Override public long getCreationTime() throws IllegalStateException { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { checkValidForRead(); return _sessionData.getCreated(); @@ -433,7 +451,7 @@ public class Session implements SessionHandler.SessionIf @Override public String getId() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { return _sessionData.getId(); } @@ -460,8 +478,12 @@ public class Session implements SessionHandler.SessionIf @Override public long getLastAccessedTime() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { + if (isInvalid()) + { + throw new IllegalStateException("Session not valid"); + } return _sessionData.getLastAccessed(); } } @@ -483,10 +505,14 @@ public class Session implements SessionHandler.SessionIf @Override public void setMaxInactiveInterval(int secs) { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { _sessionData.setMaxInactiveMs((long)secs * 1000L); _sessionData.calcAndSetExpiry(); + //dirty metadata writes can be skipped, but changing the + //maxinactiveinterval should write the session out because + //it may affect the session on other nodes, or on the same + //node in the case of the nullsessioncache _sessionData.setDirty(true); if (LOG.isDebugEnabled()) @@ -512,7 +538,7 @@ public class Session implements SessionHandler.SessionIf { long time = 0; - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { long remaining = _sessionData.getExpiry() - now; long maxInactive = _sessionData.getMaxInactiveMs(); @@ -561,7 +587,8 @@ public class Session implements SessionHandler.SessionIf time = (remaining > 0 ? (Math.min(maxInactive, TimeUnit.SECONDS.toMillis(evictionPolicy))) : 0); if (LOG.isDebugEnabled()) - LOG.debug("Session {} timer set to lesser of maxInactive={} and inactivityEvict={}", getId(), maxInactive, evictionPolicy); + LOG.debug("Session {} timer set to lesser of maxInactive={} and inactivityEvict={}", getId(), + maxInactive, evictionPolicy); } } } @@ -575,7 +602,7 @@ public class Session implements SessionHandler.SessionIf @Override public int getMaxInactiveInterval() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { long maxInactiveMs = _sessionData.getMaxInactiveMs(); return (int)(maxInactiveMs < 0 ? -1 : maxInactiveMs / 1000); @@ -605,8 +632,6 @@ public class Session implements SessionHandler.SessionIf */ protected void checkValidForWrite() throws IllegalStateException { - checkLocked(); - if (_state == State.INVALID) throw new IllegalStateException("Not valid for write: id=" + _sessionData.getId() + " created=" + _sessionData.getCreated() + @@ -630,8 +655,6 @@ public class Session implements SessionHandler.SessionIf */ protected void checkValidForRead() throws IllegalStateException { - checkLocked(); - if (_state == State.INVALID) throw new IllegalStateException("Invalid for read: id=" + _sessionData.getId() + " created=" + _sessionData.getCreated() + @@ -647,19 +670,13 @@ public class Session implements SessionHandler.SessionIf throw new IllegalStateException("Invalid for read: id=" + _sessionData.getId() + " not resident"); } - protected void checkLocked() throws IllegalStateException - { - if (!_lock.isLocked()) - throw new IllegalStateException("Session not locked"); - } - /** * @see javax.servlet.http.HttpSession#getAttribute(java.lang.String) */ @Override public Object getAttribute(String name) { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { checkValidForRead(); return _sessionData.getAttribute(name); @@ -673,8 +690,9 @@ public class Session implements SessionHandler.SessionIf @Deprecated(since = "Servlet API 2.2") public Object getValue(String name) { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { + checkValidForRead(); return _sessionData.getAttribute(name); } } @@ -685,11 +703,11 @@ public class Session implements SessionHandler.SessionIf @Override public Enumeration getAttributeNames() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { checkValidForRead(); final Iterator itor = _sessionData.getKeys().iterator(); - return new Enumeration () + return new Enumeration<>() { @Override @@ -725,7 +743,7 @@ public class Session implements SessionHandler.SessionIf @Deprecated(since = "Servlet API 2.2") public String[] getValueNames() throws IllegalStateException { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { checkValidForRead(); Iterator itor = _sessionData.getKeys().iterator(); @@ -748,7 +766,7 @@ public class Session implements SessionHandler.SessionIf public void setAttribute(String name, Object value) { Object old = null; - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { // if session is not valid, don't accept the set checkValidForWrite(); @@ -802,7 +820,7 @@ public class Session implements SessionHandler.SessionIf String id = null; String extendedId = null; - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { while (true) { @@ -838,7 +856,7 @@ public class Session implements SessionHandler.SessionIf String newId = _handler._sessionIdManager.renewSessionId(id, extendedId, request); - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { switch (_state) { @@ -916,7 +934,7 @@ public class Session implements SessionHandler.SessionIf * * @return the lock */ - public Lock lock() + public AutoLock lock() { return _lock.lock(); } @@ -928,7 +946,7 @@ public class Session implements SessionHandler.SessionIf { boolean result = false; - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { while (true) @@ -952,6 +970,8 @@ public class Session implements SessionHandler.SessionIf { try { + if (LOG.isDebugEnabled()) + LOG.debug("Session {} waiting for id change to complete", _sessionData.getId()); _stateChangeCompleted.await(); } catch (InterruptedException e) @@ -985,7 +1005,7 @@ public class Session implements SessionHandler.SessionIf */ protected void finishInvalidate() throws IllegalStateException { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { try { @@ -1023,7 +1043,7 @@ public class Session implements SessionHandler.SessionIf @Override public boolean isNew() throws IllegalStateException { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { checkValidForRead(); return _newSession; @@ -1032,7 +1052,7 @@ public class Session implements SessionHandler.SessionIf public void setIdChanged(boolean changed) { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { _idChanged = changed; } @@ -1040,7 +1060,7 @@ public class Session implements SessionHandler.SessionIf public boolean isIdChanged() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { return _idChanged; } @@ -1058,9 +1078,6 @@ public class Session implements SessionHandler.SessionIf return _sessionData; } - /** - * - */ public void setResident(boolean resident) { _resident = resident; @@ -1077,7 +1094,7 @@ public class Session implements SessionHandler.SessionIf @Override public String toString() { - try (Lock lock = _lock.lock()) + try (AutoLock lock = _lock.lock()) { return String.format("%s@%x{id=%s,x=%s,req=%d,res=%b}", getClass().getSimpleName(), diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCache.java index b71a55f25b1..b47810a9225 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCache.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionCache.java @@ -96,6 +96,16 @@ public interface SessionCache extends LifeCycle * @throws Exception if any error occurred */ Session renewSessionId(String oldId, String newId, String oldExtendedId, String newExtendedId) throws Exception; + + /** + * Adds a new Session, with a never-before-used id, + * to the cache. + * + * @param id + * @param session + * @throws Exception + */ + void add(String id, Session session) throws Exception; /** * Get an existing Session. If necessary, the cache will load the data for @@ -116,9 +126,34 @@ public interface SessionCache extends LifeCycle * @param id the session id * @param session the current session object * @throws Exception if any error occurred + * @deprecated use {@link #release(String, Session)} instead */ + @Deprecated void put(String id, Session session) throws Exception; + /** + * Finish using a Session. This is called by the SessionHandler + * once a request is finished with a Session. SessionCache + * implementations may want to delay writing out Session contents + * until the last request exits a Session. + * + * @param id the session id + * @param session the current session object + * @throws Exception if any error occurred + */ + void release(String id, Session session) throws Exception; + + /** + * Called when a response is about to be committed. The + * cache can write the session to ensure that the + * SessionDataStore contains changes to the session + * that occurred during the lifetime of the request. This + * can help ensure that if a subsequent request goes to a + * different server, it will be able to see the session + * changes via the shared store. + */ + void commit(Session session) throws Exception; + /** * Check to see if a Session is in the cache. Does NOT consult * the SessionDataStore. @@ -241,4 +276,18 @@ public interface SessionCache extends LifeCycle * @return if true
unloadable session will be deleted */ boolean isRemoveUnloadableSessions(); + + /** + * If true, a dirty session will be written to the SessionDataStore + * just before a response is returned to the client. This ensures + * that subsequent requests to either the same node or a different + * node see the changed session data. + */ + void setFlushOnResponseCommit(boolean flushOnResponse); + + /** + * @returntrue
if dirty sessions should be written + * before the response is committed. + */ + boolean isFlushOnResponseCommit(); } 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 57a041c9ccc..3b3a9fd0493 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 @@ -58,6 +58,7 @@ public class SessionData implements Serializable protected Map_attributes; protected boolean _dirty; protected long _lastSaved; //time in msec since last save + protected boolean _metaDataDirty; //non-attribute data has changed /** * Serialize the attribute map of the session. @@ -161,11 +162,6 @@ public class SessionData implements Serializable } public SessionData(String id, String cpath, String vhost, long created, long accessed, long lastAccessed, long maxInactiveMs) - { - this(id, cpath, vhost, created, accessed, lastAccessed, maxInactiveMs, new ConcurrentHashMap ()); - } - - public SessionData(String id, String cpath, String vhost, long created, long accessed, long lastAccessed, long maxInactiveMs, Map attributes) { _id = id; setContextPath(cpath); @@ -175,7 +171,13 @@ public class SessionData implements Serializable _lastAccessed = lastAccessed; _maxInactiveMs = maxInactiveMs; calcAndSetExpiry(); - _attributes = attributes; + _attributes = new ConcurrentHashMap<>(); + } + + public SessionData(String id, String cpath, String vhost, long created, long accessed, long lastAccessed, long maxInactiveMs, Map attributes) + { + this(id, cpath, vhost, created, accessed, lastAccessed, maxInactiveMs); + putAllAttributes(attributes); } /** @@ -239,6 +241,22 @@ public class SessionData implements Serializable setDirty(true); } + /** + * @return the metaDataDirty + */ + public boolean isMetaDataDirty() + { + return _metaDataDirty; + } + + /** + * @param metaDataDirty true means non-attribute data has changed + */ + public void setMetaDataDirty(boolean metaDataDirty) + { + _metaDataDirty = metaDataDirty; + } + /** * @param name the name of the attribute * @return the value of the attribute named @@ -266,6 +284,15 @@ public class SessionData implements Serializable return old; } + /** + * Clear all dirty flags. + */ + public void clean() + { + setDirty(false); + setMetaDataDirty(false); + } + public void putAllAttributes(Map attributes) { _attributes.putAll(attributes); @@ -365,11 +392,13 @@ public class SessionData implements Serializable public void calcAndSetExpiry(long time) { setExpiry(calcExpiry(time)); + setMetaDataDirty(true); } public void calcAndSetExpiry() { setExpiry(calcExpiry()); + setMetaDataDirty(true); } public long getCreated() 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 4357403c527..0505da7722c 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 @@ -29,8 +29,6 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import javax.servlet.AsyncEvent; -import javax.servlet.AsyncListener; import javax.servlet.DispatcherType; import javax.servlet.ServletException; import javax.servlet.SessionCookieConfig; @@ -60,7 +58,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.statistic.CounterStatistic; import org.eclipse.jetty.util.statistic.SampleStatistic; -import org.eclipse.jetty.util.thread.Locker.Lock; +import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; import org.eclipse.jetty.util.thread.Scheduler; @@ -74,7 +72,8 @@ public class SessionHandler extends ScopedHandler { static final Logger LOG = Log.getLogger("org.eclipse.jetty.server.session"); - public static final EnumSet DEFAULT_TRACKING = EnumSet.of(SessionTrackingMode.COOKIE, SessionTrackingMode.URL); + public static final EnumSet DEFAULT_TRACKING = EnumSet.of(SessionTrackingMode.COOKIE, + SessionTrackingMode.URL); /** * Session cookie name. @@ -123,11 +122,12 @@ public class SessionHandler extends ScopedHandler public static final Set DEFAULT_SESSION_TRACKING_MODES = Collections.unmodifiableSet( new HashSet<>( - Arrays.asList(new SessionTrackingMode[]{SessionTrackingMode.COOKIE, SessionTrackingMode.URL}))); + Arrays.asList(SessionTrackingMode.COOKIE, SessionTrackingMode.URL))); @SuppressWarnings("unchecked") public static final Class extends EventListener>[] SESSION_LISTENER_TYPES = - new Class[]{ + new Class[] + { HttpSessionAttributeListener.class, HttpSessionIdListener.class, HttpSessionListener.class @@ -140,54 +140,6 @@ public class SessionHandler extends ScopedHandler */ public static final java.math.BigDecimal MAX_INACTIVE_MINUTES = new java.math.BigDecimal(Integer.MAX_VALUE / 60); - /** - * SessionAsyncListener - * - * Used to ensure that a request for which async has been started - * has its session completed as the request exits the context. - */ - public class SessionAsyncListener implements AsyncListener - { - @Override - public void onComplete(AsyncEvent event) throws IOException - { - // An async request has completed, so we can complete the session, - // but we must locate the session instance for this context - Request request = Request.getBaseRequest(event.getAsyncContext().getRequest()); - HttpSession session = request.getSession(false); - String id; - if (session != null) - id = session.getId(); - else - { - id = (String)request.getAttribute(DefaultSessionIdManager.__NEW_SESSION_ID); - if (id == null) - id = request.getRequestedSessionId(); - } - - if (id != null) - complete(getSession(id)); - } - - @Override - public void onTimeout(AsyncEvent event) throws IOException - { - - } - - @Override - public void onError(AsyncEvent event) throws IOException - { - complete(Request.getBaseRequest(event.getAsyncContext().getRequest()).getSession(false)); - } - - @Override - public void onStartAsync(AsyncEvent event) throws IOException - { - event.getAsyncContext().addListener(this); - } - } - @Deprecated(since = "Servlet API 2.1") static final HttpSessionContext __nullSessionContext = new HttpSessionContext() { @@ -246,7 +198,6 @@ public class SessionHandler extends ScopedHandler protected Scheduler _scheduler; protected boolean _ownScheduler = false; - protected final SessionAsyncListener _sessionAsyncListener = new SessionAsyncListener(); /** * Constructor. @@ -270,6 +221,8 @@ public class SessionHandler extends ScopedHandler /** * Called by the {@link SessionHandler} when a session is first accessed by a request. + * + * Updates the last access time for the session and generates a fresh cookie if necessary. * * @param session the session object * @param secure whether the request is secure or not @@ -288,9 +241,8 @@ public class SessionHandler extends ScopedHandler // Do we need to refresh the cookie? if (isUsingCookies() && (s.isIdChanged() || - (getSessionCookieConfig().getMaxAge() > 0 && getRefreshCookieAge() > 0 && ((now - s.getCookieSetTime()) / 1000 > getRefreshCookieAge())) - ) - ) + (getSessionCookieConfig().getMaxAge() > 0 && getRefreshCookieAge() > 0 && + ((now - s.getCookieSetTime()) / 1000 > getRefreshCookieAge())))) { HttpCookie cookie = getSessionCookie(session, _context == null ? "/" : (_context.getContextPath()), secure); s.cookieSet(); @@ -402,10 +354,9 @@ public class SessionHandler extends ScopedHandler } /** - * Called by the {@link SessionHandler} when a session is last accessed by a request. + * Called when a request is finally leaving a session. * * @param session the session object - * @see #access(HttpSession, boolean) */ public void complete(HttpSession session) { @@ -416,30 +367,35 @@ public class SessionHandler extends ScopedHandler return; Session s = ((SessionIf)session).getSession(); - try { - s.complete(); - _sessionCache.put(s.getId(), s); + _sessionCache.release(s.getId(), s); } catch (Exception e) { LOG.warn(e); } } - - private void ensureCompletion(Request baseRequest) + + /** + * Called when a response is about to be committed. + * We might take this opportunity to persist the session + * so that any subsequent requests to other servers + * will see the modifications. + */ + public void commit(HttpSession session) { - if (baseRequest.isAsyncStarted()) + if (session == null) + return; + + Session s = ((SessionIf)session).getSession(); + try { - if (LOG.isDebugEnabled()) - LOG.debug("Adding AsyncListener for {}", baseRequest); - if (!baseRequest.getHttpChannelState().hasListener(_sessionAsyncListener)) - baseRequest.getAsyncContext().addListener(_sessionAsyncListener); + _sessionCache.commit(s); } - else + catch (Exception e) { - complete(baseRequest.getSession(false)); + LOG.warn(e); } } @@ -578,11 +534,11 @@ public class SessionHandler extends ScopedHandler * @param extendedId the session id * @return the HttpSession
with the corresponding id or null if no session with the given id exists */ - public HttpSession getHttpSession(String extendedId) + protected HttpSession getHttpSession(String extendedId) { String id = getSessionIdManager().getId(extendedId); - Session session = getSession(id); + if (session != null && !session.getExtendedId().equals(extendedId)) session.setIdChanged(true); return session; @@ -618,7 +574,7 @@ public class SessionHandler extends ScopedHandler /** * @return same as SessionCookieConfig.getSecure(). If true, session * cookies are ALWAYS marked as secure. If false, a session cookie is - * ONLY marked as secure if _secureRequestOnly == true and it is a HTTPS request. + * ONLY marked as secure if _secureRequestOnly == true and it is an HTTPS request. */ @ManagedAttribute("if true, secure cookie flag is set on session cookies") public boolean getSecureCookies() @@ -810,7 +766,8 @@ public class SessionHandler extends ScopedHandler try { - _sessionCache.put(id, session); + _sessionCache.add(id, session); + Request.getBaseRequest(request).enterSession(session); _sessionsCreatedStats.increment(); if (request != null && request.isSecure()) @@ -909,7 +866,8 @@ public class SessionHandler extends ScopedHandler public void setSessionIdPathParameterName(String param) { _sessionIdPathParameterName = (param == null || "none".equals(param)) ? null : param; - _sessionIdPathParameterNamePrefix = (param == null || "none".equals(param)) ? null : (";" + _sessionIdPathParameterName + "="); + _sessionIdPathParameterNamePrefix = (param == null || "none".equals(param)) + ? null : (";" + _sessionIdPathParameterName + "="); } /** @@ -926,7 +884,7 @@ public class SessionHandler extends ScopedHandler * @param id The session ID stripped of any worker name. * @return A Session or null if none exists. */ - public Session getSession(String id) + protected Session getSession(String id) { try { @@ -950,7 +908,6 @@ public class SessionHandler extends ScopedHandler } session.setExtendedId(_sessionIdManager.getExtendedId(id, null)); - //session.getSessionData().setLastNode(_sessionIdManager.getWorkerName()); //TODO write through the change of node? } return session; } @@ -1081,6 +1038,12 @@ public class SessionHandler extends ScopedHandler public void setSessionTrackingModes(SetsessionTrackingModes) { + if (sessionTrackingModes != null && + sessionTrackingModes.size() > 1 && + sessionTrackingModes.contains(SessionTrackingMode.SSL)) + { + throw new IllegalArgumentException("sessionTrackingModes specifies a combination of SessionTrackingMode.SSL with a session tracking mode other than SessionTrackingMode.SSL"); + } _sessionTrackingModes = new HashSet<>(sessionTrackingModes); _usingCookies = _sessionTrackingModes.contains(SessionTrackingMode.COOKIE); _usingURLs = _sessionTrackingModes.contains(SessionTrackingMode.URL); @@ -1156,9 +1119,11 @@ public class SessionHandler extends ScopedHandler */ public void renewSessionId(String oldId, String oldExtendedId, String newId, String newExtendedId) { + Session session = null; try { - Session session = _sessionCache.renewSessionId(oldId, newId, oldExtendedId, newExtendedId); //swap the id over + //the use count for the session will be incremented in renewSessionId + session = _sessionCache.renewSessionId(oldId, newId, oldExtendedId, newExtendedId); //swap the id over if (session == null) { //session doesn't exist on this context @@ -1172,6 +1137,20 @@ public class SessionHandler extends ScopedHandler { LOG.warn(e); } + finally + { + if (session != null) + { + try + { + _sessionCache.release(newId, session); + } + catch (Exception e) + { + LOG.warn(e); + } + } + } } /** @@ -1305,7 +1284,7 @@ public class SessionHandler extends ScopedHandler //1. valid //2. expired //3. idle - try (Lock lock = session.lock()) + try (AutoLock lock = session.lock()) { if (session.getRequests() > 0) return; //session can't expire or be idle if there is a request in it @@ -1324,7 +1303,8 @@ public class SessionHandler extends ScopedHandler //most efficient if it can be done as a bulk operation to eg reduce //roundtrips to the persistent store. Only do this if the HouseKeeper that //does the scavenging is configured to actually scavenge - if (_sessionIdManager.getSessionHouseKeeper() != null && _sessionIdManager.getSessionHouseKeeper().getIntervalSec() > 0) + if (_sessionIdManager.getSessionHouseKeeper() != null && + _sessionIdManager.getSessionHouseKeeper().getIntervalSec() > 0) { _candidateSessionIdsForExpiry.add(session.getId()); if (LOG.isDebugEnabled()) @@ -1496,7 +1476,8 @@ public class SessionHandler extends ScopedHandler * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) */ @Override - public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { SessionHandler oldSessionHandler = null; HttpSession oldSession = null; @@ -1505,27 +1486,57 @@ public class SessionHandler extends ScopedHandler try { if (LOG.isDebugEnabled()) - LOG.debug("SessionHandler.doScope"); + LOG.debug("Entering scope {}, dispatch={} asyncstarted={}", this, baseRequest.getDispatcherType(), baseRequest + .isAsyncStarted()); - oldSessionHandler = baseRequest.getSessionHandler(); - oldSession = baseRequest.getSession(false); - - if (oldSessionHandler != this) + switch (baseRequest.getDispatcherType()) { - // new session context - baseRequest.setSessionHandler(this); - baseRequest.setSession(null); - checkRequestedSessionId(baseRequest, request); - } + case REQUEST: + { + //there are no previous sessionhandlers or sessions for dispatch=REQUEST + //look for a session for this context + baseRequest.setSession(null); + checkRequestedSessionId(baseRequest, request); + existingSession = baseRequest.getSession(false); + baseRequest.setSessionHandler(this); + baseRequest.setSession(existingSession); //can be null + break; + } + case ASYNC: + case ERROR: + case FORWARD: + case INCLUDE: + { + //remember previous sessionhandler and session + oldSessionHandler = baseRequest.getSessionHandler(); + oldSession = baseRequest.getSession(false); - // access any existing session for this context - existingSession = baseRequest.getSession(false); + //find any existing session for this request that has already been accessed + existingSession = baseRequest.getSession(this); + if (existingSession == null) + { + //session for this context has not been visited previously, + //try getting it + baseRequest.setSession(null); + checkRequestedSessionId(baseRequest, request); + existingSession = baseRequest.getSession(false); + } + + baseRequest.setSession(existingSession); + baseRequest.setSessionHandler(this); + break; + } + default: + break; + } if ((existingSession != null) && (oldSessionHandler != this)) { HttpCookie cookie = access(existingSession, request.isSecure()); // Handle changed ID or max-age refresh, but only if this is not a redispatched request - if ((cookie != null) && (request.getDispatcherType() == DispatcherType.ASYNC || request.getDispatcherType() == DispatcherType.REQUEST)) + if ((cookie != null) && + (request.getDispatcherType() == DispatcherType.ASYNC || + request.getDispatcherType() == DispatcherType.REQUEST)) baseRequest.getResponse().replaceCookie(cookie); } @@ -1541,13 +1552,10 @@ public class SessionHandler extends ScopedHandler } finally { - //if there is a session that was created during handling this context, then complete it if (LOG.isDebugEnabled()) - LOG.debug("FinalSession={}, old_session_handler={}, this={}, calling complete={}", baseRequest.getSession(false), oldSessionHandler, this, (oldSessionHandler != this)); - - // If we are leaving the scope of this session handler, ensure the session is completed - if (oldSessionHandler != this) - ensureCompletion(baseRequest); + LOG.debug("Leaving scope {} dispatch={}, async={}, session={}, oldsession={}, oldsessionhandler={}", + this, baseRequest.getDispatcherType(), baseRequest.isAsyncStarted(), baseRequest.getSession(false), + oldSession, oldSessionHandler); // revert the session handler to the previous, unless it was null, in which case remember it as // the first session handler encountered. @@ -1563,7 +1571,8 @@ public class SessionHandler extends ScopedHandler * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) */ @Override - public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { nextHandle(target, baseRequest, request, response); } @@ -1583,7 +1592,10 @@ public class SessionHandler extends ScopedHandler HttpSession session = getHttpSession(requestedSessionId); if (session != null && isValid(session)) + { + baseRequest.enterSession(session); //enter session for first time baseRequest.setSession(session); + } return; } else if (!DispatcherType.REQUEST.equals(baseRequest.getDispatcherType())) @@ -1592,7 +1604,7 @@ public class SessionHandler extends ScopedHandler boolean requestedSessionIdFromCookie = false; HttpSession session = null; - // Look for session id cookie + //first try getting id from a cookie if (isUsingCookies()) { Cookie[] cookies = request.getCookies(); @@ -1605,31 +1617,21 @@ public class SessionHandler extends ScopedHandler { requestedSessionId = cookies[i].getValue(); requestedSessionIdFromCookie = true; - if (LOG.isDebugEnabled()) - LOG.debug("Got Session ID {} from cookie", requestedSessionId); - + LOG.debug("Got Session ID {} from cookie {}", requestedSessionId, sessionCookie); if (requestedSessionId != null) { - session = getHttpSession(requestedSessionId); - if (session != null && isValid(session)) - { - break; - } - } - else - { - LOG.warn("null session id from cookie"); + break; } } } } } - if (isUsingURLs() && (requestedSessionId == null || session == null)) + //try getting id from a url + if (isUsingURLs() && (requestedSessionId == null)) { String uri = request.getRequestURI(); - String prefix = getSessionIdPathParameterNamePrefix(); if (prefix != null) { @@ -1648,7 +1650,6 @@ public class SessionHandler extends ScopedHandler requestedSessionId = uri.substring(s, i); requestedSessionIdFromCookie = false; - session = getHttpSession(requestedSessionId); if (LOG.isDebugEnabled()) LOG.debug("Got Session ID {} from URL", requestedSessionId); } @@ -1657,8 +1658,16 @@ public class SessionHandler extends ScopedHandler baseRequest.setRequestedSessionId(requestedSessionId); baseRequest.setRequestedSessionIdFromCookie(requestedSessionId != null && requestedSessionIdFromCookie); - if (session != null && isValid(session)) - baseRequest.setSession(session); + + if (requestedSessionId != null) + { + session = getHttpSession(requestedSessionId); + if (session != null && isValid(session)) + { + baseRequest.enterSession(session); //request enters this session for first time + baseRequest.setSession(session); + } + } } /** diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java index 120cf057662..e6a2b830e88 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java @@ -84,11 +84,11 @@ public abstract class AbstractHttpTest HttpTester.Input input = HttpTester.from(socket.getInputStream()); HttpTester.parseResponse(input, response); - if (httpVersion.is("HTTP/1.1") - && response.isComplete() - && response.get("content-length") == null - && response.get("transfer-encoding") == null - && !__noBodyCodes.contains(response.getStatus())) + if (httpVersion.is("HTTP/1.1") && + response.isComplete() && + response.get("content-length") == null && + response.get("transfer-encoding") == null && + !__noBodyCodes.contains(response.getStatus())) assertThat("If HTTP/1.1 response doesn't contain transfer-encoding or content-length headers, " + "it should contain connection:close", response.get("connection"), is("close")); return response; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java new file mode 100644 index 00000000000..d97d8fb5efe --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java @@ -0,0 +1,221 @@ +// +// ======================================================================== +// 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; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Exchanger; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.io.ChannelEndPoint; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.thread.Scheduler; +import org.hamcrest.Matchers; +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.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Extended Server Tester. + */ +public class AsyncCompletionTest extends HttpServerTestFixture +{ + private static final Exchanger X = new Exchanger<>(); + private static final AtomicBoolean COMPLETE = new AtomicBoolean(); + + private static class DelayedCallback extends Callback.Nested + { + private CompletableFuture _delay = new CompletableFuture<>(); + + public DelayedCallback(Callback callback) + { + super(callback); + } + + @Override + public void succeeded() + { + _delay.complete(null); + } + + @Override + public void failed(Throwable x) + { + _delay.completeExceptionally(x); + } + + public void proceed() + { + try + { + _delay.get(10, TimeUnit.SECONDS); + getCallback().succeeded(); + } + catch(Throwable th) + { + th.printStackTrace(); + getCallback().failed(th); + } + } + } + + + @BeforeEach + public void init() throws Exception + { + COMPLETE.set(false); + + startServer(new ServerConnector(_server, new HttpConnectionFactory() + { + @Override + public Connection newConnection(Connector connector, EndPoint endPoint) + { + return configure(new ExtendedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint); + } + }) + { + @Override + protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + { + return new ExtendedEndPoint(channel, selectSet, key, getScheduler()); + } + }); + } + + private static class ExtendedEndPoint extends SocketChannelEndPoint + { + public ExtendedEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) + { + super(channel, selector, key, scheduler); + } + + @Override + public void write(Callback callback, ByteBuffer... buffers) throws IllegalStateException + { + DelayedCallback delay = new DelayedCallback(callback); + super.write(delay, buffers); + try + { + X.exchange(delay); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + } + + private static class ExtendedHttpConnection extends HttpConnection + { + public ExtendedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint) + { + super(config, connector, endPoint, false); + } + + @Override + public void onCompleted() + { + COMPLETE.compareAndSet(false,true); + super.onCompleted(); + } + } + + // Tests from here use these parameters + public static Stream tests() + { + List