From 00f322f0921c4b79e7e3642986e75b9b8560e33a Mon Sep 17 00:00:00 2001 From: Dan Allen Date: Mon, 20 May 2024 14:01:08 -0600 Subject: [PATCH] migrate documentation to Antora --- documentation/jetty/.gitignore | 2 + documentation/jetty/README.adoc | 32 + documentation/jetty/antora.yml | 28 + .../jetty/modules/ROOT/pages/index.adoc | 24 + .../code/examples/jetty-modules/jpms.mod | 8 + .../code/examples/jetty-modules/jvm.mod | 6 + .../examples/jetty-modules/postgresql.mod | 15 + .../examples/jetty-modules/remote-debug.mod | 5 + .../jetty/modules/code/examples/pom.xml | 206 ++ .../docs/programming/ArchitectureDocs.java | 33 + .../jetty/docs/programming/ComponentDocs.java | 284 +++ .../jetty/docs/programming/ContentDocs.java | 403 ++++ .../jetty/docs/programming/HTTP2Docs.java | 94 + .../jetty/docs/programming/JMXDocs.java | 271 +++ .../docs/programming/SelectorManagerDocs.java | 316 +++ .../jetty/docs/programming/WebSocketDocs.java | 632 ++++++ .../client/ClientConnectorDocs.java | 429 +++++ .../client/http/HTTPClientDocs.java | 1158 +++++++++++ .../client/http2/HTTP2ClientDocs.java | 416 ++++ .../client/http3/HTTP3ClientDocs.java | 271 +++ .../client/websocket/WebSocketClientDocs.java | 195 ++ .../migration/ServletToHandlerDocs.java | 647 +++++++ .../docs/programming/server/ServerDocs.java | 335 ++++ .../server/http/HTTPServerDocs.java | 1652 ++++++++++++++++ .../server/http/SessionHandlerDocs.java | 65 + .../server/http2/HTTP2ServerDocs.java | 349 ++++ .../server/http3/HTTP3ServerDocs.java | 341 ++++ .../server/session/SessionDocs.java | 465 +++++ .../server/websocket/WebSocketServerDocs.java | 312 +++ .../images/jmc-server-dump.png | Bin 0 -> 268442 bytes .../jetty/modules/operations-guide/nav.adoc | 40 + .../pages/annotations/index.adoc | 219 +++ .../operations-guide/pages/arch/index.adoc | 160 ++ .../operations-guide/pages/begin/index.adoc | 337 ++++ .../operations-guide/pages/deploy/index.adoc | 485 +++++ .../pages/features/index.adoc | 36 + .../operations-guide/pages/howtos/index.adoc | 24 + .../modules/operations-guide/pages/index.adoc | 17 + .../operations-guide/pages/jaas/index.adoc | 333 ++++ .../operations-guide/pages/jaspi/index.adoc | 70 + .../operations-guide/pages/jmx/index.adoc | 281 +++ .../operations-guide/pages/jndi/index.adoc | 420 ++++ .../pages/jsf-taglibs/index.adoc | 41 + .../operations-guide/pages/jsp/index.adoc | 171 ++ .../operations-guide/pages/jstl/index.adoc | 23 + .../pages/keystore/index.adoc | 274 +++ .../pages/modules/custom.adoc | 235 +++ .../operations-guide/pages/modules/index.adoc | 493 +++++ .../pages/modules/standard.adoc | 721 +++++++ .../pages/protocols/index.adoc | 1230 ++++++++++++ .../pages/quickstart/index.adoc | 67 + .../operations-guide/pages/server/index.adoc | 359 ++++ .../operations-guide/pages/session/index.adoc | 974 ++++++++++ .../operations-guide/pages/start/index.adoc | 622 ++++++ .../pages/start/start-jpms.adoc | 96 + .../operations-guide/pages/tools/index.adoc | 80 + .../pages/troubleshooting/index.adoc | 246 +++ .../operations-guide/pages/xml/index.adoc | 518 +++++ .../images/jmc-server-dump.png | Bin 0 -> 268442 bytes .../jetty/modules/programming-guide/nav.adoc | 48 + .../programming-guide/pages/arch/bean.adoc | 160 ++ .../programming-guide/pages/arch/io.adoc | 375 ++++ .../programming-guide/pages/arch/jmx.adoc | 300 +++ .../pages/arch/listener.adoc | 34 + .../programming-guide/pages/arch/threads.adoc | 257 +++ .../programming-guide/pages/client/http.adoc | 1008 ++++++++++ .../programming-guide/pages/client/http2.adoc | 238 +++ .../programming-guide/pages/client/http3.adoc | 155 ++ .../programming-guide/pages/client/index.adoc | 30 + .../pages/client/io-arch.adoc | 178 ++ .../pages/client/websocket.adoc | 530 ++++++ .../programming-guide/pages/index.adoc | 25 + .../maven-jetty/jetty-jspc-maven-plugin.adoc | 242 +++ .../maven-jetty/jetty-maven-helloworld.adoc | 276 +++ .../pages/maven-jetty/jetty-maven-plugin.adoc | 1162 +++++++++++ .../pages/migration/11-to-12.adoc | 185 ++ .../pages/migration/94-to-10.adoc | 130 ++ .../pages/server/compliance.adoc | 121 ++ .../pages/server/fastcgi.adoc | 16 + .../programming-guide/pages/server/http.adoc | 1694 +++++++++++++++++ .../programming-guide/pages/server/http2.adoc | 200 ++ .../programming-guide/pages/server/http3.adoc | 135 ++ .../programming-guide/pages/server/index.adoc | 29 + .../pages/server/io-arch.adoc | 223 +++ .../pages/server/session.adoc | 1107 +++++++++++ .../pages/server/websocket.adoc | 343 ++++ .../pages/troubleshooting/component-dump.adoc | 26 + .../pages/troubleshooting/debugging.adoc | 29 + .../pages/troubleshooting/index.adoc | 21 + .../pages/troubleshooting/logging.adoc | 57 + .../pages/troubleshooting/state-tracking.adoc | 22 + .../pages/troubleshooting/thread-dump.adoc | 16 + documentation/jetty/pom.xml | 163 ++ documentation/pom.xml | 3 +- .../eclipse/jetty/tests/testers/RunJetty.java | 262 +++ 95 files changed, 27364 insertions(+), 2 deletions(-) create mode 100644 documentation/jetty/.gitignore create mode 100644 documentation/jetty/README.adoc create mode 100644 documentation/jetty/antora.yml create mode 100644 documentation/jetty/modules/ROOT/pages/index.adoc create mode 100644 documentation/jetty/modules/code/examples/jetty-modules/jpms.mod create mode 100644 documentation/jetty/modules/code/examples/jetty-modules/jvm.mod create mode 100644 documentation/jetty/modules/code/examples/jetty-modules/postgresql.mod create mode 100644 documentation/jetty/modules/code/examples/jetty-modules/remote-debug.mod create mode 100644 documentation/jetty/modules/code/examples/pom.xml create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ArchitectureDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/HTTP2Docs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/SessionHandlerDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java create mode 100644 documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java create mode 100644 documentation/jetty/modules/operations-guide/images/jmc-server-dump.png create mode 100644 documentation/jetty/modules/operations-guide/nav.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/annotations/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/arch/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/begin/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/deploy/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/features/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/howtos/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/jaas/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/jaspi/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/jmx/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/jndi/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/jsf-taglibs/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/jsp/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/jstl/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/keystore/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/modules/custom.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/modules/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/modules/standard.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/protocols/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/quickstart/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/server/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/session/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/start/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/start/start-jpms.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/tools/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/troubleshooting/index.adoc create mode 100644 documentation/jetty/modules/operations-guide/pages/xml/index.adoc create mode 100644 documentation/jetty/modules/programming-guide/images/jmc-server-dump.png create mode 100644 documentation/jetty/modules/programming-guide/nav.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/arch/bean.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/arch/io.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/arch/jmx.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/arch/listener.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/arch/threads.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/client/http.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/client/http2.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/client/http3.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/client/index.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/client/io-arch.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/client/websocket.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/index.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-jspc-maven-plugin.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-helloworld.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-plugin.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/migration/11-to-12.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/migration/94-to-10.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/compliance.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/fastcgi.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/http.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/http2.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/http3.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/index.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/io-arch.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/session.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/server/websocket.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/troubleshooting/component-dump.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/troubleshooting/debugging.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/troubleshooting/index.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/troubleshooting/logging.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/troubleshooting/state-tracking.adoc create mode 100644 documentation/jetty/modules/programming-guide/pages/troubleshooting/thread-dump.adoc create mode 100644 documentation/jetty/pom.xml create mode 100644 tests/jetty-testers/src/main/java/org/eclipse/jetty/tests/testers/RunJetty.java diff --git a/documentation/jetty/.gitignore b/documentation/jetty/.gitignore new file mode 100644 index 000000000000..27ef6457e2b3 --- /dev/null +++ b/documentation/jetty/.gitignore @@ -0,0 +1,2 @@ +/.asciidoctorconfig +/provided-antora-playbook.yml diff --git a/documentation/jetty/README.adoc b/documentation/jetty/README.adoc new file mode 100644 index 000000000000..e6321b51e0e2 --- /dev/null +++ b/documentation/jetty/README.adoc @@ -0,0 +1,32 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Documentation + +This project is the root of the Jetty documentation. +The content files in this project get sourced by the Antora playbook in the playbook repository that builds the website. + +In order to build the documentation locally, you first need to prepare a jetty-home directory by running the following command from the top-level folder of the Jetty project: + + $ mvn install -Dcollector -Pfast -am -pl documentation/jetty + +Then you can use the following command from this directory to prepare and run Antora using a preview profile: + + $ mvn antora -N + +If you don't run the first command, the Antora build will still succeed, but you will get warnings about missing includes for files taken from jetty-home. + +The `antora:antora` goal, which the `antora` lifecycle invokes, takes advantage of the playbook provider feature so the playbook for this branch can be centrally managed in the playbook repository. + +Note that this preview profile does not run the jetty blocks, so you will only see the configuration for those runs in the preview site. +If you want to build the full site, use the build in the playbook repository. diff --git a/documentation/jetty/antora.yml b/documentation/jetty/antora.yml new file mode 100644 index 000000000000..c553fa0c27c8 --- /dev/null +++ b/documentation/jetty/antora.yml @@ -0,0 +1,28 @@ +name: jetty +version: '12' +title: Eclipse Jetty +asciidoc: + attributes: + javadoc-url: https://eclipse.dev/jetty/javadoc/jetty-12 + jdurl: '{javadoc-url}' + jetty-home: ${jetty.home}@ + version: 12.0.10-SNAPSHOT + idprefix: '' + idseparator: '' + ee-all: ee{8,9,10} + ee-current: ee10 + ee-current-caps: EE 10 + run-jetty-classpath: ${settings.localRepository}/org/eclipse/jetty/tests/jetty-testers/${project.version}/jetty-testers-${project.version}.jar${path.separator}${run.jetty.classpath} +nav: +- modules/operations-guide/nav.adoc +- modules/programming-guide/nav.adoc +ext: + collector: + - run: + command: mvn install -ntp -B -Dcollector -Pfast -am -pl documentation/jetty + scan: + dir: documentation/jetty/target/collector + - scan: + dir: jetty-core/jetty-server/src/main/java + files: org/eclipse/jetty/server/CustomRequestLog.java + base: modules/code/partials diff --git a/documentation/jetty/modules/ROOT/pages/index.adoc b/documentation/jetty/modules/ROOT/pages/index.adoc new file mode 100644 index 000000000000..ac1c57eb289b --- /dev/null +++ b/documentation/jetty/modules/ROOT/pages/index.adoc @@ -0,0 +1,24 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Eclipse Jetty + +This section of the site contains the documentation for {page-component-title} {page-version}. + +== xref:operations-guide:index.adoc[] + +The Eclipse Jetty Operations Guide targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications. + +== xref:programming-guide:index.adoc[] + +The Eclipse Jetty Programming Guide targets developers who want to use the Eclipse Jetty libraries in their applications, and advanced sysops/devops that want to customize the deployment of web applications. diff --git a/documentation/jetty/modules/code/examples/jetty-modules/jpms.mod b/documentation/jetty/modules/code/examples/jetty-modules/jpms.mod new file mode 100644 index 000000000000..6d261a6eb542 --- /dev/null +++ b/documentation/jetty/modules/code/examples/jetty-modules/jpms.mod @@ -0,0 +1,8 @@ +[description] +JPMS Configuration Module + +[ini] +--jpms + +[jpms] +# Additional JPMS configuration. diff --git a/documentation/jetty/modules/code/examples/jetty-modules/jvm.mod b/documentation/jetty/modules/code/examples/jetty-modules/jvm.mod new file mode 100644 index 000000000000..e8b2fc51d0a9 --- /dev/null +++ b/documentation/jetty/modules/code/examples/jetty-modules/jvm.mod @@ -0,0 +1,6 @@ +[description] +JVM Options Module + +[exec] +-Xmx1g +-Xlog:gc*,gc+stats=off:file=logs/gc.log:time,level,tags diff --git a/documentation/jetty/modules/code/examples/jetty-modules/postgresql.mod b/documentation/jetty/modules/code/examples/jetty-modules/postgresql.mod new file mode 100644 index 000000000000..e5489e145db8 --- /dev/null +++ b/documentation/jetty/modules/code/examples/jetty-modules/postgresql.mod @@ -0,0 +1,15 @@ +[description] +Postgres JDBC Driver Module + +[lib] +lib/postgresql-${postgresql-version}.jar + +[files] +maven://org.postgresql/postgresql/${postgresql-version}|lib/postgresql-${postgresql-version}.jar + +[ini] +postgresql-version?=42.6.0 + +[ini-template] +## Postgres JDBC version. +# postgresql-version=42.6.0 diff --git a/documentation/jetty/modules/code/examples/jetty-modules/remote-debug.mod b/documentation/jetty/modules/code/examples/jetty-modules/remote-debug.mod new file mode 100644 index 000000000000..9cae360e33f5 --- /dev/null +++ b/documentation/jetty/modules/code/examples/jetty-modules/remote-debug.mod @@ -0,0 +1,5 @@ +[description] +Enables remote debugging + +[exec] +-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 diff --git a/documentation/jetty/modules/code/examples/pom.xml b/documentation/jetty/modules/code/examples/pom.xml new file mode 100644 index 000000000000..5d885e78c1bb --- /dev/null +++ b/documentation/jetty/modules/code/examples/pom.xml @@ -0,0 +1,206 @@ + + + + 4.0.0 + + org.eclipse.jetty.documentation + documentation + 12.0.10-SNAPSHOT + ../../../../pom.xml + + code-examples + pom + Documentation :: Code Examples + + + true + true + + + + + org.eclipse.jetty + jetty-alpn-server + + + org.eclipse.jetty + jetty-client + + + org.eclipse.jetty + jetty-infinispan-embedded-query + + + org.eclipse.jetty + jetty-infinispan-remote-query + + + org.eclipse.jetty + jetty-jmx + + + org.eclipse.jetty + jetty-nosql + + + org.eclipse.jetty + jetty-rewrite + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-session + + + org.eclipse.jetty + jetty-unixdomain-server + + + org.eclipse.jetty + jetty-util-ajax + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + ${project.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-servlets + ${project.version} + + + org.eclipse.jetty.ee10.websocket + jetty-ee10-websocket-jakarta-server + ${project.version} + + + org.eclipse.jetty.fcgi + jetty-fcgi-client + + + org.eclipse.jetty.gcloud + jetty-gcloud-session-manager + + + org.eclipse.jetty.http2 + jetty-http2-client-transport + + + org.eclipse.jetty.http2 + jetty-http2-server + + + org.eclipse.jetty.http3 + jetty-http3-client-transport + + + org.eclipse.jetty.http3 + jetty-http3-server + + + org.eclipse.jetty.memcached + jetty-memcached-sessions + + + org.eclipse.jetty.websocket + jetty-websocket-jetty-client + + + org.eclipse.jetty.websocket + jetty-websocket-jetty-server + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile-code-examples + + compile + + compile + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + false + + + [21,) + [ERROR] OLD JDK [${java.version}] in use. Jetty documentation ${project.version} MUST use JDK 21 or newer + + + + + + + + + + jdk17-18 + + [17,19) + + + + + maven-compiler-plugin + + + **/ArchitectureDocs.java + + + + + + + + jdk19-20 + + [19,21) + + + + + maven-compiler-plugin + + ${java.specification.version} + ${java.specification.version} + ${java.specification.version} + true + + + + + + + jdk21+ + + [21,) + + + + + maven-compiler-plugin + + ${java.specification.version} + ${java.specification.version} + ${java.specification.version} + + + + + + + diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ArchitectureDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ArchitectureDocs.java new file mode 100644 index 000000000000..721372f5c247 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ArchitectureDocs.java @@ -0,0 +1,33 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming; + +import java.util.concurrent.Executors; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +@SuppressWarnings("unused") +public class ArchitectureDocs +{ + public void configureVirtualThreads() + { + // tag::virtual[] + QueuedThreadPool threadPool = new QueuedThreadPool(); + threadPool.setVirtualThreadsExecutor(Executors.newVirtualThreadPerTaskExecutor()); + + Server server = new Server(threadPool); + // end::virtual[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java new file mode 100644 index 000000000000..f309dad3af35 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java @@ -0,0 +1,284 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.Container; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class ComponentDocs +{ + public void start() throws Exception + { + // tag::start[] + class Monitor extends AbstractLifeCycle + { + } + + class Root extends ContainerLifeCycle + { + // Monitor is an internal component. + private final Monitor monitor = new Monitor(); + + public Root() + { + // The Monitor life cycle is managed by Root. + addManaged(monitor); + } + } + + class Service extends ContainerLifeCycle + { + // An instance of the Java scheduler service. + private ScheduledExecutorService scheduler; + + @Override + protected void doStart() throws Exception + { + // Java's schedulers cannot be restarted, so they must + // be created anew every time their container is started. + scheduler = Executors.newSingleThreadScheduledExecutor(); + // Even if Java scheduler does not implement + // LifeCycle, make it part of the component tree. + addBean(scheduler); + // Start all the children beans. + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + // Perform the opposite operations that were + // performed in doStart(), in reverse order. + super.doStop(); + removeBean(scheduler); + scheduler.shutdown(); + } + } + + // Create a Root instance. + Root root = new Root(); + + // Create a Service instance. + Service service = new Service(); + + // Link the components. + root.addBean(service); + + // Start the root component to + // start the whole component tree. + root.start(); + // end::start[] + } + + public void restart() throws Exception + { + // tag::restart[] + class Root extends ContainerLifeCycle + { + } + + class Service extends ContainerLifeCycle + { + // An instance of the Java scheduler service. + private ScheduledExecutorService scheduler; + + @Override + protected void doStart() throws Exception + { + // Java's schedulers cannot be restarted, so they must + // be created anew every time their container is started. + scheduler = Executors.newSingleThreadScheduledExecutor(); + // Even if Java scheduler does not implement + // LifeCycle, make it part of the component tree. + addBean(scheduler); + // Start all the children beans. + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + // Perform the opposite operations that were + // performed in doStart(), in reverse order. + super.doStop(); + removeBean(scheduler); + scheduler.shutdown(); + } + } + + Root root = new Root(); + Service service = new Service(); + root.addBean(service); + + // Start the Root component. + root.start(); + + // Stop temporarily Service without stopping the Root. + service.stop(); + + // Restart Service. + service.start(); + // end::restart[] + } + + public void getBeans() throws Exception + { + // tag::getBeans[] + class Root extends ContainerLifeCycle + { + } + + class Service extends ContainerLifeCycle + { + private ScheduledExecutorService scheduler; + + @Override + protected void doStart() throws Exception + { + scheduler = Executors.newSingleThreadScheduledExecutor(); + addBean(scheduler); + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + removeBean(scheduler); + scheduler.shutdown(); + } + } + + Root root = new Root(); + Service service = new Service(); + root.addBean(service); + + // Start the Root component. + root.start(); + + // Find all the direct children of root. + Collection children = root.getBeans(); + // children contains only service + + // Find all descendants of root that are instance of a particular class. + Collection schedulers = root.getContainedBeans(ScheduledExecutorService.class); + // schedulers contains the service scheduler. + // end::getBeans[] + } + + public void lifecycleListener() + { + // tag::lifecycleListener[] + Server server = new Server(); + + // Add an event listener of type LifeCycle.Listener. + server.addEventListener(new LifeCycle.Listener() + { + @Override + public void lifeCycleStarted(LifeCycle lifeCycle) + { + System.getLogger("server").log(INFO, "Server {0} has been started", lifeCycle); + } + + @Override + public void lifeCycleFailure(LifeCycle lifeCycle, Throwable failure) + { + System.getLogger("server").log(INFO, "Server {0} failed to start", lifeCycle, failure); + } + + @Override + public void lifeCycleStopped(LifeCycle lifeCycle) + { + System.getLogger("server").log(INFO, "Server {0} has been stopped", lifeCycle); + } + }); + // end::lifecycleListener[] + } + + public void containerListener() + { + // tag::containerListener[] + Server server = new Server(); + + // Add an event listener of type LifeCycle.Listener. + server.addEventListener(new Container.Listener() + { + @Override + public void beanAdded(Container parent, Object child) + { + System.getLogger("server").log(INFO, "Added bean {1} to {0}", parent, child); + } + + @Override + public void beanRemoved(Container parent, Object child) + { + System.getLogger("server").log(INFO, "Removed bean {1} from {0}", parent, child); + } + }); + // end::containerListener[] + } + + public void containerSiblings() + { + // tag::containerSiblings[] + class Parent extends ContainerLifeCycle + { + } + + class Child + { + } + + // The older child takes care of its siblings. + class OlderChild extends Child implements Container.Listener + { + private Set siblings = new HashSet<>(); + + @Override + public void beanAdded(Container parent, Object child) + { + siblings.add(child); + } + + @Override + public void beanRemoved(Container parent, Object child) + { + siblings.remove(child); + } + } + + Parent parent = new Parent(); + + Child older = new OlderChild(); + // The older child is a child bean _and_ a listener. + parent.addBean(older); + + Child younger = new Child(); + // Adding a younger child will notify the older child. + parent.addBean(younger); + // end::containerSiblings[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java new file mode 100644 index 000000000000..fad96cac2776 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java @@ -0,0 +1,403 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.CompletableTask; +import org.eclipse.jetty.util.IteratingCallback; +import org.eclipse.jetty.util.Utf8StringBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class ContentDocs +{ + private static final Logger LOG = LoggerFactory.getLogger(ContentDocs.class); + + @SuppressWarnings("unused") + class Idiomatic + { + // tag::idiomatic[] + public void read(Content.Source source) + { + // Read from the source in a loop. + while (true) + { + // Read a chunk, must be eventually released. + Content.Chunk chunk = source.read(); // <1> + + // If no chunk, demand to be called back when there are more chunks. + if (chunk == null) + { + source.demand(() -> read(source)); + return; + } + + // If there is a failure reading, handle it. + if (Content.Chunk.isFailure(chunk)) + { + boolean fatal = chunk.isLast(); + if (fatal) + { + // A fatal failure, such as a network failure. + handleFatalFailure(chunk.getFailure()); + // No recovery is possible, stop reading + // by returning without demanding. + return; + } + else + { + // A transient failure such as a read timeout. + handleTransientFailure(chunk.getFailure()); + // Recovery is possible, try to read again. + continue; + } + } + + // A normal chunk of content, consume it. + consume(chunk); + + // Release the chunk. + chunk.release(); // <2> + + // Stop reading if EOF was reached. + if (chunk.isLast()) + return; + + // Loop around to read another chunk. + } + } + // end::idiomatic[] + } + + @SuppressWarnings("unused") + static class Async + { + // tag::async[] + public void read(Content.Source source) + { + // Read a chunk, must be eventually released. + Content.Chunk chunk = source.read(); // <1> + + // If no chunk, demand to be called back when there are more chunks. + if (chunk == null) + { + source.demand(() -> read(source)); + return; + } + + // If there is a failure reading, always treat it as fatal. + if (Content.Chunk.isFailure(chunk)) + { + // If the failure is transient, fail the source + // to indicate that there will be no more reads. + if (!chunk.isLast()) + source.fail(chunk.getFailure()); + + // Handle the failure and stop reading by not demanding. + handleFatalFailure(chunk.getFailure()); + return; + } + + // Consume the chunk asynchronously, and do not + // read more chunks until this has been consumed. + CompletableFuture consumed = consumeAsync(chunk); + + // Release the chunk. + chunk.release(); // <2> + + // Only when the chunk has been consumed try to read more. + consumed.whenComplete((result, failure) -> + { + if (failure == null) + { + // Continue reading if EOF was not reached. + if (!chunk.isLast()) + source.demand(() -> read(source)); + } + else + { + // If there is a failure reading, handle it, + // and stop reading by not demanding. + handleFatalFailure(failure); + } + }); + } + // end::async[] + + private CompletableFuture consumeAsync(Content.Chunk chunk) + { + return CompletableFuture.completedFuture(null); + } + } + + private static void handleFatalFailure(Throwable failure) + { + } + + private static void handleTransientFailure(Throwable failure) + { + } + + private void consume(Content.Chunk chunk) + { + } + + @SuppressWarnings("unused") + static class ChunkSync + { + private FileChannel fileChannel; + + // tag::chunkSync[] + public void consume(Content.Chunk chunk) throws IOException + { + // Consume the chunk synchronously within this method. + + // For example, parse the bytes into other objects, + // or copy the bytes elsewhere (e.g. the file system). + fileChannel.write(chunk.getByteBuffer()); + + if (chunk.isLast()) + fileChannel.close(); + } + // end::chunkSync[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::chunkAsync[] + // CompletableTask is-a CompletableFuture. + public class ChunksToString extends CompletableTask + { + private final List chunks = new ArrayList<>(); + private final Content.Source source; + + public ChunksToString(Content.Source source) + { + this.source = source; + } + + @Override + public void run() + { + while (true) + { + // Read a chunk, must be eventually released. + Content.Chunk chunk = source.read(); // <1> + + if (chunk == null) + { + source.demand(this); + return; + } + + if (Content.Chunk.isFailure(chunk)) + { + handleFatalFailure(chunk.getFailure()); + return; + } + + // A normal chunk of content, consume it. + consume(chunk); + + // Release the chunk. + // This pairs the call to read() above. + chunk.release(); // <2> + + if (chunk.isLast()) + { + // Produce the result. + String result = getResult(); + + // Complete this CompletableFuture with the result. + complete(result); + + // The reading is complete. + return; + } + } + } + + public void consume(Content.Chunk chunk) + { + // The chunk is not consumed within this method, but + // stored away for later use, so it must be retained. + chunk.retain(); // <3> + chunks.add(chunk); + } + + public String getResult() + { + Utf8StringBuilder builder = new Utf8StringBuilder(); + // Iterate over the chunks, copying and releasing. + for (Content.Chunk chunk : chunks) + { + // Copy the chunk bytes into the builder. + builder.append(chunk.getByteBuffer()); + + // The chunk has been consumed, release it. + // This pairs the retain() in consume(). + chunk.release(); // <4> + } + return builder.toCompleteString(); + } + } + // end::chunkAsync[] + + static class SinkWrong + { + // tag::sinkWrong[] + public void wrongWrite(Content.Sink sink, ByteBuffer content1, ByteBuffer content2) + { + // Initiate a first write. + sink.write(false, content1, Callback.NOOP); + + // WRONG! Cannot initiate a second write before the first is complete. + sink.write(true, content2, Callback.NOOP); + } + // end::sinkWrong[] + } + + static class SinkMany + { + // tag::sinkMany[] + public void manyWrites(Content.Sink sink, ByteBuffer content1, ByteBuffer content2) + { + // Initiate a first write. + Callback.Completable resultOfWrites = Callback.Completable.with(callback1 -> sink.write(false, content1, callback1)) + // Chain a second write only when the first is complete. + .compose(callback2 -> sink.write(true, content2, callback2)); + + // Use the resulting Callback.Completable as you would use a CompletableFuture. + // For example: + resultOfWrites.whenComplete((ignored, failure) -> + { + if (failure == null) + System.getLogger("sink").log(INFO, "writes completed successfully"); + else + System.getLogger("sink").log(INFO, "writes failed", failure); + }); + } + // end::sinkMany[] + } + + // tag::copy[] + @SuppressWarnings("InnerClassMayBeStatic") + class Copy extends IteratingCallback + { + private final Content.Source source; + private final Content.Sink sink; + private final Callback callback; + private Content.Chunk chunk; + + public Copy(Content.Source source, Content.Sink sink, Callback callback) + { + this.source = source; + this.sink = sink; + // The callback to notify when the copy is completed. + this.callback = callback; + } + + @Override + protected Action process() throws Throwable + { + // If the last write completed, succeed this IteratingCallback, + // causing onCompleteSuccess() to be invoked. + if (chunk != null && chunk.isLast()) + return Action.SUCCEEDED; + + // Read a chunk. + chunk = source.read(); + + // No chunk, demand to be called back when there will be more chunks. + if (chunk == null) + { + source.demand(this::iterate); + return Action.IDLE; + } + + // The read failed, re-throw the failure + // causing onCompleteFailure() to be invoked. + if (Content.Chunk.isFailure(chunk)) + throw chunk.getFailure(); + + // Copy the chunk. + sink.write(chunk.isLast(), chunk.getByteBuffer(), this); + return Action.SCHEDULED; + } + + @Override + public void succeeded() + { + // After every successful write, release the chunk. + chunk.release(); + super.succeeded(); + } + + @Override + public void failed(Throwable x) + { + super.failed(x); + } + + @Override + protected void onCompleteSuccess() + { + // The copy is succeeded, succeed the callback. + callback.succeeded(); + } + + @Override + protected void onCompleteFailure(Throwable failure) + { + // In case of a failure, either on the + // read or on the write, release the chunk. + chunk.release(); + + // The copy is failed, fail the callback. + callback.failed(failure); + } + + @Override + public InvocationType getInvocationType() + { + return InvocationType.NON_BLOCKING; + } + } + // end::copy[] + + static class Blocking + { + // tag::blocking[] + public void blockingWrite(Content.Sink sink, ByteBuffer content1, ByteBuffer content2) throws IOException + { + // First blocking write, returns only when the write is complete. + Content.Sink.write(sink, false, content1); + + // Second blocking write, returns only when the write is complete. + // It is legal to perform the writes sequentially, since they are blocking. + Content.Sink.write(sink, true, content2); + } + // end::blocking[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/HTTP2Docs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/HTTP2Docs.java new file mode 100644 index 000000000000..a6ed9dfe66c1 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/HTTP2Docs.java @@ -0,0 +1,94 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.util.Callback; + +@SuppressWarnings("unused") +public class HTTP2Docs +{ + public void dataDemanded() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + CompletableFuture sessionCF = http2Client.connect(serverAddress, new Session.Listener() {}); + Session session = sessionCF.get(); + + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP2Client {version}"); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders); + HeadersFrame headersFrame = new HeadersFrame(request, null, true); + + // tag::dataUnwrap[] + record Chunk(ByteBuffer byteBuffer, Callback callback) + { + } + + // A queue that consumers poll to consume content asynchronously. + Queue dataQueue = new ConcurrentLinkedQueue<>(); + + // Implementation of Stream.Listener.onDataAvailable(Stream stream) + // in case of unwrapping of the Data object for asynchronous content + // consumption and demand. + Stream.Listener listener = new Stream.Listener() + { + @Override + public void onDataAvailable(Stream stream) + { + Stream.Data data = stream.readData(); + + if (data == null) + { + stream.demand(); + return; + } + + // Get the content buffer. + ByteBuffer byteBuffer = data.frame().getByteBuffer(); + + // Unwrap the Data object, converting it to a Chunk. + // The Data.release() semantic is maintained in the completion of the Callback. + dataQueue.offer(new Chunk(byteBuffer, Callback.from(() -> + { + // When the buffer has been consumed, then: + // A) release the Data object. + data.release(); + // B) possibly demand more DATA frames. + if (!data.frame().isEndStream()) + stream.demand(); + }))); + + // Do not demand more data here, to avoid to overflow the queue. + } + }; + // end::dataUnwrap[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java new file mode 100644 index 000000000000..4ffdee236423 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java @@ -0,0 +1,271 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming; + +import java.lang.management.ManagementFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import javax.rmi.ssl.SslRMIClientSocketFactory; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.jmx.ConnectorServer; +import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.jmx.ObjectMBean; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.annotation.ManagedOperation; +import org.eclipse.jetty.util.annotation.Name; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +@SuppressWarnings("unused") +public class JMXDocs +{ + public void server() + { + // tag::server[] + Server server = new Server(); + + // Create an MBeanContainer with the platform MBeanServer. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + + // Add MBeanContainer to the root component. + server.addBean(mbeanContainer); + // end::server[] + } + + public void client() + { + // tag::client[] + HttpClient httpClient = new HttpClient(); + + // Create an MBeanContainer with the platform MBeanServer. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + + // Add MBeanContainer to the root component. + httpClient.addBean(mbeanContainer); + // end::client[] + } + + public void remote() throws Exception + { + // tag::remote[] + Server server = new Server(); + + // Setup Jetty JMX. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + server.addBean(mbeanContainer); + + // Setup ConnectorServer. + + // Bind the RMI server to the wildcard address and port 1999. + // Bind the RMI registry to the wildcard address and port 1099. + JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1999, "/jndi/rmi:///jmxrmi"); + ConnectorServer jmxServer = new ConnectorServer(jmxURL, "org.eclipse.jetty.jmx:name=rmiconnectorserver"); + + // Add ConnectorServer as a bean, so it is started + // with the Server and also exported as MBean. + server.addBean(jmxServer); + + server.start(); + // end::remote[] + } + + public static void main(String[] args) throws Exception + { + new JMXDocs().remote(); + } + + public void remoteAuthorization() throws Exception + { + // tag::remoteAuthorization[] + Server server = new Server(); + + // Setup Jetty JMX. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + server.addBean(mbeanContainer); + + // Setup ConnectorServer. + JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1099, "/jndi/rmi:///jmxrmi"); + Map env = new HashMap<>(); + env.put("com.sun.management.jmxremote.access.file", "/path/to/users.access"); + env.put("com.sun.management.jmxremote.password.file", "/path/to/users.password"); + ConnectorServer jmxServer = new ConnectorServer(jmxURL, env, "org.eclipse.jetty.jmx:name=rmiconnectorserver"); + server.addBean(jmxServer); + + server.start(); + // end::remoteAuthorization[] + } + + public void tlsRemote() throws Exception + { + // tag::tlsRemote[] + Server server = new Server(); + + // Setup Jetty JMX. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + server.addBean(mbeanContainer); + + // Setup SslContextFactory. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + // Setup ConnectorServer with SslContextFactory. + JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1099, "/jndi/rmi:///jmxrmi"); + ConnectorServer jmxServer = new ConnectorServer(jmxURL, null, "org.eclipse.jetty.jmx:name=rmiconnectorserver", sslContextFactory); + server.addBean(jmxServer); + + server.start(); + // end::tlsRemote[] + } + + public void tlsJMXConnector() throws Exception + { + // tag::tlsJMXConnector[] + // System properties necessary for an RMI client to trust a self-signed certificate. + System.setProperty("javax.net.ssl.trustStore", "/path/to/trustStore"); + System.setProperty("javax.net.ssl.trustStorePassword", "secret"); + + JMXServiceURL jmxURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://domain.com:1100/jmxrmi"); + + Map clientEnv = new HashMap<>(); + // Required to connect to the RMI registry via TLS. + clientEnv.put(ConnectorServer.RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); + + try (JMXConnector client = JMXConnectorFactory.connect(jmxURL, clientEnv)) + { + Set names = client.getMBeanServerConnection().queryNames(null, null); + } + // end::tlsJMXConnector[] + } + + public void jmxAnnotation() throws Exception + { + // tag::jmxAnnotation[] + // Annotate the class with @ManagedObject and provide a description. + @ManagedObject("Services that provide useful features") + class Services + { + private final Map services = new ConcurrentHashMap<>(); + private boolean enabled = true; + + // A read-only attribute with description. + @ManagedAttribute(value = "The number of services", readonly = true) + public int getServiceCount() + { + return services.size(); + } + + // A read-write attribute with description. + // Only the getter is annotated. + @ManagedAttribute(value = "Whether the services are enabled") + public boolean isEnabled() + { + return enabled; + } + + // There is no need to annotate the setter. + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + // An operation with description and impact. + // The @Name annotation is used to annotate parameters + // for example to display meaningful parameter names. + @ManagedOperation(value = "Retrieves the service with the given name", impact = "INFO") + public Object getService(@Name(value = "serviceName") String n) + { + return services.get(n); + } + } + // end::jmxAnnotation[] + } + + public void jmxCustomMBean() + { + // tag::jmxCustomMBean[] + //package com.acme; + @ManagedObject + class Service + { + } + + //package com.acme.jmx; + class ServiceMBean extends ObjectMBean + { + ServiceMBean(Object service) + { + super(service); + } + } + // end::jmxCustomMBean[] + } + + public void jmxCustomMBeanOverride() + { + // tag::jmxCustomMBeanOverride[] + //package com.acme; + // No Jetty JMX annotations. + class CountService + { + private int count; + + public int getCount() + { + return count; + } + + public void addCount(int value) + { + count += value; + } + } + + //package com.acme.jmx; + @ManagedObject("the count service") + class CountServiceMBean extends ObjectMBean + { + public CountServiceMBean(Object service) + { + super(service); + } + + private CountService getCountService() + { + return (CountService)super.getManagedObject(); + } + + @ManagedAttribute("the current service count") + public int getCount() + { + return getCountService().getCount(); + } + + @ManagedOperation(value = "adds the given value to the service count", impact = "ACTION") + public void addCount(@Name("count delta") int value) + { + getCountService().addCount(value); + } + } + // end::jmxCustomMBeanOverride[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java new file mode 100644 index 000000000000..cc60b78e7f92 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java @@ -0,0 +1,316 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.ConnectionStatistics; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IteratingCallback; + +@SuppressWarnings("unused") +public class SelectorManagerDocs +{ + // tag::connect[] + public void connect(SelectorManager selectorManager, Map context) throws IOException + { + String host = "host"; + int port = 8080; + + // Create an unconnected SocketChannel. + SocketChannel socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + + // Connect and register to Jetty. + if (socketChannel.connect(new InetSocketAddress(host, port))) + selectorManager.accept(socketChannel, context); + else + selectorManager.connect(socketChannel, context); + } + // end::connect[] + + // tag::accept[] + public void accept(ServerSocketChannel acceptor, SelectorManager selectorManager) throws IOException + { + // Wait until a client connects. + SocketChannel socketChannel = acceptor.accept(); + socketChannel.configureBlocking(false); + + // Accept and register to Jetty. + Object attachment = null; + selectorManager.accept(socketChannel, attachment); + } + // end::accept[] + + public void connection() + { + // tag::connection[] + // Extend AbstractConnection to inherit basic implementation. + class MyConnection extends AbstractConnection + { + public MyConnection(EndPoint endPoint, Executor executor) + { + super(endPoint, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + + // Declare interest for fill events. + // When the fill event happens, method onFillable() below is invoked. + fillInterested(); + } + + @Override + public void onFillable() + { + // Invoked when a fill event happens. + } + } + // end::connection[] + } + + public void connectionListener() throws Exception + { + // tag::connectionListener[] + class ThresholdConnectionListener implements Connection.Listener + { + private final AtomicInteger connections = new AtomicInteger(); + + private int threshold; + private boolean notified; + + public ThresholdConnectionListener(int threshold) + { + this.threshold = threshold; + } + + @Override + public void onOpened(Connection connection) + { + int count = connections.incrementAndGet(); + if (count > threshold && !notified) + { + notified = true; + System.getLogger("connection.threshold").log(System.Logger.Level.WARNING, "Connection threshold exceeded"); + } + } + + @Override + public void onClosed(Connection connection) + { + int count = connections.decrementAndGet(); + // Reset the alert when we are below 90% of the threshold. + if (count < threshold * 0.9F) + notified = false; + } + } + + // Configure server-side connectors with Connection.Listeners. + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + // Add statistics. + connector.addBean(new ConnectionStatistics()); + // Add your own Connection.Listener. + connector.addBean(new ThresholdConnectionListener(2048)); + server.start(); + + // Configure client-side HttpClient with Connection.Listeners. + HttpClient httpClient = new HttpClient(); + // Add statistics. + httpClient.addBean(new ConnectionStatistics()); + // Add your own Connection.Listener. + httpClient.addBean(new ThresholdConnectionListener(512)); + httpClient.start(); + // end::connectionListener[] + } + + public void echoWrong() + { + // tag::echo-wrong[] + class WrongEchoConnection extends AbstractConnection implements Callback + { + public WrongEchoConnection(EndPoint endPoint, Executor executor) + { + super(endPoint, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + + // Declare interest for fill events. + fillInterested(); + } + + @Override + public void onFillable() + { + try + { + ByteBuffer buffer = BufferUtil.allocate(1024); + int filled = getEndPoint().fill(buffer); + if (filled > 0) + { + // Filled some bytes, echo them back. + getEndPoint().write(this, buffer); + } + else if (filled == 0) + { + // No more bytes to fill, declare + // again interest for fill events. + fillInterested(); + } + else + { + // The other peer closed the + // connection, close it back. + getEndPoint().close(); + } + } + catch (Exception x) + { + getEndPoint().close(x); + } + } + + @Override + public void succeeded() + { + // The write is complete, fill again. + onFillable(); + } + + @Override + public void failed(Throwable x) + { + getEndPoint().close(x); + } + } + // end::echo-wrong[] + } + + public void echoCorrect() + { + // tag::echo-correct[] + class EchoConnection extends AbstractConnection + { + private final IteratingCallback callback = new EchoIteratingCallback(); + + public EchoConnection(EndPoint endp, Executor executor) + { + super(endp, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + + // Declare interest for fill events. + fillInterested(); + } + + @Override + public void onFillable() + { + // Start the iteration loop that reads and echoes back. + callback.iterate(); + } + + class EchoIteratingCallback extends IteratingCallback + { + private ByteBuffer buffer; + + @Override + protected Action process() throws Throwable + { + // Obtain a buffer if we don't already have one. + if (buffer == null) + buffer = BufferUtil.allocate(1024); + + int filled = getEndPoint().fill(buffer); + if (filled > 0) + { + // We have filled some bytes, echo them back. + getEndPoint().write(this, buffer); + + // Signal that the iteration should resume + // when the write() operation is completed. + return Action.SCHEDULED; + } + else if (filled == 0) + { + // We don't need the buffer anymore, so + // don't keep it around while we are idle. + buffer = null; + + // No more bytes to read, declare + // again interest for fill events. + fillInterested(); + + // Signal that the iteration is now IDLE. + return Action.IDLE; + } + else + { + // The other peer closed the connection, + // the iteration completed successfully. + return Action.SUCCEEDED; + } + } + + @Override + protected void onCompleteSuccess() + { + // The iteration completed successfully. + getEndPoint().close(); + } + + @Override + protected void onCompleteFailure(Throwable cause) + { + // The iteration completed with a failure. + getEndPoint().close(cause); + } + + @Override + public InvocationType getInvocationType() + { + return InvocationType.NON_BLOCKING; + } + } + } + // end::echo-correct[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java new file mode 100644 index 000000000000..b0a65e64e336 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java @@ -0,0 +1,632 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming; + +import java.io.InputStream; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jetty.util.IteratingCallback; +import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.websocket.api.Callback; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class WebSocketDocs +{ + @SuppressWarnings("InnerClassMayBeStatic") + // tag::autoDemand[] + // Attribute autoDemand is true by default. + @WebSocket(autoDemand = true) + public class AutoDemandAnnotatedEndPoint + { + @OnWebSocketOpen + public void onOpen(Session session) + { + // No need to demand here, because this endpoint is auto-demanding. + } + + @OnWebSocketMessage + public void onText(String message) + { + System.getLogger("ws.message").log(INFO, message); + // No need to demand here, because this endpoint is auto-demanding. + } + } + + public class AutoDemandListenerEndPoint implements Session.Listener.AutoDemanding + { + private Session session; + + @Override + public void onWebSocketOpen(Session session) + { + this.session = session; + // No need to demand here, because this endpoint is auto-demanding. + } + + @Override + public void onWebSocketText(String message) + { + System.getLogger("ws.message").log(INFO, message); + // No need to demand here, because this endpoint is auto-demanding. + } + } + // end::autoDemand[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::autoDemandWrong[] + public class WrongAutoDemandListenerEndPoint implements Session.Listener.AutoDemanding + { + private Session session; + + @Override + public void onWebSocketOpen(Session session) + { + this.session = session; + // No need to demand here, because this endpoint is auto-demanding. + } + + @Override + public void onWebSocketText(String message) + { + // Perform an asynchronous operation, such as invoking + // a third party service or just echoing the message back. + session.sendText(message, Callback.NOOP); + + // Returning from this method will automatically demand, + // so this method may be entered again before sendText() + // has been completed, causing a WritePendingException. + } + } + // end::autoDemandWrong[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::explicitDemand[] + public class ExplicitDemandListenerEndPoint implements Session.Listener + { + private Session session; + + @Override + public void onWebSocketOpen(Session session) + { + this.session = session; + + // Explicitly demand here, otherwise no other event is received. + session.demand(); + } + + @Override + public void onWebSocketText(String message) + { + // Perform an asynchronous operation, such as invoking + // a third party service or just echoing the message back. + + // We want to demand only when sendText() has completed, + // which is notified to the callback passed to sendText(). + session.sendText(message, Callback.from(session::demand, failure -> + { + // Handle the failure, in this case just closing the session. + session.close(StatusCode.SERVER_ERROR, "failure", Callback.NOOP); + })); + + // Return from the method without demanding yet, + // waiting for the completion of sendText() to demand. + } + } + // end::explicitDemand[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::listenerEndpoint[] + public class ListenerEndPoint implements Session.Listener + { + private Session session; + + @Override + public void onWebSocketOpen(Session session) + { + // The WebSocket endpoint has been opened. + + // Store the session to be able to send data to the remote peer. + this.session = session; + + // You may configure the session. + session.setMaxTextMessageSize(16 * 1024); + + // You may immediately send a message to the remote peer. + session.sendText("connected", Callback.from(session::demand, Throwable::printStackTrace)); + } + + @Override + public void onWebSocketText(String message) + { + // A WebSocket text message is received. + + // You may echo it back if it matches certain criteria. + if (message.startsWith("echo:")) + { + // Only demand for more events when sendText() is completed successfully. + session.sendText(message.substring("echo:".length()), Callback.from(session::demand, Throwable::printStackTrace)); + } + else + { + // Discard the message, and demand for more events. + session.demand(); + } + } + + @Override + public void onWebSocketBinary(ByteBuffer payload, Callback callback) + { + // A WebSocket binary message is received. + + // Save only PNG images. + boolean isPNG = true; + byte[] pngBytes = new byte[]{(byte)0x89, 'P', 'N', 'G'}; + for (int i = 0; i < pngBytes.length; ++i) + { + if (pngBytes[i] != payload.get(i)) + { + // Not a PNG image. + isPNG = false; + break; + } + } + + if (isPNG) + savePNGImage(payload); + + // Complete the callback to release the payload ByteBuffer. + callback.succeed(); + + // Demand for more events. + session.demand(); + } + + @Override + public void onWebSocketError(Throwable cause) + { + // The WebSocket endpoint failed. + + // You may log the error. + cause.printStackTrace(); + + // You may dispose resources. + disposeResources(); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) + { + // The WebSocket endpoint has been closed. + + // You may dispose resources. + disposeResources(); + } + } + // end::listenerEndpoint[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::streamingListenerEndpoint[] + public class StreamingListenerEndpoint implements Session.Listener + { + private Session session; + + @Override + public void onWebSocketOpen(Session session) + { + this.session = session; + session.demand(); + } + + @Override + public void onWebSocketPartialText(String payload, boolean fin) + { + // Forward chunks to external REST service, asynchronously. + // Only demand when the forwarding completed successfully. + CompletableFuture result = forwardToREST(payload, fin); + result.whenComplete((ignored, failure) -> + { + if (failure == null) + session.demand(); + else + failure.printStackTrace(); + }); + } + + @Override + public void onWebSocketPartialBinary(ByteBuffer payload, boolean fin, Callback callback) + { + // Save chunks to file. + appendToFile(payload, fin); + + // Complete the callback to release the payload ByteBuffer. + callback.succeed(); + + // Demand for more events. + session.demand(); + } + } + // end::streamingListenerEndpoint[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::annotatedEndpoint[] + @WebSocket(autoDemand = false) // <1> + public class AnnotatedEndPoint + { + @OnWebSocketOpen // <2> + public void onOpen(Session session) + { + // The WebSocket endpoint has been opened. + + // You may configure the session. + session.setMaxTextMessageSize(16 * 1024); + + // You may immediately send a message to the remote peer. + session.sendText("connected", Callback.from(session::demand, Throwable::printStackTrace)); + } + + @OnWebSocketMessage // <3> + public void onTextMessage(Session session, String message) + { + // A WebSocket textual message is received. + + // You may echo it back if it matches certain criteria. + if (message.startsWith("echo:")) + { + // Only demand for more events when sendText() is completed successfully. + session.sendText(message.substring("echo:".length()), Callback.from(session::demand, Throwable::printStackTrace)); + } + else + { + // Discard the message, and demand for more events. + session.demand(); + } + } + + @OnWebSocketMessage // <3> + public void onBinaryMessage(Session session, ByteBuffer payload, Callback callback) + { + // A WebSocket binary message is received. + + // Save only PNG images. + boolean isPNG = true; + byte[] pngBytes = new byte[]{(byte)0x89, 'P', 'N', 'G'}; + for (int i = 0; i < pngBytes.length; ++i) + { + if (pngBytes[i] != payload.get(i)) + { + // Not a PNG image. + isPNG = false; + break; + } + } + + if (isPNG) + savePNGImage(payload); + + // Complete the callback to release the payload ByteBuffer. + callback.succeed(); + + // Demand for more events. + session.demand(); + } + + @OnWebSocketError // <4> + public void onError(Throwable cause) + { + // The WebSocket endpoint failed. + + // You may log the error. + cause.printStackTrace(); + + // You may dispose resources. + disposeResources(); + } + + @OnWebSocketClose // <5> + public void onClose(int statusCode, String reason) + { + // The WebSocket endpoint has been closed. + + // You may dispose resources. + disposeResources(); + } + } + // end::annotatedEndpoint[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::partialAnnotatedEndpoint[] + @WebSocket(autoDemand = false) + public class PartialAnnotatedEndpoint + { + @OnWebSocketMessage + public void onTextMessage(Session session, String partialText, boolean fin) + { + // Forward the partial text. + // Demand only when the forward completed. + CompletableFuture result = forwardToREST(partialText, fin); + result.whenComplete((ignored, failure) -> + { + if (failure == null) + session.demand(); + else + failure.printStackTrace(); + }); + } + + @OnWebSocketMessage + public void onBinaryMessage(Session session, ByteBuffer partialPayload, boolean fin, Callback callback) + { + // Save partial payloads to file. + appendToFile(partialPayload, fin); + // Complete the callback to release the payload ByteBuffer. + callback.succeed(); + // Demand for more events. + session.demand(); + } + } + // end::partialAnnotatedEndpoint[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::streamingAnnotatedEndpoint[] + @WebSocket + public class StreamingAnnotatedEndpoint + { + @OnWebSocketMessage + public void onTextMessage(Reader reader) + { + // Read from the Reader and forward. + // Caution: blocking APIs. + forwardToREST(reader); + } + + @OnWebSocketMessage + public void onBinaryMessage(InputStream stream) + { + // Read from the InputStream and save to file. + // Caution: blocking APIs. + appendToFile(stream); + } + } + // end::streamingAnnotatedEndpoint[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::sessionConfigure[] + public class ConfigureEndpoint implements Session.Listener + { + @Override + public void onWebSocketOpen(Session session) + { + // Configure the max length of incoming messages. + session.setMaxTextMessageSize(16 * 1024); + + // Configure the idle timeout. + session.setIdleTimeout(Duration.ofSeconds(30)); + + // Demand for more events. + session.demand(); + } + } + // end::sessionConfigure[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::sendNonBlocking[] + @WebSocket + public class NonBlockingSendEndpoint + { + @OnWebSocketMessage + public void onText(Session session, String text) + { + // Send textual data to the remote peer. + session.sendText("data", new Callback() // <1> + { + @Override + public void succeed() + { + // Send binary data to the remote peer. + ByteBuffer bytes = readImageFromFile(); + session.sendBinary(bytes, new Callback() // <2> + { + @Override + public void succeed() + { + // Both sends succeeded. + } + + @Override + public void fail(Throwable x) + { + System.getLogger("websocket").log(System.Logger.Level.WARNING, "could not send binary data", x); + } + }); + } + + @Override + public void fail(Throwable x) + { + // No need to rethrow or close the session. + System.getLogger("websocket").log(System.Logger.Level.WARNING, "could not send textual data", x); + } + }); + + // remote.sendString("wrong", Callback.NOOP); // May throw WritePendingException! <3> + } + } + // end::sendNonBlocking[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::streamSendNonBlocking[] + @WebSocket(autoDemand = false) + public class StreamSendNonBlockingEndpoint + { + @OnWebSocketMessage + public void onText(Session session, String text) + { + new Sender(session).iterate(); + } + + private class Sender extends IteratingCallback implements Callback // <1> + { + private final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); + private final Session session; + private boolean finished; + + private Sender(Session session) + { + this.session = session; + } + + @Override + protected Action process() throws Throwable // <2> + { + if (finished) + return Action.SUCCEEDED; // <4> + + int read = readChunkToSendInto(byteBuffer); + if (read < 0) + { + // No more bytes to send, finish the WebSocket message. + session.sendPartialBinary(byteBuffer, true, this); // <3> + finished = true; + return Action.SCHEDULED; + } + else + { + // Send the chunk. + session.sendPartialBinary(byteBuffer, false, this); // <3> + return Action.SCHEDULED; + } + } + + @Override + public void succeed() + { + // When the send succeeds, succeed this IteratingCallback. + succeeded(); + } + + @Override + public void fail(Throwable x) + { + // When the send fails, fail this IteratingCallback. + failed(x); + } + + @Override + protected void onCompleteSuccess() + { + session.demand(); // <5> + } + + @Override + protected void onCompleteFailure(Throwable x) + { + x.printStackTrace(); + } + } + } + // end::streamSendNonBlocking[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::pingPongListener[] + public class RoundTripListenerEndpoint implements Session.Listener + { + private Session session; + + @Override + public void onWebSocketOpen(Session session) + { + this.session = session; + // Send to the remote peer the local nanoTime. + ByteBuffer buffer = ByteBuffer.allocate(8).putLong(NanoTime.now()).flip(); + session.sendPing(buffer, Callback.NOOP); + // Demand for more events. + session.demand(); + } + + @Override + public void onWebSocketPong(ByteBuffer payload) + { + // The remote peer echoed back the local nanoTime. + long start = payload.getLong(); + + // Calculate the round-trip time. + long roundTrip = NanoTime.since(start); + + // Demand for more events. + session.demand(); + } + } + // end::pingPongListener[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::sessionClose[] + @WebSocket + public class CloseEndpoint + { + @OnWebSocketMessage + public void onText(Session session, String text) + { + if ("close".equalsIgnoreCase(text)) + session.close(StatusCode.NORMAL, "bye", Callback.NOOP); + } + } + // end::sessionClose[] + + private static CompletableFuture forwardToREST(String payload, boolean fin) + { + return null; + } + + private static void forwardToREST(Reader reader) + { + } + + private static void appendToFile(ByteBuffer payload, boolean fin) + { + } + + private static void appendToFile(InputStream stream) + { + } + + private static void disposeResources() + { + } + + private static void savePNGImage(ByteBuffer byteBuffer) + { + } + + private static ByteBuffer readImageFromFile() + { + return null; + } + + private static int readChunkToSendInto(ByteBuffer byteBuffer) + { + return 0; + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java new file mode 100644 index 000000000000..d621cc99f6eb --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java @@ -0,0 +1,429 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.client; + +import java.io.ByteArrayOutputStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.io.Transport; +import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; +import org.eclipse.jetty.io.ssl.SslConnection; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; +import org.eclipse.jetty.util.thread.Scheduler; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class ClientConnectorDocs +{ + public void simplest() throws Exception + { + // tag::simplest[] + ClientConnector clientConnector = new ClientConnector(); + clientConnector.start(); + // end::simplest[] + } + + public void typical() throws Exception + { + // tag::typical[] + // Create and configure the SslContextFactory. + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + sslContextFactory.addExcludeProtocols("TLSv1", "TLSv1.1"); + + // Create and configure the thread pool. + QueuedThreadPool threadPool = new QueuedThreadPool(); + threadPool.setName("client"); + + // Create and configure the ClientConnector. + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(sslContextFactory); + clientConnector.setExecutor(threadPool); + clientConnector.start(); + // end::typical[] + } + + public void advanced() throws Exception + { + // tag::advanced[] + class CustomClientConnector extends ClientConnector + { + @Override + protected SelectorManager newSelectorManager() + { + return new ClientSelectorManager(getExecutor(), getScheduler(), getSelectors()) + { + @Override + protected void endPointOpened(EndPoint endpoint) + { + System.getLogger("endpoint").log(INFO, "opened %s", endpoint); + } + + @Override + protected void endPointClosed(EndPoint endpoint) + { + System.getLogger("endpoint").log(INFO, "closed %s", endpoint); + } + }; + } + } + + // Create and configure the thread pool. + QueuedThreadPool threadPool = new QueuedThreadPool(); + threadPool.setName("client"); + + // Create and configure the scheduler. + Scheduler scheduler = new ScheduledExecutorScheduler("scheduler-client", false); + + // Create and configure the custom ClientConnector. + CustomClientConnector clientConnector = new CustomClientConnector(); + clientConnector.setExecutor(threadPool); + clientConnector.setScheduler(scheduler); + clientConnector.start(); + // end::advanced[] + } + + public void connect() throws Exception + { + // tag::connect[] + class CustomConnection extends AbstractConnection + { + public CustomConnection(EndPoint endPoint, Executor executor) + { + super(endPoint, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + System.getLogger("connection").log(INFO, "Opened connection {0}", this); + } + + @Override + public void onFillable() + { + } + } + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.start(); + + String host = "serverHost"; + int port = 8080; + SocketAddress address = new InetSocketAddress(host, port); + + // The Transport instance. + Transport transport = Transport.TCP_IP; + + // The ClientConnectionFactory that creates CustomConnection instances. + ClientConnectionFactory connectionFactory = (endPoint, context) -> + { + System.getLogger("connection").log(INFO, "Creating connection for {0}", endPoint); + return new CustomConnection(endPoint, clientConnector.getExecutor()); + }; + + // The Promise to notify of connection creation success or failure. + CompletableFuture connectionPromise = new Promise.Completable<>(); + + // Populate the context with the mandatory keys to create and obtain connections. + Map context = new ConcurrentHashMap<>(); + context.put(Transport.class.getName(), transport); + context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); + clientConnector.connect(address, context); + + // Use the Connection when it's available. + + // Use it in a non-blocking way via CompletableFuture APIs. + connectionPromise.whenComplete((connection, failure) -> + { + System.getLogger("connection").log(INFO, "Created connection for {0}", connection); + }); + + // Alternatively, you can block waiting for the connection (or a failure). + // CustomConnection connection = connectionPromise.get(); + // end::connect[] + } + + public void telnet() throws Exception + { + // tag::telnet[] + class TelnetConnection extends AbstractConnection + { + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private Consumer consumer; + + public TelnetConnection(EndPoint endPoint, Executor executor) + { + super(endPoint, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + + // Declare interest for fill events. + fillInterested(); + } + + @Override + public void onFillable() + { + try + { + ByteBuffer buffer = BufferUtil.allocate(1024); + while (true) + { + int filled = getEndPoint().fill(buffer); + if (filled > 0) + { + while (buffer.hasRemaining()) + { + // Search for newline. + byte read = buffer.get(); + if (read == '\n') + { + // Notify the consumer of the line. + consumer.accept(bytes.toString(StandardCharsets.UTF_8)); + bytes.reset(); + } + else + { + bytes.write(read); + } + } + } + else if (filled == 0) + { + // No more bytes to fill, declare + // again interest for fill events. + fillInterested(); + return; + } + else + { + // The other peer closed the + // connection, close it back. + getEndPoint().close(); + return; + } + } + } + catch (Exception x) + { + getEndPoint().close(x); + } + } + + public void onLine(Consumer consumer) + { + this.consumer = consumer; + } + + public void writeLine(String line, Callback callback) + { + line = line + "\r\n"; + getEndPoint().write(callback, ByteBuffer.wrap(line.getBytes(StandardCharsets.UTF_8))); + } + } + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.start(); + + String host = "example.org"; + int port = 80; + SocketAddress address = new InetSocketAddress(host, port); + + ClientConnectionFactory connectionFactory = (endPoint, context) -> + new TelnetConnection(endPoint, clientConnector.getExecutor()); + + CompletableFuture connectionPromise = new Promise.Completable<>(); + + Map context = new HashMap<>(); + context.put(Transport.class.getName(), Transport.TCP_IP); + context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); + clientConnector.connect(address, context); + + connectionPromise.whenComplete((connection, failure) -> + { + if (failure == null) + { + // Register a listener that receives string lines. + connection.onLine(line -> System.getLogger("app").log(INFO, "line: {0}", line)); + + // Write a line. + connection.writeLine("GET / HTTP/1.0\r\n", Callback.NOOP); + } + else + { + failure.printStackTrace(); + } + }); + // end::telnet[] + } + + public void tlsTelnet() throws Exception + { + // tag::tlsTelnet[] + class TelnetConnection extends AbstractConnection + { + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private Consumer consumer; + + public TelnetConnection(EndPoint endPoint, Executor executor) + { + super(endPoint, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + + // Declare interest for fill events. + fillInterested(); + } + + @Override + public void onFillable() + { + try + { + ByteBuffer buffer = BufferUtil.allocate(1024); + while (true) + { + int filled = getEndPoint().fill(buffer); + if (filled > 0) + { + while (buffer.hasRemaining()) + { + // Search for newline. + byte read = buffer.get(); + if (read == '\n') + { + // Notify the consumer of the line. + consumer.accept(bytes.toString(StandardCharsets.UTF_8)); + bytes.reset(); + } + else + { + bytes.write(read); + } + } + } + else if (filled == 0) + { + // No more bytes to fill, declare + // again interest for fill events. + fillInterested(); + return; + } + else + { + // The other peer closed the + // connection, close it back. + getEndPoint().close(); + return; + } + } + } + catch (Exception x) + { + getEndPoint().close(x); + } + } + + public void onLine(Consumer consumer) + { + this.consumer = consumer; + } + + public void writeLine(String line, Callback callback) + { + line = line + "\r\n"; + getEndPoint().write(callback, ByteBuffer.wrap(line.getBytes(StandardCharsets.UTF_8))); + } + } + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.start(); + + // Use port 443 to contact the server using encrypted HTTP. + String host = "example.org"; + int port = 443; + SocketAddress address = new InetSocketAddress(host, port); + + ClientConnectionFactory connectionFactory = (endPoint, context) -> + new TelnetConnection(endPoint, clientConnector.getExecutor()); + + // Wrap the "telnet" ClientConnectionFactory with the SslClientConnectionFactory. + connectionFactory = new SslClientConnectionFactory(clientConnector.getSslContextFactory(), + clientConnector.getByteBufferPool(), clientConnector.getExecutor(), connectionFactory); + + // We will obtain a SslConnection now. + CompletableFuture connectionPromise = new Promise.Completable<>(); + + Map context = new ConcurrentHashMap<>(); + context.put(Transport.class.getName(), Transport.TCP_IP); + context.put(ClientConnector.CLIENT_CONNECTION_FACTORY_CONTEXT_KEY, connectionFactory); + context.put(ClientConnector.CONNECTION_PROMISE_CONTEXT_KEY, connectionPromise); + clientConnector.connect(address, context); + + connectionPromise.whenComplete((sslConnection, failure) -> + { + if (failure == null) + { + // Unwrap the SslConnection to access the "line" APIs in TelnetConnection. + TelnetConnection connection = (TelnetConnection)sslConnection.getSslEndPoint().getConnection(); + // Register a listener that receives string lines. + connection.onLine(line -> System.getLogger("app").log(INFO, "line: {0}", line)); + + // Write a line. + connection.writeLine("GET / HTTP/1.0\r\n", Callback.NOOP); + } + else + { + failure.printStackTrace(); + } + }); + // end::tlsTelnet[] + } + + public static void main(String[] args) throws Exception + { + new ClientConnectorDocs().tlsTelnet(); + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java new file mode 100644 index 000000000000..031cd030220f --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java @@ -0,0 +1,1158 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.client.http; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.eclipse.jetty.client.AsyncRequestContent; +import org.eclipse.jetty.client.Authentication; +import org.eclipse.jetty.client.AuthenticationStore; +import org.eclipse.jetty.client.BasicAuthentication; +import org.eclipse.jetty.client.BufferingResponseListener; +import org.eclipse.jetty.client.BytesRequestContent; +import org.eclipse.jetty.client.CompletableResponseListener; +import org.eclipse.jetty.client.ConnectionPool; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.Destination; +import org.eclipse.jetty.client.DigestAuthentication; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.InputStreamRequestContent; +import org.eclipse.jetty.client.InputStreamResponseListener; +import org.eclipse.jetty.client.OutputStreamRequestContent; +import org.eclipse.jetty.client.PathRequestContent; +import org.eclipse.jetty.client.ProxyConfiguration; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.client.Result; +import org.eclipse.jetty.client.RoundRobinConnectionPool; +import org.eclipse.jetty.client.Socks5; +import org.eclipse.jetty.client.Socks5Proxy; +import org.eclipse.jetty.client.StringRequestContent; +import org.eclipse.jetty.client.transport.HttpClientConnectionFactory; +import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; +import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; +import org.eclipse.jetty.fcgi.client.transport.HttpClientTransportOverFCGI; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpCookieStore; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.client.transport.ClientConnectionFactoryOverHTTP3; +import org.eclipse.jetty.http3.client.transport.HttpClientTransportOverHTTP3; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.Transport; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.MemoryConnector; +import org.eclipse.jetty.server.MemoryTransport; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import static java.lang.System.Logger.Level.ERROR; +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class HTTPClientDocs +{ + public void start() throws Exception + { + // tag::start[] + // Instantiate HttpClient. + HttpClient httpClient = new HttpClient(); + + // Configure HttpClient, for example: + httpClient.setFollowRedirects(false); + + // Start HttpClient. + httpClient.start(); + // end::start[] + } + + public void stop() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + // tag::stop[] + // Stop HttpClient. + httpClient.stop(); + // end::stop[] + } + + public void stopFromOtherThread() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + // tag::stopFromOtherThread[] + // Stop HttpClient from a new thread. + // Use LifeCycle.stop(...) to rethrow checked exceptions as unchecked. + new Thread(() -> LifeCycle.stop(httpClient)).start(); + // end::stopFromOtherThread[] + } + + public void tlsExplicit() throws Exception + { + // tag::tlsExplicit[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(sslContextFactory); + + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)); + httpClient.start(); + // end::tlsExplicit[] + } + + public void tlsNoValidation() + { + // tag::tlsNoValidation[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + // Disable the validation of the server host name at the TLS level. + sslContextFactory.setEndpointIdentificationAlgorithm(null); + // end::tlsNoValidation[] + } + + public void tlsAppValidation() + { + // tag::tlsAppValidation[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + // Only allow to connect to subdomains of domain.com. + sslContextFactory.setHostnameVerifier((hostName, session) -> hostName.endsWith(".domain.com")); + // end::tlsAppValidation[] + } + + public void sslHandshakeListener() + { + // tag::sslHandshakeListener[] + // Create a SslHandshakeListener. + SslHandshakeListener listener = new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) throws SSLException + { + SSLEngine sslEngine = event.getSSLEngine(); + System.getLogger("tls").log(INFO, "TLS handshake successful to %s", sslEngine.getPeerHost()); + } + + @Override + public void handshakeFailed(Event event, Throwable failure) + { + SSLEngine sslEngine = event.getSSLEngine(); + System.getLogger("tls").log(ERROR, "TLS handshake failure to %s", sslEngine.getPeerHost(), failure); + } + }; + + HttpClient httpClient = new HttpClient(); + + // Add the SslHandshakeListener as bean to HttpClient. + // The listener will be notified of TLS handshakes success and failure. + httpClient.addBean(listener); + // end::sslHandshakeListener[] + } + + public void simpleBlockingGet() throws Exception + { + // tag::simpleBlockingGet[] + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // Perform a simple GET and wait for the response. + ContentResponse response = httpClient.GET("http://domain.com/path?query"); + // end::simpleBlockingGet[] + } + + public void headFluent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::headFluent[] + ContentResponse response = httpClient.newRequest("http://domain.com/path?query") + .method(HttpMethod.HEAD) + .agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0") + .send(); + // end::headFluent[] + } + + public void headNonFluent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::headNonFluent[] + Request request = httpClient.newRequest("http://domain.com/path?query"); + request.method(HttpMethod.HEAD); + request.agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0"); + ContentResponse response = request.send(); + // end::headNonFluent[] + } + + public void postFluent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::postFluent[] + ContentResponse response = httpClient.POST("http://domain.com/entity/1") + .param("p", "value") + .send(); + // end::postFluent[] + } + + public void fileFluent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::fileFluent[] + ContentResponse response = httpClient.POST("http://domain.com/upload") + .file(Paths.get("file_to_upload.txt"), "text/plain") + .send(); + // end::fileFluent[] + } + + public void totalTimeout() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::totalTimeout[] + ContentResponse response = httpClient.newRequest("http://domain.com/path?query") + .timeout(5, TimeUnit.SECONDS) + .send(); + // end::totalTimeout[] + } + + public void simpleNonBlocking() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::simpleNonBlocking[] + httpClient.newRequest("http://domain.com/path") + .send(result -> + { + // Your logic here + }); + // end::simpleNonBlocking[] + } + + public void nonBlockingTotalTimeout() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::nonBlockingTotalTimeout[] + httpClient.newRequest("http://domain.com/path") + .timeout(3, TimeUnit.SECONDS) + .send(result -> + { + /* Your logic here */ + }); + // end::nonBlockingTotalTimeout[] + } + + // @checkstyle-disable-check : LeftCurly + public void listeners() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::listeners[] + httpClient.newRequest("http://domain.com/path") + // Add request hooks. + .onRequestQueued(request -> { /* ... */ }) + .onRequestBegin(request -> { /* ... */ }) + .onRequestHeaders(request -> { /* ... */ }) + .onRequestCommit(request -> { /* ... */ }) + .onRequestContent((request, content) -> { /* ... */ }) + .onRequestFailure((request, failure) -> { /* ... */ }) + .onRequestSuccess(request -> { /* ... */ }) + // Add response hooks. + .onResponseBegin(response -> { /* ... */ }) + .onResponseHeader((response, field) -> true) + .onResponseHeaders(response -> { /* ... */ }) + .onResponseContentAsync((response, chunk, demander) -> demander.run()) + .onResponseFailure((response, failure) -> { /* ... */ }) + .onResponseSuccess(response -> { /* ... */ }) + // Result hook. + .send(result -> { /* ... */ }); + // end::listeners[] + } + + public void pathRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::pathRequestContent[] + ContentResponse response = httpClient.POST("http://domain.com/upload") + .body(new PathRequestContent("text/plain", Paths.get("file_to_upload.txt"))) + .send(); + // end::pathRequestContent[] + } + + public void inputStreamRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::inputStreamRequestContent[] + ContentResponse response = httpClient.POST("http://domain.com/upload") + .body(new InputStreamRequestContent("text/plain", new FileInputStream("file_to_upload.txt"))) + .send(); + // end::inputStreamRequestContent[] + } + + public void bytesStringRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + byte[] bytes = new byte[1024]; + String string = new String(bytes); + // tag::bytesStringRequestContent[] + ContentResponse bytesResponse = httpClient.POST("http://domain.com/upload") + .body(new BytesRequestContent("text/plain", bytes)) + .send(); + + ContentResponse stringResponse = httpClient.POST("http://domain.com/upload") + .body(new StringRequestContent("text/plain", string)) + .send(); + // end::bytesStringRequestContent[] + } + + public void asyncRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::asyncRequestContent[] + AsyncRequestContent content = new AsyncRequestContent(); + httpClient.POST("http://domain.com/upload") + .body(content) + .send(result -> + { + // Your logic here + }); + + // Content not available yet here. + + // An event happens in some other class, in some other thread. + class ContentPublisher + { + void publish(byte[] bytes, boolean lastContent) + { + // Wrap the bytes into a new ByteBuffer. + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + // Write the content. + content.write(buffer, Callback.NOOP); + + // Close AsyncRequestContent when all the content is arrived. + if (lastContent) + content.close(); + } + } + // end::asyncRequestContent[] + } + + public void outputStreamRequestContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::outputStreamRequestContent[] + OutputStreamRequestContent content = new OutputStreamRequestContent(); + + // Use try-with-resources to close the OutputStream when all content is written. + try (OutputStream output = content.getOutputStream()) + { + httpClient.POST("http://localhost:8080/") + .body(content) + .send(result -> + { + // Your logic here + }); + + // Content not available yet here. + + // Content is now available. + byte[] bytes = new byte[]{'h', 'e', 'l', 'l', 'o'}; + output.write(bytes); + } + // End of try-with-resource, output.close() called automatically to signal end of content. + // end::outputStreamRequestContent[] + } + + public void futureResponseListener() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::completableResponseListener[] + Request request = httpClient.newRequest("http://domain.com/path"); + + // Limit response content buffer to 512 KiB. + CompletableFuture completable = new CompletableResponseListener(request, 512 * 1024) + .send(); + + // You can attach actions to the CompletableFuture, + // to be performed when the request+response completes. + + // Wait at most 5 seconds for request+response to complete. + ContentResponse response = completable.get(5, TimeUnit.SECONDS); + // end::completableResponseListener[] + } + + public void bufferingResponseListener() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::bufferingResponseListener[] + httpClient.newRequest("http://domain.com/path") + // Buffer response content up to 8 MiB + .send(new BufferingResponseListener(8 * 1024 * 1024) + { + @Override + public void onComplete(Result result) + { + if (!result.isFailed()) + { + byte[] responseContent = getContent(); + // Your logic here + } + } + }); + // end::bufferingResponseListener[] + } + + public void inputStreamResponseListener() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::inputStreamResponseListener[] + InputStreamResponseListener listener = new InputStreamResponseListener(); + httpClient.newRequest("http://domain.com/path") + .send(listener); + + // Wait for the response headers to arrive. + Response response = listener.get(5, TimeUnit.SECONDS); + + // Look at the response before streaming the content. + if (response.getStatus() == HttpStatus.OK_200) + { + // Use try-with-resources to close input stream. + try (InputStream responseContent = listener.getInputStream()) + { + // Your logic here + } + } + else + { + response.abort(new IOException("Unexpected HTTP response")); + } + // end::inputStreamResponseListener[] + } + + public void forwardContent() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + String host1 = "localhost"; + String host2 = "localhost"; + int port1 = 8080; + int port2 = 8080; + // tag::forwardContent[] + // Prepare a request to server1, the source. + Request request1 = httpClient.newRequest(host1, port1) + .path("/source"); + + // Prepare a request to server2, the sink. + AsyncRequestContent content2 = new AsyncRequestContent(); + Request request2 = httpClient.newRequest(host2, port2) + .path("/sink") + .body(content2); + + request1.onResponseContentSource(new Response.ContentSourceListener() + { + @Override + public void onContentSource(Response response, Content.Source contentSource) + { + // Only execute this method the very first time + // to initialize the request to server2. + + request2.onRequestCommit(request -> + { + // Only when the request to server2 has been sent, + // then demand response content from server1. + contentSource.demand(() -> forwardContent(response, contentSource)); + }); + + // Send the request to server2. + request2.send(result -> System.getLogger("forwarder").log(INFO, "Forwarding to server2 complete")); + } + + private void forwardContent(Response response, Content.Source contentSource) + { + // Read one chunk of content. + Content.Chunk chunk = contentSource.read(); + if (chunk == null) + { + // The read chunk is null, demand to be called back + // when the next one is ready to be read. + contentSource.demand(() -> forwardContent(response, contentSource)); + // Once a demand is in progress, the content source must not be read + // nor demanded again until the demand callback is invoked. + return; + } + // Check if the chunk is last and empty, in which case the + // read/demand loop is done. Demanding again when the terminal + // chunk has been read will invoke the demand callback with + // the same terminal chunk, so this check must be present to + // avoid infinitely demanding and reading the terminal chunk. + if (chunk.isLast() && !chunk.hasRemaining()) + { + chunk.release(); + return; + } + + // When a response chunk is received from server1, forward it to server2. + content2.write(chunk.getByteBuffer(), Callback.from(() -> + { + // When the request chunk is successfully sent to server2, + // release the chunk to recycle the buffer. + chunk.release(); + // Then demand more response content from server1. + contentSource.demand(() -> forwardContent(response, contentSource)); + }, x -> + { + chunk.release(); + response.abort(x); + })); + } + }); + + // When the response content from server1 is complete, + // complete also the request content to server2. + request1.onResponseSuccess(response -> content2.close()); + + // Send the request to server1. + request1.send(result -> System.getLogger("forwarder").log(INFO, "Sourcing from server1 complete")); + // end::forwardContent[] + } + + public void getCookies() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::getCookies[] + HttpCookieStore cookieStore = httpClient.getHttpCookieStore(); + List cookies = cookieStore.match(URI.create("http://domain.com/path")); + // end::getCookies[] + } + + public void setCookie() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::setCookie[] + HttpCookieStore cookieStore = httpClient.getHttpCookieStore(); + HttpCookie cookie = HttpCookie.build("foo", "bar") + .domain("domain.com") + .path("/") + .maxAge(TimeUnit.DAYS.toSeconds(1)) + .build(); + cookieStore.add(URI.create("http://domain.com"), cookie); + // end::setCookie[] + } + + public void requestCookie() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::requestCookie[] + ContentResponse response = httpClient.newRequest("http://domain.com/path") + .cookie(HttpCookie.from("foo", "bar")) + .send(); + // end::requestCookie[] + } + + public void removeCookie() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::removeCookie[] + HttpCookieStore cookieStore = httpClient.getHttpCookieStore(); + URI uri = URI.create("http://domain.com"); + List cookies = cookieStore.match(uri); + for (HttpCookie cookie : cookies) + { + cookieStore.remove(uri, cookie); + } + // end::removeCookie[] + } + + public void emptyCookieStore() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::emptyCookieStore[] + httpClient.setHttpCookieStore(new HttpCookieStore.Empty()); + // end::emptyCookieStore[] + } + + public void filteringCookieStore() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::filteringCookieStore[] + class GoogleOnlyCookieStore extends HttpCookieStore.Default + { + @Override + public boolean add(URI uri, HttpCookie cookie) + { + if (uri.getHost().endsWith("google.com")) + return super.add(uri, cookie); + return false; + } + } + + httpClient.setHttpCookieStore(new GoogleOnlyCookieStore()); + // end::filteringCookieStore[] + } + + public void addAuthentication() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::addAuthentication[] + // Add authentication credentials. + AuthenticationStore auth = httpClient.getAuthenticationStore(); + + URI uri1 = new URI("http://mydomain.com/secure"); + auth.addAuthentication(new BasicAuthentication(uri1, "MyRealm", "userName1", "password1")); + + URI uri2 = new URI("http://otherdomain.com/admin"); + auth.addAuthentication(new BasicAuthentication(uri1, "AdminRealm", "admin", "password")); + // end::addAuthentication[] + } + + public void clearResults() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::clearResults[] + httpClient.getAuthenticationStore().clearAuthenticationResults(); + // end::clearResults[] + } + + public void preemptedResult() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::preemptedResult[] + AuthenticationStore auth = httpClient.getAuthenticationStore(); + URI uri = URI.create("http://domain.com/secure"); + auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, "username", "password")); + // end::preemptedResult[] + } + + public void requestPreemptedResult() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::requestPreemptedResult[] + URI uri = URI.create("http://domain.com/secure"); + Authentication.Result authn = new BasicAuthentication.BasicResult(uri, "username", "password"); + Request request = httpClient.newRequest(uri); + authn.apply(request); + request.send(); + // end::requestPreemptedResult[] + } + + public void proxy() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::proxy[] + HttpProxy proxy = new HttpProxy("proxyHost", 8888); + + // Do not proxy requests for localhost:8080. + proxy.getExcludedAddresses().add("localhost:8080"); + + // Add the new proxy to the list of proxies already registered. + ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration(); + proxyConfig.addProxy(proxy); + + ContentResponse response = httpClient.GET("http://domain.com/path"); + // end::proxy[] + } + + public void proxySocks5() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::proxySocks5[] + Socks5Proxy proxy = new Socks5Proxy("proxyHost", 8888); + String socks5User = "jetty"; + String socks5Pass = "secret"; + var socks5AuthenticationFactory = new Socks5.UsernamePasswordAuthenticationFactory(socks5User, socks5Pass); + // Add the authentication method to the proxy. + proxy.putAuthenticationFactory(socks5AuthenticationFactory); + + // Do not proxy requests for localhost:8080. + proxy.getExcludedAddresses().add("localhost:8080"); + + // Add the new proxy to the list of proxies already registered. + ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration(); + proxyConfig.addProxy(proxy); + + ContentResponse response = httpClient.GET("http://domain.com/path"); + // end::proxySocks5[] + } + + public void proxyAuthentication() throws Exception + { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // tag::proxyAuthentication[] + AuthenticationStore auth = httpClient.getAuthenticationStore(); + + // Proxy credentials. + URI proxyURI = new URI("http://proxy.net:8080"); + auth.addAuthentication(new BasicAuthentication(proxyURI, "ProxyRealm", "proxyUser", "proxyPass")); + + // Server credentials. + URI serverURI = new URI("http://domain.com/secure"); + auth.addAuthentication(new DigestAuthentication(serverURI, "ServerRealm", "serverUser", "serverPass")); + + // Proxy configuration. + ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration(); + HttpProxy proxy = new HttpProxy("proxy.net", 8080); + proxyConfig.addProxy(proxy); + + ContentResponse response = httpClient.newRequest(serverURI).send(); + // end::proxyAuthentication[] + } + + public void defaultTransport() throws Exception + { + // tag::defaultTransport[] + // No transport specified, using default. + HttpClient httpClient = new HttpClient(); + httpClient.start(); + // end::defaultTransport[] + } + + public void http11Transport() throws Exception + { + // tag::http11Transport[] + // Configure HTTP/1.1 transport. + HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP(); + transport.setHeaderCacheSize(16384); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::http11Transport[] + } + + public void http2Transport() throws Exception + { + // tag::http2Transport[] + // The HTTP2Client powers the HTTP/2 transport. + HTTP2Client http2Client = new HTTP2Client(); + http2Client.setInitialSessionRecvWindow(64 * 1024 * 1024); + + // Create and configure the HTTP/2 transport. + HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(http2Client); + transport.setUseALPN(true); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::http2Transport[] + } + + public void http3Transport() throws Exception + { + // tag::http3Transport[] + // HTTP/3 requires secure communication. + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + // The HTTP3Client powers the HTTP/3 transport. + ClientQuicConfiguration clientQuicConfig = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(clientQuicConfig); + http3Client.getQuicConfiguration().setSessionRecvWindow(64 * 1024 * 1024); + + // Create and configure the HTTP/3 transport. + HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(http3Client); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::http3Transport[] + } + + public void fcgiTransport() throws Exception + { + // tag::fcgiTransport[] + String scriptRoot = "/var/www/wordpress"; + HttpClientTransportOverFCGI transport = new HttpClientTransportOverFCGI(scriptRoot); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::fcgiTransport[] + } + + public void dynamicDefault() throws Exception + { + // tag::dynamicDefault[] + // Dynamic transport speaks HTTP/1.1 by default. + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::dynamicDefault[] + } + + public void dynamicOneProtocol() + { + // tag::dynamicOneProtocol[] + ClientConnector connector = new ClientConnector(); + + // Equivalent to HttpClientTransportOverHTTP. + HttpClientTransportDynamic http11Transport = new HttpClientTransportDynamic(connector, HttpClientConnectionFactory.HTTP11); + + // Equivalent to HttpClientTransportOverHTTP2. + HTTP2Client http2Client = new HTTP2Client(connector); + HttpClientTransportDynamic http2Transport = new HttpClientTransportDynamic(connector, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client)); + // end::dynamicOneProtocol[] + } + + public void dynamicH1H2H3() throws Exception + { + // tag::dynamicH1H2H3[] + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(connector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, connector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + + // The order of the protocols indicates the client's preference. + // The first is the most preferred, the last is the least preferred, but + // the protocol version to use can be explicitly specified in the request. + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http1, http2, http3); + + HttpClient client = new HttpClient(transport); + client.start(); + // end::dynamicH1H2H3[] + } + + public void dynamicExplicitVersion() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(connector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, connector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + // tag::dynamicExplicitVersion[] + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http1, http2, http3); + HttpClient client = new HttpClient(transport); + client.start(); + + // The server supports HTTP/1.1, HTTP/2 and HTTP/3. + + ContentResponse http1Response = client.newRequest("https://host/") + // Specify the version explicitly. + .version(HttpVersion.HTTP_1_1) + .send(); + + ContentResponse http2Response = client.newRequest("https://host/") + // Specify the version explicitly. + .version(HttpVersion.HTTP_2) + .send(); + + ContentResponse http3Response = client.newRequest("https://host/") + // Specify the version explicitly. + .version(HttpVersion.HTTP_3) + .send(); + + // Make a clear-text upgrade request from HTTP/1.1 to HTTP/2. + // The request will start as HTTP/1.1, but the response will be HTTP/2. + ContentResponse upgradedResponse = client.newRequest("https://host/") + .headers(headers -> headers + .put(HttpHeader.UPGRADE, "h2c") + .put(HttpHeader.HTTP2_SETTINGS, "") + .put(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings")) + .send(); + // end::dynamicExplicitVersion[] + } + + public void dynamicPreferH3() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(connector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, connector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + // tag::dynamicPreferH3[] + // Client prefers HTTP/3. + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http3, http2, http1); + HttpClient client = new HttpClient(transport); + client.start(); + + // No explicit HTTP version specified. + // Either HTTP/3 succeeds, or communication failure. + ContentResponse httpResponse = client.newRequest("https://host/") + .send(); + // end::dynamicPreferH3[] + } + + public void dynamicPreferH2() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(connector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, connector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + // tag::dynamicPreferH2[] + // Client prefers HTTP/2. + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(connector, http2, http1, http3); + HttpClient client = new HttpClient(transport); + client.start(); + + // No explicit HTTP version specified. + // Either HTTP/1.1 or HTTP/2 will be negotiated via ALPN. + // HTTP/3 only possible by specifying the version explicitly. + ContentResponse httpResponse = client.newRequest("https://host/") + .send(); + // end::dynamicPreferH2[] + } + + public void getConnectionPool() throws Exception + { + // tag::getConnectionPool[] + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + ConnectionPool connectionPool = httpClient.getDestinations().stream() + // Find the destination by filtering on the Origin. + .filter(destination -> destination.getOrigin().getAddress().getHost().equals("domain.com")) + .findAny() + // Get the ConnectionPool. + .map(Destination::getConnectionPool) + .orElse(null); + // end::getConnectionPool[] + } + + public void setConnectionPool() throws Exception + { + // tag::setConnectionPool[] + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // The max number of connections in the pool. + int maxConnectionsPerDestination = httpClient.getMaxConnectionsPerDestination(); + + // The max number of requests per connection (multiplexing). + // Start with 1, since this value is dynamically set to larger values if + // the transport supports multiplexing requests on the same connection. + int maxRequestsPerConnection = 1; + + HttpClientTransport transport = httpClient.getTransport(); + + // Set the ConnectionPool.Factory using a lambda. + transport.setConnectionPoolFactory(destination -> + new RoundRobinConnectionPool(destination, + maxConnectionsPerDestination, + maxRequestsPerConnection)); + // end::setConnectionPool[] + } + + public void unixDomain() throws Exception + { + // tag::unixDomain[] + // This is the path where the server "listens" on. + Path unixDomainPath = Path.of("/path/to/server.sock"); + + // Creates a ClientConnector. + ClientConnector clientConnector = new ClientConnector(); + + // You can use Unix-Domain for HTTP/1.1. + HttpClientTransportOverHTTP http1Transport = new HttpClientTransportOverHTTP(clientConnector); + + // You can use Unix-Domain also for HTTP/2. + HTTP2Client http2Client = new HTTP2Client(clientConnector); + HttpClientTransportOverHTTP2 http2Transport = new HttpClientTransportOverHTTP2(http2Client); + + // You can use Unix-Domain also for the dynamic transport. + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + HttpClientTransportDynamic dynamicTransport = new HttpClientTransportDynamic(clientConnector, http1, http2); + + // Choose the transport you prefer for HttpClient, for example the dynamic transport. + HttpClient httpClient = new HttpClient(dynamicTransport); + httpClient.start(); + + ContentResponse response = httpClient.newRequest("jetty.org", 80) + // Specify that the request must be sent over Unix-Domain. + .transport(new Transport.TCPUnix(unixDomainPath)) + .send(); + // end::unixDomain[] + } + + public void memory() throws Exception + { + // tag::memory[] + // The server-side MemoryConnector speaking HTTP/1.1. + Server server = new Server(); + MemoryConnector memoryConnector = new MemoryConnector(server, new HttpConnectionFactory()); + server.addConnector(memoryConnector); + // ... + + // The code above is the server-side. + // ---- + // The code below is the client-side. + + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + // Use the MemoryTransport to communicate with the server-side. + Transport transport = new MemoryTransport(memoryConnector); + + httpClient.newRequest("http://localhost/") + // Specify the Transport to use. + .transport(transport) + .send(); + // end::memory[] + } + + public void mixedTransports() throws Exception + { + Path unixDomainPath = Path.of("/path/to/server.sock"); + + Server server = new Server(); + MemoryConnector memoryConnector = new MemoryConnector(server, new HttpConnectionFactory()); + + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(sslContextFactory); + + ClientConnectionFactory.Info http1 = HttpClientConnectionFactory.HTTP11; + + HTTP2Client http2Client = new HTTP2Client(clientConnector); + ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client); + + ClientQuicConfiguration quicConfiguration = new ClientQuicConfiguration(sslContextFactory, null); + HTTP3Client http3Client = new HTTP3Client(quicConfiguration, clientConnector); + ClientConnectionFactoryOverHTTP3.HTTP3 http3 = new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client); + + // tag::mixedTransports[] + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, http2, http1, http3)); + httpClient.start(); + + // Make a TCP request to a 3rd party web application. + ContentResponse thirdPartyResponse = httpClient.newRequest("https://third-party.com/api") + // No need to specify the Transport, TCP will be used by default. + .send(); + + // Upload the third party response content to a validation process. + ContentResponse validatedResponse = httpClient.newRequest("http://localhost/validate") + // The validation process is available via Unix-Domain. + .transport(new Transport.TCPUnix(unixDomainPath)) + .method(HttpMethod.POST) + .body(new BytesRequestContent(thirdPartyResponse.getContent())) + .send(); + + // Process the validated response intra-process by sending + // it to another web application in the same Jetty server. + ContentResponse response = httpClient.newRequest("http://localhost/process") + // The processing is in-memory. + .transport(new MemoryTransport(memoryConnector)) + .method(HttpMethod.POST) + .body(new BytesRequestContent(validatedResponse.getContent())) + .send(); + // end::mixedTransports[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java new file mode 100644 index 000000000000..e06cd144a147 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java @@ -0,0 +1,416 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.client.http2; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.PushPromiseFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.frames.SettingsFrame; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.Callback; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class HTTP2ClientDocs +{ + public void start() throws Exception + { + // tag::start[] + // Instantiate HTTP2Client. + HTTP2Client http2Client = new HTTP2Client(); + + // Configure HTTP2Client, for example: + http2Client.setStreamIdleTimeout(15000); + + // Start HTTP2Client. + http2Client.start(); + // end::start[] + } + + public void stop() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + // tag::stop[] + // Stop HTTP2Client. + http2Client.stop(); + // end::stop[] + } + + public void clearTextConnect() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + // tag::clearTextConnect[] + // Address of the server's clear-text port. + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + + // Connect to the server, the CompletableFuture will be + // notified when the connection is succeeded (or failed). + CompletableFuture sessionCF = http2Client.connect(serverAddress, new Session.Listener() {}); + + // Block to obtain the Session. + // Alternatively you can use the CompletableFuture APIs to avoid blocking. + Session session = sessionCF.get(); + // end::clearTextConnect[] + } + + public void encryptedConnect() throws Exception + { + // tag::encryptedConnect[] + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + + ClientConnector connector = http2Client.getClientConnector(); + + // Address of the server's encrypted port. + SocketAddress serverAddress = new InetSocketAddress("localhost", 8443); + + // Connect to the server, the CompletableFuture will be + // notified when the connection is succeeded (or failed). + CompletableFuture sessionCF = http2Client.connect(connector.getSslContextFactory(), serverAddress, new Session.Listener() {}); + + // Block to obtain the Session. + // Alternatively you can use the CompletableFuture APIs to avoid blocking. + Session session = sessionCF.get(); + // end::encryptedConnect[] + } + + public void configure() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + + // tag::configure[] + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + http2Client.connect(serverAddress, new Session.Listener() + { + @Override + public Map onPreface(Session session) + { + Map configuration = new HashMap<>(); + + // Disable push from the server. + configuration.put(SettingsFrame.ENABLE_PUSH, 0); + + // Override HTTP2Client.initialStreamRecvWindow for this session. + configuration.put(SettingsFrame.INITIAL_WINDOW_SIZE, 1024 * 1024); + + return configuration; + } + }); + // end::configure[] + } + + public void newStream() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + // tag::newStream[] + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + CompletableFuture sessionCF = http2Client.connect(serverAddress, new Session.Listener() {}); + Session session = sessionCF.get(); + + // Configure the request headers. + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP2Client {version}"); + + // The request metadata with method, URI and headers. + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders); + + // The HTTP/2 HEADERS frame, with endStream=true + // to signal that this request has no content. + HeadersFrame headersFrame = new HeadersFrame(request, null, true); + + // Open a Stream by sending the HEADERS frame. + session.newStream(headersFrame, null); + // end::newStream[] + } + + public void newStreamWithData() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + // tag::newStreamWithData[] + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + CompletableFuture sessionCF = http2Client.connect(serverAddress, new Session.Listener() {}); + Session session = sessionCF.get(); + + // Configure the request headers. + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.CONTENT_TYPE, "application/json"); + + // The request metadata with method, URI and headers. + MetaData.Request request = new MetaData.Request("POST", HttpURI.from("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders); + + // The HTTP/2 HEADERS frame, with endStream=false to + // signal that there will be more frames in this stream. + HeadersFrame headersFrame = new HeadersFrame(request, null, false); + + // Open a Stream by sending the HEADERS frame. + CompletableFuture streamCF = session.newStream(headersFrame, null); + + // Block to obtain the Stream. + // Alternatively you can use the CompletableFuture APIs to avoid blocking. + Stream stream = streamCF.get(); + + // The request content, in two chunks. + String content1 = "{\"greet\": \"hello world\"}"; + ByteBuffer buffer1 = StandardCharsets.UTF_8.encode(content1); + String content2 = "{\"user\": \"jetty\"}"; + ByteBuffer buffer2 = StandardCharsets.UTF_8.encode(content2); + + // Send the first DATA frame on the stream, with endStream=false + // to signal that there are more frames in this stream. + CompletableFuture dataCF1 = stream.data(new DataFrame(stream.getId(), buffer1, false)); + + // Only when the first chunk has been sent we can send the second, + // with endStream=true to signal that there are no more frames. + dataCF1.thenCompose(s -> s.data(new DataFrame(s.getId(), buffer2, true))); + // end::newStreamWithData[] + } + + public void responseListener() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + CompletableFuture sessionCF = http2Client.connect(serverAddress, new Session.Listener() {}); + Session session = sessionCF.get(); + + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP2Client {version}"); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders); + HeadersFrame headersFrame = new HeadersFrame(request, null, true); + + // tag::responseListener[] + // Open a Stream by sending the HEADERS frame. + session.newStream(headersFrame, new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData metaData = frame.getMetaData(); + + // Is this HEADERS frame the response or the trailers? + if (metaData.isResponse()) + { + MetaData.Response response = (MetaData.Response)metaData; + System.getLogger("http2").log(INFO, "Received response {0}", response); + if (!frame.isEndStream()) + { + // Demand for DATA frames, so that onDataAvailable() + // below will be called when they are available. + stream.demand(); + } + } + else + { + System.getLogger("http2").log(INFO, "Received trailers {0}", metaData.getHttpFields()); + } + } + + @Override + public void onDataAvailable(Stream stream) + { + // Read a Data object. + Stream.Data data = stream.readData(); + + if (data == null) + { + // Demand more DATA frames. + stream.demand(); + return; + } + + // Get the content buffer. + ByteBuffer buffer = data.frame().getByteBuffer(); + + // Consume the buffer, here - as an example - just log it. + System.getLogger("http2").log(INFO, "Consuming buffer {0}", buffer); + + // Tell the implementation that the buffer has been consumed. + data.release(); + + if (!data.frame().isEndStream()) + { + // Demand more DATA frames when they are available. + stream.demand(); + } + } + }); + // end::responseListener[] + } + + public void reset() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + CompletableFuture sessionCF = http2Client.connect(serverAddress, new Session.Listener() {}); + Session session = sessionCF.get(); + + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP2Client {version}"); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders); + HeadersFrame headersFrame = new HeadersFrame(request, null, true); + + // tag::reset[] + // Open a Stream by sending the HEADERS frame. + CompletableFuture streamCF = session.newStream(headersFrame, new Stream.Listener() + { + @Override + public void onReset(Stream stream, ResetFrame frame, Callback callback) + { + // The server reset this stream. + + // Succeed the callback to signal that the reset event has been handled. + callback.succeeded(); + } + }); + Stream stream = streamCF.get(); + + // Reset this stream (for example, the user closed the application). + stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); + // end::reset[] + } + + public void push() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + CompletableFuture sessionCF = http2Client.connect(serverAddress, new Session.Listener() {}); + Session session = sessionCF.get(); + + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP2Client {version}"); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders); + HeadersFrame headersFrame = new HeadersFrame(request, null, true); + + // tag::push[] + // Open a Stream by sending the HEADERS frame. + CompletableFuture streamCF = session.newStream(headersFrame, new Stream.Listener() + { + @Override + public Stream.Listener onPush(Stream pushedStream, PushPromiseFrame frame) + { + // The "request" the client would make for the pushed resource. + MetaData.Request pushedRequest = frame.getMetaData(); + // The pushed "request" URI. + HttpURI pushedURI = pushedRequest.getHttpURI(); + // The pushed "request" headers. + HttpFields pushedRequestHeaders = pushedRequest.getHttpFields(); + + // If needed, retrieve the primary stream that triggered the push. + Stream primaryStream = pushedStream.getSession().getStream(frame.getStreamId()); + + // Return a Stream.Listener to listen for the pushed "response" events. + return new Stream.Listener() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + // Handle the pushed stream "response". + + MetaData metaData = frame.getMetaData(); + if (metaData.isResponse()) + { + // The pushed "response" headers. + HttpFields pushedResponseHeaders = metaData.getHttpFields(); + + // Typically a pushed stream has data, so demand for data. + stream.demand(); + } + } + + @Override + public void onDataAvailable(Stream stream) + { + // Handle the pushed stream "response" content. + + Stream.Data data = stream.readData(); + + if (data == null) + { + stream.demand(); + return; + } + + // The pushed stream "response" content bytes. + ByteBuffer buffer = data.frame().getByteBuffer(); + // Consume the buffer and release the Data object. + data.release(); + + if (!data.frame().isEndStream()) + { + // Demand more DATA frames when they are available. + stream.demand(); + } + } + }; + } + }); + // end::push[] + } + + public void pushReset() throws Exception + { + HTTP2Client http2Client = new HTTP2Client(); + http2Client.start(); + SocketAddress serverAddress = new InetSocketAddress("localhost", 8080); + CompletableFuture sessionCF = http2Client.connect(serverAddress, new Session.Listener() {}); + Session session = sessionCF.get(); + + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP2Client {version}"); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders); + HeadersFrame headersFrame = new HeadersFrame(request, null, true); + + // tag::pushReset[] + // Open a Stream by sending the HEADERS frame. + CompletableFuture streamCF = session.newStream(headersFrame, new Stream.Listener() + { + @Override + public Stream.Listener onPush(Stream pushedStream, PushPromiseFrame frame) + { + // Reset the pushed stream to tell the server you are not interested. + pushedStream.reset(new ResetFrame(pushedStream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); + + // Not interested in listening to pushed response events. + return null; + } + }); + // end::pushReset[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java new file mode 100644 index 000000000000..a18a2b14903c --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java @@ -0,0 +1,271 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.client.http3; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http3.HTTP3ErrorCode; +import org.eclipse.jetty.http3.api.Session; +import org.eclipse.jetty.http3.api.Stream; +import org.eclipse.jetty.http3.client.HTTP3Client; +import org.eclipse.jetty.http3.frames.DataFrame; +import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.quic.client.ClientQuicConfiguration; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class HTTP3ClientDocs +{ + public void start() throws Exception + { + // tag::start[] + // Instantiate HTTP3Client. + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); + + // Configure HTTP3Client, for example: + http3Client.getHTTP3Configuration().setStreamIdleTimeout(15000); + + // Start HTTP3Client. + http3Client.start(); + // end::start[] + } + + public void stop() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); + http3Client.start(); + // tag::stop[] + // Stop HTTP3Client. + http3Client.stop(); + // end::stop[] + } + + public void connect() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); + http3Client.start(); + // tag::connect[] + // Address of the server's port. + SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); + + // Connect to the server, the CompletableFuture will be + // notified when the connection is succeeded (or failed). + CompletableFuture sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {}); + + // Block to obtain the Session. + // Alternatively you can use the CompletableFuture APIs to avoid blocking. + Session session = sessionCF.get(); + // end::connect[] + } + + public void configure() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); + http3Client.start(); + + // tag::configure[] + SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); + http3Client.connect(serverAddress, new Session.Client.Listener() + { + @Override + public Map onPreface(Session session) + { + Map configuration = new HashMap<>(); + + // Add here configuration settings. + + return configuration; + } + }); + // end::configure[] + } + + public void newStream() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); + http3Client.start(); + // tag::newStream[] + SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); + CompletableFuture sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {}); + Session.Client session = sessionCF.get(); + + // Configure the request headers. + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP3Client {version}"); + + // The request metadata with method, URI and headers. + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8444/path"), HttpVersion.HTTP_3, requestHeaders); + + // The HTTP/3 HEADERS frame, with endStream=true + // to signal that this request has no content. + HeadersFrame headersFrame = new HeadersFrame(request, true); + + // Open a Stream by sending the HEADERS frame. + session.newRequest(headersFrame, new Stream.Client.Listener() {}); + // end::newStream[] + } + + public void newStreamWithData() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); + http3Client.start(); + // tag::newStreamWithData[] + SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); + CompletableFuture sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {}); + Session.Client session = sessionCF.get(); + + // Configure the request headers. + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.CONTENT_TYPE, "application/json"); + + // The request metadata with method, URI and headers. + MetaData.Request request = new MetaData.Request("POST", HttpURI.from("http://localhost:8444/path"), HttpVersion.HTTP_3, requestHeaders); + + // The HTTP/3 HEADERS frame, with endStream=false to + // signal that there will be more frames in this stream. + HeadersFrame headersFrame = new HeadersFrame(request, false); + + // Open a Stream by sending the HEADERS frame. + CompletableFuture streamCF = session.newRequest(headersFrame, new Stream.Client.Listener() {}); + + // Block to obtain the Stream. + // Alternatively you can use the CompletableFuture APIs to avoid blocking. + Stream stream = streamCF.get(); + + // The request content, in two chunks. + String content1 = "{\"greet\": \"hello world\"}"; + ByteBuffer buffer1 = StandardCharsets.UTF_8.encode(content1); + String content2 = "{\"user\": \"jetty\"}"; + ByteBuffer buffer2 = StandardCharsets.UTF_8.encode(content2); + + // Send the first DATA frame on the stream, with endStream=false + // to signal that there are more frames in this stream. + CompletableFuture dataCF1 = stream.data(new DataFrame(buffer1, false)); + + // Only when the first chunk has been sent we can send the second, + // with endStream=true to signal that there are no more frames. + dataCF1.thenCompose(s -> s.data(new DataFrame(buffer2, true))); + // end::newStreamWithData[] + } + + public void responseListener() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); + http3Client.start(); + SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); + CompletableFuture sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {}); + Session.Client session = sessionCF.get(); + + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP3Client {version}"); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8444/path"), HttpVersion.HTTP_3, requestHeaders); + HeadersFrame headersFrame = new HeadersFrame(request, true); + + // tag::responseListener[] + // Open a Stream by sending the HEADERS frame. + session.newRequest(headersFrame, new Stream.Client.Listener() + { + @Override + public void onResponse(Stream.Client stream, HeadersFrame frame) + { + MetaData metaData = frame.getMetaData(); + MetaData.Response response = (MetaData.Response)metaData; + System.getLogger("http3").log(INFO, "Received response {0}", response); + } + + @Override + public void onDataAvailable(Stream.Client stream) + { + // Read a chunk of the content. + Stream.Data data = stream.readData(); + if (data == null) + { + // No data available now, demand to be called back. + stream.demand(); + } + else + { + // Process the content. + process(data.getByteBuffer()); + + // Notify the implementation that the content has been consumed. + data.release(); + + if (!data.isLast()) + { + // Demand to be called back. + stream.demand(); + } + } + } + }); + // end::responseListener[] + } + + private void process(ByteBuffer byteBuffer) + { + } + + public void reset() throws Exception + { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HTTP3Client http3Client = new HTTP3Client(new ClientQuicConfiguration(sslContextFactory, null)); + http3Client.start(); + SocketAddress serverAddress = new InetSocketAddress("localhost", 8444); + CompletableFuture sessionCF = http3Client.connect(serverAddress, new Session.Client.Listener() {}); + Session.Client session = sessionCF.get(); + + HttpFields requestHeaders = HttpFields.build() + .put(HttpHeader.USER_AGENT, "Jetty HTTP3Client {version}"); + MetaData.Request request = new MetaData.Request("GET", HttpURI.from("http://localhost:8080/path"), HttpVersion.HTTP_2, requestHeaders); + HeadersFrame headersFrame = new HeadersFrame(request, true); + + // tag::reset[] + // Open a Stream by sending the HEADERS frame. + CompletableFuture streamCF = session.newRequest(headersFrame, new Stream.Client.Listener() + { + @Override + public void onFailure(Stream.Client stream, long error, Throwable failure) + { + // The server reset this stream. + } + }); + Stream stream = streamCF.get(); + + // Reset this stream (for example, the user closed the application). + stream.reset(HTTP3ErrorCode.REQUEST_CANCELLED_ERROR.code(), new ClosedChannelException()); + // end::reset[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java new file mode 100644 index 000000000000..f1750d753732 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java @@ -0,0 +1,195 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.client.websocket; + +import java.net.HttpCookie; +import java.net.URI; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpProxy; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.JettyUpgradeListener; +import org.eclipse.jetty.websocket.client.WebSocketClient; + +@SuppressWarnings("unused") +public class WebSocketClientDocs +{ + public void start() throws Exception + { + // tag::start[] + // Instantiate WebSocketClient. + WebSocketClient webSocketClient = new WebSocketClient(); + + // Configure WebSocketClient, for example: + webSocketClient.setMaxTextMessageSize(8 * 1024); + + // Start WebSocketClient. + webSocketClient.start(); + // end::start[] + } + + public void startWithHttpClient() throws Exception + { + // tag::startWithHttpClient[] + // Instantiate and configure HttpClient. + HttpClient httpClient = new HttpClient(); + // For example, configure a proxy. + httpClient.getProxyConfiguration().addProxy(new HttpProxy("localhost", 8888)); + + // Instantiate WebSocketClient, passing HttpClient to the constructor. + WebSocketClient webSocketClient = new WebSocketClient(httpClient); + // Configure WebSocketClient, for example: + webSocketClient.setMaxTextMessageSize(8 * 1024); + + // Start WebSocketClient; this implicitly starts also HttpClient. + webSocketClient.start(); + // end::startWithHttpClient[] + } + + public void stop() throws Exception + { + WebSocketClient webSocketClient = new WebSocketClient(); + webSocketClient.start(); + // tag::stop[] + // Stop WebSocketClient. + // Use LifeCycle.stop(...) to rethrow checked exceptions as unchecked. + new Thread(() -> LifeCycle.stop(webSocketClient)).start(); + // end::stop[] + } + + public void connectHTTP11() throws Exception + { + // tag::connectHTTP11[] + // Use a standard, HTTP/1.1, HttpClient. + HttpClient httpClient = new HttpClient(); + + // Create and start WebSocketClient. + WebSocketClient webSocketClient = new WebSocketClient(httpClient); + webSocketClient.start(); + + // The client-side WebSocket EndPoint that + // receives WebSocket messages from the server. + ClientEndPoint clientEndPoint = new ClientEndPoint(); + // The server URI to connect to. + URI serverURI = URI.create("ws://domain.com/path"); + + // Connect the client EndPoint to the server. + CompletableFuture clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI); + // end::connectHTTP11[] + } + + public void connectHTTP2() throws Exception + { + // tag::connectHTTP2[] + // Use the HTTP/2 transport for HttpClient. + HTTP2Client http2Client = new HTTP2Client(); + HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client)); + + // Create and start WebSocketClient. + WebSocketClient webSocketClient = new WebSocketClient(httpClient); + webSocketClient.start(); + + // The client-side WebSocket EndPoint that + // receives WebSocket messages from the server. + ClientEndPoint clientEndPoint = new ClientEndPoint(); + // The server URI to connect to. + URI serverURI = URI.create("wss://domain.com/path"); + + // Connect the client EndPoint to the server. + CompletableFuture clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI); + // end::connectHTTP2[] + } + + public void connectHTTP2Dynamic() throws Exception + { + // tag::connectHTTP2Dynamic[] + // Use the dynamic HTTP/2 transport for HttpClient. + ClientConnector clientConnector = new ClientConnector(); + HTTP2Client http2Client = new HTTP2Client(clientConnector); + HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client))); + + // Create and start WebSocketClient. + WebSocketClient webSocketClient = new WebSocketClient(httpClient); + webSocketClient.start(); + + ClientEndPoint clientEndPoint = new ClientEndPoint(); + URI serverURI = URI.create("wss://domain.com/path"); + + // Connect the client EndPoint to the server. + CompletableFuture clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI); + // end::connectHTTP2Dynamic[] + } + + public void customHTTPRequest() throws Exception + { + WebSocketClient webSocketClient = new WebSocketClient(new HttpClient()); + webSocketClient.start(); + + // tag::customHTTPRequest[] + ClientEndPoint clientEndPoint = new ClientEndPoint(); + URI serverURI = URI.create("ws://domain.com/path"); + + // Create a custom HTTP request. + ClientUpgradeRequest customRequest = new ClientUpgradeRequest(); + // Specify a cookie. + customRequest.getCookies().add(new HttpCookie("name", "value")); + // Specify a custom header. + customRequest.setHeader("X-Token", "0123456789ABCDEF"); + // Specify a custom sub-protocol. + customRequest.setSubProtocols("chat"); + + // Connect the client EndPoint to the server with a custom HTTP request. + CompletableFuture clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI, customRequest); + // end::customHTTPRequest[] + } + + public void inspectHTTPResponse() throws Exception + { + WebSocketClient webSocketClient = new WebSocketClient(new HttpClient()); + webSocketClient.start(); + + // tag::inspectHTTPResponse[] + ClientEndPoint clientEndPoint = new ClientEndPoint(); + URI serverURI = URI.create("ws://domain.com/path"); + + // The listener to inspect the HTTP response. + JettyUpgradeListener listener = new JettyUpgradeListener() + { + @Override + public void onHandshakeResponse(Request request, Response response) + { + // Inspect the HTTP response here. + } + }; + + // Connect the client EndPoint to the server with a custom HTTP request. + CompletableFuture clientSessionPromise = webSocketClient.connect(clientEndPoint, serverURI, null, listener); + // end::inspectHTTPResponse[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ClientEndPoint + { + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java new file mode 100644 index 000000000000..3fd14d54da37 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java @@ -0,0 +1,647 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.migration; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.Trailers; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Context; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Session; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.CompletableTask; +import org.eclipse.jetty.util.Fields; + +import static java.nio.charset.StandardCharsets.UTF_8; + +@SuppressWarnings("unused") +public class ServletToHandlerDocs +{ + @SuppressWarnings("InnerClassMayBeStatic") + // tag::request[] + public class RequestAPIs extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Gets the request method. + // Replaces: + // - servletRequest.getMethod(); + String method = request.getMethod(); + + // Gets the request protocol name and version. + // Replaces: + // - servletRequest.getProtocol(); + String protocol = request.getConnectionMetaData().getProtocol(); + + // Gets the full request URI. + // Replaces: + // - servletRequest.getRequestURL(); + String fullRequestURI = request.getHttpURI().asString(); + + // Gets the request context. + // Replaces: + // - servletRequest.getServletContext() + Context context = request.getContext(); + + // Gets the context path. + // Replaces: + // - servletRequest.getContextPath() + String contextPath = context.getContextPath(); + + // Gets the request path. + // Replaces: + // - servletRequest.getRequestURI(); + String requestPath = request.getHttpURI().getPath(); + + // Gets the request path after the context path. + // Replaces: + // - servletRequest.getServletPath() + servletRequest.getPathInfo() + String pathInContext = Request.getPathInContext(request); + + // Gets the request query. + // Replaces: + // - servletRequest.getQueryString() + String queryString = request.getHttpURI().getQuery(); + + // Gets request parameters. + // Replaces: + // - servletRequest.getParameterNames(); + // - servletRequest.getParameter(name); + // - servletRequest.getParameterValues(name); + // - servletRequest.getParameterMap(); + Fields queryParameters = Request.extractQueryParameters(request, UTF_8); + Fields allParameters = Request.getParameters(request); + + // Gets cookies. + // Replaces: + // - servletRequest.getCookies(); + List cookies = Request.getCookies(request); + + // Gets request HTTP headers. + // Replaces: + // - servletRequest.getHeaderNames() + // - servletRequest.getHeader(name) + // - servletRequest.getHeaders(name) + // - servletRequest.getDateHeader(name) + // - servletRequest.getIntHeader(name) + HttpFields requestHeaders = request.getHeaders(); + + // Gets the request Content-Type. + // Replaces: + // - servletRequest.getContentType() + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + + // Gets the request Content-Length. + // Replaces: + // - servletRequest.getContentLength() + // - servletRequest.getContentLengthLong() + long contentLength = request.getLength(); + + // Gets the request locales. + // Replaces: + // - servletRequest.getLocale() + // - servletRequest.getLocales() + List locales = Request.getLocales(request); + + // Gets the request scheme. + // Replaces: + // - servletRequest.getScheme() + String scheme = request.getHttpURI().getScheme(); + + // Gets the server name. + // Replaces: + // - servletRequest.getServerName() + String serverName = Request.getServerName(request); + + // Gets the server port. + // Replaces: + // - servletRequest.getServerPort() + int serverPort = Request.getServerPort(request); + + // Gets the remote host/address. + // Replaces: + // - servletRequest.getRemoteAddr() + // - servletRequest.getRemoteHost() + String remoteAddress = Request.getRemoteAddr(request); + + // Gets the remote port. + // Replaces: + // - servletRequest.getRemotePort() + int remotePort = Request.getRemotePort(request); + + // Gets the local host/address. + // Replaces: + // - servletRequest.getLocalAddr() + // - servletRequest.getLocalHost() + String localAddress = Request.getLocalAddr(request); + + // Gets the local port. + // Replaces: + // - servletRequest.getLocalPort() + int localPort = Request.getLocalPort(request); + + // Gets the request attributes. + // Replaces: + // - servletRequest.getAttributeNames() + // - servletRequest.getAttribute(name) + // - servletRequest.setAttribute(name, value) + // - servletRequest.removeAttribute(name) + String name = "name"; + Object value = "value"; + Set names = request.getAttributeNameSet(); + Object attribute = request.getAttribute(name); + Object oldValue = request.setAttribute(name, value); + Object removedValue = request.removeAttribute(name); + request.clearAttributes(); + Map map = request.asAttributeMap(); + + // Gets the request trailers. + // Replaces: + // - servletRequest.getTrailerFields() + HttpFields trailers = request.getTrailers(); + + // Gets the HTTP session. + // Replaces: + // - servletRequest.getSession() + // - servletRequest.getSession(create) + boolean create = true; + Session session = request.getSession(create); + + callback.succeeded(); + return false; + } + } + // end::request[] + + @SuppressWarnings("InnerClassMayBeStatic") + public class RequestContentAPIsString extends Handler.Abstract + { + // tag::requestContent-string[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Non-blocking read the request content as a String. + // Use with caution as the request content may be large. + CompletableFuture completable = Content.Source.asStringAsync(request, UTF_8); + + completable.whenComplete((requestContent, failure) -> + { + if (failure == null) + { + // Process the request content here. + + // Implicitly respond with status code 200 and no content. + callback.succeeded(); + } + else + { + // Implicitly respond with status code 500. + callback.failed(failure); + } + }); + + return true; + } + // end::requestContent-string[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class RequestContentAPIsByteBuffer extends Handler.Abstract + { + // tag::requestContent-buffer[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Non-blocking read the request content as a ByteBuffer. + // Use with caution as the request content may be large. + CompletableFuture completable = Content.Source.asByteBufferAsync(request); + + completable.whenComplete((requestContent, failure) -> + { + if (failure == null) + { + // Process the request content here. + + // Implicitly respond with status code 200 and no content. + callback.succeeded(); + } + else + { + // Implicitly respond with status code 500. + callback.failed(failure); + } + }); + + return true; + } + // end::requestContent-buffer[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class RequestContentAPIsInputStream extends Handler.Abstract + { + // tag::requestContent-stream[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Read the request content as an InputStream. + // Note that InputStream.read() may block. + try (InputStream inputStream = Content.Source.asInputStream(request)) + { + while (true) + { + int read = inputStream.read(); + + // EOF was reached, stop reading. + if (read < 0) + break; + + // Process the read byte here. + } + } + + // Implicitly respond with status code 200 and no content. + callback.succeeded(); + return true; + } + // end::requestContent-stream[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class RequestContentAPIsSource extends Handler.Abstract + { + // tag::requestContent-source[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + CompletableTask reader = new CompletableTask<>() + { + @Override + public void run() + { + // Read in a loop. + while (true) + { + // Read a chunk of content. + Content.Chunk chunk = request.read(); + + // If there is no content, demand to be + // called back when more content is available. + if (chunk == null) + { + request.demand(this); + return; + } + + // If a failure is read, complete with a failure. + if (Content.Chunk.isFailure(chunk)) + { + Throwable failure = chunk.getFailure(); + completeExceptionally(failure); + return; + } + + if (chunk instanceof Trailers trailers) + { + // Possibly process the request trailers here. + // Trailers have an empty ByteBuffer and are a last chunk. + } + + // Process the request content chunk here. + // After the processing, the chunk MUST be released. + chunk.release(); + + // If the last chunk is read, complete normally. + if (chunk.isLast()) + { + complete(null); + return; + } + + // Not the last chunk of content, loop around to read more. + } + } + }; + + // Initiate the read of the request content. + reader.start(); + + // When the read is complete, complete the Handler callback. + callback.completeWith(reader); + + return true; + } + // end::requestContent-source[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::response[] + public class ResponseAPIs extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Sets/Gets the response HTTP status. + // Replaces: + // - servletResponse.setStatus(code); + // - servletResponse.getStatus(); + response.setStatus(HttpStatus.OK_200); + int status = response.getStatus(); + + // Gets the response HTTP headers. + // Replaces: + // - servletResponse.setHeader(name, value); + // - servletResponse.addHeader(name, value); + // - servletResponse.setDateHeader(name, date); + // - servletResponse.addDateHeader(name, date); + // - servletResponse.setIntHeader(name, value); + // - servletResponse.addIntHeader(name, value); + // - servletResponse.getHeaderNames() + // - servletResponse.getHeader(name) + // - servletResponse.getHeaders(name) + // - servletResponse.containsHeader(name) + HttpFields.Mutable responseHeaders = response.getHeaders(); + + // Sets an HTTP cookie. + // Replaces: + // - Cookie cookie = new Cookie("name", "value"); + // - cookie.setDomain("example.org"); + // - cookie.setPath("/path"); + // - cookie.setMaxAge(24 * 3600); + // - cookie.setAttribute("SameSite", "Lax"); + // - servletResponse.addCookie(cookie); + HttpCookie cookie = HttpCookie.build("name", "value") + .domain("example.org") + .path("/path") + .maxAge(Duration.ofDays(1).toSeconds()) + .sameSite(HttpCookie.SameSite.LAX) + .build(); + Response.addCookie(response, cookie); + + // Sets the response Content-Type. + // Replaces: + // - servletResponse.setContentType(type) + responseHeaders.put(HttpHeader.CONTENT_TYPE, "text/plain; charset=UTF-8"); + + // Sets the response Content-Length. + // Replaces: + // - servletResponse.setContentLength(length) + // - servletResponse.setContentLengthLong(length) + responseHeaders.put(HttpHeader.CONTENT_LENGTH, 1024L); + + // Sets/Gets the response trailers. + // Replaces: + // - servletResponse.setTrailerFields(() -> trailers) + // - servletResponse.getTrailerFields() + HttpFields trailers = HttpFields.build().put("checksum", 0xCAFE); + response.setTrailersSupplier(trailers); + Supplier trailersSupplier = response.getTrailersSupplier(); + + // Gets whether the response is committed. + // Replaces: + // - servletResponse.isCommitted() + boolean committed = response.isCommitted(); + + // Resets the response. + // Replaces: + // - servletResponse.reset(); + response.reset(); + + // Sends a redirect response. + // Replaces: + // - servletResponse.encodeRedirectURL(location) + // - servletResponse.sendRedirect(location) + String location = Request.toRedirectURI(request, "/redirect"); + Response.sendRedirect(request, response, callback, location); + + // Sends an error response. + // Replaces: + // - servletResponse.sendError(code); + // - servletResponse.sendError(code, message); + Response.writeError(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, "Request Cannot be Processed"); + + callback.succeeded(); + return true; + } + } + // end::response[] + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIsImplicit extends Handler.Abstract + { + // tag::responseContent-implicit[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Produces an implicit response with status code 200 + // with no content when returning from this method. + + // The Handler callback must be completed when returning true. + callback.succeeded(); + return true; + } + // end::responseContent-implicit[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIsImplicitWithStatus extends Handler.Abstract + { + // tag::responseContent-implicit-status[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Produces an implicit response with status 204 + // with no content when returning from this method. + response.setStatus(HttpStatus.NO_CONTENT_204); + + // The Handler callback must be completed when returning true. + callback.succeeded(); + return true; + } + // end::responseContent-implicit-status[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIsExplicit extends Handler.Abstract + { + // tag::responseContent-explicit[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + // Produces an explicit response with status 204 with no content. + response.setStatus(HttpStatus.NO_CONTENT_204); + + // This explicit first write() writes the response status code and headers. + // It is also the last write (as specified by the first parameter) + // and writes an empty content (the second parameter, a null ByteBuffer). + // When this write completes, the Handler callback is completed. + response.write(true, null, callback); + + return true; + } + // end::responseContent-explicit[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPISimpleContent extends Handler.Abstract + { + // tag::responseContent-content[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + ByteBuffer content = UTF_8.encode("Hello World"); + + // Explicit first write that writes the response status code, headers and content. + // When this write completes, the Handler callback is completed. + response.write(true, content, callback); + + return true; + } + // end::responseContent-content[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIFlush extends Handler.Abstract + { + // tag::responseContent-content[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + ByteBuffer content = UTF_8.encode("Hello World"); + response.getHeaders().put(HttpHeader.CONTENT_LENGTH, content.remaining()); + + // Flush the response status code and the headers (no content). + // This is the fist but non-last write. + Callback.Completable completable = new Callback.Completable(); + response.write(false, null, completable); + + // When the first write completes, perform the second (and last) write. + completable.whenComplete((ignored, failure) -> + { + if (failure == null) + { + // Now explicitly write the content as the last write. + // When this write completes, the Handler callback is completed. + response.write(true, content, callback); + } + else + { + // Implicitly respond with status code 500. + callback.failed(failure); + } + }); + + return true; + } + // end::responseContent-content[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIString extends Handler.Abstract + { + // tag::responseContent-string[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + // Utility method to write UTF-8 string content. + // When this write completes, the Handler callback is completed. + Content.Sink.write(response, true, "Hello World", callback); + + return true; + } + // end::responseContent-string[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPIEcho extends Handler.Abstract + { + // tag::responseContent-echo[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + // Utility method to echo the content from the request to the response. + // When the echo completes, the Handler callback is completed. + Content.copy(request, response, callback); + + return true; + } + // end::responseContent-echo[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + public class ResponseContentAPITrailers extends Handler.Abstract + { + // tag::responseContent-trailers[] + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + response.setStatus(HttpStatus.OK_200); + + // The trailers must be set on the response before the first write. + HttpFields.Mutable trailers = HttpFields.build(); + response.setTrailersSupplier(trailers); + + // Explicit first write that writes the response status code, headers and content. + // The trailers have not been written yet; they will be written with the last write. + ByteBuffer content = UTF_8.encode("Hello World"); + Callback.Completable completable = new Callback.Completable(); + response.write(false, content, completable); + + completable.whenComplete((ignored, failure) -> + { + if (failure == null) + { + // Update the trailers + trailers.put("Content-Checksum", 0xCAFE); + + // Explicit last write to write the trailers + // and complete the Handler callback. + response.write(true, null, callback); + } + else + { + // Implicitly respond with status code 500. + callback.failed(failure); + } + }); + + return true; + } + // end::responseContent-trailers[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java new file mode 100644 index 000000000000..a109be5ed6d3 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java @@ -0,0 +1,335 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.server; + +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.Executor; + +import org.eclipse.jetty.http.CookieCompliance; +import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.AbstractConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.DetectorConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.IteratingCallback; +import org.eclipse.jetty.util.ajax.AsyncJSON; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +@SuppressWarnings("unused") +public class ServerDocs +{ + public void http() throws Exception + { + // tag::http[] + // Create the HTTP/1.1 ConnectionFactory. + HttpConnectionFactory http = new HttpConnectionFactory(); + + Server server = new Server(); + + // Create the connector with the ConnectionFactory. + ServerConnector connector = new ServerConnector(server, http); + connector.setPort(8080); + + server.addConnector(connector); + server.start(); + // end::http[] + } + + public void httpUnix() throws Exception + { + // tag::httpUnix[] + // Create the HTTP/1.1 ConnectionFactory. + HttpConnectionFactory http = new HttpConnectionFactory(); + + Server server = new Server(); + + // Create the connector with the ConnectionFactory. + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, http); + connector.setUnixDomainPath(Path.of("/tmp/jetty.sock")); + + server.addConnector(connector); + server.start(); + // end::httpUnix[] + } + + public void tlsHttp() throws Exception + { + // tag::tlsHttp[] + // Create the HTTP/1.1 ConnectionFactory. + HttpConnectionFactory http = new HttpConnectionFactory(); + + // Create and configure the TLS context factory. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore.p12"); + sslContextFactory.setKeyStorePassword("secret"); + + // Create the TLS ConnectionFactory, + // setting HTTP/1.1 as the wrapped protocol. + SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, http.getProtocol()); + + Server server = new Server(); + + // Create the connector with both ConnectionFactories. + ServerConnector connector = new ServerConnector(server, tls, http); + connector.setPort(8443); + + server.addConnector(connector); + server.start(); + // end::tlsHttp[] + } + + public void detector() throws Exception + { + // tag::detector[] + // Create the HTTP/1.1 ConnectionFactory. + HttpConnectionFactory http = new HttpConnectionFactory(); + + // Create and configure the TLS context factory. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore.p12"); + sslContextFactory.setKeyStorePassword("secret"); + + // Create the TLS ConnectionFactory, + // setting HTTP/1.1 as the wrapped protocol. + SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, http.getProtocol()); + + Server server = new Server(); + + // Create the detector ConnectionFactory to + // detect whether the initial bytes are TLS. + DetectorConnectionFactory tlsDetector = new DetectorConnectionFactory(tls); // <1> + + // Create the connector with both ConnectionFactories. + ServerConnector connector = new ServerConnector(server, tlsDetector, http); // <2> + connector.setPort(8181); + + server.addConnector(connector); + server.start(); + // end::detector[] + } + + // tag::jsonHttpConnectionFactory[] + public class JSONHTTPConnectionFactory extends AbstractConnectionFactory + { + public JSONHTTPConnectionFactory() + { + super("JSONHTTP"); + } + + @Override + public Connection newConnection(Connector connector, EndPoint endPoint) + { + JSONHTTPConnection connection = new JSONHTTPConnection(endPoint, connector.getExecutor()); + // Call configure() to apply configurations common to all connections. + return configure(connection, connector, endPoint); + } + } + // end::jsonHttpConnectionFactory[] + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::jsonHttpConnection[] + public class JSONHTTPConnection extends AbstractConnection + { + // The asynchronous JSON parser. + private final AsyncJSON parser = new AsyncJSON.Factory().newAsyncJSON(); + private final IteratingCallback callback = new JSONHTTPIteratingCallback(); + + public JSONHTTPConnection(EndPoint endPoint, Executor executor) + { + super(endPoint, executor); + } + + @Override + public void onOpen() + { + super.onOpen(); + + // Declare interest in being called back when + // there are bytes to read from the network. + fillInterested(); + } + + @Override + public void onFillable() + { + callback.iterate(); + } + + private class JSONHTTPIteratingCallback extends IteratingCallback + { + private ByteBuffer buffer; + + @Override + protected Action process() throws Throwable + { + if (buffer == null) + buffer = BufferUtil.allocate(getInputBufferSize(), true); + + while (true) + { + int filled = getEndPoint().fill(buffer); + if (filled > 0) + { + boolean parsed = parser.parse(buffer); + if (parsed) + { + Map request = parser.complete(); + + // Allow applications to process the request. + invokeApplication(request, this); + + // Signal that the iteration should resume when + // the application completed the request processing. + return Action.SCHEDULED; + } + else + { + // Did not receive enough JSON bytes, + // loop around to try to read more. + } + } + else if (filled == 0) + { + // We don't need the buffer anymore, so + // don't keep it around while we are idle. + buffer = null; + + // No more bytes to read, declare + // again interest for fill events. + fillInterested(); + + // Signal that the iteration is now IDLE. + return Action.IDLE; + } + else + { + // The other peer closed the connection, + // the iteration completed successfully. + return Action.SUCCEEDED; + } + } + } + + @Override + protected void onCompleteSuccess() + { + getEndPoint().close(); + } + + @Override + protected void onCompleteFailure(Throwable cause) + { + getEndPoint().close(cause); + } + } + } + // end::jsonHttpConnection[] + + private void invokeApplication(Map request, Callback callback) + { + } + + // tag::jsonHttpAPI[] + class JSONHTTPRequest + { + // Request APIs + } + + class JSONHTTPResponse + { + // Response APIs + } + + interface JSONHTTPService + { + void service(JSONHTTPRequest request, JSONHTTPResponse response, Callback callback); + } + // end::jsonHttpAPI[] + + public void httpCompliance() + { + // tag::httpCompliance[] + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setHttpCompliance(HttpCompliance.RFC7230); + // end::httpCompliance[] + } + + public void httpComplianceCustom() + { + // tag::httpComplianceCustom[] + HttpConfiguration httpConfiguration = new HttpConfiguration(); + + // RFC7230 compliance, but allow Violation.MULTIPLE_CONTENT_LENGTHS. + HttpCompliance customHttpCompliance = HttpCompliance.from("RFC7230,MULTIPLE_CONTENT_LENGTHS"); + + httpConfiguration.setHttpCompliance(customHttpCompliance); + // end::httpComplianceCustom[] + } + + public void uriCompliance() + { + // tag::uriCompliance[] + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setUriCompliance(UriCompliance.RFC3986); + // end::uriCompliance[] + } + + public void uriComplianceCustom() + { + // tag::uriComplianceCustom[] + HttpConfiguration httpConfiguration = new HttpConfiguration(); + + // RFC3986 compliance, but enforce Violation.AMBIGUOUS_PATH_SEPARATOR. + UriCompliance customUriCompliance = UriCompliance.from("RFC3986,-AMBIGUOUS_PATH_SEPARATOR"); + + httpConfiguration.setUriCompliance(customUriCompliance); + // end::uriComplianceCustom[] + } + + public void cookieCompliance() + { + // tag::cookieCompliance[] + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setRequestCookieCompliance(CookieCompliance.RFC6265); + httpConfiguration.setResponseCookieCompliance(CookieCompliance.RFC6265); + // end::cookieCompliance[] + } + + public void cookieComplianceCustom() + { + // tag::cookieComplianceCustom[] + HttpConfiguration httpConfiguration = new HttpConfiguration(); + + // RFC6265 compliance, but enforce Violation.RESERVED_NAMES_NOT_DOLLAR_PREFIXED. + CookieCompliance customUriCompliance = CookieCompliance.from("RFC6265,-RESERVED_NAMES_NOT_DOLLAR_PREFIXED"); + httpConfiguration.setRequestCookieCompliance(customUriCompliance); + + httpConfiguration.setResponseCookieCompliance(CookieCompliance.RFC6265); + + // end::cookieComplianceCustom[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java new file mode 100644 index 000000000000..40aa6abdd0aa --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java @@ -0,0 +1,1652 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.server.http; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.security.Security; +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.CompletableFuture; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.conscrypt.OpenSSLProvider; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http.MultiPart; +import org.eclipse.jetty.http.MultiPartFormData; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.http3.server.HTTP3ServerConnectionFactory; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.rewrite.handler.CompactPathRule; +import org.eclipse.jetty.rewrite.handler.RedirectRegexRule; +import org.eclipse.jetty.rewrite.handler.RewriteHandler; +import org.eclipse.jetty.rewrite.handler.RewriteRegexRule; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.CustomRequestLog; +import org.eclipse.jetty.server.FormFields; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.MemoryConnector; +import org.eclipse.jetty.server.MemoryTransport; +import org.eclipse.jetty.server.NetworkConnector; +import org.eclipse.jetty.server.ProxyConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.RequestLogWriter; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.Slf4jRequestLogWriter; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.server.handler.CrossOriginHandler; +import org.eclipse.jetty.server.handler.DefaultHandler; +import org.eclipse.jetty.server.handler.EventsHandler; +import org.eclipse.jetty.server.handler.QoSHandler; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.server.handler.SecuredRedirectHandler; +import org.eclipse.jetty.server.handler.StateTrackingHandler; +import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +import static java.lang.System.Logger.Level.ERROR; +import static java.lang.System.Logger.Level.INFO; +import static java.nio.charset.StandardCharsets.UTF_8; + +@SuppressWarnings("unused") +public class HTTPServerDocs +{ + public void simple() throws Exception + { + // tag::simple[] + // Create and configure a ThreadPool. + QueuedThreadPool threadPool = new QueuedThreadPool(); + threadPool.setName("server"); + + // Create a Server instance. + Server server = new Server(threadPool); + + // Create a ServerConnector to accept connections from clients. + Connector connector = new ServerConnector(server); + + // Add the Connector to the Server + server.addConnector(connector); + + // Set a simple Handler to handle requests/responses. + server.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Succeed the callback to signal that the + // request/response processing is complete. + callback.succeeded(); + return true; + } + }); + + // Start the Server to start accepting connections from clients. + server.start(); + // end::simple[] + } + + public void serverRequestLogSLF4J() + { + // tag::serverRequestLogSLF4J[] + Server server = new Server(); + + // Sets the RequestLog to log to an SLF4J logger named "org.eclipse.jetty.server.RequestLog" at INFO level. + server.setRequestLog(new CustomRequestLog(new Slf4jRequestLogWriter(), CustomRequestLog.EXTENDED_NCSA_FORMAT)); + // end::serverRequestLogSLF4J[] + } + + public void serverRequestLogFile() + { + // tag::serverRequestLogFile[] + Server server = new Server(); + + // Use a file name with the pattern 'yyyy_MM_dd' so rolled over files retain their date. + RequestLogWriter logWriter = new RequestLogWriter("/var/log/yyyy_MM_dd.jetty.request.log"); + // Retain rolled over files for 2 weeks. + logWriter.setRetainDays(14); + // Log times are in the current time zone. + logWriter.setTimeZone(TimeZone.getDefault().getID()); + + // Set the RequestLog to log to the given file, rolling over at midnight. + server.setRequestLog(new CustomRequestLog(logWriter, CustomRequestLog.EXTENDED_NCSA_FORMAT)); + // end::serverRequestLogFile[] + } + + public void configureConnector() throws Exception + { + // tag::configureConnector[] + Server server = new Server(); + + // The number of acceptor threads. + int acceptors = 1; + + // The number of selectors. + int selectors = 1; + + // Create a ServerConnector instance. + ServerConnector connector = new ServerConnector(server, acceptors, selectors, new HttpConnectionFactory()); + + // Configure TCP/IP parameters. + + // The port to listen to. + connector.setPort(8080); + // The address to bind to. + connector.setHost("127.0.0.1"); + + // The TCP accept queue size. + connector.setAcceptQueueSize(128); + + server.addConnector(connector); + server.start(); + // end::configureConnector[] + } + + public void configureConnectorUnix() throws Exception + { + // tag::configureConnectorUnix[] + Server server = new Server(); + + // The number of acceptor threads. + int acceptors = 1; + + // The number of selectors. + int selectors = 1; + + // Create a ServerConnector instance. + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, acceptors, selectors, new HttpConnectionFactory()); + + // Configure Unix-Domain parameters. + + // The Unix-Domain path to listen to. + connector.setUnixDomainPath(Path.of("/tmp/jetty.sock")); + + // The TCP accept queue size. + connector.setAcceptQueueSize(128); + + server.addConnector(connector); + server.start(); + // end::configureConnectorUnix[] + } + + public void configureConnectorQuic() throws Exception + { + // tag::configureConnectorQuic[] + Server server = new Server(); + + // Configure the SslContextFactory with the keyStore information. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + // Create a QuicServerConnector instance. + Path pemWorkDir = Path.of("/path/to/pem/dir"); + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslContextFactory, pemWorkDir); + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); + + // The port to listen to. + connector.setPort(8080); + // The address to bind to. + connector.setHost("127.0.0.1"); + + server.addConnector(connector); + server.start(); + // end::configureConnectorQuic[] + } + + public void memoryConnector() throws Exception + { + // tag::memoryConnector[] + Server server = new Server(); + + // Create a MemoryConnector instance that speaks HTTP/1.1. + MemoryConnector connector = new MemoryConnector(server, new HttpConnectionFactory()); + + server.addConnector(connector); + server.start(); + + // The code above is the server-side. + // ---- + // The code below is the client-side. + + HttpClient httpClient = new HttpClient(); + httpClient.start(); + + ContentResponse response = httpClient.newRequest("http://localhost/") + // Use the memory Transport to communicate with the server-side. + .transport(new MemoryTransport(connector)) + .send(); + // end::memoryConnector[] + } + + public void configureConnectors() throws Exception + { + // tag::configureConnectors[] + Server server = new Server(); + + // Create a ServerConnector instance on port 8080. + ServerConnector connector1 = new ServerConnector(server, 1, 1, new HttpConnectionFactory()); + connector1.setPort(8080); + server.addConnector(connector1); + + // Create another ServerConnector instance on port 9090, + // for example with a different HTTP configuration. + HttpConfiguration httpConfig2 = new HttpConfiguration(); + httpConfig2.setHttpCompliance(HttpCompliance.LEGACY); + ServerConnector connector2 = new ServerConnector(server, 1, 1, new HttpConnectionFactory(httpConfig2)); + connector2.setPort(9090); + server.addConnector(connector2); + + server.start(); + // end::configureConnectors[] + } + + public void sameRandomPort() throws Exception + { + // tag::sameRandomPort[] + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + Server server = new Server(); + + // The plain HTTP configuration. + HttpConfiguration plainConfig = new HttpConfiguration(); + + // The secure HTTP configuration. + HttpConfiguration secureConfig = new HttpConfiguration(plainConfig); + secureConfig.addCustomizer(new SecureRequestCustomizer()); + + // First, create the secure connector for HTTPS and HTTP/2. + HttpConnectionFactory https = new HttpConnectionFactory(secureConfig); + HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(secureConfig); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(https.getProtocol()); + ConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, https.getProtocol()); + ServerConnector secureConnector = new ServerConnector(server, 1, 1, ssl, alpn, http2, https); + server.addConnector(secureConnector); + + // Second, create the plain connector for HTTP. + HttpConnectionFactory http = new HttpConnectionFactory(plainConfig); + ServerConnector plainConnector = new ServerConnector(server, 1, 1, http); + server.addConnector(plainConnector); + + // Third, create the connector for HTTP/3. + Path pemWorkDir = Path.of("/path/to/pem/dir"); + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslContextFactory, pemWorkDir); + QuicServerConnector http3Connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); + server.addConnector(http3Connector); + + // Set up a listener so that when the secure connector starts, + // it configures the other connectors that have not started yet. + secureConnector.addEventListener(new NetworkConnector.Listener() + { + @Override + public void onOpen(NetworkConnector connector) + { + int port = connector.getLocalPort(); + + // Configure the plain connector for secure redirects from http to https. + plainConfig.setSecurePort(port); + + // Configure the HTTP3 connector port to be the same as HTTPS/HTTP2. + http3Connector.setPort(port); + } + }); + + server.start(); + // end::sameRandomPort[] + } + + public void sslHandshakeListener() throws Exception + { + // tag::sslHandshakeListener[] + // Create a SslHandshakeListener. + SslHandshakeListener listener = new SslHandshakeListener() + { + @Override + public void handshakeSucceeded(Event event) throws SSLException + { + SSLEngine sslEngine = event.getSSLEngine(); + System.getLogger("tls").log(INFO, "TLS handshake successful to %s", sslEngine.getPeerHost()); + } + + @Override + public void handshakeFailed(Event event, Throwable failure) + { + SSLEngine sslEngine = event.getSSLEngine(); + System.getLogger("tls").log(ERROR, "TLS handshake failure to %s", sslEngine.getPeerHost(), failure); + } + }; + + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + // Add the SslHandshakeListener as bean to ServerConnector. + // The listener will be notified of TLS handshakes success and failure. + connector.addBean(listener); + // end::sslHandshakeListener[] + } + + public void http11() throws Exception + { + // tag::http11[] + Server server = new Server(); + + // The HTTP configuration object. + HttpConfiguration httpConfig = new HttpConfiguration(); + // Configure the HTTP support, for example: + httpConfig.setSendServerVersion(false); + + // The ConnectionFactory for HTTP/1.1. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig); + + // Create the ServerConnector. + ServerConnector connector = new ServerConnector(server, http11); + connector.setPort(8080); + + server.addConnector(connector); + server.start(); + // end::http11[] + } + + public void proxyHTTP() throws Exception + { + // tag::proxyHTTP[] + Server server = new Server(); + + // The HTTP configuration object. + HttpConfiguration httpConfig = new HttpConfiguration(); + // Configure the HTTP support, for example: + httpConfig.setSendServerVersion(false); + + // The ConnectionFactory for HTTP/1.1. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig); + + // The ConnectionFactory for the PROXY protocol. + ProxyConnectionFactory proxy = new ProxyConnectionFactory(http11.getProtocol()); + + // Create the ServerConnector. + ServerConnector connector = new ServerConnector(server, proxy, http11); + connector.setPort(8080); + + server.addConnector(connector); + server.start(); + // end::proxyHTTP[] + } + + public void proxyHTTPUnix() throws Exception + { + // tag::proxyHTTPUnix[] + Server server = new Server(); + + // The HTTP configuration object. + HttpConfiguration httpConfig = new HttpConfiguration(); + // Configure the HTTP support, for example: + httpConfig.setSendServerVersion(false); + + // The ConnectionFactory for HTTP/1.1. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig); + + // The ConnectionFactory for the PROXY protocol. + ProxyConnectionFactory proxy = new ProxyConnectionFactory(http11.getProtocol()); + + // Create the ServerConnector. + UnixDomainServerConnector connector = new UnixDomainServerConnector(server, proxy, http11); + connector.setUnixDomainPath(Path.of("/tmp/jetty.sock")); + + server.addConnector(connector); + server.start(); + // end::proxyHTTPUnix[] + } + + public void tlsHttp11() throws Exception + { + // tag::tlsHttp11[] + Server server = new Server(); + + // The HTTP configuration object. + HttpConfiguration httpConfig = new HttpConfiguration(); + // Add the SecureRequestCustomizer because TLS is used. + httpConfig.addCustomizer(new SecureRequestCustomizer()); + + // The ConnectionFactory for HTTP/1.1. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig); + + // Configure the SslContextFactory with the keyStore information. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + // The ConnectionFactory for TLS. + SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, http11.getProtocol()); + + // The ServerConnector instance. + ServerConnector connector = new ServerConnector(server, tls, http11); + connector.setPort(8443); + + server.addConnector(connector); + server.start(); + // end::tlsHttp11[] + } + + public void http11H2C() throws Exception + { + // tag::http11H2C[] + Server server = new Server(); + + // The HTTP configuration object. + HttpConfiguration httpConfig = new HttpConfiguration(); + + // The ConnectionFactory for HTTP/1.1. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig); + + // The ConnectionFactory for clear-text HTTP/2. + HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + + // The ServerConnector instance. + ServerConnector connector = new ServerConnector(server, http11, h2c); + connector.setPort(8080); + + server.addConnector(connector); + server.start(); + // end::http11H2C[] + } + + public void tlsALPNHTTP() throws Exception + { + // tag::tlsALPNHTTP[] + Server server = new Server(); + + // The HTTP configuration object. + HttpConfiguration httpConfig = new HttpConfiguration(); + // Add the SecureRequestCustomizer because TLS is used. + httpConfig.addCustomizer(new SecureRequestCustomizer()); + + // The ConnectionFactory for HTTP/1.1. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpConfig); + + // The ConnectionFactory for HTTP/2. + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig); + + // The ALPN ConnectionFactory. + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + // The default protocol to use in case there is no negotiation. + alpn.setDefaultProtocol(http11.getProtocol()); + + // Configure the SslContextFactory with the keyStore information. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + // The ConnectionFactory for TLS. + SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); + + // The ServerConnector instance. + ServerConnector connector = new ServerConnector(server, tls, alpn, h2, http11); + connector.setPort(8443); + + server.addConnector(connector); + server.start(); + // end::tlsALPNHTTP[] + } + + public void h3() throws Exception + { + // tag::h3[] + Server server = new Server(); + + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.addCustomizer(new SecureRequestCustomizer()); + + // Create and configure the HTTP/3 connector. + // It is mandatory to configure the PEM directory. + Path pemWorkDir = Path.of("/path/to/pem/dir"); + ServerQuicConfiguration serverQuicConfig = new ServerQuicConfiguration(sslContextFactory, pemWorkDir); + QuicServerConnector connector = new QuicServerConnector(server, serverQuicConfig, new HTTP3ServerConnectionFactory(serverQuicConfig)); + connector.setPort(843); + + server.addConnector(connector); + + server.start(); + // end::h3[] + } + + public void conscrypt() + { + // tag::conscrypt[] + // Configure the JDK with the Conscrypt provider. + Security.addProvider(new OpenSSLProvider()); + + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + // Configure Jetty's SslContextFactory to use Conscrypt. + sslContextFactory.setProvider("Conscrypt"); + // end::conscrypt[] + } + + public void handlerTree() + { + class LoggingHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + } + + class App1Handler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + } + + class App2Handler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + callback.succeeded(); + return true; + } + } + + // tag::handlerTree[] + Server server = new Server(); + + GzipHandler gzipHandler = new GzipHandler(); + server.setHandler(gzipHandler); + + Handler.Sequence sequence = new Handler.Sequence(); + gzipHandler.setHandler(sequence); + + sequence.addHandler(new App1Handler()); + sequence.addHandler(new App2Handler()); + // end::handlerTree[] + } + + public void handlerAPI() + { + class MyHandler extends Handler.Abstract + { + @Override + // tag::handlerAPI[] + public boolean handle(Request request, Response response, Callback callback) throws Exception + // end::handlerAPI[] + { + return false; + } + } + } + + public void handlerHello() throws Exception + { + // tag::handlerHello[] + class HelloWorldHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + response.setStatus(200); + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/html; charset=UTF-8"); + + // Write a Hello World response. + Content.Sink.write(response, true, """ + + + + Jetty Hello World Handler + + +

Hello World

+ + + """, callback); + return true; + } + } + + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Set the Hello World Handler. + server.setHandler(new HelloWorldHandler()); + + server.start(); + // end::handlerHello[] + } + + public void handlerFilter() throws Exception + { + class HelloWorldHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + return true; + } + } + + // tag::handlerFilter[] + class FilterHandler extends Handler.Wrapper + { + public FilterHandler(Handler handler) + { + super(handler); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + String path = Request.getPathInContext(request); + if (path.startsWith("/old_path/")) + { + // Rewrite old paths to new paths. + HttpURI uri = request.getHttpURI(); + String newPath = "/new_path/" + path.substring("/old_path/".length()); + HttpURI newURI = HttpURI.build(uri).path(newPath).asImmutable(); + + // Modify the request object by wrapping the HttpURI. + Request newRequest = Request.serveAs(request, newURI); + + // Forward to the next Handler using the wrapped Request. + return super.handle(newRequest, response, callback); + } + else + { + // Forward to the next Handler as-is. + return super.handle(request, response, callback); + } + } + } + + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Link the Handlers in a chain. + server.setHandler(new FilterHandler(new HelloWorldHandler())); + + server.start(); + // end::handlerFilter[] + } + + public void handlerForm() + { + // tag::handlerForm[] + class FormHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + if (MimeTypes.Type.FORM_ENCODED.is(contentType)) + { + // Convert the request content into Fields. + CompletableFuture completableFields = FormFields.from(request); // <1> + + // When all the request content has arrived, process the fields. + completableFields.whenComplete((fields, failure) -> // <2> + { + if (failure == null) + { + processFields(fields); + // Send a simple 200 response, completing the callback. + response.setStatus(HttpStatus.OK_200); + callback.succeeded(); + } + else + { + // Reading the request content failed. + // Send an error response, completing the callback. + Response.writeError(request, response, callback, failure); + } + }); + + // The callback will be eventually completed in all cases, return true. + return true; + } + else + { + // Send an error response, completing the callback, and returning true. + Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "invalid request"); + return true; + } + } + } + // end::handlerForm[] + } + + private static void processFields(Fields fields) + { + } + + public void handlerMultiPart() + { + // tag::handlerMultiPart[] + class MultiPartFormDataHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE); + if (MimeTypes.Type.MULTIPART_FORM_DATA.is(contentType)) + { + // Extract the multipart boundary. + String boundary = MultiPart.extractBoundary(contentType); + + // Create and configure the multipart parser. + MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary); + // By default, uploaded files are stored in this directory, to + // avoid to read the file content (which can be large) in memory. + parser.setFilesDirectory(Path.of("/tmp")); + // Convert the request content into parts. + CompletableFuture completableParts = parser.parse(request); // <1> + + // When all the request content has arrived, process the parts. + completableParts.whenComplete((parts, failure) -> // <2> + { + if (failure == null) + { + // Use the Parts API to process the parts. + processParts(parts); + // Send a simple 200 response, completing the callback. + response.setStatus(HttpStatus.OK_200); + callback.succeeded(); + } + else + { + // Reading the request content failed. + // Send an error response, completing the callback. + Response.writeError(request, response, callback, failure); + } + }); + + // The callback will be eventually completed in all cases, return true. + return true; + } + else + { + // Send an error response, completing the callback, and returning true. + Response.writeError(request, response, callback, HttpStatus.BAD_REQUEST_400, "invalid request"); + return true; + } + } + } + // end::handlerMultiPart[] + } + + private void processParts(MultiPartFormData.Parts parts) + { + } + + public void flush() + { + // tag::flush[] + class FlushingHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Set the response status code. + response.setStatus(HttpStatus.OK_200); + // Set the response headers. + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); + + // Commit the response with a "flush" write. + Callback.Completable.with(flush -> response.write(false, null, flush)) + // When the flush is finished, send the content and complete the callback. + .whenComplete((ignored, failure) -> + { + if (failure == null) + response.write(true, UTF_8.encode("HELLO"), callback); + else + callback.failed(failure); + }); + + // Return true because the callback will eventually be completed. + return true; + } + } + // end::flush[] + } + + public void contentLength() + { + // tag::contentLength[] + class ContentLengthHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Set the response status code. + response.setStatus(HttpStatus.OK_200); + + String content = """ + { + "result": 0, + "advice": { + "message": "Jetty Rocks!" + } + } + """; + // Must count the bytes, not the characters! + byte[] bytes = content.getBytes(UTF_8); + long contentLength = bytes.length; + + // Set the response headers before the response is committed. + HttpFields.Mutable responseHeaders = response.getHeaders(); + // Set the content type. + responseHeaders.put(HttpHeader.CONTENT_TYPE, "application/json; charset=UTF-8"); + // Set the response content length. + responseHeaders.put(HttpHeader.CONTENT_LENGTH, contentLength); + + // Commit the response. + response.write(true, ByteBuffer.wrap(bytes), callback); + + // Return true because the callback will eventually be completed. + return true; + } + } + // end::contentLength[] + } + + public void handlerContinue100() + { + // tag::continue[] + class Continue100Handler extends Handler.Wrapper + { + public Continue100Handler(Handler handler) + { + super(handler); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + HttpFields requestHeaders = request.getHeaders(); + if (requestHeaders.contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString())) + { + // Analyze the request and decide whether to receive the content. + long contentLength = request.getLength(); + if (contentLength > 0 && contentLength < 1024) + { + // Small request content, ask to send it by + // sending a 100 Continue interim response. + CompletableFuture processing = response.writeInterim(HttpStatus.CONTINUE_100, HttpFields.EMPTY) // <1> + // Then read the request content into a ByteBuffer. + .thenCompose(ignored -> Promise.Completable.with(p -> Content.Source.asByteBuffer(request, p))) + // Then store the ByteBuffer somewhere. + .thenCompose(byteBuffer -> store(byteBuffer)); + + // At the end of the processing, complete + // the callback with the CompletableFuture, + // a simple 200 response in case of success, + // or a 500 response in case of failure. + callback.completeWith(processing); // <2> + return true; + } + else + { + // The request content is too large, send an error. + Response.writeError(request, response, callback, HttpStatus.PAYLOAD_TOO_LARGE_413); + return true; + } + } + else + { + return super.handle(request, response, callback); + } + } + } + // end::continue[] + } + + private static CompletableFuture store(ByteBuffer byteBuffer) + { + return new CompletableFuture<>(); + } + + public void earlyHints() + { + // tag::earlyHints103[] + class EarlyHints103Handler extends Handler.Wrapper + { + public EarlyHints103Handler(Handler handler) + { + super(handler); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception + { + String pathInContext = Request.getPathInContext(request); + + // Simple logic that assumes that every HTML + // file has associated the same CSS stylesheet. + if (pathInContext.endsWith(".html")) + { + // Tell the client that a Link is coming + // sending a 103 Early Hints interim response. + HttpFields.Mutable interimHeaders = HttpFields.build() + .put(HttpHeader.LINK, "; rel=preload; as=style"); + + response.writeInterim(HttpStatus.EARLY_HINTS_103, interimHeaders) // <1> + .whenComplete((ignored, failure) -> // <2> + { + if (failure == null) + { + try + { + // Delegate the handling to the child Handler. + boolean handled = super.handle(request, response, callback); + if (!handled) + { + // The child Handler did not produce a final response, do it here. + Response.writeError(request, response, callback, HttpStatus.NOT_FOUND_404); + } + } + catch (Throwable x) + { + callback.failed(x); + } + } + else + { + callback.failed(failure); + } + }); + + // This Handler sent an interim response, so this Handler + // (or its descendants) must produce a final response, so return true. + return true; + } + else + { + // Not a request for an HTML page, delegate + // the handling to the child Handler. + return super.handle(request, response, callback); + } + } + } + // end::earlyHints103[] + } + + public void contextHandler() throws Exception + { + // tag::contextHandler[] + class ShopHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Implement the shop, remembering to complete the callback. + return true; + } + } + + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create a ContextHandler with contextPath. + ContextHandler context = new ContextHandler(new ShopHandler(), "/shop"); + + // Link the context to the server. + server.setHandler(context); + + server.start(); + // end::contextHandler[] + } + + public void contextHandlerCollection() throws Exception + { + // tag::contextHandlerCollection[] + class ShopHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Implement the shop, remembering to complete the callback. + return true; + } + } + + class RESTHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Implement the REST APIs, remembering to complete the callback. + return true; + } + } + + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + + // Create the context for the shop web application and add it to ContextHandlerCollection. + contextCollection.addHandler(new ContextHandler(new ShopHandler(), "/shop")); + + // Link the ContextHandlerCollection to the Server. + server.setHandler(contextCollection); + + server.start(); + + // Create the context for the API web application. + ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api"); + // Web applications can be deployed after the Server is started. + contextCollection.deployHandler(apiContext, Callback.NOOP); + // end::contextHandlerCollection[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::servletContextHandler-servlet[] + public class ShopCartServlet extends HttpServlet + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + // Implement the shop cart functionality. + } + } + // end::servletContextHandler-servlet[] + + public void servletContextHandler() throws Exception + { + // tag::servletContextHandler-setup[] + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Add the CrossOriginHandler to protect from CSRF attacks. + CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of("http://domain.com")); + crossOriginHandler.setAllowCredentials(true); + server.setHandler(crossOriginHandler); + + // Create a ServletContextHandler with contextPath. + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/shop"); + // Link the context to the server. + crossOriginHandler.setHandler(context); + + // Add the Servlet implementing the cart functionality to the context. + ServletHolder servletHolder = context.addServlet(ShopCartServlet.class, "/cart/*"); + // Configure the Servlet with init-parameters. + servletHolder.setInitParameter("maxItems", "128"); + + server.start(); + // end::servletContextHandler-setup[] + } + + public void webAppContextHandler() throws Exception + { + // tag::webAppContextHandler[] + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create a WebAppContext. + WebAppContext context = new WebAppContext(); + // Link the context to the server. + server.setHandler(context); + + // Configure the path of the packaged web application (file or directory). + context.setWar("/path/to/webapp.war"); + // Configure the contextPath. + context.setContextPath("/app"); + + server.start(); + // end::webAppContextHandler[] + } + + public void resourceHandler() throws Exception + { + // tag::resourceHandler[] + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create and configure a ResourceHandler. + ResourceHandler handler = new ResourceHandler(); + // Configure the directory where static resources are located. + handler.setBaseResource(ResourceFactory.of(handler).newResource("/path/to/static/resources/")); + // Configure directory listing. + handler.setDirAllowed(false); + // Configure welcome files. + handler.setWelcomeFiles(List.of("index.html")); + // Configure whether to accept range requests. + handler.setAcceptRanges(true); + + // Link the context to the server. + server.setHandler(handler); + + server.start(); + // end::resourceHandler[] + } + + public void multipleResourcesHandler() + { + // tag::multipleResourcesHandler[] + ResourceHandler handler = new ResourceHandler(); + + // For multiple directories, use ResourceFactory.combine(). + Resource resource = ResourceFactory.combine( + ResourceFactory.of(handler).newResource("/path/to/static/resources/"), + ResourceFactory.of(handler).newResource("/another/path/to/static/resources/") + ); + handler.setBaseResource(resource); + // end::multipleResourcesHandler[] + } + + public void defaultServlet() + { + // tag::defaultServlet[] + // Create a ServletContextHandler with contextPath. + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/app"); + + // Add the DefaultServlet to serve static content. + ServletHolder servletHolder = context.addServlet(DefaultServlet.class, "/"); + // Configure the DefaultServlet with init-parameters. + servletHolder.setInitParameter("resourceBase", "/path/to/static/resources/"); + servletHolder.setAsyncSupported(true); + // end::defaultServlet[] + } + + public void serverGzipHandler() throws Exception + { + // tag::serverGzipHandler[] + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create and configure GzipHandler. + GzipHandler gzipHandler = new GzipHandler(); + server.setHandler(gzipHandler); + // Only compress response content larger than this. + gzipHandler.setMinGzipSize(1024); + // Do not compress these URI paths. + gzipHandler.setExcludedPaths("/uncompressed"); + // Also compress POST responses. + gzipHandler.addIncludedMethods("POST"); + // Do not compress these mime types. + gzipHandler.addExcludedMimeTypes("font/ttf"); + + // Create a ContextHandlerCollection to manage contexts. + ContextHandlerCollection contexts = new ContextHandlerCollection(); + gzipHandler.setHandler(contexts); + + server.start(); + // end::serverGzipHandler[] + } + + public void contextGzipHandler() throws Exception + { + class ShopHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Implement the shop, remembering to complete the callback. + return true; + } + } + + class RESTHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Implement the REST APIs, remembering to complete the callback. + return true; + } + } + + // tag::contextGzipHandler[] + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + // Link the ContextHandlerCollection to the Server. + server.setHandler(contextCollection); + + // Create the context for the shop web application wrapped with GzipHandler so only the shop will do gzip. + GzipHandler shopGzipHandler = new GzipHandler(new ContextHandler(new ShopHandler(), "/shop")); + + // Add it to ContextHandlerCollection. + contextCollection.addHandler(shopGzipHandler); + + // Create the context for the API web application. + ContextHandler apiContext = new ContextHandler(new RESTHandler(), "/api"); + + // Add it to ContextHandlerCollection. + contextCollection.addHandler(apiContext); + + server.start(); + // end::contextGzipHandler[] + } + + public void rewriteHandler() throws Exception + { + // tag::rewriteHandler[] + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create and link the RewriteHandler to the Server. + RewriteHandler rewriteHandler = new RewriteHandler(); + server.setHandler(rewriteHandler); + + // Compacts URI paths with double slashes, e.g. /ctx//path/to//resource. + rewriteHandler.addRule(new CompactPathRule()); + // Rewrites */products/* to */p/*. + rewriteHandler.addRule(new RewriteRegexRule("/(.*)/product/(.*)", "/$1/p/$2")); + // Redirects permanently to a different URI. + RedirectRegexRule redirectRule = new RedirectRegexRule("/documentation/(.*)", "https://docs.domain.com/$1"); + redirectRule.setStatusCode(HttpStatus.MOVED_PERMANENTLY_301); + rewriteHandler.addRule(redirectRule); + + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + rewriteHandler.setHandler(contextCollection); + + server.start(); + // end::rewriteHandler[] + } + + public void statisticsHandler() throws Exception + { + // tag::statisticsHandler[] + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create and link the StatisticsHandler to the Server. + StatisticsHandler statsHandler = new StatisticsHandler(); + server.setHandler(statsHandler); + + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + statsHandler.setHandler(contextCollection); + + server.start(); + // end::statisticsHandler[] + } + + public void dataRateHandler() throws Exception + { + // tag::dataRateHandler[] + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create and link the MinimumDataRateHandler to the Server. + // Create the MinimumDataRateHandler with a minimum read rate of 1KB per second and no minimum write rate. + StatisticsHandler.MinimumDataRateHandler dataRateHandler = new StatisticsHandler.MinimumDataRateHandler(1024L, 0L); + server.setHandler(dataRateHandler); + + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + dataRateHandler.setHandler(contextCollection); + + server.start(); + // end::dataRateHandler[] + } + + public void eventsHandler() throws Exception + { + // tag::eventsHandler[] + class MyEventsHandler extends EventsHandler + { + @Override + protected void onBeforeHandling(Request request) + { + // The nanoTime at which the request is first received. + long requestBeginNanoTime = request.getBeginNanoTime(); + + // The nanoTime just before invoking the next Handler. + request.setAttribute("beforeHandlingNanoTime", NanoTime.now()); + } + + @Override + protected void onComplete(Request request, int status, HttpFields headers, Throwable failure) + { + // Retrieve the before handling nanoTime. + long beforeHandlingNanoTime = (long)request.getAttribute("beforeHandlingNanoTime"); + + // Record the request processing time and the status that was sent back to the client. + long processingTime = NanoTime.millisSince(beforeHandlingNanoTime); + System.getLogger("trackTime").log(INFO, "processing request %s took %d ms and ended with status code %d", request, processingTime, status); + } + } + + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + // Link the EventsHandler as the outermost Handler after Server. + MyEventsHandler eventsHandler = new MyEventsHandler(); + server.setHandler(eventsHandler); + + ContextHandler appHandler = new ContextHandler("/app"); + eventsHandler.setHandler(appHandler); + + server.start(); + // end::eventsHandler[] + } + + public void simpleQoSHandler() throws Exception + { + // tag::simpleQoSHandler[] + class ShopHandler extends Handler.Abstract + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Implement the shop, remembering to complete the callback. + callback.succeeded(); + return true; + } + } + + int maxThreads = 256; + QueuedThreadPool serverThreads = new QueuedThreadPool(maxThreads); + Server server = new Server(serverThreads); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create and configure QoSHandler. + QoSHandler qosHandler = new QoSHandler(); + // Set the max number of concurrent requests, + // for example in relation to the thread pool. + qosHandler.setMaxRequestCount(maxThreads / 2); + // A suspended request may stay suspended for at most 15 seconds. + qosHandler.setMaxSuspend(Duration.ofSeconds(15)); + server.setHandler(qosHandler); + + // Provide quality of service to the shop + // application by wrapping ShopHandler with QoSHandler. + qosHandler.setHandler(new ShopHandler()); + + server.start(); + // end::simpleQoSHandler[] + } + + public void advancedQoSHandler() + { + // tag::advancedQoSHandler[] + class PriorityQoSHandler extends QoSHandler + { + @Override + protected int getPriority(Request request) + { + String pathInContext = Request.getPathInContext(request); + + // Payment requests have the highest priority. + if (pathInContext.startsWith("/payment/")) + return 3; + + // Login, checkout and admin requests. + if (pathInContext.startsWith("/login/")) + return 2; + if (pathInContext.startsWith("/checkout/")) + return 2; + if (pathInContext.startsWith("/admin/")) + return 2; + + // Health-check requests from the load balancer. + if (pathInContext.equals("/health-check")) + return 1; + + // Other requests have the lowest priority. + return 0; + } + } + // end::advancedQoSHandler[] + } + + public void securedHandler() throws Exception + { + // tag::securedHandler[] + Server server = new Server(); + + // Configure the HttpConfiguration for the clear-text connector. + int securePort = 8443; + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSecurePort(securePort); + + // The clear-text connector. + ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); + connector.setPort(8080); + server.addConnector(connector); + + // Configure the HttpConfiguration for the secure connector. + HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); + // Add the SecureRequestCustomizer because TLS is used. + httpConfig.addCustomizer(new SecureRequestCustomizer()); + + // The HttpConnectionFactory for the secure connector. + HttpConnectionFactory http11 = new HttpConnectionFactory(httpsConfig); + + // Configure the SslContextFactory with the keyStore information. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + // The ConnectionFactory for TLS. + SslConnectionFactory tls = new SslConnectionFactory(sslContextFactory, http11.getProtocol()); + + // The secure connector. + ServerConnector secureConnector = new ServerConnector(server, tls, http11); + secureConnector.setPort(8443); + server.addConnector(secureConnector); + + // Create and link the SecuredRedirectHandler to the Server. + SecuredRedirectHandler securedHandler = new SecuredRedirectHandler(); + server.setHandler(securedHandler); + + // Create a ContextHandlerCollection to hold contexts. + ContextHandlerCollection contextCollection = new ContextHandlerCollection(); + securedHandler.setHandler(contextCollection); + + server.start(); + // end::securedHandler[] + } + + public void crossOriginAllowedOrigins() + { + // tag::crossOriginAllowedOrigins[] + CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + // The allowed origins are regex patterns. + crossOriginHandler.setAllowedOriginPatterns(Set.of("http://domain\\.com")); + // end::crossOriginAllowedOrigins[] + } + + public void stateTrackingHandle() + { + // tag::stateTrackingHandle[] + StateTrackingHandler stateTrackingHandler = new StateTrackingHandler(); + + // Emit an event if the Handler callback is not completed with 5 seconds. + stateTrackingHandler.setHandlerCallbackTimeout(5000); + // end::stateTrackingHandle[] + } + + public void stateTrackingListener() + { + // tag::stateTrackingListener[] + StateTrackingHandler stateTrackingHandler = new StateTrackingHandler(new StateTrackingHandler.Listener() + { + @Override + public void onHandlerCallbackNotCompleted(Request request, StateTrackingHandler.ThreadInfo handlerThreadInfo) + { + // Your own event handling logic. + } + }); + + // Emit an event if the Handler callback is not completed with 5 seconds. + stateTrackingHandler.setHandlerCallbackTimeout(5000); + // end::stateTrackingListener[] + } + + public void defaultHandler() throws Exception + { + // tag::defaultHandler[] + Server server = new Server(); + server.setDefaultHandler(new DefaultHandler(false, true)); + + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Add a ContextHandlerCollection to manage contexts. + ContextHandlerCollection contexts = new ContextHandlerCollection(); + + // Link the contexts to the Server. + server.setHandler(contexts); + + server.start(); + // end::defaultHandler[] + } + + public void continue100() + { + // tag::continue100[] + class Continue100HttpServlet extends HttpServlet + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + // Inspect the method and headers. + boolean isPost = HttpMethod.POST.is(request.getMethod()); + boolean expects100 = HttpHeaderValue.CONTINUE.is(request.getHeader("Expect")); + long contentLength = request.getContentLengthLong(); + + if (isPost && expects100) + { + if (contentLength > 1024 * 1024) + { + // Rejects uploads that are too large. + response.sendError(HttpStatus.PAYLOAD_TOO_LARGE_413); + } + else + { + // Getting the request InputStream indicates that + // the application wants to read the request content. + // Jetty will send the 100 Continue response at this + // point, and the client will send the request content. + ServletInputStream input = request.getInputStream(); + + // Read and process the request input. + } + } + else + { + // Process normal requests. + } + } + } + // end::continue100[] + } + + public void requestCustomizer() throws Exception + { + // tag::requestCustomizer[] + Server server = new Server(); + + // Configure the secure connector. + HttpConfiguration httpsConfig = new HttpConfiguration(); + + // Add the SecureRequestCustomizer. + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + + // Configure the SslContextFactory with the KeyStore information. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + // Configure the Connector to speak HTTP/1.1 and HTTP/2. + HttpConnectionFactory h1 = new HttpConnectionFactory(httpsConfig); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig); + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(h1.getProtocol()); + SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); + ServerConnector connector = new ServerConnector(server, ssl, alpn, h2, h1); + server.addConnector(connector); + + server.start(); + // end::requestCustomizer[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/SessionHandlerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/SessionHandlerDocs.java new file mode 100644 index 000000000000..d7416311ccf5 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http/SessionHandlerDocs.java @@ -0,0 +1,65 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.server.http; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; +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.server.Session; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.session.SessionHandler; +import org.eclipse.jetty.util.Callback; + +@SuppressWarnings("unused") +public class SessionHandlerDocs +{ + public void setupSession() throws Exception + { + // tag::session[] + class MyAppHandler extends Handler.Abstract.NonBlocking + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Your web application implemented here. + + // You can access the HTTP session. + Session session = request.getSession(false); + + return true; + } + } + + Server server = new Server(); + Connector connector = new ServerConnector(server); + server.addConnector(connector); + + // Create a ContextHandler with contextPath. + ContextHandler contextHandler = new ContextHandler("/myApp"); + server.setHandler(contextHandler); + + // Create and link the SessionHandler. + SessionHandler sessionHandler = new SessionHandler(); + contextHandler.setHandler(sessionHandler); + + // Link your web application Handler. + sessionHandler.setHandler(new MyAppHandler()); + + server.start(); + // end::session[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java new file mode 100644 index 000000000000..749602bd8d06 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java @@ -0,0 +1,349 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.server.http2; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.ErrorCode; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.api.server.ServerSessionListener; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.http2.frames.PushPromiseFrame; +import org.eclipse.jetty.http2.frames.ResetFrame; +import org.eclipse.jetty.http2.frames.SettingsFrame; +import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.resource.ResourceFactory; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class HTTP2ServerDocs +{ + public void setup() throws Exception + { + // tag::setup[] + // Create a Server instance. + Server server = new Server(); + + ServerSessionListener sessionListener = new ServerSessionListener() {}; + + // Create a ServerConnector with RawHTTP2ServerConnectionFactory. + RawHTTP2ServerConnectionFactory http2 = new RawHTTP2ServerConnectionFactory(sessionListener); + + // Configure RawHTTP2ServerConnectionFactory, for example: + + // Configure the max number of concurrent requests. + http2.setMaxConcurrentStreams(128); + // Enable support for CONNECT. + http2.setConnectProtocolEnabled(true); + + // Create the ServerConnector. + ServerConnector connector = new ServerConnector(server, http2); + + // Add the Connector to the Server + server.addConnector(connector); + + // Start the Server so it starts accepting connections from clients. + server.start(); + // end::setup[] + } + + public void accept() + { + // tag::accept[] + ServerSessionListener sessionListener = new ServerSessionListener() + { + @Override + public void onAccept(Session session) + { + SocketAddress remoteAddress = session.getRemoteSocketAddress(); + System.getLogger("http2").log(INFO, "Connection from {0}", remoteAddress); + } + }; + // end::accept[] + } + + public void preface() + { + // tag::preface[] + ServerSessionListener sessionListener = new ServerSessionListener() + { + @Override + public Map onPreface(Session session) + { + // Customize the settings, for example: + Map settings = new HashMap<>(); + + // Tell the client that HTTP/2 push is disabled. + settings.put(SettingsFrame.ENABLE_PUSH, 0); + + return settings; + } + }; + // end::preface[] + } + + public void request() + { + // tag::request[] + ServerSessionListener sessionListener = new ServerSessionListener() + { + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + // This is the "new stream" event, so it's guaranteed to be a request. + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + + // Return a Stream.Listener to handle the request events, + // for example request content events or a request reset. + return new Stream.Listener() + { + // Override callback methods for events you are interested in. + }; + } + }; + // end::request[] + } + + public void requestContent() + { + // tag::requestContent[] + ServerSessionListener sessionListener = new ServerSessionListener() + { + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + + // Demand for request data content. + stream.demand(); + + // Return a Stream.Listener to handle the request events. + return new Stream.Listener() + { + @Override + public void onDataAvailable(Stream stream) + { + Stream.Data data = stream.readData(); + + if (data == null) + { + stream.demand(); + return; + } + + // Get the content buffer. + ByteBuffer buffer = data.frame().getByteBuffer(); + + // Consume the buffer, here - as an example - just log it. + System.getLogger("http2").log(INFO, "Consuming buffer {0}", buffer); + + // Tell the implementation that the buffer has been consumed. + data.release(); + + if (!data.frame().isEndStream()) + { + // Demand more DATA frames when they are available. + stream.demand(); + } + } + }; + } + }; + // end::requestContent[] + } + + public void response() + { + // tag::response[] + ServerSessionListener sessionListener = new ServerSessionListener() + { + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + // Send a response after reading the request. + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + if (frame.isEndStream()) + { + respond(stream, request); + return null; + } + else + { + // Demand for request data. + stream.demand(); + + // Return a listener to handle the request events. + return new Stream.Listener() + { + @Override + public void onDataAvailable(Stream stream) + { + Stream.Data data = stream.readData(); + + if (data == null) + { + stream.demand(); + return; + } + + // Consume the request content. + data.release(); + + if (data.frame().isEndStream()) + respond(stream, request); + else + stream.demand(); + } + }; + } + } + + private void respond(Stream stream, MetaData.Request request) + { + // Prepare the response HEADERS frame. + + // The response HTTP status and HTTP headers. + MetaData.Response response = new MetaData.Response(HttpStatus.OK_200, null, HttpVersion.HTTP_2, HttpFields.EMPTY); + + if (HttpMethod.GET.is(request.getMethod())) + { + // The response content. + ByteBuffer resourceBytes = getResourceBytes(request); + + // Send the HEADERS frame with the response status and headers, + // and a DATA frame with the response content bytes. + stream.headers(new HeadersFrame(stream.getId(), response, null, false)) + .thenCompose(s -> s.data(new DataFrame(s.getId(), resourceBytes, true))); + } + else + { + // Send just the HEADERS frame with the response status and headers. + stream.headers(new HeadersFrame(stream.getId(), response, null, true)); + } + } + // tag::exclude[] + + private ByteBuffer getResourceBytes(MetaData.Request request) + { + return ByteBuffer.allocate(1024); + } + // end::exclude[] + }; + // end::response[] + } + + public void reset() + { + float maxRequestRate = 0F; + // tag::reset[] + ServerSessionListener sessionListener = new ServerSessionListener() + { + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + float requestRate = calculateRequestRate(); + + if (requestRate > maxRequestRate) + { + stream.reset(new ResetFrame(stream.getId(), ErrorCode.REFUSED_STREAM_ERROR.code), Callback.NOOP); + return null; + } + else + { + // The request is accepted. + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + // Return a Stream.Listener to handle the request events. + return new Stream.Listener() + { + // Override callback methods for events you are interested in. + }; + } + } + // tag::exclude[] + + private float calculateRequestRate() + { + return 0F; + } + // end::exclude[] + }; + // end::reset[] + } + + public void push() throws Exception + { + // tag::push[] + // The favicon bytes. + ByteBuffer faviconBuffer = BufferUtil.toBuffer(ResourceFactory.root().newResource("/path/to/favicon.ico"), true); + + ServerSessionListener sessionListener = new ServerSessionListener() + { + // By default, push is enabled. + private boolean pushEnabled = true; + + @Override + public void onSettings(Session session, SettingsFrame frame) + { + // Check whether the client sent an ENABLE_PUSH setting. + Map settings = frame.getSettings(); + Integer enablePush = settings.get(SettingsFrame.ENABLE_PUSH); + if (enablePush != null) + pushEnabled = enablePush == 1; + } + + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + if (pushEnabled && request.getHttpURI().toString().endsWith("/index.html")) + { + // Push the favicon. + HttpURI pushedURI = HttpURI.build(request.getHttpURI()).path("/favicon.ico"); + MetaData.Request pushedRequest = new MetaData.Request("GET", pushedURI, HttpVersion.HTTP_2, HttpFields.EMPTY); + PushPromiseFrame promiseFrame = new PushPromiseFrame(stream.getId(), 0, pushedRequest); + stream.push(promiseFrame, null) + .thenCompose(pushedStream -> + { + // Send the favicon "response". + MetaData.Response pushedResponse = new MetaData.Response(HttpStatus.OK_200, null, HttpVersion.HTTP_2, HttpFields.EMPTY); + return pushedStream.headers(new HeadersFrame(pushedStream.getId(), pushedResponse, null, false)) + .thenCompose(pushed -> pushed.data(new DataFrame(pushed.getId(), faviconBuffer.slice(), true))); + }); + } + // Return a Stream.Listener to handle the request events. + return new Stream.Listener() + { + // Override callback methods for events you are interested in. + }; + } + }; + // end::push[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java new file mode 100644 index 000000000000..f93598dd2acd --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java @@ -0,0 +1,341 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.server.http3; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.RejectedExecutionException; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http3.HTTP3ErrorCode; +import org.eclipse.jetty.http3.api.Session; +import org.eclipse.jetty.http3.api.Stream; +import org.eclipse.jetty.http3.frames.DataFrame; +import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.http3.server.RawHTTP3ServerConnectionFactory; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.quic.server.ServerQuicConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import static java.lang.System.Logger.Level.INFO; + +@SuppressWarnings("unused") +public class HTTP3ServerDocs +{ + public void setup() throws Exception + { + // tag::setup[] + // Create a Server instance. + Server server = new Server(); + + // HTTP/3 is always secure, so it always need a SslContextFactory. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + // The listener for session events. + Session.Server.Listener sessionListener = new Session.Server.Listener() {}; + + ServerQuicConfiguration quicConfiguration = new ServerQuicConfiguration(sslContextFactory, Path.of("/path/to/pem/dir")); + // Configure the max number of requests per QUIC connection. + quicConfiguration.setMaxBidirectionalRemoteStreams(1024); + + // Create and configure the RawHTTP3ServerConnectionFactory. + RawHTTP3ServerConnectionFactory http3 = new RawHTTP3ServerConnectionFactory(quicConfiguration, sessionListener); + http3.getHTTP3Configuration().setStreamIdleTimeout(15000); + + // Create and configure the QuicServerConnector. + QuicServerConnector connector = new QuicServerConnector(server, quicConfiguration, http3); + + // Add the Connector to the Server. + server.addConnector(connector); + + // Start the Server so it starts accepting connections from clients. + server.start(); + // end::setup[] + } + + public void accept() + { + // tag::accept[] + Session.Server.Listener sessionListener = new Session.Server.Listener() + { + @Override + public void onAccept(Session session) + { + SocketAddress remoteAddress = session.getRemoteSocketAddress(); + System.getLogger("http3").log(INFO, "Connection from {0}", remoteAddress); + } + }; + // end::accept[] + } + + public void preface() + { + // tag::preface[] + Session.Server.Listener sessionListener = new Session.Server.Listener() + { + @Override + public Map onPreface(Session session) + { + Map settings = new HashMap<>(); + + // Customize the settings + + return settings; + } + }; + // end::preface[] + } + + public void request() + { + // tag::request[] + Session.Server.Listener sessionListener = new Session.Server.Listener() + { + @Override + public Stream.Server.Listener onRequest(Stream.Server stream, HeadersFrame frame) + { + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + + // Return a Stream.Server.Listener to handle the request events, + // for example request content events or a request reset. + return new Stream.Server.Listener() {}; + } + }; + // end::request[] + } + + public void requestContent() + { + // tag::requestContent[] + Session.Server.Listener sessionListener = new Session.Server.Listener() + { + @Override + public Stream.Server.Listener onRequest(Stream.Server stream, HeadersFrame frame) + { + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + + // Demand to be called back when data is available. + stream.demand(); + + // Return a Stream.Server.Listener to handle the request content. + return new Stream.Server.Listener() + { + @Override + public void onDataAvailable(Stream.Server stream) + { + // Read a chunk of the request content. + Stream.Data data = stream.readData(); + + if (data == null) + { + // No data available now, demand to be called back. + stream.demand(); + } + else + { + // Get the content buffer. + ByteBuffer buffer = data.getByteBuffer(); + + // Consume the buffer, here - as an example - just log it. + System.getLogger("http3").log(INFO, "Consuming buffer {0}", buffer); + + // Tell the implementation that the buffer has been consumed. + data.release(); + + if (!data.isLast()) + { + // Demand to be called back. + stream.demand(); + } + } + } + }; + } + }; + // end::requestContent[] + } + + public void response() + { + // tag::response[] + Session.Server.Listener sessionListener = new Session.Server.Listener() + { + @Override + public Stream.Server.Listener onRequest(Stream.Server stream, HeadersFrame frame) + { + // Send a response after reading the request. + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + if (frame.isLast()) + { + respond(stream, request); + return null; + } + else + { + // Demand to be called back when data is available. + stream.demand(); + return new Stream.Server.Listener() + { + @Override + public void onDataAvailable(Stream.Server stream) + { + Stream.Data data = stream.readData(); + if (data == null) + { + stream.demand(); + } + else + { + // Consume the request content. + data.release(); + + if (data.isLast()) + respond(stream, request); + else + stream.demand(); + } + } + }; + } + } + + private void respond(Stream.Server stream, MetaData.Request request) + { + // Prepare the response HEADERS frame. + + // The response HTTP status and HTTP headers. + MetaData.Response response = new MetaData.Response(HttpStatus.OK_200, null, HttpVersion.HTTP_3, HttpFields.EMPTY); + + if (HttpMethod.GET.is(request.getMethod())) + { + // The response content. + ByteBuffer resourceBytes = getResourceBytes(request); + + // Send the HEADERS frame with the response status and headers, + // and a DATA frame with the response content bytes. + stream.respond(new HeadersFrame(response, false)) + .thenCompose(s -> s.data(new DataFrame(resourceBytes, true))); + } + else + { + // Send just the HEADERS frame with the response status and headers. + stream.respond(new HeadersFrame(response, true)); + } + } + // tag::exclude[] + + private ByteBuffer getResourceBytes(MetaData.Request request) + { + return ByteBuffer.allocate(1024); + } + // end::exclude[] + }; + // end::response[] + } + + public void reset() + { + float maxRequestRate = 0F; + // tag::reset[] + Session.Server.Listener sessionListener = new Session.Server.Listener() + { + @Override + public Stream.Server.Listener onRequest(Stream.Server stream, HeadersFrame frame) + { + float requestRate = calculateRequestRate(); + + if (requestRate > maxRequestRate) + { + stream.reset(HTTP3ErrorCode.REQUEST_REJECTED_ERROR.code(), new RejectedExecutionException()); + return null; + } + else + { + // The request is accepted. + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + // Return a Stream.Listener to handle the request events. + return new Stream.Server.Listener() {}; + } + } + // tag::exclude[] + + private float calculateRequestRate() + { + return 0F; + } + // end::exclude[] + }; + // end::reset[] + } + + // TODO: push not yet implemented in HTTP/3. +/* + public void push() throws Exception + { + // tag::push[] + // The favicon bytes. + ByteBuffer faviconBuffer = BufferUtil.toBuffer(server.getDefaultFavicon(), true); + + ServerSessionListener sessionListener = new ServerSessionListener() + { + // By default, push is enabled. + private boolean pushEnabled = true; + + @Override + public void onSettings(Session session, SettingsFrame frame) + { + // Check whether the client sent an ENABLE_PUSH setting. + Map settings = frame.getSettings(); + Integer enablePush = settings.get(SettingsFrame.ENABLE_PUSH); + if (enablePush != null) + pushEnabled = enablePush == 1; + } + + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + MetaData.Request request = (MetaData.Request)frame.getMetaData(); + if (pushEnabled && request.getURIString().endsWith("/index.html")) + { + // Push the favicon. + HttpURI pushedURI = HttpURI.build(request.getURI()).path("/favicon.ico"); + MetaData.Request pushedRequest = new MetaData.Request("GET", pushedURI, HttpVersion.HTTP_2, HttpFields.EMPTY); + PushPromiseFrame promiseFrame = new PushPromiseFrame(stream.getId(), 0, pushedRequest); + stream.push(promiseFrame, null) + .thenCompose(pushedStream -> + { + // Send the favicon "response". + MetaData.Response pushedResponse = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, HttpFields.EMPTY); + return pushedStream.headers(new HeadersFrame(pushedStream.getId(), pushedResponse, null, false)) + .thenCompose(pushed -> pushed.data(new DataFrame(pushed.getId(), faviconBuffer, true))); + }); + } + // Return a Stream.Listener to handle the request events. + return new Stream.Listener(); + } + }; + // end::push[] + } + */ +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java new file mode 100644 index 000000000000..5f6da14053d5 --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java @@ -0,0 +1,465 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.server.session; + +import java.io.File; +import java.io.FileInputStream; +import java.net.InetSocketAddress; +import java.util.Properties; + +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.gcloud.session.GCloudSessionDataStoreFactory; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.memcached.session.MemcachedSessionDataMapFactory; +import org.eclipse.jetty.nosql.mongodb.MongoSessionDataStoreFactory; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.Session; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.session.CachingSessionDataStoreFactory; +import org.eclipse.jetty.session.DatabaseAdaptor; +import org.eclipse.jetty.session.DefaultSessionCache; +import org.eclipse.jetty.session.DefaultSessionCacheFactory; +import org.eclipse.jetty.session.DefaultSessionIdManager; +import org.eclipse.jetty.session.FileSessionDataStore; +import org.eclipse.jetty.session.FileSessionDataStoreFactory; +import org.eclipse.jetty.session.HouseKeeper; +import org.eclipse.jetty.session.NullSessionCache; +import org.eclipse.jetty.session.NullSessionCacheFactory; +import org.eclipse.jetty.session.NullSessionDataStore; +import org.eclipse.jetty.session.SessionCache; +import org.eclipse.jetty.session.SessionHandler; +import org.eclipse.jetty.session.infinispan.InfinispanSessionData; +import org.eclipse.jetty.session.infinispan.InfinispanSessionDataStore; +import org.eclipse.jetty.util.Callback; +import org.infinispan.Cache; +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.RemoteCacheManager; +import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; +import org.infinispan.commons.marshall.ProtoStreamMarshaller; +import org.infinispan.manager.DefaultCacheManager; + +@SuppressWarnings("unused") +public class SessionDocs +{ + public void cachingSessionDataStore() + { + //tag::cachingsds[] + Server server = new Server(); + + //Make a factory for memcached L2 caches for SessionData + MemcachedSessionDataMapFactory mapFactory = new MemcachedSessionDataMapFactory(); + mapFactory.setExpirySec(0); //items in memcached don't expire + mapFactory.setHeartbeats(true); //tell memcached to use heartbeats + mapFactory.setAddresses(new InetSocketAddress("localhost", 11211)); //use a local memcached instance + mapFactory.setWeights(new int[]{100}); //set the weighting + + //Make a FileSessionDataStoreFactory for creating FileSessionDataStores + //to persist the session data + FileSessionDataStoreFactory storeFactory = new FileSessionDataStoreFactory(); + storeFactory.setStoreDir(new File("/tmp/sessions")); + storeFactory.setGracePeriodSec(3600); + storeFactory.setSavePeriodSec(0); + + //Make a factory that plugs the L2 cache into the SessionDataStore + CachingSessionDataStoreFactory cachingSessionDataStoreFactory = new CachingSessionDataStoreFactory(); + cachingSessionDataStoreFactory.setSessionDataMapFactory(mapFactory); + cachingSessionDataStoreFactory.setSessionStoreFactory(storeFactory); + + //Register it as a bean so that all SessionManagers will use it + //to make FileSessionDataStores that use memcached as an L2 SessionData cache. + server.addBean(cachingSessionDataStoreFactory); + //end::cachingsds[] + } + + public void coreSessionHandler() + { + try + { + //tag:coresession[] + Server server = new Server(); + org.eclipse.jetty.session.SessionHandler sessionHandler = new org.eclipse.jetty.session.SessionHandler(); + sessionHandler.setSessionCookie("SIMPLE"); + sessionHandler.setUsingCookies(true); + sessionHandler.setUsingURLs(false); + sessionHandler.setSessionPath("/"); + server.setHandler(sessionHandler); + sessionHandler.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + Session session = request.getSession(false); + Content.Sink.write(response, true, "Session=" + session.getId(), callback); + return true; + } + }); + //end::coresession[] + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void defaultSessionCache() + { + //tag::defaultsessioncache[] + Server server = new Server(); + + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + //EVICT_ON_INACTIVE: evict a session after 60sec inactivity + cacheFactory.setEvictionPolicy(60); + //Only useful with the EVICT_ON_INACTIVE policy + cacheFactory.setSaveOnInactiveEviction(true); + cacheFactory.setFlushOnResponseCommit(true); + cacheFactory.setInvalidateOnShutdown(false); + cacheFactory.setRemoveUnloadableSessions(true); + cacheFactory.setSaveOnCreate(true); + + //Add the factory as a bean to the server, now whenever a + //SessionManager starts it will consult the bean to create a new DefaultSessionCache + server.addBean(cacheFactory); + //end::defaultsessioncache[] + } + + public void defaultSessionIdManagerWithHouseKeeper() + { + try + { + //tag::housekeeper[] + Server server = new Server(); + DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server); + idMgr.setWorkerName("server7"); + server.addBean(idMgr, true); + + HouseKeeper houseKeeper = new HouseKeeper(); + houseKeeper.setSessionIdManager(idMgr); + //set the frequency of scavenge cycles + houseKeeper.setIntervalSec(600L); + idMgr.setSessionHouseKeeper(houseKeeper); + //end::housekeeper[] + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void fileSessionDataStore() + { + //tag::filesessiondatastore[] + + //create a context + WebAppContext app1 = new WebAppContext(); + app1.setContextPath("/app1"); + + //First, we create a DefaultSessionCache + DefaultSessionCache cache = new DefaultSessionCache(app1.getSessionHandler()); + cache.setEvictionPolicy(SessionCache.NEVER_EVICT); + cache.setFlushOnResponseCommit(true); + cache.setInvalidateOnShutdown(false); + cache.setRemoveUnloadableSessions(true); + cache.setSaveOnCreate(true); + + //Now, we configure a FileSessionDataStore + FileSessionDataStore store = new FileSessionDataStore(); + store.setStoreDir(new File("/tmp/sessions")); + store.setGracePeriodSec(3600); + store.setSavePeriodSec(0); + + //Tell the cache to use the store + cache.setSessionDataStore(store); + + //Tell the context to use the cache/store combination + app1.getSessionHandler().setSessionCache(cache); + + //end::filesessiondatastore[] + } + + public void fileSessionDataStoreFactory() + { + //tag::filesessiondatastorefactory[] + Server server = new Server(); + + //First lets configure a DefaultSessionCacheFactory + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + //NEVER_EVICT + cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); + cacheFactory.setFlushOnResponseCommit(true); + cacheFactory.setInvalidateOnShutdown(false); + cacheFactory.setRemoveUnloadableSessions(true); + cacheFactory.setSaveOnCreate(true); + + //Add the factory as a bean to the server, now whenever a + //SessionManager starts it will consult the bean to create a new DefaultSessionCache + server.addBean(cacheFactory); + + //Now, lets configure a FileSessionDataStoreFactory + FileSessionDataStoreFactory storeFactory = new FileSessionDataStoreFactory(); + storeFactory.setStoreDir(new File("/tmp/sessions")); + storeFactory.setGracePeriodSec(3600); + storeFactory.setSavePeriodSec(0); + + //Add the factory as a bean on the server, now whenever a + //SessionManager starts, it will consult the bean to create a new FileSessionDataStore + //for use by the DefaultSessionCache + server.addBean(storeFactory); + //end::filesessiondatastorefactory[] + } + + public void jdbcSessionDataStore() + { + //tag::dbaDatasource[] + DatabaseAdaptor datasourceAdaptor = new DatabaseAdaptor(); + datasourceAdaptor.setDatasourceName("/jdbc/myDS"); + //end::dbaDatasource[] + + //tag::dbaDriver[] + DatabaseAdaptor driverAdaptor = new DatabaseAdaptor(); + driverAdaptor.setDriverInfo("com.mysql.jdbc.Driver", "jdbc:mysql://127.0.0.1:3306/sessions?user=sessionsadmin"); + //end::dbaDriver[] + } + + public void minimumDefaultSessionIdManager() + { + //tag::default[] + Server server = new Server(); + DefaultSessionIdManager idMgr = new DefaultSessionIdManager(server); + //you must set the workerName unless you set the env viable JETTY_WORKER_NAME + idMgr.setWorkerName("server3"); + server.addBean(idMgr, true); + //end::default[] + } + + public void mixedSessionCache() + { + //tag::mixedsessioncache[] + Server server = new Server(); + + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + //NEVER_EVICT + cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); + cacheFactory.setFlushOnResponseCommit(true); + cacheFactory.setInvalidateOnShutdown(false); + cacheFactory.setRemoveUnloadableSessions(true); + cacheFactory.setSaveOnCreate(true); + + //Add the factory as a bean to the server, now whenever a + //SessionManager starts it will consult the bean to create a new DefaultSessionCache + server.addBean(cacheFactory); + + ContextHandlerCollection contexts = new ContextHandlerCollection(); + server.setHandler(contexts); + + //Add a webapp that will use a DefaultSessionCache via the DefaultSessionCacheFactory + WebAppContext app1 = new WebAppContext(); + app1.setContextPath("/app1"); + contexts.addHandler(app1); + + //Add a webapp that uses an explicit NullSessionCache instead + WebAppContext app2 = new WebAppContext(); + app2.setContextPath("/app2"); + NullSessionCache nullSessionCache = new NullSessionCache(app2.getSessionHandler()); + nullSessionCache.setFlushOnResponseCommit(true); + nullSessionCache.setRemoveUnloadableSessions(true); + nullSessionCache.setSaveOnCreate(true); + //If we pass an existing SessionCache instance to the SessionHandler, it must be + //fully configured: this means we must also provide SessionDataStore + nullSessionCache.setSessionDataStore(new NullSessionDataStore()); + app2.getSessionHandler().setSessionCache(nullSessionCache); + //end::mixedsessioncache[] + } + + public void mongoSessionDataStore() + { + //tag::mongosdfactory[] + Server server = new Server(); + + MongoSessionDataStoreFactory mongoSessionDataStoreFactory = new MongoSessionDataStoreFactory(); + mongoSessionDataStoreFactory.setGracePeriodSec(3600); + mongoSessionDataStoreFactory.setSavePeriodSec(0); + mongoSessionDataStoreFactory.setDbName("HttpSessions"); + mongoSessionDataStoreFactory.setCollectionName("JettySessions"); + + // Either set the connectionString + mongoSessionDataStoreFactory.setConnectionString("mongodb:://localhost:27017"); + // or alternatively set the host and port. + mongoSessionDataStoreFactory.setHost("localhost"); + mongoSessionDataStoreFactory.setPort(27017); + //end::mongosdfactory[] + } + + public void infinispanEmbedded() + { + try + { + //tag::infinispanembed[] + /* Create a core SessionHandler + * Alternatively in a Servlet Environment do: + * WebAppContext webapp = new WebAppContext(); + * SessionHandler sessionHandler = webapp.getSessionHandler(); + */ + SessionHandler sessionHandler = new SessionHandler(); + + //Use an Infinispan local cache configured via an infinispan xml file + DefaultCacheManager defaultCacheManager = new DefaultCacheManager("path/to/infinispan.xml"); + Cache localCache = defaultCacheManager.getCache(); + + //Configure the Jetty session datastore with Infinispan + InfinispanSessionDataStore infinispanSessionDataStore = new InfinispanSessionDataStore(); + infinispanSessionDataStore.setCache(localCache); + infinispanSessionDataStore.setSerialization(false); //local cache does not serialize session data + infinispanSessionDataStore.setInfinispanIdleTimeoutSec(0); //do not use infinispan auto delete of unused sessions + infinispanSessionDataStore.setQueryManager(new org.eclipse.jetty.session.infinispan.EmbeddedQueryManager(localCache)); //enable Jetty session scavenging + infinispanSessionDataStore.setGracePeriodSec(3600); + infinispanSessionDataStore.setSavePeriodSec(0); + + //Configure a SessionHandler to use the local Infinispan cache as a store of SessionData + DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler); + sessionCache.setSessionDataStore(infinispanSessionDataStore); + sessionHandler.setSessionCache(sessionCache); + + //end::infinispanembed[] + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void infinispanRemote() + { + try + { + //tag::infinispanremote[] + /* Create a core SessionHandler + * Alternatively in a Servlet Environment do: + * WebAppContext webapp = new WebAppContext(); + * SessionHandler sessionHandler = webapp.getSessionHandler(); + */ + SessionHandler sessionHandler = new SessionHandler(); + + //Configure Infinispan to provide a remote cache called "JettySessions" + Properties hotrodProperties = new Properties(); + hotrodProperties.load(new FileInputStream("/path/to/hotrod-client.properties")); + org.infinispan.client.hotrod.configuration.ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.withProperties(hotrodProperties); + configurationBuilder.marshaller(new ProtoStreamMarshaller()); + configurationBuilder.addContextInitializer(new org.eclipse.jetty.session.infinispan.InfinispanSerializationContextInitializer()); + org.infinispan.client.hotrod.RemoteCacheManager remoteCacheManager = new RemoteCacheManager(configurationBuilder.build()); + RemoteCache remoteCache = remoteCacheManager.getCache("JettySessions"); + + //Configure the Jetty session datastore with Infinispan + InfinispanSessionDataStore infinispanSessionDataStore = new InfinispanSessionDataStore(); + infinispanSessionDataStore.setCache(remoteCache); + infinispanSessionDataStore.setSerialization(true); //remote cache serializes session data + infinispanSessionDataStore.setInfinispanIdleTimeoutSec(0); //do not use infinispan auto delete of unused sessions + infinispanSessionDataStore.setQueryManager(new org.eclipse.jetty.session.infinispan.RemoteQueryManager(remoteCache)); //enable Jetty session scavenging + infinispanSessionDataStore.setGracePeriodSec(3600); + infinispanSessionDataStore.setSavePeriodSec(0); + + //Configure a SessionHandler to use a remote Infinispan cache as a store of SessionData + DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler); + sessionCache.setSessionDataStore(infinispanSessionDataStore); + sessionHandler.setSessionCache(sessionCache); + //end::infinispanremote[] + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void gcloudSessionDataStore() + { + try + { + //tag::gcloudsessiondatastorefactory[] + Server server = new Server(); + + //Ensure there is a SessionCacheFactory + DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); + + //Add the factory as a bean to the server, now whenever a + //SessionManager starts it will consult the bean to create a new DefaultSessionCache + server.addBean(cacheFactory); + + //Configure the GCloudSessionDataStoreFactory + GCloudSessionDataStoreFactory storeFactory = new GCloudSessionDataStoreFactory(); + storeFactory.setGracePeriodSec(3600); + storeFactory.setSavePeriodSec(0); + storeFactory.setBackoffMs(2000); //increase the time between retries of failed writes + storeFactory.setMaxRetries(10); //increase the number of retries of failed writes + + //Add the factory as a bean on the server, now whenever a + //SessionManager starts, it will consult the bean to create a new GCloudSessionDataStore + //for use by the DefaultSessionCache + server.addBean(storeFactory); + //end::gcloudsessiondatastorefactory[] + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void nullSessionCache() + { + //tag::nullsessioncache[] + Server server = new Server(); + NullSessionCacheFactory cacheFactory = new NullSessionCacheFactory(); + cacheFactory.setFlushOnResponseCommit(true); + cacheFactory.setRemoveUnloadableSessions(true); + cacheFactory.setSaveOnCreate(true); + + //Add the factory as a bean to the server, now whenever a + //SessionManager starts it will consult the bean to create a new NullSessionCache + server.addBean(cacheFactory); + //end::nullsessioncache[] + } + + public void servletContextWithSessionHandler() + { + //tag:schsession[] + Server server = new Server(); + + ServletContextHandler context = new ServletContextHandler("/foo", ServletContextHandler.SESSIONS); + //make idle sessions valid for only 5mins + context.getSessionHandler().setMaxInactiveInterval(300); + //turn off use of cookies + context.getSessionHandler().setUsingCookies(false); + + server.setHandler(context); + //end::schsession[] + } + + public void webAppWithSessionHandler() + { + //tag:wacsession[] + Server server = new Server(); + + WebAppContext context = new WebAppContext(); + //make idle sessions valid for only 5mins + context.getSessionHandler().setMaxInactiveInterval(300); + //turn off use of cookies + context.getSessionHandler().setUsingCookies(false); + + server.setHandler(context); + //end::wacsession[] + } +} diff --git a/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java new file mode 100644 index 000000000000..c50d54520e8e --- /dev/null +++ b/documentation/jetty/modules/code/examples/src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java @@ -0,0 +1,312 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.docs.programming.server.websocket; + +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.server.ServerContainer; +import jakarta.websocket.server.ServerEndpoint; +import jakarta.websocket.server.ServerEndpointConfig; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; +import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler; + +@SuppressWarnings("unused") +public class WebSocketServerDocs +{ + public void standardContainerWebAppContext() throws Exception + { + // tag::standardContainerWebAppContext[] + // Create a Server with a ServerConnector listening on port 8080. + Server server = new Server(8080); + + // Create a WebAppContext with the given context path. + WebAppContext handler = new WebAppContext("/path/to/webapp", "/ctx"); + server.setHandler(handler); + + // Starting the Server will start the WebAppContext. + server.start(); + // end::standardContainerWebAppContext[] + } + + public void standardContainerServletContextHandler() throws Exception + { + // tag::standardContainerServletContextHandler[] + // Create a Server with a ServerConnector listening on port 8080. + Server server = new Server(8080); + + // Create a ServletContextHandler with the given context path. + ServletContextHandler handler = new ServletContextHandler("/ctx"); + server.setHandler(handler); + + // Ensure that JavaxWebSocketServletContainerInitializer is initialized, + // to setup the ServerContainer for this web application context. + JakartaWebSocketServletContainerInitializer.configure(handler, null); + + // Starting the Server will start the ServletContextHandler. + server.start(); + // end::standardContainerServletContextHandler[] + } + + public void standardEndpointsInitialization() throws Exception + { + // tag::standardEndpointsInitialization[] + // Create a Server with a ServerConnector listening on port 8080. + Server server = new Server(8080); + + // Create a ServletContextHandler with the given context path. + ServletContextHandler handler = new ServletContextHandler("/ctx"); + server.setHandler(handler); + + // Ensure that JavaxWebSocketServletContainerInitializer is initialized, + // to setup the ServerContainer for this web application context. + JakartaWebSocketServletContainerInitializer.configure(handler, null); + + // Add a WebSocket-initializer Servlet to register WebSocket endpoints. + handler.addServlet(MyJavaxWebSocketInitializerServlet.class, "/*"); + + // Starting the Server will start the ServletContextHandler. + server.start(); + // end::standardEndpointsInitialization[] + } + + @SuppressWarnings("InnerClassMayBeStatic") + // tag::standardWebSocketInitializerServlet[] + public class MyJavaxWebSocketInitializerServlet extends HttpServlet + { + @Override + public void init() throws ServletException + { + try + { + // Retrieve the ServerContainer from the ServletContext attributes. + ServerContainer container = (ServerContainer)getServletContext().getAttribute(ServerContainer.class.getName()); + + // Configure the ServerContainer. + container.setDefaultMaxTextMessageBufferSize(128 * 1024); + + // Simple registration of your WebSocket endpoints. + container.addEndpoint(MyJavaxWebSocketEndPoint.class); + + // Advanced registration of your WebSocket endpoints. + container.addEndpoint( + ServerEndpointConfig.Builder.create(MyJavaxWebSocketEndPoint.class, "/ws") + .subprotocols(List.of("my-ws-protocol")) + .build() + ); + } + catch (DeploymentException x) + { + throw new ServletException(x); + } + } + } + // end::standardWebSocketInitializerServlet[] + + public void standardContainerAndEndpoints() throws Exception + { + // tag::standardContainerAndEndpoints[] + // Create a Server with a ServerConnector listening on port 8080. + Server server = new Server(8080); + + // Create a ServletContextHandler with the given context path. + ServletContextHandler handler = new ServletContextHandler("/ctx"); + server.setHandler(handler); + + // Setup the ServerContainer and the WebSocket endpoints for this web application context. + JakartaWebSocketServletContainerInitializer.configure(handler, (servletContext, container) -> + { + // Configure the ServerContainer. + container.setDefaultMaxTextMessageBufferSize(128 * 1024); + + // Simple registration of your WebSocket endpoints. + container.addEndpoint(MyJavaxWebSocketEndPoint.class); + + // Advanced registration of your WebSocket endpoints. + container.addEndpoint( + ServerEndpointConfig.Builder.create(MyJavaxWebSocketEndPoint.class, "/ws") + .subprotocols(List.of("my-ws-protocol")) + .build() + ); + }); + + // Starting the Server will start the ServletContextHandler. + server.start(); + // end::standardContainerAndEndpoints[] + } + + public void jettyContainerWithUpgradeHandler() throws Exception + { + // tag::jettyContainerWithUpgradeHandler[] + // Create a Server with a ServerConnector listening on port 8080. + Server server = new Server(8080); + + // Create a ContextHandler with the given context path. + ContextHandler contextHandler = new ContextHandler("/ctx"); + server.setHandler(contextHandler); + + // Create a WebSocketUpgradeHandler that implicitly creates a ServerWebSocketContainer. + WebSocketUpgradeHandler webSocketHandler = WebSocketUpgradeHandler.from(server, contextHandler, container -> + { + // Configure the ServerWebSocketContainer. + container.setMaxTextMessageSize(128 * 1024); + + // Map a request URI to a WebSocket endpoint, for example using a regexp. + container.addMapping("regex|/ws/v\\d+/echo", (rq, rs, cb) -> new EchoEndPoint()); + + // Advanced registration of a WebSocket endpoint. + container.addMapping("/ws/adv", (rq, rs, cb) -> + { + List subProtocols = rq.getSubProtocols(); + if (subProtocols.contains("my-ws-protocol")) + return new MyJettyWebSocketEndPoint(); + return null; + }); + }); + contextHandler.setHandler(webSocketHandler); + + // Starting the Server will start the ContextHandler and the WebSocketUpgradeHandler, + // which would run the configuration of the ServerWebSocketContainer. + server.start(); + // end::jettyContainerWithUpgradeHandler[] + } + + private static class EchoEndPoint + { + } + + public void jettyContainerWithContainer() throws Exception + { + // tag::jettyContainerWithContainer[] + // Create a Server with a ServerConnector listening on port 8080. + Server server = new Server(8080); + + // Create a ContextHandler with the given context path. + ContextHandler contextHandler = new ContextHandler("/ctx"); + server.setHandler(contextHandler); + + // Create a ServerWebSocketContainer, which is also stored as an attribute in the context. + ServerWebSocketContainer container = ServerWebSocketContainer.ensure(server, contextHandler); + + // You can use WebSocketUpgradeHandler if you want, but it is not necessary. + // You can ignore the line below, it is shown only for reference. + WebSocketUpgradeHandler webSocketHandler = new WebSocketUpgradeHandler(container); + + // You can directly use ServerWebSocketContainer from any Handler. + contextHandler.setHandler(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + // Retrieve the ServerWebSocketContainer. + ServerWebSocketContainer container = ServerWebSocketContainer.get(request.getContext()); + + // Verify special conditions for which a request should be upgraded to WebSocket. + String pathInContext = Request.getPathInContext(request); + if (pathInContext.startsWith("/ws/echo") && request.getHeaders().contains("X-WS", "true")) + { + try + { + // This is a WebSocket upgrade request, perform a direct upgrade. + boolean upgraded = container.upgrade((rq, rs, cb) -> new EchoEndPoint(), request, response, callback); + if (upgraded) + return true; + // This was supposed to be a WebSocket upgrade request, but something went wrong. + Response.writeError(request, response, callback, HttpStatus.UPGRADE_REQUIRED_426); + return true; + } + catch (Exception x) + { + Response.writeError(request, response, callback, HttpStatus.UPGRADE_REQUIRED_426, "failed to upgrade", x); + return true; + } + } + else + { + // Handle a normal HTTP request. + response.setStatus(HttpStatus.OK_200); + callback.succeeded(); + return true; + } + } + }); + + // Starting the Server will start the ContextHandler. + server.start(); + // end::jettyContainerWithContainer[] + } + + @ServerEndpoint("/ws") + private static class MyJavaxWebSocketEndPoint + { + } + + @WebSocket + private static class MyJettyWebSocketEndPoint + { + } + + public void uriTemplatePathSpec() + { + // tag::uriTemplatePathSpec[] + Server server = new Server(8080); + + ContextHandler contextHandler = new ContextHandler("/ctx"); + server.setHandler(contextHandler); + + // Create a WebSocketUpgradeHandler. + WebSocketUpgradeHandler webSocketHandler = WebSocketUpgradeHandler.from(server, contextHandler, container -> + { + container.addMapping("/ws/chat/{room}", (upgradeRequest, upgradeResponse, callback) -> + { + // Retrieve the URI template. + UriTemplatePathSpec pathSpec = (UriTemplatePathSpec)upgradeRequest.getAttribute(PathSpec.class.getName()); + + // Match the URI template. + String pathInContext = Request.getPathInContext(upgradeRequest); + Map params = pathSpec.getPathParams(pathInContext); + String room = params.get("room"); + + // Create the new WebSocket endpoint with the URI template information. + return new MyWebSocketRoomEndPoint(room); + }); + }); + contextHandler.setHandler(webSocketHandler); + // end::uriTemplatePathSpec[] + } + + @WebSocket + private static class MyWebSocketRoomEndPoint + { + public MyWebSocketRoomEndPoint(String room) + { + } + } +} diff --git a/documentation/jetty/modules/operations-guide/images/jmc-server-dump.png b/documentation/jetty/modules/operations-guide/images/jmc-server-dump.png new file mode 100644 index 0000000000000000000000000000000000000000..33cd92938cbe75fb4cfec68b54ab6c1d940bc786 GIT binary patch literal 268442 zcmafa1yEc;^XCJCTS5pBoZtir!QI{6A-KCcNpK17?(Xgc5AF_&yZZtQNAmr@x~r?J zx~aFdZ)Zn(dV0Ehe$x{oFDr(Mgo^|K0IGzzup$7wAq4=Kdj#m$95csk$mk zz*M*Iw9O@_sHjLCXQ7BH>qnLoo0ek-A6E220IY%n2;@kZxms&F#=$hwbak4{B($@$ z14R>R1b3p_cGMr#d(Hi6qTb1B)pv%N?fq%wDb_1t6qgfCh)@x8r?}<=50l;k2qgY* z1&y=5O!5DFLr~#(5OW3oW+ISWBX^6#{O48qX5r%Eg0BVrKc%wuBGTM(qobp3*V@1& z?E;;YDFx~2>8L0ulG|Gq!G9|&L#R0u4Z}ph`1GCU-bO}7W@be{fBtN42H&0Nt4K;q zdp$oj$Hpohe0XNL2ZNmtXAZ>fa)t&Wo)FyDP1~JBzSFk^Vc_|IH4baOS^cyz)Z@ofP#STm0o` zd;Sl;7<;*Z1@7+&Dp^p&XOtIF{M3<9fS$M2{=xZK{n;kY6YJH~+2T*!PfDeM4du9+ z-ii^MV=+_EvL1)2IWZZR4om7Q6VpiqJDx0G z$!@f0hPVaWQ4P}FFwI^Fx$9m7>%0QE0zw?(dUxt!#cB1UaPf{vA?2&Fb3ZlTOHW;d>lRPe+^~?*o03p-D%~Xm&~>!GQO}Y&a_+5drB88i(RX#{Ax$pt96I->cXg$9N7%D6 zS(l}=L^kGzgzAf6Pq_*KD!z%(s|-Ya;+t&N)`*4+8b*>xn1UW6rnhj}Z{Rf-P`07Z zZaPdw_bZ27El%G@=vD*lTP>r~rl0Qe z0FsQ#iSNvVThm(mpjIK>jKD9vl-Q^U4ZKZLxg-NA=ur7yv=wzJ4S3t~phgZGDPLQ1 zGM~*<+QM^<){G(N>r*DuyMrv%e&Sw=%9^S0FfloL*JnO7p3{U=VdhW)RdX1bo;~O4 zn31SnteB69A{Ptt1A=`)sxT>9Hr}(*_aS#(P`}@2HqbIn4f-@Y3Xd;xFzM;x7^}-m z;~sT-O|9@iojEX{@C=k+WUOdMLIV^B4Ri~1NXy5X>{uE zZ6w4VMXk=%g?UuSo?L7LfkBy-UMC`IlMloz)TbufkV8gciQvK0LYLk1@;SzY@M*lc zJE(Bnd*-YmdzKBI3GjQ1AO*Qp9Rh%nYhKNqg&kO)9okf|)qI(VDTcI`3laCCXkt8g zV{)e!uW-$qJk6+E~R49xx;mSh)zpGrGv>kh!OSeF2Eo3ahM`9rN{6+Zz>J0 zp`AvTF-+kX?OdP3E*TQ@rQzDtM7q`OJNlrV3vw;|8lUwn)J0vXJ32O>M%&X(-X50I zOgv+WH5+5S2*g4lHVjFt9cJuOV`SDj5UP9W87uc5RofAeC@OMrd$puFAwT}`uVk2mpv93GMdbzSjAw#s|T zj>aeDVsS6yo3_+<{*+;i>^1f%-G)$+GVHXWI&KvsG>%%5ThMc3+D(r`!@@%o***LD zr4)_KH0t~*c9uP5uv`r7!HGpFoRQ0pJ}yeP%d0;hXi@K@QpP9iuW&l7w%X7r97;?L zenr@=+#M*P4HWqlB3jNxI8NpanS~j_o~3f^=w(t8IW~K+rYB@9ay7xB0>o%3Dr(MH zz_}-WILFlCQqrz3tTV6I9%$~67-DDlaQwNz5ZwlWkddXj%6MZ{XF2m@WJ}G#Spm0S zLED#s=hQpyN5gk7Grx&veF|04?XdxN8|~Ywu_zu&QaA`{TqfV5=(FgSL$i;MPdcYF zr^|6tVj?!M(&j!rJIg{$>^BVI82I&fL1ZqDJJYMF=1pqi>;4SUpPCXkLHjdi+FU={ z(BR4C5o~UVTvyLwz);h_DQLx<)iG}mB86ZO#8@2>u_ZgK%a|E05tmz}IDz-fNO2nD zqpEc;tx2grtu^!D^2Vi7f#K#T#T3K|$?FMc3F{x-!A3C#B4ylYLx zcBMo<$-5B{0oy#9Q@MAn`>k%{HJoJI=Tjo>ZI@5BBesL^gPt{QVwEy6r;d$uPIFlO zg=VwcfIOUx82u{SALX##mFtWSw{5EdwMS30_-97FZ+k4hX+exP7gI|dweVxbH&&LO z&H>gELaY#uotT3ap63&pB>!_1rrdD_jKb%b0P-74!H62^S?YpUQ_(j(O(n1f2XXP2h}YdN=gC-lgSDD zZoaNLg`9%%G$%ErEVA5qelYU2=T#2sCAr z@jJ$&q|ckfW1EOp5-C-}f=c|*&|T})aWW!>K@9`^9_BPV-?4p50p2RqZZV-d!uSro zT$8|}_QcECQ5LHOWHgw|NX3q;&8p>`Apq+`hwgHQ#)i1B3SN}}ECB`9#lGetd=;p)P*t)oHRW3KhmPx$hU3eGBDmR7dYipub}F@9a3At~67~q)=)8 z$2A*rcOZ%vDs@=grFi09-A~>koqcl?agZK@_&I@|r^#}_uS-ie9LK4?V$!5CfP#2T zwScFgXPsnhF%eBql7xS%0>3%xgfmaCoXA6`J&uGy%}HUS8j*xv2@MB*m8?-+Wvv-O zC5?)!wI|#d76#YKU^Y6_iAS%KB~!*1kqHT*7E|Su`{T87MsjMa%5TDf276bgKi(JH z>2G>B!*a54z*Q2}N-H2O ztdj&yRA!O!OL*>U#_Auq!3Mw5ocHYl?B67#kg!5-9NV`y=Q}i>L>!2-ujp zYwt>GdwXxMu&~fqrSjchA03bC^VDEr%;K%~@2oj69C+N7$b8?5!mhNsf4G4tO!wk6 z{Z%XRB8Q`>DwKaGgCSK^f7kQn-2H)S*rRK3&JMDkj3e>mEW1=WDB63pbKH;|;d@eD z8-~QtTNU2c$!;kW5=;>uZ8qA|PQ?7gd}kRMSZSmCM~u(Z;B!{RURD;SobNg2QIdHM z&=_fKS|9R0O~%h&8NL5q&WU7{`?1;e(imHYS_f4DhY_RTT5Ku`^=FXv<36|ZJordW zuJXEu%2zGlj1>=uTFFGK&BM@Jmz#(84~fZ(p3N_UIc~b90Hr?H51JrxB8>MKe35ZG zl)VKC526pu6BGn$+TPfBklmf5!K7uT6Z85&nZp_Z{SGfS+co@GgF2eZ7H0Bre~&`I zd40Jv{Na@TuP5(AX{^IT-cXg>q)$DM2Ae76Vr?PVgve}dZlE=ux+7TT)6($|XVcqYKfZ_3tj8M{lLdm8oOzlGrg<&iCWkQaXt` zY_rc_@3`rC&c@PI84?>9eD!^Px)6G>+2Xe^l-rEsW-?^Q^y+XqaEO*4L>1s-mgkpC zVDR>bcD3CzbZHY}JJdMh_!)HKy$A}Cfwt%y0boMEzoz|a_k%XG2uvz0Ep8kNlac+K z<+(+Hs*}@n@nR`DEI@n>b}j4!JNt6gcWb3iG~35ju6G!T3~F*aLEP@6%**VycAu@0 za8NC+Tb$BE<0i2HAm3zlx01yg?j~*LwAuRQXUJGN)pvd%**saALnxxJ%A%zS$7r?b z1MTs8H_TuM_PM+j$KcLYFS@44cuoP;$LK#${f$& zxw^dc^72BW3?4pp4*O^3mLi*n>HL_6u7z33 zEY1nKY|1HmA=}pG7OdaQgWwa#^?9`BiqEY@GFB+fzE~MF&P%z-q+WK2B^ETal!Xt9 zC3;o(U+|cOyUgbSBPp!4++JRHaFz;MmZ-sDBT+J|-;x*A?Z?;Hea1R0Wn!YT4-OTy zgrw5R-hh1Y4G#^T@}XkvU040YCLou=*fn6O(wcx zTzTn-=$$_hmFHH<7q1&WCVtnf5?E{6Z)tV(Dqba*8vCy%DE#L7VNxt?lk zYHDu0v5ywN_(Jh-5n+I?wJXZRNvXf=+p8N4s~-Bx=FV!6v85d4i4m>=?>~Gi27+Ss zsTP=OLlTHJlWsSat#J8|evpR*Tbf067=!iJBm_GY(p78G%E7~j!|o&l_A?;525GMu zmzDERiPmo~cP!qERCZ!OxoKUy<9>M6{P~M!BQ2=)a`-px9cLg?4HUgb(RQq@Ih1{M zF;?}SnnNyA&E4IfVrrm~7>8MB)RT0pI67RG>TT9e3agC_EI2>^Q+WZ0o^CWJ^Z0n_PM)$-da_e}T*#Qb!oQ`j2-n>3qdprU$3K4-8{l`-?Z~izSYoe_GqO z3M$gqJS_|2N(-#jOYLRoN`%v;RHPcz`9&18Hhb0f+So=z#6LLH(c(Gj)*C8%vjY2W zbXr909=~h8cNjPyxgg`ZFH+TBIUdS#I#8KTt@Qmw+p}2k9OK#Gsi8mxa+bq5QCjmf zr+?Q&euNDKHK$^s^u2B}f^(?o6OYc$7*e6>Q;C1+!~LDOA(42up~~YmaLv3F9_Pr} zQ}ny3I=C-_-foQM(2x`^%728ZJm6=QF-EN1j$Zi(DbJ8e^QM-M z?<6e=+3-r}`=$N~F8*S#JL=&c<&nPajbT#YHE44i!KDX_1C5^RhztrPL!o-y=Dj90 zPtiE$^`29x6bsW#e`$P7PBm_YJ+fh8bV!77S^nvJX!Drf%Vc4iOLN?a4h&h0JZ1RX z|63ZSsqh7lvMGvnyjw%0zS{irqVJ^;j&XHrsdcdM2HhAbq!O(nD@)+;5&zo)YIlt1(!T1?zr{PT z|9?Q#{}Z#SENfkPhJJc_@^QlMh}UzQoft=RmHpfL?@9vmI5&LGlHK#`>v_Kcg4ceo zJ!!n@gVf~ycKF(BNk$qYDsLt_00x6W?M(Y@SdBMuUKTrWQeKjM<*f(%nwxXcQ{epeM-OVe-i9tEa6ID z^w}eYw;*}5!^gZ*xjNDHYjp0*ZPB+|; zZ~nWix#O*lL^b6g#N-tZX4RQt7~u(?rekMU*UMrM9B`<9HB~i@9XJ&fX^W{ z#N|q(qh)^4sHS$}jxCBiyR@IbXUyO-diD^n0xMgQTl>Aer8Hm8JN1!a?O?nqrLJZ5 zG<^+Ne;rCy(wS9Ie{98P4b!I{NCO=g#)hZpr|3{}#puB%-K{co*Y_Bii!7(;fN+}4 z)(kckf@jlWzp+>I2O472L|bl`&f@5`MOdBPbWa$7zt+zL>eY*4oj7LQZBuPnl2j4z zC3!h%X7cv_3 z-cA+xf!)0Bz{&+~5{h};1LmircuE!mCW8&{cy?FCO)quR*sm-aa;MQbqi7ZxMac}B zRxEtZUog{!UuV6808t3W@0AxPw4?1rglDQCr36PxW@=83Jz2Au{E!mbLNfO~S#k?Y zrE$l#srQE^+}?C5Fgh&Wbnz5~H{>Wf^^V-UDZ#r ziNVQB5pX`n<5FB)o$E2ZUr9qR<*lx8MXQMn=#P29e~#oFDU4p$y12ikYT@?zP;u5= zz>a<>DlV>`!9_pZt=jJ=co(&jwlXe8CV|e|;4yky>ng~bx9#Xei&uBbWPfVJpta~t zmNYnBTWH;Rb9qwO8$P0qs5ZRNqMr(-wEN52v>z4#Hj+sSyk)oF{<;#(Y%Y6IJ-zV# zNI6+D?K0AdSJ=oJclQ#kNbsn55_5(8?Jh<~O$cM8fZMI9D`Yf}o44r_k6pZT~S%Bg5NSB=`imSK@uXNOS^XuD;Cw?&Rn%H~)XTuZD z6Nzt4Hfxnnlyg~*J+cg1ZD8s3qoO!&O&KlF#L8GT#9!sAC22y^D(x$$2AJ=!A-qQ9Z$)zA_?&B9U})vLAZ+Q^Jxn)>Z_^Mo3j|A4DKQnJ6Op#=H2QWEPduh!mz0M zEEF2@W>2n7wMR3iN4;H>tpiI$7k=EE;4C^029{6X0HkM6RLT~(|BHH z+DZ^#bA4g&s`xqB>lTVBL#OIYo`>yoFh%a4I&g&&V|( z{MK_xX#VO^Zk{811SsN=M2dRCrblPOY<7zW58P&njmAiN33D++x%gVy$vm4{_a2jAH2X7nksJ~X9yWvt z-(KODE&lZ(E~*3cHP6>Tkr(P)D%4x^w6lBFpXmO`$VdpWYkcgjjQcSl$j@i4){n)^ z=9B)e$BG&Ie^v+FNjWcaPC+kJuzqPl-{t(<5o=V2y(oov(q}Arf7O&cav@wB z4^Nn927)tRDw`WJzBl9#j2xMxU|Z)4B= z<3+ZZ=*+I7@cn65ol4^+LjH7_F%K&cdvSg$g&6zJ=`PkLZ$nWjVNb}iI2CX#!^y?RI)iDH!HTx;Uzffi+;C1;&f&7 z_FE|pz;2^&hqX0G&&@uW1eT*&OLf~?BbwVD2dpic>`190!3t}Iq^|rs3`0Wui(Ib7 z13G)bg%TCc^fwmTwAzi66P*xu+^;p!sKK9`@KnNhzKDF5zKtGFW;$E*mG7Vld>e%{ zOcRZb3ZMw(VMqjUYJ|77-yN^~s);1D#)SqjQ)|tWOiL?d=~6r}s za;XBGr~17`U6Nf-)MtL%#|D_A4ClNI{^EQGFO?AI`6l{!?^~2*g?A0O*J;et z2PS&yt$IhL<$IqM2Lt)1b2TK9z$6RfhJsvgH`izQ8{d_MwYdUWNI|&=%GXVpw^4DO z_S>$C8%UpCO@Aj;bR4RPiK{o z-=ZWABNt`h(PK9?yxbmIIb>&@V9>lDc8mGiH9 z+vWKhJuqDGc7J(mBq5JVjbI?ux1n%mq)_iw-p_Z3#lC3CxCiO!TO4Pb%dF1fWRVo% z8K(~4e1nS<8uHtd-P6oQ&BhN}=~F*`94fEgmm0k#h1bl`CW=l$!W___pi<*)W0-QP zE-hpqLq3Batt%8UC!s8Gd3@}JH{aaZ(Xv6;oTufW)<6P;G4DPRSGXB&*3C%6HNEqM zb9+9)rlf9W6;8?&3vKBBTA|+!CkQOG{qPi%$jA6(%M}NzR$Uo4USLVbXD%L(|3WSC z2L+I)>F?(xZWWgpo>7P8N063u2*pGguMS8(eQ-tG#CAR7kwujxvEX@#VD)soIEZb% zJ2i(hexKzwMweOwO%hizKedwx8sovcrQW~xS;Y4awOldjOU{iYfSqz+`xW{m`)_KP z7H?$rcFnyyRB0%=l~w)t`zkUlHd)$^v3`2pZiJtMX4Ka9Sy(BeB+r$U3U=aJyyKPTAXkqic|K8=jz20IKqu6${ zxp(gmKvk%&@_K8Iu!Bj`#bZfHzb5atw6rkiNt&lW2a(>nD14sH+uvB5P23{$S9DC@ z@U)Y3eE;F1k5A^2P=-=ZllnsvGWDkGz2Hpr3VUTD#>QRQ~vw7F&!oDGI3SsE5{^KDtt8|DV?hG$WpYrj z{tk+mjE`}9YR+88qW0h9C_!;8z z1<)iHM!!7w1cfi`^0bigfj;z5o_6oAyaaXVyrLg21)1pr;pMl>vQP&=t?3S zp{jY|zo?6Z*I7Mph+nedK}bD!D3*1Fb{rWu6Drf(Z{7fYtS+`TJxE@3(USRf0`KQF ztwN{sH0W+;zcB(6DK@7!95qONwsLfY-af2-o0j_rNdA8iKMF#}l}K^1@r3$irA6fS zru@?=1dKhZuWu#K|^}4yaw4So1#8(qG#LTNL_^g`<8vzuPcU%!_A$ zD8}WQi45&0j zQFYX|GGQYaFV}^<`+T2^N#&C}Cvy+`;qvercUJE5+;+3d{NTxEO=uRJM<^9wGnt(PCKUmg6D zr6$ffNBsK=iq8}!-*eMnh{Mhn8UG?(rekRbGsQ~!CL0xl+eS1Ra?%$4vptrXdRH(& zVMF@TErG3x**WUN-(w9D<#=8K&EX9@MQsnv(Ka`(vZD!I5mVLUMdubL**z?m09$9G zj1OUbCO-=kYz#)fm+A!F)~HgK;vF?+X7HBjkUz+kTJRS;a&$39fFyrnt=_{Yw6i-v zez)N5rgO?AvFK?@-KBKO>3>)D+g*!!6_OLE4#z45tb9XDCF=;6p9KvkOuDyoBm6|Z z1b^|T3As>`>t-0MJ6}P`?~rxoYCY*9QwtIS*BI1>ww# zLpr`FSULm_9mU`Tv{{Gv^${O@mFf0J2 zaAbKRAp{EG*zb%e;2*3= zpeMAPr_xAMRK%Ii{txG~aBBL8^O5BL{n>tqnes14jf9*!oY^SST68~k=+7Ss35n_| zsz>VYIRD^71c;ZNG>nQTj@iGe5dT-8b(Ww{EgQCkuLb)rt%Blc6bxdMVj!|9vuetx z+d)lGj>$D%OuuOGG={Ge!#_AL3cs!a%KC)(F4q>roEsUTvL zf;mi18yR$Pm}sl_|e#49XA1@TX=yKKt8EY5V7{;zy3TzUZ^ zx5=}d8p9G3KbFDrQPe}{wM9e^Z^>mQXX7D=OYd0}PxO2VuT-2mFb zO4nq?mjxh;kk{+s+T6~?Mb@Pu?Q;uRx3)xW{#^#2Z(X(J4Z*>;`OM?TYjEsOq43MA zJ`hA()63V!8?vNiyBn_%Xe6@M>PAOa`&2w5y-5nH{T{uT*L##xPGwOYo*_=kPcRNZQeBUYhd+J(V^Z}x`o1WY<+$^~B_>T#dUI3- z1~=LA9F}wLl#~@dpN5-}eho`*zI&;ENjp_4zV<^0tY%MLLV9{~LYlRj_>-ATrYw5l5sFWa^|12zcL$HLT9<#uzb z4!dxkL37{x*&3}h9s?{AtOr>x9A7G1`;1=6hz8K zN*z0Mc)NO+@umKXsAPN$jg5iOi+XbIuCIjaR(XBlY%w$kt5Z(y9~l$l#b2+Oe-ZL{ ztap8{88)&$Zn;_S%h87e7UnC+Q?p5=3Ml%G=SU(}*vucu>!Sx6Z|4i*1{nn1OqUZV zDGXh#h(PT^l?R?|C>Sf+omU}w>L<3p&N?eu?c(o<2vKnx@LH`^h?ac!s_)1tsHrAZ z|L6{Y)Oi-_3mW_?3v8zy4tPiaKjzz|ODGu&vrDjMf(%>*O7Lg1K(LdC0hcKrsSH#Q z=|ufiT8HyM+w-W^jSB1`g{-$K{Ln|^=nm~E{%?Gb(Rp_ET84Cg3pO|r)NBYm?2-50 z6)sj$h333zkKBU>f^+HV505NLD+_WAcX$n!x4tr1(=16&&W8>G{66;~=IMvHC6~(A zC(8^d;-3}Up9=CfxIAubKnA^4d~r)v|JLk&q2eD;xpZD{Tp$n?PgOLzEJ89aa)to2 zQyoW5W62HP%%=_pA@Xlp*A`iq%QW<7lQCmD?R`Ahrt3s~uhU2nR)Cp5J=?4eUKkxSa)_Ba^o8J@wQrw5y! zR^?aNwyicJX~oj%+Ae0Fpn^Hh=*u#A%Ez5r=UchmfiC-$UB_=p9H?~#0RS0$i-P%!J>Rf?CVpZ6Rv}>8Tc>up4Y9wVCEm zR;$}Re@)yHCmV8Hqp#C>7YfoQk{`+eK|Xj*nN(x#pK~*GDIq~HuR8gR|I}4}k;XSs z%lJnGh9Jm))$_dR+`3s!4oYU2$WbBgr#2YD^|-w z`U_%H<({gWJJTh7tc0b}-XV+R`|LARmK#iszqC^4t*z{N7A|4iXvFM3p3O}>IA)&V z+m1t;&F-13p0-ZmKxX@rBUH-olb%%=7}C7c zTVq-OteF!Y?qy~Zq0)o`q6${7?K|b;10;a>ONmNlMt#{8+0AiFI^#*>ispn?B(zx- z`C-jTOzihmt_C%q)geK}F>79&t(sonfj6IV0ZlXbv%6L;m+5r36f^>E2M0w(8ZVDt zqXNlKf=#lgD$}0+J~TO|jSH$bNH4hqJ+BsY!c}RLt4Le#h8vY$ZDahz{GILByy_IN zhT_jakHFm;>Bpsp+j?s#DgYWF;uRj~?e~^}$0U6-)VvTfb*owHyV=DjHZzAS2~%WN zvcu#-cim~>QOkaPQHy_dN;oINR!Vj(VOQbqihlH?WU+zpv>*&Z7$oJvMn5N(P@T*e zpB@*7+y(m$FB!M{+5aKgau|K3K>;-EvgwTLMu9o9`hLX94+!UDoR) zUKqq&Zvb@oS4#qtIylR6P~n}z`SQ&#$9)4Vfk-V;>8zp-L_KflkE|~L;(`AFPLs!! zvi@;4Obt2=v8wxC4Mo;ZDT%E5g^!mt{_7W3Dj!1WR2N;xqpi)n$ctvL)s>$Fc(hFR z&i5^1A2CGNnz$D#k0PfJ6&i6aFTn5rYo^65S4eEVRZ zXD&f&?Y&4M|35|am~RM1@+rX1EVs0U-E2a)t%P~K5Yi=& zdF1nBf->}^>U4AG*(3gle^FgkCKbm-xAQ`Jp_&?xZYcvrvCMo-<>SukVp4bQ;E5C~=(5uKWSY~m;v}$quEVwlD>lLJKY=kjX#>bm8 zfKmP}bh(}Vh6Z|2eE1u$9N2VuuCiwt=SkIqrAW8ho(hrw;$GaRPM)wqM@Z~6S4!Feu7o!W}n6SPunYR2N|oM1_$( zhF47$^IT}YhTE>Z5bojEo}3|aL7>NM?;S6r+Y;W3u^C~1h9M)RVc+j>=8+0yvE_Uy zm|gifyj+x6RQQ9coiJ8RgGX=4a(^f{(Ppj1<+iJNU0bqsxz7HUA>r=Dau}S-=FJA- z_$DHrj;PdXrfM@A_q$?8JM-mse`s{iR{L(%_t=MK|0aofiqlD^Q(zeFVYSH_(w!ag zvN}!%)Kzq#g|ygT4lPS7UOU5j3YxoS@^;AX59vY@@msvEZX-UscGpU5utqO4kFWhs zSh-p}xjTfw=Qj%Ld)6p@MtWbMN3FBExuV}ce0g`$EyOQ+yF3+n%1t5tx@Ui&Q{1@w zqSa@BA)-m!%V>lEiTW}03M)G^;_-NKo|qT_IpHZ;>*3g=1jBipcWvE#_334x~7 zLNd$AvvS4ZF_Ht>^D&skiaW9C+P-|+BH~4(Jvr=7=CGapJ@e?%o$jJzRVkb`huL%7 zmG4Q3Ih23hlX-o|Ogy>&QVU?k66 zygTp~KQ||8#r0|k=^uDIIIHS>w)+g0Z*?1CwI0))?DN-lRwB5gCW3BL_3t?R$9%so=U%1AD_UOe zV29yWB^N<_(I9%?qzx=oEBvv{a5d*Gmkh6G%vY^cNUZTqm5eg4r6^;o6TC!!p6qTx7saV%Ckwi z-Wb`6*b>@XvOT+_!2<_J`Gn=z?U>l3xyqjDp(~3&3(Y=)#UBtE9;^4rWrxpnk~K<) zUVQ)VIWS(UKdwbTnuj=Y6ip<-i6}_EhT?uR(Mddsk3Ro;ISmmc&X$-rout2L=Zg^Y)bM@ zAazXmsNS)}65(%+lB9tVje32)WW|WRjNl3ypXt%Sl^| zdSs-f{UqD1GS4!Ve>J_SyT%LtbgsIe!6oK#;9CocFtKmqCWHb+KVeBB_HEiNVgl!{ zcMKk^eZ!wwB*??X1^u8!yD>o`!=Er2qu#^MB*L#0nWeBEajrjU231V!#?v#{X7rCc zun*E-Lj$37@+v!+WeE8c>&m<8QIbYV!$r-jB7eR1M(8{aFJz{X6CbW3ZarGgGFMRK5*Hr+IQp9cWaPm81QISBHY;({(#9T@_EAHvoqhQvlQ!o0V} zHz#%VslvU-mZM4O7*GH_xW*$75$b55iEBoAmW|E%05ZN2MH@AniC0yp1>s~(@d%_T za8C0W<-Mjrr$WlBbbZ>{**yMcWkWN;#ALHzIPx7kS$;D|bo6^ru4C+bhuB=XTZLRd z#fzobgtVyCv?!N6iLM{f<*6LFI3F*r=|*`HR^KH~27Tr0R_2Kjrj0(ydyQHE=eNkB zb2Qk)zjwrQ*!)GCX>SBZq1bH*4RsMMQI~b6^0~phaU?(sIH^fDg(vG1&hP*C_*);2 zRJw)Ll=P18GEbSB?;~)YhErfl@GXhx1?4Zh%xb|={TV$#(?;dVMTGhk3Qe)O?X?rh zZeI6tnQ`h|e}ghhYcX%bwkL=*7WDh|6wNjhsna#?%9+i%+`_uCPqGoRFB=y&?d05K zbQ5{nch_}s{%i1xzH`Nua!*WK!9WoXDj_>WS8}Dtx|J|OV^Z18d*F0l5!F!0+)L0$ zQt@jxZ6~cDRNr<1Wv^){l$nr{A*%?K5lC}?)>x@1xX{wPTh7DNaJVT-`1eU}J1X`1 zWHJ#SBJ4wf1xE;DWyLA}jl}BnTAS#|fG4&s-=Ca&+=mv6kBDbBXDQ4{74Pcm>sN;5 zLK|eE0CAA+g6pj+mfh_2jf%q*M@ryvn%32ao9yt@Y@*_qWi;j2*M+3x)+nU8n(3KQ7DK1XA;2dAT z%{zxcOGa(AWwZ=0*{U=fub|n^&%i+T+152Fc<-{4s}j`ib!FkLg&MQpdgp~A1ZAgH z=9BPrD-rdQ|M)DWRr=!|(%xE~N9>km`Q**Ht<;-~N=OsOeA^8!%*pZooPhMV|)_A)73p%Wl4Msn#dPox3Pb~xwdI=UcG2FC*zwC=> zpC2Zy5Ovo6JKBwu1>LVHAwcXfRSw0pW1`c>0P=}q9c-VH+wtJU4qQA^z z-#5J#m(@)1hi*h@y$mW%S(9{ex6n%b+Z1%?8Ar9@GaveXUgkUtoqG#A?MG_n~2kbT1F zQiL4cwX-*Q_Y)`E51E2?8_6&vH<@0qC7v~rA%W_gD`xgXuZAWJI*SC(0zmSYp_fg?%v&)I zu{4d7+w`(V|3KJqR~POT?|nX{{@W7Lvno-9Y>;%ouKBmT?>>SKvgKc=nLcK*)YmgF zKeII5Mzef}{{>yXdQ=I;x>PtrO)PEV!Rp7~fKa%Rx25(P>%#x_iIVef>271FV#^j@ zmLeR}4}<;sW3Cc9GeKTQ~C#rP6^GmD`q5>tDfw2x@GsVldj^10R5VCgFdZ1J|b4V zf^?wIpqdK!G=%*DMC-s%MzdA5*;3x*(LDCxjQ7&&aJGNx*^!8)-Q3dE{G)N;Z#X3S zFY_$DwAZtQK3Tn2&LGM|tFU0ro8KqxYCSh6OKDx>&8tMl7nVpuebG2b_Ww%dP#~v( z`9~^LYK#=`4_uW5KMR< z9NV64N37mNz&KyhP*)GB4~pNOh)VQAOr_OXK?EELQonDxX{q+c+kuj& zB)oztu%b1jwm&A!@HlDkJp%yqIi3upMvY2FY_&z_)M#0ffGGsCKH3>k^Xjwm#`mmDTW_B9i#E9=f&0OK21%E ztA)Yqdg#t#yDhNZ{>?j*>((B3+}c;TvJ{Va$$TWJ}c(C}?8 zm5<{W;fIPU3%6SQ1Q53{fmVmc)_ND(ZWpc)QOc(s%or?Lt$ECL{c!OqPFKj(gkn=f z`{52*G-;a{;6knEx+a5Rb5gHXR=7NGwf7TsH~Ula8Ajy21_Ci)L+ND|{>eyEiChc< zxw}COWkP6x@v3J@PhA!A^88ooJAowir28_j=%_dl5>?W>XATHT5KP>2-B-Fx+#wlo*%RyuOa2v=E*|z`)fVIV(HJy z(Tow9=XUb)(iemkRU%7H`vp83@3HWku6P;Wjf}hzM1;kU_*MGLHpj&Ckr(_nvsaa^ zs=D%I*Px$fP8%$i=090oKi@$__&}RdYJK{3JrZpBux`luYU37M<-q~*zM@T+GcO#V zuE|qd*Woy?Ptnj&Ssx`<@Au$xMvPW*BU0w())i5D5CXcd!$-2%E_gIKe2l~PDOH+H z{iWXLXar&jlkF8RBS6$;cG2m*2pxU2#BU4=51nL>bJZT8E}Tcofk0Rr@c*)U|FkVyCSjYT zW`p&V9UaR0BC@iuu&Ai0+&Z$rVRdki7Oh>nLHaFOXbgmbrEldqnkMk2FO^)pxGDp` zBAlxk-Yk?qo{*&w*FUbD-vy8EtHc#%J_bkK{ebTa-~ZP2C;d2Gu9_2B^e*f9Vmb}| z9{{N<)0T}a<3EN}Ef_dtqV`QSr8sYZfwP88_6rBdJpE%QKsU;b_6@UH1?lfXMEB)1 z2Xl(~8F_JmsJOU~EO8K=^zYXApE5+&7iC313#z=P%aGeYOty-B_d9>AxhtloDp{gX zG8h~M=Vx^uJ~X`yWtJxoJRWhJyA*AV7F%h87Cz0-vBs25Fp9*1{*D1wHPLXYwV+>5ImK{y(^Y-(Q{SUpZ#6UMC%GwY8$!RMgzu zyWXPa(Zm{!%P>5Jmgc6$#-;`8%k}42YaMi|YZsx6k&=>c(Jo$P$i-6`!|}?Qu|qm~ z%lTck=|US7hMx<~36iC#4rfOvT0ho){IPP3@U>-~!Q*#RweXtW2!pj!&pN)5^v)U| z8giSh8DwrUOQJS%m^sZm-JI25DnE^bX4UtZH{eu8%_tGO(@7 z#&Rvv5C5@8S+;t}ZQ2Vez1$Og1S~RDnwpsv-SZDS3S8}Cc>IN@lz;|MXj`k!O#NP5fj}^|5~xLc;~Vbs1L1pON_7+hz+` z)q7O)bqsNNJx)CHLs|*6-f2wk`X}YR9ONK)+O%j1W`f0bth1P$=5zdch^#XP`^)39 zI$u?nqDELaXJJ59=~x`Ip}X0E@#$FguQ!Z!29m$2NeRjz-e~uCe2I5GXS5CXw7-gC zzyQIY=0ih56e-qG(%sI55NHJ{%OT|*;Z%* z>osq6>hyT(4n}nuv|Z->uRkYsHnd!hpO3~=ct8HScYbzzk|KYNWIy24e${IN&{{Wj z4LB}?p;Hx#shS*{*cValxWqIhl)CN?;vv7huEda=CV~N!I}dGw>o!hbb*jpKMZM6! z{PHS0zN-4I+x0Gq=)^(}_ty8-j) zWi2@)?C;2Ll$7`9Ql&!oYU&Ic*hCmO7DNQ53I(Df+V3KkU7|Bi(8-epR&grkTKk-N zPf)Vb$}}NKSdY|6W*~hLF?%)?N*D(BmksV%)?wM@W)t=6zrY->qFVR3E3=?`jev0P zt3fpFG{qS#jmgwSv$l;h-KkOwJ0v1_ zo!>Z%$KUA9{@})!?6Hay!GtFVIGWl@`my%S*j7mvDA`e{i4E1o;r|wrh8-CCE<2Ip zi-t-}1$A@NRq*y%@495D#O4e1bhj_{&PclE{|*fZ5BDR5Y%*HH8+yg9&J=9FZ`5z! zW3OpP`6?!gj;^THw%4-x{w^%JPVy8^JayC>aEpsDtZEYnj6D7GDCBk1G3KVTa zXTUPPmDtIJZ@vKQxio__@JtrgdiI^^NmXZhIJ;mIXSlPZh}4(yWu_!fFDprL`7c4l(-M|Pp=aUNkU z<&LGKi9}RgoWuOoT0~1i z`5KPxGJa;W6P%Sr0S3~Gy54-cCEGJ{jMu-0(Kz*_ym*#s-(>6ws%EVz#bub&Z} z02bOy+c(vB*Q$)SB&4Rm!%R^h<%J;@3FpXaV$|N($A535Xo@5lF$DiL>rU-Oo;9`lqM;`EUzI zNF<}*eA>%Jt3|QoY?B*g;kB(OS$`5FB#rgE53s1@=gs73UVq1i+pBw^z@$4LMV>Ob z=(JCkN9&KD2r74O4_(s^W~xWl(C`+C3hN2{yK`3suU`4Eg0kgqU7j>p>-v33ThwHl zEU+sq{LANJ5q76API_4q03ypS9>zEE&LGVV6uYxkLG3bw+SRDvkeYF5H7uCP-SK)C z*lcoMvc=ujUO-0Khf?#5wk!Ar0cL3U?}@p?2LS~InE+w-lz{8RiHYS4LFP?LlgqO{ z16HamHzNVLQnNDBiOMSOqobyb2Z`t}Yt8>2=rj0W03(V*I;ap?iD6kN zsND4Qyx;ctuFlR2iyy07liB^CWEwWs*6BQ0QH7?q1UJS{h&H&+s;mIu_ib`wJQM@( zR7q-NSU4>ay4Fa*4u=@Uc~HfeVfU({JVh@P9|rJ8{<@Us0mN0;px)P+m)-}zIZiu z|GEOP9Blss;Q{fyXmrnB(GiRA4xo2zN@04^O4^zm{FgE8@k zXhfZwEl<)W$C%c=W+xA(W(0zz`9<=CJVDsr^jkU34vw<4sIR83c6KZ?y?1N)0YANt z80qLLO6&a^*rukY*s&ITZ|;GLfRzhMjvmB_@!#fKuW z7+0HZV`%W4%~zIo={?V4iN5Cx7GD=yLjFronR9p^8glA^z$T!g2vW7-NK1#7X0slc zZ)G&WN-*$?bK>-4CcD^v^(&C z7WkUwT{|fgwjf{d&v_A@8VM!UoL)$$>Se0P0fj_V-~^tEXuftS4b3 z{zswaX(ZhQib1b!yJc*^ARQd5(!D78h;t_X3m|l4XI}RffQq`$PmTm&PkRscawD}> zrMKl@1$J9kygaQ`>IiE_F^a7X2sl)N@!jK5NkQZgX_h z%EyG5B1egV9+#6_^9Oa@bhBynh;M0vm-%g$E2Om;=}7){>vYEK##B~~U=ML4Q;U6y z0WDAN1p8Z>MKYK6nCUZz6S0B?7Rz zx(YF4?i!2?A>36$(#@MHL|pRn8HL9QiHQG=^P5yWh|&3M#10Pp+Uiwe(jO|Ch#+gZ-wGHa z_HO;#Wi>LN>Zx-7=YH~*DB=7)9nl8Klh3PpCTZyC*3r=s0RaJwn3%X{X{njz2Z2~E zHwY&bvB4eT{4K3Y@a4-F$QRIUoviWU|M#4e_k~OXWX5gp&!f^9ki%5#_2!D$ouZ?o z11WH~hrJb=JwKr!Al^cYhp&oEud@ID_G1@7A zZCp0GW;!9x8;iffVr>D3#r%i8x5Nc378sC* z=W;TU`51)ogt}mcp)ddY_d7f@Orc1k?Fz$ASp3L8~|Up!pSR=sy?Ax4MM5?&Fu(ARtV9}64V ztn_t_!tTtDrUb%iEfNPn+R{u%*@~X$tO>bPe8Kh@b`s?%*rc;aMIz`NonJ_|xC=wJ z^KoeSO4&h1ba!_BHx`yU5|eF)2V`1nH5FQU@XDD1N7(HhU1i0cnk9#kGzI$-RG|j1aJd-gb zogSUKasU+4_5=QwGqN7KNG_L~%q>MbO3^-jGcKHZIR$Ctr#Mr)soex-{i=`AAtRx%yu zo<92a$MYY@Y>q?$Y$A6I<5iUpMoFI`N_t&j9+p1#qppWO*`!E)L!+SE~Hv;xG>S22vb;7_bI>|3|%&5NgfwS_Ai*?97Zc{NDKJ8G}`fMVt%4DIiS zn}T*PouPlH>50J*a3hJMz&9Etv~YDMXDb~Gqg^UXh&|i;!c04fRX6wb;a5P$HAki-Fo5XLWMLrB$}tV=3A|~(;qa^pHx^ZD*@|qp z1|5|ggvHO_hF}^3-10i?QzS=2&&3O`aASEcudZjag)&&x6iB}vl9Am>ps*Bh*mF|M zT&ML!k~hXkLZ8UcI>axcs*a7NG5NN5y+w%_rwQMOr?&7?X{fd7+#5}Ej`mqgO`9Xa z?i}YjAb%mGjOdrLkv;vZ8~yP1bbBP^u-Pay7k~jwPD0A@iV&_YPIgXM4bH>0aUg(! zp}q|69+SR<@i$9H>3>A;S4%e6Q&vO*{HEoSrWvL8tSg!oR8{hCJF9echQI=Hh4$G{|pz*BsrbG(zm9)v=-akCrCxjMblku z;(j8EkI5E=+lQc>5f-EjlL_diUT!RYmk1KeuT_oO5VwV+{&u$Z?D5HX`O{JsH?Vlk zV}E8~T9cg>E=!OoRTi`o8X41K&C#u%cCS2i8(;l)7zg5RZ=PzMa0 za%Q~}%k}hVLzmTN$Zusx>DAM|PNZ5dg7b5$RyR|v@ELtvoeNpHRF%edR}v&;BLO+% zNV%^pw%XeF48!U=ZP_cPel7=(0Y(B|8+m$_c0F8L@a{6RdD)P~Co%eL<9br+k$5FL zimGJZwZE5s?XNRyiOy~qgEBs$*{psRqrsgj8)Niwz8*5PZ;gG%EU>ag=w~gr8P70W z7(1?+W$C`U)*L>Gt?`=2x{p$5opJ%z8?egM6B#hk(a1!f+@rHl67p1Mk;|NuU1Mb@ zhkGgV%d-|oaaOJ15(k~$|Gq7_3|{L_P0aod`K-v;V9g_Dva3bQC(-xnP#e}?%6yVh z2$J=#u^5VHcElIVz%K{@o99VWrEu$-Z{9+j@SVJNb}`wnEsS`1K;iRfTMvRU@loAQ zm{=^Af7_eb_w8sh>^&!Otk}GGlwarhh&l5ZkLJIbpFKz;`;`M$Jw`7 zS!1T~EWm!;>=u6+NkRM3w9a3hpmo0~m*)y~#Lxohbu)Zy{EqB)w)=c@&%0=+`Z0%* z=wHT^xjC+m#9jz6%{%>@owg7sO5?q%XgF(WMXPj=U;kdk@D)(8+PHe8=x1*+nJQ;p zL#o;|w)c?IV0Z{>oR90={k??pT+~gMW+R5>oXBhsfPn=$k!Zc7%dL6g|R9X*}JBEX#W0r^$2|=d4`K)#OU;lxfrN zhq|kEjE%!p!!l4-27r8f{4dQ4F06)n4F#YlN!#=O@>2Oa6UT|pU6Je;WPlf~=*xyo zZrSEq#t-a9Z$ZX;91Yu{M?71-mBHZLp9V(CGp&-!Rxn@y_EJHnwzk#S5rx{0R_43J zV^a~(;L3qP6(wj-ZIYKDv7un5q5Dh)}D)bwa5E62z9{0|Go+U4UWoDd}m(dFqdCK zr;kP70IOLR!mB=~d}Y7V*^?0)20t2FLS4Rs>LQzTR%TbM0nf|7x1C_Mb}M_?HCGN$ z-V2$+r;vnS;uNE8$&Ve%4atnANXB?PE_O{tehCTUL*vx0-;D~V3Oed?V_Rwzh4w6( z@OUaF@AHqoLrpuI4(M}Zm2J98a0ceg{Uhm)1t?^Nxt*M=lH9-kk)rlMwc}m;xqG-0 z=wuJrDZaR|78s>_RCIY3O?ihB3*xIf#U%%C+AMTN@sD4AGomBFvuw`LUS#@tazQT$78Zh2-WYW z4aY*b{E3aQ>`S9X50*ZD2pf-6LaBL|^y_rz${b4RxDWFs%j@%hJvY*cZO!Q2N=nLxSNyrha;!GbkIxf? zZsg%zbfXcC$8>VKDwT*EZ6)Q2QK!jY4(m=;)myc0jRxA_K#Zv-n*@vpcVy_@InC&f>s=5*c7tJq;rFxvj@!J}ET5rnz9i8Wy zMxKJXMPsidQJrt)JDBeGN`86LrtENY_EWC5FK+an(8fW4VF!Z&!8=?dCikVs34Z17 zlUsbNWj78e#-+DR>0>zvRJ!_`uw0%l8;r$_va1d5iR>Z@NV~hl%qZ42<&?rRjSY(N z{65cGT$}Z$!BV9Co6skR>XeCETSoB1;~}htDk z|8%v-W3#fSgpCH7<%{3UVayqG=Ow{fqv;t$3Mh1Kv#mZyw-G!!6&lE?)@Ow&+wpTx z5b@n;z3wZaK`ZbrXSOeopL0)?0ZMI zK!1#Wh=qz_Or}VGZiBN%CNLr^(Nv@nNEOI}cp>;c*D9?I9 zqy8;Su<3LgYC&N9#eBYkV`Mk$YjOSg`D6*g(=OarS0p*l)$`U51$KDoEU|;%){XL% zYD$$E1V4DQ_@Yxh2;)g;j^$KCHl~mO5|JRk&)Zh3_v7JGLWXEwAT9utcr$8b_kl*Mal7Pi-lji?t#Q zx!5Gdf~E3e0@{m}gQbGfG)$FvjI1B<(qnx128Xg-c7V&d9|~ESnaHTsfuKq@Li*a| z`+auzqvK+sNq48S-C8bIhM@!};^Kf1Bv7ZI!BJU@eQ62I6l+w3F{Rb?YTy8Dr>}!% zKbmWaUC;HMb!>lF$yr(&9dL=yYf&XUWE&fnz5i5byzKhJmp= zaD2dT$V548*9Z*8$@KXkR)zLf7sHyeXnw^vb|x|*BBlSS3$gMzU<6(GS)#$?3P&uA zYy_!H$r6_ltHHFo_}8(O*UTp0)1*Y9U^S-g)!D^~_?Ir@B`_eB-?J>k+%$m`DgBkc zTTN|_e3s~Z&1Kc>Z(eWFC$(H17YP@W{L020;)dlD*v=4H%0wHF`&P!g4?jl>f?hMN z-FlyCAfvR~p#beFl70RCvfEMoP{%=t8N8kz#2Dm5;1U8p#Zdq_dSe`!Mw% zId5r#`a(IA&RXZ6$u93^BC}_B^M(6I&FZCd-Uc?svBfi{nW6rvmcw{P9aow(uGMmo z#c1tPo7gwkc*5s?YYR>{et85!Xy~*R?;dxI%P+CeoG6@a@XX-UU=79^HBQ+~$8nQ< zqt};f_nC-5Ik+}_u2%b_lZmk0Ue>ebNtu(%jTbCdzxX=B`V~@u++3_?0og=mn;m7E zy=)%8yiw2F3@57+B8DobQtX{Bc#u}E?RLmHE5qAmuoJR8Z7&MgrM_d(#*7v>S01Y` z&ctg>GYTwNOx0#u_*P}F*m~whPN~f`S=ttbg8@Z)woBTnM&Wx(Jk{wMT35+WbZU2lnx!Ul!Pa<>+o(z40~2qx6<}U(w9w5Iz)IKaa-pfwp+Rd|;=9Ql z{p)0gw&{j7WSV9ll`ziU2z>~Chx*|w?Xs-Fvix#GT=Ug-Awfh8CYn=LQCDC)GZuIc z{b{EeI%PDmAf>P~7K09P_t&-7rNge+uV&#OM>!a9NuWB5JvS0A|NJw5{B{!2(P*8wE z!%eRkbz1&ni(T`2KHr$Cn1NX+DTc)d5|0Rel7orz5k7AVdmDR~XZsc1Z-bTQ3V(^6 zD^3uFdsBL%^!b||<6*g?9rAW;sWT1(5io@7Nuw#W-u=k)BSO0_U*E3C{BmJnCdN3r zKdFCsN(DU{;P!y3&fuF^EMCrQQ-5ye|{)~gn zzfw;t`}o-TDSLayZ9bK2L#7$?v57^Q)cN@+$>cilQn}a0!|kcCQtt!pW44vXm6msK z&#ean(&_Pod;5a?X*B}EU^4jO78h$wVE#SJWmXRL9`(ml7}y|~O@l5$Kn}s<{w~9T z4tPU8UHA`9j06-bWGBC#grblIBN6F&x?ewZN!t~&m6JbB4sQ$_XW&;bMWPbClkn#Y zfCtlLDPch5(t12@(BQOKe~!6W=H20AsNe!cZkvmi%Zq_23XgvWv-8rkA0h<$p_ueKCf_Em%PBGj_;_&&G0qWrKc+FiT6 z=l{>#DDLcYu60D8n$@ylSjL?b+jF}pA|<6I!((%%$(AuWIW9|>Iniv98%FFPIC9A% zXm)JD&`!n79;2clWtLK_(U~C>L585d{Qx0oS6iIU3|&|pc$kdAQV9mrgv;GTE?2=D z?YVMzsZ3zv(GYTzMN?ZEqf@~NR6Bo3++HyOAql7=g^9(ov@LfSlUah)UMEXNYW0 z;*F>NOReLa;=<_XqAeG4IiNl1t?cdwYC zD2jG`3hlYm=4+QkhBOG9!`fWR_;7I{-}rS=nxY7%ExawiQD2!#KMYaI(8mRDm60k z`}Gm@-M>2sHMM^?0gYBWgK*{>xS$^*5p7QqU~|=ovx5sG??cUQ(=G`ILWT_*m6-3w z(vn_BJ_3(Ddm$D2#oBj|q%!*aZC%8q@feLhFrM#g|3Xs<0atO$V^s_2DpmEE5@T_- zvYU(X&TJ9sT2VT~|H(3h=e{wQcESDKXxloLBcOo)9spfwD z{ovU#>968yAk@EY;gt|n(*tFdIc2nj{~T3R{Nv(&4_mF9RY=9Fs}4v@8x0Sqbfco0 z2L&Z%%Oobjt#82bIDIqU(yORK1Y*-*($cm#us{`NXgqK0v5+8yPm6B~t{1uv3Ec^t zBU$=d1_t^XrruxoJ2r_tFEQr^iBIOw+No=tsN*6yGmWP~qaWNew=By>_XOsdahO>W;p9V&#z&u^EqMNn6+@p$7{dNx| z9kDPm7w0vFko|10;bu_p&Y%Dx#-W0uYL@=cgVwpAI+Qnui{4(aHs}0CoAR)| zsFxp7QLA5R2uC=n&|%XiJN}T)B5&;{i{{@A)iw|~cs!&>i{!(?9m$tep1YB1(slihcXW1&DY;MkZ zzxQ@js?F()tHtUE)i=|m7B(L0)CAI>vg)$^-%)yi*r#E+|C48UzDuPV!xfy;X`whJ zNTUJYVlg8#^A8UkNO}VH{#Fq*SZRDz|A4>R>i@%Oih&dw5!k>cDIdJQKQsmef>BZ$ z{DeeA+};Kr9t@1Zot(5JkPCwPA-)JNeA=$JT+0!%!*w}6EQd0`uP3E3Ku%A6uL_&W z;Qs_!j4(h4{|3GmFP(?5944|nqPjmC{pagxED?J4^z`H;I~yB>pPwI~ufH`G)1{^) z8k;>gSFA82cLqr;H8TF6ZpgjAagu>c%Lo%cy`jR6Z-t8X7yuDp=`fkn4s6HttU1iB z0;?E~PrN_Mix&4;4OHn-OWcT~z#1(YnDp)QO6=*OqUMXhfM5KLOM2&|L;lan7GXLJq6JL6RTQ>J z{647YjPA2-yUWG;#s-F14D1if6ZV$qJL9X&X1tcX7g%Km20hI48j}r0G<->6>HSyI zA{xA#EnPu-fkm&eBKv{fG|vbqTdnQNU@++KE)!he2s%y0sZNX2NvPUu%K5)ZlV@nx zuuPbqr1uUvsAQe@4UHs5M5OS~)C#+WW0Dv@`Zj2$ec42;j8pzp{>!vf;u1svtuNp$1Alfr(`K9~M)9Z&I zyDak4u0_$538x4Gf=>QVNerSu%PkSjq6((=*zIr9nuEiB()$TwD}9Sogu2$$K;~Ut zmVC=Ki(_)g!QNzX|4M%h1HPFQIPtB+7Y-eiSx|d32I+z{{z86xCD%Bh;XRY&av&5? zY};|MC&!YfWo79(!w4fc1}32#Qr2C20VDEKFrCiBo8!X`*^B@rfB~*z;n(H~E{r=`nd#U)Ts60tt}o$70?JlN&m6 zMcUS7$5ARZPDK(!-=L9vT?v!Ic_yWR^E57`B&5Q9<~df!xl#G|_*iZH%d<2Qpcdr4 z?oaRou(GpWiXibhrVRJ+5rg`R?3Ps#L$uQW;JWbVoH6
    >D4 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 z%0D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

    11;aDlj3!5HSD9HeqGPM3+q9n7hoTfe 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

    ==s*DF8cQPTzgk8XWf_ttD~K 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 Wy`;IS&NfJL?hIV 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 literal 0 HcmV?d00001 diff --git a/documentation/jetty/modules/operations-guide/nav.adoc b/documentation/jetty/modules/operations-guide/nav.adoc new file mode 100644 index 000000000000..eb03e27de1f4 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/nav.adoc @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +.xref:operations-guide:index.adoc[] +* xref:begin/index.adoc[] +* xref:features/index.adoc[] +* xref:howtos/index.adoc[] +* xref:arch/index.adoc[] +* xref:start/index.adoc[] +** xref:start/start-jpms.adoc[] +* xref:modules/index.adoc[] +** xref:modules/custom.adoc[] +** xref:modules/standard.adoc[] +* xref:deploy/index.adoc[] +* xref:server/index.adoc[] +* xref:protocols/index.adoc[] +* xref:keystore/index.adoc[] +* xref:session/index.adoc[] +* xref:quickstart/index.adoc[] +* xref:annotations/index.adoc[] +* xref:jsp/index.adoc[] +* xref:jstl/index.adoc[] +* xref:jsf-taglibs/index.adoc[] +* xref:jndi/index.adoc[] +* xref:jaas/index.adoc[] +* xref:jaspi/index.adoc[] +* xref:jmx/index.adoc[] +* xref:tools/index.adoc[] +* xref:troubleshooting/index.adoc[] +* xref:xml/index.adoc[] diff --git a/documentation/jetty/modules/operations-guide/pages/annotations/index.adoc b/documentation/jetty/modules/operations-guide/pages/annotations/index.adoc new file mode 100644 index 000000000000..d20e7a807a94 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/annotations/index.adoc @@ -0,0 +1,219 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Annotations + +Enable the `{ee-all}-annotations` module if your webapp - or any of its third party libraries - uses any of the following: + +* Annotations: +** @Resource +** @Resources +** @PostConstruct +** @PreDestroy +** @DeclaredRoles +** @RunAs +** @MultipartConfig +** @WebServlet +** @WebFilter +** @WebListener +** @WebInitParam +** @ServletSecurity, @HttpConstraint, @HttpMethodConstraint +** @HandlesTypes +* javax.servlet.ServletContainerInitializers or jakarta.servlet.ServletContainerInitializers +* JSP + + +[[scanning]] +== Annotation Scanning + +According to more recent versions of the Jakarta Servlet Specification, the `web.xml` file can contain the attribute `metadata-complete`. +If this is set to `true`, then _no_ annotation scanning takes place, and your descriptor must contain the equivalent xml statements of any annotations. + +If it is `metadata-complete=false`, or your `web.xml` predates the inclusion of this attribute, annotation scanning is required to take place. + +To prevent annotation scanning you can use the `WebAppContext.setConfigurationDiscovered(false)` method. +Here's an example context XML file that calls this method: + +[,xml,subs=attributes+] +---- + + + + + false + +---- +<1> Configures a link:{javadoc-url}/org/eclipse/jetty/{ee-current}/webapp/WebAppContext.html[`WebAppContext`], which is the Jetty component that represents a standard Servlet web application. +<2> Specifies that scanning should not take place. + +However, despite `metadata-complete=true`, scanning of classes may _still_ occur because of `ServletContainerInitializer`. +Classes implementing this interface are found by Jetty using the http://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html[javax.util.ServiceLoader] mechanism, and if one is present _and_ it includes the `@HandlesTypes` annotation, then Jetty must scan the class hierarchy of the web application. +This may be very time-consuming if you have many jars. + +Jetty can reduce the time taken by limiting the jars that are scanned. + +[[og-container-include-jar-pattern]] +=== The container classpath + +By default, Jetty will _not_ scan any classes that are on the container's classpath. + +Sometimes, you may have third party libraries on the container's classpath that you need to be scanned. +In this case, use the `org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern` context attribute to define which container jars and class directories to scan. +The value of this attribute is a regular expression. + +Here's an example from a context XML file that includes any jar whose name starts with `foo-` or `bar-`, or a directory named `classes`: + +[,xml,subs=attributes+] +---- + + + + + + org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern + .*/foo-[^/]*\.jar$|.*/bar-[^/]*\.jar$|.*/classes/.* + + +---- +<1> Configures a link:{javadoc-url}/org/eclipse/jetty/{ee-current}/webapp/WebAppContext.html[`WebAppContext`], which is the Jetty component that represents a standard Servlet web application. +<2> Specifies a context attribute. +<3> Specifies the name of the context attribute. +<4> Specifies the value of the context attribute. + +Note that the order of the patterns defines the ordering of the scanning of the jars or class directories. + +[[og-web-inf-include-jar-pattern]] +=== The webapp classpath + +By default, Jetty will scan __all__ classes from `WEB-INF/classes` and _all_ jars from `WEB-INF/lib` according to the order, if any, established by absolute or relative ordering clauses in `web.xml`. + +If your webapp contains many jar files that you know do not contain any annotations, you can significantly speed up deployment by omitting them from scanning. +However, be careful if your webapp uses a `ServletContainerInitializer` with a `@HandlesTypes` annotation that you don't exclude jars that contain classes matching the annotation. + +Use the `org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern` context attribute to define a regular expression for jars and class directories to select for scanning. + +Here's an example of a context XML file that sets a pattern that matches any jar on the webapp's classpath that starts with `"spring-"`: + +[,xml,subs=attributes+] +---- + + + + + + org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern + .*/spring-[^/]*\.jar$ + + +---- +<1> Configures a link:{javadoc-url}/org/eclipse/jetty/{ee-current}/webapp/WebAppContext.html[`WebAppContext`], which is the Jetty component that represents a standard Servlet web application. +<2> Specifies a context attribute. +<3> Specifies the name of the context attribute. +<4> Specifies the value of the context attribute. + +=== Multi-threading + +By default, Jetty performs annotation scanning in a multi-threaded manner in order to complete it in the minimum amount of time. + +If you don't want multi-threaded scanning, you can configure Jetty to revert to single-threaded scanning. +There are several options to configure this: + +1. Set the context attribute `org.eclipse.jetty.annotations.multiThreaded` to `false` +2. Set the `Server` attribute `org.eclipse.jetty.annotations.multiThreaded` to `false` +3. Set the `System` property `org.eclipse.jetty.annotations.multiThreaded` to `false` + +Method 1 will only affect the current webapp. +Method 2 will affect all webapps deployed to the same Server instance. +Method 3 will affect all webapps deployed in the same JVM. + +By default, Jetty will wait a maximum of 60 seconds for all of the scanning threads to complete. +You can set this to a higher or lower number of seconds by doing one of the following: + +1. Set the context attribute `org.eclipse.jetty.annotations.maxWait` +2. Set the `Server` attribute `org.eclipse.jetty.annotations.maxWait` +3. Set the `System` property `org.eclipse.jetty.annotations.maxWait` + +Method 1 will only affect the current webapp. +Method 2 will affect all webapps deployed to the same Server instance. +Method 3 will affect all webapps deployed in the same JVM. + +[[scis]] +== ServletContainerInitializers + +The `ServletContainerInitializer` class can exist in: the container's classpath, the webapp's `WEB-INF/classes` directory, the webapp's `WEB-INF/lib` jars, or any external extraClasspath that you have configured on the webapp. + +The Jakarta Servlet Specification does not define any order in which a `ServletContainerInitializer` must be called when the webapp starts. +By default, Jetty will call them in the following order: + +1. ServletContainerInitializers from the container's classpath +2. ServletContainerInitializers from `WEB-INF/classes` +3. ServletContainerInitializers from `WEB-INF/lib` jars __in the order established in web.xml__, or in the order that the SCI is returned by the http://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html[javax.util.ServiceLoader] if there is _no_ ordering. + +=== Exclusions + +By default, as according to the Jakarta Servlet Specification, all `ServletContainerInitializer` instances that are discovered are invoked. + +Sometimes, depending on your requirements, you may need to prevent some being called at all. + +In this case, you can define the `org.eclipse.jetty.containerInitializerExclusionPattern` context attribute. + +This is a regular expression that defines http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html[patterns] of classnames that you want to exclude. +Here's an example of setting the context attribute in a context XML file: + +[,xml,subs=attributes+] +---- + + + + + + org.eclipse.jetty.containerInitializerExclusionPattern + com.acme.*|com.corp.SlowContainerInitializer + + +---- +<1> Configures a link:{javadoc-url}/org/eclipse/jetty/{ee-current}/webapp/WebAppContext.html[`WebAppContext`], which is the Jetty component that represents a standard Servlet web application. +<2> Specifies a context attribute. +<3> Specifies the name of the context attribute. +<4> Specifies the value of the context attribute. + +In this example we exclude *all* `ServletContainerInitializer` instances in the `com.acme package`, and the specific class `com.corp.SlowContainerInitializer`. + +It is possible to use exclusion and ordering together to control `ServletContainerInitializer` invocation - the exclusions will be applied before the ordering. + +=== Ordering + +If you need `ServletContainerInitializer` classes called in a specific order, you can use the context attribute `org.eclipse.jetty.containerInitializerOrder`. +Set it to a list of comma separated `ServletContainerInitializer` class names in the order that you want them applied. + +You may optionally use the wildcard character `+*+` *once* in the list. +It will match all `ServletContainerInitializer` classes not explicitly named in the list. + +Here is an example context XML file that ensures the `com.example.PrioritySCI` will be called first, followed by the `com.acme.FooSCI`, then all other SCIs: + +[,xml,subs=attributes+] +---- + + + + + + org.eclipse.jetty.containerInitializerOrder + org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer, com.acme.FooSCI, * + + +---- +<1> Configures a link:{javadoc-url}/org/eclipse/jetty/{ee-current}/webapp/WebAppContext.html[`WebAppContext`], which is the Jetty component that represents a standard Servlet web application. +<2> Specifies a context attribute. +<3> Specifies the name of the context attribute. +<4> Specifies the value of the context attribute. diff --git a/documentation/jetty/modules/operations-guide/pages/arch/index.adoc b/documentation/jetty/modules/operations-guide/pages/arch/index.adoc new file mode 100644 index 000000000000..0d66e3d5dedc --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/arch/index.adoc @@ -0,0 +1,160 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Architecture Overview + +Jetty is an HTTP server and Servlet Container, and supports deployments of web applications. + +The xref:server/index.adoc[Jetty server] listens on one or more network ports using one or more xref:protocols/index.adoc[protocol connectors]. + +Clients send HTTP requests for specific URIs, such as `+https://host/store/cart+`. + +The HTTP requests arrive to the connectors through the network; the Jetty server processes the requests and, based on their URIs, forwards them to the appropriate xref:deploy/index.adoc[deployed web application]. + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam roundCorner 10 + +scale 1.25 + +cloud Internet as internet +rectangle "Jetty Server" as server +rectangle "HTTP/1.1 Connector" as http +rectangle "HTTP/2 Connector" as http2 +rectangle "WebApp "Store"" as store +rectangle "WebApp "Catalog"" as catalog + +internet -- http +internet -- http2 +http -- server +http2 -- server +server -- store +server -- catalog +---- + +[[concepts]] +== Main Concepts +There are three main concepts on which the Jetty standalone server is based: + +* The <>, where Jetty modules provides Jetty features. +* The <>, that provides a place where you configure which Jetty modules you want to enable, configure the properties of each enabled module, and therefore configure the features you need for your web applications. +* The <>, that starts a JVM that runs Jetty with the configuration you specified. + +After installing Jetty, you will want to set up a <> where you configure <>. + +[[modules]] +== Jetty Modules + +The Jetty standalone server is made of Java components that are assembled together, configured and started to provide different features. + +A Jetty _module_ provides one or more components that work together to provide typically one feature, although they may provide more than one feature. + +A Jetty module is nothing more than Jetty components assembled together like you would do using Java APIs, just done in a declarative way using configuration files. +What you can do in Java code to assemble Jetty components can be done using Jetty modules. + +A Jetty module may be dependent on other Jetty modules: for example, the `http` Jetty module depends on the `server` Jetty module which in turn depends on the `threadpool` and `logging` Jetty modules. + +Every feature in a Jetty server is enabled by enabling the corresponding Jetty module(s). + +For example, if you enable only the `http` Jetty module, then your Jetty standalone server will only be able to listen to a network port for clear-text HTTP requests. +It will not be able to process secure HTTP (i.e. `https`) requests, it will not be able to process WebSocket, or HTTP/2, or HTTP/3 or any other protocol because the correspondent modules have not been enabled. + +You can even start a Jetty server _without_ listening on a network port -- for example because you have enabled a custom module you wrote that provides the features you need. + +This allows the Jetty standalone server to be as small as necessary: modules that are not enabled are not loaded, don't waste memory, and you don't risk a client using a module that you did not know was even there. + +For more detailed information about the Jetty module system, see xref:modules/index.adoc[this section]. + +[[jetty-base]] +== `$JETTY_HOME` and `$JETTY_BASE` + +Instead of managing multiple Jetty distributions out of many locations, it is possible to maintain a separation between the binary installation of the standalone Jetty, known as `$JETTY_HOME`, and the customizations for your specific environment(s), known as `$JETTY_BASE`. + +This separation between the binary installation directory and the specific configuration directory allows managing multiple, different, server configurations, and allows for quick, drop-in upgrades of Jetty. + +There should always only be *one* `$JETTY_HOME` (per version of Jetty), but there can be many `$JETTY_BASE` directories that reference it. + +This separation between `$JETTY_HOME` and `$JETTY_BASE` allows Jetty upgrades without affecting your web applications. +`$JETTY_HOME` contains the Jetty runtime and libraries and the default configuration, while a `$JETTY_BASE` contains your web applications and any override of the default configuration. + +For example, with the `$JETTY_HOME` installation the default value for the network port for clear-text HTTP is `8080`. +However, you may want that port to be `6060`, because xref:protocols/index.adoc#proxy[Jetty is behind a load balancer] that is configured to forward to the backend on port `6060`. +In this case, you configure the clear-text HTTP port in `$JETTY_BASE`, not in `$JETTY_HOME`. +When you upgrade Jetty, you will upgrade only the files in `$JETTY_HOME`, and all the configuration in `$JETTY_BASE` will remain unchanged, keeping your clear-text HTTP port at `6060`. + +Installing the Jetty runtime and libraries in `$JETTY_HOME` also allows you to leverage file system permissions: `$JETTY_HOME` may be owned by an administrator user (so that only administrators can upgrade it), while `$JETTY_BASE` directories may be owned by a less privileged user. + +If you had changed the default configuration in `$JETTY_HOME`, when you upgrade Jetty, say from version `10.0.0` to version `10.0.1`, your changes would be lost. +Maintaining all the changes in `$JETTY_HOME`, and having to reconfigure these with each upgrade results in a massive commitment of time and effort. + +To recap: + +`$JETTY_HOME`:: +This is the location for the Jetty binaries. +`$JETTY_BASE`:: +This is the location for your configurations and customizations to the Jetty binaries. + +[[start]] +== Start Mechanism + +The Jetty start mechanism provides two features: + +* The mean to configure your `$JETTY_BASE` by enabling the desired modules, and to display the configuration of your `$JETTY_BASE`. +* The mean to start Jetty itself, by starting a JVM that reads the Jetty configuration in `$JETTY_BASE`, which is then executed to assemble and start the Jetty components. + +The Jetty start mechanism is invoked by executing `$JETTY_HOME/start.jar` from within your `$JETTY_BASE`, and you can think of it as the Jetty command line program, similar to many Unix/Windows command line programs. + +For example, you can ask for help: + +---- +$ java -jar $JETTY_HOME/start.jar --help +---- + +Or you can list all available modules (or only those with a specific tag): + +---- +# List all the modules. +$ java -jar $JETTY_HOME/start.jar --list-modules=* + +# List all the modules tagged as "demo". +$ java -jar $JETTY_HOME/start.jar --list-modules=demo +---- + +You can enable a module, for example the `http` module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http +---- + +Once you have one or more module enabled, you can display the current configuration, to verify that the configuration is correct: + +---- +$ java -jar $JETTY_HOME/start.jar --list-config +---- + +You can enable a Jetty demo module, which will deploy a demo web application: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=demo-simple +---- + +Finally, you can start Jetty: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +Read more information at the xref:start/index.adoc[Jetty start mechanism section]. diff --git a/documentation/jetty/modules/operations-guide/pages/begin/index.adoc b/documentation/jetty/modules/operations-guide/pages/begin/index.adoc new file mode 100644 index 000000000000..ce4a361dc05e --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/begin/index.adoc @@ -0,0 +1,337 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Getting Started + +If you are new to Eclipse Jetty, read on to download, install, start and deploy web applications to Jetty. + +== Quick Setup + +Jetty is distributed in an artifact that expands in a directory called `$JETTY_HOME`, which should not be modified. + +Configuration for Jetty is typically done in a directory called `$JETTY_BASE`. +There may be more than one `$JETTY_BASE` directories with different configurations. + +Jetty supports the deployment of EE8, EE9 and EE10 standard web applications, as well as the deployment of Jetty-specific web applications. + +For example, the following commands can be used to set up a `$JETTY_BASE` directory that supports deployment of EE10 `+*.war+` files and a clear-text HTTP connector: + +---- +$ export JETTY_HOME=/path/to/jetty-home +$ mkdir /path/to/jetty-base +$ cd /path/to/jetty-base +$ java -jar $JETTY_HOME/start.jar --add-modules=server,http,ee10-deploy +---- + +The last command creates a `$JETTY_BASE/start.d/` directory and other directories that contain the configuration of the server, including the `$JETTY_BASE/webapps/` directory, in which standard EE10 `+*.war+` files can be deployed. + +To deploy Jetty's demo web applications, run this command: + +---- +$ java -jar $JETTY_HOME/start.jar --add-module=demos +---- + +Now you can start the Jetty server with: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +Point your browser at `+http://localhost:8080+` to see the demo web applications deployed in Jetty. + +The Jetty server can be stopped with `ctrl+c` in the terminal window. + +The following sections will guide you in details about <>, <> and <> Jetty, and <> your web applications to Jetty. + +Read the xref:arch/index.adoc[Jetty architecture section] for more information about Jetty modules, `$JETTY_HOME`, `$JETTY_BASE` and how to customize and start Jetty. + +[[download]] +== Downloading Jetty + +The Jetty distribution is a file of the form `jetty-home-.`, available for download from https://eclipse.dev/jetty/download.php[] + +The Jetty distribution is available in both `zip` and `gzip` formats; download the one most appropriate for your system, typically `zip` for Windows and `gzip` for other operating systems. + +[[install]] +== Installing Jetty + +After the download, unpacking Jetty will extract the files into a directory called `jetty-home-`, where `` is the version of Jetty that you downloaded. +For example, installing Jetty {version} will create a directory called `jetty-home-{version}`. + +IMPORTANT: It is important that *only* stable release versions are used in production environments. +Versions that have been deprecated or are released as Milestones (M), Alpha, Beta or Release Candidates (RC) are *not* suitable for production as they may contain security flaws or incomplete/non-functioning feature sets. + +Unpack Jetty file into a convenient location, such as `/opt`. +The rest of the instructions in this documentation will refer to this location as `$JETTY_HOME`, or `${jetty.home}`. + +CAUTION: For Windows users, you should unpack Jetty to a path that does not contain spaces. + +If you are new to Jetty, you should read the xref:arch/index.adoc[Jetty architecture section] to become familiar with the terms used in this documentation. +Otherwise, you can jump to the <>. + +[[start]] +== Starting Jetty + +Jetty as a standalone server has no graphical user interface; configuring and running the server is done from the command line. + +First, create a `$JETTY_BASE` directory. + +---- +$ JETTY_BASE=/path/to/jetty.base +$ mkdir $JETTY_BASE +$ cd $JETTY_BASE +---- + +You will typically start Jetty by executing `$JETTY_HOME/start.jar` from this directory. +However, if you try to start Jetty from an empty `$JETTY_BASE`, it will complain that you haven't enabled any modules: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +.... + +Jetty uses a xref:modules/index.adoc[module system] to configure and assemble the server; these modules are enabled and configured in xref:arch/index.adoc#jetty-base[`$JETTY_BASE`]. +Since the `$JETTY_BASE` directory you just created is empty, Jetty has no configuration it can use to assemble the server. + +NOTE: See the xref:arch/index.adoc[architecture section] of this document for more information on the design of Jetty's module system. + +You can explore what modules are available with the `--list-modules` flag: + +---- +$ java -jar $JETTY_HOME/start.jar --list-modules=* +---- + +Now try to enable the xref:protocols/index.adoc#http[`http`] module. + +NOTE: If you want to enable support for protocols like secure HTTP/1.1 or HTTP/2 or HTTP/3, or want to configure Jetty behind a load balancer, read xref:protocols/index.adoc[this section]. + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http +---- + +[jetty%nowrap] +.... +[jetty] +args=--add-modules=http +highlight=([a-z\-]+ *transitively enabled) +.... + +When Jetty enables the `http` module, it also automatically enables a number of transitive dependencies of the `http` module, such as the `server` module, the `logging-jetty` module, and so on. + +You can now start Jetty: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +args=--module=http +highlight=(\{.*:8080}) +.... + +Jetty is listening on port `8080` for clear-text HTTP/1.1 connections. +But since it has no web applications deployed, it will just reply with `404 Not Found` to every request. + +Before you <>, take a moment to see what happened to the `$JETTY_BASE` directory once you enabled the `http` module: + +[source] +---- +$JETTY_BASE +├── resources +│ └── jetty-logging.properties <1> +└── start.d <2> + └── http.ini <3> +---- + +<1> The `resources/jetty-logging.properties` file configures the server's logging level; this file was auto-generated when the `jetty-logging` module was activated as a transitive dependency of the `http` module. +<2> The `start.d/` directory contains the `+*.ini+` configuration files for any modules you have explicitly activated. +<3> The `start.d/http.ini` file is the `http` module configuration file, where you can specify values for the xref:modules/standard.adoc#http[`http` module properties]. + +[NOTE] +==== +By default, Jetty does *not* generate `+*.ini+` configuration files in `start.d/` for modules activated as transitive dependencies. +To manually configure such modules, you should activate them directly via Jetty's `--add-modules` flag. +==== + +In the `http.ini` file you can find the following (among other contents): + +.http.ini +[source] +---- +--module=http <1> +# jetty.http.port=8080 <2> +... +---- + +<1> This line enables the `http` module and should not be modified. +<2> This commented line specifies the default value for the `jetty.http.port` property, which is the network port that Jetty uses to listen for clear-text HTTP connections. + +Try changing the default port. +Open `http.ini`, uncomment the line containing `jetty.http.port=`, and change its value to `9999`: + +.http.ini +---- +--module=http +jetty.http.port=9999 +... +---- + +If you restart Jetty, it will use this new value: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +args=--module=http jetty.http.port=9999 +highlight=(\{.*:9999}) +.... + +You can also specify the value of a module property when you start up Jetty. +A property value specified on the command-line in this way will *override* the value configured in a module's `+*.ini+` file. + +---- +$ java -jar $JETTY_HOME/start.jar jetty.http.port=8080 +---- + +[jetty%nowrap] +.... +[jetty] +args=--module=http jetty.http.port=8080 +highlight=(\{.*:8080}) +.... + +For more detailed information about the Jetty start mechanism, you can read the xref:arch/index.adoc#start[Jetty start mechanism] section. + +[[deploy]] +== Deploying Web Applications + +You can deploy two types of web application resources with Jetty: + +* *Standard Web Application Archives*, in the form of `+*.war+` files or web application directories, defined by the https://jakarta.ee/specifications/servlet/[Servlet specification]. +Their deployment is described in <>. +* *Jetty context XML files*, that allow you to customize the deployment of standard web applications, and also allow you to use Jetty components -- and possibly custom components written by you -- to assemble and deploy your web applications. +Their deployment is described in xref:deploy/index.adoc[this section]. + +Jetty supports the deployment of both standard web applications and Jetty context XML files in a specific EE _environment_, such as the old Java EE 8, or Jakarta EE 9, or Jakarta {ee-current-caps}. + +Jetty supports _simultaneous_ deployment of web applications each to a possibly different environment, for example an old Java EE 8 web application alongside a new Jakarta {ee-current-caps} web application. + +Refer to the section about xref:deploy/index.adoc[deployment] for further information about how to deploy to different environments. + +In the following sections you can find simple examples of deployments of Jakarta {ee-current-caps} web applications. + +[[deploy-war]] +=== Deploying +*.war+ Files + +A standard Servlet web application is packaged in either a `+*.war+` file or in a directory with the structure of a `+*.war+` file. + +[NOTE] +==== +Recall that the structure of a `+*.war+` file is as follows: + +[source] +---- +mywebapp.war +├── index.html <1> +└── WEB-INF <2> + ├── classes/ <3> + ├── lib/ <4> + └── web.xml <5> +---- +<1> Publicly accessible resources such as `+*.html+`, `+*.jsp+`, `+*.css+`, `+*.js+` files, etc. are placed in `+*.war+` or in sub-directories of the `+*.war+`. +<2> `WEB-INF` is a special directory used to store anything related to the web application that must not be publicly accessible, but may be accessed by other resources. +<3> `WEB-INF/classes` stores the web application's compiled `+*.class+` files +<4> `WEB-INF/lib` stores the web application's `+*.jar+` files +<5> `WEB-INF/web.xml` is the web application deployment descriptor, which defines the components and the configuration of your web application. +==== + +To deploy a standard web application, you need to enable the xref:modules/standard.adoc#eeN-deploy[`{ee-current}-deploy` module]. + +[NOTE] +==== +The following examples assume you're deploying a Jakarta {ee-current-caps} application; for other versions of Jakarta EE, make sure to activate the corresponding `{ee-all}-deploy` module. + +Refer to the section about xref:deploy/index.adoc[deployment] for further information about how to deploy to different environments. +==== + +[source,subs=attributes+] +---- +$ java -jar $JETTY_HOME/start.jar --add-modules={ee-current}-deploy +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +args=--add-modules={ee-current}-deploy +.... + +The `{ee-current}-deploy` module creates `$JETTY_BASE/webapps`, which is the directory where Jetty looks for any `+*.war+` files or web application directories to deploy. + +Activating one of Jetty's `{ee-all}-deploy` modules enables web application deployment. +Whether these web applications are served via clear-text HTTP/1.1, or secure HTTP/1.1, or secure HTTP/2, or HTTP/3 (or even all of these protocols) depends on whether the correspondent Jetty protocol modules have been enabled. +Refer to the xref:protocols/index.adoc[section about protocols] for further information. + +Now you're ready to copy a web application to the `$JETTY_BASE/webapps` directory. +You can use one of the demos shipped with Jetty: + +[source,subs=attributes+] +---- +$ java -jar $JETTY_HOME/start.jar --add-modules={ee-current}-demo-simple +---- + +The `$JETTY_BASE` directory is now: + +[source,subs=attributes+] +---- +$JETTY_BASE +├── resources +│ └── jetty-logging.properties +├── start.d +│ ├── deploy.ini +│ ├── {ee-current}-demo-simple.ini +│ └── http.ini +└── webapps + └── {ee-current}-demo-simple.war +---- + +Now start Jetty: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http,deploy,{ee-current}-demo-simple +highlight=WebAppContext +.... + +Note the highlighted line that logs the deployment of `{ee-current}-demo-simple.war`. + +Now you can access the web application by pointing your browser to `pass:a[http://localhost:8080/{ee-current}-demo-simple]`. + +[[deploy-war-advanced]] +=== Advanced Deployment + +If you want to customize the deployment of your web application -- for example, by specifying a `contextPath` different from the file/directory name, or by specifying JNDI entries, or by specifying virtual hosts -- read xref:deploy/index.adoc[this section]. diff --git a/documentation/jetty/modules/operations-guide/pages/deploy/index.adoc b/documentation/jetty/modules/operations-guide/pages/deploy/index.adoc new file mode 100644 index 000000000000..e13d18934407 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/deploy/index.adoc @@ -0,0 +1,485 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Web Application Deployment + +Most of the time you want to be able to customize the deployment of your web applications, for example by changing the `contextPath`, or by adding JNDI entries, or by configuring virtual hosts, etc. + +Jetty supports the deployment of each web application to a specific _environment_. +The available environments are: + +* Java EE 8 -- Supports Servlet 4.0 (and associated specifications) in the `+javax.*+` packages. +* Jakarta EE 9 -- Supports Servlet 5.0 (and associated specifications) in the `+jakarta.*+` packages. +* Jakarta EE 10 -- Supports Servlet 6.0 (and associated specifications) in the `+jakarta.*+` packages. +* Jetty Core -- Supports web applications written against the Jetty `Handler` APIs, without any Servlet dependencies. + +This means that you can simultaneously deploy an old Java EE 8 web application, say `old-ee8.war`, alongside a new Jakarta {ee-current-caps} web application, say `new-{ee-current}.war`, alongside a web application that only uses the Jetty `Handler` APIs, say `app-jetty.xml`. + +The customization of the deployment (for example, web application context path, etc.) is performed by processing <>. + +The `deploy` module contains the `DeploymentManager` component that scans the `$JETTY_BASE/webapps` directory for changes, following the deployment rules described in <>. + +For each specific environment there is a specific deploy module that you must enable: + +* For Java EE 8, xref:modules/standard.adoc#eeN-deploy[`ee8-deploy`] +* For Java EE 9, xref:modules/standard.adoc#eeN-deploy[`ee9-deploy`] +* For Java {ee-current-caps}, xref:modules/standard.adoc#eeN-deploy[`{ee-current}-deploy`] +* For Jetty Core, xref:modules/standard.adoc#core-deploy[`core-deploy`] + +Each of these modules provide the environment specific features, and depend on the `deploy` module that provides the scanning features. + +[[hot-static]] +== Hot vs Static Deployment + +The `DeploymentManager` scans the `$JETTY_BASE/webapps` directory for changes every `N` seconds, where `N` is configured via the `jetty.deploy.scanInterval` property. + +By default, the scan interval is `0` seconds, which means _static_ deployment, and the `DeploymentManager` will not scan the `$JETTY_BASE/webapps` directory for changes. +This means that to deploy/redeploy/undeploy a web application you will need to stop and restart Jetty. + +Setting the scan interval to a value of `1` second (or greater) means that _hot_ deployment is enabled: if a file is added/changed/removed from the `$JETTY_BASE/webapps` directory, the `DeploymentManager` will notice the change and respectively deploy/redeploy/undeploy the web application. + +The following command line enables _hot_ deployment by specifying the `jetty.deploy.scanInterval` property on the command line, and therefore only for this particular run: + +---- +$ java -jar $JETTY_HOME/start.jar jetty.deploy.scanInterval=1 +---- + +To make _hot_ deployment persistent, you need to edit the appropriate `-deploy` module configuration file, `$JETTY_BASE/start.d/-deploy.ini` (eg: `ee10-deploy.ini`), uncomment the module property `jetty.deploy.scanInterval` and change the value to `1` second (or greater): + +.-deploy.ini +[source,subs=+quotes] +---- +--module=deploy +#jetty.deploy.scanInterval=1# +... +---- + +[[rules]] +== Deployment Rules + +_Adding_ a `+*.war+` file, a `+*.war+` directory, a Jetty context XML file or a normal directory to `$JETTY_BASE/webapps` causes the `DeploymentManager` to deploy the new web application. + +_Updating_ a `+*.war+` file or a Jetty context XML file causes the `DeploymentManager` to redeploy the web application, which means that the Jetty context component representing the web application is stopped, then reconfigured, and then restarted. + +_Removing_ a `+*.war+` file, a `+*.war+` directory, a Jetty context XML file or a normal directory from `$JETTY_BASE/webapps` causes the `DeploymentManager` to undeploy the web application, which means that the Jetty context component representing the web application is stopped and removed from the Jetty server. + +[[rules-context-path]] +=== Context Path Resolution + +When a file or directory is added to `$JETTY_BASE/webapps`, the `DeploymentManager` derives the web application `contextPath` from the file or directory name, with the following rules: + +* If the directory name is, for example, `mywebapp/`, it is deployed as a standard web application if it contains a `WEB-INF/` subdirectory, otherwise it is deployed as a web application of static content. +The `contextPath` would be `/mywebapp` (that is, the web application is reachable at `+http://localhost:8080/mywebapp/+`). +* If the directory name is `ROOT`, case-insensitive, the `contextPath` is `/` (that is, the web application is reachable at `+http://localhost:8080/+`). +* If the directory name ends with `.d`, for example `config.d/`, it is ignored, although it may be referenced to configure other web applications (for example to store common files). +* If the `+*.war+` file name is, for example, `mywebapp.war`, it is deployed as a standard web application with the context path `/mywebapp` (that is, the web application is reachable at `+http://localhost:8080/mywebapp/+`). +* If the file name is `ROOT.war`, case-insensitive, the `contextPath` is `/` (that is, the web application is reachable at `+http://localhost:8080/+`). +* If both the `mywebapp.war` file and the `mywebapp/` directory exist, only the file is deployed. +This allows the directory with the same name to be the `+*.war+` file unpack location and avoid that the web application is deployed twice. +* A <> named `mywebapp.xml` is deployed as a web application by processing the directives contained in the XML file itself, which must set the `contextPath`, which could be different from the name of the XML file. +* If both `mywebapp.xml` and `mywebapp.war` exist, only the XML file is deployed. +This allows the XML file to reference the `+*.war+` file and avoid that the web application is deployed twice. + +[[rules-environment]] +=== Environment Resolution + +A web application is always deployed to a specific environment, which is either configured for the deployed application or set to the default environment. + +If only a single specific deployer module is enabled, for example `{ee-current}-deploy`, then it is the default environment and applications will be deployed to it without any additional configuration. + +If multiple deployer modules are enabled, then the default environment is: + +* The most recent Jakarta EE environment of the `{ee-all}-deploy` modules that are enabled. +* Otherwise, the `core` environment, if the `core-deploy` module is enabled. +* Otherwise, no deployer environment has been enabled, and therefore no application can be deployed. + +For example, if `core-deploy`, `ee9-deploy` and the `{ee-current}-deploy` modules are enabled, then `{ee-current}` is the default environment, to which applications will be deployed unless otherwise configured (see below). + +To configure a specific environment for an application, you add a `+*.properties+` file with the same name of the web application. +For example, an application deployed to `$JETTY_BASE/webapps/my-ee9-app.war` is configured with the file `$JETTY_BASE/webapps/my-ee9-app.properties`, with the following content: + +.my-ee9-app.properties +[,properties] +---- +environment=ee9 +---- + +In case of simultaneous multiple deployer environments, it is good practice to always specify the `+*.properties+` file for your web applications. + +[CAUTION] +==== +If you do *not* specify the `+*.properties+` file for your web applications, then the deployer for the default environment will be used. + +For example, if you have enabled the deployer Jetty module for all Jakarta EE versions, and you deploy an EE 9 web application _without_ the `+*.properties+` file, then it will be deployed by the {ee-current-caps} deployer, with unspecified results. + +This unspecified deployment may not work as the EE 9 web application may use APIs that have been removed in {ee-current-caps}, causing an error at runtime. +==== + +[[jetty]] +== Deploying Jetty Context XML Files + +A Jetty context XML file is a xref:xml/index.adoc[Jetty XML file] that allows you to customize the deployment of web applications. + +NOTE: Recall that the `DeploymentManager` component of the Jetty `deploy` module <> to Jetty context XML files over `+*.war+` files or directories. + +To deploy a web application using a Jetty context XML file, simply place the file in the `$JETTY_BASE/webapps` directory. + +A simple Jetty context XML file, for example named `wiki.xml` is the following: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + + /wiki + /opt/myapps/myapp.war + +---- +<1> Configures a link:{javadoc-url}/org/eclipse/jetty/{ee-current}/webapp/WebAppContext.html[`WebAppContext`], which is the Jetty component that represents a standard Servlet web application. +<2> Specifies the web application `contextPath`, which may be different from the `+*.war+` file name. +<3> Specifies the file system path of the `+*.war+` file. + +The Jetty content XML file may be accompanied by a `+*.properties+` file that specifies the xref:deploy/index.adoc[environment] to use for the deployment: + +.wiki.properties +[,properties,subs=attributes+] +---- +environment={ee-current} +---- + +Refer to <> for more information about specifying the environment. + +The `$JETTY_BASE` directory would look like this: + +---- +$JETTY_BASE +├── resources +│ └── jetty-logging.properties +├── start.d +│ ├── deploy.ini +│ └── http.ini +└── webapps + ├── wiki.properties + └── wiki.xml +---- + +TIP: The `+*.war+` file may be placed anywhere in the file system and does not need to be placed in the `$JETTY_BASE/webapps` directory. + +IMPORTANT: If you place both the Jetty context XML file _and_ the `+*.war+` file in the `$JETTY_BASE/webapps` directory, remember that they must have the same file name, for example `wiki.xml` and `wiki.war`, so that the `DeploymentManager` deploys the web application only once using the Jetty context XML file (and not the `+*.war+` file). + +You can use the features of xref:xml/index.adoc[Jetty XML files] to avoid to hard-code file system paths or other configurations in your Jetty context XML files, for example by using system properties: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + + /wiki + /myapp.war + +---- + +Note how the `+*.war+` file path is now obtained by resolving the system property `myapps.dir` that you can specify on the command line when you start Jetty: + +---- +$ java -jar $JETTY_HOME/start.jar -Dmyapps.dir=/opt/myapps +---- + +[[jndi]] +== Configuring JNDI Entries + +A web application may _reference_ a JNDI entry, such as a JDBC `DataSource` from the web application `web.xml` file. +The JNDI entry must be _defined_ in a xref:jndi/index.adoc#xml[Jetty XML file], for example a context XML like so: + +.mywebapp.xml +[,xml,subs="attributes+,+quotes"] +---- + + + + + /mywebapp + /opt/webapps/mywebapp.war + # + + jdbc/myds + + + jdbc:mysql://localhost:3306/databasename + user + password + + + # + +---- + +For more information and examples on how to use JNDI in Jetty, refer to the xref:jndi/index.adoc[JNDI] feature section. + +[IMPORTANT] +==== +Class `com.mysql.cj.jdbc.MysqlConnectionPoolDataSource` is present in the MySQL JDBC driver file, `mysql-connector-java-.jar`, which must be available on the server's classpath . + +If the class is instead present _within_ the web application, then the JNDI entry must be declared in a `WEB-INF/jetty-env.xml` file - see the xref:jndi/index.adoc[JNDI] feature section for more information and examples. +==== + +[[virtual-hosts]] +== Configuring Virtual Hosts + +A _virtual host_ is an internet domain name, registered in the Domain Name Server (DNS), for an IP address such that multiple virtual hosts will resolve to the same IP address of a single server instance. + +If you have multiple web applications deployed on the same Jetty server, by using virtual hosts you will be able to target a specific web application. + +For example, you may have a web application for your business and a web application for your hobbies , both deployed in the same Jetty server. +By using virtual hosts, you will be able to have the first web application available at `+http://domain.biz/+`, and the second web application available at `+http://hobby.net/+`. + +Another typical case is when you want to use different subdomains for different web application, for example a project website is at `+http://project.org/+` and the project documentation is at `+http://docs.project.org+`. + +Virtual hosts can be used with any context that is a subclass of link:{javadoc-url}/org/eclipse/jetty/server/handler/ContextHandler.html[ContextHandler]. + +[[virtual-hosts-names]] +=== Virtual Host Names + +Jetty supports the following variants to be specified as virtual host names: + +`www.hostname.com`:: +A fully qualified domain name. It is important to list all variants as a site may receive traffic for both `www.hostname.com` and `hostname.com`. + +`*.hostname.com`:: +A wildcard domain name which will match only one level of arbitrary subdomains. +*.foo.com will match www.foo.com and m.foo.com, but not www.other.foo.com. + +`10.0.0.2`:: +An IP address may be set as a virtual host to indicate that a web application should handle requests received on the network interface with that IP address for protocols that do not indicate a host name such as HTTP/0.9 or HTTP/1.0. + +`@ConnectorName`:: +A Jetty server `Connector` name to indicate that a web application should handle requests received on the server `Connector` with that name, and therefore received on a specific socket address (either an IP port for `ServerConnector`, or a Unix-Domain path for `UnixDomainServerConnector`). +A server `Connector` name can be set via link:{javadoc-url}/org/eclipse/jetty/server/AbstractConnector.html#setName(java.lang.String)[]. + +`www.√integral.com`:: +Non-ASCII and https://en.wikipedia.org/wiki/Internationalized_domain_name[IDN] domain names can be set as virtual hosts using https://en.wikipedia.org/wiki/Punycode[Puny Code] equivalents that may be obtained from a https://www.punycoder.com/[Punycode/IDN converters]. +For example if the non-ASCII domain name `www.√integral.com` is given to a browser, then the browser will make a request that uses the domain name `www.xn--integral-7g7d.com`, which is the name that should be added as the virtual host name. + +[[virtual-hosts-config]] +=== Virtual Hosts Configuration + +If you have a web application `mywebapp.war` you can configure its virtual hosts in this way: + +[,xml,subs=attributes+] +---- + + + + + /mywebapp + /opt/webapps/mywebapp.war + + + mywebapp.com + www.mywebapp.com + mywebapp.net + www.mywebapp.net + + + +---- + +Your web application will be available at: + +* `+http://mywebapp.com/mywebapp+` +* `+http://www.mywebapp.com/mywebapp+` +* `+http://mywebapp.net/mywebapp+` +* `+http://www.mywebapp.net/mywebapp+` + +[NOTE] +==== +You configured the `contextPath` of your web application to `/mywebapp`. + +As such, a request to `+http://mywebapp.com/other+` will not match your web application because the `contextPath` does not match. + +Likewise, a request to `+http://other.com/mywebapp+` will not match your web application because the virtual host does not match. +==== + +[[virtual-hosts-same-context]] +=== Same Context Path, Different Virtual Hosts + +If you want to deploy different web applications to the same context path, typically the root context path `/`, you must use virtual hosts to differentiate among web applications. + +You have `domain.war` that you want to deploy at `+http://domain.biz/+` and `hobby.war` that you want to deploy at `+http://hobby.net+`. + +To achieve this, you simply use the same context path of `/` for each of your webapps, while specifying different virtual hosts for each of your webapps: + +.domain.xml +[,xml,subs=attributes+] +---- + + + + + / + /opt/webapps/domain.war + + + domain.biz + + + +---- + +.hobby.xml +[,xml,subs=attributes+] +---- + + + + + / + /opt/webapps/hobby.war + + + hobby.net + + + +---- + +[[virtual-hosts-port]] +=== Different Port, Different Web Application + +Sometimes it is required to serve different web applications from different socket addresses (either different IP ports, or different Unix-Domain paths), and therefore from different server ``Connector``s. + +For example, you want requests to `+http://localhost:8080/+` to be served by one web application, but requests to `+http://localhost:9090/+` to be served by another web application. + +This configuration may be useful when Jetty sits behind a load balancer. + +In this case, you want to xref:protocols/index.adoc[configure multiple connectors], each with a different name, and then reference the connector name in the web application virtual host configuration: + +.domain.xml +[,xml,subs="attributes+,+quotes"] +---- + + + + + / + /opt/webapps/domain.war + + + #@port8080# + + + +---- + +.hobby.xml +[,xml,subs="attributes+,+quotes"] +---- + + + + + / + /opt/webapps/hobby.war + + + #@port9090# + + + +---- + +[NOTE] +==== +Web application `domain.war` has a virtual host of `@port8080`, where `port8080` is the name of a Jetty connector. + +Likewise, web application `hobby.war` has a virtual host of `@port9090`, where `port9090` is the name of another Jetty connector. + +See xref:protocols/index.adoc[this section] for further information about how to configure connectors. +==== + +[[extract-war]] +== Configuring `+*.war+` File Extraction + +By default, `+*.war+` files are uncompressed and its content extracted in a temporary directory. +// TODO: reference the `work` module and how it works, perhaps in a section about the `deploy` module? +The web application resources are served by Jetty from the files extracted in the temporary directory, not from the files within the `+*.war+` file, for performance reasons. + +If you do not want Jetty to extract the `+*.war+` files, you can disable this feature, for example: + +.mywebapp.xml +[,xml,subs="attributes+,+quotes"] +---- + + + + + /mywebapp + /opt/webapps/mywebapp.war + #false# + +---- + +[[jetty-override-web-xml]] +== Overriding `web.xml` + +You can configure an additional `web.xml` that complements the `web.xml` file that is present in the web application `+*.war+` file. +This additional `web.xml` is processed _after_ the `+*.war+` file `web.xml`. +This allows you to add host specific configuration or server specific configuration without having to extract the web application `web.xml`, modify it, and repackage it in the `+*.war+` file. + +.mywebapp.xml +[,xml,subs="attributes+,+quotes"] +---- + + + + + /mywebapp + /opt/webapps/mywebapp.war + #/opt/webapps/mywebapp-web.xml# + +---- + +The format of the additional `web.xml` is exactly the same as a standard `web.xml` file, for example: + +.mywebapp-web.xml +[,xml,linenums,highlight=10-11] +---- + + + + my-servlet + + host + 192.168.0.13 + + + +---- + +In the example above, you configured the `my-servlet` Servlet (defined in the web application `web.xml`), adding a host specific `init-param` with the IP address of the host. + + +// TODO: move this section to its own file +// TODO: configuring from the Jetty context XML file happens before web.xml +// What about jetty-web.xml? Can this be specified externally, e.g. WebAppContext.setJettyWebXml() ? +[[init-params]] +== Configuring ``init-param``s + +TODO diff --git a/documentation/jetty/modules/operations-guide/pages/features/index.adoc b/documentation/jetty/modules/operations-guide/pages/features/index.adoc new file mode 100644 index 000000000000..083fe540aa3a --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/features/index.adoc @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Eclipse Jetty Features + +If you know Eclipse Jetty already, jump to a feature: + +Protocols:: +* xref:protocols/index.adoc#http[HTTP/1.1 Support] +* xref:protocols/index.adoc#http2[HTTP/2 Support] +* xref:protocols/index.adoc#http3[HTTP/3 Support] +* xref:protocols/index.adoc#websocket[WebSocket Support] + +Technologies:: +* xref:annotations/index.adoc[Servlet Annotations] +* xref:jaas/index.adoc[JAAS] +* xref:jndi/index.adoc[JNDI] +* xref:jsp/index.adoc[JSP] +* xref:jmx/index.adoc[JMX Monitoring & Management] + +Clustering:: +* xref:session/index.adoc[HTTP Session Caching and Clustering] + +Performance:: +* xref:server/index.adoc#threadpool-virtual[Virtual Threads] +* xref:quickstart/index.adoc[Faster Web Application Deployment] diff --git a/documentation/jetty/modules/operations-guide/pages/howtos/index.adoc b/documentation/jetty/modules/operations-guide/pages/howtos/index.adoc new file mode 100644 index 000000000000..49d7b9cbb9c1 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/howtos/index.adoc @@ -0,0 +1,24 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Eclipse Jetty How-Tos + +* xref:protocols/index.adoc#http[Configure Clear-Text HTTP/1.1] +* xref:protocols/index.adoc#https[Configure Secure HTTP/1.1 (https)] +* xref:protocols/index.adoc#http2c[Configure Clear-Text HTTP/2] +* xref:protocols/index.adoc#http2s[Configure Secure HTTP/2] +* xref:protocols/index.adoc#http3[Configure HTTP/3] +* xref:protocols/index.adoc#proxy[Configure Jetty Behind a Load Balancer or Reverse Proxy] +* xref:server/index.adoc#logging[Configure Jetty Logging] +* xref:server/index.adoc#threadpool[Configure Jetty Thread Pool and Virtual Threads] +* xref:troubleshooting/index.adoc[Troubleshooting] diff --git a/documentation/jetty/modules/operations-guide/pages/index.adoc b/documentation/jetty/modules/operations-guide/pages/index.adoc new file mode 100644 index 000000000000..6f97667b88ad --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/index.adoc @@ -0,0 +1,17 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +[reftext=Operations Guide] += Jetty {page-version} Operations Guide + +The Eclipse Jetty Operations Guide targets sysops, devops, and developers who want to install Eclipse Jetty as a standalone server to deploy web applications. diff --git a/documentation/jetty/modules/operations-guide/pages/jaas/index.adoc b/documentation/jetty/modules/operations-guide/pages/jaas/index.adoc new file mode 100644 index 000000000000..346feceffafb --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/jaas/index.adoc @@ -0,0 +1,333 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += JAAS + +JAAS implements a Java version of the standard Pluggable Authentication Module (PAM) framework. + +JAAS can be used for two purposes: + +* for authentication of users, to reliably and securely determine who is currently executing Java code, regardless of whether the code is running as an application, an applet, a bean, or a servlet +* for authorization of users to ensure they have the access control rights (permissions) required to do the actions performed + +JAAS authentication is performed in a pluggable fashion. +This permits applications to remain independent from underlying authentication technologies. +New or updated authentication technologies can be plugged under an application without requiring modifications to the application itself. + +See Java Authentication and Authorization Service (JAAS) http://java.sun.com/javase/7/docs/technotes/guides/security/jaas/JAASRefGuide.html[Reference Guide] for more information about JAAS. + +The Jetty JAAS support aims to dictate as little as possible whilst providing a sufficiently flexible infrastructure to allow users to drop either one of the <>, or their +own custom https://docs.oracle.com/javase/7/docs/api/javax/security/auth/spi/LoginModule.html[LoginModule]s. + +[[configuration]] +== Configuration + +[[module]] +=== The `jaas` module + +Enable the `{ee-all}-jaas` module appropriate for your EE platform: + +[source] +---- +include::{jetty-home}/modules/jaas.mod[] +---- + +The configurable items in the resulting `$jetty.base/start.d/jaas.ini` file are: + +jetty.jaas.login.conf:: +This is the location of the file that will be referenced by the system property `java.security.auth.login.config`: Jetty sets this system property for you based on the value of this property. +The value of this property is assumed to be _relative to ``$JETTY_BASE``_. +The default value is `etc/login.conf`, which resolves to `$JETTY_BASE/etc/login.conf`. +If you don't want to put your login module configuration file here, you can change this property to point to where it is. + +See more about the contents of this file in the <> section. + +[[webapp]] +=== Configure the webapp for JAAS + +The `` in `web.xml` will be used to identify the `org.eclipse.jetty.security.jaas.JAASLoginService` declaration that integrates JAAS with Jetty. + +For example, this `web.xml` contains a realm called `Test JAAS Realm`: + +[,xml] +---- + + FORM + Test JAAS Realm + + /login/login + /login/error + + +---- +<1> The name of the realm, which must be _identical_ to the name of an `org.eclipse.jetty.security.jaas.JAASLoginService` declaration. + +We now need to declare an `org.eclipse.jetty.security.jaas.JAASLoginService` that references the realm name of `Test JAAS Realm`. +Here's an example of a suitable XML snippet: + +[,xml] +---- + + Test JAAS Realm + xyz + +---- +<1> The name is the _same_ as that declared in the `` in `web.xml`. +<2> The name that identifies a set of `javax.security.auth.spi.LoginModule` configurations that comprise the <> identified in the `jetty.jaas.login.conf` property of the <>. + +The `org.eclipse.jetty.security.jaas.JAASLoginService` can be declared in a couple of different places, pick whichever suits your purposes best: + +* If you have more than one webapp that you would like to use the same security infrastructure, then you can declare your `org.eclipse.jetty.security.jaas.JAASLoginService` as a bean that is added to the `org.eclipse.jetty.server.Server`. +The file in which you declare this needs to be on Jetty's execution path. +The recommended procedure is to create a file in your `$jetty.base/etc` directory and then ensure it is on the classpath either by adding it to the Jetty xref:start/index.adoc[start command line], or more conveniently to a xref:modules/custom.adoc[custom module]. ++ +Here's an example of this type of XML file: ++ +[,xml] +---- + + + + + + + Test JAAS Realm + xyz + + + + +---- + +* Alternatively, if you want to use JAAS with a specific webapp only, you declare your `org.eclipse.jetty.security.jaas.JAASLoginService` in a context XLM file specific to that webapp: ++ +[,xml] +---- + + + + + + + + Test JAAS Realm + xyz + + + + + +---- + +[[loginconf]] +=== Configure JAAS + +We now need to setup the contents of the file we specified as the `jetty.jaas.login.conf` property when we <>. +Refer to the https://docs.oracle.com/javase/7/docs/api/javax/security/auth/login/Configuration.html[syntax rules] of this file for a full description. + +Remembering the example we set up <>, the contents of the `$jetty.base/etc/login.conf` file could look as follows: + +[source] +---- +xyz { <1> + com.acme.SomeLoginModule required debug=true; <2> + com.other.OtherLoginModule optional; <3> +}; +---- +<1> The name of the configuration _exactly_ as specified in your `org.eclipse.jetty.security.jaas.JAASLoginService` declaration. +<2> The first `LoginModule` declaration, containing the classname of the `LoginModule` and its configuration properties. +<3> A second `LoginModule` declaration. +You can provide as many `LoginModule` alternatives as you like, with a minimum of one. +Refer to the https://docs.oracle.com/javase/7/docs/api/javax/security/auth/login/Configuration.html[JAAS documentation] for more information on the standard configuration properties, and how JAAS interprets this file. + +[[loginmodules]] +== Provided LoginModules + +* link:{javadoc-url}/org/eclipse/jetty/security/jaas/spi/JDBCLoginModule.html[`org.eclipse.jetty.security.jaas.spi.JDBCLoginModule`] +* link:{javadoc-url}/org/eclipse/jetty/security/jaas/spi/PropertyFileLoginModule.html[`org.eclipse.jetty.security.jaas.spi.PropertyFileLoginModule`] +* link:{javadoc-url}/org/eclipse/jetty/security/jaas/spi/DataSourceLoginModule.html[`org.eclipse.jetty.security.jaas.spi.DataSourceLoginModule`] +* link:{javadoc-url}/org/eclipse/jetty/security/jaas/spi/LdapLoginModule.html[`org.eclipse.jetty.security.jaas.ldap.LdapLoginModule`] + +[[og-password]] +[NOTE] +==== +Passwords can be obfuscated using the xref:tools/index.adoc#password[Jetty Password tool]. +==== + +=== JDBCLoginModule + +The `org.eclipse.jetty.security.jaas.spi.JDBCLoginModule` stores user passwords and roles in a database accessed via JDBC calls. +You can configure the JDBC connection information, as well as the names of the table and columns storing the username and credential, and the names of the table and columns storing the roles. + +Here is an example <> entry for it using an HSQLDB driver: + +[source] +---- +jdbc { <1> + org.eclipse.jetty.security.jaas.spi.JDBCLoginModule required <2><3> + dbUrl="jdbc:hsqldb:." <4> + dbUserName="sa" <5> + dbDriver="org.hsqldb.jdbcDriver" <6> + userTable="myusers" <7> + userField="myuser" <8> + credentialField="mypassword" <9> + userRoleTable="myuserroles" <10> + userRoleUserField="myuser" <11> + userRoleRoleField="myrole"; <12> +}; +---- +<1> The name of the configuration. +<2> The name of the `LoginModule` class. +<3> A standard JAAS flag making successful authentication via this `LoginModule` mandatory. +<4> The JDBC url used to connect to the database. +<5> The name of the JDBC user to use for the connection. +<6> The name of the JDBC Driver class. +<7> The name of the table holding the user authenication information. +<8> The name of the column holding the user name. +<9> The name of the column holding the user credential. +<10> The name of the table holding the user authorization information. +<11> The name of the column holding the user name. +<12> The name of the column holding the user role. + +The properties *7*-*12* are used to format the following queries: + +[,sql] +---- +select from where =? +select from where =? +---- + +Credential and role information is lazily read from the database when a previously unauthenticated user requests authentication. +Note that this information is _only_ cached for the length of the authenticated session. +When the user logs out or the session expires, the information is flushed from memory. + +Note that passwords can be stored in the database in plain text or encoded formats -- see the note on "Passwords/Credentials" above. + +=== DataSourceLoginModule + +Similar to the `org.eclipse.jetty.security.jaas.spi.JDBCLoginModule`, but using a `javax.sql.DataSource` to connect to the database instead of a JDBC driver. +The `javax.sql.DataSource` is obtained at runtime by performing a JNDI lookup on `java:comp/env/$\{dnJNDIName}`. + +A sample login module configuration for this `LoginModule`: + +[source] +---- +ds { <1> + org.eclipse.jetty.security.jaas.spi.DataSourceLoginModule required <2><3> + dbJNDIName="ds" <4> + userTable="myusers" <5> + userField="myuser" <6> + credentialField="mypassword" <7> + userRoleTable="myuserroles" <8> + userRoleUserField="myuser" <9> + userRoleRoleField="myrole"; <10> +}; +---- +<1> The name of the configuration. +<2> The name of the `LoginModule` class. +<3> A standard JAAS flag making successful authentication via this `LoginModule` mandatory. +<4> The JNDI name, relative to `java:comp/env/` to lookup to obtain the `javax.sql.DataSource`. +<5> The name of the table holding the user authenication information. +<6> The name of the column holding the user name. +<7> The name of the column holding the user credential. +<8> The name of the table holding the user authorization information. +<9> The name of the column holding the user name. +<10> The name of the column holding the user role. + +=== PropertyFileLoginModule + +With this login module implementation, the authentication and role information is read from a property file. + +[source] +---- +props { <1> + org.eclipse.jetty.security.jaas.spi.PropertyFileLoginModule required <2><3> + file="/somewhere/somefile.props"; <4> +}; +---- +<1> The name of the configuration. +<2> The name of the `LoginModule` class. +<3> A standard JAAS flag making successful authentication via this `LoginModule` mandatory. +<4> The location of a properties file containing the authentication and authorization information. + +The property file must be of the format: + +[source] +---- +: [, ...] +---- + +Here's an example: + +---- +fred: OBF:1xmk1w261u9r1w1c1xmq,user,admin +harry: changeme,user,developer +tom: MD5:164c88b302622e17050af52c89945d44,user +dick: CRYPT:adpexzg3FUZAk,admin +---- + +The contents of the file are fully read in and cached in memory the first time a user requests authentication. + +=== LdapLoginModule + +The `org.eclipse.jetty.security.jaas.spi.LdapLoginModule` uses LDAP to access authentication and authorization information stored in a directory. +The LDAP connection information and structure of the authentication/authorization data can be configured. + +Here's an example: + +[source] +---- +example { <1> + org.eclipse.jetty.security.jaas.spi.LdapLoginModule required <2><3> + contextFactory="com.sun.jndi.ldap.LdapCtxFactory" <4> + hostname="ldap.example.com" <5> + port="389" <6> + bindDn="cn=Directory Manager" <7> + bindPassword="directory" <8> + authenticationMethod="simple" <9> + useLdaps="true" <10> + userBaseDn="ou=people,dc=alcatel" <11> + userRdnAttribute="uid" <12> + userIdAttribute="cn" <13> + userPasswordAttribute="userPassword" <14> + userObjectClass="inetOrgPerson" <15> + roleBaseDn="ou=groups,dc=example,dc=com" <16> + roleNameAttribute="cn" <17> + roleMemberAttribute="uniqueMember" <18> + roleObjectClass="groupOfUniqueNames"; <19> + forceBindingLogin="false" <20> + debug="false" <21> +}; +---- +<1> The name of the configuration. +<2> The name of the `LoginModule` class. +<3> A standard JAAS flag making successful authentication via this `LoginModule` mandatory. +<4> The name of the context factory to use for the LDAP connection. +<5> The hostname for the LDAP connection. Optional. +<6> The port for the LDAP connection. Optional. +<7> The caller security Principal. Optional. +<8> The caller security credential. Optional. +<9> The security level for the LDAP connection environment. Optional. +<10> If true, use `ldaps` instead of `ldap` for the connection url. +<11> The distinguished name of the directory to search for user information. +<12> The name of the attribute for the user roles. +<13> The name of the attribute for the user id. +<14> The name of the attribute for the user password. +<15> The `ObjectClass` for users. +<16> The distinguished name of the directory to search for role information. +<17> The name of the attribute for roles. +<18> The name of the attribute storing the user for the roles `ObjectClass`. +<19> The name of the `ObjectClass` for roles. +<20> If true, the authentication proceeds on the basis of a successful LDAP binding using the username and credential provided by the user. +If false, then authentication proceeds based on username and password information retrieved from LDAP. +<21> If true, failed login attempts are logged on the server. diff --git a/documentation/jetty/modules/operations-guide/pages/jaspi/index.adoc b/documentation/jetty/modules/operations-guide/pages/jaspi/index.adoc new file mode 100644 index 000000000000..32a18ab5cfd0 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/jaspi/index.adoc @@ -0,0 +1,70 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += JASPI + +Enabling this module allows Jetty to utilize authentication modules that implement the Jakarta Authentication (JASPI) specification. JASPI provides an SPI (Service Provider Interface) for pluggable, portable, and standardized authentication modules. Compatible modules are portable between servers that support the JASPI specification. This module provides a bridge from Jakarta Authentication to the Jetty Security framework. + +Only modules conforming to the "Servlet Container Profile" with the ServerAuthModule interface within the https://jakarta.ee/specifications/authentication/2.0/jakarta-authentication-spec-2.0.pdf[JakartaAuthentication] are supported. These modules must be configured before start-up. Operations for runtime registering or de-registering authentication modules are not supported. + +[[configuration]] +== Configuration + +[[module]] +=== The `jaspi` module + +Enable the `jaspi` module: + +---- +include::{jetty-home}/modules/ee10-jaspi.mod[] +---- + +[[xml]] +=== Configure JASPI + +Activate either the `ee9-jaspi` or `{ee-current}-jaspi` module, whichever matches your EE platform version. + +[source,subs=attributes+] +---- +$ java -jar $JETTY_HOME/start.jar --add-modules={ee-current}-jaspi +---- + +You can then register a `AuthConfigProvider` onto the static `AuthConfigFactory` obtained with `AuthConfigFactory.getFactory()`. This registration can be done in the XML configuration file which will be copied to `$JETTY_BASE/etc/jaspi/jaspi-authmoduleconfig.xml` when the module is enabled. + +==== JASPI Demo +The `ee9-jaspi-demo` and `{ee-current}-jaspi-demo` modules illustrate setting up HTTP Basic Authentication using the EE9 and {ee-current-caps} Jakarta Authentication modules that come packaged with Jetty. + +The following example uses Jetty's {ee-current-caps} implementation of `AuthConfigProvider` to register a `ServerAuthModule` directly. + +[,xml] +---- +include::{jetty-home}/etc/jaspi/jetty-ee10-jaspi-demo.xml[] +---- + +Other custom or 3rd party modules that are compatible with the `ServerAuthModule` interface in JASPI can be registered in the same way. + +=== Integration with Jetty Authentication Mechanisms + +To integrate with Jetty authentication mechanisms you must add a `LoginService` to your context. The `LoginService` provides a way for you to obtain a `UserIdentity` from a username and credentials. JASPI can interact with this Jetty `LoginService` by using the `PasswordValidationCallback`. + +The `CallerPrincipalCallback` and `GroupPrincipalCallback` do not require use of a Jetty `LoginService`. The principal from the `CallerPrincipalCallback` will be used directly with the `IdentityService` to produce a `UserIdentity`. + +=== Replacing the Jetty DefaultAuthConfigFactory + +Jetty provides an implementation of the `AuthConfigFactory` interface which is used to register `AuthConfigProviders`. This can be replaced by a custom implementation by adding a custom module which provides `auth-config-factory`. +This custom module must reference an XML file which sets a new instance of the `AuthConfigFactory` with the static method `AuthConfigFactory.setFactory()`. +For an example of this see the `{ee-current}-jaspi-default-auth-config-factory` module, which provides the default implementation used by Jetty. + +---- +include::{jetty-home}/modules/ee10-jaspi-default-auth-config-factory.mod[] +---- diff --git a/documentation/jetty/modules/operations-guide/pages/jmx/index.adoc b/documentation/jetty/modules/operations-guide/pages/jmx/index.adoc new file mode 100644 index 000000000000..f4186bd9ca30 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/jmx/index.adoc @@ -0,0 +1,281 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += JMX Monitoring & Management + +Monitoring and management of a Jetty server is important because it allows you to monitor the status of the server (_"Is the server processing requests?"_) and to manage -- i.e. read and possibly change -- its configuration. + +The ability to read and change the Jetty configuration is very important for troubleshooting Jetty -- please refer to the xref:troubleshooting/index.adoc[troubleshooting section] for more information. + +Jetty relies on the Java Management Extensions (JMX) APIs included in OpenJDK to provide monitoring and management. + +The JMX APIs support a JVM-local `MBeanServer`, accessible only from within the JVM itself (or by tools that can _attach_ to a running JVM), and a way to expose the `MBeanServer` to remote clients via Java's RMI (Remote Method Invocation). + +[[local]] +== Enabling Local JMX Support + +As with many other Jetty features, local JMX support is enabled with the xref:modules/standard.adoc#jmx[`jmx` Jetty module]: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=jmx +---- + +With the `jmx` Jetty module enabled, Jetty components will be exported as JMX _MBeans_ to the JVM platform `MBeanServer`, so that they can be accessed by JMX compliant tools. + +Each Jetty component will export to its correspondent MBean relevant configuration parameters, so that a JMX tool can read and possibly change the component configuration through the MBean. + +Note that the Jetty MBeans are registered into the platform `MBeanServer`, but are not available to remote clients: they are _local_ to the JVM. + +This configuration is useful when you develop and test your Jetty server locally. + +JMX compliant tools such as https://adoptium.net/jmc.html[Java Mission Control (JMC)] can be started locally on your machine and can attach to other JVMs running on your machine, showing you the registered MBeans among which you will find the Jetty MBeans. + +NOTE: Enabling only the local JMX support is the most secure option for monitoring and management, but only users that have local access to the JVM will be able to browse the MBeans. +If you need to access the MBeans from a remote machine, read <>. + +[[remote]] +== Enabling Remote JMX Support + +There are two ways to configure a Jetty server so that it is possible to access the JVM platform MBeans from remote clients: + +* Use the `com.sun.management.jmxremote` and related system properties when starting Jetty. +Unfortunately, this solution does not work well with firewalls, and will not be discussed further. +* Use the `jmx-remote` Jetty module. + +Both ways use Java's Remote Method Invocation (RMI) to communicate between the client and the server. + +[IMPORTANT] +.Refresher: How RMI Works +==== +A server application that wants to make an object available to remote clients must _export_ the object. + +Exporting an object creates an RMI _stub_ that contains the host/port of the RMI _server_ that accepts incoming invocations from clients and forwards them to the object. +During the creation of the RMI stub, the host stored in the RMI stub is retrieved from the local name resolution system (for example, in Linux, from `/etc/hosts`). + +The RMI stub is then sent, along with a name that uniquely identifies the object, to the RMI _registry_. +The RMI registry is a service that maps names to RMI stubs; it may be external to both clients and server, although often it is part of the server JVM. + +When a client application wants to connect to the server object using RMI, it first connects to the RMI registry to download the RMI stub for the RMI server; recall that the RMI stub contains the host/port to connect to the RMI server. +Then, the client uses the RMI stub to connect to the RMI server, typically to a host/port that may be different from the RMI registry host/port (in particular, by default the RMI server port will be different from the RMI registry port). +==== + +Remote access to the platform MBeans, and therefore the Jetty MBeans, is enabled by the `jmx-remote` Jetty module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=jmx-remote +---- + +This command creates the `jmx-remote.ini` file: + +[source,subs=+quotes] +---- +JETTY_BASE +└── start.d + └── #jmx-remote.ini# +---- + +Enabling the `jmx-remote` module transitively enables the <> as well. + +The configuration for the RMI registry and the RMI server is specified by a `JMXServiceURL`. +The string format of an RMI `JMXServiceURL` is the following: + +---- +service:jmx:rmi://:/jndi/rmi://:/jmxrmi +---- + +Below you can find examples of ``JMXServiceURL``s: + +[source,subs=+quotes] +---- +*service:jmx:rmi:///jndi/rmi:///jmxrmi* +where: + rmi_server_host = local host address + rmi_server_port = randomly chosen + rmi_registry_host = local host address + rmi_registry_port = 1099 + +*service:jmx:rmi://0.0.0.0:1099/jndi/rmi://0.0.0.0:1099/jmxrmi* +where: + rmi_server_host = any address + rmi_server_port = 1099 + rmi_registry_host = any address + rmi_registry_port = 1099 + +*service:jmx:rmi://localhost:1100/jndi/rmi://localhost:1099/jmxrmi* +where: + rmi_server_host = loopback address + rmi_server_port = 1100 + rmi_registry_host = loopback address + rmi_registry_port = 1099 +---- + +The default `JMXServiceURL` configured by the `jmx-remote` module is the following: + +---- +service:jmx:rmi://localhost:1099/jndi/rmi://localhost:1099/jmxrmi +---- + +With the default configuration, only clients that are local to the server machine can connect to the RMI registry and RMI server - this is done for security reasons. +However, even with this local-only configuration, it would still be possible to access the MBeans from remote using an SSH tunnel, as explained in <>. + +By specifying an appropriate `JMXServiceURL`, you can fine tune the network address the RMI registry and the RMI server bind to, and the ports that the RMI registry and the RMI server listen to. +The RMI server and RMI registry hosts and ports can be the same (as in the default configuration) because RMI is able to multiplex traffic arriving to one port to multiple RMI objects. + +If you need to allow JMX remote access through a firewall, you must open both the RMI registry and the RMI server ports. +The default configuration simplifies the firewall configuration because you only need to open port `1099`. + +[NOTE] +==== +When Jetty is started with the `jmx-remote` module enabled, the RMI stub of the Jetty component that provides access to the MBeans is exported to the RMI registry. + +The RMI stub contains the host/port to connect to the RMI server, but the host is typically the machine host name, not the host specified in the `JMXServiceURL` (the latter is only used to specify the network address the RMI server binds to). + +To control the host stored in the RMI stub you need to set the system property `java.rmi.server.hostname` with the desired value in the module configuration file, `jmx-remote.ini`. +==== + +IMPORTANT: If your client cannot connect to the server, the most common cause is a mismatch between the RMI server host of the `JMXServiceURL` and the RMI server host of the RMI stub. + +You can customize the RMI server host/port, the RMI registry host/port and the system property `java.rmi.server.hostname` by editing the `jmx-remote.ini` configuration file. +Further information about the `jmx-remote` module configuration can be found xref:modules/standard.adoc#jmx-remote[here]. + +[[remote-ssh-tunnel]] +=== Remote JMX Access with Port Forwarding via SSH Tunnel + +You can access JMX MBeans on a remote machine when the RMI ports are not open, for example because of firewall policies, but you have SSH access to the machine, using local port forwarding via an SSH tunnel. + +In this case you want to configure the `JMXServiceURL` that binds the RMI server and the RMI registry to the loopback interface only and to the same port: + +---- +service:jmx:rmi://localhost:1099/jndi/rmi://localhost:1099/jmxrmi +---- + +You must set the system property `-Djava.rmi.server.hostname=localhost` so that the RMI stub contains `localhost` as the host name to connect to. +This is, incidentally, the default configuration of the `jmx-remote` module. + +Then you set up the local port forwarding with the SSH tunnel: + +---- +$ ssh -L 1099:localhost:1099 @ +---- + +Thanks to the local port forwarding of the SSH tunnel, when the client connects to `localhost:1099` on your local computer, the traffic will be forwarded to `machine_host` and when there, the SSH daemon will forward the traffic to `localhost:1099` on `machine_host`, which is exactly where the RMI server and the RMI registry listens to. + +The client first contacts the RMI registry, so it connects to `localhost:1099` on your local computer; the traffic is forwarded to `machine_host` through the SSH tunnel, connects to the RMI registry and the RMI stub is downloaded to the client. + +Then the client uses the RMI stub to connect to the RMI server. The RMI stub contains `localhost` as the RMI server host because that is what you have configured with the system property `java.rmi.server.hostname`. + +The client will connect again to `localhost:1099` on your local computer, this time to contact the RMI server; the traffic is forwarded to `machine_host` through the SSH tunnel, arrives to `machine_host` and connects to the RMI server. + +[[remote-auth]] +=== Remote JMX Access Authentication & Authorization + +The standard `javax.management.remote.JMXConnectorServer` class, used by the `jmx-remote` module to provide remote JMX access to Jetty MBeans, provides several options to authenticate and authorize users. +For a complete guide to controlling authentication and authorization in JMX, see https://docs.oracle.com/en/java/javase/11/management/monitoring-and-management-using-jmx-technology.html[the official JMX documentation]. + +The simplest way to control JMX authentication and authorization is to specify two files: one contains username and password pairs, and the other contains username and permission pairs. + +This is achieved by enabling the `jmx-remote-auth` Jetty module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=jmx-remote-auth +---- + +Enabling the `jmx-remote-auth` Jetty module creates the following files: + +---- +$JETTY_BASE +├── etc +│ ├── jmxremote.access +│ ├── jmxremote.password +│ └── jmx-remote-auth.xml +└── start.d + ├── jmx-remote-auth.ini + └── jmx-remote.ini +---- + +Then you edit the `$JETTY_BASE/etc/jmxremote.password` file, adding the username/password pairs that you need: + +.$JETTY_BASE/etc/jmxremote.password +---- +# The file format is: +alice wonderland +bob marley +---- + +You must also edit the `$JETTY_BASE/etc/jmxremote.access` file to give permissions to your users: + +.$JETTY_BASE/etc/jmxremote.access +---- +# The file format is: +alice readwrite +bob readonly +---- + +The above files define user `alice` with password `wonderland` to have `readwrite` access, and user `bob` with password `marley` to have `readonly` access. + +[[remote-secure]] +=== Securing Remote JMX Access with TLS + +The JMX communication via RMI happens by default in clear-text, but it is possible to secure the JMX communication via RMI with TLS. + +If you want to reuse the configuration that you are using for the xref:protocols/index.adoc#https[`https` module], you can just enable the `jmx-remote-ssl.xml` Jetty module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=jmx-remote-ssl +---- + +[NOTE] +==== +The `jmx-remote-ssl` Jetty module depends on the `ssl` Jetty module that in turn requires a KeyStore (read xref:protocols/index.adoc#ssl[this section] for more information). +==== + +The KeyStore must contain a valid certificate signed by a Certification Authority. +Having certificates signed by a Certification Authority simplifies by a lot the configuration needed to get the RMI communication over TLS working properly. + +The RMI mechanic is the usual one: the RMI client (typically a monitoring console) will connect first to the RMI registry (using TLS), download the RMI stub that contains the address and port of the RMI server to connect to, then connect to the RMI server (using TLS). + +This also mean that if the RMI registry and the RMI server are on different hosts, the RMI client must have available the cryptographic material to validate the certificates from both hosts. +This is where having certificates signed by a Certification Authority simplifies the configuration: if they are signed by a well known Certification Authority, the client does not need any extra configuration -- everything will be handled by the Java runtime. + +If the certificates are not signed by a Certification Authority (for example the certificate is self-signed), then you need to specify the TLS system properties that allow RMI (especially when acting as an RMI client) to retrieve the cryptographic material necessary to establish the TLS connection. + +[IMPORTANT] +==== +When the RMI server exports the `JMXConnectorServer` it acts as an RMI _client_ towards the RMI registry, and as such you must specify the TLS system properties as detailed below. +==== + +You must edit the `$JETTY_BASE/start.d/jmx-remote-ssl.ini` file and add the TrustStore path and password: + +.$JETTY_BASE/start.d/jmx-remote-ssl.ini +---- +--module=jmx-remote-ssl + +# System properties necessary for non-trusted certificates. +-Djavax.net.ssl.trustStore=/path/to/trustStore.p12 +-Djavax.net.ssl.trustStorePassword=password +---- + +[IMPORTANT] +==== +The TrustStore must contain the certificate you want to trust. + +If you are using self-signed certificates, the KeyStore already contains the self-signed certificate and therefore the KeyStore can be used as a TrustStore, and the system properties above can refer to the KeyStore path and password. +==== + +JMX compliant tools that offer a graphical user interface also must be started specifying the TrustStore path and password. + +For example, to launch https://adoptium.net/jmc.html[Java Mission Control (JMC)]: + +---- +$ jmc -vmargs -Djavax.net.ssl.trustStore=/path/to/trustStore.p12 -Djavax.net.ssl.trustStorePassword=password +---- diff --git a/documentation/jetty/modules/operations-guide/pages/jndi/index.adoc b/documentation/jetty/modules/operations-guide/pages/jndi/index.adoc new file mode 100644 index 000000000000..8057f817aa29 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/jndi/index.adoc @@ -0,0 +1,420 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += JNDI + +For each specific Jakarta EE _environment_ there is a specific `plus` module that you must enable in order to use JNDI resources in your webapp: + +* For Java EE 8, `ee8-plus` +* For Java EE 9, `ee9-plus` +* For Java {ee-current-caps}, `{ee-current}-plus` + +If you have already enabled an xref:annotations/index.adoc[`annotations`] module suitable for your EE environment, an appropriate `plus` module will already have been enabled and you do not need to explicitly enable a `plus` module. + +If you have extra jars associated with your JNDI resources, eg database drivers etc, that are not located inside your webapp then you must ensure they are on either the container classpath or a Jakarta EE environment classpath. +You can enable the `ext` module and place your jars in `${jetty.base}/lib/ext` to make them visible on the container classpath. +To make them visible on an EE environment classpath you should create a custom, EE environment-specific module. + +You can now declare JNDI resources and reference them within your webapps. + +== Declaring resources + +You must declare the objects you want bound so that you can then hook them into your webapp via `env-entry`, `resource-ref` and `resource-env-refs` in `web.xml`, `web-fragment.xml` or `override-web.xml`. + +You make these declarations in Jetty XML files that are either _external_ or _internal_ to your webapp. +A server or context XML file is external to your webapp. +The special `WEB-INF/jetty-env.xml` file is internal to your webapp. +See the section on <> for more information on how to choose in which XML file to place your declarations. + +Regardless of its location, the XML file contains declarations of JNDI resources that can be referenced later within your webapp. + +The declarations are new instances of the following types: + +<>:: +Used for `env-entry` type of entries +<>:: +Used for most other type of resources +<>:: +For a JTA manager +<>:: +For the link between a `web.xml` resource name and a naming entry + +Declarations of each of these types follow a similar pattern: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + + +---- +<1> Defines a resource to Jetty. +<2> Specifies the <> of the resource. +<3> Specifies the name of the resource which will be looked up by the webapp relative to the `java:comp/` or `java:comp/env` namespace. +<4> Specifies the value of the resource. + + +[[env]] +=== org.eclipse.jetty.plus.jndi.EnvEntry + +Sometimes it is useful to pass configuration information to a webapp at runtime that you either cannot or cannot conveniently code into a `web.xml` ``. +In such cases, you can use the `org.eclipse.jetty.plus.jndi.EnvEntry` class, and optionally even override an entry of the same name in `web.xml`. + +Here's an example that defines the equivalent of an `env-entry` called `mySpecialValue` with value `4000` that overrides an `` declaration of the same name in web.xml: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + mySpecialValue + 4000 + true + +---- +<1> Define an `EnvEntry` that corresponds to an ``. +<2> <> at the JVM level. +<3> The name of the entry, corresponding to a lookup by the webapp of `java:comp/env/mySpecialValue`. +<4> The value of the entry, in this case the integer value `4000`. +<5> `true` means to override the value of an `` of the same name in `web.xml`. + +Note that if you don't want to override the `web.xml` value, simply omit the last argument, or set it to `false`. + +The Servlet Specification allows binding only the following object types to an `env-entry`: + +* java.lang.String +* java.lang.Integer +* java.lang.Float +* java.lang.Double +* java.lang.Long +* java.lang.Short +* java.lang.Character +* java.lang.Byte +* java.lang.Boolean + +Jetty is a little more flexible and allows you to also bind: + +* custom POJOs +* http://docs.oracle.com/javase/1.5.0/docs/api/javax/naming/Reference.html[`javax.naming.References`] +* http://docs.oracle.com/javase/1.5.0/docs/api/javax/naming/Referenceable.html[`javax.naming.Referenceables`] + +Be aware that if you take advantage of this feature, your web application is __not portable__. + +[[resource]] +=== org.eclipse.jetty.plus.jndi.Resource + +You can configure any type of resource that you want to refer to in `web.xml` via a `resource-ref` or `resource-env-ref` by using the `org.eclipse.jetty.plus.jndi.Resource` type of naming entry. + +You provide the scope, the name of the object (relative to `java:comp/env`) and a POJO, `javax.naming.Reference` or `javax.naming.Referenceable` instance. + +==== DataSources + +This example configures a http://db.apache.org/derby[Derby] DataSource named `jdbc/myds`: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + jdbc/myds + + + test + create + + + + +---- + +This would be linked into the webapp's JNDI namespace via an entry in a `web.xml` like so: + +[,xml] +---- + + jdbc/myds + javax.sql.DataSource + Container + +---- + +[NOTE] +==== +When configuring Resources, ensure that the type of object you configure matches the type of object you expect to look up in `java:comp/env`. +For database connection factories, this means that the object you register as a Resource _must_ implement the `javax.sql.DataSource` interface. + +Also note that the http://jcp.org/aboutJava/communityprocess/pr/jsr244/index.html[J2EE Specification] recommends storing DataSources relative to `jdbc/` and thus looked up by the application as `java:comp/env/jdbc/xxx`. +Eg The Datasource bound in Jetty as `jdbc/users` would be looked up by the application as `java:comp/env/jdbc/users` + +==== + +//TODO For more examples of datasource configurations, see xref:jndi-datasource-examples[]. + + +==== JMS Queues, Topics and ConnectionFactories + +Jetty can bind any implementation of the JMS destinations and connection factories. + +Here is an example of binding an http://activemq.apache.org[ActiveMQ] in-JVM connection factory: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + jms/connectionFactory + + + vm://localhost?broker.persistent=false + + + + +---- + +The corresponding entry in `web.xml` to bind the ConnectionFactory into the webapp's JNDI namespace would be: + +.wiki.xml +[,xml,subs=attributes+] +---- + + jms/connectionFactory + javax.jms.ConnectionFactory + Container + +---- + +[NOTE] +==== +The http://jcp.org/aboutJava/communityprocess/pr/jsr244/index.html[J2EE Specification] recommends storing JMS connection factories under `jms/`. +Eg The ConnectionFactory bound in Jetty as `jms/inqueue` would be looked up by the application as `java:comp/env/jms/inqueue`. +==== + +==== Mail + +To configure access to `javax.mail.Session` from within a webapp, declare an `org.eclipse.jetty.plus.jndi.Resource` with an `org.eclipse.jetty.{ee-all}.jndi.factories.MailSessionReference` that will hold the mail configuration and create the instance of the `Session` when it is referenced: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + mail/Session + + + fred + OBF:1xmk1w261z0f1w1c1xmq + + + XXX + me@me + true + + + + + + +---- +<1> Use the `org.eclipse.jetty.{ee-current}.factories.MailSessionReference` class to hold the configuration. +<2> Set the username for the mail instance. +<3> Set the password for the mail instance -- use the xref:tools/index.adoc#password[Jetty Password tool] to obfuscate the password. +<4> Set all other applicable properties. + +The webapp performs a lookup for `java:comp/env/mail/Session` at runtime and obtains a `javax.mail.Session` that has the correct configuration to permit it to send email via SMTP. + +[NOTE] +==== +Jetty does not provide the `javax.mail` and `javax.activation` jars. + +Note also that the http://jcp.org/aboutJava/communityprocess/pr/jsr244/index.html[J2EE Specification] recommends storing JavaMail connection factories under `mail/`. +Eg The `MailSessionReference` bound to jetty as `mail/smtp` would be looked up by the application as `java:comp/env/mail/smtp`. +==== + +[[tx]] +=== org.eclipse.jetty.plus.jndi.Transaction + +To perform distributed transactions with your resources, a _transaction manager_ that supports the JTA interfaces is required. +The transaction manager is looked up by the application as `java:comp/UserTransaction`. + +Jetty does not ship with a JTA manager, but _does_ provide the infrastructure to plug in the JTA manager of your choice. + +If your JTA library's implementation of `UserTransaction` implements `javax.naming.Reference`, then you should use the `org.eclipse.jetty.plus.jndi.Transaction` object in a <> to register it in JNDI: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + + + +---- + +If your JTA library's implementation of `UserTransaction` does _not_ implement `javax.naming.Reference`, then you should use the Jakarta EE specific Jetty class to register it in JNDI: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + + + +---- + +Jetty will automatically bind this JTA manager to the webapp's JNDI namespace at `java:comp/UserTransaction`. + +[[link]] +=== org.eclipse.jetty.plus.jndi.Link + +Usually, the name you provide for the `org.eclipse.jetty.plus.jndi.Resource` is the same name you reference in `web.xml`. +This ensures that the two are linked together and thus accessible to your webapp. + +However, if the names cannot be the same, then it is possible to effectively alias one to another using an `org.eclipse.jetty.plus.jndi.Link`. + +Supposing you have a declaration for a Datasource named `jdbc/workforce` in a Jetty context XML file, but your web.xml wants to link to a `` named `jdbc/employees`, and you cannot edit the web.xml. +You can create a `WEB-INF/jetty-env.xml` file with an `org.eclipse.jetty.plus.jndi.Link` that ties together the names `jdbc/workforce` and `jdbc/employees`: + +The context XML file declares `jdbc/workforce`: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + + jdbc/workforce + + + test + create + + + + +---- + +The `web.xml` refers to it as `jdbc/employees`: + +.wiki.xml +[,xml,subs=attributes+] +---- + + jdbc/employees + javax.sql.DataSource + Container + +---- + +Create a `WEB-INF/jetty-env.xml` file with a `org.eclipse.jetty.plus.jndi.Link` to link these names together: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + jdbc/employees + jdbc/workforce + +---- +<1> The name as referenced in the `web.xml` file. +<2> The name as referenced in the context XML file. + +[[xml]] +=== Jetty XML files + +You can define naming resources in three places: + +Server XML file:: +Naming resources defined in a server XML file are <> at the JVM, `org.eclipse.jetty.server.Server` or environment level. +Note that the classes for the resource _must_ be visible at the point in time that the XML executes. +For example, environment level resources should be declared in an XML file that is referenced by a custom module that contains an `[environment]` clause at the matching environment level to ensure the classpath for that environment is available. + +Context XML file:: +Entries in a context XML file should be <> at the level of the webapp to which they apply (it is possible to use a less strict scoping level of Server or JVM, but not recommended). +As a context XML file executes _before_ the webapp's classes are available, the classes for your resource must be external to the webapp and on either the container or environment classpath. + +WEB-INF/jetty-env.xml:: +Naming resources in a `WEB-INF/jetty-env.xml` file are <> to the webapp in which the file resides. +The resources defined here may use classes from inside your webapp. + +[[scope]] +=== Resource scoping + +Naming resources within Jetty belong to different scopes, in increasing order of restrictiveness: + +*JVM scope:* +The name is unique across the JVM instance, and is visible to all application code. +This scope is represented by a `null` first parameter to the resource declaration. +For example: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + jms/connectionFactory + + + vm://localhost?broker.persistent=false + + + +---- +<1> Empty first arg equates to JVM scope for the object bound to name `jms/connectionFactory`. + +*Environment scope:* +The name is unique within a Jetty _environment_. +It is represented by referencing the name of the Jakarta EE environment as the first parameter to the resource declaration. +For example: + +.wiki.xml +[,xml,subs=attributes+] +---- + + {ee-current} + jms/connectionFactory + + + vm://localhost?broker.persistent=false + + + +---- + +*Webapp scope:* +The name is unique to the `org.eclipse.jetty.{ee-all}.webapp.WebAppContext` instance, and is only visible to that application. +This scope is represented by referencing the instance as the first parameter to the resource declaration. +For example: + +.wiki.xml +[,xml,subs=attributes+] +---- + + + jms/connectionFactory + + + vm://localhost?broker.persistent=false + + + +---- diff --git a/documentation/jetty/modules/operations-guide/pages/jsf-taglibs/index.adoc b/documentation/jetty/modules/operations-guide/pages/jsf-taglibs/index.adoc new file mode 100644 index 000000000000..516b042fc33e --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/jsf-taglibs/index.adoc @@ -0,0 +1,41 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += JavaServer Faces TagLibs + +If you want to use JSF with your webapp, you should copy the relevant jars from your implementation of choice into your `$JETTY_BASE` directory, ideally into `$JETTY_BASE/lib/ext`. +If that directory does not exist, enable the `ext` module, which will create the directory and ensure all jars within it are put onto the container classpath. + + +Then you will need to tell Jetty which of those jars contains the `+*.tld+` files. +To accomplish that, you need to specify either the name of the file or a pattern that matches the name/s of the file/s as the `org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern` context attribute. +You will need to preserve the existing value of the attribute, and add in your extra pattern. + +Here's an example of using a context xml file to add in a pattern to match files starting with `jsf-`, which contain the `+*.tld+` files: + +[,xml] +---- + + + + + + org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern + .*/jetty-servlet-api-[^/]*\.jar$|.*/javax.servlet.jsp.jstl-.*\.jar$|.*/org.apache.taglibs.taglibs-standard-impl-.*\.jar$|.*/jsf-[^/]*\.jar$ + + +---- +<1> Configures a link:{javadoc-url}/org/eclipse/jetty/webapp/WebAppContext.html[`WebAppContext`], which is the Jetty component that represents a standard Servlet web application. +<2> Specifies a context attribute. +<3> Specifies the name of the context attribute. +<4> Adds the additional pattern `+.*/jsf-[^/]*\.jar$+` to those already existing. diff --git a/documentation/jetty/modules/operations-guide/pages/jsp/index.adoc b/documentation/jetty/modules/operations-guide/pages/jsp/index.adoc new file mode 100644 index 000000000000..e3a71dd4902f --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/jsp/index.adoc @@ -0,0 +1,171 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Java Server Pages + +Jetty supports JSP via the `{ee-all}-jsp` modules, which are based on Apache Jasper: + +[source,subs=attributes+] +---- +include::{jetty-home}/modules/ee10-jsp.mod[] +---- + +Logging has been bridged to Jetty logging, so you can enable logging for the `org.apache.jasper` package, subpackages and classes as usual. + +== Configuration of the JSP Servlet + +The `org.eclipse.jetty.jsp.JettyJspServlet` is the servlet responsible for serving JSPs. + +It is configured as the default jsp servlet in the `$JETTY_HOME/etc/webdefault.xml` file. +Notice that Jetty identifies the jsp servlet by the presence of the `id="jsp"` attribute in the `` declaration. + +That file maps the `org.eclipse.jetty.jsp.JettyJspServlet` to the following partial urls: + +* `+*.jsp+` +* `+*.jspf+` +* `+*.jspx+` +* `+*.xsp+` +* `+*.JSP+` +* `+*.JSPF+` +* `+*.JSPX+` +* `+*.XSP+` + +You can change to a different servlet, change or add ````s or add extra ````s in your `web.xml` file. + +Here's an example of adding an `` to augment the definitions from the standard `webdefault.xml` file: + +[,xml] +---- + + jsp + + keepgenerated + true + + +---- +<1> This identifies this servlet as the jsp servlet to Jetty. +<2> This identifies this declaration as augmenting the already-defined servlet called `jsp`. +<3> This init param controls whether the jsp servlet retains the `+*.java+` files generated during jsp compilation. +<4> This sets the value of the init param + +Another element you might consider adding to the default setup is `async-supported`: + +[,xml] +---- + + jsp + true + +---- +<1> This identifies this servlet as the jsp servlet to Jetty. +<2> This identifies this declaration as augmenting the already-defined servlet called `jsp`. +<3> By default, the jsp servlet does not support async. + +There are many configuration parameters for the Apache Jasper JSP Servlet, here are some of them: + +.JSP Servlet Parameters +[cols=",,,",options="header"] +|=== +|init param |Description |Default |`webdefault.xml` + +|checkInterval |If non-zero and `development` is `false`, background jsp recompilation is enabled. This value is the interval in seconds between background recompile checks. + |0 |– +|classpath |The classpath is dynamically generated if the context has a URL classloader. The `org.apache.catalina.jsp_classpath` +context attribute is used to add to the classpath, but if this is not set, this `classpath` configuration item is added to the classpath instead.` |- |– + +|classdebuginfo |Include debugging info in class file. |true |– + +|compilerClassName |If not set, defaults to the Eclipse jdt compiler. |- |– + +|compiler |Used if the Eclipse jdt compiler cannot be found on the +classpath. It is the classname of a compiler that Ant should invoke. |– +|– + +|compilerTargetVM |Target vm to compile for. |1.8 |1.8 + +|compilerSourceVM |Sets source compliance level for the jdt compiler. +|1.8 |1.8 + +|development |If `true` recompilation checks occur at the frequency governed by `modificationTestInterval`. |true |– + +|displaySourceFragment |Should a source fragment be included in +exception messages |true |– + +|dumpSmap |Dump SMAP JSR45 info to a file. |false |– + +|enablePooling |Determines whether tag handler pooling is enabled. |true |– + +|engineOptionsClass |Allows specifying the Options class used to +configure Jasper. If not present, the default EmbeddedServletOptions +will be used. |- |– + +|errorOnUseBeanInvalidClassAttribute |Should Jasper issue an error when +the value of the class attribute in an useBean action is not a valid +bean class |true |– + +|fork |Only relevant if you use Ant to compile JSPs: by default Jetty will use the Eclipse jdt compiler.|true |- + +|genStrAsCharArray |Option for generating Strings as char arrays. |false |– + +|ieClassId |The class-id value to be sent to Internet Explorer when +using tags. |clsid:8AD9C840-044E-11D1-B3E9-00805F499D93 |– + +|javaEncoding |Pass through the encoding to use for the compilation. +|UTF8 |– + +|jspIdleTimeout |The amount of time in seconds a JSP can be idle before +it is unloaded. A value of zero or less indicates never unload. |-1 |– + +|keepgenerated |Do you want to keep the generated Java files around? +|true |– + +|mappedFile |Support for mapped Files. Generates a servlet that has a +print statement per line of the JSP file |true |– + +|maxLoadedJsps |The maximum number of JSPs that will be loaded for a web +application. If more than this number of JSPs are loaded, the least +recently used JSPs will be unloaded so that the number of JSPs loaded at +any one time does not exceed this limit. A value of zero or less +indicates no limit. |-1 |– + +|modificationTestInterval |If `development=true`, interval between +recompilation checks, triggered by a request. |4 |– + +|quoteAttributeEL | When EL is used in an attribute value on a JSP page, should the rules for quoting of attributes described in JSP.1.6 be applied to the expression + |true |- + +|recompileOnFail |If a JSP compilation fails should the +modificationTestInterval be ignored and the next access trigger a +re-compilation attempt? Used in development mode only and is disabled by +default as compilation may be expensive and could lead to excessive +resource usage. |false |– + +|scratchDir |Directory where servlets are generated. The default is the value of the context attribute `javax.servlet.context.tempdir`, or the system property `java.io.tmpdir` if the context attribute is not set. |– |– + +|strictQuoteEscaping |Should the quote escaping required by section JSP.1.6 of the JSP specification be applied to scriplet expression. + |true|- + +|suppressSmap |Generation of SMAP info for JSR45 debugging. |false |– + +|trimSpaces |Should template text that consists entirely of whitespace be removed from the output (true), replaced with a single space (single) or left unchanged (false)? Note that if a JSP page or tag file specifies a trimDirectiveWhitespaces value of true, that will take precedence over this configuration setting for that page/tag. +trimmed? |false |– + +|xpoweredBy |Generate an X-Powered-By response header. |false |false + +|=== + +[NOTE] +==== +If the value you set doesn't take effect, try using all lower case instead of camel case, or capitalizing only some of the words in the name, as Jasper is inconsistent in its parameter naming strategy. +==== diff --git a/documentation/jetty/modules/operations-guide/pages/jstl/index.adoc b/documentation/jetty/modules/operations-guide/pages/jstl/index.adoc new file mode 100644 index 000000000000..1032798aaea6 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/jstl/index.adoc @@ -0,0 +1,23 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += JavaServer Pages Standard Tag Libraries + +The JavaServer Pages Standard Tag Library (JSTL) is part of the Jetty distribution, and is available via the `{ee-all}-jstl` modules: + +[source,subs=attributes+] +---- +include::{jetty-home}/modules/ee10-jstl.mod[] +---- + +When enabled, Jetty will make the JSTL tags available for your webapps. diff --git a/documentation/jetty/modules/operations-guide/pages/keystore/index.adoc b/documentation/jetty/modules/operations-guide/pages/keystore/index.adoc new file mode 100644 index 000000000000..0cc0f2c24940 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/keystore/index.adoc @@ -0,0 +1,274 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Configuring SSL/TLS KeyStores + +A KeyStore is a file on the file system that contains a private key and a public certificate, along with the certificate chain of the certificate authorities that issued the certificate. +The private key, the public certificate and the certificate chain, but more generally the items present in a KeyStore, are typically referred to as "cryptographic material". + +Keystores may encode the cryptographic material with different encodings, the most common being https://en.wikipedia.org/wiki/PKCS_12[PKCS12], and are typically protected by a password. + +Refer to the xref:protocols/index.adoc#ssl[secure protocols section] for more information about how to configure a secure connector using a KeyStore. + +[[create]] +== Creating a KeyStore + +KeyStores are created with the JDK tool `$JAVA_HOME/bin/keytool`. + +The following command creates a KeyStore file containing a private key and a self-signed certificate: + +[source] +---- +keytool + -genkeypair <1> + -alias mykey <2> + -validity 90 <3> + -keyalg RSA <4> + -keysize 2048 <5> + -keystore /path/to/keystore.p12 <6> + -storetype pkcs12 <7> + -dname "CN=domain.com, OU=Unit, O=Company, L=City, S=State, C=Country" <8> + -ext san=dns:www.domain.com,dns:domain.org <9> + -v <10> +---- +<1> the command to generate a key and certificate pair +<2> the alias name of the key and certificate pair +<3> specifies the number of days after which the certificate expires +<4> the algorithm _must_ be RSA (the DSA algorithm does not work for web sites) +<5> indicates the strength of the key +<6> the KeyStore file +<7> the KeyStore type, stick with the standard PKCS12 +<8> the distinguished name (more below) -- customize it with your values for CN, OU, O, L, S and C +<9> the extension with the subject alternative names (more below) +<10> verbose output + +The command prompts for the KeyStore password that you must choose to protect the access to the KeyStore. + +[IMPORTANT] +==== +The important parts of the command above are the _Common Name_ (CN) part of the distinguished name, and the subject alternative names (SAN). + +The CN value must be the main domain you own and that you want to use for your web applications. +For example, if you have bought domains `domain.com` and `domain.org`, you want to specify `CN=domain.com` as your main domain. + +Furthermore, to specify additional domains or subdomains within the same certificate, you must specify the SAN extension. +In the example above, `san=dns:www.domain.com,dns:domain.org` specifies `www.domain.com` and `domain.org` as alternative names for your web applications (that you can configure using xref:deploy/index.adoc#virtual-hosts[virtual hosts]). + +In rare cases, you may want to specify IP addresses, rather than domains, in the SAN extension. +The syntax in such case is `san=ip:127.0.0.1,ip:[::1]`, which specifies as subject alternative names IPv4 `127.0.0.1` and IPv6 `[::1]`. +==== + +[[create-many]] +=== KeyStores with Multiple Entries + +A single KeyStore may contain multiple key/certificate pairs. +This is useful when you need to support multiple domains on the same Jetty server (typically accomplished using xref:deploy/index.adoc#virtual-hosts[virtual hosts]). + +You can create multiple key/certificate pairs as detailed in the <>, provided that you assign each one to a different alias. + +Compliant TLS clients will send the xref:protocols/index.adoc#ssl-sni[TLS SNI extension] when creating new connections, and Jetty will automatically choose the right certificate by matching the SNI name sent by the client with the CN or SAN of certificates present in the KeyStore. + +[[csr]] +== Creating a Certificate Signing Request + +Self-signed certificates are not trusted by browsers and generic clients: you need to establish a trust chain by having your self-signed certificate signed by a certificate authority (CA). + +Browsers and generic clients (e.g. Java clients) have an internal list of trusted certificate authorities root certificates; they use these trusted certificates to verify the certificate they received from the server when they connect to your web applications. + +To have your self-signed certificate signed by a certificate authority you first need to produce a _certificate signing request_ (CSR): + +[source] +---- +keytool + -certreq <1> + -file domain.com.csr <2> + -keystore keystore.p12 <3> +---- +<1> the command to generate a certificate signing request +<2> the file to save the CSR +<3> the keystore that contains the self-signed certificate + +Then, you have to send the CSR file to the certificate authority of your choice, and wait for their reply (they will probably require a proof that you really own the domains indicated in your certificate). + +Eventually, the certificate authority will reply to you with one or more files containing the CA certificate chain, and your certificate signed by their certificate chain. + +[[csr-import]] +== Importing the Signed Certificate + +The file you receive from the CA is typically in PEM format, and you *must* import it back into the same KeyStore file you used to generate the CSR. +You must import *both* the certificate chain and your signed certificate. + +First, import the certificate chain: + +[source] +---- +keytool + -importcert <1> + -alias ca <2> + -file chain_from_ca.pem <3> + -keystore keystore.p12 <4> + -trustcacerts <5> + -v <6> +---- +<1> the command to import certificates +<2> use the `ca` alias to differentiate from the alias of the server certificate +<3> the file containing the certificate chain received from the CA +<4> your KeyStore file +<5> specify that you trust CA certificates +<6> verbose output + +Then, import the signed certificate: + +---- +keytool + -importcert + -file signed_certificate.pem + -keystore keystore.p12 + -trustcacerts + -v +---- + +Now you have a trusted certificate in your KeyStore that you can use for the domains of your web applications. + +// TODO: add a section about renewal? + +Refer to the section about configuring xref:protocols/index.adoc#ssl[secure protocols] to configure the secure connector with your newly created KeyStore. + +[[client-authn]] +== Creating a KeyStore for Client Certificate Authentication + +For the majority of secure web sites, it is the client (typically the browser) that validates the certificate sent by the server (by verifying the certificate chain). +This is the _server domain certificate_. + +However, the TLS protocol supports a _mutual authentication_ mode where also the client must send a certificate to the server, that the server validates. + +You typically want to sign the client certificate(s) with a server certificate that you control, and you must distribute the client certificate(s) to all the clients that need it, and redistribute the client certificates when they expire. +The _server authentication certificate_ may be different from the _server domain certificate_, but it's typically stored in the same KeyStore for simplicity (although under a different alias). + +First, you want to create the private key and server authentication certificate that you will use to sign client certificates: + +[source] +---- +keytool + -genkeypair + -alias server_authn <1> + -validity 90 + -keyalg RSA + -keysize 2048 + -keystore keystore.p12 <2> + -storetype pkcs12 + -dname "CN=server_authn, OU=Unit, O=Company, L=City, S=State, C=Country" <3> + -ext bc=ca:true <4> + -v +---- +<1> use the `server_authn` alias to differentiate from the alias of the server certificate +<2> the KeyStore file +<3> the CN is not that important, since this certificate will not be validated by clients +<4> the extension with the basic constraints (more below) + +IMPORTANT: The important difference with the <> is the _basic constraints_ extension (`bc`) that indicates that this certificates acts as a certificate authority (`ca:true`). + +Now you want to export both the private key and server authentication certificate. +Unfortunately, the `keytool` program cannot export private keys, so you need to use a different command line program like `openssl`, or a graphical program like https://keystore-explorer.org/[KeyStore Explorer]. + +Let's use `openssl` to export the server authentication private key: + +---- +openssl + pkcs12 + -in keystore.p12 + -nodes + -nocerts + -out server_authn.key +---- + +Now let's export the server authentication certificate: + +---- +keytool + -exportcert + -keystore keystore.p12 + -rfc + -file server_authn.crt + -v +---- + +At this point, you want to create a client KeyStore, so that you can sign the client certificate with the server authentication cryptographic material: + +[source] +---- +keytool + -genkeypair + -validity 90 + -keyalg RSA + -keysize 2048 + -keystore client_keystore.p12 <1> + -storetype pkcs12 + -dname "CN=client, OU=Unit, O=Company, L=City, S=State, C=Country" <2> + -v +---- +<1> the client KeyStore file +<2> the CN is not that important, since it will not be validated by the server + +Now produce a certificate signing request (CSR): + +---- +keytool + -certreq + -file client.csr + -keystore client_keystore.p12 +---- + +Now you need to sign the CSR, but again the `keytool` program does not have this functionality, and you must resort again to use `openssl`: + +---- +openssl + x509 + -req + -days 90 + -in client.csr + -CA server_authn.crt + -CAkey server_authn.key + -CAcreateserial + -sha256 + -out signed.crt +---- + +Now you need to import the server authentication certificate and the signed client certificate into the client KeyStore. + +First, the server authentication certificate: + +---- +keytool + -importcert + -alias ca + -file server_authn.crt + -keystore client_keystore.p12 + -v +---- + +Then, the signed client certificate: + +---- +keytool + -importcert + -file signed.crt + -keystore client_keystore.p12 + -v +---- + +Now you can distribute `client_keystore.p12` to your client(s). + +// TODO: add a section about renewal? + +Refer to the section about configuring xref:protocols/index.adoc#ssl[secure protocols] to configure the secure connector to require client authentication. diff --git a/documentation/jetty/modules/operations-guide/pages/modules/custom.adoc b/documentation/jetty/modules/operations-guide/pages/modules/custom.adoc new file mode 100644 index 000000000000..b84f95dca30f --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/modules/custom.adoc @@ -0,0 +1,235 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Custom Jetty Modules + +In addition to the modules that come packaged with Jetty, you can create your own custom modules. + +NOTE: Make sure you have read the xref:modules/index.adoc[Jetty modules section] if you are not familiar with the concepts used in this section. + +Custom modules can be used for a number of reasons -- they can extend Jetty features, or add new features, or make additional libraries available to the server, etc. + +[[modify]] +== Modifying an Existing Module + +The standard Jetty modules typically come with a number of configurable properties that can be easily customized without the need of writing a custom module. + +However, there may be cases where the customization is more complex than a simple property, and a custom module is necessary. + +For example, let's assume that you want to modify the order of the TLS cipher suites offered by the server when a client connects, using the https://www.openssl.org/docs/man1.1.0/man1/ciphers.html[OpenSSL cipher list format]. + +The Jetty class that handles the TLS configuration is `SslContextFactory`, and it already has a method `setCipherComparator(Comparator)`; however, you need to pass your custom implementation, which cannot be represented with a simple module property. + +The `SslContextFactory` component is already allocated by the standard Jetty module `ssl`, so what you need to do is the following: + +* Write the custom cipher `Comparator` and package it into a `+*.jar+` file (exercise left to reader). +* Write a custom Jetty XML file that calls the `SslContextFactory.setCipherComparator(Comparator)` method. +* Write a custom Jetty module file that depends on the standard `ssl` module. + +Start with the custom Jetty XML file, `$JETTY_BASE/etc/custom-ssl.xml`: + +.custom-ssl.xml +[,xml] +---- + + + + + + + + + ECDH+AESGCM:ECDH+AES256:!aNULL:!MD5:!DSS:!ADH + + + + + + +---- +<1> Reference the existing `SslContextFactory` object created by the standard `ssl` module using its `id`. +<2> Call the `setCipherComparator()` method. +<3> Instantiate your custom cipher comparator. +<4> Pass to the constructor the ordering string in OpenSSL format, reading it from the module property `com.acme.ssl.cipherList`. + +CAUTION: The cipher list used above may not be secure -- it's just an example. + +Then write your custom module in the `$JETTY_BASE/modules/custom-ssl.mod` file: + +.custom-ssl.mod +[source] +---- +[description] +Customizes the standard ssl module. + +[tags] <1> +acme + +[depends] <2> +ssl + +[lib] <3> +lib/custom-cipher-comparator.jar + +[xml] <4> +etc/custom-ssl.xml + +[ini-template] <5> +## The cipher list in OpenSSL format. +# com.acme.ssl.cipherList=ECDH+AESGCM:ECDH+AES256:!aNULL:!MD5:!DSS:!ADH + +---- +<1> A tag that characterizes this custom module (see xref:modules/index.adoc#directive-tags[here]). +<2> This custom module depends on the standard `ssl` module. +<3> The custom cipher comparator class is compiled and packaged into this `+*.jar+` file. +<4> The custom Jetty XML file from above. +<5> The text that will be copied in the `custom-ssl.ini` file when this custom module will be enabled. + +Now you can xref:start/index.adoc#configure-enable[enable] the custom module with the following command issued from the `$JETTY_BASE` directory: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=https,custom-ssl +---- + +The command above will produce the following `$JETTY_BASE` directory structure: + +[source,subs=+quotes] +---- +$JETTY_BASE +├── etc +│ └── custom-ssl.xml +├── modules +│ └── custom-ssl.mod +├── resources +│ └── jetty-logging.properties +└── start.d + ├── https.ini + └── ##custom-ssl.ini## +---- + +In the custom XML file you have used a custom module property to parametrize your custom cipher comparator. +This custom module property was then referenced in the `[ini-template]` section of the custom module file, so that when the custom module is enabled, a correspondent `custom-ssl.ini` file is created. + +In this way, updating the cipher list won't require you to update the XML file, but just the `custom-ssl.ini` file. + +[[create]] +== Creating a New Module + +In the cases where you need to enhance Jetty with a custom functionality, you can write a new Jetty module that provides it. + +For example, let's assume that you need to add a custom auditing component that integrates with the auditing tools used by your company. +This custom auditing component should measure the HTTP request processing times and record them (how they are recorded is irrelevant here -- could be in a local log file or sent via network to an external service). + +The Jetty libraries already provide a way to measure HTTP request processing times via xref:programming-guide:server/http.adoc#handler-use-events[`EventsHandler`]: you write a custom `EventsHandler` subclass that overrides the methods corresponding to the events you are interested in. + +The steps to create a Jetty module are similar to those necessary to <>: + +* Write the auditing component and package it into a `+*.jar+` file. +* Write a custom Jetty XML file that wires the auditing component to the `Handler` tree. +* Write a custom Jetty module file that puts everything together. + +Let's start with the auditing component, sketched below: + +[,java] +---- +package com.acme.audit; + +public class AuditingEventsHandler extends EventsHandler { + // Auditing is implemented here. +} +---- + +Let's assume that this class is compiled and packaged into `acme-audit.jar`, and that it has a dependency on `acme-util.jar`. +Both `+*.jar+` files will be put in the `$JETTY_BASE/lib/` directory. + +Next, let's write the Jetty XML file that wires the auditing component to the `ServerConnector`, `$JETTY_BASE/etc/acme-audit.xml`: + +.acme-audit.xml +[,xml,options=nowrap] +---- + + + + + + + + + + + + + + + +---- +<1> Reference `Server` instance. +<2> Call `insertHandler()` on the `Server` so that the auditing component will be inserted just after the `Server` and just before its child `Handler`. +<3> Instantiate the auditing component. +<4> Configure the auditing component with a property. + +The last step is to create the custom Jetty module file for the auditing component, `$JETTY_BASE/modules/acme-audit.mod`: + +.acme-audit.mod +---- +[description] +Adds ACME auditing to the Jetty Server. + +[tags] <1> +acme +audit + +[depends] <2> +server + +[libs] <3> +lib/acme-audit.jar +lib/acme-util.jar + +[xml] <4> +etc/acme-audit.xml + +[ini-template] <5> +## An auditing property. +# com.acme.audit.some.property=42 +---- +<1> The tags that characterize this custom module (see xref:modules/index.adoc#directive-tags[here]). +<2> This custom module depends on the standard `server` module. +<3> The `+*.jar+` files that contains the custom auditing component, and its dependencies. +<4> The custom Jetty XML file from above. +<5> The text that will be copied in the `acme-audit.ini` file when this custom module will be enabled. + +Now you can xref:start/index.adoc#configure-enable[enable] the custom auditing module with the following command issued from the `$JETTY_BASE` directory: + +[source] +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http,acme-audit +---- + +The command above will produce the following `$JETTY_BASE` directory structure: + +[source,subs=+quotes] +---- +$JETTY_BASE +├── etc +│ └── acme-audit.xml +├── modules +│ └── acme-audit.mod +├── resources +│ └── jetty-logging.properties +└── start.d + ├── http.ini + └── ##acme-audit.ini## +---- + +Enabling the custom auditing component will create the `$JETTY_BASE/start.d/acme-audit.ini` module configuration file that you can edit to configure auditing properties. diff --git a/documentation/jetty/modules/operations-guide/pages/modules/index.adoc b/documentation/jetty/modules/operations-guide/pages/modules/index.adoc new file mode 100644 index 000000000000..8937a9584aa6 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/modules/index.adoc @@ -0,0 +1,493 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Modules + +A Jetty _module_ provides one or more Java components that work together to implement one or more features. +Such features could be listening for clear-text HTTP/1.1 requests, exposing Jetty components to JMX, provide hot-deployment of web applications, etc. + +Every Jetty feature is provided by a Jetty module. + +A Jetty module is defined in a `.mod` file, where `` is the module name (see also the <>). + +Jetty module files are read from the typical xref:start/index.adoc#configure[configuration source directories], under the `modules/` subdirectory; from higher priority to lower priority: + +* The `$JETTY_BASE/modules/` directory. +* If a directory is specified with the `--add-config-dir` option, its `modules/` subdirectory. +* The `$JETTY_HOME/modules/` directory. + +The standard Jetty modules that Jetty provides out-of-the-box are under `$JETTY_HOME/modules/`. + +xref:modules/custom.adoc[Custom Jetty modules] should be put under `$JETTY_BASE/modules/`. + +[[names]] +== Module Names + +A Jetty module has a unique name. +The module name is by default derived from the file name, so module file `acme.mod` identifies a module named `acme`. + +However, a module file may specify a <>+] directive for a _virtual_ module, so that many modules may provide a different implementation for the same feature. + +For example, among the standard modules provided by Jetty, the `server` module depends on the `logging` module, but there is no correspondent `logging.mod` file. + +However, the `logging-jetty.mod` file has, among others, this section: + +.logging-jetty.mod +---- +[provides] +logging|default +---- + +This section means that the `logging-jetty.mod` file provides the virtual module `logging`, and it is the default provider. + +The `logging-log4j2.mod` file has a similar section: + +.logging-log4j2.mod +---- +[provides] +logging +---- + +If there are no enabled modules that provide the `logging` virtual module, either explicitly or transitively, then the default provider is used, in this case `logging-jetty.mod`. + +Otherwise, a module that provides the `logging` virtual module is explicitly or transitively enabled, and the default provider is not used. + +[[components]] +== Module Components + +A Jetty module may provide one or more Java components that implement a feature. +These Java components are nothing more than regular Java classes that are instantiated and configured via xref:xml/index.adoc[Jetty XML] files. + +The Jetty XML file of a Jetty module may instantiate and assemble together its own components, or reference existing components from other Jetty modules to enhance or reconfigure them. + +The Jetty module's XML files are read from the typical xref:start/index.adoc#configure[configuration source directories], under the `etc/` subdirectory; from higher priority to lower priority: + +* The `$JETTY_BASE/etc/` directory. +* If a directory is specified with the `--add-config-dir` option, its `etc/` subdirectory. +* The `$JETTY_HOME/etc/` directory. + +The standard Jetty modules XML files that Jetty provides out-of-the-box are under `$JETTY_HOME/etc/`. + +For example, a Jetty XML file that allocates Jetty's `QueuedThreadPool` could be as simple as: + +[,xml] +.jetty-threadpool.xml +---- + + + + + + + + + +---- + +Note how the Jetty XML file above is allocating (with the `` element) a `QueuedThreadPool` instance, giving it the unique `id` of `threadPool` (so that other modules can reference it, if they need to). +It is then calling the setter method `QueuedThreadPool.setMaxThreads(int)` with the value defined by the <> `jetty.threadPool.maxThreads`; if the property value is not defined, it will have the default value of `256`. + +This is nothing more than Java code in XML format with configurable properties support that can be leveraged by the xref:start/index.adoc[Jetty start mechanism]. + +The Jetty module's XML files make easy to instantiate and assemble Java components (just write the equivalent Java code in XML format), and make easy to configure them by declaring module properties that can be easily customized elsewhere (for example, in `+*.ini+` files as described in xref:start/index.adoc#configure-enable[this section], or on the command line as described in xref:start/index.adoc#start[this section]). + +[IMPORTANT] +==== +Remember that the standard Jetty XML files in `$JETTY_HOME/etc/` should not be modified. + +Even if you need to modify a standard Jetty component, write a new Jetty XML file, save it under `$JETTY_BASE/etc/`, and create a xref:modules/custom.adoc[custom Jetty module] so that it gets processed when Jetty starts. +==== + +[[properties]] +== Module Properties + +A Jetty module property is declared in the <> via the `` element. +Modules properties are used to parametrize Jetty components so that you can customize their values when Jetty starts, rather than hard-coding it in the XML files. + +NOTE: You can declare your own properties, but the `+jetty.*+` namespace is reserved. + +A module property can be given a value in a Jetty module `[ini]` section (see <>), in a `+*.ini+` file as described in xref:start/index.adoc#configure-enable[this section], or on the command line as described in xref:start/index.adoc#start[this section]. + +The syntax to specify a property value is the following: + +=:: +Sets the property value unconditionally. ++=:: +Appends the value to the existing value. +This is useful to append a value to properties that accept a comma separated list of values, for example: ++ +---- +jetty.webapp.addProtectedClasses+=,com.acme +---- ++ +// TODO: check what happens if the property is empty and +=,value is done: is the comma stripped? If so add a sentence about this. +?=:: +Sets the property value only if it is not already set. +This is useful to define default values, for example for "version" properties, where the "version" property can be explicitly configured to a newer version, but if it is not explicitly configured it will have a default version (see also xref:start/index.adoc#configure-custom-module[here]). +For example: ++ +---- +conscrypt.version?=2.5.1 +jetty.sslContext.provider?=Conscrypt +---- + +[[directives]] +== Module Directives + +Lines that start with `#` are comments. + +[[directive-description]] +=== [description] + +A text that describes the module. + +This text will be shown by the xref:start/index.adoc#configure[Jetty start mechanism] when using the `--list-modules` command. + +[[directive-tags]] +=== [tags] + +A list of words that characterize the module. + +Modules that have the same tags will be shown by the Jetty start mechanism when using the `--list-modules=` command. + +.example.mod +---- +[tags] +demo +webapp +jsp +---- + +[[directive-provides]] +=== [provides] + +A module name with an optional `default` specifier. + +As explained in the <>, there can be many module files each providing a different implementation for the same feature. + +The format is: + +---- +[provides] +[|default] +---- + +where the `|default` part is optional and specifies that the module is the default provider. + +[[directive-depends]] +=== [depends] + +A list of module names that this module depends on. + +For example, the standard module `http` depends on module `server`. +Enabling the `http` module also enables, transitively, the `server` module, since the `http` module cannot work without the `server` module; when the `server` module is transitively enabled, the modules it depends on will be transitively enabled, and so on recursively. + +The `[depends]` directive establishes a https://en.wikipedia.org/wiki/Partially_ordered_set[_partial order_] relationship among modules so that enabled modules can be sorted and organized in a graph. +Circular dependencies are not allowed. + +The order of the enabled modules is used to determine the processing of the configuration, for example the order of processing of the <>+] section, the order of processing of XML files defined in the <>+] section, etc. + +[[directive-after]] +=== [after] + +This directive indicates that this module is ordered after the listed module names, if they are enabled. + +For example, module `https` is `[after]` module `http2`. +Enabling the `https` module _does not_ enable the `http2` module. + +However, if the `http2` module is enabled (explicitly or transitively), then the `https` module is <> _after_ the `http2` module. +In this way, you are guaranteed that the `https` module is processed after the `http2` module. + +[[directive-before]] +=== [before] + +This directive indicates that this module is ordered before the listed module names, if they are enabled. + +One use of this directive is to create a prerequisite module without the need to modify the `depends` directive of an existing module. +For example, to create a custom `org.eclipse.jetty.server.Server` subclass instance to be used by the standard `server` module, without modifying the existing `server.mod` file nor the `jetty.xml` file that it uses. This can be achieved by creating the `custom-server` xref:modules/custom.adoc[Jetty custom module]: + +.custom-server.mod +---- +[description] +This module creates a custom Server subclass instance. + +[before] +server + +[xml] +etc/custom-server.xml +---- + +The `custom-server.xml` file is the following: + +.custom-server.xml +[,xml] +---- + + + + +---- + +The presence of the `[before]` directive in `custom-server.mod` causes the processing of the `custom-server.xml` file to happen before the processing of the standard `jetty.xml` file referenced by the standard `server.mod` Jetty module. + +Thus, the instance assigned to the `Server` identifier is your custom `com.acme.server.CustomJettyServer` instance from the `custom-server.xml` file; this instance is then used while processing the `jetty.xml` file. + +[[directive-files]] +=== [files] + +A list of paths (directories and/or files) that are necessary for the module, created or resolved when the module is enabled. + +Each path may be of the following types: + +Path Name:: +A path name representing a file, or a directory if the path name ends with `/`, such as `webapps/`. +The file or directory will be created relative to `$JETTY_BASE`, if not already present. ++ +For example: ++ +---- +[files] +logs/ +---- + +Maven Artifact:: +An URI representing a Maven artifact to be downloaded from Maven Central, if not already present. +Property expansion is supported. ++ +The format is: ++ +---- +[files] +maven:////[/]| +---- ++ +where `` is optional, and `` after the `|` is the path under `$JETTY_BASE` where the downloaded file should be saved. ++ +For example: ++ +[,options=nowrap] +---- +[files] +maven://org.postgresql/postgresql/${postgresql-version}|lib/postgresql-${postgresql-version}.jar +---- + +BaseHome:: +An URI representing a `$JETTY_HOME` resource to be copied in `$JETTY_BASE`, if not already present. +URIs of this type are typically only used by standard Jetty modules; custom modules should not need to use it. ++ +The format is: ++ +---- +[files] +basehome:| +---- ++ +For example: ++ +---- +[files] +basehome:modules/demo.d/demo-moved-context.xml|webapps/demo-moved-context.xml +---- + +HTTP URL:: + +An `http://` or `https://` URL to be downloaded, if not already present. ++ +The format is: ++ +---- +[files] +| +---- ++ +For example: ++ +---- +[files] +https://acme.com/favicon.ico|webapps/acme/favicon.ico +---- + +[[directive-libs]] +=== [libs] + +A list of paths, relative to the xref:start/index.adoc#configure[configuration source directories], of `+*.jar+` library files and/or directories that are added to the server class-path (or module-path when xref:start/start-jpms.adoc[running in JPMS mode]). + +The `[libs]` section if often used in conjunction with the `[files]` section. + +For example: + +---- +[files] +maven://org.postgresql/postgresql/${postgresql-version}|lib/postgresql-${postgresql-version}.jar + +[libs] +lib/postgresql-${postgresql-version}.jar +---- + +The `postgresql-.jar` artifact is downloaded from Maven Central, if not already present, into the `$JETTY_BASE/lib/` directory when the module is enabled. + +When Jetty starts, the `$JETTY_BASE/lib/postgresql-.jar` will be in the server class-path (or module-path). + +[[directive-xml]] +=== [xml] + +A list of paths, relative to the xref:start/index.adoc#configure[configuration source directories], of Jetty `+*.xml+` files that are passed as program arguments to be processed when Jetty starts (see the xref:start/index.adoc#start-xml[section about assembling Jetty components]). + +Jetty XML files are read from the typical xref:start/index.adoc#configure[configuration source directories], under the `etc/` subdirectory. +Standard Jetty XML files are under `$JETTY_HOME/etc/`, while custom Jetty XML files are typically under `$JETTY_BASE/etc/`. + +For example: + +---- +[xml] +etc/custom/components.xml +---- + +[[directive-ini]] +=== [ini] + +A list of program arguments to pass to the command line when Jetty is started. + +The program arguments may include any command line option (see xref:start/index.adoc#reference[here] for the list of command line options), <> and/or <>. + +A property defined in the `[ini]` section is available in the `+*.mod+` module file for property expansion, for example: + +---- +[ini] +postgresql-version?=42.6.0 + +[lib] +lib/postgresql-${postgresql-version}.jar +---- + +In the example above, the `[lib]` section contains `$\{postgresql-version}`, a reference to property `postgresql-version` whose value is defined in the `[ini]` section. +The expression `${}` _expands_ the property replacing the expression with the property value. + +See also the xref:start/start-jpms.adoc[JPMS section] for additional examples about the `[ini]` section. + +[[directive-ini-template]] +=== [ini-template] + +A list of properties to be copied in the `+*.ini+` file generated when xref:start/index.adoc#configure-enable[the module is enabled]. + +The list of properties is derived from the <> that declare them. + +The properties are typically assigned their default value and commented out, so that it is evident which properties have been uncommented and customized with a non-default value. + +[[directive-exec]] +=== [exec] + +A list of JVM command line options and/or system properties passed to a forked JVM. + +When the `[exec]` section is present, the JVM running the Jetty start mechanism will fork another JVM, passing the JVM command line options and system properties listed in the `[exec]` sections of the enabled modules. + +This is necessary because JVM options such as `-Xmx` (that specifies the max JVM heap size) cannot be changed in a running JVM. +For an example, see xref:start/index.adoc#configure-custom-module-exec[this section]. + +You can avoid that the Jetty start mechanism forks the second JVM, as explained in xref:start/index.adoc#configure-dry-run[this section]. + +[[directive-jpms]] +=== [jpms] + +A list of JVM command line options related to the Java Module System. + +This section is processed only when Jetty is xref:start/start-jpms.adoc[started in JPMS mode]. + +The directives are: + +add-modules:: +Equivalent to the JVM option `--add-modules`. +The format is: ++ +---- +[jpms] +add-modules: (,)* +---- ++ +where `module` is a JPMS module name. + +patch-module:: +Equivalent to the JVM option `--patch-module`. +The format is: ++ +---- +[jpms] +patch-module: =(:)* +---- +where `module` is a JPMS module name. + +add-opens:: +Equivalent to the JVM option `--add-opens`. +The format is: ++ +---- +[jpms] +add-opens: /=(,)* +---- +where `module` and `target-module` are a JPMS module names. + +add-exports:: +Equivalent to the JVM option `--add-exports`. +The format is: ++ +---- +[jpms] +add-exports: /=(,)* +---- +where `module` and `target-module` are a JPMS module names. + +add-reads:: +Equivalent to the JVM option `--add-exports`. +The format is: ++ +---- +[jpms] +add-reads: =(,)* +---- +where `module` and `target-module` are a JPMS module names. + +[[directive-license]] +=== [license] + +The license under which the module is released. + +A Jetty module may be released under a license that is different from Jetty's, or use libraries that require end-users to accept their licenses in order to be used. + +You can put the license text in the `[license]` section, and when the Jetty module is enabled the license text will be printed on the terminal, and the user prompted to accept the license. +If the user does not accept the license, the module will not be enabled. + +For example: + +---- +[license] +Acme Project is an open source project hosted on GitHub +and released under the Apache 2.0 license. +https://www.apache.org/licenses/LICENSE-2.0.txt +---- + +[[directive-version]] +=== [version] + +The minimum Jetty version for which this module is valid. + +For example, a module may only be valid for Jetty 10 and later, but not for earlier Jetty versions (because it references components that have been introduced in Jetty 10). + +For example: + +---- +[version] +10.0 +---- + +A Jetty module with such a section will only work for Jetty 10.0.x or later. diff --git a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc new file mode 100644 index 000000000000..0ac54f529b83 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc @@ -0,0 +1,721 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Standard Modules + +[[alpn]] +== Module `alpn` + +The `alpn` module enables support for the ALPN negotiation mechanism of the TLS protocol. + +You can configure the list of application protocols negotiated by the ALPN mechanism, as well as the default protocol to use if the ALPN negotiation fails (for example, the client does not support ALPN). + +The module properties are: + +---- +include::{jetty-home}/modules/alpn.mod[tags=documentation] +---- + +[[bytebufferpool]] +== Module `bytebufferpool` + +The `bytebufferpool` module allows you to configure the server-wide `ByteBuffer` pool. +Pooling ``ByteBuffer``s results in less memory usage and less pressure on the Garbage Collector. + +``ByteBuffer``s are pooled in _buckets_; each bucket as a capacity that is a multiple of a capacity factor that you can configure. +For example, if a request for a `ByteBuffer` of capacity 2000 is requested, and the capacity factor is 1024, then the pool will allocate a buffer from the second bucket, of capacity 2048 (1024 * 2). + +Applications that need to sustain many concurrent requests -- or load spikes -- may require many buffers during peak load. These buffers will remain pooled once the system transitions to a lighter load (or becomes idle), and it may be undesirable to retain a lot of memory for an idle system. + +It is possible to configure the max heap memory and the max direct memory that the pool retains. +Excess buffers will not be pooled and will be eventually garbage collected. + +The module file is `$JETTY_HOME/modules/bytebufferpool.mod`: + +---- +include::{jetty-home}/modules/bytebufferpool.mod[] +---- + +Among the configurable properties, the most relevant are: + +`jetty.byteBufferPool.maxHeapMemory`:: +This property allows you to cap the max heap memory retained by the pool. + +`jetty.byteBufferPool.maxDirectMemory`:: +This property allows you to cap the max direct memory retained by the pool. + +[[console-capture]] +== Module `console-capture` + +The `console-capture` module captures `System.out` and `System.err` output and appends it to a rolling file. + +The file is rolled every day at the midnight of the configured timezone. +Old, rolled files are kept for the number of days specified by the `jetty.console-capture.retainDays` property. + +The module properties are: + +---- +include::{jetty-home}/modules/console-capture.mod[tags=documentation] +---- + +[[core-deploy]] +== Module `core-deploy` + +include::{jetty-home}/modules/ee10-deploy.mod[tags=description] + +Deployment is managed via a `DeploymentManager` component that watches a directory for changes. +See xref:deploy/index.adoc[how to deploy web applications] for more information. + +TODO + +[[cross-origin]] +== Module `cross-origin` + +The `cross-origin` module provides support for the https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[CORS protocol] implemented by browsers when performing cross-origin requests. + +This module installs the xref:programming-guide:server/http.adoc#handler-use-cross-origin[`CrossOriginHandler`] in the `Handler` tree; `CrossOriginHandler` inspects cross-origin requests and adds the relevant CORS response headers. + +`CrossOriginHandler` should be used when an application performs cross-origin requests to your domain, to protect from https://owasp.org/www-community/attacks/csrf[cross-site request forgery] attacks. + +The module properties are: + +---- +include::{jetty-home}/modules/cross-origin.mod[tags=documentation] +---- + +You must configure at least the property `jetty.crossorigin.allowedOriginPatterns` to allow one or more origins. + +It is recommended that you consider configuring also the property `jetty.crossorigin.allowCredentials`. +When set to `true`, clients send cookies and authentication headers in cross-origin requests to your domain. +When set to `false`, cookies and authentication headers are not sent. + +[[eeN-deploy]] +== Module `{ee-all}-deploy` + +include::{jetty-home}/modules/ee10-deploy.mod[tags=description] + +Deployment is managed via a `DeploymentManager` component that watches a directory for changes. +See xref:deploy/index.adoc[how to deploy web applications] for more information. + +Adding files or directories to this monitored directory will cause the `DeploymentManager` to deploy them as web applications; updating files already existing in this monitored directory will cause the `DeploymentManager` to re-deploy the corresponding web application; removing files in this monitored directory will cause the `DeploymentManager` to "undeploy" the corresponding web application. +(You can find a more detailed discussion of these rules in the xref:deploy/index.adoc#rules[deployment rules] section.) + +Multiple versions of this module exist (`{ee-all}-deploy`) to support each Jakarta EE platform's version of the Java Servlet specification. +Jetty's configuration properties are nearly identical across these versions; the configuration properties for the `{ee-current}-deploy` Jetty module are: + +---- +include::{jetty-home}/modules/ee10-deploy.mod[tags=ini-template] +---- + +Among the configurable properties, the most relevant are: + +`jetty.deploy.monitoredDir`:: +The name of the monitored directory. +`jetty.deploy.scanInterval`:: +The scan period in seconds, that is how frequently the `DeploymentManager` wakes up to scan the monitored directory for changes. +Setting `jetty.deploy.scanInterval=0` disabled _hot_ deployment so that only static deployment will be possible (see also xref:deploy/index.adoc#hot-static[here] for more information). + +[[eeN-webapp]] +== Module `{ee-all}-webapp` + +include::{jetty-home}/modules/ee10-webapp.mod[tags=description] + +Multiple versions of this module exist (`{ee-all}-webapp`) to support each Jakarta EE platform's version of the Java Servlet specification. +Jetty's configuration properties are identical across all versions of this module, and are as follows: + +---- +include::{jetty-home}/modules/ee10-webapp.mod[tags=ini-template] +---- + +[[http]] +== Module `http` + +The `http` module provides the clear-text connector and support for the clear-text HTTP/1.1 protocol, and depends on the <>. + +The module properties to configure the clear-text connector are: + +---- +include::{jetty-home}/modules/http.mod[tags=documentation] +---- + +Among the configurable properties, the most relevant are: + +`jetty.http.port`:: +The network port that Jetty listens to for clear-text HTTP/1.1 connections -- default `8080`. +`jetty.http.idleTimeout`:: +The amount of time a connection can be idle (i.e. no bytes received and no bytes sent) until the server decides to close it to save resources -- default `30` seconds. +`jetty.http.acceptors`:: +The number of threads that compete to accept connections -- default 1. Use -1 to let the accept heuristic decides the value; the current heuristic calculates a value based on the number of cores). +Refer to <> for more information about acceptor threads. +`jetty.http.selectors`:: +The number of NIO selectors (with an associated thread) that manage connections -- default -1 (i.e. a select heuristic decides the value; the current heuristic calculates a value based on the number of cores). + +[[http-acceptors]] +=== Configuration of Acceptors + +Accepting connections from remote clients may be configured as a blocking operation, or a non-blocking operation. + +When accepting connections is configured as a blocking operation (the number of acceptors is greater than zero), a thread is blocked in the `accept()` call until a connection is accepted, and other acceptor threads (if any) are blocked on the lock acquired by the accepting thread just before the `accept()` call. + +When the accepting thread accepts a connection, it performs a little processing of the just accepted connection, before forwarding it to other components. + +During this little processing other connections may be established; if there is only one accepting thread, the newly established connections are waiting for the accepting thread to finish the processing of the previously accepted connection and call again `accept()`. + +Servers that manage a very high number of connections that may (naturally) come and go, or that handle inefficient protocols that open and close connections very frequently (such as HTTP/1.0) may benefit of an increased number of acceptor threads, so that when one acceptor thread processes a just accepted connection, another acceptor thread can immediately take over accepting connections. + +When accepting connections is configured as a non-blocking operation (the number of acceptors is zero), then the server socket is set in non-blocking mode and added to a NIO selector. +In this way, no dedicated acceptor threads exist: the work of accepting connections is performed by the selector thread. + +[[http-selectors]] +=== Configuration of Selectors + +Performing a NIO `select()` call is a blocking operation, where the selecting thread is blocked in the `select()` call until at least one connection is ready to be processed for an I/O operation. +There are 4 I/O operations: ready to be accepted, ready to be connected, ready to be read and ready to be written. + +A single NIO selector can manage thousands of connections, with the assumption that not many of them will be ready at the same time. + +For a single NIO selector, the ratio between the average number of selected connections over the total number of connections for every `select()` call depends heavily on the protocol but also on the application. + +Multiplexed TCP protocols such as HTTP/2 tend to be busier than duplex protocols such as HTTP/1.1, leading to a higher ratio. + +REST applications that exchange many little JSON messages tend to be busier than file server applications, leading to a higher ratio. + +The higher the ratio, the higher the number of selectors you want to have, compatibly with the number of cores -- there is no point in having 64 selector threads on a single core hardware. + +[[http2]] +== Module `http2` + +The `http2` module enables support for the secure HTTP/2 protocol. + +The module properties are: + +---- +include::{jetty-home}/modules/http2.mod[tags=documentation] +---- + +// tag::rate-control[] +The `jetty.http2.rateControl.maxEventsPerSecond` property controls the number of "bad" or "unnecessary" frames that a client may send before the server closes the connection (with code https://tools.ietf.org/html/rfc7540#section-7[`ENHANCE_YOUR_CALM`]) to avoid a denial of service. + +For example, an attacker may send empty `SETTINGS` frames to a server in a tight loop. +While the `SETTINGS` frames don't change the server configuration and each of them is somehow harmless, the server will be very busy processing them because they are sent by the attacker one after the other, causing a CPU spike and eventually a denial of service (as all CPUs will be busy processing empty `SETTINGS` frames). + +The same attack may be performed with `PRIORITY` frames, empty `DATA` frames, `PING` frames, etc. + +[[http2c]] +== Module `http2c` + +The `http2c` module enables support for the clear-text HTTP/2 protocol. + +The module properties are: + +---- +include::{jetty-home}/modules/http2c.mod[tags=documentation] +---- + +The `jetty.http2.rateControl.maxEventsPerSecond` property controls the number of "bad" or "unnecessary" frames that a client may send before the server closes the connection (with code https://tools.ietf.org/html/rfc7540#section-7[`ENHANCE_YOUR_CALM`]) to avoid a denial of service. + +For example, an attacker may send empty `SETTINGS` frames to a server in a tight loop. +While the `SETTINGS` frames don't change the server configuration and each of them is somehow harmless, the server will be very busy processing them because they are sent by the attacker one after the other, causing a CPU spike and eventually a denial of service (as all CPUs will be busy processing empty `SETTINGS` frames). + +The same attack may be performed with `PRIORITY` frames, empty `DATA` frames, `PING` frames, etc. + +[[http3]] +== Module `http3` + +The `http3` module enables support for the HTTP/3 protocol. + +The module properties are: + +---- +include::{jetty-home}/modules/http3.mod[tags=documentation] +---- + +[[http-forwarded]] +== Module `http-forwarded` + +The `http-forwarded` module provides support for processing the `Forwarded` HTTP header (defined in https://tools.ietf.org/html/rfc7239[RFC 7239]) and the now obsoleted `X-Forwarded-*` HTTP headers. + +The module properties are: + +---- +include::{jetty-home}/modules/http-forwarded.mod[tags=documentation] +---- + +[[https]] +== Module `https` + +The `https` module provides the HTTP/1.1 protocol to the <>. + +The module file is `$JETTY_HOME/modules/https.mod`: + +---- +include::{jetty-home}/modules/https.mod[] +---- + +[[jmx]] +== Module `jmx` + +include::{jetty-home}/modules/ee10-webapp.mod[tags=description] + +This configuration is useful for xref:jmx/index.adoc#local[local development and testing]. +If you need to xref:jmx/index.adoc#remote[enable remote access], use the xref:jmx/index.adoc#remote[`jmx-remote` module]. + +[[jmx-remote]] +== Module `jmx-remote` + +The `jmx-remote` module provides remote access to JMX clients. + +The module properties to configure remote JMX connector are: + +---- +include::{jetty-home}/modules/jmx-remote.mod[tags=documentation] +---- + +The system property `java.rmi.server.hostname` is specified with the usual notation, prepending a `-D` in front of the system property name. + +The system property `java.rmi.server.hostname` is uncommented because it is necessary in the default configuration -- most systems do not have the local name resolution configured properly for remote access. + +As an example, in a Linux machine named `beryl`, the `/etc/hosts` file may contain these entries: + +---- +127.0.0.1 localhost +127.0.1.1 beryl +---- + +If the system property `java.rmi.server.hostname` is not specified, the RMI implementation uses the host name `beryl` to figure out the IP address to store in the RMI stub, in this case `127.0.1.1`. +However, we the RMI server is configured to bind to `localhost`, i.e. `127.0.0.1`. + +If the system property `java.rmi.server.hostname` is not specified, the RMI client will try to connect to `127.0.1.1` (because that's what in the RMI stub) and fail because nothing is listening on that address. + +[[requestlog]] +== Module `requestlog` + +The `requestlog` module provides HTTP request/response logging in the standard https://en.wikipedia.org/wiki/Common_Log_Format[NCSA format], or in a custom format of your choice. + +The module properties are: + +---- +include::{jetty-home}/modules/requestlog.mod[tags=documentation] +---- + +The property `jetty.requestlog.formatString` can be customized using format codes. + +javadoc::code:partial$org/eclipse/jetty/server/CustomRequestLog.java[] + +[[resources]] +== Module `resources` + +include::{jetty-home}/modules/resources.mod[tags=description] + +A common use-case for this module is to provide resources for third-party libraries via the xref:start/index.adoc#start-class-path[server classpath]. +For instance, many logging libraries (including https://logging.apache.org/log4j/2.x/[Log4j2] and https://logback.qos.ch/[Logback]) look for their configuration files on the classpath. + +Jetty provides a logging library implementation -- enabled via the `logging-jetty` module -- whose configuration file is `$JETTY_BASE/resources/jetty-logging.properties`. + +[[rewrite]] +== Module `rewrite` + +The `rewrite` module inserts the `RewriteHandler` at the beginning of the `Handler` chain, providing URI-rewriting features similar to the https://httpd.apache.org/docs/current/mod/mod_rewrite.html[Apache's mod_rewrite] or the https://nginx.org/en/docs/http/ngx_http_rewrite_module.html[Nginx rewrite module]. + +The module properties are: + +---- +include::{jetty-home}/modules/rewrite.mod[tags=documentation] +---- + +A common use of the `rewrite` module is to redirect/rewrite old URI paths that have been renamed, for example from `+/old/*+` to `+/new/*+`; in this way, the old paths will not result in a `404` response, but rather be redirected/rewritten to the new paths. + +`RewriteHandler` matches incoming requests against a set of rules that you can specify in the `$JETTY_BASE/etc/jetty-rewrite-rules.xml` file. + +Rules can be matched against request data such as the request URI or the request headers; if there is a match, the rule is applied. + +The rule file `$JETTY_BASE/etc/jetty-rewrite-rules.xml` is initially empty, but contains commented examples of rules that you can add. + +The list of available rules can be found link:{javadoc-url}/org/eclipse/jetty/rewrite/handler/package-summary.html[here]. + +An example of `jetty-rewrite-rules.xml` is the following: + +[,xml] +.jetty-rewrite-rules.xml +---- + + + + + + + + 301 + /old/(.*) + /new/$1 + + + + +---- + +Rules can be scoped to a specific virtual host. +In the example below, the rule will only be evaluated if the virtual host matches `example.com`: + +[,xml] +.jetty-rewrite-rules.xml +---- + + + + + + + + + example.com + + + + + /advice + /support + + + + + + + +---- + +[[server]] +== Module `server` + +The `server` module provides generic server support, and configures generic HTTP properties that apply to all HTTP protocols, the scheduler properties and the server specific properties. + +The `server` module depends on the <>, the <> and the xref:server/index.adoc#logging[`logging` module]. + +[NOTE] +==== +The `server` module configures the shared parameters for generic HTTP handling, but does not enable any specific network protocol. You have to explicitly enable the protocols you want to support by enabling, for example, the <> for clear-text HTTP/1.1 support, or the <> for secure HTTP/2 support, etc. + +See also the xref:protocols/index.adoc[protocols section] for more information about the supported protocols. +==== + +[[server-http-config]] +=== HTTP Configuration Properties + +The module properties to configure generic HTTP properties are listed below. Mostly they frequently apply to HTTP/1, HTTP/2 and HTTP/3, but some parameters are version specific: + +---- +include::{jetty-home}/modules/server.mod[tags=documentation-http-config] +---- + +Among the configurable properties, the most relevant are: + +`jetty.httpConfig.headerCacheSize`:: +The header cache is used when parsing HTTP/1 to more efficiently handle fields that are repeated in every request on a connection. If the server does not receive persistent connection or infrequent repeated fields, then there may be a performance gain in reducing the cache size. If large fields are frequently repeated, then a large cache may be beneficial. + +`jetty.httpConfig.delayDispatchUntilContent`:: +It is not uncommon for the network packets containing a request header to arrive before packets that contain the data of any request body. In such cases it may be beneficial for overall performance to delay dispatching the request to be handled until the first data packet arrives, as this may avoid blocking the handling thread. However, if minimum latency for receiving the request without content is important, then this parameter can be set to false. + +`jetty.httpConfig.sendServerVersion`:: +Whether you want to send the `Server` header in every HTTP response: ++ +[source,subs=attributes+] +---- +HTTP/1.1 200 OK +Content-Length: 0 +Server: Jetty({version}) +---- + +[[server-config]] +=== Server Configuration Properties + +The module properties to configure the Jetty server are: + +---- +include::{jetty-home}/modules/server.mod[tags=documentation-server-config] +---- + +Among the configurable properties, the most relevant are: + +`jetty.server.dumpAfterStart`:: +Whether to perform a `Server.dump()` operation after the `Server` has started. +The output of the dump operation is sent to `System.err`. +See also the xref:troubleshooting/index.adoc#dump[Jetty Server Dump] section for more information. + +`jetty.server.dumpBeforeStop`:: +Whether to perform a `Server.dump()` operation before the `Server` stops. +The output of the dump operation is sent to `System.err`. +See also the xref:troubleshooting/index.adoc#dump[Jetty Server Dump] section for more information. + +`jetty.server.stopAtShutdown`:: +Whether to call `Server.stop()` through a JVM shutdown hook when the JVM exits. + +[[server-compliance]] +=== Server Compliance Properties + +The Jetty server strives to keep up with the latest https://en.wikipedia.org/wiki/Request_for_Comments[IETF RFC]s for compliance with internet specifications, which are periodically updated. When possible, Jetty will support backwards compatibility by providing compliance modes that can be configured to allow violations of the current specifications that may have been allowed in obsoleted specifications. +The module properties to configure the Jetty server compliance are: + +---- +include::{jetty-home}/modules/server.mod[tags=documentation-server-compliance] +---- + +Among the configurable properties, the most relevant are: + +`jetty.httpConfig.compliance`:: +Configures the compliance to HTTP specifications. +The value could be: + +* One of the predefined link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] constants, such as `RFC7230` or `RFC2616`. +For example: `jetty.httpConfig.compliance=RFC2616`. +* A comma-separated list of violations to allow or forbid, as specified by the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html#from(java.lang.String)[`HttpCompliance.from(String)`] method. +For example, `jetty.httpConfig.compliance=RFC7230,MULTIPLE_CONTENT_LENGTHS` means that the HTTP compliance is that defined by `RFC7230`, but also allows the `HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS`, so that requests that have multiple `Content-Length` headers are accepted (they would be rejected when using just `HttpCompliance.RFC7230`). ++ +For more information about `HttpCompliance` see also xref:programming-guide:server/compliance.adoc#http[this section]. + +`jetty.httpConfig.uriCompliance`:: +Configures the compliance to URI specifications. +The value could be: + +* One of the predefined link:{javadoc-url}/org/eclipse/jetty/http/UriCompliance.html[`UriCompliance`] constants, such as `DEFAULT` or `RFC3986`. +For example: `jetty.httpConfig.compliance=RFC3986`. +* A comma-separated list of violations to allow or forbid, as specified by the link:{javadoc-url}/org/eclipse/jetty/http/UriCompliance.html#from(java.lang.String)[`UriCompliance.from(String)`] method. +For example, `jetty.httpConfig.uriCompliance=RFC3986,-AMBIGUOUS_PATH_SEPARATOR` means that the URI compliance is that defined by `RFC3986`, but also does not allow the `UriCompliance.Violation.AMBIGUOUS_PATH_SEPARATOR`, so that requests that have URIs such as `/foo/bar%2Fbaz` (where `%2F` is the URL-encoded `/` character) are rejected (they would be accepted when using just `UriCompliance.RFC3986`). ++ +For more information about `UriCompliance` see also xref:programming-guide:server/compliance.adoc#uri[this section]. + +`jetty.httpConfig.requestCookieCompliance`:: +`jetty.httpConfig.responseCookieCompliance`:: +Configures the compliance to HTTP cookie specifications. +The value could be: + +* One of the predefined link:{javadoc-url}/org/eclipse/jetty/http/CookieCompliance.html[`CookieCompliance`] constants, such as `RFC6265`. +For example: `jetty.httpConfig.compliance=RFC6265`. +* A comma-separated list of violations to allow or forbid, as specified by the link:{javadoc-url}/org/eclipse/jetty/http/CookieCompliance.html#from(java.lang.String)[`CookieCompliance.from(String)`] method. +For example, `jetty.httpConfig.requestCookieCompliance=RFC6265,-RESERVED_NAMES_NOT_DOLLAR_PREFIXED` means that the cookie compliance is that defined by `RFC6265`, but also does not allow the `CookieCompliance.Violation.RESERVED_NAMES_NOT_DOLLAR_PREFIXED`, so that requests that have cookie headers such as `Cookie: $foo=bar` are rejected (they would be accepted when using just `CookieCompliance.RFC6265`). ++ +For more information about `CookieCompliance` see also xref:programming-guide:server/compliance.adoc#cookie[this section]. + +[[scheduler-config]] +=== Server Scheduler Configuration Properties + +The module properties to configure the Jetty server scheduler are: + +---- +include::{jetty-home}/modules/server.mod[tags=documentation-scheduler-config] +---- + +[[ssl]] +== Module `ssl` + +The `ssl` module provides the secure connector, and allows you to configure the KeyStore properties and the TLS parameters, and depends on the <>. + +[[ssl-connector]] +=== Secure Connector Properties + +The module properties to configure the secure connector are: + +---- +include::{jetty-home}/modules/ssl.mod[tags=documentation-connector] +---- + +Among the configurable properties, the most relevant are: + +`jetty.ssl.port`:: +The network port that Jetty listens to for secure connections -- default `8443`. +`jetty.ssl.idleTimeout`:: +The amount of time a connection can be idle (i.e. no bytes received and no bytes sent) until the server decides to close it to save resources -- default `30000` milliseconds. +`jetty.ssl.acceptors`:: +The number of threads that compete to accept connections -- default 1. Use -1 to let the accept heuristic decides the value; the current heuristic calculates a value based on the number of cores). +Refer to <> for more information about acceptor threads. +`jetty.ssl.selectors`:: +The number of NIO selectors (with an associated thread) that manage connections -- default -1 (i.e. a select heuristic decides the value; the current heuristic calculates a value based on the number of cores). +Refer to <> for more information about selector threads. + +The module properties to configure the KeyStore and TLS parameters are: + +---- +include::{jetty-home}/modules/ssl.mod[tags=documentation-ssl-context] +---- + +[[ssl-keystore-tls]] +=== KeyStore Properties and TLS Properties + +Among the configurable properties, the most relevant are: + +`jetty.sslContext.keyStorePath`:: +The KeyStore path on the file system, either an absolute path or a relative path to `$JETTY_BASE` -- defaults to `$JETTY_BASE/etc/keystore.p12`. +`jetty.sslContext.keyStorePassword`:: +The KeyStore password, which you want to explicitly configure. +The password may be obfuscated with the xref:tools/index.adoc#password[Jetty Password tool]. + +If you need to configure client certificate authentication, you want to configure one of these properties (they are mutually exclusive): + +`jetty.sslContext.needClientAuth`:: +Whether client certificate authentication should be required. +`jetty.sslContext.wantClientAuth`:: +Whether client certificate authentication should be requested. + +If you configure client certificate authentication, you need to configure and distribute a client KeyStore as explained in xref:keystore/index.adoc#client-authn[this section]. + +[[ssl-reload]] +== Module `ssl-reload` + +The `ssl-reload` module provides a periodic scanning of the directory where the KeyStore file resides. +When the scanning detects a change to the KeyStore file, the correspondent `SslContextFactory.Server` component is reloaded with the new KeyStore configuration. + +The module properties are: + +---- +include::{jetty-home}/modules/ssl-reload.mod[tags=documentation] +---- + +[[state-tracking]] +== Module `state-tracking` + +The `state-tracking` Jetty module inserts the `StateTrackingHandler` at the beginning of the Handler chain. + +`StateTrackingHandler` is a xref:troubleshooting/index.adoc[troubleshooting] `Handler` that tracks usages of `Handler`/`Request`/`Response` asynchronous APIs, and logs at warning level invalid usages of the APIs that may lead to blockages, deadlocks, or missing completion of ``Callback``s. + +This module can be enabled to troubleshoot web applications that do not behave as expected, for example: + +* That consume a lot of threads (possibly because they block). +* That do not send responses (or send only partial responses) to clients. +* That timeout when apparently they have received or have sent all data. + +The module properties are: + +---- +include::{jetty-home}/modules/state-tracking.mod[tags=documentation] +---- + +[[test-keystore]] +== Module `test-keystore` + +The `test-keystore` module creates on-the-fly a KeyStore containing a self-signed certificate for domain `localhost`. +The KeyStore file is automatically deleted when the JVM exits, and re-created when you restart Jetty, to enforce the fact that it is a _test_ KeyStore that should not be reused if not for testing. + +The module file is `$JETTY_HOME/modules/test-keystore.mod`: + +---- +include::{jetty-home}/modules/test-keystore.mod[] +---- + +Note how properties `jetty.sslContext.keyStorePath` and `jetty.sslContext.keyStorePassword` are configured, only if not already set (via the `?=` operator), directly in the module file, rather than in a `+*.ini+` file. +This is done to avoid that these properties accidentally overwrite a real KeyStore configuration. + +[[threadpool]] +== Module `threadpool` + +The `threadpool` module allows you to configure the server-wide thread pool. + +The thread pool creates threads on demand up to `maxThreads`, and idles them out if they are not used. + +Since Jetty uses the thread pool internally to execute critical tasks, it is not recommended to constrain the thread pool to small values of `maxThreads` with the purpose of limiting HTTP request concurrency, as this could very likely cause a server lockup when Jetty needs to run a critical task but there are no threads available. +Start with the default value of `maxThreads`, and tune for larger values if needed. + +The module properties to configure the thread pool are: + +---- +include::{jetty-home}/modules/threadpool.mod[tags=documentation] +---- + +Among the configurable properties, the most relevant are: + +`jetty.threadPool.namePrefix`:: +The name prefix to use for the thread names. + +`jetty.threadPool.detailedDump`:: +Whether the thread pool should dump the whole stack trace of each thread, or just the topmost stack frame -- defaults to `false`. + +`jetty.threadPool.idleTimeout`:: +The time, in milliseconds, after which an idle thread is released from the pool -- defaults to 60000, i.e. 60 seconds. + +`jetty.threadPool.maxThreads`:: +The max number of threads pooled by the thread pool -- defaults to 200. + +If you want to use virtual threads, introduced as a preview feature in Java 19 and Java 20, and become an official feature since Java 21, use the following modules: + +* The <> Jetty module for Java 21 or later. +* The <> Jetty module for Java 19 and Java 20. + +See also the xref:server/index.adoc#threadpool[section about configuring the thread pool]. + +[[threadpool-virtual]] +== Module `threadpool-virtual` + +The `threadpool-virtual` module allows you to configure the server-wide thread pool, similarly to what you can do with the <> Jetty module, but also specify to use virtual threads, introduced as an official feature since Java 21. + +CAUTION: Only use this module if you are using Java 21 or later. +If you are using Java 19 or Java 20, use the <> Jetty module instead. + +Refer to the <> Jetty module for the general features provided by that Jetty module that also this Jetty module provides. + +The module properties to configure the thread pool are: + +---- +include::{jetty-home}/modules/threadpool-virtual.mod[tags=documentation] +---- + +The specific properties to configure virtual threads are: + +`jetty.threadPool.virtual.namePrefix`:: +The name prefix to use for the virtual thread names. + +`jetty.threadPool.virtual.inheritInheritableThreadLocals`:: +Whether virtual threads inherit the values of `InheritableThreadLocal` variables. + +[[threadpool-virtual-preview]] +== Module `threadpool-virtual-preview` + +The `threadpool-virtual-preview` module allows you to configure the server-wide thread pool, similarly to what you can do with the <> Jetty module, but also specify to use virtual threads, introduced as a preview feature in Java 19 and in Java 20. + +CAUTION: Only use this module if you are using Java 19 or Java 20. +If you are using Java 21 or later, use the <> Jetty module instead. + +NOTE: To enable preview features, this module needs to specify the `+--enable-preview+` command line option using the xref:modules/index.adoc#directive-exec[[exec\] directive], and as such it will fork another JVM. + +Refer to the <> Jetty module for the general features provided by that Jetty module that also this Jetty module provides. + +The module properties to configure the thread pool are: + +---- +include::{jetty-home}/modules/threadpool-virtual-preview.mod[tags=documentation] +---- + +The specific properties to configure virtual threads are: + +`jetty.threadPool.virtual.namePrefix`:: +The name prefix to use for the virtual thread names. + +`jetty.threadPool.virtual.allowSetThreadLocals`:: +Whether virtual threads are allowed to set thread locals. + +`jetty.threadPool.virtual.inheritInheritableThreadLocals`:: +Whether virtual threads inherit the values of `InheritableThreadLocal` variables. + +[[well-known]] +== Module `well-known` + +The `well-known` Jetty module creates a `ResourceHandler` deployed at the `/.well-known` context path which serves files from a directory. +By default, the directory created at `$JETTY_BASE/.well-known` is used, but it can be configured from `well-known.ini` to anywhere in the filesystem. +Note that the `.well-known` directory may be seen as a hidden directory by the filesystem. + +The concept of well-known URIs has been defined in https://datatracker.ietf.org/doc/html/rfc5785[RFC5785]. +This module can be used for things like the automatic renewal of https://letsencrypt.org/[Let's Encrypt] certificates. +See https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml[IANA Well-Known URIs] for more possible examples of how this can be used. + +The module properties are: + +---- +include::{jetty-home}/modules/well-known.mod[tags=documentation] +---- diff --git a/documentation/jetty/modules/operations-guide/pages/protocols/index.adoc b/documentation/jetty/modules/operations-guide/pages/protocols/index.adoc new file mode 100644 index 000000000000..1b27e9875c01 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/protocols/index.adoc @@ -0,0 +1,1230 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Connectors and Protocols + +Connectors are the network components through which Jetty accepts incoming network connections. + +Each connector listens on a network port and can be configured with `ConnectionFactory` components that _understand_ one or more network protocols. + +Understanding a protocol means that the connector is able to interpret incoming network bytes (for example, the bytes that represent an HTTP/1.1 request) and convert them into more abstract objects (for example an `HttpServletRequest` object) that are then processed by applications. +Conversely, an abstract object (for example an `HttpServletResponse`) is converted into the correspondent outgoing network bytes (the bytes that represent an HTTP/1.1 response). + +Like other Jetty components, connectors are enabled and configured by enabling and configuring the correspondent Jetty module. + +IMPORTANT: Recall that you must always issue the commands to enable Jetty modules from within the `$JETTY_BASE` directory, and that the Jetty module configuration files are in the `$JETTY_BASE/start.d/` directory. + +You can obtain the list of connector-related modules in this way: + +---- +$ java -jar $JETTY_HOME/start.jar --list-modules=connector +---- + +[[http]] +== Clear-Text HTTP/1.1 + +Clear text HTTP/1.1 is enabled with the `http` Jetty module with the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +.... + +After having enabled the `http` module, the `$JETTY_BASE` directory looks like this: + +[source,subs=+quotes] +---- +JETTY_BASE +├── resources +│ └── jetty-logging.properties +└── start.d + └── #http.ini# +---- + +The `http.ini` file is the file that you want to edit to configure network and protocol parameters -- for more details see xref:modules/standard.adoc#http[this section]. + +Note that the `http` Jetty module depends on the `server` Jetty module. + +Some parameters that you may want to configure are in fact common HTTP parameters that are applied not only for clear-text HTTP/1.1, but also for secure HTTP/1.1 or for clear-text HTTP/2 or for encrypted HTTP/2, or for HTTP/3, and these configuration parameters may be present in the `server` module configuration file. + +You can force the creation of the `server.ini` file via: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=server +---- + +Now the `$JETTY_BASE` directory looks like this: + +[source] +---- +JETTY_BASE +├── resources +│ └── jetty-logging.properties +└── start.d + ├── http.ini + └── server.ini +---- + +Now you can edit the `server.ini` file -- for more details see xref:modules/standard.adoc#server[this section]. + +[[https]] +== Secure HTTP/1.1 + +Secure HTTP/1.1 is enabled with both the `ssl` and `https` Jetty modules with the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=ssl,https +---- + +[jetty%nowrap] +.... +[jetty] +args=--add-modules=ssl,https +.... + +The command above enables the `ssl` module, that provides the secure network connector, the KeyStore configuration and TLS configuration -- for more details see <>. +Then, the xref:modules/standard.adoc#https[`https` module] adds HTTP/1.1 as the protocol secured by TLS. + +The `$JETTY_BASE` directory looks like this: + +[source] +---- +$JETTY_BASE +├── resources +│ └── jetty-logging.properties +└── start.d + ├── https.ini + └── ssl.ini +---- + +Note that the KeyStore file is missing, because you have to provide one with the cryptographic material you want (read xref:keystore/index.adoc[this section] to create your own KeyStore). +You need to configure these two properties by editing `ssl.ini`: + +* `jetty.sslContext.keyStorePath` +* `jetty.sslContext.keyStorePassword` + +As a quick example, you can enable the xref:modules/standard.adoc#test-keystore[`test-keystore` module], that creates on-the-fly a KeyStore containing a self-signed certificate: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=test-keystore +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=ssl,https +args=--add-modules=test-keystore +.... + +The `$JETTY_BASE` directory is now: + +[source,subs=+quotes] +---- +├── etc +│ └── #test-keystore.p12# +├── resources +│ └── jetty-logging.properties +└── start.d + ├── https.ini + ├── ssl.ini + └── test-keystore.ini +---- + +Starting Jetty yields: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=ssl,https,test-keystore +highlight=(\{.*:8443}) +.... + +Note how Jetty is listening on port `8443` for the secure HTTP/1.1 protocol. + +[WARNING] +==== +If you point your browser at `+https://localhost:8443/+` you will get a warning from the browser about a "potential security risk ahead", or that "your connection is not private", or similar message depending on the browser. + +This is normal because the certificate contained in `test-keystore.p12` is self-signed -- and as such not signed by a recognized certificate authority -- and therefore browsers do not trust it. +==== + +[[http2]] +== Configuring HTTP/2 + +https://tools.ietf.org/html/rfc7540[HTTP/2] is the successor of the HTTP/1.1 protocol, but it is quite different from HTTP/1.1: where HTTP/1.1 is a duplex, text-based protocol, HTTP/2 is a multiplex, binary protocol. + +Because of these fundamental differences, a client and a server need to _negotiate_ what version of the HTTP protocol they speak, based on what versions each side supports. + +To ensure maximum compatibility, and reduce the possibility that an intermediary that only understands HTTP/1.1 will close the connection when receiving unrecognized HTTP/2 bytes, HTTP/2 is typically deployed over secure connections, using the TLS protocol to wrap HTTP/2. + +IMPORTANT: Browsers only support secure HTTP/2. + +The protocol negotiation is performed by the https://tools.ietf.org/html/rfc7301[ALPN TLS extension]: the client advertises the list of protocols it can speak, and the server communicates to the client the protocol chosen by the server. + +For example, you can have a client that only supports HTTP/1.1 and a server that supports both HTTP/1.1 and HTTP/2: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant "client\nsupports\nhttp/1.1" as client +participant "server\nsupports\nhttp/1.1 & http/2" as server + +group TLS handshake +client -> server : ClientHello (alpn=[http/1.1]) +server -> server : picks http/1.1 +server -> client : ServerHello (alpn=http/1.1) +...rest of TLS handshake... +end +group TLS HTTP/1.1 +client -> server : HTTP/1.1 GET +server -> client : HTTP/1.1 200 +end +---- + +Nowadays, it's common that both clients and servers support HTTP/2, so servers prefer HTTP/2 as the protocol to speak: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant "client\nsupports\nhttp/1.1 & http/2" as client +participant "server\nsupports\nhttp/1.1 & http/2" as server + +group TLS handshake +client -> server : ClientHello (alpn=[http/1.1,h2]) +server -> server : picks http/2 +server -> client : ServerHello (alpn=h2) +...rest of TLS handshake... +end +group TLS HTTP/2 +client -> server : HTTP/2 GET +server -> client : HTTP/2 200 +end +---- + +When you configure a connector with the HTTP/2 protocol, you typically want to also configure the HTTP/1.1 protocol. +The reason to configure both protocols is that you typically do not control the clients: for example an old browser that does not support HTTP/2, or a monitoring console that performs requests using HTTP/1.1, or a heartbeat service that performs a single HTTP/1.0 request to verify that the server is alive. + +== Secure vs Clear-Text HTTP/2 + +Deciding whether you want to configure Jetty with <> or <> depends on your use case. + +You want to configure secure HTTP/2 when Jetty is exposed directly to browsers, because browsers only support secure HTTP/2. + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam roundCorner 10 + +rectangle browser +cloud internet +rectangle jetty + +jetty <--> internet : TLS+HTTP/2 +internet <--> browser : TLS+HTTP/2 +---- + +You may configure clear-text HTTP/2 (mostly for performance reasons) if you offload TLS at a load balancer (for example, https://haproxy.org/[HAProxy]) or at a reverse proxy (for example, https://nginx.org/[nginx]). + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam roundCorner 10 + +rectangle browser +cloud internet +rectangle haproxy +rectangle jetty + +note right of haproxy: TLS offload + +jetty <--> haproxy : HTTP/2 (clear-text) +haproxy <--> internet : TLS+HTTP/2 +internet <--> browser : TLS+HTTP/2 +---- + +You may configure clear-text HTTP/2 (mostly for performance reasons) to call microservices deployed to different Jetty servers (although you may want to use secure HTTP/2 for confidentiality reasons). + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam roundCorner 10 + +rectangle browser +cloud internet +rectangle haproxy +rectangle jetty +rectangle microservice1 +rectangle microservice2 +rectangle microservice3 + +note right of haproxy: TLS offload + +internet <--> browser : TLS+HTTP/2 +haproxy <--> internet : TLS+HTTP/2 +jetty <--> haproxy : HTTP/2 (clear-text) +microservice1 <--> jetty : HTTP/2 +microservice2 <--> jetty : HTTP/2 +microservice3 <--> jetty : HTTP/2 +microservice2 <--> microservice3 : HTTP/2 +microservice1 <--> microservice3 : HTTP/2 +---- + +[[http2s]] +== Secure HTTP/2 + +When you enable secure HTTP/2 you typically want to enable also secure HTTP/1.1, for backwards compatibility reasons: in this way, old browsers or other clients that do not support HTTP/2 will be able to connect to your server. + +You need to enable: + +* the `ssl` Jetty module, which provides the secure connector and the KeyStore and TLS configuration +* the `http2` Jetty module, which adds ALPN handling and adds the HTTP/2 protocol to the secured connector +* optionally, the `https` Jetty module, which adds the HTTP/1.1 protocol to the secured connector + +Use the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=ssl,http2,https +---- + +As when enabling the `https` Jetty module, you need a valid KeyStore (read xref:keystore/index.adoc[this section] to create your own KeyStore). + +As a quick example, you can enable the xref:modules/standard.adoc#test-keystore[`test-keystore` module], that creates on-the-fly a KeyStore containing a self-signed certificate: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=test-keystore +---- + +Starting Jetty yields: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=ssl,http2,https,test-keystore +highlight=(\{.*:8443}) +.... + +Note how Jetty is listening on port `8443` and the protocols supported are the sequence `(ssl, alpn, h2, http/1.1)`. + +The (ordered) list of protocols after `alpn` are the _application protocols_, in the example above `(h2, http/1.1)`. + +When a new connection is accepted by the connector, Jetty first interprets the TLS bytes, then it handles the ALPN negotiation knowing that the application protocols are (in order) `h2` and then `http/1.1`. + +You can customize the list of application protocols and the default protocol to use in case the ALPN negotiation fails by editing the xref:modules/standard.adoc#alpn[`alpn` module] properties. + +The HTTP/2 protocol parameters can be configured by editing the xref:modules/standard.adoc#http2[`http2` module] properties. + +[[http2c]] +== Clear-Text HTTP/2 + +When you enable clear-text HTTP/2 you typically want to enable also clear-text HTTP/1.1, for backwards compatibility reasons and to allow clients to https://tools.ietf.org/html/rfc7540#section-3.2[upgrade] from HTTP/1.1 to HTTP/2. + +You need to enable: + +* the `http` Jetty module, which provides the clear-text connector and adds the HTTP/1.1 protocol to the clear-text connector +* the `http2c` Jetty module, which adds the HTTP/2 protocol to the clear-text connector + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http,http2c +---- + +Starting Jetty yields: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http,http2c +highlight=(\{.+:8080}) +.... + +Note how Jetty is listening on port `8080` and the protocols supported are HTTP/1.1 and `h2c` (i.e. clear-text HTTP/2). + +With this configuration, browsers and client applications will be able to connect to port `8080` using: + +* HTTP/1.1 directly (e.g. `curl --http1.1 ++http://localhost:8080++`): +---- +GET / HTTP/1.1 +Host: localhost:8080 +---- +* HTTP/1.1 with upgrade to HTTP/2 (e.g. `curl --http2 ++http://localhost:8080++`): +---- +GET / HTTP/1.1 +Host: localhost:8080 +Connection: Upgrade, HTTP2-Settings +Upgrade: h2c +HTTP2-Settings: +---- +* HTTP/2 directly (e.g. `curl --http2-prior-knowledge ++http://localhost:8080++`): +---- +50 52 49 20 2a 20 48 54 54 50 2f 32 2e 30 0d 0a +0d 0a 53 4d 0d 0a 0d 0a 00 00 12 04 00 00 00 00 +00 00 03 00 00 00 64 00 04 40 00 00 00 00 02 00 +00 00 00 00 00 1e 01 05 00 00 00 01 82 84 86 41 +8a a0 e4 1d 13 9d 09 b8 f0 1e 07 7a 88 25 b6 50 +c3 ab b8 f2 e0 53 03 2a 2f 2a +---- + +The HTTP/2 protocol parameters can be configured by editing the xref:modules/standard.adoc#http2c[`http2c` module] properties. + +[[http3]] +== HTTP/3 + +When you enable support for the HTTP/3 protocol, by default the secure HTTP/2 protocol is also enabled, so that browsers or clients that do not support HTTP/3 will be able to connect to your server. + +You need to enable: + +* the `ssl` Jetty module, which provides the KeyStore and TLS configuration +* the `http3` Jetty module, which adds the HTTP/3 protocol on the HTTP/3 connector + +Use the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=ssl,http3 +---- + +Enabling any module Jetty module that supports secure network communication requires a valid KeyStore (read xref:keystore/index.adoc[this section] to create your own KeyStore), that, as a quick example, you can enable with the xref:modules/standard.adoc#test-keystore[`test-keystore` module], that creates on-the-fly a KeyStore containing a self-signed certificate: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=test-keystore +---- + +Starting Jetty yields: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--approve-all-licenses --add-modules=ssl,http3,test-keystore +highlight=(\{.*:8444}) +.... + +Note how Jetty is listening on port `8443` for HTTP/2 and on port `8444` for HTTP/3. + +The HTTP/3 protocol parameters can be configured by editing the xref:modules/standard.adoc#http3[`http3` module] properties. + +[[websocket]] +== WebSocket + +WebSocket is a network protocol for bidirectional data communication initiated via the https://tools.ietf.org/html/rfc7230#section-6.7[HTTP/1.1 upgrade mechanism]. +WebSocket provides a simple, low-level, framing protocol layered over TCP. +One or more WebSocket frames compose a WebSocket _message_ that is either a UTF-8 _text_ message or _binary_ message. + +Jetty provides an implementation of the following standards and specifications. + +http://tools.ietf.org/html/rfc6455[RFC-6455] - The WebSocket Protocol:: +Jetty supports version 13 of the released and final specification. + +http://www.jcp.org/en/jsr/detail?id=356[JSR-356] - The Java WebSocket API (`javax.websocket`):: +This is the official Java API for working with WebSockets. + +https://tools.ietf.org/html/rfc7692[RFC-7692] - WebSocket Per-Message Deflate Extension:: +This is the replacement for perframe-compression, switching the compression to being based on the entire message, not the individual frames. + +https://tools.ietf.org/html/rfc8441[RFC-8441] - Bootstrapping WebSockets with HTTP/2:: +Allows a single stream of an HTTP/2 connection to be upgraded to WebSocket. +This allows one TCP connection to be shared by both protocols and extends HTTP/2's more efficient use of the network to WebSockets. + +[[websocket-configure]] +=== Configuring WebSocket + +Jetty provides two WebSocket implementations: one based on the Java WebSocket APIs defined by JSR 356, provided by module `websocket-javax`, and one based on Jetty specific WebSocket APIs, provided by module `websocket-jetty`. +The Jetty `websocket` module enables both implementations, but each implementation can be enabled independently. + +NOTE: Remember that a WebSocket connection is always initiated from the HTTP protocol (either an HTTP/1.1 upgrade or an HTTP/2 connect), therefore to enable WebSocket you need to enable HTTP. + +To enable WebSocket support, you also need to decide what version of the HTTP protocol you want WebSocket to be initiated from, and whether you want secure HTTP. + +For example, to enable clear-text WebSocket from HTTP/1.1, use the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http,websocket +---- + +To enable secure WebSocket from HTTP/2, use the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http2,websocket +---- + +When enabling secure protocols you need a valid KeyStore (read xref:keystore/index.adoc[this section] to create your own KeyStore). +As a quick example, you can enable the xref:modules/standard.adoc#test-keystore[`test-keystore` module], that creates on-the-fly a KeyStore containing a self-signed certificate: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=test-keystore +---- + +To enable WebSocket on both HTTP/1.1 and HTTP/2, both clear-text and secure, use the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http,https,http2c,http2,websocket +---- + +[[websocket-disable]] +=== Selectively Disabling WebSocket + +Enabling the WebSocket Jetty modules comes with a startup cost because Jetty must perform two steps: + +. Scan web applications `+*.war+` files (and all the jars and classes inside it) looking for WebSocket EndPoints classes (either annotated with WebSocket API annotations or extending/implementing WebSocket API classes/interfaces). +This can be a significant cost if your web application contains a lot of classes and/or jar files. + +. Configure and wire WebSocket EndPoints so that WebSocket messages are delivered to the correspondent WebSocket EndPoint. + +WebSocket support is by default enabled for all web applications. + +For a specific web application, you can disable step 2 for Java WebSocket support (i.e. when the `websocket-javax` module is enabled) by setting the context attribute `org.eclipse.jetty.websocket.javax` to `false`: + +[,xml] +---- + + + + + + org.eclipse.jetty.websocket.javax + false + + + ... + + +---- + +Furthermore, for a specific web application, you can disable step 1 (and therefore also step 2) as described in the xref:annotations/index.adoc[annotations processing section]. + +[[websocket-webapp-client]] +=== Using WebSocket Client in WebApps + +Web applications may need to use a WebSocket client to communicate with third party WebSocket services. + +If the web application uses the Java WebSocket APIs, the WebSocket client APIs are provided by the Servlet Container and are available to the web application by enabling the WebSocket server APIs, and therefore you must enable the `websocket-javax` Jetty module. + +However, the Java WebSocket Client APIs are quite limited (for example, they do not support secure WebSocket). +For this reason, web applications may want to use the Jetty WebSocket Client APIs. + +When using the Jetty WebSocket Client APIs, web applications should include the required jars and their dependencies in the `WEB-INF/lib` directory of the `+*.war+` file. +Alternatively, when deploying your web applications in Jetty, you can enable the `websocket-jetty-client` Jetty module to allow web applications to use the Jetty WebSocket Client APIs provided by Jetty, without the need to include jars and their dependencies in the `+*.war+` file. + +[[fcgi]] +== FastCGI + +FastCGI is a network protocol primarily used by a _web server_ to communicate to a __FastCGI server__. + +FastCGI servers are typically used to serve web content generated by dynamic web languages, primarily http://www.php.net/[PHP], but also Python, Ruby, Perl and others. + +Web servers that supports FastCGI are, among others, http://httpd.apache.org/[Apache], http://nginx.org/[Nginx], and Jetty. +Web servers typically act as reverse proxies, converting HTTP requests that they receive from clients (browsers) to FastCGI requests that are forwarded to the FastCGI server. +The FastCGI server spawns the dynamic web language interpreter, passing it the information contained in the FastCGI request and a dynamic web language script is executed, producing web content, typically HTML. +The web content is then formatted into a FastCGI response that is returned to the web server, which converts it to an HTTP response that is then returned to the client. + +The most well known FastCGI server is the http://php-fpm.org/[PHP FastCGI Process Manager], or `php-fpm`. +In the following we will assume that `php-fpm` is used as FastCGI server. + +This is a diagram of what described above: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +Browser -> Jetty : GET /path (HTTP) +Jetty -> "php-fpm" : GET /path (FastCGI) +"php-fpm" -> PHP : spawn PHP interpreter +PHP -> "index.php" : execute PHP script +"index.php" -> "php-fpm" : HTML +"php-fpm" -> Jetty : 200 OK + HTML (FastCGI) +Jetty -> Browser : 200 OK + HTML (HTTP) + +---- + +Jetty can be configured to act as a web server that supports FastCGI, replacing the functionality that is normally provided by Apache or Nginx. +This allows users to leverage Jetty features such as the support for HTTP/1.1, HTTP/2 and HTTP/3, Jetty's scalability, and of course Jetty's native support for Java Web Standards such as Servlets, JSPs, etc. + +With such configuration, users can not only deploy their Java Web Applications in Jetty, but also serve their http://wordpress.com/[WordPress] site or blog or their https://drupal.org/[Drupal] site without having to install and manage multiple web servers. + +[[fcgi-configure]] +=== Configuring WordPress + +This section explains how to configure Jetty to serve your https://wordpress.com/[WordPress] site. + +The prerequisites are: + +* Have `php-fpm` installed on your server host, and configured to listen either on a Unix-Domain socket (such as `/run/php/php-fpm.sock`), or on a TCP socket (such as `localhost:9000`). +* Have WordPress installed on your server host, for example under `/var/www/wordpress`. +For more information about how to install WordPress and the necessary PHP modules, please refer to the https://wordpress.org/support/article/how-to-install-wordpress/[WordPress Installation Guide]. + +Then, the <> and/or the <> Jetty modules should be enabled to allow browsers to connect to Jetty. + +Lastly, enable the `fcgi-proxy` module to provide FastCGI support (to convert HTTP requests from browsers to FastCGI for `php-fpm` and vice versa), and the `core-deploy` module to deploy your WordPress web application as a xref:deploy/index.adoc#jetty[Jetty context XML file]. + +For example: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=ssl,https,fcgi-proxy,core-deploy +---- + +TIP: The `https` Jetty module requires a KeyStore. If you do not already have one configured, you can add the `test-keystore` Jetty module to the command line above to create a KeyStore on-the-fly. + +Now you can deploy a Jetty context XML file that represents your WordPress web application. + +Use the following file as example, copy it as `$JETTY_BASE/webapps/wordpress.xml` and customize it as necessary: + +[,xml,options=nowrap] +---- + + + + + + /var/www/wordpress + + + / + + + + + wordpress.originalPath + wordpress.originalQuery + + + $path + /index.php + + + + + + + + *.php + + + + (https?)://([^/]+)(.*) + http://localhost:9000$3 + + + wordpress.originalPath + wordpress.originalQuery + (.+?\\.php) + true + /run/php/php-fpm.sock + + + + + + / + + + + /index.php + REHANDLE + false + + + + + + + + + +---- +<1> Specify the WordPress installation path. +<2> Specify the context path of your web application. +<3> The `FastCGIProxyHandler` forwards requests whose URI path matches `+*.php+` to `php-fpm`. +<4> The client URI regex pattern to match. +<5> The URI used to forward the request to `php-fpm`, where `+$3+` is the 3rd matching group of the client URI regex pattern (int this example, the client URI path). +If `php-fpm` is configured to listen on a TCP socket, the host and port must match the listening TCP socket. +If `php-fpm` is configured to listen on a Unix-Domain socket, the host and port values are ignored but must be present. +<6> If `php-fpm` is configured to listen on a Unix-Domain socket, specify the Unix-Domain socket path, otherwise omit this line. +<7> The `ResourceHandler` serves static files from WordPress, such as `+*.css+`, `+*.js+` and image files. + +Now you can start Jetty and navigate to `+http://localhost:8080+` with your browser to enjoy WordPress: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[[ssl]] +== Configuring Secure Protocols + +Secure protocols are normal protocols such as HTTP/1.1, HTTP/2 or WebSocket that are wrapped by the https://en.wikipedia.org/wiki/Transport_Layer_Security[TLS protocol]. +Any network protocol based on TCP can be wrapped with TLS. + +QUIC, the protocol based on UDP that transports HTTP/3, uses TLS messages but not the TLS protocol framing. + +The `https` scheme used in URIs really means `tls+http/1.1` (or `tls+http/2`, or `quic+http/3`) and similarly the `wss` scheme used in URIs really means `tls+websocket`, etc. +Senders wrap the underlying protocol bytes (e.g. HTTP bytes or WebSocket bytes) with the TLS protocol, while receivers first interpret the TLS protocol to obtain the underlying protocol bytes, and then interpret the wrapped bytes. + +The xref:modules/standard.adoc#ssl[`ssl` Jetty module] allows you to configure a secure network connector; if other modules require encryption, they declare a dependency on the `ssl` module. + +It is the job of other Jetty modules to configure the wrapped protocol. +For example, it is the <> that configures the wrapped protocol to be HTTP/1.1. +Similarly, it is the <> that configures the wrapped protocol to be HTTP/2. +If you enable _both_ the `https` and the `http2` module, you will have a single secure connector that will be able to interpret both HTTP/1.1 and HTTP/2. + +TIP: Recall from the xref:modules/index.adoc[section about modules], that only modules that are explicitly enabled get their module configuration file (`+*.ini+`) saved in `$JETTY_BASE/start.d/`, and you want `$JETTY_BASE/start.d/ssl.ini` to be present so that you can configure the connector properties, the KeyStore properties and the TLS properties. + +[[ssl-customize]] +=== Customizing KeyStore and SSL/TLS Configuration + +Secure protocols have a slightly more complicated configuration since they require to configure a _KeyStore_. +Refer to the xref:keystore/index.adoc[KeyStore section] for more information about how to create and manage a KeyStore. + +For simple cases, you only need to configure the KeyStore path and KeyStore password as explained in xref:modules/standard.adoc#ssl-keystore-tls[this section]. + +For more advanced configuration you may want to configure the TLS protocol versions, or the ciphers to include/exclude, etc. +The correct way of doing this is to create a custom xref:xml/index.adoc[Jetty XML file] and reference it in `$JETTY_BASE/start.d/ssl.ini`: + +.ssl.ini +[source] +---- +jetty.sslContext.keyStorePassword=my_passwd! <1> +etc/tls-config.xml <2> +---- +<1> Configures the `jetty.sslContext.keyStorePassword` property with the KeyStore password. +<2> References your newly created `$JETTY_BASE/etc/tls-config.xml`. + +The `ssl.ini` file above only shows the lines that are not commented out (you can leave the lines that are commented unmodified for future reference). + +You want to create the `$JETTY_BASE/etc/tls-config.xml` with the following template content: + +.tls-config.xml +[,xml] +---- + + + + + + ... <1> + + +---- +<1> Here goes your advanced configuration. + +The `tls-config.xml` file references the `sslContextFactory` component (created by the `ssl` Jetty module) that configures the KeyStore and TLS parameters, so that you can now call its APIs via XML, and you will have full flexibility for any advanced configuration you want (see below for few examples). + +Refer to the link:{javadoc-url}/org/eclipse/jetty/util/ssl/SslContextFactory.Server.html[SslContextFactory.Server javadocs] for the list of methods that you can call through the Jetty XML file. + +CAUTION: Use module properties whenever possible, and only resort to use a Jetty XML file for advanced configuration that you cannot do using module properties. + +[[ssl-customize-versions]] +==== Customizing SSL/TLS Protocol Versions + +By default, the SSL protocols (SSL, SSLv2, SSLv3, etc.) are already excluded because they are vulnerable. +To explicitly add the exclusion of TLSv1.0 and TLSv1.1 (that are also vulnerable -- which leaves only TLSv1.2 and TLSv1.3 available), you want to use this XML: + +.tls-config.xml +[,xml] +---- + + + + + + + + + TLSv1.0 + TLSv1.1 + + + + + +---- + +[[ssl-customize-ciphers]] +==== Customizing SSL/TLS Ciphers + +You can precisely set the list of excluded ciphers, completely overriding Jetty's default, with this XML: + +.tls-config.xml +[,xml] +---- + + + + + + + + ^TLS_RSA_.*$ + ^.*_RSA_.*_(MD5|SHA|SHA1)$ + ^.*_DHE_RSA_.*$ + 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 + + + + +---- + +Note how each array item specifies a _regular expression_ that matches multiple ciphers, or specifies a precise cipher to exclude. + +You can choose to create multiple XML files, and reference them all from `$JETTY_BASE/start.d/ssl.ini`, or put all your custom configurations in a single XML file. + +[[ssl-renew]] +=== Renewing the Certificates + +When you create a certificate, you must specify for how many days it is valid. + +The typical validity is 90 days, and while this period may seem short, it has two advantages: + +* Reduces the risk in case of compromised/stolen keys. +* Encourages automation, i.e. certificate renewal performed by automated tools (rather than manually) at scheduled times. + +To renew a certificate, you must go through the xref:keystore/index.adoc#create[same steps] you followed to create the certificate the first time, and then you can <> without the need to stop Jetty. + +[[ssl-reload]] +=== Watching and Reloading the KeyStore + +Jetty can be configured to monitor the directory of the KeyStore file, and reload the `SslContextFactory` component if the KeyStore file changed. + +This feature can be enabled by activating the `ssl-reload` Jetty module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=ssl-reload +---- + +For more information about the configuration of the `ssl-reload` Jetty module, see xref:modules/standard.adoc#ssl-reload[this section]. + +[[ssl-conscrypt]] +=== Using Conscrypt as SSL/TLS Provider + +If not explicitly configured, the TLS implementation is provided by the JDK you are using at runtime. + +OpenJDK's vendors may replace the default TLS provider with their own, but you can also explicitly configure an alternative TLS provider. + +The standard TLS provider from OpenJDK is implemented in Java (no native code), and its performance is not optimal, both in CPU usage and memory usage. + +A faster alternative, implemented natively, is Google's https://github.com/google/conscrypt/[Conscrypt], which is built on https://boringssl.googlesource.com/boringssl/[BoringSSL], which is Google's fork of https://www.openssl.org/[OpenSSL]. + +CAUTION: As Conscrypt eventually binds to a native library, there is a higher risk that a bug in Conscrypt or in the native library causes a JVM crash, while the Java implementation will not cause a JVM crash. + +To use Conscrypt as the TLS provider just enable the `conscrypt` Jetty module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=conscrypt +---- + +[[ssl-sni]] +=== Configuring SNI + +Server Name Indication (SNI) is a TLS extension that clients send to indicate what domain they want to connect to during the initial TLS handshake. + +Modern TLS clients (e.g. browsers) always send the SNI extension; however, older TLS clients may not send the SNI extension. + +Being able to handle the SNI is important when you have xref:deploy/index.adoc#virtual-hosts[virtual hosts] and a KeyStore with multiple certificates, one for each domain. + +For example, you may have deployed over a secure connector two web applications, both at context path `/`, one at virtual host `one.com` and one at virtual host `two.net`. +The KeyStore contains two certificates, one for `one.com` and one for `two.net`. + +There are three `ssl` module properties that control the SNI behavior on the server: one that works at the TLS level, and two that works at the HTTP level. + +The property that works at the TLS level is: + +`jetty.sslContext.sniRequired`:: +Whether SNI is required at the TLS level, defaults to `false`. +Its behavior is explained by the following table: ++ +.Behavior of the `jetty.sslContext.sniRequired` property +[cols="3*a"] +|=== +| +| `sniRequired=false` +| `sniRequired=true` + +| SNI = `null` +| client receives default certificate +| client receives TLS failure + +| SNI = `wrong.org` +| client receives default certificate +| client receives TLS failure + +| SNI = `one.com` +| client receives `one.com` certificate +| client receives `one.com` certificate +|=== ++ +[WARNING] +==== +The _default certificate_ is the certificate returned by the TLS implementation in case there is no SNI match, and you should not rely on this certificate to be the same across Java vendors and versions, or Jetty versions, or TLS provider vendors and versions. + +In the example above it could be either the `one.com` certificate or the `two.net` certificate. +==== + +When `jetty.sslContext.sniRequired=true`, clients that don't send a valid SNI receive a TLS failure, and their attempt to connect to the server fails. +The details of this failure may not be reported and could be difficult to figure out that the failure is related to an invalid SNI. + +For this reason, other two properties are defined at the HTTP level, so that clients can received an HTTP 400 response with more details about what went wrong while trying to connect to the server: + +`jetty.ssl.sniRequired`:: +Whether SNI is required at the HTTP level, defaults to `false`. +Its behavior is similar to the `jetty.sslContext.sniRequired` property above, and is explained by the following table: ++ +.Behavior of the `jetty.ssl.sniRequired` property +[cols=3*a] +|=== +| +| `sniRequired=false` +| `sniRequired=true` + +| SNI = `null` +| Accept +| Reject: 400 Bad Request + +| SNI = `wrong.org` +| Accept +| Reject: 400 Bad Request + +| SNI = `one.com` +| Accept +| Accept +|=== + +When `jetty.ssl.sniRequired=true`, the SNI is matched against the certificate sent to the client, and only if there is a match the request is accepted. + +When the request is accepted, there could be an additional check controlled by the following property: + +`jetty.ssl.sniHostCheck`:: +Whether the certificate sent to the client matches the `Host` header, defaults to `true`. +Its behavior is explained by the following table: ++ +.Behavior of the `jetty.ssl.sniHostCheck` property +[cols="3*a"] +|=== +| +| `sniHostCheck=false` +| `sniHostCheck=true` + +| certificate = `one.com` + +`Host: wrong.org` +| Accept +| Reject: 400 Bad Request + +| certificate = `one.com` + +`Host: one.com` +| Accept +| Accept +|=== + +In the normal case with the default server configuration, for a TLS clients that sends SNI, and then sends an HTTP request with the correct `Host` header, Jetty will pick the correct certificate from the KeyStore based on the SNI received from the client, and accept the request. + +Accepting the request does not mean that the request is responded with an HTTP 200 OK, but just that the request passed successfully the SNI checks and will be processed by the server. +If the request URI is for a resource that does not exist, the response will likely be a 404 Not Found. + +You may modify the default values of the SNI properties if you want stricter control over old/broken TLS clients or bad HTTP requests. + +[[proxy]] +== Jetty Behind a Load Balancer or Reverse Proxy + +You may need to configure one or more Jetty instances behind an _intermediary_, typically a load balancer such as https://haproxy.org[HAProxy], or a reverse proxy such as https://httpd.apache.org[Apache HTTP Server] or https://nginx.org[Nginx]. + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam padding 5 + +scale 1.5 + +rectangle client +rectangle proxy +rectangle "Jetty" as jetty1 +rectangle "Jetty" as jetty2 + +client -- proxy +proxy -- jetty1 +proxy -- jetty2 +---- + +[WARNING] +==== +HAProxy can communicate either HTTP/1.1 or HTTP/2 to backend servers such as Jetty. + +Apache HTTP Server and Nginx can only speak HTTP/1.1 to backend servers such as Jetty, and have no plans to support HTTP/2 towards backend servers. +==== + +In these setups, typically the proxy performs TLS offloading, and the communication with backend servers happens in clear-text. +It is possible, however, to configure the proxy so that all the bytes arriving from the client are tunnelled opaquely to the backend Jetty server (that therefore needs to perform the TLS offloading) and vice versa the bytes arriving from the Jetty server are tunnelled opaquely to the client. + +Also in these setups, the TCP/IP connection terminating on the Jetty servers does not originate from the client, but from the proxy, so that the remote IP address and port number may be reported incorrectly in backend server logs, or worse applications may not work because they need to be able to differentiate different clients based on the client IP address. + +For this reason, intermediaries typically implement at least one of several _de facto_ standards to communicate information about the original client connection to the backend Jetty server. + +Jetty supports two methods to process client information sent by intermediaries: + +* The `Forwarded` HTTP header, defined in https://tools.ietf.org/html/rfc7239[RFC 7239] and replacing the old `X-Forwarded-*` headers, defined in <>. +* The https://www.haproxy.org/download/2.2/doc/proxy-protocol.txt[Proxy Protocol], defined in <>. + +In both methods, web applications that call `HttpServletRequest.getRemoteAddr()` will receive the remote client IP address as specified by the client information sent by the intermediary, not the physical IP address of TCP connection with the intermediary. +Likewise, `HttpServletRequest.getRemotePort()` will return the remote client IP port as specified by the client information sent by the intermediary, and `HttpServletRequest.isSecure()` will return whether the client made a secure request using the `https` scheme as specified by the client information sent by the intermediary. + +[[proxy-forwarded]] +=== Configuring the Forwarded Header + +The `Forwarded` HTTP header is added by the intermediary with information about the client and the client request, for example: + +---- +GET / HTTP/1.1 +Host: domain.com +Forwarded: for=2.36.72.144:21216;proto=https +---- + +In the example above, the intermediary added the `Forwarded` header specifying that the client remote address is `2.36.72.144:21216` and that the request was made with the `https` scheme. + +Let's assume you have already configured Jetty with the HTTP/1.1 protocol with the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http +---- + +Support for the `Forwarded` HTTP header (and its predecessor `X-Forwarded-*` headers) is enabled with the `http-forwarded` Jetty module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http-forwarded +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +args=--add-modules=http-forwarded +.... + +With the `http-forwarded` Jetty module enabled, Jetty interprets the `Forwarded` header and makes its information available to web applications via the standard Servlet APIs. + +For further information about configuring the `http-forwarded` Jetty module, see xref:modules/standard.adoc#http-forwarded[this section]. + +[[proxy-protocol]] +=== Configuring the Proxy Protocol + +The https://www.haproxy.org/download/2.2/doc/proxy-protocol.txt[Proxy Protocol] is the _de facto_ standard, introduced by https://haproxy.org[HAProxy], to communicate client information to backend servers via the TCP connection, rather than via HTTP headers. + +The information about the client connection is sent as a small data frame on each newly established connection. +This mechanism is therefore independent of any protocol, so it can be used for TLS, HTTP/1.1, HTTP/2, etc. + +[NOTE] +==== +There are 2 versions of the proxy protocol: v1 and v2, both supported by Jetty. + +Proxy protocol v1 is human readable, but it only carries information about the client TCP connection (IP address and IP port). + +Proxy protocol v2 has a binary format, carries the information about the client TCP connection, and can carry additional arbitrary information encoded in pairs `(type, value)` where `type` is a single byte that indicates the value's meaning, and `value` is a variable length byte array that can encode user-defined data. +==== + +Support for the proxy protocol can be enabled for the clear-text connector or for the secure connector (or both). + +Let's assume you have already configured Jetty with the HTTP/1.1 clear-text protocol with the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http +---- + +To enable proxy protocol support for the clear-text connector, enable the `proxy-protocol` Jetty module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=proxy-protocol +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +args=--add-modules=proxy-protocol +.... + +Starting Jetty yields: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +args=--module=proxy-protocol +highlight=(\{.*:8080}) +.... + +Note how in the example above the list of protocols for the clear-text connector is first `proxy` and then `http/1.1`. +For every new TCP connection, Jetty first interprets the proxy protocol bytes with the client information; after this initial proxy protocol processing, Jetty interprets the incoming bytes as HTTP/1.1 bytes. + +Enabling proxy protocol support for the secure connector is similar. + +Let's assume you have already configured Jetty with the HTTP/1.1 secure protocol and the test KeyStore with the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=https,test-keystore +---- + +Enable the `proxy-protocol-ssl` Jetty module with the following command (issued from within the `$JETTY_BASE` directory): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=proxy-protocol-ssl +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=https +args=--add-modules=proxy-protocol-ssl +.... + +Starting Jetty yields: + +---- +$ java -jar $JETTY_HOME/start.jar +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=https,test-keystore,proxy-protocol-ssl +highlight=(\{.*:8443}) +.... + +Note how in the example above the list of protocols for the secure connector is first `proxy`, then `ssl` and then `http/1.1`. + +[[proxy-haproxy]] +=== HAProxy and Jetty with HTTP/1.1 and HTTP/2 + +https://haproxy.org[HAProxy] is an open source solution that offers load balancing and proxying for TCP and HTTP based application, and can be used as a replacement for Apache or Nginx when these are used as reverse proxies. + +The deployment proposed here has HAProxy playing the role that Apache and Nginx usually do: to perform the TLS offloading (that is, decrypt incoming bytes and encrypt outgoing bytes) and then forwarding the now clear-text traffic to a backend Jetty server, speaking either HTTP/1.1 or HTTP/2. +Since HAProxy's TLS offloading is based on OpenSSL, it is much more efficient than the Java implementation shipped with OpenJDK. + +After you have installed HAProxy on your system, you want to configure it so that it can perform TLS offloading. + +HAProxy will need a single file containing the X509 certificates and the private key, all in https://en.wikipedia.org/wiki/X.509[PEM format], with the following order: + +1. The site certificate; this certificate's Common Name refers to the site domain (for example: CN=*.webtide.com) and is signed by Certificate Authority #1. +2. The Certificate Authority #1 certificate; this certificate may be signed by Certificate Authority #2. +3. The Certificate Authority #2 certificate; this certificate may be signed by Certificate Authority #3; and so on until the Root Certificate Authority. +4. The Root Certificate Authority certificate. +5. The private key corresponding to the site certificate. + +Refer to the xref:keystore/index.adoc[section about KeyStores] for more information about generating the required certificates and private key. + +Now you can create the HAProxy configuration file (in Linux it's typically `/etc/haproxy/haproxy.cfg`). +This is a minimal configuration: + +.haproxy.cfg +[source] +---- +global +tune.ssl.default-dh-param 1024 + +defaults +timeout connect 10000ms +timeout client 60000ms +timeout server 60000ms + +frontend fe_http <1> +mode http +bind *:80 +# Redirect to https +redirect scheme https code 301 + +frontend fe_https <2> +mode tcp +bind *:443 ssl no-sslv3 crt /path/to/domain.pem ciphers TLSv1.2 alpn h2,http/1.1 +default_backend be_http + +backend be_http <3> +mode tcp +server domain 127.0.0.1:8282 send-proxy-v2 +---- +<1> The `fe_http` front-end accepts connections on port 80 and redirects them to use the `https` scheme. +<2> The `fe_https` front-end accepts connections on port 443, and it is where the TLS decryption/encryption happens. +You must specify the path to the PEM file containing the TLS key material (the `crt /path/to/domain.pem` part), the ciphers that are suitable for HTTP/2 (`ciphers TLSv1.2`), and the ALPN protocols supported (`alpn h2,http/1.1`). +This front-end then forwards the now decrypted bytes to the backend in `mode tcp`. +The `mode tcp` says that HAProxy will not try to interpret the bytes but instead opaquely forwards them to the backend. +<3> The `be_http` backend will forward (again in `mode tcp`) the clear-text bytes to a Jetty connector that talks clear-text HTTP/2 and HTTP/1.1 on port 8282. +The `send-proxy-v2` directive sends the proxy protocol v2 bytes to the backend server. + +On the Jetty side, you need to enable the following modules: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=proxy-protocol,http2c,http,deploy +---- + +You need to specify the host (`127.0.0.1`) and port (`8282`) you have configured in HAProxy when you start Jetty: + +---- +$ java -jar $JETTY_HOME/start.jar jetty.http.host=127.0.0.1 jetty.http.port=8282 +---- + +[NOTE] +==== +You want the Jetty connector that listens on port `8282` to be available only to HAProxy, and not to remote clients. + +For this reason, you want to specify the `jetty.http.host` property on the command line (or in `$JETTY_BASE/start.d/http.ini` to make this setting persistent) to bind the Jetty connector only on the loopback interface (`127.0.0.1`), making it available to HAProxy but not to remote clients. + +If your Jetty instance runs on a different machine and/or on a different (sub)network, you may want to adjust both the back-end section of the HAProxy configuration file and the `jetty.http.host` property to match accordingly. +==== + +With this configuration for HAProxy and Jetty, browsers supporting HTTP/2 will connect to HAProxy, which will decrypt the traffic and send it to Jetty. +Likewise, HTTP/1.1 clients will connect to HAProxy, which will decrypt the traffic and send it to Jetty. + +The Jetty connector, configured with the `http2c` and the `http` modules is able to distinguish whether the incoming bytes are HTTP/2 or HTTP/1.1 and will handle the request accordingly. + +The response is relayed back to HAProxy, which will encrypt it and send it back to the remote client. + +This configuration offers you efficient TLS offloading, HTTP/2 support and transparent fallback to HTTP/1.1 for clients that don't support HTTP/1.1. diff --git a/documentation/jetty/modules/operations-guide/pages/quickstart/index.adoc b/documentation/jetty/modules/operations-guide/pages/quickstart/index.adoc new file mode 100644 index 000000000000..997d2f2fff87 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/quickstart/index.adoc @@ -0,0 +1,67 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Faster Web Application Deployment + +The auto discovery features of the Servlet Specification can make deployments slow and uncertain. +Auto discovery of web application configuration can be useful during the development as it allows new features and frameworks to be enabled simply by dropping in a jar file. +However for production deployment, the need to scan the contents of many jars can have a significant impact at startup time. + +The `quickstart` module allows a webapp to be pre-scanned, making startup predictable and faster. +During scanning all declarative configuration (ie from web.xml, web-fragment.xml and annotations) are encoded into an effective `web.xml`, called `WEB-INF/quickstart-web.xml`, which can be inspected to understand what will be deployed. + +[NOTE] +==== +Programmatic configuration is _not_ encoded into the generated `quickstart-web.xml` file. +==== + +With `quickstart`, webapps that took many seconds to scan and deploy can now be deployed in a few hundred milliseconds. + +== Enabling + +Enable the `quickstart` module for your jetty base: + +---- +$ cd $JETTY-BASE +$ java -jar $JETTY_HOME/start.jar --add-modules=quickstart +---- + +The `$JETTY-BASE/start.d/quickstart.ini` file contains these configurable parameters: + +jetty.quickstart.mode:: + The values are: + + AUTO::: + Allows jetty to run either with or without a `quickstart-web.xml` file. + If jetty detects the file, then it will be used, otherwise the app is started normally. + GENERATE::: + In this mode, jetty will generate a `quickstart-web.xml` file and then terminate. + Use this mode first before changing to either `AUTO` or `QUICKSTART`. + QUICKSTART::: + In this mode, if jetty does not detect a `quickstart-web.xml` file then jetty will not start. + +jetty.quickstart.origin:: +Use this parameter to set the name of the attribute in the `quickstart-web.xml` file that contains the origin of each element. +Knowing the descriptor or annotation from which each element derives can be useful for debugging. +Note that the origin attribute does not conform to the web xml schema, so if you deploy with xml validation, you'll see errors. +It is probably best to do a few trial runs with the attribute set, then turn it off for final generation. + +jetty.quickstart.xml:: +Use this parameter to change the name of the generated file. +By default this is `quickstart-web.xml` in the webapp's `WEB-INF` directory. +The file named by this parameter will always be interpreted relative to `WEB-INF`. + +If your webapp is a war file, you will need to either first unpack it yourself, or use a context xml file (or code equivalent) that calls `WebAppContext.setExtractWAR(true)`. +If you allow Jetty to do the unpacking, it will use the usual mechanisms to find the location to which to unpack. +Note that by default Jetty unpacks to a temporary location which is _not_ reused between executions. +So either specify the directory to which to unpack, or make a `work` directory in your base to ensure the unpacked war is preserved and reused across restarts. diff --git a/documentation/jetty/modules/operations-guide/pages/server/index.adoc b/documentation/jetty/modules/operations-guide/pages/server/index.adoc new file mode 100644 index 000000000000..f147d11d4013 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/server/index.adoc @@ -0,0 +1,359 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Server + +The Jetty `Server` object is the central component that links protocol connectors to web applications. + +The `Server` component is defined by the xref:modules/standard.adoc#server[`server` Jetty module], that in turn depends on other Jetty modules that provide key functionalities, in particular: + +* <> +* xref:modules/standard.adoc#bytebufferpool[`ByteBuffer` pooling] +* <> + +[[logging]] +== Logging + +You can configure two types of logging in Jetty: _server logging_ and _request logging_. + +* <> refers to the console output produced by Jetty itself. +* <> refers to the information that Jetty can capture about incoming HTTP requests and responses. + +[[logging-server]] +=== Server Logging + +Jetty uses the http://slf4j.org/[SLF4J] API for its logging. +SLF4J is a generic abstraction layer that is supported by many different logging frameworks (or SLF4J _bindings_). + +Jetty provides a default binding via the `jetty-slf4j-impl` Maven artifact, but you can plug in the SLF4J _binding_ <> provided by other logging frameworks. + +Jetty's server logging is enabled by default with the `logging` Jetty module. +You typically won't have to enable the `logging` module directly, since it is a transitive dependency of the `server` module, and thus pulled in by many of the most commonly used modules. + +The `logging` module is a xref:modules/index.adoc#names[_virtual_ module] and its default implementation is provided by the `logging-jetty` Jetty module, which uses the Jetty SLF4J binding. + +[[logging-server-default]] +==== Default Configuration + +Jetty's default SLF4J binding uses an _appender_ (`org.eclipse.jetty.logging.StdErrAppender`) to format a logging message with metadata (like a timestamp) before sending it to `System.err`. +The default `StdErrAppender` format is: + +---- +:::: +---- + +where `` is a timestamp with the format `yyyy-MM-dd HH:mm:ss.SSS`. + +You can configure the appender via a file named `jetty-logging.properties`, which must be found in the server xref:start/index.adoc#start-class-path[class-path]. +When you enable the `logging-jetty` module -- either directly or by transitive dependency, as in the following example -- Jetty automatically generates a `jetty-logging.properties` file in `$JETTY_BASE/resources/`: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http +---- + +[jetty%nowrap] +.... +[jetty] +args=--add-modules=http +highlight=(logging-jetty *transitively enabled|copy.*\.properties) +.... + +You can specify the following configuration options in `jetty-logging.properties`: + +`org.eclipse.jetty.LEVEL=`:: +Sets the logging level for the logger tree based at `org.eclipse.jetty`. +You can specify any of the usual SLF4J logging levels -- `TRACE`, `DEBUG`, `INFO` (default), `WARN` and `ERROR` -- plus two additional levels: `ALL` (an alias for `TRACE`) and `OFF` (disables logging entirely). +You can also configure a default logging level for specific loggers, or arbitrary logger trees: +* `com.example.MyComponent.LEVEL=DEBUG` (sets logging level of logger `com.example.MyComponent` to `DEBUG`) +* `com.example.LEVEL=DEBUG` (sets logging level of tree `+com.example.*+` to `DEBUG`) + +`com.example.STACKS=`:: +Specifies whether to hide stack traces for some arbitrary logger tree `+com.example.*+`. +The exception type and message are logged normally; only stack traces are hidden. +Default value is `false` + +`org.eclipse.jetty.logging.appender.NAME_CONDENSE=`:: +Specifies whether to condense logger names, so that for example `org.eclipse.jetty.util.QueuedThreadPool` becomes `oeju.QueuedThreadPool`. +Default value is `true`. + +`org.eclipse.jetty.logging.appender.MESSAGE_ALIGN=`:: +Specifies the column at which the logging `` should be printed. +The value `0` specifies no alignment. +Default value is `0`. + +`org.eclipse.jetty.logging.appender.MESSAGE_ESCAPE=`:: +Specifies whether to escape ISO control characters such as `\r` or `\n` present in the message. +Character `\r` is replaced with `<` and character `\n` is replaced with `|`; all other ISO control characters are replaced with `?`. +Default value is `false`. + +`org.eclipse.jetty.logging.appender.ZONE_ID=`:: +Specifies the timezone ID (such as `PST`, or `America/Los_Angeles` or `GMT-8:00`) for the `` part of the logging line. +The empty string specifies the `UTC` timezone. +Default value is the local timezone. + +When using the Jetty SLF4J binding, the logging levels can be dynamically changed via JMX, see xref:troubleshooting/index.adoc#logging[the troubleshooting section] for more information. + +[[logging-server-default-rolling]] +==== Capturing Logs to a Rolling File + +Logging to `System.err` may be fine at development time, but you will typically want to capture logs on disk for later inspection, or if you don't have a terminal access (for example, if you started Jetty as a service). + +The `console-capture` Jetty module allows you to capture what is written to `System.out` and `System.err` and write it to a log file. +By default, `console-capture` logs to a file in the `$JETTY_BASE/logs/` directory. + +See the xref:modules/standard.adoc#console-capture[`console-capture` module documentation] for details on configuring how logs are written to the `log` directory. + +[NOTE] +==== +The `console-capture` Jetty module should be used only in conjunction with the `logging-jetty` module, as other SLF4J bindings such as LogBack or Log4j2 have their own, more sophisticated, rolling file appenders. +==== + +[[logging-server-custom]] +==== Custom Configuration + +You can use a different SLF4J binding if you are more familiar with other logging libraries, or if you need custom logging appenders. +There are a number of out-of-the-box Jetty modules that you can use: + +* `logging-logback`, to use the http://logback.qos.ch/[LogBack] binding +* `logging-log4j2`, to use the https://logging.apache.org/log4j/2.x/[Log4j2] binding +* `logging-log4j1`, to use the https://logging.apache.org/log4j/1.2/[Log4j1] binding (note that Log4j 1.x is end-of-life) +* `logging-jul`, to use the `java.util.logging` binding +* `logging-noop`, to use the SLF4J no-operation binding (discards all logging) + +[[logging-server-custom-logback]] +==== Logging with LogBack + +To enable the `logging-logback` module, run: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=logging-logback,http +---- + +Since LogBack is released under a license that is different from Jetty's, you will be prompted to accept the LogBack license. +Once you accept the LogBack license, your `$JETTY_BASE` directory will have the following structure. + +---- +$JETTY_BASE +├── lib +│ └── logging +│ ├── logback-classic-.jar +│ └── logback-core-.jar +├── resources +│ └── logback.xml +└── start.d + ├── http.ini + └── logging-logback.ini +---- + +Jetty downloaded the required LogBack `+*.jar+` files, and created a `$JETTY_BASE/resources/logback.xml` file for configuring your LogBack logging. +Please refer to the http://logback.qos.ch/manual/configuration.html[LogBack configuration manual] for more information about how to configure LogBack. + +[[logging-server-custom-log4j2]] +==== Logging with Log4j2 + +To enable the `logging-log4j2` module, run: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=logging-log4j2,http +---- + +After accepting the Log4j2 license, you will have the following directory structure: + +---- +$JETTY_BASE +├── lib +│ └── logging +│ ├── log4j-api-.jar +│ ├── log4j-core-.jar +│ └── log4j-slf4j2-impl-.jar +├── resources +│ └── log4j2.xml +└── start.d + ├── http.ini + └── logging-log4j2.ini +---- + +Jetty downloaded the required Log4j2 `+*.jar+` files, and created a `$JETTY_BASE/resources/log4j2.xml` file that you can configure to customize your Log4j2 logging. + +Please refer to the https://logging.apache.org/log4j/2.x/manual/configuration.html[Log4j2 configuration manual] for more information about how to configure Log4j2. + +[[logging-server-bridges]] +==== Bridging Logging to SLF4J + +When you use libraries that provide the features you need (for example, JDBC drivers), it may be possible that those libraries use a different logging framework than SLF4J. + +SLF4J provides http://www.slf4j.org/legacy.html[bridges for legacy logging APIs] that allows you to bridge logging from one of these legacy logging frameworks to SLF4J. +Once the logging is bridged to SLF4J, you can use Jetty's <> or a <> so that your logging is centralized in one place. + +Jetty provides the `logging-jul-capture` module for bridging from `java.util.logging` to SLF4J. + +IMPORTANT: The modules `logging-jcl-capture` and `logging-log4j1-capture` similarly provide bridges from Jakarta Commons Logging (JCL) and Apache Log4j, respectively; however, these modules are obsolete and should not be used anymore. + +[[logging-server-bridge-jul]] +==== Bridging from `java.util.logging` + +For libraries that use `java.util.logging` as their logging framework, you can enable Jetty's `logging-jul-capture` module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=logging-jul-capture +---- + +The `logging-jul-capture` module implies `--exec` and therefore xref:start/index.adoc#start[spawns a second JVM] because it needs to provide the system property `java.util.logging.config.file` (so that `java.util.logging` can read the configuration from the specified file), and because it needs to make available on the System ClassLoader the class `org.slf4j.bridge.SLF4JBridgeHandler`. + +For example, a library that uses `java.util.logging` as its logging library is the Postgresql JDBC driver. +With the `logging-jul-capture` Jetty module, the logging follows this diagram: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant "Postgresql JDBC" as postgresql +participant java.util.logging +participant SLF4JBridgeHandler +participant Jetty +participant SLF4J +participant "Jetty SLF4J Binding" as binding + + +postgresql -> java.util.logging +java.util.logging -> SLF4JBridgeHandler +SLF4JBridgeHandler -> SLF4J +SLF4J -> binding +Jetty -> SLF4J +SLF4J -> binding +---- + +Note how Jetty logs directly to SLF4J, while the Postgresql JDBC driver logs to SLF4J through the `SLF4JBridgeHandler`. +They both arrive to the SLF4J binding, in this case the Jetty SLF4J binding (but could be any other SLF4J binding such as LogBack). + +[[logging-request]] +=== Request Logging + +HTTP requests and responses can be logged to provide data that can be later analyzed with other tools, that can provide information such as the most frequently accessed request URIs, the response status codes, the request/response content lengths, geographical information about the clients, etc. + +Request logging is enabled by enabling the `requestlog` Jetty module. +In the example below, both the `http` Jetty module and the `requestlog` module are enabled, so that you can make HTTP requests to the server and have them logged: + +---- +$ cd $JETTY_BASE +$ java -jar $JETTY_HOME/start.jar --add-modules=http,requestlog +---- + +The `$JETTY_BASE` directory looks like this: + +[source] +---- +$JETTY_BASE +├── logs +├── resources +│ └── jetty-logging.properties +└── start.d + ├── http.ini + └── requestlog.ini +---- + +The `$JETTY_BASE/start.d/requestlog.ini` file is the Jetty module configuration file that allows you to configure the `requestlog` module, see xref:modules/standard.adoc#requestlog[this section] for more details. + +By default the `requestlog` Jetty module produces the `$JETTY_BASE/logs/yyyy_MM_dd.request.log`, where the pattern `yyyy_MM_dd` is replaced with the current date, for example `2020_01_31`. + +The format of the request log lines is the result of a _format string_ that uses formatting symbols to log relevant request/response data. + +The default format is the https://en.wikipedia.org/wiki/Common_Log_Format[NCSA Format] extended with referrer data and user-agent data. +A typical log line looks like this: + +[,options=nowrap] +---- +192.168.0.100 - - [31/Jan/2020:20:30:40 +0000] "GET / HTTP/1.1" 200 6789 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36" +---- + +The line above (that uses fake values) shows `192.168.0.100` for the client IP address, a hard-coded `-` for the identity, `-` for the authenticated user name, `[31/Jan/2020:20:30:40 +0000]` for the date and time with timezone, `"GET / HTTP/1.1"` for the HTTP request line, `200` for the HTTP response status code, `6789` for the HTTP response content length, `"-"` for the referrer and `"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36"` for the user-agent. + +The format string can be customized as described in xref:modules/standard.adoc#requestlog[this section]. +Request log files are rolled every day, and retained for customizable number of days, by default 90 days. + +[NOTE] +==== +When Jetty is behind a load balancer, you want to log the remote client IP address, not the load balancer IP address. Refer to xref:protocols/index.adoc#proxy[this section] to configure the load balancer and Jetty to retain the remote client IP address information. +==== + +[[threadpool]] +== Thread Pooling + +Jetty uses thread pooling to efficiently execute tasks that provide Jetty functionalities. + +Like any other component, the Jetty thread pool is configured and enabled via the xref:modules/standard.adoc#threadpool[`threadpool` Jetty module], that is transitively enabled by the xref:modules/standard.adoc#server[`server` Jetty module] which, in turn, is transitively enabled by a protocol module such as the xref:protocols/index.adoc#http[`http` Jetty module]: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=http +---- + +The command above gives you the default configuration for the thread pool. + +If you want to explicitly configure the thread pool, it is enough to explicitly specify the xref:modules/standard.adoc#threadpool[`threadpool`] module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=threadpool,http +---- + +After the command above, the `$JETTY_BASE` directory looks like this: + +[source] +---- +$JETTY_BASE +├── resources +│ └── jetty-logging.properties +└── start.d + ├── http.ini + └── threadpool.ini +---- + +Now you can customize the `threadpool.ini` file to explicitly configure the thread pool. + +[[threadpool-virtual]] +=== Virtual Threads Support + +Virtual threads have been introduced as a preview feature in Java 19 and Java 20, and have become an official feature since Java 21. + +The xref:modules/standard.adoc#threadpool-virtual-preview[`threadpool-virtual-preview`] Jetty module provides support for virtual threads in Java 19 and Java 20, and it is mutually exclusive with the `threadpool` Jetty module. + +The xref:modules/standard.adoc#threadpool-virtual[`threadpool-virtual`] Jetty module provides support for virtual threads in Java 21 or later, and it is mutually exclusive with the `threadpool` Jetty module. + +If you have already enabled the `threadpool` Jetty module, it is sufficient to remove it by removing the `$JETTY_BASE/start.d/threadpool.ini` file. + +When using Java 21 or later, you can enable the xref:modules/standard.adoc#threadpool-virtual[`threadpool-virtual`] module: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=threadpool-virtual,http +---- + +After the command above, the `$JETTY_BASE` directory looks like this: + +[source] +---- +$JETTY_BASE +├── resources +│ └── jetty-logging.properties +└── start.d + ├── http.ini + └── threadpool-virtual.ini +---- + +Now you can customize the `threadpool-virtual.ini` file to explicitly configure the thread pool and the virtual threads and then start Jetty: + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=threadpool-virtual,http +.... diff --git a/documentation/jetty/modules/operations-guide/pages/session/index.adoc b/documentation/jetty/modules/operations-guide/pages/session/index.adoc new file mode 100644 index 000000000000..7596c1dcf5ca --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/session/index.adoc @@ -0,0 +1,974 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += HTTP Session Management + +HTTP sessions are a concept within the Servlet API which allow requests to store and retrieve information across the time a user spends in an application. +Jetty offers a number of pluggable alternatives for managing and distributing/persisting sessions. +Choosing the best alternative is an important consideration for every application as is the correct configuration to achieve optimum performance. + +[[overview]] +== HTTP Session Overview + +=== Terminology + +Before diving into the specifics of how to plug-in and configure various alternative HTTP session management modules, let's review some useful terminology: + +Session:: +is a means of retaining information across requests for a particular user. +The Servlet Specification defines the semantics of sessions. +Some of the most important characteristics of sessions is that they have a unique id and that their contents cannot be shared between different contexts (although the id can be): if a session is invalidated in one context, then all other sessions that share the same id in other contexts will also be invalidated. +Sessions can expire or they can be explicitly invalidated. + +SessionIdManager:: +is responsible for allocating session ids. +A Jetty server can have at most 1 SessionIdManager. + +HouseKeeper:: +is responsible for periodically orchestrating the removal of expired sessions. +This process is referred to as <>. + +SessionHandler:: +is responsible for managing the lifecycle of sessions. +A context can have at most 1 `SessionHandler`. + +SessionCache:: +is a L1 cache of in-use session objects. +The `SessionCache` is used by the `SessionHandler`. + +SessionDataStore:: +is responsible for all clustering/persistence operations on sessions. +A `SessionCache` uses a `SessionDataStore` as a backing store. + +CachingSessionDataStore:: +is an L2 cache of session data. +A `SessionCache` can use a `CachingSessionDataStore` as its backing store. + +More details on these concepts can be found in the xref:programming-guide:server/session.adoc[Programming Guide]. + +[NOTE] +==== +``SessionDataStore``s implementations interact with other, usually third party, systems responsible for storing and/or distributing session information. +Sessions can be distributed without being persisted. +They can also be persisted without being distributed. +Because persisting session information to a shared store is a very common way of distributing (also known as "clustering") sessions, in the documentation we will often refer to just "persisting". +==== + +[[modules]] +=== Session Modules + +There are a number of modules that offer pluggable alternatives for http session management. +You can design how you want to cache and store http sessions by selecting alternative combinations of session modules. + +For example, Jetty ships with two alternative implementations of the `SessionCache`: + +* one that caches sessions in memory: <> +* one that does not actually cache: <> + +There are at least 6 alternative implementations of the `SessionDataStore` that you can use to persist/distribute your http sessions: + +* file system storage: <> +* relational database storage: <> +* NoSQL database storage: <> +* Google Cloud datastore storage: <> +* Hazelcast: <> or <> +* Infinispan: <> or <> + +TIP: It is worth noting that if you do not configure _any_ session modules, Jetty will still provide HTTP sessions that are cached in memory but are never persisted. + +[[base]] +== The Base Session Module + +The `sessions` module is the base module that all other session modules depend upon. +As such it will be _transitively_ enabled if you enable any of the other session modules: you need to _explicitly_ enable it if you wish to _change_ any settings from their defaults. + +Enabling the `sessions` module puts the `$JETTY_HOME/etc/sessions/id-manager.xml` file onto the execution path and generates a `$JETTY_BASE/start.d/sessions.ini` file. + +The `id-manager.xml` file instantiates a `DefaultSessionIdManager` and `HouseKeeper`. +The former is used to generate and manage session ids whilst the latter is responsible for periodic <> of expired sessions. + +=== Configuration + +The `$JETTY_BASE/start.d/sessions.ini` file contains these configuration properties: + +jetty.sessionIdManager.workerName:: +This uniquely identifies the jetty server instance and is applied to the `SessionIdManager`. +You can either provide a value for this property, or you can allow Jetty to try and synthesize a `workerName` - the latter option is _only_ advisable in the case of a single, non-clustered deployment. +There are two ways a default `workerName` can be synthesized: + +* if running on Google AppEngine, the `workerName` will be formed by concatenating the values of the environment variables `JETTY_WORKER_INSTANCE` and `GAE_MODULE_INSTANCE` +* otherwise, the `workerName` will be formed by concatenating the environment variable `JETTY_WORKER_INSTANCE` and the literal `0`. + +So, if you're not running on Google AppEngine, and you haven't configured one, the workerName will always be: `node0`. + +IMPORTANT: If you have more than one Jetty instance, it is *crucial* that you configure the `workerName` differently for each instance. + +jetty.sessionScavengeInterval.seconds:: +This is the period in _seconds_ between runs of the `HouseKeeper`, responsible for orchestrating the removal of expired sessions. +By default it will run approximately every 600 secs (ie 10 mins). +As a rule of thumb, you should ensure that the <> interval is shorter than the `` of your sessions to ensure that they are promptly scavenged. +On the other hand, if you have a backend store configured for your sessions, <> too frequently can increase the load on it. + +CAUTION: Don't forget that the `` is specified in `web.xml` in _minutes_ and the value of the `jetty.sessionScavengeInterval.seconds` is in _seconds_. + +[[base-scavenge]] +=== Session Scavenging + +The `HouseKeeper` is responsible for the periodic initiation of session scavenge cycles. +The `jetty.sessionScavengeInterval.seconds` property in `$JETTY_BASE/start.d/sessions.ini` controls the periodicity of the cycle. + +[NOTE] +==== +The HouseKeeper semi-randomly adds an additional 10% to the configured `sessionScavengeInterval`. +This is to prevent multiple nodes in a cluster that are all started at once from syncing up scavenge cycles and placing extra load on the configured persistence mechanism. +==== + +A session whose expiry time has been exceeded is considered eligible for scavenging. +The session might be present in a `SessionCache` and/or present in the session persistence/clustering mechanism. + +Scavenging occurs for all contexts on a server at every cycle. +The `HouseKeeper` sequentially asks the `SessionHandler` in each context to find and remove expired sessions. +The `SessionHandler` works with the `SessionDataStore` to evaluate candidates for expiry held in the `SessionCache`, and also to sweep the persistence mechanism to find expired sessions. + +The sweep takes two forms: once per cycle the `SessionDataStore` searches for sessions for its own context that have expired; infrequently, the `SessionDataStore` will widen the search to expired sessions in all contexts. +The former finds sessions that are no longer in this context's `SessionCache`, and using some heuristics, are unlikely to be in the `SessionCache` of the same context on another node either. +These sessions will be loaded and fully expired, meaning that `HttpSessionListener.destroy()` will be called for them. +The latter finds sessions that have not been disposed of by scavenge cycles on any other context/node. +As these will be sessions that expired a long time ago, and may not be appropriate to load by the context doing the scavenging, these are summarily deleted without `HttpSessionListener.destroy()` being called. + +A combination of these sweeps should ensure that the persistence mechanism does not fill over time with expired sessions. + +As aforementioned, the sweep period needs to be short enough to find expired sessions in a timely fashion, but not so often that it overloads the persistence mechanism. + +[[cache]] +== Modules for HTTP Session Caching + +In this section we will look at the alternatives for the `SessionCache`, i.e. the L1 cache of in-use session objects. +Jetty ships with 2 alternatives: an in-memory cache, and a null cache. +The latter does not actually do any caching of sessions, and can be useful if you either want to minimize your support for sessions, or you are in a clustered deployment without a sticky loadbalancer. + +The <> go into more detail on this. + +[[cache-hash]] +=== Caching in Memory + +If you wish to change any of the default configuration values you should enable the `session-cache-hash` xref:modules/index.adoc[module]. +The name `"hash"` harks back to historical Jetty session implementations, whereby sessions were kept in memory using a HashMap. + +==== Configuration + +The `$JETTY_BASE/start.d/session-cache-hash.ini` contains the following configurable properties: + +jetty.session.evictionPolicy:: +Integer, default -1. +This controls whether session objects that are held in memory are subject to eviction from the cache. +Eviction means that the session is removed from the cache. +This can reduce the memory footprint of the cache and can be useful if you have a lot of sessions. +Eviction is usually used in conjunction with a `SessionDataStore` that persists sessions. +The eviction strategies and their corresponding values are: + -1 (NO EVICTION)::: + sessions are never evicted from the cache. + The only way they leave are via expiration or invalidation. + 0 (EVICT AFTER USE)::: + sessions are evicted from the cache as soon as the last active request for it finishes. + The session will be passed to the `SessionDataStore` to be written out before eviction. + >= 1 (EVICT ON INACTIVITY)::: + any positive number is the time in seconds after which a session that is in the cache but has not experienced any activity will be evicted. + Use the `jetty.session.saveOnInactiveEvict` property to force a session write before eviction. + +NOTE: If you are not using one of the session store modules, ie one of the ``session-store-xxxx``s, then sessions will be lost when the context is stopped, or the session is evicted. + +jetty.session.saveOnInactiveEvict:: +Boolean, default `false`. +This controls whether a session will be persisted to the `SessionDataStore` if it is being evicted due to the EVICT ON INACTIVITY policy. +Usually sessions will be written to the `SessionDataStore` whenever the last simultaneous request exits the session. +However, as `SessionDataStores` can be configured to skip some writes (see the documentation for the `session-store-xxx` module that you are using), this option is provided to ensure that the session will be written out. + +NOTE: Be careful with this option, as in clustered scenarios it would be possible to "re-animate" a session that has actually been deleted by another node. + +jetty.session.saveOnCreate:: +Boolean, default `false`. +Controls whether a session that is newly created will be immediately saved to the `SessionDataStore` or lazily saved as the last request for the session exits. +This can be useful if the request dispatches to another context and needs to re-use the same session id. + +jetty.session.removeUnloadableSessions:: +Boolean, default `false`. +Controls whether the session cache should ask a `SessionDataStore` to delete a session that cannot be restored - for example because it is corrupted. + +jetty.session.flushOnResponseCommit:: +Boolean, default `false`. +If true, if a session is "dirty" - ie its attributes have changed - it will be written to the `SessionDataStore` as the response is about to commit. +This ensures that all subsequent requests whether to the same or different node will see the updated session data. +If false, a dirty session will only be written to the backing store when the last simultaneous request for it leaves the session. + +jetty.session.invalidateOnShutdown:: +Boolean, default `false`. +If true, when a context is shutdown, all sessions in the cache are invalidated and deleted both from the cache and from the `SessionDataStore`. + +[[cache-null]] +=== No Caching + +You may need to use the `session-cache-null` module if your clustering setup does not have a sticky load balancer, or if you want absolutely minimal support for sessions. +If you enable this module, but you don't enable a module that provides session persistence (ie one of the `session-store-xxx` modules), then sessions will _neither_ be retained in memory _nor_ persisted. + +==== Configuration + +The `$JETTY_BASE/start.d/session-cache-null.ini` contains the following configurable properties: + +jetty.session.saveOnCreate:: +Boolean, default `false`. +Controls whether a session that is newly created will be immediately saved to the `SessionDataStore` or lazily saved as the last request for the session exits. +This can be useful if the request dispatches to another context and needs to re-use the same session id. + +jetty.session.removeUnloadableSessions:: +Boolean, default `false`. +Controls whether the session cache should ask a `SessionDataStore` to delete a session that cannot be restored - for example because it is corrupted. + +jetty.session.flushOnResponseCommit:: +Boolean, default `false`. +If true, if a session is "dirty" - ie its attributes have changed - it will be written to the backing store as the response is about to commit. +This ensures that all subsequent requests whether to the same or different node will see the updated session data. +If false, a dirty session will only be written to the backing store when the last simultaneous request for it leaves the session. + +[[filesystem]] +== Modules for Persistent HTTP Sessions: File System + +The `session-store-file` Jetty module supports persistent storage of session data in a filesystem. + +IMPORTANT: Persisting sessions to the local file system should *never* be used in a clustered environment. + +Enabling this module creates the `$JETTY_BASE/sessions` directory. +By default session data will be saved to this directory, one file representing each session. + +File names follow this pattern: + +`+[expiry]_[contextpath]_[virtualhost]_[id]+` + +expiry:: +This is the expiry time in milliseconds since the epoch. + +contextpath:: +This is the context path with any special characters, including `/`, replaced by the `_` underscore character. +For example, a context path of `/catalog` would become `_catalog`. +A context path of simply `/` becomes just `__`. + +virtualhost:: +This is the first virtual host associated with the context and has the form of 4 digits separated by `.` characters: `+[digit].[digit].[digit].[digit]+`. +If there are no virtual hosts associated with a context, then `0.0.0.0` is used. + +id:: +This is the unique id of the session. + +Putting all of the above together as an example, a session with an id of `node0ek3vx7x2y1e7pmi3z00uqj1k0` for the context with path `/test` with no virtual hosts and an expiry of `1599558193150` would have a file name of: + +`1599558193150__test_0.0.0.0_node0ek3vx7x2y1e7pmi3z00uqj1k0` + +=== Configuration + +The `$JETTY_BASE/start.d/sessions.ini` file contains the following properties which may be modified to customise filesystem session storage: + +jetty.session.storeDir:: +The default is `$JETTY_BASE/sessions`. +This is a path that defines the location for storage of session files. + +jetty.session.file.deleteUnrestorableFiles:: +Boolean, default `false`. +If set to `true`, unreadable files will be deleted. +This is useful to prevent repeated logging of the same error when the scavenger periodically (re-)attempts to load the corrupted information for a session in order to expire it. + +jetty.session.gracePeriod.seconds:: +Integer, default 3600. +Used during session <>. +Multiples of this period are used to define how long ago a stored session must have expired before it should be <>. + +jetty.session.savePeriod.seconds:: +Integer, in seconds, default is `0`. +Whenever a session is accessed by a request, its `lastAccessTime` and `expiry` are updated. +Even if your sessions are read-mostly, the `lastAccessTime` and `expiry` will always change. +For heavily-used, read-mostly sessions you can save some time by skipping some writes for sessions for which only these fields have changed (ie no session attributes changed). +The value of this property is used to skip writes for these kinds of sessions: the session will only be written out if the time since the last write exceeds the value of this property. + +[WARNING] +==== +You should be careful in the use of this property in clustered environments: if you set too large a value for this property, the session may not be written out sufficiently often to update its `expiry` time thus making it appear to other nodes that it has expired. +Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - it would be undesirable to set a `savePeriod` that is larger than the `maxIdleTime`. +==== + +[[jdbc]] +== Modules for Persistent HTTP Sessions: JDBC + +Enabling the `session-store-jdbc` module configures Jetty to persist session data in a relational database. + +=== Configuration + +After enabling the module, the `$JETTY_BASE/start.d/session-store-jdbc.ini` file contains the following customizable properties: + +jetty.session.gracePeriod.seconds:: +Integer, default 3600. +Used during session <>. +Multiples of this period are used to define how long ago a stored session must have expired before it should be <>. + +jetty.session.savePeriod.seconds:: +Integer, in seconds, default is `0`. +Whenever a session is accessed by a request, its `lastAccessTime` and `expiry` are updated. +Even if your sessions are read-mostly, the `lastAccessTime` and `expiry` will always change. +For heavily-used, read-mostly sessions you can save some time by skipping some writes for sessions for which only these fields have changed (ie no session attributes changed). +The value of this property is used to skip writes for these kinds of sessions: the session will only be written out if the time since the last write exceeds the value of this property. + +[WARNING] +==== +You should be careful in the use of this property in clustered environments: if you set too large a value for this property, the session may not be written out sufficiently often to update its `expiry` time thus making it appear to other nodes that it has expired. +Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - it would be undesirable to set a `savePeriod` that is larger than the `maxIdleTime`. +==== + +db-connection-type:: +Default `datasource`. +Set to either `datasource` or `driver` depending on the type of connection being used. +Depending which you select, there are additional properties available: + +`datasource`::: +jetty.session.jdbc.datasourceName:::: +Name of the remote datasource. + +`driver`::: +jetty.session.jdbc.driverClass:::: +Name of the JDBC driver that controls access to the remote database, such as `com.mysql.jdbc.Driver` +jetty.session.jdbc.driverUrl:::: +URL of the database which includes the driver type, host name and port, service name and any specific attributes unique to the database, such as a username. +As an example, here is a mysql connection with the username appended: `jdbc:mysql://127.0.0.1:3306/sessions?user=sessionsadmin`. + +jetty.session.jdbc.blobType:: +Optional. +Default `blob` or `bytea` for Postgres. +This is the keyword used by the particular database to identify the blob data type. +If netiher default is suitable you can set this value explicitly. + +jetty.session.jdbc.longType:: +Optional. +Default `bigint` or `number(20)` for Oracle. +This is the keyword used by the particular database to identify the long integer data type. +Set this explicitly if neither of the default values is appropriate. + +jetty.session.jdbc.stringType:: +Optional. +Default `varchar`. +This is the keyword used by the particular database to identify character type. +If the default is not suitable, you can set this value explicitly. + +jetty.session.jdbc.schema.schemaName:: +jetty.session.jdbc.schema.catalogName:: +Optional. +The exact meaning of these two properties is dependent on your database vendor, but can broadly be described as further scoping for the session table name. +See https://en.wikipedia.org/wiki/Database_schema[] and https://en.wikipedia.org/wiki/Database_catalog[]. +These extra scoping names can come into play at startup time when Jetty determines if the session table already exists, or otherwise creates it on-the-fly. +If you have employed either of these concepts when you pre-created the session table, or you want to ensure that Jetty uses them when it auto-creates the session table, then you have two options: either set them explicitly, or let Jetty infer them from a database connection (obtained using either a Datasource or Driver according to the `db-connection-type` you have configured). +To set them explicitly, uncomment and supply appropriate values for the `jetty.session.jdbc.schema.schemaName` and/or `jetty.session.jdbc.schema.catalogName` properties. +Alternatively, to allow Jetty to infer them from a database connection, use the special string `INFERRED` instead. +If you leave them blank or commented out, then the sessions table will not be scoped by schema or catalog name. + +jetty.session.jdbc.schema.table:: +Default `JettySessions`. +This is the name of the table in which session data is stored. + +jetty.session.jdbc.schema.accessTimeColumn:: +Default `accessTime`. +This is the name of the column that stores the time - in ms since the epoch - at which a session was last accessed + +jetty.session.jdbc.schema.contextPathColumn:: +Default `contextPath`. +This is the name of the column that stores the `contextPath` of a session. + +jetty.session.jdbc.schema.cookieTimeColumn:: +Default `cookieTime`. +This is the name of the column that stores the time - in ms since the epoch - that the cookie was last set for a session. + +jetty.session.jdbc.schema.createTimeColumn:: +Default `createTime`. +This is the name of the column that stores the time - in ms since the epoch - at which a session was created. + +jetty.session.jdbc.schema.expiryTimeColumn:: +Default `expiryTime`. +This is name of the column that stores - in ms since the epoch - the time at which a session will expire. + +jetty.session.jdbc.schema.lastAccessTimeColumn:: +Default `lastAccessTime`. +This is the name of the column that stores the time - in ms since the epoch - that a session was previously accessed. + +jetty.session.jdbc.schema.lastSavedTimeColumn:: +Default `lastSavedTime`. +This is the name of the column that stores the time - in ms since the epoch - at which a session was last written. + +jetty.session.jdbc.schema.idColumn:: +Default `sessionId`. +This is the name of the column that stores the id of a session. + +jetty.session.jdbc.schema.lastNodeColumn:: +Default `lastNode`. +This is the name of the column that stores the `workerName` of the last node to write a session. + +jetty.session.jdbc.schema.virtualHostColumn:: +Default `virtualHost`. +This is the name of the column that stores the first virtual host of the context of a session. + +jetty.session.jdbc.schema.maxIntervalColumn:: +Default `maxInterval`. +This is the name of the column that stores the interval - in ms - during which a session can be idle before being considered expired. + +jetty.session.jdbc.schema.mapColumn:: +Default `map`. +This is the name of the column that stores the serialized attributes of a session. + +[[mongo]] +== Modules for Persistent HTTP Sessions: MongoDB + +Enabling the `session-store-mongo` module configures Jetty to store session data in MongoDB. + +Because MongoDB is not a technology provided by the Eclipse Foundation, you will be prompted to assent to the licenses of the external vendor (Apache in this case) during the install. +Jars needed by MongoDB are downloaded and stored into a directory named `$JETTY_BASE/lib/nosql/`. + +IMPORTANT: If you want to use updated versions of the jar files automatically downloaded by Jetty, you can place them in the associated `$JETTY_BASE/lib/` directory and use the `--skip-create-files=` command line option to prevent errors when starting your server. + +=== Configuration + +The `$JETTY_BASE/start.d/session-store-mongo.ini` file contains these configurable properties: + +jetty.session.mongo.dbName:: +Default is "HttpSessions". +This is the name of the database in MongoDB used to store the session collection. + +jetty.session.mongo.collectionName:: +Default is "jettySessions". +This is the name of the collection in MongoDB used to store all of the sessions. + +The connection type-:: +You can connect to MongoDB either using a host/port combination, or a URI. +By default, the host/port method is selected, but you can change this by commenting out the unwanted method, and uncommenting the other one. +connection-type=address::: +Used when utilizing a direct connection to the MongoDB server. +jetty.session.mongo.host:::: +Host name or address for the remote MongoDB instance. +jetty.session.mongo.port:::: +Port number for the remote MongoDB instance. +connection-type=uri::: +Used when utilizing MongoURI for secured connections. +jetty.session.mongo.connectionString:::: +The string defining the MongoURI value, such as `+mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]+`. +More information on how to format the MongoURI string can be found in the https://docs.mongodb.com/manual/reference/connection-string/[official documentation for mongo]. +[NOTE] +==== +You will only use *one* `connection-type` at a time, either `address` or `uri`. +If both are utilized in your `session-store-mongo.ini`, only the _last_ `connection-type` configured in the file will be used. +==== + +jetty.session.gracePeriod.seconds:: +Integer, in seconds. +Default 3600. +Used during session <>. +Multiples of this period are used to define how long ago a stored session must have expired before it should be <>. + +jetty.session.savePeriod.seconds:: +Integer, in seconds, default is `0`. +Whenever a session is accessed by a request, its `lastAccessTime` and `expiry` are updated. +Even if your sessions are read-mostly, the `lastAccessTime` and `expiry` will always change. +For heavily-used, read-mostly sessions you can save some time by skipping some writes for sessions for which only these fields have changed (ie no session attributes changed). +The value of this property is used to skip writes for these kinds of sessions: the session will only be written out if the time since the last write exceeds the value of this property. + +[WARNING] +==== +You should be careful in the use of this property in clustered environments: if you set too large a value for this property, the session may not be written out sufficiently often to update its `expiry` time thus making it appear to other nodes that it has expired. +Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - it would be undesirable to set a `savePeriod` that is larger than the `maxIdleTime`. +==== + +[[infinispan]] +== Modules for Persistent HTTP Sessions: Infinispan + +In order to persist/cluster sessions using Infinispan, Jetty needs to know how to contact Infinispan. +There are two options: a remote Infinispan instance, or an in-process Infinispan instance. +The former is referred to as "remote" Infinispan and the latter as "embedded" Infinispan. +If you wish Jetty to be able to <> expired sessions, you will also need to enable the appropriate `infinispan-[remote|embedded]-query` module. + +[[infinispan-remote]] +=== Remote Infinispan Session Module + +The `session-store-infinispan-remote` module configures Jetty to talk to an external Infinispan instance to store session data. + +Because Infinispan is not a technology provided by the Eclipse Foundation, you will be prompted to assent to the licenses of the external vendor (Apache in this case). + +Infinispan-specific jar files are download to the directory named `$JETTY_BASE/lib/infinispan/`. + +In addition to adding these modules to the classpath of the server it also added several ini configuration files to the `$JETTY_BASE/start.d` directory. + +NOTE: If you have updated versions of the jar files automatically downloaded by Jetty, you can place them in the associated `$JETTY_BASE/lib/` directory and use the `--skip-create-files=` command line option to prevent errors when starting your server. + +==== Configuration + +The `$JETTY_BASE/start.d/session-store-infinispan-remote.ini` contains the following configurable properties: + +jetty.session.infinispan.remoteCacheName:: +Default `"sessions"`. +This is the name of the cache in Infinispan where sessions will be stored. + +jetty.session.infinispan.idleTimeout.seconds:: +Integer, in seconds, default `0`. +This is the amount of time, in seconds, that a session entry in Infinispan can be idle (ie neither read nor written) before Infinispan will delete its entry. +Usually, you do *not* want to set a value for this, as you want Jetty to manage all session expiration (and call any HttpSessionListeners). +You *should* enable the <> to allow jetty to <> for expired sessions. +If you do not, then there is the possibility that sessions can be left in Infinispan but no longer referenced by any Jetty node (so called "zombie" or "orphan" sessions), in which case you can use this feature to ensure their removal. + +IMPORTANT: You should make sure that the number of seconds you specify is larger than the configured `maxIdleTime` for sessions. + +jetty.session.gracePeriod.seconds:: +Integer, default 3600. +Used during session <>. +Multiples of this period are used to define how long ago a stored session must have expired before it should be <>. + +jetty.session.savePeriod.seconds:: +Integer, in seconds, default is `0`. +Whenever a session is accessed by a request, its `lastAccessTime` and `expiry` are updated. +Even if your sessions are read-mostly, the `lastAccessTime` and `expiry` will always change. +For heavily-used, read-mostly sessions you can save some time by skipping some writes for sessions for which only these fields have changed (ie no session attributes changed). +The value of this property is used to skip writes for these kinds of sessions: the session will only be written out if the time since the last write exceeds the value of this property. + +[WARNING] +==== +You should be careful in the use of this property in clustered environments: if you set too large a value for this property, the session may not be written out sufficiently often to update its `expiry` time thus making it appear to other nodes that it has expired. +Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - it would be undesirable to set a `savePeriod` that is larger than the `maxIdleTime`. +==== + +[[infinispan-remote-query]] +=== Remote Infinispan Query Module + +The `infinispan-remote-query` module allows Jetty to <> expired sessions. +Note that this is an *additional* module, to be used in conjunction with the `session-store-infinispan-remote` module. + +There are no configuration properties associated with this module. + +[[infinispan-embedded]] +=== Embedded Infinispan Session Module + +Enabling the `session-store-infinispan-embedded` module runs an in-process instance of Infinispan. + +Because Infinispan is not a technology provided by the Eclipse Foundation, you will be prompted to assent to the licenses of the external vendor (Apache in this case). +Infinispan-specific jar files will be downloaded and saved to a directory named `$JETTY_BASE/lib/infinispan/`. + +NOTE: If you have updated versions of the jar files automatically downloaded by Jetty, you can place them in the associated `$JETTY_BASE/lib/` directory and use the `--skip-create-files=` command line option to prevent errors when starting your server. + +==== Configuration + +The `$JETTY_BASE/start.d/session-store-infinispan-embedded.ini` contains the following configurable properties: + +jetty.session.infinispan.idleTimeout.seconds:: +Integer, in seconds, default `0`. +This is the amount of time, in seconds, that a session entry in Infinispan can be idle (ie neither read nor written) before Infinispan will delete its entry. +Usually, you do *not* want to set a value for this, as you want Jetty to manage all session expiration (and call any HttpSessionListeners). +You *should* enable the <> to allow Jetty to <> for expired sessions. +If you do not, then there is the possibility that expired sessions can be left in Infinispan. + +IMPORTANT: You should make sure that the number of seconds you specify is larger than the configured `maxIdleTime` for sessions. + +jetty.session.gracePeriod.seconds:: +Integer, default 3600. +Used during session <>. +Multiples of this period are used to define how long ago a stored session must have expired before it should be <>. + +jetty.session.savePeriod.seconds:: +Integer, in seconds, default is `0`. +Whenever a session is accessed by a request, its `lastAccessTime` and `expiry` are updated. +Even if your sessions are read-mostly, the `lastAccessTime` and `expiry` will always change. +For heavily-used, read-mostly sessions you can save some time by skipping some writes for sessions for which only these fields have changed (ie no session attributes changed). +The value of this property is used to skip writes for these kinds of sessions: the session will only be written out if the time since the last write exceeds the value of this property. + +[WARNING] +==== +Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - it would be undesirable to set a `savePeriod` that is larger than the `maxIdleTime`. +==== + +[[infinispan-embedded-query]] +=== Embedded Infinispan Query Module + +The `infinispan-embedded-query` module allows Jetty to <> expired sessions. + +There are no configuration properties associated with this module. + +=== Converting Session Format for Jetty-9.4.13 + +From Jetty-9.4.13 onwards, we have changed the format of the serialized session when using a remote cache (ie using hotrod). +Prior to release 9.4.13 we used the default Infinispan serialization, however this was not able to store sufficient information to allow Jetty to properly deserialize session attributes in all circumstances. +See issue https://github.com/eclipse/jetty.project/issues/2919[] for more background. + +We have provided a conversion program which will convert any sessions stored in Infinispan to the new format. + +IMPORTANT: We recommend that you backup your stored sessions before running the conversion program. + +How to use the converter: + +---- +java -cp jetty-jakarta-servlet-api-4.0.2.jar:jetty-util-{VERSION}.jar:jetty-server-{VERSION}.jar:infinispan-remote-9.1.0.Final.jar:jetty-infinispan-{VERSION}.jar:[other classpath] org.eclipse.jetty.session.infinispan.InfinispanSessionLegacyConverter + +Usage: InfinispanSessionLegacyConverter [-Dhost=127.0.0.1] [-Dverbose=true|false] [check] +---- + +The classpath:: +Must contain the servlet-api, jetty-util, jetty-server, jetty-infinispan and infinispan-remote jars. If your sessions contain attributes that use application classes, you will also need to also put those classes onto the classpath. If your session has been authenticated, you may also need to include the jetty-security and jetty-http jars on the classpath. + +Parameters:: +When used with no arguments the usage message is printed. When used with the `cache-name` parameter the conversion is performed. When used with both `cache-name` and `check` parameters, sessions are checked for whether or not they are converted. +-Dhost::: you can optionally provide a system property with the address of your remote Infinispan server. Defaults to the localhost. +-Dverbose::: defaults to false. If true, prints more comprehensive stacktrace information about failures. Useful to diagnose why a session is not converted. +cache-name::: the name of the remote cache containing your sessions. This is mandatory. +check::: the optional check command will verify sessions have been converted. Use it _after_ doing the conversion. + +To perform the conversion, run the InfinispanSessionLegacyConverter with just the `cache-name`, and optionally the `host` system property. +The following command will attempt to convert all sessions in the cached named `my-remote-cache` on the machine `myhost`, ensuring that application classes in the `/my/custom/classes` directory are on the classpath: + +---- +java -cp jetty-jakarta-servlet-api-4.0.2.jar:jetty-util-{VERSION}.jar:jetty-server-{VERSION}.jar:infinispan-remote-9.1.0.Final.jar:jetty-infinispan-{VERSION}.jar:/my/custom/classes org.eclipse.jetty.session.infinispan.InfinispanSessionLegacyConverter -Dhost=myhost my-remote-cache +---- + +If the converter fails to convert a session, an error message and stacktrace will be printed and the conversion will abort. The failed session should be untouched, however _it is prudent to take a backup of your cache before attempting the conversion_. + +[[hazelcast]] +== Modules for Persistent HTTP Sessions: Hazelcast + +Hazelcast can be used to cluster session information in one of two modes: either remote or embedded. +Remote mode means that Hazelcast will create a client to talk to other instances, possibly on other nodes. +Embedded mode means that Hazelcast will start a local instance and communicate with that. + +[[hazelcast-remote]] +=== Remote Hazelcast Clustering + +Enabling the `session-store-hazelcast-remote` module allows jetty to communicate with a remote Hazelcast instance to cluster session data. + +Because Hazelcast is not a technology provided by the Eclipse Foundation, you will be prompted to assent to the licenses of the external vendor (Apache in this case). + +Hazelcast-specific jar files will be downloaded and saved to a directory named `$JETTY_BASE/lib/hazelcast/`. + +NOTE: If you have updated versions of the jar files automatically downloaded by Jetty, you can place them in the associated `$JETTY_BASE/lib/` directory and use the `--skip-create-files=` command line option to prevent errors when starting your server. + +==== Configuration + +The `start.d/session-store-hazelcast-remote.ini` contains a list of all the configurable options for the Hazelcast module: + +jetty.session.hazelcast.mapName:: +The default is "jetty-distributed-session-map". +This is the name of the Map in Hazelcast where sessions will be stored. + +jetty.session.hazelcast.onlyClient:: +Boolean, default `true`. +The Hazelcast instance will be configured in client mode. + +jetty.session.hazelcast.configurationLocation:: +Optional. +This is the path to an external Hazelcast xml configuration file. + +jetty.session.hazelcast.useQueries:: +Boolean, default `false`. +If `true`, Jetty will use Hazelcast queries to find sessions to <>. +If `false` sessions that are not currently in a <> cannot be <>, and will need to be removed by some external process. + +jetty.session.hazelcast.addresses:: +Optional. +These are the addresses of remote Hazelcast instances with which to communicate. + +jetty.session.gracePeriod.seconds:: +Integer, in seconds. +Default 3600. +Used during session <>. +Multiples of this period are used to define how long ago a stored session must have expired before it should be <>. + +jetty.session.savePeriod.seconds:: +Integer, in seconds, default is `0`. +Whenever a session is accessed by a request, its `lastAccessTime` and `expiry` are updated. +Even if your sessions are read-mostly, the `lastAccessTime` and `expiry` will always change. +For heavily-used, read-mostly sessions you can save some time by skipping some writes for sessions for which only these fields have changed (ie no session attributes changed). +The value of this property is used to skip writes for these kinds of sessions: the session will only be written out if the time since the last write exceeds the value of this property. + +[WARNING] +==== +You should be careful in the use of this property in clustered environments: if you set too large a value for this property, the session may not be written out sufficiently often to update its `expiry` time thus making it appear to other nodes that it has expired. +Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - it would be undesirable to set a `savePeriod` that is larger than the `maxIdleTime`. +==== + +IMPORTANT: Be aware that if your session attributes contain classes from inside your webapp (or Jetty classes) then you will need to put these classes onto the classpath of all of your Hazelcast instances. + +[[hazelcast-embedded]] +=== Embedded Hazelcast Clustering + +This will run an in-process instance of Hazelcast. +This can be useful for example during testing. +To enable this you enable the `session-store-hazelcast-embedded` module. + +Because Hazelcast is not a technology provided by the Eclipse Foundation, you will be prompted to assent to the licenses of the external vendor (Apache in this case). + +Hazelcast-specific jar files will be downloaded to a directory named `$JETTY_BASE/lib/hazelcast/`. + +==== Configuration + +The `$JETTY_BASE/start.d/start.d/session-store-hazelcast-embedded.ini` contains a list of all the configurable options for the Hazelcast module: + +jetty.session.hazelcast.mapName:: +The default is "jetty-distributed-session-map". +This is the name of the Map in Hazelcast where sessions will be stored. +jetty.session.hazelcast.hazelcastInstanceName +Default is "JETTY_DISTRIBUTED_SESSION_INSTANCE". +This is the unique name of the Hazelcast instance that will be created. + +jetty.session.hazelcast.configurationLocation:: +Optional. +This is the path to an external Hazelcast xml configuration file. + +jetty.session.hazelcast.useQueries:: +Boolean, default `false'. +If `true`, Jetty will use Hazelcast queries to find expired sessions to <>. +If `false` sessions that are not currently in a <> cannot be <>, and will need to be removed by some external process. + +jetty.session.gracePeriod.seconds:: +Integer, in seconds. +Default 3600. +Used during session <>. +Multiples of this period are used to define how long ago a stored session must have expired before it should be <>. + +jetty.session.savePeriod.seconds:: +Integer, in seconds, default is `0`. +Whenever a session is accessed by a request, its `lastAccessTime` and `expiry` are updated. +Even if your sessions are read-mostly, the `lastAccessTime` and `expiry` will always change. +For heavily-used, read-mostly sessions you can save some time by skipping some writes for sessions for which only these fields have changed (ie no session attributes changed). +The value of this property is used to skip writes for these kinds of sessions: the session will only be written out if the time since the last write exceeds the value of this property. + +[WARNING] +==== +You should be careful in the use of this property in clustered environments: if you set too large a value for this property, the session may not be written out sufficiently often to update its `expiry` time thus making it appear to other nodes that it has expired. +Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - it would be undesirable to set a `savePeriod` that is larger than the `maxIdleTime`. +==== + +IMPORTANT: If your session attributes contain classes from inside your webapp (or jetty classes) then you will need to put these classes onto the classpath of all of your hazelcast instances. In the case of embedded hazelcast, as it is started before your webapp, it will NOT have access to your webapp's classes - you will need to extract these classes and put them onto the jetty server's classpath. + +[[gcloud]] +== Modules for Persistent HTTP Sessions: Google Cloud DataStore + +Jetty can store http session information into GCloud by enabling the `session-store-gcloud` module. + +=== Preparation + +You will first need to create a project and enable the Google Cloud API: https://cloud.google.com/docs/authentication#preparation[]. +Take note of the project id that you create in this step as you need to supply it in later steps. + +=== Communicating with GCloudDataStore + +==== When Running Jetty Outside of Google Infrastructure + +Before running Jetty, you will need to choose one of the following methods to set up the local environment to enable remote GCloud DataStore communications. + +1. Using the GCloud SDK: + * Ensure you have the GCloud SDK installed: https://cloud.google.com/sdk/?hl=en[] + * Use the GCloud tool to set up the project you created in the preparation step: `gcloud config set project PROJECT_ID` + * Use the GCloud tool to authenticate a Google account associated with the project created in the preparation step: `gcloud auth login ACCOUNT` + +2. Using environment variables + * Define the environment variable `GCLOUD_PROJECT` with the project id you created in the preparation step. + * Generate a JSON https://cloud.google.com/storage/docs/authentication?hl=en#service_accounts[service account key] and then define the environment variable `GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/key.json` + +==== When Running Jetty Inside of Google Infrastructure + +The Google deployment tools will automatically configure the project and authentication information for you. + +=== Configuring Indexes for Session Data + +Using some special, composite indexes can speed up session search operations, although it may make write operations slower. +By default, indexes will _not_ be used. +In order to use them, you will need to manually upload a file that defines the indexes. +This file is named `index.yaml` and you can find it in your distribution in `$JETTY_BASE/etc/sessions/gcloud/index.yaml`. + +Follow the instructions https://cloud.google.com/datastore/docs/tools/#the_development_workflow_using_gcloud[here] to upload the pre-generated `index.yaml` file. + +=== Communicating with the GCloudDataStore Emulator + +To enable communication using the GCloud Emulator: + +* Ensure you have the GCloud SDK installed: https://cloud.google.com/sdk/?hl=en[] +* Follow the instructions https://cloud.google.com/datastore/docs/tools/datastore-emulator[here] on how to start the GCloud datastore emulator, and how to propagate the environment variables that it creates to the terminal in which you run Jetty. + +=== Enabling the Google Cloud DataStore Module + +The `session-store-gcloud` module provides GCloud support for storing session data. + +Because the Google Cloud DataStore is not a technology provided by the Eclipse Foundation, when enabling the module you will be prompted to assent to the licenses of the external vendor. + +As GCloud requires certain Java Commons Logging features to work correctly, Jetty routes these through SLF4J. +By default, Jetty implements the SLF4J api, but you can choose a different logging implementation by following the instructions xref:server/index.adoc#logging[here] + +IMPORTANT: If you want to use updated versions of the jar files automatically downloaded during the module enablement, you can place them in the associated `$JETTY_BASE/lib/` directory and use the `--skip-create-files=` command line option to prevent errors when starting your server. + +== Configuration + +The `$JETTY_BASE/start.d/session-store-gcloud.ini` file contains all of the configurable properties for the `session-store-gcloud` module: + +jetty.session.gcloud.maxRetries:: +Integer. +Default 5. +Maximum number of retries to connect to GCloud DataStore to write a session. + +jetty.session.gcloud.backoffMs:: +Integer in milliseconds. +Default 1000. +Number of milliseconds between successive attempts to connect to the GCloud DataStore to write a session. + +jetty.session.gracePeriod.seconds:: +Integer, in seconds. +Default 3600. +Used during session <>. +Multiples of this period are used to define how long ago a stored session must have expired before it should be <>. + +jetty.session.savePeriod.seconds:: +Integer, in seconds, default is `0`. +Whenever a session is accessed by a request, its `lastAccessTime` and `expiry` are updated. +Even if your sessions are read-mostly, the `lastAccessTime` and `expiry` will always change. +For heavily-used, read-mostly sessions you can save some time by skipping some writes for sessions for which only these fields have changed (ie no session attributes changed). +The value of this property is used to skip writes for these kinds of sessions: the session will only be written out if the time since the last write exceeds the value of this property. + +[WARNING] +==== +You should be careful in the use of this property in clustered environments: if you set too large a value for this property, the session may not be written out sufficiently often to update its `expiry` time thus making it appear to other nodes that it has expired. +Thorough consideration of the `maxIdleTime` of the session when setting the `savePeriod` is imperative - it would be undesirable to set a `savePeriod` that is larger than the `maxIdleTime`. +==== + +jetty.session.gcloud.namespace:: +Optional. +Sets the namespace for GCloud Datastore to use. +If set, partitions the visibility of session data between webapps, which is helpful for multi-tenant deployments. +More information can be found https://cloud.google.com/datastore/docs/concepts/multitenancy[here.] + +Configuration of the stored session object and its fields names-:: +You should very rarely, if ever, need to change these defaults. +jetty.session.gcloud.model.kind::: +The default is "GCloudSession". +This is the type of the object that is stored in GCloud. +jetty.session.gcloud.model.id::: +The default is "id". +This is the session id. +jetty.session.gcloud.model.contextPath::: +The default is "contextPath". +This is the canonicalized context path of the context to which the session belongs. +jetty.session.gcloud.model.vhost::: +The default is "vhost". +This is the canonicalized virtual host of the context to which the session belongs. +jetty.session.gcloud.model.accessed::: +The default is "accessed". +This is the current access time of the session. +jetty.session.gcloud.model.lastAccessed::: +The default is "lastAccessed". +This is the last access time of the session. +jetty.session.gcloud.model.createTime::: +The default is "createTime". +This is the time, in ms since the epoch, at which the session was created. +jetty.session.gcloud.model.cookieSetTime::: +The default is "cookieSetTime". +This is the time at which the session cookie was last set. +jetty.session.gcloud.model.lastNode::: +The default is "lastNode". +This is the `workerName` of the last node to manage the session. +jetty.session.gcloud.model.expiry::: +The default is "expiry". +This is the time, in ms since the epoch, at which the session will expire. +jetty.session.gcloud.model.maxInactive::: +The default is "maxInactive". +This is the session timeout in ms. +jetty.session.gcloud.model.attributes::: +The default is "attributes". +This is a map of all the session attributes. + +[[memcached]] + +== Modules for Persistent HTTP Sessions: The L2 Session Data Cache + +If your chosen persistence technology is slow, it can be helpful to locally cache the session data. +The `CachingSessionDataStore` is a special type of `SessionDataStore` that locally caches session data, which makes reads faster. It writes-through to your chosen type of `SessionDataStore` when session data changes. + +=== MemcachedSessionDataMap + +The `MemcachedSessionDataMap` uses `memcached` to perform caching of `SessionData`. + +To enable it with the Jetty distribution, enable the `session-store-cache` module, along with your chosen `session-store-xxxx` module. + +==== Configuration + +The `$JETTY_BASE/start.d/session-store-cache.ini` contains the following configurable properties: + +jetty.session.memcached.host:: +Default value is `localhost`. +This is the host on which the memcached server resides. + +jetty.session.memcached.port:: +Default value is `11211`. +This is the port on which the memcached server is listening. + +jetty.session.memcached.expirySec:: +Default value `0`. +This is the length of time in seconds that an item can remain in the memcached cache, where 0 indicates indefinitely. + +jetty.session.memcached.heartbeats:: +Default value `true`. +Whether the memcached system should generate heartbeats. + +[[usecases]] +== Session Scenarios + +=== Minimizing Support for Sessions + +The standard support for webapps in Jetty will use sessions cached in memory, but not persisted/clustered, with a scavenge for expired sessions that occurs every 10 minutes. +If you wish to pare back support for sessions because you know your app doesn't use them (or use JSPs that use them), then you can do the following: + +* enable the <> and <> to 0 to prevent scavenging +* enable the <> to prevent sessions being cached in memory + +If you wish to do any further minimization, you should consult the xref:programming-guide:server/session.adoc[Programming Guide]. + +=== Clustering with a Sticky Load Balancer + +Preferably, your cluster will utilize a sticky load balancer. +This will route requests for the same session to the same Jetty instance. +In this case, the <> can be used to keep in-use session objects <>. +You can fine-tune the cache by controlling how long session objects remain in memory with the <>. + +If you have a large number of sessions or very large session objects, then you may want to manage your memory allocation by controlling the amount of time session objects spend in the cache. +The `EVICT_ON_SESSION_EXIT` eviction policy will remove a session object from the cache as soon as the last simultaneous request referencing it exits. +Alternatively, the `EVICT_ON_INACTIVITY` policy will remove a session object from the cache after a configurable amount of time has passed without a request referencing it. + +If your sessions are very long lived and infrequently referenced, you might use the `EVICT_ON_INACTIVITY_POLICY` to control the size of the cache. + +If your sessions are small, or relatively few or stable in number or they are read-mostly, then you might select the `NEVER_EVICT` policy. +With this policy, session objects will remain in the cache until they either expire or are explicitly invalidated. + +If you have a high likelihood of simultaneous requests for the same session object, then the `EVICT_ON_SESSION_EXIT` policy will ensure the session object stays in the cache as long as it is needed. + +=== Clustering Without a Sticky Load Balancer + +Without a sticky load balancer requests for the same session may arrive on any node in the cluster. +This means it is likely that the copy of the session object in any `SessionCache` is likely to be out-of-date, as the session was probably last accessed on a different node. +In this case, your choices are to use either the <> or to de-tune the <>. +If you use the `NullSessionCache` all session object caching is avoided. +This means that every time a request references a session it must be read in from persistent storage. +It also means that there can be no sharing of session objects for multiple requests for the same session: each will have their own independent session object. +Furthermore, the outcome of session writes are indeterminate because the Servlet Specification does not mandate ACID transactions for sessions. + +If you use the `DefaultSessionCache`, there is a risk that the caches on some nodes will contain out-of-date session information as simultaneous requests for the same session are scattered over the cluster. +To mitigate this somewhat you can use the `EVICT_ON_SESSION_EXIT` eviction policy: this will ensure that the session is removed from the cache as soon as the last simultaneous request for it exits. +Again, due to the lack of session transactionality, the ordering outcome of write operations cannot be guaranteed. +As the session is cached while at least one request is accessing it, it is possible for multiple simultaneous requests to share the same session object. + +=== Handling Corrupted or Unreadable Session Data + +For various reasons it might not be possible for the `SessionDataStore` to re-read a stored session. +One scenario is that the session stores a serialized object in its attributes, and after a re-deployment there in an incompatible class change. +Setting the `$JETTY_BASE/start.d/session-cache-hash.ini` or `$JETTY_BASE/start.d/session-cache-null.ini` property `jetty.session.removeUnloadableSessions` to `true` will allow the unreadable session to be removed from persistent storage. +This can be useful for preventing the <> from continually generating errors on the same expired, but un-readable session. diff --git a/documentation/jetty/modules/operations-guide/pages/start/index.adoc b/documentation/jetty/modules/operations-guide/pages/start/index.adoc new file mode 100644 index 000000000000..669aba586ae9 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/start/index.adoc @@ -0,0 +1,622 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Start Mechanism + +NOTE: Make sure you have read the xref:arch/index.adoc[Jetty architecture section] if you are not familiar with the terms used in this section. + +The Jetty start mechanism is invoked by executing `$JETTY_HOME/start.jar`, from within a `$JETTY_BASE` directory, with zero or more command line options: + +---- +$ cd $JETTY_BASE +$ java -jar $JETTY_HOME/start.jar ... +---- + +The Jetty start mechanism has two main modes of operation: + +* The _tool_ mode, detailed in <>, when it is used as a command line tool to configure the `$JETTY_BASE` directory by enabling modules, creating sub-directories and files, downloading files, etc. +In this mode, the JVM started with `java -jar $JETTY_HOME/start.jar` performs the specified command and then exits. +* The _start_ mode, detailed in <>, when it is used to start the JVM that runs Jetty with the specified configuration. +In this mode, the JVM started with `java -jar $JETTY_HOME/start.jar` starts Jetty and does not exit until stopped, for example by hitting kbd:[Ctrl+C] on the terminal. + +Refer to the <> for the complete list of the available command line options. + +You want to use the Jetty start mechanism to <> and then to <>. + +[[configure]] +== Configuring $JETTY_BASE + +Within the Jetty start mechanism, the source of configurations is layered in this order, from higher priority to lower priority: + +* The command line options. +* The `$JETTY_BASE` directory, and its files. +* The directory specified with the `--add-config-dir` option, and its files. +* The `$JETTY_HOME` directory, and its files. + +[[configure-enable]] +=== Enabling Modules + +You can enable Jetty modules persistently across restarts with the `--add-modules` command: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=server,http +---- + +The Jetty start mechanism will look for the specified modules following the order specified above. +In the common case (without a `--add-config-dir` directory), it will look in `$JETTY_BASE/modules/` first and then in `$JETTY_HOME/modules/`. + +Since the `server` and `http` modules are standard Jetty modules, they are present in `$JETTY_HOME/modules/` and loaded from there. + +When you enable a Jetty module, the Jetty start mechanism: + +* Creates the correspondent `+$JETTY_BASE/start.d/*.ini+` module configuration file. +The content of these `+*.ini+` files is copied from the `[ini-template]` section of the correspondent `+*.mod+` file. +* Executes the directives specified in `[files]` section (if present) of the `+*.mod+` file. +This may simply create a file or a directory, or download files from the Internet. +This step is performed transitively for all module dependencies. + +For example, enabling the `server` and `http` modules results in the `$JETTY_BASE` directory to have the following structure: + +---- +$JETTY_BASE +├── resources +│ └── jetty-logging.properties +└── start.d + ├── http.ini + └── server.ini +---- + +The `$JETTY_BASE/resources/jetty-logging.properties` is created by the `[files]` directives of the `logging-jetty` module, which is a transitive dependency of the `server` module. + +[[configure-disable]] +=== Disabling Modules + +A module is enabled because the correspondent `+$JETTY_BASE/start.d/*.ini+` file contains a `--module=` directive. + +Commenting out the `--module=` directive effectively disables the module. + +Deleting the correspondent `+$JETTY_BASE/start.d/*.ini+` file also disables the module. + +[[configure-edit-ini]] +=== Editing `+*.ini+` Files + +You can now edit the `+$JETTY_BASE/start.d/*.ini+` configuration files, typically by uncommenting properties to change their default value. + +The `+$JETTY_BASE/start.d/*.ini+` configuration file may be missing, if the correspondent module is a transitive dependency. +You can easily generate the configuration file by explicitly enabling the module, for example to generate the `$JETTY_BASE/start.d/logging-jetty.ini` configuration file you would issue the following command (the module order does not matter): + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=server,http,logging-jetty +---- + +The `$JETTY_BASE` directory structure is now: + +[source,subs=+quotes] +---- +$JETTY_BASE +├── resources +│ └── jetty-logging.properties +└── start.d + ├── http.ini + ├── ##logging-jetty.ini## + └── server.ini +---- + +You want to edit the `+$JETTY_BASE/start.d/*.ini+` configuration files so that the configuration is applied every time Jetty is started (or re-started). + +For example, `$JETTY_BASE/start.d/http.ini` contains the following property, commented out: + +.http.ini +---- +# jetty.http.port=8080 +---- + +You can change the clear-text HTTP port Jetty listens to by uncommenting that property and changing its value: + +.http.ini +---- +jetty.http.port=9876 +---- + +When Jetty is started (or re-started) this configuration is applied and Jetty will listen for clear-text HTTP/1.1 on port `9876`. + +[[configure-enable-command-line]] +=== Enabling Modules on Command Line + +You can also enable a module transiently, only for the current execution of the `java -jar $JETTY_HOME/start.jar` command. + +If you have an empty `$JETTY_BASE`, the following command enables the `server` and `http` modules, but does not create any `+$JETTY_BASE/start.d/*.ini+` files. + +---- +$ java -jar $JETTY_HOME/start.jar --module=server,http +---- + +Since there are no `+$JETTY_BASE/start.d/*.ini+` files, you can only customize the properties via the command line, for example: + +---- +$ java -jar $JETTY_HOME/start.jar --module=server,http jetty.http.port=9876 +---- + +Enabling modules on the command line is useful to verify that the modules work as expected, or to try different configurations. + +NOTE: It is possible to enable some module persistently via `--add-modules` and some other module transiently via `--module`. + +Remember that once the current execution terminates, the modules enabled transiently on the command line via `--module` and their configuration are not saved and will not be enabled on the next execution (unless you specify them again on the command line). + +[[configure-custom-module]] +=== Adding Your Own Modules + +NOTE: Refer to the xref:modules/custom.adoc[custom module section] for the details about how to create your own modules. + +You can add your own modules by adding a `+$JETTY_BASE/modules/*.mod+` file. + +For example, you may want to add a Postgres JDBC driver to the server class-path, to avoid that each deployed web application bring its own version. This allows you to control the exact Postgres JDBC driver version for all web applications. + +Create the `$JETTY_BASE/modules/postgresql.mod` file: + +.postgresql.mod +---- +include::code:example$jetty-modules/postgresql.mod[] +---- + +Then enable it: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=postgresql +---- + +Enabling the `postgresql` module will execute the `[files]` directive (downloading the `+*.jar+` file from Maven Central if not already present) and create the `$JETTY_BASE/start.d/postgresql.ini` with the content of the `[ini-template]` section. + +The `[lib]` section ensures that the specified file is in the server class-path when Jetty is started. + +You can <> to verify that the server class-path is correct. + +[[configure-custom-module-exec]] +=== Custom Module with JVM Options + +Using a custom Jetty module, you can customize the JVM startup options. + +This is useful if you need to start Jetty and want to specify JVM options such as: + +* `+-Xmx+`, to specify the max heap size +* `+-Xlog:gc+`, to specify the GC log file and options +* `+-javaagent+`, to specify Java agents +* `+-XX:+` options, for example to specify the GC implementation +* `+--enable-preview+`, to enable Java preview features + +Start by creating `$JETTY_BASE/modules/jvm.mod`: + +.jvm.mod +---- +include::code:example$jetty-modules/jvm.mod[] +---- + +Enable it: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=jvm +---- + +Since the module defines an `[exec]` section, it will fork _another_ JVM when Jetty is started. + +This means that when you start Jetty, there will be _two_ JVMs running: one created by you when you run `java -jar $JETTY_HOME/start.jar`, and another forked by the Jetty start mechanism with the JVM options you specified (that cannot be applied to an already running JVM). + +Again, you can <> to verify that it is correct. + +[TIP] +==== +The second JVM forked by the Jetty start mechanism when one of the modules requires forking, for example a module that contains an `[exec]` section, may not be desirable, and may be avoided as explained in <>. +==== + +[[configure-display]] +=== Displaying the Configuration + +Once you have enabled and configured the `$JETTY_BASE`, you can display the configuration to verify that it is correct. + +Using the standard `server` and `http` Jetty modules, and the `postgresql` and `jvm` custom Jetty module defined above, you obtain: + +---- +$ java -jar $JETTY_HOME/start.jar --list-config +---- + +[jetty%nowrap] +.... +[jetty] +setupModules=code:example$jetty-modules/jvm.mod,code:example$jetty-modules/postgresql.mod +setupArgs=--add-modules=server,http,postgresql,jvm +args=--list-config +.... + +Note how the configuration displayed above includes: + +* In the list of enabled modules, the `postgresql` and `jvm` modules +* In the list of JVM arguments, those specified by the `jvm` module +* In the server class-path, the `+*.jar+` file specified by the `postgresql` module + +[[configure-dry-run]] +=== Displaying the JVM Command Line + +The Jetty start mechanism can display a full JVM command line that will start Jetty with the configuration you specified, with the `--dry-run` option: + +---- +$ java -jar $JETTY_HOME/start.jar --dry-run +---- + +The full JVM command line generated by `--dry-run` can be split in various parts that can be used individually, for example in scripts. + +Furthermore, Jetty modules may specify the `--exec` option that will fork a second JVM to start Jetty, which may not be desirable. +Some option, such as `--jpms`, imply `--exec`, as it won't be possible to modify the module-path in the already started JVM. + +To start Jetty without forking a second JVM, the `--dry-run` option can be used to generate a command line that is then executed so that starting Jetty only spawns one JVM. + +IMPORTANT: You can use the `--dry-run` option as explained below to avoid forking a second JVM when using modules that have the `[exec]` section, or the `--exec` option, or when using the `--jpms` option. + +For example, using the `--dry-run` option with the `jvm.mod` introduced in <> produces the following command line: + +---- +$ java -jar $JETTY_HOME/start.jar --dry-run +---- + +[jetty%nowrap] +.... +[jetty] +setupModules=code:example$jetty-modules/jvm.mod +setupArgs=--add-modules=http,jvm +args=--dry-run +replace=( ),$1\\\n +.... + +You can then run the generated command line. + +For example, in the Linux `bash` shell you can run it by wrapping it into `$(\...)`: + +---- +$ $(java -jar $JETTY_HOME/start.jar --dry-run) +---- + +The `--dry-run` option is quite flexible and below you can find a few examples of how to use it to avoid forking a second JVM, or generating scripts or creating an arguments file that can be passed to (a possibly alternative) `java` executable. + +To display the `java` executable used to start Jetty: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar --dry-run=##java## +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +args=--dry-run=java +.... + +To display the JVM options: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar --dry-run=##opts## +---- + +[jetty%nowrap] +.... +[jetty] +setupModules=code:example$jetty-modules/jvm.mod +setupArgs=--add-modules=http,jvm +args=--dry-run=opts +replace=( ),$1\\\n +.... + +To display the JVM class-path: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar --dry-run=##path## +---- + +[jetty%nowrap] +.... +[jetty] +setupModules=code:example$jetty-modules/postgresql.mod +setupArgs=--add-modules=http,jvm +args=--dry-run=path +replace=( |:),$1\\\n +.... + +To display the JVM class-path and module-path, if you want to xref:start/start-jpms.adoc[start Jetty using JPMS] with the `--jpms` option: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar ##--jpms## --dry-run=##path## +---- + +[jetty%nowrap] +.... +[jetty] +setupModules=code:example$jetty-modules/postgresql.mod +setupArgs=--add-modules=http,jvm +args=--jpms --dry-run=path +replace=( |:),$1\\\n +.... + +To display the JVM main class: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar --dry-run=##main## +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +args=--dry-run=main +.... + +To display the JVM main class when xref:start/start-jpms.adoc[starting Jetty using JPMS]: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar --jpms --dry-run=##main## +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +args=--jpms --dry-run=main +.... + +The main class is typically Jetty's `XmlConfiguration` class that accepts, as program arguments, a list of properties and a list of Jetty XML files to process. +The Jetty XML files compose together the Jetty components that are then configured with the values from the command line properties. + +To display the program arguments passed to the main class: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar --dry-run=##args## +---- + +[jetty%nowrap] +.... +[jetty] +setupModules=code:example$jetty-modules/postgresql.mod +setupArgs=--add-modules=http +args=--dry-run=args +replace=( ),$1\\\n +.... + +Note how the program arguments are a list of properties in the form `=` and a list of Jetty XML files. + +The various parts of the full JVM command line can be combined to leverage the arguments file feature (that is, specify the JVM options in a file rather than on the command line) that is built-in in the `java` executable: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar --dry-run=##opts,path,main,args## > /tmp/jvm_cmd_line.txt +$ /some/other/java @/tmp/jvm_cmd_line.txt +---- + +Using `--dry-run=opts,path,main,args` can be used to avoid that the Jetty start mechanism forks a second JVM when using modules that require forking: + +---- +$ java $(java -jar $JETTY_HOME/start.jar --dry-run=opts,path,main,args) +---- + +The output of different `--dry-run` executions can be creatively combined in a shell script: + +[source,subs=+quotes] +---- +$ OPTS=$(java -jar start.jar --dry-run=##opts,path##) +$ MAIN=$(java -jar start.jar --dry-run=##main##) +$ ARGS=$(java -jar start.jar --dry-run=##args##) +$ java $OPTS -Dextra=opt $MAIN $ARGS extraProp=value extra.xml +---- + +[[start]] +== Starting Jetty + +After you have configured the `$JETTY_BASE` directory, as explained in <>, you can start Jetty as a standalone server. + +In the _start_ mode, the Jetty start mechanism computes a JVM command line with JVM options, system properties, class-path, module-path, main class and program arguments, and then executes it, forking a new JVM if necessary. + +The Jetty start mechanism performs these steps: + +. Loads all the Jetty modules files (that have extension `+*.mod+`) from the `modules/` subdirectory of each configuration source directory (see <> for the list of configuration sources). +In this way, a Jetty module graph can be built in memory, where the module dependencies form the edges of the graph and each node contains the metadata information declared by each module (for example, the libraries that it needs, the XML files to process, and so on), in preparation for the next step. +. Reads the Jetty module configuration files (that have extension `+*.ini+`) from the `start.d/` subdirectory of each configuration source directory and from the command line. +This step produces a list of _enabled_ modules; for each enabled module all its dependencies are transitively resolved by navigating the graph built in the previous steps. +. Processes the list of enabled (explicitly and transitively) modules, gathering the list of libraries to add to the class-path, the JPMS directives to add to the command line, the properties and XML files to add as program arguments, etc., so that a full JVM command line can be generated. +. Executes the command line, either in-JVM or by forking a second JVM (if the `--exec` option is present or implied by other options such as `--jpms`), and waits for the JVM, or the forked JVM, to exit. + +[[start-class-path]] +=== Server Class-Path + +When the Jetty server is started in-JVM, the server class-path gathered by processing the enabled modules is organized in a `URLClassLoader`, the Jetty Start ClassLoader, that is a child of the System ClassLoader: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam roundCorner 10 + +rectangle "Jetty Start JVM" { + rectangle "System ClassLoader" as system + rectangle "Jetty Start ClassLoader" as start + + note right of system: start.jar + note right of start: jetty-server.jar\njetty-http.jar\njetty-io.jar\njetty-util.jar\njetty-xml.jar\netc. + + system <-- start +} +---- + +The System ClassLoader only has `$JETTY_HOME/start.jar` in its class-path, since the JVM was started with `java -jar $JETTY_HOME/start.jar`. +The Jetty Start ClassLoader has in its class-path the `+*.jar+` files gathered by processing the enabled modules, typically from `+$JETTY_HOME/lib/jetty-*.jar+`, but possibly also from `+$JETTY_BASE/lib/*.jar+` if custom modules extend the server class-path with their own `+*.jar+` files. + +When the Jetty server is started in a forked JVM, there will be two JVMs: one started by you with `java -jar $JETTY_HOME/start.jar` and one forked by the Jetty start mechanism. +In the forked JVM, the System ClassLoader has the server class-path and/or module-path in its class-path, since the forked JVM is started with `+java --class-path $JETTY_HOME/lib/jetty-server-.jar:...+`: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam roundCorner 10 + +rectangle "Jetty Start JVM" as startJVM { + rectangle "System ClassLoader" as system1 + note right of system1: start.jar +} + +rectangle "Forked JVM" as forkedJVM { + rectangle "System ClassLoader" as system2 + + note right of system2: jetty-server.jar\njetty-http.jar\njetty-io.jar\njetty-util.jar\njetty-xml.jar\netc. +} + +startJVM --> forkedJVM: " waits for" +---- + +It is worth mentioning that there are two standard Jetty modules that allow you to easily add entries to the Jetty server class-path: + +* The xref:modules/standard.adoc#resources[`resources` module], which adds the `$JETTY_BASE/resources` directory to the server class-path. +This is useful if you have third party libraries that lookup resources from the class-path: just put those resources in the `$JETTY_BASE/resources/` directory. + +Logging libraries often perform class-path lookup of their configuration files (for example, `log4j.properties`, `log4j.xml`, `logging.properties`, and `logback.xml`), so `$JETTY_BASE/resources/` is the ideal place to add those files. + +* The the `ext` module, that adds all the `+*.jar+` files under the `$JETTY_BASE/lib/ext/` directory, and subdirectories recursively, to the server class-path. + ++ +[CAUTION] +==== +On one hand, the `ext` module provides a handy place to put third party libraries and their dependencies; on the other hand, the `$JETTY_BASE/lib/ext/` directory may become a confused mixture of many `+*.jar+` files from different third party libraries. + +Prefer to group third party libraries and their dependencies into their own directories using xref:modules/custom.adoc[custom modules], or at least group them into `$JETTY_BASE/lib/ext/` subdirectories such as `$JETTY_BASE/lib/ext/util/` or `$JETTY_BASE/lib/ext/acme/`. +==== + +[[start-xml]] +=== Assembling Jetty Components + +The Jetty start mechanism eventually invokes, by default, main class `org.eclipse.jetty.xml.XmlConfiguration`, passing properties and xref:xml/index.adoc[Jetty XML files] as program arguments. + +The Jetty XML files are nothing more than Java code in XML format. + +The XML files are processed to instantiate Jetty components such as `org.eclipse.jetty.server.Server` or `org.eclipse.jetty.util.ssl.SslContextFactory$Server`. +The components are then assembled together to provide the configured Jetty features. + +The Jetty XML files are parametrized using properties, and a property is just a name/value pair. + +This parametrization of the XML files allows an XML file that resides in `$JETTY_HOME/etc/` to _declare_ a property such as `jetty.http.port`, and allow this property to be set in a `$JETTY_BASE/start.d/http.ini` file, so that you don't need to change the XML files in `$JETTY_HOME`, but only change files in your `$JETTY_BASE`. + +You can write your own xref:modules/custom.adoc[custom modules] with your own Jetty XML files, and your own properties, to further customize Jetty. + +[[stop]] +== Stopping Jetty + +When Jetty is started, the Jetty components that you have configured by enabling Jetty modules are assembled and started. + +If you have started Jetty from a terminal, you can exit the Jetty JVM by hitting kbd:[Ctrl+C] on the same terminal. + +Similarly, from a different terminal, you can exit the Jetty JVM using `kill -INT ` or `kill -TERM `. + +In the three cases above, the JVM is exited, but by default Jetty components are not stopped. +If you want to stop the Jetty components, to stop Jetty more gracefully, you can start Jetty with this property: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar ##jetty.server.stopAtShutdown=true## +---- + +This property can also be set in `$JETTY_BASE/start.d/server.ini` so that it is persistently configured across Jetty restarts (see also xref:modules/standard.adoc#server[the `server` module]). + +The `jetty.server.stopAtShutdown` property configures a JVM shutdown hook that is run, stopping the `Server` instance, when the JVM exits. + +Obviously, the JVM can also be stopped with `kill -KILL ` that exits the process abruptly without running the JVM shutdown hooks. + +[[stop-remote]] +=== Stopping Jetty from Remote + +You can configure a Jetty server so that it can be stopped by remote clients using a command sent through a TCP socket. + +You can start Jetty with the following properties: + +* `stop.host`, the host name Jetty will bind to listen for stop commands. Defaults to `127.0.0.1` which means that the stop command can be issued only clients that run on the same host as Jetty. +* `stop.port`, the port number Jetty will listen to for stop commands. Defaults to `-1`, which means that Jetty will not listen to any port. +* `stop.key`, the password to verify when a stop command is received. Defaults to a password that is randomly generated and printed when Jetty starts. + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar ##stop.port=8181## +---- + +[jetty%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +args=stop.port=8181 +highlight=(?i)stop.key +.... + +In the example above, Jetty is started with just the `stop.port` property, and the `stop.key` is printed on the terminal when Jetty starts. + +CAUTION: You can choose your own `stop.key`, but make sure it's a strong password. + +A remote client can now use the Jetty start mechanism to stop the remote Jetty server: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar ##--stop## stop.port=8181 stop.key= +---- + +Note the `--stop` command along with the `stop.port` and `stop.key` properties. +The `stop.key` must be the same as the one of remote Jetty server, either the one you chose, or the one printed on the terminal when Jetty starts. + +Remote clients can wait for the remote Jetty server to shut down by specifying the `stop.wait` property with the number of seconds to wait: + +---- +$ java -jar $JETTY_HOME/start.jar --stop stop.port=8181 stop.key= stop.wait=15 +---- + +If the time specified elapses, without the confirmation that the remote Jetty server stopped, then the `--stop` command exits with a non-zero return code. + +== Start Mechanism Logging + +The steps performed by the Jetty start mechanism are logged by the `StartLog` class, that outputs directly, by default, to `System.err`. + +This is necessary to avoid that the Jetty start mechanism depend on logging libraries that may clash with those defined by Jetty logging modules, when Jetty is started in-VM. + +[NOTE] +==== +This section is about the logging performed by the Jetty start mechanism _before_ it configures and starts Jetty. +See the xref:server/index.adoc#logging[logging section] for information about logging when Jetty starts. +==== + +You can enable DEBUG level logging with the `--debug` command line option, for both the _tool_ and _start_ modes: + +---- +$ java -jar $JETTY_HOME/start.jar --debug ... +---- + +You can send the start log output to a file, by default relative to `$JETTY_BASE`, with the `--start-log-file=` option: + +---- +$ java -jar $JETTY_HOME/start.jar --debug --start-log-file=start.log ... +---- + +This is useful for capturing startup issues where the Jetty-specific logger has not yet kicked in due to a possible startup configuration error. + +[[reference]] +== Usage Reference + +[jetty] +.... +[jetty] +args=--help +.... diff --git a/documentation/jetty/modules/operations-guide/pages/start/start-jpms.adoc b/documentation/jetty/modules/operations-guide/pages/start/start-jpms.adoc new file mode 100644 index 000000000000..b115238ad864 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/start/start-jpms.adoc @@ -0,0 +1,96 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Starting Jetty using JPMS + +Jetty modules are proper https://en.wikipedia.org/wiki/Java_Platform_Module_System[JPMS] modules: each Jetty module has a `module-info.class` file. +This makes possible to run Jetty from the module-path, rather than the class-path. + +To start Jetty on the module-path rather than the class-path, it is enough to add the `--jpms` option to the command line, for example: + +---- +$ java -jar $JETTY_HOME/start.jar --jpms +---- + +[NOTE] +==== +The `--jpms` option implies the `--exec` option. + +When running on the module-path using the `--jpms` option, the Jetty start mechanism will fork a second JVM passing it the right JVM options to run on the module-path. + +Therefore, you will have two JVMs running: one that runs `start.jar` and one that runs Jetty on the module-path. + +Forking a second JVM may be avoided as explained in xref:start/index.adoc#configure-dry-run[this section]. +==== + +When Jetty is started in JPMS mode, all JPMS modules in the module-path are added to the set of JPMS _root modules_ through the JVM option `--add-modules ALL_MODULE_PATH`. + +For a `+*.jar+` file that is not a JPMS module, but is on the module-path, the JVM will assume internally it is an automatic JPMS module, with a JPMS module name derived from the `+*.jar+` file name. + +Rather than adding the `--jpms` option to the command line, you can use a custom Jetty module to centralize your JPMS configuration, where you can specify additional JPMS directives. + +Create the `$JETTY_BASE/modules/jpms.mod` file: + +.jpms.mod +---- +include::code:example$jetty-modules/jpms.mod[] +---- + +The `[ini]` section with `--jpms` is equivalent to passing the `--jpms` option to the command line (see also xref:modules/index.adoc#directive-ini[this section]). + +The `[jpms]` section allows you to specify additional JPMS configuration, for example additional `--add-modules` options, or `--add-opens` options, etc. (see also xref:modules/index.adoc#directive-jpms[this section]). + +Then enable it: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=jpms +---- + +Now you can start Jetty without extra command line options, and it will start in JPMS mode because you have enabled the `jpms` module. + +[[advanced]] +== Advanced JPMS Configuration + +Web applications may need additional services from the Servlet Container, such as JDBC `DataSource` references or JTA `UserTransaction` references. + +For example, for JDBC it is typical to store, in JNDI, a reference to the connection pool's `DataSource` or directly a reference to the JDBC driver's `DataSource` (for example, `org.postgresql.ds.PGConnectionPoolDataSource`). +Jetty needs to be able to instantiate those classes and therefore needs to be able to load those classes and all their super-classes, among which includes `javax.sql.DataSource`. + +When Jetty runs on the class-path, this is easily achieved by using a xref:modules/custom.adoc[custom module] as explained in xref:start/index.adoc#configure-custom-module[this section]. + +However, when running on the module-path, things are quite different. + +When Jetty tries to load, for example, class `org.postgresql.ds.PGConnectionPoolDataSource`, it must be in a JPMS module that is resolved in the run-time module graph. +Furthermore, any dependency, for example classes from the `java.sql` JPMS module, must also be in a module present in the resolved module graph. + +Thanks to the fact that when Jetty starts in JPMS mode the `--add-modules ALL_MODULE_PATH` option is added to the JVM command line, every `+*.jar+` file in the module-path is also present in the module graph. + +There are now two cases for the `postgresql-.jar` file: either it is a proper JPMS module, or it is an automatic JPMS module (either an explicit automatic JPMS module with the `Automatic-Module-Name` attribute in the manifest, or an implicit automatic JPMS module whose name is derived from the `+*.jar+` file name). + +If the `postgresql-.jar` file is a proper JPMS module, then there is nothing more that you should do: the `postgresql-.jar` file is in the module-path, and all the modules in the module-path are in the module graph, and any dependency declared in the `module-info.class` will be added to the module graph. + +Otherwise, `postgresql-.jar` file is an automatic module, and will likely have a dependency on the JDK-bundled `java.sql` JPMS module. +However, the `java.sql` JPMS module is not in the module graph, because automatic modules do not have a way to declare their dependencies. + +For this reason, you have to manually add the `java.sql` dependency to the module graph. +Using the `postgresql.mod` introduced in xref:start/index.adoc#configure-custom-module[this section] as an example, modify your custom module in the following way: + +.postgresql.mod +---- +... + +[jpms] +add-modules: java.sql +---- + +The `[jpms]` section is only used when Jetty is started on the module-path. diff --git a/documentation/jetty/modules/operations-guide/pages/tools/index.adoc b/documentation/jetty/modules/operations-guide/pages/tools/index.adoc new file mode 100644 index 000000000000..632bba21cf4a --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/tools/index.adoc @@ -0,0 +1,80 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Tools + +[[password]] +== Password Obfuscation + +There are many cases where you might need to provide credentials such as usernames and passwords to authenticate your access to certain services, for example KeyStore and TrustStore passwords, JDBC credentials, Basic or Digest authentication credentials, etc. + +Passwords are typically stored in clear-text in configuration files, because a program such as Jetty reading the configuration file must be able to retrieve the original password to authenticate with the service. + +You can protect clear-text stored passwords from _casual view_ by obfuscating them using class link:{javadoc-url}/org/eclipse/jetty/util/security/Password.html[`org.eclipse.jetty.util.security.Password`]: + +[,bash,subs=attributes+] +---- +$ java -cp jetty-util-{version}.jar org.eclipse.jetty.util.security.Password --prompt +Username: <1> +Password: secret <2> +OBF:1yta1t331v8w1v9q1t331ytc <3> +MD5:5eBe2294EcD0E0F08eAb7690D2A6Ee69 <4> +---- +<1> Hit kbd:[Enter] to specify a blank user. +<2> Enter the password you want to obfuscate. +<3> The obfuscated password. +<4> The MD5 checksum of the password. + +The `Password` tool produced an obfuscated string for the password `secret`, namely `OBF:1yta1t331v8w1v9q1t331ytc` (the prefix `OBF:` must be retained). +The obfuscated string can be de-obfuscated to obtain the original password. + +Now you can use the obfuscated password in Jetty configuration files, for example to specify the KeyStore password in `ssl.ini` when configuring secure connectors, as explained xref:protocols/index.adoc#ssl-customize[here]. +For example: + +.ssl.ini +[,properties] +---- +jetty.sslContext.keyStorePassword=OBF:1yta1t331v8w1v9q1t331ytc +---- + +CAUTION: Remember that password obfuscation only protects from _casual view_ -- it can be de-obfuscated to obtain the original password. + +TIP: You can also use the obfuscated password in your Java source code. + +You can also use obfuscated passwords in Jetty XML files where a clear-text password is usually required. +Here is an example, setting an obfuscated password for a JDBC `DataSource`: + +[,xml,subs=attributes+] +---- + + + jdbc/myDS + + + + + org.postgresql.ds.PGSimpleDataSource + dbuser + + + OBF:1yta1t331v8w1v9q1t331ytc + + + ... + + + + + +---- +<1> Note the usage of `Password.deobfuscate(\...)` to avoid storing the clear-text password in the XML file. diff --git a/documentation/jetty/modules/operations-guide/pages/troubleshooting/index.adoc b/documentation/jetty/modules/operations-guide/pages/troubleshooting/index.adoc new file mode 100644 index 000000000000..ccc662b29135 --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/troubleshooting/index.adoc @@ -0,0 +1,246 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Troubleshooting + +To troubleshoot Jetty when used as a standalone server, there are two main tools: the Jetty Server Dump and enabling DEBUG level logging. + +Jetty is based on components organized as a tree, with the `Server` instance at the root of the tree. + +As explained in the xref:jmx/index.adoc[JMX section], these components can be exported as JMX MBeans and therefore be accessible from JMX Consoles such as Java Missions Control (JMC). + +Being able to take a snapshot of the state of Jetty while it is running is the most useful information that can be attached when reporting an issue. +Such state includes: + +* The thread pool configuration and its current state, including how many threads are in use, and their stack trace. +* The TLS configuration. +* The I/O configuration and its current state, including the ports Jetty listens to, how many connections are currently open, and he state of each connection, and the state of the request/response handling for each connection. +* The `Handler` structure and its configuration. +* The web applications deployed and their configurations, including the class loader information. + +The prerequisite for troubleshooting is to enable JMX, so that Jetty -- possibly a production server -- can be accessed from a remote location to obtain the information exported via JMX, and possibly be able to reconfigure Jetty to solve the issue. + +IMPORTANT: Make sure you read about how to secure the access to Jetty when using xref:jmx/index.adoc#remote[remote JMX]. + +[[dump]] +== Server Dump + +The Jetty Server Dump is obtained by invoking, via JMX, the `Server.dump()` operation, as shown below using https://adoptium.net/jmc.html[Java Mission Control (JMC)]: + +image::jmc-server-dump.png[] + +Find the `Server` MBean in the MBean Tree, under `org.eclipse.jetty.server:type=server,id=0`. +Then click on the "Operations" tab, select the `dump()` operation, and then click the `Execute` button. +In the bottom panel you will see the result of the invocation, that you can copy into a text editor and save to your file system. + +[CAUTION] +==== +Taking a Jetty Server Dump is a relatively expensive operation, as it dumps the state of all connections (which can be thousands), and the state of all threads. + +The result of the invocation may produce a large string, possibly few MiB, that may impact the server memory usage. + +Furthermore, dumping the state of the I/O Jetty components takes a little CPU time off the handling of the actual I/O, possibly slowing it down temporarily. + +While the slow-down caused by taking the Jetty Server Dump may be noticeable on highly loaded systems, it is typically a very small price to pay to obtain the information about the Jetty state that may be critical to the resolution of an issue. +==== + +[NOTE] +==== +The format of the Jetty Server Dump output is subject to change at any time, as Jetty developers modify the Jetty code and decide to include more state, or remove state that is no longer relevant. + +The Jetty Server Dump is organized in a tree whose structure is similar to the runtime Jetty component tree. + +At the end of the dump output there is a legend that explains the type of tree node: whether it is a node that represent a _managed_ component, or an _array_ node (or a _map_ node) that represent some component state, etc. +==== + +[[dump-start-stop]] +=== Dump at Server Start/Stop + +The `Server.dump()` operation may also be invoked just after the `Server` starts (to log the state of the freshly started server), and just before the `Server` stops (which may be useful to log the state of server that is not working properly). + +You can temporarily enable the Jetty Server Dump at start time by overriding the `jetty.server.dumpAfterStart` property on the command line: + +[source,subs=+quotes] +---- +$ java -jar $JETTY_HOME/start.jar *jetty.server.dumpAfterStart=true* +---- + +To make this change persistent across server restarts, see the xref:modules/standard.adoc#server[`server` module] configuration for more information about how to configure the server to dump at start/stop time. + +[[dump-detailed]] +=== Detailed ThreadPool Information + +By default, the dump of the thread pool will only dump the topmost stack frame of each thread. +It is possible to configure the thread pool to dump the whole stack trace for each thread; while this may be a little more expensive, it provides complete information about the state of each thread, which may be important to diagnose the issue. + +See the xref:modules/standard.adoc#threadpool[`threadpool` module] configuration for more information about how to configure the thread pool to dump detailed thread information. + +Detailed thread pool information can also be turned on/off on-the-fly via JMX, by finding the `ThreadPool` MBean under `org.eclipse.jetty.util.thread:type=queuedthreadpool,id=0`, then selecting the `detailedDump` attribute and setting it to `true`. You can now perform the `Server.dump()` operation as explained above, and then set `detailedDump` back to `false`. + +[[dump-example]] +=== Dump Example + +Below you can find a simple example of a Jetty Server Dump, with annotations for the principal components: + +[jetty.small%nowrap] +.... +[jetty] +setupArgs=--add-modules=http +args=jetty.http.selectors=1 jetty.http.acceptors=1 jetty.threadPool.minThreads=4 jetty.server.dumpAfterStart=true +delete=^[0-9]\{4} +callouts= <$N>,Server@,= QueuedThreadPool,HandlerCollection@,= ServerConnector,ManagedSelector@,keys @,startJarLoader@,unmanaged +.... +ifdef::run-jetty[] +<1> The `Server` instance at the root of the tree +<2> The thread pool component +<3> The root of the `Handler` structure +<4> The connector listening on port `8080` for the HTTP/1.1 protocol +<5> A selector component that manages connections +<6> The connections currently managed by the selector component +<7> The server `ClassLoader` and its classpath +<8> The legend for the dump nodes +endif::[] + +[[logging]] +== Enabling DEBUG Logging + +Enabling DEBUG level logging for the `org.eclipse.jetty` logger name provides the maximum amount of information to troubleshoot Jetty issues. + +Refer to the xref:server/index.adoc#logging[logging section] for more information about how to configure logging in Jetty. + +[CAUTION] +==== +Enabling DEBUG level logging for `org.eclipse.jetty` is very, *very* expensive. + +Your server could be slowed down to almost a halt, especially if it is under heavy load. +Furthermore, the log file could quickly fill up the entire filesystem (unless configured to roll over), so you want to be really careful using DEBUG logging. + +For production servers, consider using the <> first, and enable DEBUG logging only as a last resort. +==== + +However, sometimes issues are such that only DEBUG logging can really tell what's going on in the system, and enabling DEBUG logging is your best chance to figure the issue out. +Below you can find few suggestions that can help you reduce the impact when you have to enable DEBUG logging. + +[[logging-backend]] +=== Jetty Behind a Load Balancer + +If Jetty instances are behind a load balancer, you may configure the load balancer to send less load to a particular Jetty instance, and enable DEBUG logging in that instance only. + +[[logging-jmx]] +=== Enabling DEBUG Logging for a Short Time + +In certain cases the issue can be reproduced reliably, but only in the production environment. + +You can use JMX to temporarily enable DEBUG logging, reproduce the issue, and then disable DEBUG logging. + +Alternatively, if you cannot reliably reproduce the issue, but you _know_ it is happening, you can temporarily enable DEBUG logging for a small period of time, let's say 10-60 seconds, and then disable DEBUG logging. + +Changing the log level at runtime is a feature of the logging implementation that you are using. + +The Jetty SLF4J implementation, used by default, exposes via JMX method `boolean JettyLoggerFactoryMBean.setLoggerLevel(String loggerName, String levelName)` that you can invoke via a JMX console to change the level for the specified logger name. +The method returns `true` if the logger level was successfully changed. + +For example, you can pass the string `org.eclipse.jetty` as the first parameter, and the string `DEBUG` (upper case) as the second parameter. +You can then use the string `INFO` or `WARN` (upper case) to restore the logging level to its previous value. + +[[logging-subpackages]] +=== Enabling DEBUG Logging for SubPackages + +Enabling DEBUG logging for the `org.eclipse.jetty` logger name implies that all children logger names, recursively, inherit the DEBUG level. + +Processing a single HTTP request involves many Jetty components: the I/O subsystem (under `org.eclipse.jetty.io`), the thread pool (under `org.eclipse.jetty.util`), the HTTP/1.1 parsing (under `org.eclipse.jetty.http`), etc. + +If you can cut the amount of DEBUG logging to just what you need to troubleshoot the issue, the impact of enabling DEBUG logging will be much less than enabling it for all Jetty components. + +For example, if you need to troubleshoot a client that sends bad HTTP/1.1 requests, it may be enough to enable only the `org.eclipse.jetty.http` logger name, therefore saving the large amount of DEBUG logging produced by the I/O subsystem and by the thread pool. + +In another case, you may need to troubleshoot only HTTP/2 requests, and therefore enabling only the `org.eclipse.jetty.http2` logger name could be enough. + +[[debugging]] +== Remote Debugging + +The Java Virtual Machines allows remote processes on different hosts to connect for debugging purposes, by using specific command line options. + +[CAUTION] +==== +While it is possible to enable remote debugging on a Jetty server, it is typically not recommended for security and performance reasons. +Only enable remote debugging on a Jetty server as a last resort to troubleshoot issues that could not be troubleshot otherwise. +==== + +You can easily create a custom Jetty module (see xref:modules/custom.adoc[this section]) with the following content: + +.remote-debug.mod +---- +include::code:example$jetty-modules/remote-debug.mod[] +---- + +The `[exec]` directive (documented xref:modules/index.adoc#directive-exec[here]) is necessary to pass the `-agentlib:jdwp` JVM option to the forked JVM that runs Jetty, so that you can attach with a debugger. + +[NOTE] +==== +The `address` parameter of the `-agentlib:jdwp` command line option specifies the network address and port the Jetty JVM listens on for remote debugging. + +Please refer to the https://docs.oracle.com/en/java/javase/17/docs/specs/jpda/conninv.html[Java Debug Wire Protocol documentation] for additional information about the `-agentlib:jdwp` command line option and its parameters. +==== + +You can now enable the `remote-debug` Jetty module with the following command issued from the `$JETTY_BASE` directory: + +---- +$ java -jar $JETTY_HOME/start.jar --add-modules=server,remote-debug +---- + +The command above minimally adds a Jetty server without connectors (via the `server` Jetty module) and the `remote-debug` Jetty module, and produces the following `$JETTY_BASE` directory structure: + +[source,subs=+quotes] +---- +$JETTY_BASE +├── modules +│ └── remote-debug.mod +├── resources +│ └── jetty-logging.properties +└── start.d + ├── ##remote-debug.ini## + └── server.ini +---- + +You can easily disable the `remote-debug` Jetty module as explained in xref:start/index.adoc#configure-disable[this section]. + +Alternatively, you can enable the `remote-debug` module on the command line, as explained in xref:start/index.adoc#configure-enable-command-line[this section]. + +Starting the Jetty server with the `remote-debug` module enabled yields: + +[jetty%nowrap] +.... +[jetty] +setupModules=code:example$jetty-modules/remote-debug.mod +setupArgs=--add-modules=server,remote-debug +highlight=5005 +.... + +Note how the JVM is listening on port `5005` to allow remote debuggers to connect. + +If you want to avoid to fork a second JVM to pass the `-agentlib:jdwp` JVM option, please read xref:start/index.adoc#configure-dry-run[this section]. + +[[handlers]] +== Troubleshooting Handlers + +[[handlers-state-tracking]] +=== `StateTrackingHandler` + +Jetty's `StateTrackingHandler` (described in xref:modules/standard.adoc#state-tracking[this module]) can be used to troubleshoot problems in web applications. + +`StateTrackingHandler` tracks the usages of `Handler`/`Request`/`Response` asynchronous APIs by web applications, emitting events (logged at warning level) when an invalid usage of the APIs is detected. + +In conjunction with <>, it dumps the state of current requests, detailing whether they have reads or writes that are pending, whether callbacks have been completed, along with thread stack traces (including virtual threads) of operations that have been started but not completed, or are stuck in blocking code. + +You need to enable the `state-tracking` Jetty module, and configure it to track what you are interested in tracking (for more details, see the link:{javadoc-url}/org/eclipse/jetty/server/handler/StateTrackingHandler.html[javadocs]). diff --git a/documentation/jetty/modules/operations-guide/pages/xml/index.adoc b/documentation/jetty/modules/operations-guide/pages/xml/index.adoc new file mode 100644 index 000000000000..933374d0e51f --- /dev/null +++ b/documentation/jetty/modules/operations-guide/pages/xml/index.adoc @@ -0,0 +1,518 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty XML + +// TODO: merge this small section into the syntax and maybe call it "Jetty XML Reference". + +The Jetty XML format is a straightforward mapping of XML elements to Java APIs so that any object can be instantiated and getters, setters, and methods can be called. + +The Jetty XML format is very similar to that of frameworks like Spring or Plexus, although it predates all of them and it's typically more powerful as it can invoke any Java API. + +The Jetty XML format is used in xref:modules/index.adoc[Jetty modules] to create the Jetty server components, as well as in xref:deploy/index.adoc[Jetty XML context files] to configure web applications, but it can be used to call any Java API. + +[[syntax]] +== Jetty XML Syntax + +The Jetty XML syntax defines XML element that allow you to call any Java API and that allow you to interact in a simpler way with the xref:modules/index.adoc[Jetty module system] and the xref:deploy/index.adoc[Jetty deploy system]. + +The Jetty XML elements define attributes such as `id`, `name`, `class`, etc. that may be replaced by correspondent elements, so that these XML documents are equivalent: + +[,xml] +---- + + + + + + + + +---- + +[,xml] +---- + + + + + + stderr + err + java.lang.System + + println + HELLO + + + +---- + +The version using attributes is typically shorter and nicer to read, but sometimes the attribute value cannot be a literal string (for example, it could be the value of a system property) and that's where elements gives you the required flexibility. + +[[syntax-configure]] +=== `` + +Element `Configure` must be the root element of the XML document. + +The following Jetty XML creates an empty `String` and assigns it the id `mystring`: + +[,xml] +---- + + + + +---- + +This is equivalent to the following Java code: + +[,java] +---- +var mystring = new String(); +---- + +If an object with the id `mystring` already exists, then it is not created again but rather just referenced. + +Within element ``, the created object (if any) is in <> and may be the implicit target of other, nested, elements. + +Typically the `` element is used to configure a `Server` instance or `ContextHandler` subclasses such as `WebAppContext` that represent web applications. + +[[syntax-arg]] +=== `` + +Element `Arg` is used to pass arguments to <> and <>. + +The following example creates a minimal Jetty `Server`: + +[,xml] +---- + + + + + 8080 + +---- + +Arguments may have a `type` attribute that explicitly performs <>. + +Arguments may also have a `name` attribute, which is matched with the corresponding Java annotation in the source class, that helps to identify arguments: + +[,xml] +---- + + + + + 8080 + +---- + +[[syntax-new]] +=== `` + +Element `` creates a new object of the type specified by the mandatory `class` attribute. +A sequence of `Arg` elements, that must be contiguous and before other elements, may be present to specify the constructor arguments. + +Within element `` the newly created object is in <> and may be the implicit target of other, nested, elements. + +The following example creates an `ArrayList`: + +[,xml] +---- + + + + + + 16 + + +---- + +This is equivalent to the following Java code: + +[,java] +---- +var mylist = new ArrayList(16); +---- + +[[syntax-call]] +=== `` + +Element `` invokes a method specified by the mandatory `name` attribute. +A sequence of `Arg` elements, that must be contiguous and before other elements, may be present to specify the method arguments. + +Within element `` the return value, if the return type is not `void`, is in <> and may be the implicit target of other, nested, elements. + +[,xml] +---- + + + + + + + 0 + + + + +---- + +This is equivalent to the following Java code: + +[,java] +---- +new ArrayList().listIterator(0).next(); +---- + +It is possible to call `static` methods by specifying the `class` attribute: + +[,xml] +---- + + + + + + jdk.java.net + + +---- + +This is equivalent to the following Java code: + +[,java] +---- +var myhost = InetAddress.getByName("jdk.java.net"); +---- + +The `class` attribute (or `` element) can also be used to specify the Java class or interface to use to lookup the non-``static`` method name. +This is necessary when the object in scope, onto which the `` would be applied, is an instance of a class that is not visible to Jetty classes, or not accessible because it is not `public`. +For example: + +[,xml,subs=+quotes] +---- + + + + + + ## + + +---- + +In the example above, `Executors.newSingleThreadScheduledExecutor()` returns an object whose class is a private JDK implementation class. +Without an explicit `class` attribute (or `` element), it is not possible to invoke the method `shutdown()` when it is obtained via reflection from the private JDK implementation class, because while the method is `public`, the private JDK implementation class is not, therefore this exception is thrown: + +[source] +---- +java.lang.IllegalAccessException: class org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration (in module org.eclipse.jetty.xml) cannot access a member of class java.util.concurrent.Executors$DelegatedExecutorService (in module java.base) with modifiers "public" +---- + +The solution is to explicitly use the `class` attribute (or `` element) of the `` element that is invoking the `shutdown()` method, specifying a publicly accessible class or interface that the object in scope extends or implements (in the example above `java.util.concurrent.ExecutorService`). + +[[syntax-get]] +=== `` + +Element `` retrieves the value of a JavaBean property specified by the mandatory `name` attribute. + +If the JavaBean property is `foo` (or `Foo`), `` first attempts to invoke _method_ `getFoo()` or _method_ `isFoo()`; failing that, attempts to retrieve the value from _field_ `foo` (or `Foo`). + +[,xml] +---- + + + + + + + + + + + Jetty + + + +---- + +The `class` attribute (or `` element) allows to perform `static` calls, or to lookup the getter method from the specified class, as described in the <` section>>. + +[[syntax-set]] +=== `` + +Element `` stores the value of a JavaBean property specified by the mandatory `name` attribute. + +If the JavaBean property is `foo` (or `Foo`), `` first attempts to invoke _method_ `setFoo(...)` with the value in the <> as argument; failing that, attempts to store the value in the scope to _field_ `foo` (or `Foo`). + +[,xml] +---- + + + + + + true + + + + + + +---- + +The `class` attribute (or `` element) allows to perform `static` calls, or to lookup the setter method from the specified class, as described in the <` section>>. + +[[syntax-map]] +=== `` and `` + +Element `` allows the creation of a new `java.util.Map` implementation, specified by the `class` attribute -- by default a `HashMap`. + +The map entries are specified with a sequence of `` elements, each with exactly 2 `` elements, for example: + +[,xml] +---- + + + + + + + host + + + localhost + + + + + +---- + +[[syntax-put]] +=== `` + +Element `` is a convenience element that puts a key/value pair into objects that implement `java.util.Map`. +You can only specify the key value via the `name` attribute, so the key can only be a literal string (for keys that are not literal strings, use the `` element). + +[,xml] +---- + + + + + + + + localhost + + + + +---- + +[[syntax-array]] +=== `` and `` + +Element `` creates a new array, whose component type may be specified by the `type` attribute, or by a `Type` child element. + +[,xml] +---- + + + + + + + literalString + + 1.0D + + + + + +---- + +[[syntax-ref]] +=== `` + +Element `` allows you to reference an object via the `refid` attribute`, putting it into <> so that nested elements can operate on it. +You must give a unique `id` attribute to the objects you want to reference. + +[,xml] +---- + + + + + + + + + + + + + + + Server version is: + + + +---- + +[[syntax-property]] +=== `` + +Element `` retrieves the value of the Jetty module property specified by the `name` attribute, and it is mostly used when creating xref:modules/custom.adoc[custom Jetty modules] or when using xref:deploy/index.adoc#jetty[Jetty context XML files]. + +The `deprecated` attribute allows you to specify a comma separated list of old, deprecated, property names for backward compatibility. + +The `default` attribute allows you to specify a default value for the property, if it has not been explicitly defined. + +For example, you may want to configure the context path of your web application in this way: + +[,xml,subs=+quotes] +---- + + + + + + ## + + /opt/myapps/mywiki.war + +---- + +The `contextPath` value is resolved by looking for the Jetty module property `com.myapps.mywiki.context.path`; if this property is not set, then the default value of `/wiki` is used. + +[[syntax-system-property]] +=== `` + +Element `` retrieves the value of the JVM system property specified by the `name` attribute, via `System.getProperty(...)`. + +The `deprecated` attribute allows you to specify a comma separated list of old, deprecated, system property names for backward compatibility. + +The `default` attribute allows you to specify a default value for the system property value, if it has not been explicitly defined. + +The following example creates a minimal Jetty `Server` that listens on a port specified by the `com.acme.http.port` system property: + +[,xml] +---- + + + + + + + + +---- + +[[syntax-env]] +=== `` + +Element `` retrieves the value of the environment variable specified by the `name` attribute, via `System.getenv(...)`. + +The `deprecated` attribute allows you to specify a comma separated list of old, deprecated, environment variable names for backward compatibility. + +The `default` attribute allows you to specify a default value for the environment variable value, if it has not been explicitly defined. + +The following example creates a minimal Jetty `Server` that listens on a port specified by the `COM_ACME_HTTP_PORT` environment variable: + +[,xml] +---- + + + + + + + + +---- + +[[syntax-types]] +=== Type Coercion + +Elements that have the `type` attribute explicitly perform the type coercion of the string value present in the XML document to the Java type specified by the `type` attribute. + +Supported types are the following: + +* all primitive types and their boxed equivalents, for example `type="int"` but also `type="Integer"` (short form) and `type="java.lang.Integer"` (fully qualified form) +* `java.lang.String`, in both short form and fully qualified form +* `java.net.URL`, in both short form and fully qualified form +* `java.net.InetAddress`, in both short form and fully qualified form + +[[syntax-scope]] +=== Scopes + +Elements that create new objects or that return a value create a _scope_. +Within these elements there may be nested elements that will operate on that scope, i.e. on the new object or returned value. + +The following example illustrates how scopes work: + +[,xml] +---- + + + + + 8080 + + + + 5000 + + + + + + + https + + + + + + + + 256 + + + + + + + + + + + + + + + Server version is: + + + +---- + +// TODO: port the documentation from old_docs/jetty-xml/*.adoc diff --git a/documentation/jetty/modules/programming-guide/images/jmc-server-dump.png b/documentation/jetty/modules/programming-guide/images/jmc-server-dump.png new file mode 100644 index 0000000000000000000000000000000000000000..33cd92938cbe75fb4cfec68b54ab6c1d940bc786 GIT binary patch literal 268442 zcmafa1yEc;^XCJCTS5pBoZtir!QI{6A-KCcNpK17?(Xgc5AF_&yZZtQNAmr@x~r?J zx~aFdZ)Zn(dV0Ehe$x{oFDr(Mgo^|K0IGzzup$7wAq4=Kdj#m$95csk$mk zz*M*Iw9O@_sHjLCXQ7BH>qnLoo0ek-A6E220IY%n2;@kZxms&F#=$hwbak4{B($@$ z14R>R1b3p_cGMr#d(Hi6qTb1B)pv%N?fq%wDb_1t6qgfCh)@x8r?}<=50l;k2qgY* z1&y=5O!5DFLr~#(5OW3oW+ISWBX^6#{O48qX5r%Eg0BVrKc%wuBGTM(qobp3*V@1& z?E;;YDFx~2>8L0ulG|Gq!G9|&L#R0u4Z}ph`1GCU-bO}7W@be{fBtN42H&0Nt4K;q zdp$oj$Hpohe0XNL2ZNmtXAZ>fa)t&Wo)FyDP1~JBzSFk^Vc_|IH4baOS^cyz)Z@ofP#STm0o` zd;Sl;7<;*Z1@7+&Dp^p&XOtIF{M3<9fS$M2{=xZK{n;kY6YJH~+2T*!PfDeM4du9+ z-ii^MV=+_EvL1)2IWZZR4om7Q6VpiqJDx0G z$!@f0hPVaWQ4P}FFwI^Fx$9m7>%0QE0zw?(dUxt!#cB1UaPf{vA?2&Fb3ZlTOHW;d>lRPe+^~?*o03p-D%~Xm&~>!GQO}Y&a_+5drB88i(RX#{Ax$pt96I->cXg$9N7%D6 zS(l}=L^kGzgzAf6Pq_*KD!z%(s|-Ya;+t&N)`*4+8b*>xn1UW6rnhj}Z{Rf-P`07Z zZaPdw_bZ27El%G@=vD*lTP>r~rl0Qe z0FsQ#iSNvVThm(mpjIK>jKD9vl-Q^U4ZKZLxg-NA=ur7yv=wzJ4S3t~phgZGDPLQ1 zGM~*<+QM^<){G(N>r*DuyMrv%e&Sw=%9^S0FfloL*JnO7p3{U=VdhW)RdX1bo;~O4 zn31SnteB69A{Ptt1A=`)sxT>9Hr}(*_aS#(P`}@2HqbIn4f-@Y3Xd;xFzM;x7^}-m z;~sT-O|9@iojEX{@C=k+WUOdMLIV^B4Ri~1NXy5X>{uE zZ6w4VMXk=%g?UuSo?L7LfkBy-UMC`IlMloz)TbufkV8gciQvK0LYLk1@;SzY@M*lc zJE(Bnd*-YmdzKBI3GjQ1AO*Qp9Rh%nYhKNqg&kO)9okf|)qI(VDTcI`3laCCXkt8g zV{)e!uW-$qJk6+E~R49xx;mSh)zpGrGv>kh!OSeF2Eo3ahM`9rN{6+Zz>J0 zp`AvTF-+kX?OdP3E*TQ@rQzDtM7q`OJNlrV3vw;|8lUwn)J0vXJ32O>M%&X(-X50I zOgv+WH5+5S2*g4lHVjFt9cJuOV`SDj5UP9W87uc5RofAeC@OMrd$puFAwT}`uVk2mpv93GMdbzSjAw#s|T zj>aeDVsS6yo3_+<{*+;i>^1f%-G)$+GVHXWI&KvsG>%%5ThMc3+D(r`!@@%o***LD zr4)_KH0t~*c9uP5uv`r7!HGpFoRQ0pJ}yeP%d0;hXi@K@QpP9iuW&l7w%X7r97;?L zenr@=+#M*P4HWqlB3jNxI8NpanS~j_o~3f^=w(t8IW~K+rYB@9ay7xB0>o%3Dr(MH zz_}-WILFlCQqrz3tTV6I9%$~67-DDlaQwNz5ZwlWkddXj%6MZ{XF2m@WJ}G#Spm0S zLED#s=hQpyN5gk7Grx&veF|04?XdxN8|~Ywu_zu&QaA`{TqfV5=(FgSL$i;MPdcYF zr^|6tVj?!M(&j!rJIg{$>^BVI82I&fL1ZqDJJYMF=1pqi>;4SUpPCXkLHjdi+FU={ z(BR4C5o~UVTvyLwz);h_DQLx<)iG}mB86ZO#8@2>u_ZgK%a|E05tmz}IDz-fNO2nD zqpEc;tx2grtu^!D^2Vi7f#K#T#T3K|$?FMc3F{x-!A3C#B4ylYLx zcBMo<$-5B{0oy#9Q@MAn`>k%{HJoJI=Tjo>ZI@5BBesL^gPt{QVwEy6r;d$uPIFlO zg=VwcfIOUx82u{SALX##mFtWSw{5EdwMS30_-97FZ+k4hX+exP7gI|dweVxbH&&LO z&H>gELaY#uotT3ap63&pB>!_1rrdD_jKb%b0P-74!H62^S?YpUQ_(j(O(n1f2XXP2h}YdN=gC-lgSDD zZoaNLg`9%%G$%ErEVA5qelYU2=T#2sCAr z@jJ$&q|ckfW1EOp5-C-}f=c|*&|T})aWW!>K@9`^9_BPV-?4p50p2RqZZV-d!uSro zT$8|}_QcECQ5LHOWHgw|NX3q;&8p>`Apq+`hwgHQ#)i1B3SN}}ECB`9#lGetd=;p)P*t)oHRW3KhmPx$hU3eGBDmR7dYipub}F@9a3At~67~q)=)8 z$2A*rcOZ%vDs@=grFi09-A~>koqcl?agZK@_&I@|r^#}_uS-ie9LK4?V$!5CfP#2T zwScFgXPsnhF%eBql7xS%0>3%xgfmaCoXA6`J&uGy%}HUS8j*xv2@MB*m8?-+Wvv-O zC5?)!wI|#d76#YKU^Y6_iAS%KB~!*1kqHT*7E|Su`{T87MsjMa%5TDf276bgKi(JH z>2G>B!*a54z*Q2}N-H2O ztdj&yRA!O!OL*>U#_Auq!3Mw5ocHYl?B67#kg!5-9NV`y=Q}i>L>!2-ujp zYwt>GdwXxMu&~fqrSjchA03bC^VDEr%;K%~@2oj69C+N7$b8?5!mhNsf4G4tO!wk6 z{Z%XRB8Q`>DwKaGgCSK^f7kQn-2H)S*rRK3&JMDkj3e>mEW1=WDB63pbKH;|;d@eD z8-~QtTNU2c$!;kW5=;>uZ8qA|PQ?7gd}kRMSZSmCM~u(Z;B!{RURD;SobNg2QIdHM z&=_fKS|9R0O~%h&8NL5q&WU7{`?1;e(imHYS_f4DhY_RTT5Ku`^=FXv<36|ZJordW zuJXEu%2zGlj1>=uTFFGK&BM@Jmz#(84~fZ(p3N_UIc~b90Hr?H51JrxB8>MKe35ZG zl)VKC526pu6BGn$+TPfBklmf5!K7uT6Z85&nZp_Z{SGfS+co@GgF2eZ7H0Bre~&`I zd40Jv{Na@TuP5(AX{^IT-cXg>q)$DM2Ae76Vr?PVgve}dZlE=ux+7TT)6($|XVcqYKfZ_3tj8M{lLdm8oOzlGrg<&iCWkQaXt` zY_rc_@3`rC&c@PI84?>9eD!^Px)6G>+2Xe^l-rEsW-?^Q^y+XqaEO*4L>1s-mgkpC zVDR>bcD3CzbZHY}JJdMh_!)HKy$A}Cfwt%y0boMEzoz|a_k%XG2uvz0Ep8kNlac+K z<+(+Hs*}@n@nR`DEI@n>b}j4!JNt6gcWb3iG~35ju6G!T3~F*aLEP@6%**VycAu@0 za8NC+Tb$BE<0i2HAm3zlx01yg?j~*LwAuRQXUJGN)pvd%**saALnxxJ%A%zS$7r?b z1MTs8H_TuM_PM+j$KcLYFS@44cuoP;$LK#${f$& zxw^dc^72BW3?4pp4*O^3mLi*n>HL_6u7z33 zEY1nKY|1HmA=}pG7OdaQgWwa#^?9`BiqEY@GFB+fzE~MF&P%z-q+WK2B^ETal!Xt9 zC3;o(U+|cOyUgbSBPp!4++JRHaFz;MmZ-sDBT+J|-;x*A?Z?;Hea1R0Wn!YT4-OTy zgrw5R-hh1Y4G#^T@}XkvU040YCLou=*fn6O(wcx zTzTn-=$$_hmFHH<7q1&WCVtnf5?E{6Z)tV(Dqba*8vCy%DE#L7VNxt?lk zYHDu0v5ywN_(Jh-5n+I?wJXZRNvXf=+p8N4s~-Bx=FV!6v85d4i4m>=?>~Gi27+Ss zsTP=OLlTHJlWsSat#J8|evpR*Tbf067=!iJBm_GY(p78G%E7~j!|o&l_A?;525GMu zmzDERiPmo~cP!qERCZ!OxoKUy<9>M6{P~M!BQ2=)a`-px9cLg?4HUgb(RQq@Ih1{M zF;?}SnnNyA&E4IfVrrm~7>8MB)RT0pI67RG>TT9e3agC_EI2>^Q+WZ0o^CWJ^Z0n_PM)$-da_e}T*#Qb!oQ`j2-n>3qdprU$3K4-8{l`-?Z~izSYoe_GqO z3M$gqJS_|2N(-#jOYLRoN`%v;RHPcz`9&18Hhb0f+So=z#6LLH(c(Gj)*C8%vjY2W zbXr909=~h8cNjPyxgg`ZFH+TBIUdS#I#8KTt@Qmw+p}2k9OK#Gsi8mxa+bq5QCjmf zr+?Q&euNDKHK$^s^u2B}f^(?o6OYc$7*e6>Q;C1+!~LDOA(42up~~YmaLv3F9_Pr} zQ}ny3I=C-_-foQM(2x`^%728ZJm6=QF-EN1j$Zi(DbJ8e^QM-M z?<6e=+3-r}`=$N~F8*S#JL=&c<&nPajbT#YHE44i!KDX_1C5^RhztrPL!o-y=Dj90 zPtiE$^`29x6bsW#e`$P7PBm_YJ+fh8bV!77S^nvJX!Drf%Vc4iOLN?a4h&h0JZ1RX z|63ZSsqh7lvMGvnyjw%0zS{irqVJ^;j&XHrsdcdM2HhAbq!O(nD@)+;5&zo)YIlt1(!T1?zr{PT z|9?Q#{}Z#SENfkPhJJc_@^QlMh}UzQoft=RmHpfL?@9vmI5&LGlHK#`>v_Kcg4ceo zJ!!n@gVf~ycKF(BNk$qYDsLt_00x6W?M(Y@SdBMuUKTrWQeKjM<*f(%nwxXcQ{epeM-OVe-i9tEa6ID z^w}eYw;*}5!^gZ*xjNDHYjp0*ZPB+|; zZ~nWix#O*lL^b6g#N-tZX4RQt7~u(?rekMU*UMrM9B`<9HB~i@9XJ&fX^W{ z#N|q(qh)^4sHS$}jxCBiyR@IbXUyO-diD^n0xMgQTl>Aer8Hm8JN1!a?O?nqrLJZ5 zG<^+Ne;rCy(wS9Ie{98P4b!I{NCO=g#)hZpr|3{}#puB%-K{co*Y_Bii!7(;fN+}4 z)(kckf@jlWzp+>I2O472L|bl`&f@5`MOdBPbWa$7zt+zL>eY*4oj7LQZBuPnl2j4z zC3!h%X7cv_3 z-cA+xf!)0Bz{&+~5{h};1LmircuE!mCW8&{cy?FCO)quR*sm-aa;MQbqi7ZxMac}B zRxEtZUog{!UuV6808t3W@0AxPw4?1rglDQCr36PxW@=83Jz2Au{E!mbLNfO~S#k?Y zrE$l#srQE^+}?C5Fgh&Wbnz5~H{>Wf^^V-UDZ#r ziNVQB5pX`n<5FB)o$E2ZUr9qR<*lx8MXQMn=#P29e~#oFDU4p$y12ikYT@?zP;u5= zz>a<>DlV>`!9_pZt=jJ=co(&jwlXe8CV|e|;4yky>ng~bx9#Xei&uBbWPfVJpta~t zmNYnBTWH;Rb9qwO8$P0qs5ZRNqMr(-wEN52v>z4#Hj+sSyk)oF{<;#(Y%Y6IJ-zV# zNI6+D?K0AdSJ=oJclQ#kNbsn55_5(8?Jh<~O$cM8fZMI9D`Yf}o44r_k6pZT~S%Bg5NSB=`imSK@uXNOS^XuD;Cw?&Rn%H~)XTuZD z6Nzt4Hfxnnlyg~*J+cg1ZD8s3qoO!&O&KlF#L8GT#9!sAC22y^D(x$$2AJ=!A-qQ9Z$)zA_?&B9U})vLAZ+Q^Jxn)>Z_^Mo3j|A4DKQnJ6Op#=H2QWEPduh!mz0M zEEF2@W>2n7wMR3iN4;H>tpiI$7k=EE;4C^029{6X0HkM6RLT~(|BHH z+DZ^#bA4g&s`xqB>lTVBL#OIYo`>yoFh%a4I&g&&V|( z{MK_xX#VO^Zk{811SsN=M2dRCrblPOY<7zW58P&njmAiN33D++x%gVy$vm4{_a2jAH2X7nksJ~X9yWvt z-(KODE&lZ(E~*3cHP6>Tkr(P)D%4x^w6lBFpXmO`$VdpWYkcgjjQcSl$j@i4){n)^ z=9B)e$BG&Ie^v+FNjWcaPC+kJuzqPl-{t(<5o=V2y(oov(q}Arf7O&cav@wB z4^Nn927)tRDw`WJzBl9#j2xMxU|Z)4B= z<3+ZZ=*+I7@cn65ol4^+LjH7_F%K&cdvSg$g&6zJ=`PkLZ$nWjVNb}iI2CX#!^y?RI)iDH!HTx;Uzffi+;C1;&f&7 z_FE|pz;2^&hqX0G&&@uW1eT*&OLf~?BbwVD2dpic>`190!3t}Iq^|rs3`0Wui(Ib7 z13G)bg%TCc^fwmTwAzi66P*xu+^;p!sKK9`@KnNhzKDF5zKtGFW;$E*mG7Vld>e%{ zOcRZb3ZMw(VMqjUYJ|77-yN^~s);1D#)SqjQ)|tWOiL?d=~6r}s za;XBGr~17`U6Nf-)MtL%#|D_A4ClNI{^EQGFO?AI`6l{!?^~2*g?A0O*J;et z2PS&yt$IhL<$IqM2Lt)1b2TK9z$6RfhJsvgH`izQ8{d_MwYdUWNI|&=%GXVpw^4DO z_S>$C8%UpCO@Aj;bR4RPiK{o z-=ZWABNt`h(PK9?yxbmIIb>&@V9>lDc8mGiH9 z+vWKhJuqDGc7J(mBq5JVjbI?ux1n%mq)_iw-p_Z3#lC3CxCiO!TO4Pb%dF1fWRVo% z8K(~4e1nS<8uHtd-P6oQ&BhN}=~F*`94fEgmm0k#h1bl`CW=l$!W___pi<*)W0-QP zE-hpqLq3Batt%8UC!s8Gd3@}JH{aaZ(Xv6;oTufW)<6P;G4DPRSGXB&*3C%6HNEqM zb9+9)rlf9W6;8?&3vKBBTA|+!CkQOG{qPi%$jA6(%M}NzR$Uo4USLVbXD%L(|3WSC z2L+I)>F?(xZWWgpo>7P8N063u2*pGguMS8(eQ-tG#CAR7kwujxvEX@#VD)soIEZb% zJ2i(hexKzwMweOwO%hizKedwx8sovcrQW~xS;Y4awOldjOU{iYfSqz+`xW{m`)_KP z7H?$rcFnyyRB0%=l~w)t`zkUlHd)$^v3`2pZiJtMX4Ka9Sy(BeB+r$U3U=aJyyKPTAXkqic|K8=jz20IKqu6${ zxp(gmKvk%&@_K8Iu!Bj`#bZfHzb5atw6rkiNt&lW2a(>nD14sH+uvB5P23{$S9DC@ z@U)Y3eE;F1k5A^2P=-=ZllnsvGWDkGz2Hpr3VUTD#>QRQ~vw7F&!oDGI3SsE5{^KDtt8|DV?hG$WpYrj z{tk+mjE`}9YR+88qW0h9C_!;8z z1<)iHM!!7w1cfi`^0bigfj;z5o_6oAyaaXVyrLg21)1pr;pMl>vQP&=t?3S zp{jY|zo?6Z*I7Mph+nedK}bD!D3*1Fb{rWu6Drf(Z{7fYtS+`TJxE@3(USRf0`KQF ztwN{sH0W+;zcB(6DK@7!95qONwsLfY-af2-o0j_rNdA8iKMF#}l}K^1@r3$irA6fS zru@?=1dKhZuWu#K|^}4yaw4So1#8(qG#LTNL_^g`<8vzuPcU%!_A$ zD8}WQi45&0j zQFYX|GGQYaFV}^<`+T2^N#&C}Cvy+`;qvercUJE5+;+3d{NTxEO=uRJM<^9wGnt(PCKUmg6D zr6$ffNBsK=iq8}!-*eMnh{Mhn8UG?(rekRbGsQ~!CL0xl+eS1Ra?%$4vptrXdRH(& zVMF@TErG3x**WUN-(w9D<#=8K&EX9@MQsnv(Ka`(vZD!I5mVLUMdubL**z?m09$9G zj1OUbCO-=kYz#)fm+A!F)~HgK;vF?+X7HBjkUz+kTJRS;a&$39fFyrnt=_{Yw6i-v zez)N5rgO?AvFK?@-KBKO>3>)D+g*!!6_OLE4#z45tb9XDCF=;6p9KvkOuDyoBm6|Z z1b^|T3As>`>t-0MJ6}P`?~rxoYCY*9QwtIS*BI1>ww# zLpr`FSULm_9mU`Tv{{Gv^${O@mFf0J2 zaAbKRAp{EG*zb%e;2*3= zpeMAPr_xAMRK%Ii{txG~aBBL8^O5BL{n>tqnes14jf9*!oY^SST68~k=+7Ss35n_| zsz>VYIRD^71c;ZNG>nQTj@iGe5dT-8b(Ww{EgQCkuLb)rt%Blc6bxdMVj!|9vuetx z+d)lGj>$D%OuuOGG={Ge!#_AL3cs!a%KC)(F4q>roEsUTvL zf;mi18yR$Pm}sl_|e#49XA1@TX=yKKt8EY5V7{;zy3TzUZ^ zx5=}d8p9G3KbFDrQPe}{wM9e^Z^>mQXX7D=OYd0}PxO2VuT-2mFb zO4nq?mjxh;kk{+s+T6~?Mb@Pu?Q;uRx3)xW{#^#2Z(X(J4Z*>;`OM?TYjEsOq43MA zJ`hA()63V!8?vNiyBn_%Xe6@M>PAOa`&2w5y-5nH{T{uT*L##xPGwOYo*_=kPcRNZQeBUYhd+J(V^Z}x`o1WY<+$^~B_>T#dUI3- z1~=LA9F}wLl#~@dpN5-}eho`*zI&;ENjp_4zV<^0tY%MLLV9{~LYlRj_>-ATrYw5l5sFWa^|12zcL$HLT9<#uzb z4!dxkL37{x*&3}h9s?{AtOr>x9A7G1`;1=6hz8K zN*z0Mc)NO+@umKXsAPN$jg5iOi+XbIuCIjaR(XBlY%w$kt5Z(y9~l$l#b2+Oe-ZL{ ztap8{88)&$Zn;_S%h87e7UnC+Q?p5=3Ml%G=SU(}*vucu>!Sx6Z|4i*1{nn1OqUZV zDGXh#h(PT^l?R?|C>Sf+omU}w>L<3p&N?eu?c(o<2vKnx@LH`^h?ac!s_)1tsHrAZ z|L6{Y)Oi-_3mW_?3v8zy4tPiaKjzz|ODGu&vrDjMf(%>*O7Lg1K(LdC0hcKrsSH#Q z=|ufiT8HyM+w-W^jSB1`g{-$K{Ln|^=nm~E{%?Gb(Rp_ET84Cg3pO|r)NBYm?2-50 z6)sj$h333zkKBU>f^+HV505NLD+_WAcX$n!x4tr1(=16&&W8>G{66;~=IMvHC6~(A zC(8^d;-3}Up9=CfxIAubKnA^4d~r)v|JLk&q2eD;xpZD{Tp$n?PgOLzEJ89aa)to2 zQyoW5W62HP%%=_pA@Xlp*A`iq%QW<7lQCmD?R`Ahrt3s~uhU2nR)Cp5J=?4eUKkxSa)_Ba^o8J@wQrw5y! zR^?aNwyicJX~oj%+Ae0Fpn^Hh=*u#A%Ez5r=UchmfiC-$UB_=p9H?~#0RS0$i-P%!J>Rf?CVpZ6Rv}>8Tc>up4Y9wVCEm zR;$}Re@)yHCmV8Hqp#C>7YfoQk{`+eK|Xj*nN(x#pK~*GDIq~HuR8gR|I}4}k;XSs z%lJnGh9Jm))$_dR+`3s!4oYU2$WbBgr#2YD^|-w z`U_%H<({gWJJTh7tc0b}-XV+R`|LARmK#iszqC^4t*z{N7A|4iXvFM3p3O}>IA)&V z+m1t;&F-13p0-ZmKxX@rBUH-olb%%=7}C7c zTVq-OteF!Y?qy~Zq0)o`q6${7?K|b;10;a>ONmNlMt#{8+0AiFI^#*>ispn?B(zx- z`C-jTOzihmt_C%q)geK}F>79&t(sonfj6IV0ZlXbv%6L;m+5r36f^>E2M0w(8ZVDt zqXNlKf=#lgD$}0+J~TO|jSH$bNH4hqJ+BsY!c}RLt4Le#h8vY$ZDahz{GILByy_IN zhT_jakHFm;>Bpsp+j?s#DgYWF;uRj~?e~^}$0U6-)VvTfb*owHyV=DjHZzAS2~%WN zvcu#-cim~>QOkaPQHy_dN;oINR!Vj(VOQbqihlH?WU+zpv>*&Z7$oJvMn5N(P@T*e zpB@*7+y(m$FB!M{+5aKgau|K3K>;-EvgwTLMu9o9`hLX94+!UDoR) zUKqq&Zvb@oS4#qtIylR6P~n}z`SQ&#$9)4Vfk-V;>8zp-L_KflkE|~L;(`AFPLs!! zvi@;4Obt2=v8wxC4Mo;ZDT%E5g^!mt{_7W3Dj!1WR2N;xqpi)n$ctvL)s>$Fc(hFR z&i5^1A2CGNnz$D#k0PfJ6&i6aFTn5rYo^65S4eEVRZ zXD&f&?Y&4M|35|am~RM1@+rX1EVs0U-E2a)t%P~K5Yi=& zdF1nBf->}^>U4AG*(3gle^FgkCKbm-xAQ`Jp_&?xZYcvrvCMo-<>SukVp4bQ;E5C~=(5uKWSY~m;v}$quEVwlD>lLJKY=kjX#>bm8 zfKmP}bh(}Vh6Z|2eE1u$9N2VuuCiwt=SkIqrAW8ho(hrw;$GaRPM)wqM@Z~6S4!Feu7o!W}n6SPunYR2N|oM1_$( zhF47$^IT}YhTE>Z5bojEo}3|aL7>NM?;S6r+Y;W3u^C~1h9M)RVc+j>=8+0yvE_Uy zm|gifyj+x6RQQ9coiJ8RgGX=4a(^f{(Ppj1<+iJNU0bqsxz7HUA>r=Dau}S-=FJA- z_$DHrj;PdXrfM@A_q$?8JM-mse`s{iR{L(%_t=MK|0aofiqlD^Q(zeFVYSH_(w!ag zvN}!%)Kzq#g|ygT4lPS7UOU5j3YxoS@^;AX59vY@@msvEZX-UscGpU5utqO4kFWhs zSh-p}xjTfw=Qj%Ld)6p@MtWbMN3FBExuV}ce0g`$EyOQ+yF3+n%1t5tx@Ui&Q{1@w zqSa@BA)-m!%V>lEiTW}03M)G^;_-NKo|qT_IpHZ;>*3g=1jBipcWvE#_334x~7 zLNd$AvvS4ZF_Ht>^D&skiaW9C+P-|+BH~4(Jvr=7=CGapJ@e?%o$jJzRVkb`huL%7 zmG4Q3Ih23hlX-o|Ogy>&QVU?k66 zygTp~KQ||8#r0|k=^uDIIIHS>w)+g0Z*?1CwI0))?DN-lRwB5gCW3BL_3t?R$9%so=U%1AD_UOe zV29yWB^N<_(I9%?qzx=oEBvv{a5d*Gmkh6G%vY^cNUZTqm5eg4r6^;o6TC!!p6qTx7saV%Ckwi z-Wb`6*b>@XvOT+_!2<_J`Gn=z?U>l3xyqjDp(~3&3(Y=)#UBtE9;^4rWrxpnk~K<) zUVQ)VIWS(UKdwbTnuj=Y6ip<-i6}_EhT?uR(Mddsk3Ro;ISmmc&X$-rout2L=Zg^Y)bM@ zAazXmsNS)}65(%+lB9tVje32)WW|WRjNl3ypXt%Sl^| zdSs-f{UqD1GS4!Ve>J_SyT%LtbgsIe!6oK#;9CocFtKmqCWHb+KVeBB_HEiNVgl!{ zcMKk^eZ!wwB*??X1^u8!yD>o`!=Er2qu#^MB*L#0nWeBEajrjU231V!#?v#{X7rCc zun*E-Lj$37@+v!+WeE8c>&m<8QIbYV!$r-jB7eR1M(8{aFJz{X6CbW3ZarGgGFMRK5*Hr+IQp9cWaPm81QISBHY;({(#9T@_EAHvoqhQvlQ!o0V} zHz#%VslvU-mZM4O7*GH_xW*$75$b55iEBoAmW|E%05ZN2MH@AniC0yp1>s~(@d%_T za8C0W<-Mjrr$WlBbbZ>{**yMcWkWN;#ALHzIPx7kS$;D|bo6^ru4C+bhuB=XTZLRd z#fzobgtVyCv?!N6iLM{f<*6LFI3F*r=|*`HR^KH~27Tr0R_2Kjrj0(ydyQHE=eNkB zb2Qk)zjwrQ*!)GCX>SBZq1bH*4RsMMQI~b6^0~phaU?(sIH^fDg(vG1&hP*C_*);2 zRJw)Ll=P18GEbSB?;~)YhErfl@GXhx1?4Zh%xb|={TV$#(?;dVMTGhk3Qe)O?X?rh zZeI6tnQ`h|e}ghhYcX%bwkL=*7WDh|6wNjhsna#?%9+i%+`_uCPqGoRFB=y&?d05K zbQ5{nch_}s{%i1xzH`Nua!*WK!9WoXDj_>WS8}Dtx|J|OV^Z18d*F0l5!F!0+)L0$ zQt@jxZ6~cDRNr<1Wv^){l$nr{A*%?K5lC}?)>x@1xX{wPTh7DNaJVT-`1eU}J1X`1 zWHJ#SBJ4wf1xE;DWyLA}jl}BnTAS#|fG4&s-=Ca&+=mv6kBDbBXDQ4{74Pcm>sN;5 zLK|eE0CAA+g6pj+mfh_2jf%q*M@ryvn%32ao9yt@Y@*_qWi;j2*M+3x)+nU8n(3KQ7DK1XA;2dAT z%{zxcOGa(AWwZ=0*{U=fub|n^&%i+T+152Fc<-{4s}j`ib!FkLg&MQpdgp~A1ZAgH z=9BPrD-rdQ|M)DWRr=!|(%xE~N9>km`Q**Ht<;-~N=OsOeA^8!%*pZooPhMV|)_A)73p%Wl4Msn#dPox3Pb~xwdI=UcG2FC*zwC=> zpC2Zy5Ovo6JKBwu1>LVHAwcXfRSw0pW1`c>0P=}q9c-VH+wtJU4qQA^z z-#5J#m(@)1hi*h@y$mW%S(9{ex6n%b+Z1%?8Ar9@GaveXUgkUtoqG#A?MG_n~2kbT1F zQiL4cwX-*Q_Y)`E51E2?8_6&vH<@0qC7v~rA%W_gD`xgXuZAWJI*SC(0zmSYp_fg?%v&)I zu{4d7+w`(V|3KJqR~POT?|nX{{@W7Lvno-9Y>;%ouKBmT?>>SKvgKc=nLcK*)YmgF zKeII5Mzef}{{>yXdQ=I;x>PtrO)PEV!Rp7~fKa%Rx25(P>%#x_iIVef>271FV#^j@ zmLeR}4}<;sW3Cc9GeKTQ~C#rP6^GmD`q5>tDfw2x@GsVldj^10R5VCgFdZ1J|b4V zf^?wIpqdK!G=%*DMC-s%MzdA5*;3x*(LDCxjQ7&&aJGNx*^!8)-Q3dE{G)N;Z#X3S zFY_$DwAZtQK3Tn2&LGM|tFU0ro8KqxYCSh6OKDx>&8tMl7nVpuebG2b_Ww%dP#~v( z`9~^LYK#=`4_uW5KMR< z9NV64N37mNz&KyhP*)GB4~pNOh)VQAOr_OXK?EELQonDxX{q+c+kuj& zB)oztu%b1jwm&A!@HlDkJp%yqIi3upMvY2FY_&z_)M#0ffGGsCKH3>k^Xjwm#`mmDTW_B9i#E9=f&0OK21%E ztA)Yqdg#t#yDhNZ{>?j*>((B3+}c;TvJ{Va$$TWJ}c(C}?8 zm5<{W;fIPU3%6SQ1Q53{fmVmc)_ND(ZWpc)QOc(s%or?Lt$ECL{c!OqPFKj(gkn=f z`{52*G-;a{;6knEx+a5Rb5gHXR=7NGwf7TsH~Ula8Ajy21_Ci)L+ND|{>eyEiChc< zxw}COWkP6x@v3J@PhA!A^88ooJAowir28_j=%_dl5>?W>XATHT5KP>2-B-Fx+#wlo*%RyuOa2v=E*|z`)fVIV(HJy z(Tow9=XUb)(iemkRU%7H`vp83@3HWku6P;Wjf}hzM1;kU_*MGLHpj&Ckr(_nvsaa^ zs=D%I*Px$fP8%$i=090oKi@$__&}RdYJK{3JrZpBux`luYU37M<-q~*zM@T+GcO#V zuE|qd*Woy?Ptnj&Ssx`<@Au$xMvPW*BU0w())i5D5CXcd!$-2%E_gIKe2l~PDOH+H z{iWXLXar&jlkF8RBS6$;cG2m*2pxU2#BU4=51nL>bJZT8E}Tcofk0Rr@c*)U|FkVyCSjYT zW`p&V9UaR0BC@iuu&Ai0+&Z$rVRdki7Oh>nLHaFOXbgmbrEldqnkMk2FO^)pxGDp` zBAlxk-Yk?qo{*&w*FUbD-vy8EtHc#%J_bkK{ebTa-~ZP2C;d2Gu9_2B^e*f9Vmb}| z9{{N<)0T}a<3EN}Ef_dtqV`QSr8sYZfwP88_6rBdJpE%QKsU;b_6@UH1?lfXMEB)1 z2Xl(~8F_JmsJOU~EO8K=^zYXApE5+&7iC313#z=P%aGeYOty-B_d9>AxhtloDp{gX zG8h~M=Vx^uJ~X`yWtJxoJRWhJyA*AV7F%h87Cz0-vBs25Fp9*1{*D1wHPLXYwV+>5ImK{y(^Y-(Q{SUpZ#6UMC%GwY8$!RMgzu zyWXPa(Zm{!%P>5Jmgc6$#-;`8%k}42YaMi|YZsx6k&=>c(Jo$P$i-6`!|}?Qu|qm~ z%lTck=|US7hMx<~36iC#4rfOvT0ho){IPP3@U>-~!Q*#RweXtW2!pj!&pN)5^v)U| z8giSh8DwrUOQJS%m^sZm-JI25DnE^bX4UtZH{eu8%_tGO(@7 z#&Rvv5C5@8S+;t}ZQ2Vez1$Og1S~RDnwpsv-SZDS3S8}Cc>IN@lz;|MXj`k!O#NP5fj}^|5~xLc;~Vbs1L1pON_7+hz+` z)q7O)bqsNNJx)CHLs|*6-f2wk`X}YR9ONK)+O%j1W`f0bth1P$=5zdch^#XP`^)39 zI$u?nqDELaXJJ59=~x`Ip}X0E@#$FguQ!Z!29m$2NeRjz-e~uCe2I5GXS5CXw7-gC zzyQIY=0ih56e-qG(%sI55NHJ{%OT|*;Z%* z>osq6>hyT(4n}nuv|Z->uRkYsHnd!hpO3~=ct8HScYbzzk|KYNWIy24e${IN&{{Wj z4LB}?p;Hx#shS*{*cValxWqIhl)CN?;vv7huEda=CV~N!I}dGw>o!hbb*jpKMZM6! z{PHS0zN-4I+x0Gq=)^(}_ty8-j) zWi2@)?C;2Ll$7`9Ql&!oYU&Ic*hCmO7DNQ53I(Df+V3KkU7|Bi(8-epR&grkTKk-N zPf)Vb$}}NKSdY|6W*~hLF?%)?N*D(BmksV%)?wM@W)t=6zrY->qFVR3E3=?`jev0P zt3fpFG{qS#jmgwSv$l;h-KkOwJ0v1_ zo!>Z%$KUA9{@})!?6Hay!GtFVIGWl@`my%S*j7mvDA`e{i4E1o;r|wrh8-CCE<2Ip zi-t-}1$A@NRq*y%@495D#O4e1bhj_{&PclE{|*fZ5BDR5Y%*HH8+yg9&J=9FZ`5z! zW3OpP`6?!gj;^THw%4-x{w^%JPVy8^JayC>aEpsDtZEYnj6D7GDCBk1G3KVTa zXTUPPmDtIJZ@vKQxio__@JtrgdiI^^NmXZhIJ;mIXSlPZh}4(yWu_!fFDprL`7c4l(-M|Pp=aUNkU z<&LGKi9}RgoWuOoT0~1i z`5KPxGJa;W6P%Sr0S3~Gy54-cCEGJ{jMu-0(Kz*_ym*#s-(>6ws%EVz#bub&Z} z02bOy+c(vB*Q$)SB&4Rm!%R^h<%J;@3FpXaV$|N($A535Xo@5lF$DiL>rU-Oo;9`lqM;`EUzI zNF<}*eA>%Jt3|QoY?B*g;kB(OS$`5FB#rgE53s1@=gs73UVq1i+pBw^z@$4LMV>Ob z=(JCkN9&KD2r74O4_(s^W~xWl(C`+C3hN2{yK`3suU`4Eg0kgqU7j>p>-v33ThwHl zEU+sq{LANJ5q76API_4q03ypS9>zEE&LGVV6uYxkLG3bw+SRDvkeYF5H7uCP-SK)C z*lcoMvc=ujUO-0Khf?#5wk!Ar0cL3U?}@p?2LS~InE+w-lz{8RiHYS4LFP?LlgqO{ z16HamHzNVLQnNDBiOMSOqobyb2Z`t}Yt8>2=rj0W03(V*I;ap?iD6kN zsND4Qyx;ctuFlR2iyy07liB^CWEwWs*6BQ0QH7?q1UJS{h&H&+s;mIu_ib`wJQM@( zR7q-NSU4>ay4Fa*4u=@Uc~HfeVfU({JVh@P9|rJ8{<@Us0mN0;px)P+m)-}zIZiu z|GEOP9Blss;Q{fyXmrnB(GiRA4xo2zN@04^O4^zm{FgE8@k zXhfZwEl<)W$C%c=W+xA(W(0zz`9<=CJVDsr^jkU34vw<4sIR83c6KZ?y?1N)0YANt z80qLLO6&a^*rukY*s&ITZ|;GLfRzhMjvmB_@!#fKuW z7+0HZV`%W4%~zIo={?V4iN5Cx7GD=yLjFronR9p^8glA^z$T!g2vW7-NK1#7X0slc zZ)G&WN-*$?bK>-4CcD^v^(&C z7WkUwT{|fgwjf{d&v_A@8VM!UoL)$$>Se0P0fj_V-~^tEXuftS4b3 z{zswaX(ZhQib1b!yJc*^ARQd5(!D78h;t_X3m|l4XI}RffQq`$PmTm&PkRscawD}> zrMKl@1$J9kygaQ`>IiE_F^a7X2sl)N@!jK5NkQZgX_h z%EyG5B1egV9+#6_^9Oa@bhBynh;M0vm-%g$E2Om;=}7){>vYEK##B~~U=ML4Q;U6y z0WDAN1p8Z>MKYK6nCUZz6S0B?7Rz zx(YF4?i!2?A>36$(#@MHL|pRn8HL9QiHQG=^P5yWh|&3M#10Pp+Uiwe(jO|Ch#+gZ-wGHa z_HO;#Wi>LN>Zx-7=YH~*DB=7)9nl8Klh3PpCTZyC*3r=s0RaJwn3%X{X{njz2Z2~E zHwY&bvB4eT{4K3Y@a4-F$QRIUoviWU|M#4e_k~OXWX5gp&!f^9ki%5#_2!D$ouZ?o z11WH~hrJb=JwKr!Al^cYhp&oEud@ID_G1@7A zZCp0GW;!9x8;iffVr>D3#r%i8x5Nc378sC* z=W;TU`51)ogt}mcp)ddY_d7f@Orc1k?Fz$ASp3L8~|Up!pSR=sy?Ax4MM5?&Fu(ARtV9}64V ztn_t_!tTtDrUb%iEfNPn+R{u%*@~X$tO>bPe8Kh@b`s?%*rc;aMIz`NonJ_|xC=wJ z^KoeSO4&h1ba!_BHx`yU5|eF)2V`1nH5FQU@XDD1N7(HhU1i0cnk9#kGzI$-RG|j1aJd-gb zogSUKasU+4_5=QwGqN7KNG_L~%q>MbO3^-jGcKHZIR$Ctr#Mr)soex-{i=`AAtRx%yu zo<92a$MYY@Y>q?$Y$A6I<5iUpMoFI`N_t&j9+p1#qppWO*`!E)L!+SE~Hv;xG>S22vb;7_bI>|3|%&5NgfwS_Ai*?97Zc{NDKJ8G}`fMVt%4DIiS zn}T*PouPlH>50J*a3hJMz&9Etv~YDMXDb~Gqg^UXh&|i;!c04fRX6wb;a5P$HAki-Fo5XLWMLrB$}tV=3A|~(;qa^pHx^ZD*@|qp z1|5|ggvHO_hF}^3-10i?QzS=2&&3O`aASEcudZjag)&&x6iB}vl9Am>ps*Bh*mF|M zT&ML!k~hXkLZ8UcI>axcs*a7NG5NN5y+w%_rwQMOr?&7?X{fd7+#5}Ej`mqgO`9Xa z?i}YjAb%mGjOdrLkv;vZ8~yP1bbBP^u-Pay7k~jwPD0A@iV&_YPIgXM4bH>0aUg(! zp}q|69+SR<@i$9H>3>A;S4%e6Q&vO*{HEoSrWvL8tSg!oR8{hCJF9echQI=Hh4$G{|pz*BsrbG(zm9)v=-akCrCxjMblku z;(j8EkI5E=+lQc>5f-EjlL_diUT!RYmk1KeuT_oO5VwV+{&u$Z?D5HX`O{JsH?Vlk zV}E8~T9cg>E=!OoRTi`o8X41K&C#u%cCS2i8(;l)7zg5RZ=PzMa0 za%Q~}%k}hVLzmTN$Zusx>DAM|PNZ5dg7b5$RyR|v@ELtvoeNpHRF%edR}v&;BLO+% zNV%^pw%XeF48!U=ZP_cPel7=(0Y(B|8+m$_c0F8L@a{6RdD)P~Co%eL<9br+k$5FL zimGJZwZE5s?XNRyiOy~qgEBs$*{psRqrsgj8)Niwz8*5PZ;gG%EU>ag=w~gr8P70W z7(1?+W$C`U)*L>Gt?`=2x{p$5opJ%z8?egM6B#hk(a1!f+@rHl67p1Mk;|NuU1Mb@ zhkGgV%d-|oaaOJ15(k~$|Gq7_3|{L_P0aod`K-v;V9g_Dva3bQC(-xnP#e}?%6yVh z2$J=#u^5VHcElIVz%K{@o99VWrEu$-Z{9+j@SVJNb}`wnEsS`1K;iRfTMvRU@loAQ zm{=^Af7_eb_w8sh>^&!Otk}GGlwarhh&l5ZkLJIbpFKz;`;`M$Jw`7 zS!1T~EWm!;>=u6+NkRM3w9a3hpmo0~m*)y~#Lxohbu)Zy{EqB)w)=c@&%0=+`Z0%* z=wHT^xjC+m#9jz6%{%>@owg7sO5?q%XgF(WMXPj=U;kdk@D)(8+PHe8=x1*+nJQ;p zL#o;|w)c?IV0Z{>oR90={k??pT+~gMW+R5>oXBhsfPn=$k!Zc7%dL6g|R9X*}JBEX#W0r^$2|=d4`K)#OU;lxfrN zhq|kEjE%!p!!l4-27r8f{4dQ4F06)n4F#YlN!#=O@>2Oa6UT|pU6Je;WPlf~=*xyo zZrSEq#t-a9Z$ZX;91Yu{M?71-mBHZLp9V(CGp&-!Rxn@y_EJHnwzk#S5rx{0R_43J zV^a~(;L3qP6(wj-ZIYKDv7un5q5Dh)}D)bwa5E62z9{0|Go+U4UWoDd}m(dFqdCK zr;kP70IOLR!mB=~d}Y7V*^?0)20t2FLS4Rs>LQzTR%TbM0nf|7x1C_Mb}M_?HCGN$ z-V2$+r;vnS;uNE8$&Ve%4atnANXB?PE_O{tehCTUL*vx0-;D~V3Oed?V_Rwzh4w6( z@OUaF@AHqoLrpuI4(M}Zm2J98a0ceg{Uhm)1t?^Nxt*M=lH9-kk)rlMwc}m;xqG-0 z=wuJrDZaR|78s>_RCIY3O?ihB3*xIf#U%%C+AMTN@sD4AGomBFvuw`LUS#@tazQT$78Zh2-WYW z4aY*b{E3aQ>`S9X50*ZD2pf-6LaBL|^y_rz${b4RxDWFs%j@%hJvY*cZO!Q2N=nLxSNyrha;!GbkIxf? zZsg%zbfXcC$8>VKDwT*EZ6)Q2QK!jY4(m=;)myc0jRxA_K#Zv-n*@vpcVy_@InC&f>s=5*c7tJq;rFxvj@!J}ET5rnz9i8Wy zMxKJXMPsidQJrt)JDBeGN`86LrtENY_EWC5FK+an(8fW4VF!Z&!8=?dCikVs34Z17 zlUsbNWj78e#-+DR>0>zvRJ!_`uw0%l8;r$_va1d5iR>Z@NV~hl%qZ42<&?rRjSY(N z{65cGT$}Z$!BV9Co6skR>XeCETSoB1;~}htDk z|8%v-W3#fSgpCH7<%{3UVayqG=Ow{fqv;t$3Mh1Kv#mZyw-G!!6&lE?)@Ow&+wpTx z5b@n;z3wZaK`ZbrXSOeopL0)?0ZMI zK!1#Wh=qz_Or}VGZiBN%CNLr^(Nv@nNEOI}cp>;c*D9?I9 zqy8;Su<3LgYC&N9#eBYkV`Mk$YjOSg`D6*g(=OarS0p*l)$`U51$KDoEU|;%){XL% zYD$$E1V4DQ_@Yxh2;)g;j^$KCHl~mO5|JRk&)Zh3_v7JGLWXEwAT9utcr$8b_kl*Mal7Pi-lji?t#Q zx!5Gdf~E3e0@{m}gQbGfG)$FvjI1B<(qnx128Xg-c7V&d9|~ESnaHTsfuKq@Li*a| z`+auzqvK+sNq48S-C8bIhM@!};^Kf1Bv7ZI!BJU@eQ62I6l+w3F{Rb?YTy8Dr>}!% zKbmWaUC;HMb!>lF$yr(&9dL=yYf&XUWE&fnz5i5byzKhJmp= zaD2dT$V548*9Z*8$@KXkR)zLf7sHyeXnw^vb|x|*BBlSS3$gMzU<6(GS)#$?3P&uA zYy_!H$r6_ltHHFo_}8(O*UTp0)1*Y9U^S-g)!D^~_?Ir@B`_eB-?J>k+%$m`DgBkc zTTN|_e3s~Z&1Kc>Z(eWFC$(H17YP@W{L020;)dlD*v=4H%0wHF`&P!g4?jl>f?hMN z-FlyCAfvR~p#beFl70RCvfEMoP{%=t8N8kz#2Dm5;1U8p#Zdq_dSe`!Mw% zId5r#`a(IA&RXZ6$u93^BC}_B^M(6I&FZCd-Uc?svBfi{nW6rvmcw{P9aow(uGMmo z#c1tPo7gwkc*5s?YYR>{et85!Xy~*R?;dxI%P+CeoG6@a@XX-UU=79^HBQ+~$8nQ< zqt};f_nC-5Ik+}_u2%b_lZmk0Ue>ebNtu(%jTbCdzxX=B`V~@u++3_?0og=mn;m7E zy=)%8yiw2F3@57+B8DobQtX{Bc#u}E?RLmHE5qAmuoJR8Z7&MgrM_d(#*7v>S01Y` z&ctg>GYTwNOx0#u_*P}F*m~whPN~f`S=ttbg8@Z)woBTnM&Wx(Jk{wMT35+WbZU2lnx!Ul!Pa<>+o(z40~2qx6<}U(w9w5Iz)IKaa-pfwp+Rd|;=9Ql z{p)0gw&{j7WSV9ll`ziU2z>~Chx*|w?Xs-Fvix#GT=Ug-Awfh8CYn=LQCDC)GZuIc z{b{EeI%PDmAf>P~7K09P_t&-7rNge+uV&#OM>!a9NuWB5JvS0A|NJw5{B{!2(P*8wE z!%eRkbz1&ni(T`2KHr$Cn1NX+DTc)d5|0Rel7orz5k7AVdmDR~XZsc1Z-bTQ3V(^6 zD^3uFdsBL%^!b||<6*g?9rAW;sWT1(5io@7Nuw#W-u=k)BSO0_U*E3C{BmJnCdN3r zKdFCsN(DU{;P!y3&fuF^EMCrQQ-5ye|{)~gn zzfw;t`}o-TDSLayZ9bK2L#7$?v57^Q)cN@+$>cilQn}a0!|kcCQtt!pW44vXm6msK z&#ean(&_Pod;5a?X*B}EU^4jO78h$wVE#SJWmXRL9`(ml7}y|~O@l5$Kn}s<{w~9T z4tPU8UHA`9j06-bWGBC#grblIBN6F&x?ewZN!t~&m6JbB4sQ$_XW&;bMWPbClkn#Y zfCtlLDPch5(t12@(BQOKe~!6W=H20AsNe!cZkvmi%Zq_23XgvWv-8rkA0h<$p_ueKCf_Em%PBGj_;_&&G0qWrKc+FiT6 z=l{>#DDLcYu60D8n$@ylSjL?b+jF}pA|<6I!((%%$(AuWIW9|>Iniv98%FFPIC9A% zXm)JD&`!n79;2clWtLK_(U~C>L585d{Qx0oS6iIU3|&|pc$kdAQV9mrgv;GTE?2=D z?YVMzsZ3zv(GYTzMN?ZEqf@~NR6Bo3++HyOAql7=g^9(ov@LfSlUah)UMEXNYW0 z;*F>NOReLa;=<_XqAeG4IiNl1t?cdwYC zD2jG`3hlYm=4+QkhBOG9!`fWR_;7I{-}rS=nxY7%ExawiQD2!#KMYaI(8mRDm60k z`}Gm@-M>2sHMM^?0gYBWgK*{>xS$^*5p7QqU~|=ovx5sG??cUQ(=G`ILWT_*m6-3w z(vn_BJ_3(Ddm$D2#oBj|q%!*aZC%8q@feLhFrM#g|3Xs<0atO$V^s_2DpmEE5@T_- zvYU(X&TJ9sT2VT~|H(3h=e{wQcESDKXxloLBcOo)9spfwD z{ovU#>968yAk@EY;gt|n(*tFdIc2nj{~T3R{Nv(&4_mF9RY=9Fs}4v@8x0Sqbfco0 z2L&Z%%Oobjt#82bIDIqU(yORK1Y*-*($cm#us{`NXgqK0v5+8yPm6B~t{1uv3Ec^t zBU$=d1_t^XrruxoJ2r_tFEQr^iBIOw+No=tsN*6yGmWP~qaWNew=By>_XOsdahO>W;p9V&#z&u^EqMNn6+@p$7{dNx| z9kDPm7w0vFko|10;bu_p&Y%Dx#-W0uYL@=cgVwpAI+Qnui{4(aHs}0CoAR)| zsFxp7QLA5R2uC=n&|%XiJN}T)B5&;{i{{@A)iw|~cs!&>i{!(?9m$tep1YB1(slihcXW1&DY;MkZ zzxQ@js?F()tHtUE)i=|m7B(L0)CAI>vg)$^-%)yi*r#E+|C48UzDuPV!xfy;X`whJ zNTUJYVlg8#^A8UkNO}VH{#Fq*SZRDz|A4>R>i@%Oih&dw5!k>cDIdJQKQsmef>BZ$ z{DeeA+};Kr9t@1Zot(5JkPCwPA-)JNeA=$JT+0!%!*w}6EQd0`uP3E3Ku%A6uL_&W z;Qs_!j4(h4{|3GmFP(?5944|nqPjmC{pagxED?J4^z`H;I~yB>pPwI~ufH`G)1{^) z8k;>gSFA82cLqr;H8TF6ZpgjAagu>c%Lo%cy`jR6Z-t8X7yuDp=`fkn4s6HttU1iB z0;?E~PrN_Mix&4;4OHn-OWcT~z#1(YnDp)QO6=*OqUMXhfM5KLOM2&|L;lan7GXLJq6JL6RTQ>J z{647YjPA2-yUWG;#s-F14D1if6ZV$qJL9X&X1tcX7g%Km20hI48j}r0G<->6>HSyI zA{xA#EnPu-fkm&eBKv{fG|vbqTdnQNU@++KE)!he2s%y0sZNX2NvPUu%K5)ZlV@nx zuuPbqr1uUvsAQe@4UHs5M5OS~)C#+WW0Dv@`Zj2$ec42;j8pzp{>!vf;u1svtuNp$1Alfr(`K9~M)9Z&I zyDak4u0_$538x4Gf=>QVNerSu%PkSjq6((=*zIr9nuEiB()$TwD}9Sogu2$$K;~Ut zmVC=Ki(_)g!QNzX|4M%h1HPFQIPtB+7Y-eiSx|d32I+z{{z86xCD%Bh;XRY&av&5? zY};|MC&!YfWo79(!w4fc1}32#Qr2C20VDEKFrCiBo8!X`*^B@rfB~*z;n(H~E{r=`nd#U)Ts60tt}o$70?JlN&m6 zMcUS7$5ARZPDK(!-=L9vT?v!Ic_yWR^E57`B&5Q9<~df!xl#G|_*iZH%d<2Qpcdr4 z?oaRou(GpWiXibhrVRJ+5rg`R?3Ps#L$uQW;JWbVoH6

      >D4 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 z%0D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

      11;aDlj3!5HSD9HeqGPM3+q9n7hoTfe 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

      ==s*DF8cQPTzgk8XWf_ttD~K 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 Wy`;IS&NfJL?hIV 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 literal 0 HcmV?d00001 diff --git a/documentation/jetty/modules/programming-guide/nav.adoc b/documentation/jetty/modules/programming-guide/nav.adoc new file mode 100644 index 000000000000..ac50c0d7f133 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/nav.adoc @@ -0,0 +1,48 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +.xref:programming-guide:index.adoc[] +* xref:client/index.adoc[] +** xref:client/io-arch.adoc[] +** xref:client/http.adoc[] +** xref:client/http2.adoc[] +** xref:client/http3.adoc[] +** xref:client/websocket.adoc[] +* xref:server/index.adoc[] +** xref:server/http.adoc[] +** xref:server/http2.adoc[] +** xref:server/http3.adoc[] +** xref:server/compliance.adoc[] +** xref:server/session.adoc[] +** xref:server/websocket.adoc[] +** xref:server/fastcgi.adoc[] +** xref:server/io-arch.adoc[] +* Maven and Jetty +** xref:maven-jetty/jetty-maven-helloworld.adoc[] +** xref:maven-jetty/jetty-maven-plugin.adoc[] +** xref:maven-jetty/jetty-jspc-maven-plugin.adoc[] +* Jetty Architecture +** xref:arch/bean.adoc[] +** xref:arch/threads.adoc[] +** xref:arch/io.adoc[] +** xref:arch/listener.adoc[] +** xref:arch/jmx.adoc[] +* xref:troubleshooting/index.adoc[] +** xref:troubleshooting/logging.adoc[] +** xref:troubleshooting/thread-dump.adoc[] +** xref:troubleshooting/state-tracking.adoc[] +** xref:troubleshooting/component-dump.adoc[] +** xref:troubleshooting/debugging.adoc[] +* Migration Guides +** xref:migration/94-to-10.adoc[] +** xref:migration/11-to-12.adoc[] diff --git a/documentation/jetty/modules/programming-guide/pages/arch/bean.adoc b/documentation/jetty/modules/programming-guide/pages/arch/bean.adoc new file mode 100644 index 000000000000..56598234bb28 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/arch/bean.adoc @@ -0,0 +1,160 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Component Architecture + +Applications that use the Jetty libraries (both client and server) create objects from Jetty classes and compose them together to obtain the desired functionalities. + +A client application creates a `ClientConnector` instance, a `HttpClientTransport` instance and an `HttpClient` instance and compose them to have a working HTTP client that uses to call third party services. + +A server application creates a `ThreadPool` instance, a `Server` instance, a `ServerConnector` instance, a `Handler` instance and compose them together to expose an HTTP service. + +Internally, the Jetty libraries create even more instances of other components that also are composed together with the main ones created by applications. + +The end result is that an application based on the Jetty libraries is a _tree_ of components. +In server application the root of the component tree is a `Server` instance, while in client applications the root of the component tree is an `HttpClient` instance. + +Having all the Jetty components in a tree is beneficial in a number of use cases. +It makes possible to register the components in the tree as xref:arch/jmx.adoc[JMX MBeans] so that a JMX console can look at the internal state of the components. +It also makes possible to xref:troubleshooting/component-dump.adoc[dump the component tree] (and therefore each component's internal state) to a log file or to the console for xref:troubleshooting/index.adoc[troubleshooting purposes]. +// TODO: add a section on Dumpable? + +[[lifecycle]] +== Jetty Component Lifecycle + +Jetty components typically have a life cycle: they can be started and stopped. +The Jetty components that have a life cycle implement the `org.eclipse.jetty.util.component.LifeCycle` interface. + +Jetty components that contain other components implement the `org.eclipse.jetty.util.component.Container` interface and typically extend the `org.eclipse.jetty.util.component.ContainerLifeCycle` class. +`ContainerLifeCycle` can contain these type of components, also called __bean__s: + +* _managed_ beans, `LifeCycle` instances whose life cycle is tied to the life cycle of their container +* _unmanaged_ beans, `LifeCycle` instances whose life cycle is _not_ tied to the life cycle of their container +* _POJO_ (Plain Old Java Object) beans, instances that do not implement `LifeCycle` + +`ContainerLifeCycle` uses the following logic to determine if a bean should be _managed_, _unmanaged_ or _POJO_: + +* the bean implements `LifeCycle` +** the bean is not started, add it as _managed_ +** the bean is started, add it as _unmanaged_ +* the bean does not implement `LifeCycle`, add it as _POJO_ + +When a `ContainerLifeCycle` is started, it also starts recursively all its managed beans (if they implement `LifeCycle`); unmanaged beans are not started during the `ContainerLifeCycle` start cycle. +Likewise, stopping a `ContainerLifeCycle` stops recursively and in reverse order all its managed beans; unmanaged beans are not stopped during the `ContainerLifeCycle` stop cycle. + +Components can also be started and stopped individually, therefore activating or deactivating the functionalities that they offer. + +Applications should first compose components in the desired structure, and then start the root component: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java[tags=start] +---- + +The component tree is the following: + +[,screen] +---- +Root +├── Monitor (MANAGED) +└── Service (MANAGED) + └── ScheduledExecutorService (POJO) +---- + +When the `Root` instance is created, also the `Monitor` instance is created and added as bean, so `Monitor` is the first bean of `Root`. +`Monitor` is a _managed_ bean, because it has been explicitly added to `Root` via `ContainerLifeCycle.addManaged(...)`. + +Then, the application creates a `Service` instance and adds it to `Root` via `ContainerLifeCycle.addBean(...)`, so `Service` is the second bean of `Root`. +`Service` is a _managed_ bean too, because it is a `LifeCycle` and at the time it was added to `Root` is was not started. + +The `ScheduledExecutorService` within `Service` does not implement `LifeCycle` so it is added as a _POJO_ to `Service`. + +It is possible to stop and re-start any component in a tree, for example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java[tags=restart] +---- + +`Service` can be stopped independently of `Root`, and re-started. +Starting and stopping a non-root component does not alter the structure of the component tree, just the state of the subtree starting from the component that has been stopped and re-started. + +`Container` provides an API to find beans in the component tree: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java[tags=getBeans] +---- + +You can add your own beans to the component tree at application startup time, and later find them from your application code to access their services. + +[TIP] +==== +The component tree should be used for long-lived or medium-lived components such as thread pools, web application contexts, etc. + +It is not recommended adding to, and removing from, the component tree short-lived objects such as HTTP requests or TCP connections, for performance reasons. + +If you need component tree features such as automatic xref:arch/jmx.adoc[export to JMX] or xref:troubleshooting/component-dump.adoc[dump capabilities] for short-lived objects, consider having a long-lived container in the component tree instead. +You can make the long-lived container efficient at adding/removing the short-lived components using a data structure that is not part of the component tree, and make the long-lived container handle the JMX and dump features for the short-lived components. +==== + +[[listener]] +== Jetty Component Listeners + +A component that extends `AbstractLifeCycle` inherits the possibility to add/remove event _listeners_ for various events emitted by components. + +A component that implements `java.util.EventListener` that is added to a `ContainerLifeCycle` is also registered as an event listener. + +The following sections describe in details the various listeners available in the Jetty component architecture. + +[[listener-lifecycle]] +=== LifeCycle.Listener + +A `LifeCycle.Listener` emits events for life cycle events such as starting, stopping and failures: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java[tags=lifecycleListener] +---- + +For example, a life cycle listener attached to a `Server` instance could be used to create (for the _started_ event) and delete (for the _stopped_ event) a file containing the process ID of the JVM that runs the `Server`. + +[[listener-container]] +=== Container.Listener + +A component that implements `Container` is a container for other components and `ContainerLifeCycle` is the typical implementation. + +A `Container` emits events when a component (also called _bean_) is added to or removed from the container: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java[tags=containerListener] +---- + +A `Container.Listener` added as a bean will also be registered as a listener: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ComponentDocs.java[tags=containerSiblings] +---- + +[[listener-inherited]] +=== Container.InheritedListener + +A `Container.InheritedListener` is a listener that will be added to all descendants that are also ``Container``s. + +Listeners of this type may be added to the component tree root only, but will be notified of every descendant component that is added to or removed from the component tree (not only first level children). + +The primary use of `Container.InheritedListener` within the Jetty Libraries is `MBeanContainer` from the xref:arch/jmx.adoc[Jetty JMX support]. + +`MBeanContainer` listens for every component added to the tree, converts it to an MBean and registers it to the MBeanServer; for every component removed from the tree, it unregisters the corresponding MBean from the MBeanServer. diff --git a/documentation/jetty/modules/programming-guide/pages/arch/io.adoc b/documentation/jetty/modules/programming-guide/pages/arch/io.adoc new file mode 100644 index 000000000000..0eea578d98e0 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/arch/io.adoc @@ -0,0 +1,375 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty I/O Architecture + +The Jetty libraries (both client and server) use Java NIO to handle I/O, so that at its core Jetty I/O is completely non-blocking. + +[[selector-manager]] +== Jetty I/O: `SelectorManager` + +The main class of The Jetty I/O library is link:{javadoc-url}/org/eclipse/jetty/io/SelectorManager.html[`SelectorManager`]. + +`SelectorManager` manages internally a configurable number of link:{javadoc-url}/org/eclipse/jetty/io/ManagedSelector.html[`ManagedSelector`]s. +Each `ManagedSelector` wraps an instance of `java.nio.channels.Selector` that in turn manages a number of `java.nio.channels.SocketChannel` instances. + +NOTE: TODO: add image + +`SocketChannel` instances are typically created by the Jetty implementation, on client-side when connecting to a server and on server-side when accepting connections from clients. +In both cases the `SocketChannel` instance is passed to `SelectorManager` (which passes it to `ManagedSelector` and eventually to `java.nio.channels.Selector`) to be registered for use within Jetty. + +It is possible for an application to create the `SocketChannel` instances outside Jetty, even perform some initial network traffic also outside Jetty (for example for authentication purposes), and then pass the `SocketChannel` instance to `SelectorManager` for use within Jetty. + +This example shows how a client can connect to a server: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java[tags=connect] +---- + +This example shows how a server accepts a client connection: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java[tags=accept] +---- + +[[endpoint-connection]] +== Jetty I/O: `EndPoint` and `Connection` + +``SocketChannel``s that are passed to `SelectorManager` are wrapped into two related components: an link:{javadoc-url}/org/eclipse/jetty/io/EndPoint.html[`EndPoint`] and a link:{javadoc-url}/org/eclipse/jetty/io/Connection.html[`Connection`]. + +`EndPoint` is the Jetty abstraction for a `SocketChannel` or a `DatagramChannel`: you can read bytes from an `EndPoint`, you can write bytes to an `EndPoint` , you can close an `EndPoint`, etc. + +`Connection` is the Jetty abstraction that is responsible to read bytes from the `EndPoint` and to deserialize the read bytes into objects. +For example, an HTTP/1.1 server-side `Connection` implementation is responsible to deserialize HTTP/1.1 request bytes into an HTTP request object. +Conversely, an HTTP/1.1 client-side `Connection` implementation is responsible to deserialize HTTP/1.1 response bytes into an HTTP response object. + +`Connection` is the abstraction that implements the reading side of a specific protocol such as HTTP/1.1, or HTTP/2, or HTTP/3, or WebSocket: it is able to read and parse incoming bytes in that protocol. + +The writing side for a specific protocol _may_ be implemented in the `Connection` but may also be implemented in other components, although eventually the bytes to write will be written through the `EndPoint`. + +While there are primarily only two implementations of `EndPoint`,link:{javadoc-url}/org/eclipse/jetty/io/SocketChannelEndPoint.html[`SocketChannelEndPoint`] for TCP and link:{javadoc-url}/org/eclipse/jetty/io/DatagramChannelEndPoint.html[`DatagramChannelEndPoint`] for UDP (used both on the client-side and on the server-side), there are many implementations of `Connection`, typically two for each protocol (one for the client-side and one for the server-side). + +The `EndPoint` and `Connection` pairs can be chained, for example in case of encrypted communication using the TLS protocol. +There is an `EndPoint` and `Connection` TLS pair where the `EndPoint` reads the encrypted bytes from the socket and the `Connection` decrypts them; next in the chain there is an `EndPoint` and `Connection` pair where the `EndPoint` "reads" decrypted bytes (provided by the previous `Connection`) and the `Connection` deserializes them into specific protocol objects (for example HTTP/2 frame objects). + +Certain protocols, such as WebSocket, start the communication with the server using one protocol (for example, HTTP/1.1), but then change the communication to use another protocol (for example, WebSocket). +`EndPoint` supports changing the `Connection` object on-the-fly via `EndPoint.upgrade(Connection)`. +This allows to use the HTTP/1.1 `Connection` during the initial communication and later to replace it with a WebSocket `Connection`. + +// TODO: add a section on `UpgradeFrom` and `UpgradeTo`? + +`SelectorManager` is an abstract class because while it knows how to create concrete `EndPoint` instances, it does not know how to create protocol specific `Connection` instances. + +Creating `Connection` instances is performed on the server-side by link:{javadoc-url}/org/eclipse/jetty/server/ConnectionFactory.html[`ConnectionFactory`]s and on the client-side by link:{javadoc-url}/org/eclipse/jetty/io/ClientConnectionFactory.html[`ClientConnectionFactory`]s. + +On the server-side, the component that aggregates a `SelectorManager` with a set of ``ConnectionFactory``s is link:{javadoc-url}/org/eclipse/jetty/server/ServerConnector.html[`ServerConnector`] for TCP sockets, link:{javadoc-url}/org/eclipse/jetty/quic/server/QuicServerConnector.html[`QuicServerConnector`] for QUIC sockets, and link:{JDURL}/org/eclipse/jetty/unixdomain/server/UnixDomainServerConnector.html[`UnixDomainServerConnector`] for Unix-Domain sockets (see the xref:server/io-arch.adoc[server-side architecture section] for more information). + +On the client-side, the components that aggregates a `SelectorManager` with a set of ``ClientConnectionFactory``s are link:{javadoc-url}/org/eclipse/jetty/client/HttpClientTransport.html[`HttpClientTransport`] subclasses (see the xref:client/io-arch.adoc[client-side architecture section] for more information). + +[[endpoint]] +== Jetty I/O: `EndPoint` + +The Jetty I/O library use Java NIO to handle I/O, so that I/O is non-blocking. + +At the Java NIO level, in order to be notified when a `SocketChannel` or `DatagramChannel` has data to be read, the `SelectionKey.OP_READ` flag must be set. + +In the Jetty I/O library, you can call `EndPoint.fillInterested(Callback)` to declare interest in the "read" (also called "fill") event, and the `Callback` parameter is the object that is notified when such an event occurs. + +At the Java NIO level, a `SocketChannel` or `DatagramChannel` is always writable, unless it becomes congested. +In order to be notified when a channel uncongests and it is therefore writable again, the `SelectionKey.OP_WRITE` flag must be set. + +In the Jetty I/O library, you can call `EndPoint.write(Callback, ByteBuffer...)` to write the ``ByteBuffer``s and the `Callback` parameter is the object that is notified when the whole write is finished (i.e. _all_ ``ByteBuffer``s have been fully written, even if they are delayed by congestion/uncongestion). + +The `EndPoint` APIs abstract out the Java NIO details by providing non-blocking APIs based on `Callback` objects for I/O operations. +The `EndPoint` APIs are typically called by `Connection` implementations, see <>. + +[[connection]] +== Jetty I/O: `Connection` + +`Connection` is the abstraction that deserializes incoming bytes into objects, for example an HTTP request object or a WebSocket frame object, that can be used by more abstract layers. + +`Connection` instances have two lifecycle methods: + +* `Connection.onOpen()`, invoked when the `Connection` is associated with the `EndPoint`. +* `Connection.onClose(Throwable)`, invoked when the `Connection` is disassociated from the `EndPoint`, where the `Throwable` parameter indicates whether the disassociation was normal (when the parameter is `null`) or was due to an error (when the parameter is not `null`). + +When a `Connection` is first created, it is not registered for any Java NIO event. +It is therefore typical to implement `onOpen()` to call `EndPoint.fillInterested(Callback)` so that the `Connection` declares interest for read events, and it is invoked (via the `Callback`) when the read event happens. + +The abstract class `AbstractConnection` partially implements `Connection` and provides simpler APIs. +The example below shows a typical implementation that extends `AbstractConnection`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java[tags=connection] +---- + +[[connection-listener]] +=== Jetty I/O: `Connection.Listener` + +The Jetty I/O library allows applications to register xref:arch/listener.adoc[event listeners] for the `Connection` events "opened" and "closed" via the interface `Connection.Listener`. + +This is useful in many cases, for example: + +* Gather statistics about connection lifecycle, such as time of creation and duration. +* Gather statistics about the number of concurrent connections, and take action if a threshold is exceeded. +* Gather statistics about the number of bytes read and written, and the number of "messages" read and written, where "messages" may mean HTTP/1.1 requests or responses, or WebSocket frames, or HTTP/2 frames, etc. +* Gather statistics about the different types of connections being opened (TLS, HTTP/1.1, HTTP/2, WebSocket, etc.). +* Etc. + +`Connection.Listener` implementations must be added as beans to a server-side `Connector`, or to client-side `HttpClient`, `WebSocketClient`, `HTTP2Client` or `HTTP3Client`. +You can add as beans many `Connection.Listener` objects, each with its own logic, so that you can separate different logics into different `Connection.Listener` implementations. + +The Jetty I/O library provides useful `Connection.Listener` implementations that you should evaluate before writing your own: + +* link:{javadoc-url}/org/eclipse/jetty/io/ConnectionStatistics.html[`ConnectionStatistics`] +* link:{javadoc-url}/org/eclipse/jetty/server/ConnectionLimit.html[`ConnectionLimit`] + +Here is a simple example of a `Connection.Listener` used both on the client and on the server: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java[tags=connectionListener] +---- + +[[echo]] +== Jetty I/O: TCP Network Echo + +With the concepts above it is now possible to write a simple, fully non-blocking, `Connection` implementation that simply echoes the bytes that it reads back to the other peer. + +A naive, but wrong, implementation may be the following: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java[tags=echo-wrong] +---- + +WARNING: The implementation above is wrong and leads to `StackOverflowError`. + +The problem with this implementation is that if the writes always complete synchronously (i.e. without being delayed by TCP congestion), you end up with this sequence of calls: + +---- +Connection.onFillable() + EndPoint.write() + Connection.succeeded() + Connection.onFillable() + EndPoint.write() + Connection.succeeded() + ... +---- + +which leads to `StackOverflowError`. + +This is a typical side effect of asynchronous programming using non-blocking APIs, and happens in the Jetty I/O library as well. + +NOTE: The callback is invoked synchronously for efficiency reasons. +Submitting the invocation of the callback to an `Executor` to be invoked in a different thread would cause a context switch and make simple writes extremely inefficient. + +This side effect of asynchronous programming leading to `StackOverflowError` is so common that the Jetty libraries have a generic solution for it: a specialized `Callback` implementation named `org.eclipse.jetty.util.IteratingCallback` that turns recursion into iteration, therefore avoiding the `StackOverflowError`. + +`IteratingCallback` is a `Callback` implementation that should be passed to non-blocking APIs such as `EndPoint.write(Callback, ByteBuffer...)` when they are performed in a loop. + +`IteratingCallback` works by starting the loop with `IteratingCallback.iterate()`. +In turn, this calls `IteratingCallback.process()`, an abstract method that must be implemented with the code that should be executed for each loop. + +Method `process()` must return: + +* `Action.SCHEDULED`, to indicate whether the loop has performed a non-blocking, possibly asynchronous, operation +* `Action.IDLE`, to indicate that the loop should temporarily be suspended to be resumed later +* `Action.SUCCEEDED` to indicate that the loop exited successfully + +Any exception thrown within `process()` exits the loops with a failure. + +Now that you know how `IteratingCallback` works, a correct implementation for the echo `Connection` is the following: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/SelectorManagerDocs.java[tags=echo-correct] +---- + +When `onFillable()` is called, for example the first time that bytes are available from the network, the iteration is started. +Starting the iteration calls `process()`, where a buffer is allocated and filled with bytes read from the network via `EndPoint.fill(ByteBuffer)`; the buffer is subsequently written back via `EndPoint.write(Callback, ByteBuffer...)` -- note that the callback passed to `EndPoint.write()` is `this`, i.e. the `IteratingCallback` itself; finally `Action.SCHEDULED` is returned, returning from the `process()` method. + +At this point, the call to `EndPoint.write(Callback, ByteBuffer...)` may have completed synchronously; `IteratingCallback` would know that and call `process()` again; within `process()`, the buffer has already been allocated so it will be reused, saving further allocations; the buffer will be filled and possibly written again; `Action.SCHEDULED` is returned again, returning again from the `process()` method. + +At this point, the call to `EndPoint.write(Callback, ByteBuffer...)` may have not completed synchronously, so `IteratingCallback` will not call `process()` again; the processing thread is free to return to the Jetty I/O system where it may be put back into the thread pool. +If this was the only active network connection, the system would now be idle, with no threads blocked, waiting that the `write()` completes. This thread-less wait is one of the most important features that make non-blocking asynchronous servers more scalable: they use less resources. + +Eventually, the Jetty I/O system will notify that the `write()` completed; this notifies the `IteratingCallback` that can now resume the loop and call `process()` again. + +When `process()` is called, it is possible that zero bytes are read from the network; in this case, you want to deallocate the buffer since the other peer may never send more bytes for the `Connection` to read, or it may send them after a long pause -- in both cases we do not want to retain the memory allocated by the buffer; next, you want to call `fillInterested()` to declare again interest for read events, and return `Action.IDLE` since there is nothing to write back and therefore the loop may be suspended. +When more bytes are again available to be read from the network, `onFillable()` will be called again and that will start the iteration again. + +Another possibility is that during `process()` the read returns `-1` indicating that the other peer has closed the connection; this means that there will not be more bytes to read and the loop can be exited, so you return `Action.SUCCEEDED`; `IteratingCallback` will then call `onCompleteSuccess()` where you can close the `EndPoint`. + +The last case is that during `process()` an exception is thrown, for example by `EndPoint.fill(ByteBuffer)` or, in more advanced implementations, by code that parses the bytes that have been read and finds them unacceptable; any exception thrown within `process()` will be caught by `IteratingCallback` that will exit the loop with a failure and call `onCompleteFailure(Throwable)` with the exception that has been thrown, where you can close the `EndPoint`, passing the exception that is the reason for closing prematurely the `EndPoint`. + +[IMPORTANT] +==== +Asynchronous programming is hard. + +Rely on the Jetty classes to implement `Connection` to avoid mistakes that will be difficult to diagnose and reproduce. +==== + +[[content-source]] +== `Content.Source` + +The high-level abstraction that Jetty offers to read bytes is `org.eclipse.jetty.io.Content.Source`. + +`Content.Source` offers a non-blocking demand/read model where a read returns a `Content.Chunk` (see also <>). + +A `Content.Chunk` groups the following information: + +* A `ByteBuffer` with the bytes that have been read; it may be empty. +* Whether the read reached end-of-file, via its `last` flag. +* A failure that might have happened during the read, via its `getFailure()` method. + +The `Content.Chunk` returned from `Content.Source.read()` can either be a _normal_ chunk (a chunk containing a `ByteBuffer` and a `null` failure), or a _failure_ chunk (a chunk containing an empty `ByteBuffer` and a non-`null` failure). + +A failure chunk also indicates (via the `last` flag) whether the failure is a fatal (when `last=true`) or transient (when `last=false`) failure. + +A transient failure is a temporary failure that happened during the read, it may be ignored, and it is recoverable: it is possible to call `read()` again and obtain a normal chunk (or a `null` chunk). +Typical cases of transient failures are idle timeout failures, where the read timed out, but the application may decide to insist reading until some other event happens. +The application may convert a transient failure into a fatal failure by calling `Content.Source.fail(Throwable)`. + +A `Content.Source` must be fully consumed by reading all its content, or failed by calling `Content.Source.fail(Throwable)` to signal that the reader is not interested in reading anymore, otherwise it may leak underlying resources. + +Fully consuming a `Content.Source` means reading from it until it returns a `Content.Chunk` whose `last` flag is `true`. +Reading or demanding from an already fully consumed `Content.Source` is always immediately serviced with the last state of the `Content.Source`: a `Content.Chunk` with the `last` flag set to `true`, either an end-of-file chunk, or a failure chunk. + +Once failed, a `Content.Source` is considered fully consumed. +Further attempts to read from a failed `Content.Source` return a failure chunk whose `getFailure()` method returns the exception passed to `Content.Source.fail(Throwable)`. + +When reading a normal chunk, its `ByteBuffer` is typically a slice of a different `ByteBuffer` that has been read by a lower layer. +There may be multiple layers between the bottom layer (where the initial read typically happens) and the application layer that calls `Content.Source.read()`. + +By slicing the `ByteBuffer` (rather than copying its bytes), there is no copy of the bytes between the layers, which yields greater performance. +However, this comes with the cost that the `ByteBuffer`, and the associated `Content.Chunk`, have an intrinsic lifecycle: the final consumer of a `Content.Chunk` at the application layer must indicate when it has consumed the chunk, so that the bottom layer may reuse/recycle the `ByteBuffer`. + +Consuming the chunk means that the bytes in the `ByteBuffer` are read (or ignored), and that the application will not look at or reference that `ByteBuffer` ever again. + +`Content.Chunk` offers a retain/release model to deal with the `ByteBuffer` lifecycle, with a simple rule: + +IMPORTANT: A `Content.Chunk` returned by a call to `Content.Source.read()` **must** be released, except for ``Content.Chunk``s that are failure chunks. +Failure chunks _may_ be released, but they do not _need_ to be. + +The example below is the idiomatic way of reading from a `Content.Source`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=idiomatic] +---- +<1> The `read()` that must be paired with a `release()`. +<2> The `release()` that pairs the `read()`. + +Note how the reads happen in a loop, consuming the `Content.Source` as soon as it has content available to be read, and therefore no backpressure is applied to the reads. + +IMPORTANT: Calling `Content.Chunk.release()` must be done only after the bytes in the `ByteBuffer` returned by `Content.Chunk.getByteBuffer()` have been consumed. +When the `Content.Chunk` is released, the implementation may reuse the `ByteBuffer` and overwrite the bytes with different bytes; if the application looks at the `ByteBuffer` _after_ having released the `Content.Chunk` is may see other, unrelated, bytes. + +An alternative way to read from a `Content.Source`, to use when the chunk is consumed asynchronously, and you don't want to read again until the `Content.Chunk` is consumed, is the following: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=async] +---- +<1> The `read()` that must be paired with a `release()`. +<2> The `release()` that pairs the `read()`. + +Note how the reads do not happen in a loop, and therefore backpressure is applied to the reads, because there is not a next read until the chunk from the previous read has been consumed (and this may take time). + +Since the `Chunk` is consumed asynchronously, you may need to retain it to extend its lifecycle, as explained in <>. + +You can use `Content.Source` static methods to conveniently read (in a blocking way or non-blocking way), for example via `static Content.Source.asStringAsync(Content.Source, Charset)`, or via an `InputStream` using `static Content.Source.asInputStream(Content.Source)`. + +Refer to the `Content.Source` link:{javadoc-url}/org/eclipse/jetty/io/Content.Source.html[`javadocs`] for further details. + +[[content-source-chunk]] +=== `Content.Chunk` + +`Content.Chunk` offers a retain/release API to control the lifecycle of its `ByteBuffer`. + +When ``Content.Chunk``s are consumed synchronously, no additional retain/release API call is necessary, for example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=chunkSync] +---- + +On the other hand, if the `Content.Chunk` is not consumed immediately, then it must be retained, and you must arrange for the `Content.Chunk` to be released at a later time, thus pairing the retain. +For example, you may accumulate the ``Content.Chunk``s in a `List` to convert them to a `String` when all the ``Content.Chunk``s have been read. + +Since reading from a `Content.Source` is asynchronous, the `String` result is produced via a `CompletableFuture`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=chunkAsync] +---- +<1> The `read()` that must be paired with a `release()`. +<2> The `release()` that pairs the `read()`. +<3> The `retain()` that must be paired with a `release()`. +<4> The `release()` that pairs the `retain()`. + +Note how method `consume(Content.Chunk)` retains the `Content.Chunk` because it does not consume it, but rather stores it away for later use. +With this additional retain, the retain count is now `2`: one implicitly from the `read()` that returned the `Content.Chunk`, and one explicit in `consume(Content.Chunk)`. + +However, just after returning from `consume(Content.Chunk)` the `Content.Chunk` is released (pairing the implicit retain from `read()`), so that the retain count goes to `1`, and an additional release is still necessary. + +Method `getResult()` arranges to release all the ``Content.Chunk``s that have been accumulated, pairing the retains done in `consume(Content.Chunk)`, so that the retain count for the ``Content.Chunk``s goes finally to `0`. + +[[content-sink]] +== `Content.Sink` + +The high-level abstraction that Jetty offers to write bytes is `org.eclipse.jetty.io.Content.Sink`. + +The primary method to use is `Content.Sink.write(boolean, ByteBuffer, Callback)`, which performs a non-blocking write of the given `ByteBuffer`, with the indication of whether the write is the last. + +The `Callback` parameter is completed, successfully or with a failure, and possibly asynchronously by a different thread, when the write is complete. + +Your application can typically perform zero or more non-last writes, and one final last write. + +However, because the writes may be asynchronous, you cannot start a next write before the previous write is completed. + +This code is wrong: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=sinkWrong] +---- + +You must initiate a second write only when the first is finished, for example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=sinkMany] +---- + +When you need to perform an unknown number of writes, you must use an `IteratingCallback`, explained in <>, to avoid ``StackOverFlowError``s. + +For example, to copy from a `Content.Source` to a `Content.Sink` you should use the convenience method `Content.copy(Content.Source, Content.Sink, Callback)`. +For illustrative purposes, below you can find the implementation of `copy(Content.Source, Content.Sink, Callback)` that uses an `IteratingCallback`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=copy] +---- + +Non-blocking writes can be easily turned in blocking writes. +This leads to perhaps code that is simpler to read, but that also comes with a price: greater resource usage that may lead to less scalability and less performance. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ContentDocs.java[tags=blocking] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/arch/jmx.adoc b/documentation/jetty/modules/programming-guide/pages/arch/jmx.adoc new file mode 100644 index 000000000000..8324bf6a6dba --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/arch/jmx.adoc @@ -0,0 +1,300 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty JMX Support + +The Java Management Extensions (JMX) APIs are standard API for managing and monitoring resources such as applications, devices, services, and the Java Virtual Machine itself. + +The JMX API includes remote access, so a remote management console such as https://openjdk.java.net/projects/jmc/[Java Mission Control] can interact with a running application for these purposes. + +Jetty architecture is based on xref:arch/bean.adoc[components] organized in a tree. Every time a component is added to or removed from the component tree, an event is emitted, and xref:arch/bean.adoc#listener-container[Container.Listener] implementations can listen to those events and perform additional actions. + +`org.eclipse.jetty.jmx.MBeanContainer` listens to those events and registers/unregisters the Jetty components as MBeans into the platform MBeanServer. + +The Jetty components are annotated with <> so that they can provide specific JMX metadata such as attributes and operations that should be exposed via JMX. + +Therefore, when a component is added to the component tree, `MBeanContainer` is notified, it creates the MBean from the component POJO and registers it to the `MBeanServer`. +Similarly, when a component is removed from the tree, `MBeanContainer` is notified, and unregisters the MBean from the `MBeanServer`. + +The Maven coordinates for the Jetty JMX support are: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty + jetty-jmx + {version} + +---- + +== Enabling JMX Support + +Enabling JMX support is always recommended because it provides valuable information about the system, both for monitoring purposes and for troubleshooting purposes in case of problems. + +To enable JMX support on the server: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=server] +---- + +Similarly on the client: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=client] +---- + +[NOTE] +==== +The MBeans exported to the platform MBeanServer can only be accessed locally (from the same machine), not from remote machines. + +This means that this configuration is enough for development, where you have easy access (with graphical user interface) to the machine where Jetty runs, but it is typically not enough when the machine where Jetty runs is remote, or only accessible via SSH or otherwise without graphical user interface support. +In these cases, you have to enable <>. +==== + +// TODO: add a section about how to expose logging once #4830 is fixed. + +[[remote]] +== Enabling JMX Remote Access + +There are two ways of enabling remote connectivity so that JMC can connect to the remote JVM to visualize MBeans. + +* Use the `com.sun.management.jmxremote` system property on the command line. +Unfortunately, this solution does not work well with firewalls and is not flexible. +* Use Jetty's `ConnectorServer` class. + +`org.eclipse.jetty.jmx.ConnectorServer` will use by default RMI to allow connection from remote clients, and it is a wrapper around the standard JDK class `JMXConnectorServer`, which is the class that provides remote access to JMX clients. + +Connecting to the remote JVM is a two step process: + +* First, the client will connect to the RMI _registry_ to download the RMI stub for the `JMXConnectorServer`; this RMI stub contains the IP address and port to connect to the RMI server, i.e. the remote `JMXConnectorServer`. +* Second, the client uses the RMI stub to connect to the RMI _server_ (i.e. the remote `JMXConnectorServer`) typically on an address and port that may be different from the RMI registry address and port. + +The host and port configuration for the RMI registry and the RMI server is specified by a `JMXServiceURL`. +The string format of an RMI `JMXServiceURL` is: + +[,screen] +---- +service:jmx:rmi://:/jndi/rmi://:/jmxrmi +---- + +Default values are: + +[,screen] +---- +rmi_server_host = localhost +rmi_server_port = 1099 +rmi_registry_host = localhost +rmi_registry_port = 1099 +---- + +With the default configuration, only clients that are local to the server machine can connect to the RMI registry and RMI server - this is done for security reasons. +With this configuration it would still be possible to access the MBeans from remote using a <>. + +By specifying an appropriate `JMXServiceURL`, you can fine tune the network interfaces the RMI registry and the RMI server bind to, and the ports that the RMI registry and the RMI server listen to. +The RMI server and RMI registry hosts and ports can be the same (as in the default configuration) because RMI is able to multiplex traffic arriving to a port to multiple RMI objects. + +If you need to allow JMX remote access through a firewall, you must open both the RMI registry and the RMI server ports. + +`JMXServiceURL` common examples: + +[,screen] +---- +service:jmx:rmi:///jndi/rmi:///jmxrmi + rmi_server_host = local host address + rmi_server_port = randomly chosen + rmi_registry_host = local host address + rmi_registry_port = 1099 + +service:jmx:rmi://0.0.0.0:1099/jndi/rmi://0.0.0.0:1099/jmxrmi + rmi_server_host = any address + rmi_server_port = 1099 + rmi_registry_host = any address + rmi_registry_port = 1099 + +service:jmx:rmi://localhost:1100/jndi/rmi://localhost:1099/jmxrmi + rmi_server_host = loopback address + rmi_server_port = 1100 + rmi_registry_host = loopback address + rmi_registry_port = 1099 +---- + +[NOTE] +==== +When `ConnectorServer` is started, its RMI stub is exported to the RMI registry. +The RMI stub contains the IP address and port to connect to the RMI object, but the IP address is typically the machine host name, not the host specified in the `JMXServiceURL`. + +To control the IP address stored in the RMI stub you need to set the system property `java.rmi.server.hostname` with the desired value. +This is especially important when binding the RMI server host to the loopback address for security reasons. See also <> +==== + +To allow JMX remote access, create and configure a `ConnectorServer`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=remote] +---- + +[[remote-authorization]] +=== JMX Remote Access Authorization + +The standard `JMXConnectorServer` provides several options to authorize access, for example via JAAS or via configuration files. +For a complete guide to controlling authentication and authorization in JMX, see https://docs.oracle.com/en/java/javase/11/management/[the official JMX documentation]. + +In the sections below we detail one way to setup JMX authentication and authorization, using configuration files for users, passwords and roles: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=remoteAuthorization] +---- + +The `users.access` file format is defined in the `$JAVA_HOME/conf/management/jmxremote.access` file. +A simplified version is the following: + +.users.access +[,screen] +---- +user1 readonly +user2 readwrite +---- + +The `users.password` file format is defined in the `$JAVA_HOME/conf/management/jmxremote.password.template` file. +A simplified version is the following: + +.users.password +[,screen] +---- +user1 password1 +user2 password2 +---- + +CAUTION: The `users.access` and `users.password` files are not standard `*.properties` files -- the user must be separated from the role or password by a space character. + +=== Securing JMX Remote Access with TLS + +The JMX communication via RMI happens by default in clear-text. + +It is possible to configure the `ConnectorServer` with a `SslContextFactory` so that the JMX communication via RMI is encrypted: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=tlsRemote] +---- + +It is possible to use the same `SslContextFactory.Server` used to configure the Jetty `ServerConnector` that supports TLS also for the JMX communication via RMI. + +The keystore must contain a valid certificate signed by a Certification Authority. + +The RMI mechanic is the usual one: the RMI client (typically a monitoring console) will connect first to the RMI registry (using TLS), download the RMI server stub that contains the address and port of the RMI server to connect to, then connect to the RMI server (using TLS). + +This also mean that if the RMI registry and the RMI server are on different hosts, the RMI client must have available the cryptographic material to validate both hosts. + +Having certificates signed by a Certification Authority simplifies by a lot the configuration needed to get the JMX communication over TLS working properly. + +If that is not the case (for example the certificate is self-signed), then you need to specify the required system properties that allow RMI (especially when acting as an RMI client) to retrieve the cryptographic material necessary to establish the TLS connection. + +For example, trying to connect using the JDK standard `JMXConnector` with both the RMI server and the RMI registry via TLS to `domain.com` with a self-signed certificate: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=tlsJMXConnector] +---- + +Similarly, to launch JMC: + +[,screen] +---- +$ jmc -vmargs -Djavax.net.ssl.trustStore=/path/to/trustStore -Djavax.net.ssl.trustStorePassword=secret +---- + +IMPORTANT: These system properties are required when launching the `ConnectorServer` too, on the server, because it acts as an RMI client with respect to the RMI registry. + +[[remote-ssh-tunnel]] +=== JMX Remote Access with Port Forwarding via SSH Tunnel + +You can access JMX MBeans on a remote machine when the RMI ports are not open, for example because of firewall policies, but you have SSH access to the machine using local port forwarding via an SSH tunnel. + +In this case you want to configure the `ConnectorServer` with a `JMXServiceURL` that binds the RMI server and the RMI registry to the loopback interface only: `service:jmx:rmi://localhost:1099/jndi/rmi://localhost:1099/jmxrmi`. + +Then you setup the local port forwarding with the SSH tunnel: + +[,screen] +---- +$ ssh -L 1099:localhost:1099 @ +---- + +Now you can use JConsole or JMC to connect to `localhost:1099` on your local computer. +The traffic will be forwarded to `machine_host` and when there, SSH will forward the traffic to `localhost:1099`, which is exactly where the `ConnectorServer` listens. + +When you configure `ConnectorServer` in this way, you must set the system property `-Djava.rmi.server.hostname=localhost`, on the server. +This is required because when the RMI server is exported, its address and port are stored in the RMI stub. You want the address in the RMI stub to be `localhost` so that when the RMI stub is downloaded to the remote client, the RMI communication will go through the SSH tunnel. + +[[annotation]] +== Jetty JMX Annotations + +The Jetty JMX support, and in particular `MBeanContainer`, is notified every time a bean is added to the component tree. + +The bean is scanned for Jetty JMX annotations to obtain JMX metadata: the JMX attributes and JMX operations. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=jmxAnnotation] +---- + +The JMX metadata and the bean are wrapped by an instance of `org.eclipse.jetty.jmx.ObjectMBean` that exposes the JMX metadata and, upon request from JMX consoles, invokes methods on the bean to get/set attribute values and perform operations. + +You can provide a custom subclass of `ObjectMBean` to further customize how the bean is exposed to JMX. + +The custom `ObjectMBean` subclass must respect the following naming convention: `.jmx.MBean`. +For example, class `com.acme.Foo` may have a custom `ObjectMBean` subclass named `com.acme.**jmx**.Foo**MBean**`. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=jmxCustomMBean] +---- + +The custom `ObjectMBean` subclass is also scanned for Jetty JMX annotations and overrides the JMX metadata obtained by scanning the bean class. +This allows to annotate only the custom `ObjectMBean` subclass and keep the bean class free of the Jetty JMX annotations. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/JMXDocs.java[tags=jmxCustomMBeanOverride] +---- + +The scan for Jetty JMX annotations is performed on the bean class and all the interfaces implemented by the bean class, then on the super-class and all the interfaces implemented by the super-class and so on until `java.lang.Object` is reached. +For each type -- class or interface, the corresponding `+*.jmx.*MBean+` is looked up and scanned as well with the same algorithm. +For each type, the scan looks for the class-level annotation `@ManagedObject`. +If it is found, the scan looks for method-level `@ManagedAttribute` and `@ManagedOperation` annotations; otherwise it skips the current type and moves to the next type to scan. + +=== @ManagedObject + +The `@ManagedObject` annotation is used on a class at the top level to indicate that it should be exposed as an MBean. +It has only one attribute to it which is used as the description of the MBean. + +=== @ManagedAttribute + +The `@ManagedAttribute` annotation is used to indicate that a given method is exposed as a JMX attribute. +This annotation is placed always on the getter method of a given attribute. +Unless the `readonly` attribute is set to `true` in the annotation, a corresponding setter is looked up following normal naming conventions. +For example if this annotation is on a method called `String getFoo()` then a method called `void setFoo(String)` would be looked up, and if found wired as the setter for the JMX attribute. + +=== @ManagedOperation + +The `@ManagedOperation` annotation is used to indicate that a given method is exposed as a JMX operation. +A JMX operation has an _impact_ that can be `INFO` if the operation returns a value without modifying the object, `ACTION` if the operation does not return a value but modifies the object, and "ACTION_INFO" if the operation both returns a value and modifies the object. +If the _impact_ is not specified, it has the default value of `UNKNOWN`. + +=== @Name + +The `@Name` annotation is used to assign a name and description to parameters in method signatures so that when rendered by JMX consoles it is clearer what the parameter meaning is. diff --git a/documentation/jetty/modules/programming-guide/pages/arch/listener.adoc b/documentation/jetty/modules/programming-guide/pages/arch/listener.adoc new file mode 100644 index 000000000000..443e5cf04dbd --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/arch/listener.adoc @@ -0,0 +1,34 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Listeners + +The Jetty architecture is based on xref:arch/bean.adoc[components], typically organized in a component tree. +These components have an internal state that varies with the component life cycle (that is, whether the component is started or stopped), as well as with the component use at runtime. +The typical example is a thread pool, whose internal state -- such as the number of pooled threads or the job queue size -- changes as the thread pool is used by the running client or server. + +In many cases, the component state change produces an event that is broadcast to listeners. +Applications can register listeners to these components to be notified of the events they produce. + +This section lists the listeners available in the Jetty components, but the events and listener APIs are discussed in the component specific sections. + +Listeners common to both client and server: + +* xref:arch/bean.adoc#listener[] +* xref:arch/io.adoc#connection-listener[] +* xref:client/http.adoc#configuration-tls-listener[] +* xref:server/http.adoc#connector-protocol[] + +Listeners that are server specific: + +* xref:server/http.adoc#request-processing-events[] diff --git a/documentation/jetty/modules/programming-guide/pages/arch/threads.adoc b/documentation/jetty/modules/programming-guide/pages/arch/threads.adoc new file mode 100644 index 000000000000..e8b7016643e0 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/arch/threads.adoc @@ -0,0 +1,257 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Threading Architecture + +Writing a performant client or server is difficult, because it should: + +* Scale well with the number of processors. +* Be efficient at using processor caches to avoid https://en.wikipedia.org/wiki/Parallel_slowdown[parallel slowdown]. +* Support multiple network protocols that may have very different requirements; for example, multiplexed protocols such as HTTP/2 introduce new challenges that are not present in non-multiplexed protocols such as HTTP/1.1. +* Support different application threading models; for example, if a Jetty server invokes server-side application code that is allowed to call blocking APIs, then the Jetty server should not be affected by how long the blocking API call takes, and should be able to process other connections or other requests in a timely fashion. + +[[execution-strategy]] +== Execution Strategies + +The Jetty threading architecture can be modeled with a producer/consumer pattern, where produced tasks needs to be consumed efficiently. + +For example, Jetty produces (among others) these tasks: + +* A task that wraps a NIO selection event, see the xref:arch/io.adoc[Jetty I/O architecture]. +* A task that wraps the invocation of application code that may block (for example, the invocation of a Servlet to handle an HTTP request). + +A task is typically a `Runnable` object that may implement `org.eclipse.jetty.util.thread.Invocable` to indicate the behavior of the task (in particular, whether the task may block or not). + +Once a task has been produced, it may be consumed using these modes: + +* <> +* <> +* <> + +[[execution-strategy-pc]] +=== Produce-Consume +In the `Produce-Consume` mode, the producer thread loops to produce a task that is run directly by the `Producer Thread`. + +[plantuml] +---- +skinparam backgroundColor transparent + +compact concise "Producer Thread" as PT +hide time-axis + +@PT +0 is T1 #lightgreen +1 is "Run T1" #dodgerblue +5 is T2 #lightgreen +6 is "Run T2" #dodgerblue +8 is T3 #lightgreen +9 is "Run T3" #dodgerblue +12 is {hidden} +---- + +If the task is a NIO selection event, then this mode is the thread-per-selector mode which is very CPU core cache efficient, but suffers from the http://en.wikipedia.org/wiki/Head-of-line_blocking[head-of-line blocking]: if one of the tasks blocks or runs slowly, then subsequent tasks cannot be produced (and therefore cannot be consumed either) and will pay in latency the cost of running previous, possibly unrelated, tasks. + +This mode should only be used if the produced task is known to never block, or if the system tolerates well (or does not care about) head-of-line blocking. + +[[execution-strategy-pec]] +=== Produce-Execute-Consume +In the `Produce-Execute-Consume` mode, the `Producer Thread` loops to produce tasks that are submitted to a `java.util.concurrent.Executor` to be run by ``Worker Thread``s different from the `Producer Thread`. + +[plantuml] +---- +skinparam backgroundColor transparent + +compact concise "Producer Thread" as PT +compact concise "Worker Thread 1" as WT1 +compact concise "Worker Thread 2" as WT2 +compact concise "Worker Thread 3" as WT3 +hide time-axis + +@PT +0 is T1 #lightgreen +1 is T2 #lightgreen +2 is T3 #lightgreen +3 is T4 #lightgreen +4 is {hidden} + +@WT1 +1 is "Run T1" #dodgerblue +5 is {hidden} + +@WT2 +2 is "Run T2" #dodgerblue +4 is "Run T4" #dodgerblue +8 is {hidden} + +@WT3 +3 is "Run T3" #dodgerblue +6 is {hidden} +---- + +The `Executor` implementation typically adds the task to a queue, and dequeues the task when there is a worker thread available to run it. + +This mode solves the head-of-line blocking discussed in the <>, but suffers from other issues: + +* It is not CPU core cache efficient, as the data available to the producer thread will need to be accessed by another thread that likely is going to run on a CPU core that will not have that data in its caches. +* If the tasks take time to be run, the `Executor` queue may grow indefinitely. +* A small latency is added to every task: the time it waits in the `Executor` queue. + +[[execution-strategy-epc]] +=== Execute-Produce-Consume +In the `Execute-Produce-Consume` mode, the producer thread `Thread 1` loops to produce a task, then submits one internal task to an `Executor` to take over production on thread `Thread 2`, and then runs the task in `Thread 1`, and so on. + +[plantuml] +---- +skinparam backgroundColor transparent + +compact concise "Thread 1" as WT1 +compact concise "Thread 2" as WT2 +compact concise "Thread 3" as WT3 +compact concise "Thread 4" as WT4 +hide time-axis + +@WT1 +0 is T1 #lightgreen +1 is "Run T1" #dodgerblue +5 is {hidden} + +@WT2 +1 is T2 #lightgreen +2 is "Run T2" #dodgerblue +4 is T5 #lightgreen +5 is "Run T5" #dodgerblue +10 is {hidden} + +@WT3 +2 is T3 #lightgreen +3 is "Run T3" #dodgerblue +6 is {hidden} + +@WT4 +3 is T4 #lightgreen +4 is "Run T4" #dodgerblue +8 is {hidden} + +---- + +This mode may operate like <> when the take over production task run, for example, by thread `Thread 3` takes time to be executed (for example, in a busy server): then thread `Thread 2` will produce one task and run it, then produce another task and run it, etc. -- `Thread 2` behaves exactly like the `Produce-Consume` mode. +By the time thread `Thread 3` takes over task production from `Thread 2`, all the work might already be done. + +This mode may also operate similarly to <> when the take over production task always finds a free CPU core immediately (for example, in a mostly idle server): thread `Thread 1` will produce a task, yield production to `Thread 2` while `Thread 1` is running the task; `Thread 2` will produce a task, yield production to `Thread 3` while `Thread 2` is running the task, etc. + +Differently from `Produce-Execute-Consume`, here production happens on different threads, but the advantage is that the task is run by the same thread that produced it (which is CPU core cache efficient). + +[[execution-strategy-adaptive]] +=== Adaptive Execution Strategy +The modes of task consumption discussed above are captured by the `org.eclipse.jetty.util.thread.ExecutionStrategy` interface, with an additional implementation that also takes into account the behavior of the task when the task implements `Invocable`. + +For example, a task that declares itself as non-blocking can be consumed using the `Produce-Consume` mode, since there is no risk to stop production because the task will not block. + +Conversely, a task that declares itself as blocking will stop production, and therefore must be consumed using either the `Produce-Execute-Consume` mode or the `Execute-Produce-Consume` mode. +Deciding between these two modes depends on whether there is a free thread immediately available to take over production, and this is captured by the `org.eclipse.jetty.util.thread.TryExecutor` interface. + +An implementation of `TryExecutor` can be asked whether a thread can be immediately and exclusively allocated to run a task, as opposed to a normal `Executor` that can only queue the task in the expectation that there will be a thread available in the near future to run the task. + +The concept of task consumption modes, coupled with `Invocable` tasks that expose their own behavior, coupled with a `TryExecutor` that guarantees whether production can be immediately taken over are captured by the default Jetty execution strategy, named `org.eclipse.jetty.util.thread.AdaptiveExecutionStrategy`. + +[NOTE] +==== +`AdaptiveExecutionStrategy` was previously named `EatWhatYouKill`, named after a hunting proverb in the sense that one should produce (kill) only what it consumes (eats). +==== + +[[thread-pool]] +== Thread Pool +Jetty's xref:arch/threads.adoc[threading architecture] requires a more sophisticated thread pool than what offered by Java's `java.util.concurrent.ExecutorService`. + +Jetty's default thread pool implementation is link:{javadoc-url}/org/eclipse/jetty/util/thread/QueuedThreadPool.html[`QueuedThreadPool`]. + +`QueuedThreadPool` integrates with the xref:arch/bean.adoc[Jetty component model], implements `Executor`, provides a `TryExecutor` implementation (discussed in the <>), and supports <> (introduced as a preview feature in Java 19 and Java 20, and as an official feature since Java 21). + +[[thread-pool-queue]] +=== Thread Pool Queue + +`QueuedThreadPool` uses a `BlockingQueue` to store tasks that will be executed as soon as a thread is available. + +It is common, but too simplistic, to think that an upper bound to the thread pool queue is a good way to limit the number of concurrent HTTP requests. + +In case of asynchronous servers like Jetty, applications may have more than one thread handling a single request. +Furthermore, the server implementation may produce a number of tasks that _must_ be run by the thread pool, otherwise the server stops working properly. + +Therefore, the "one-thread-per-request" model is too simplistic, and the real model that predicts the number of threads that are necessary is too complicated to produce an accurate value. + +For example, a sudden large spike of requests arriving to the server may find the thread pool in an idle state where the number of threads is shrunk to the minimum. +This will cause many tasks to be queued up, way before an HTTP request is even read from the network. +Add to this that there could be I/O failures processing requests, which may be submitted as a new task to the thread pool. +Furthermore, multiplexed protocols like HTTP/2 have a much more complex model (due to xref:server/http2.adoc#flow-control[data flow control]). +For multiplexed protocols, the implementation must be able to write in order to progress reads (and must be able to read in order to progress writes), possibly causing more tasks to be submitted to the thread pool. + +If any of the submitted tasks is rejected because the queue is bounded the server may grind to a halt, because the task _must_ be executed, sometimes _necessarily_ in a different thread. + +For these reasons: + +IMPORTANT: The thread pool queue must be unbounded. + +There are better strategies to limit the number of concurrent requests, discussed in xref:server/http.adoc#handler-use-qos[this section]. + +[[thread-pool-configuration]] +=== `QueuedThreadPool` configuration + +`QueuedThreadPool` can be configured with a `maxThreads` value. + +However, some of the Jetty components (such as the xref:arch/io.adoc#selector-manager[selectors]) permanently steal threads for their internal use, or rather `QueuedThreadPool` leases some threads to these components. +These threads are reported by `QueuedThreadPool.leasedThreads` and are not available to run application code. + +`QueuedThreadPool` can be configured with a `reservedThreads` value. +This value represents the maximum number of threads that can be reserved and used by the `TryExecutor` implementation. +A negative value for `QueuedThreadPool.reservedThreads` means that the actual value will be heuristically derived from the number of CPU cores and `QueuedThreadPool.maxThreads`. +A value of zero for `QueuedThreadPool.reservedThreads` means that reserved threads are disabled, and therefore the <> is never used -- the <> is always used instead. + +`QueuedThreadPool` always maintains the number of threads between `QueuedThreadPool.minThreads` and `QueuedThreadPool.maxThreads`; during load spikes the number of thread grows to meet the load demand, and when the load on the system diminishes or the system goes idle, the number of threads shrinks. + +Shrinking `QueuedThreadPool` is important in particular in containerized environments, where typically you want to return the memory occupied by the threads to the operative system. +The shrinking of the `QueuedThreadPool` is controlled by two parameters: `QueuedThreadPool.idleTimeout` and `QueuedThreadPool.maxEvictCount`. + +`QueuedThreadPool.idleTimeout` indicates how long a thread should stay around when it is idle, waiting for tasks to execute. +The longer the threads stay around, the more ready they are in case of new load spikes on the system; however, they consume resources: a Java platform thread typically allocates 1 MiB of native memory. + +`QueuedThreadPool.maxEvictCount` controls how many idle threads are evicted for one `QueuedThreadPool.idleTimeout` period. +The larger this value is, the quicker the threads are evicted when the `QueuedThreadPool` is idle or has less load, and their resources returned to the operative system; however, large values may result in too much thread thrashing: the `QueuedThreadPool` shrinks too fast and must re-create a lot of threads in case of a new load spike on the system. + +A good balance between `QueuedThreadPool.idleTimeout` and `QueuedThreadPool.maxEvictCount` depends on the load profile of your system, and it is often tuned via trial and error. + +[[thread-pool-virtual-threads]] +=== Virtual Threads +Virtual threads have been introduced in Java 19 and Java 20 as a preview feature, and have become an official feature since Java 21. + +NOTE: In Java versions where virtual threads are a preview feature, remember to add `+--enable-preview+` to the JVM command line options to use virtual threads. + +`QueuedThreadPool` can be configured to use virtual threads by specifying the virtual threads `Executor`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/ArchitectureDocs.java[tags=virtual] +---- + +[CAUTION] +==== +Jetty cannot enforce that the `Executor` passed to `setVirtualThreadsExecutor(Executor)` uses virtual threads, so make sure to specify a _virtual_ threads `Executor` and not a normal `Executor` that uses platform threads. +==== + +`AdaptiveExecutionStrategy` makes use of this setting when it determines that a task should be run with the <>: rather than submitting the task to `QueuedThreadPool` to be run in a platform thread, it submits the task to the virtual threads `Executor`. + +[NOTE] +==== +Enabling virtual threads in `QueuedThreadPool` will default the number of reserved threads to zero, unless the number of reserved threads is explicitly configured to a positive value. + +Defaulting the number of reserved threads to zero ensures that the <> is always used, which means that virtual threads will always be used for blocking tasks. +==== diff --git a/documentation/jetty/modules/programming-guide/pages/client/http.adoc b/documentation/jetty/modules/programming-guide/pages/client/http.adoc new file mode 100644 index 000000000000..72ecc4ed1725 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/client/http.adoc @@ -0,0 +1,1008 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += HTTP Client + +[[intro]] +== HttpClient Introduction + +The Jetty HTTP client module provides easy-to-use APIs and utility classes to perform HTTP (or HTTPS) requests. + +Jetty's HTTP client is non-blocking and asynchronous. +It offers an asynchronous API that never blocks for I/O, making it very efficient in thread utilization and well suited for high performance scenarios such as load testing or parallel computation. + +However, when all you need to do is to perform a `GET` request to a resource, Jetty's HTTP client offers also a synchronous API; a programming interface where the thread that issued the request blocks until the request/response conversation is complete. + +Jetty's HTTP client supports different <>: HTTP/1.1, HTTP/2, HTTP/3 and FastCGI. +Each format has a different `HttpClientTransport` implementation, that in turn use a xref:client/io-arch.adoc#transport[low-level transport] to communicate with the server. + +This means that the semantic of an HTTP request such as: " ``GET`` the resource ``/index.html`` " can be carried over the low-level transport in different formats. +The most common and default format is HTTP/1.1. +That said, Jetty's HTTP client can carry the same request using the HTTP/2 format, the HTTP/3 format, or the FastCGI format. + +Furthermore, every format can be transported over different low-level transport, such as TCP, Unix-Domain sockets, QUIC or memory. +Supports for Unix-Domain sockets requires Java 16 or later, since Unix-Domain sockets support has been introduced in OpenJDK with https://openjdk.java.net/jeps/380[JEP 380]. + +The <> is used in Jetty's xref:server/fastcgi.adoc[FastCGI support] that allows Jetty to work as a reverse proxy to PHP (exactly like Apache or Nginx do) and therefore be able to serve, for example, WordPress websites, often in conjunction with Unix-Domain sockets (although it is possible to use FastCGI via network too). + +The HTTP/2 format allows Jetty's HTTP client to perform requests using HTTP/2 to HTTP/2 enabled websites, see also Jetty's xref:client/http2.adoc[HTTP/2 support]. + +The HTTP/3 format allows Jetty's HTTP client to perform requests using HTTP/3 to HTTP/3 enabled websites, see also Jetty's xref:client/http3.adoc[HTTP/3 support]. + +Out of the box features that you get with the Jetty HTTP client include: + +* Redirect support -- redirect codes such as 302 or 303 are automatically followed. +* Cookies support -- cookies sent by servers are stored and sent back to servers in matching requests. +* Authentication support -- HTTP "Basic", "Digest" and "SPNEGO" authentications are supported, others are pluggable. +* Forward proxy support -- HTTP proxying, SOCKS4 and SOCKS5 proxying. + +[[start]] +== Starting HttpClient + +The Jetty artifact that provides the main HTTP client implementation is `jetty-client`. +The Maven artifact coordinates are the following: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty + jetty-client + {version} + +---- + +The main class is named `org.eclipse.jetty.client.HttpClient`. + +You can think of a `HttpClient` instance as a browser instance. +Like a browser it can make requests to different domains, it manages redirects, cookies and authentication, you can configure it with a forward proxy, and it provides you with the responses to the requests you make. + +In order to use `HttpClient`, you must instantiate it, configure it, and then start it: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=start] +---- + +You may create multiple instances of `HttpClient`, but typically one instance is enough for an application. +There are several reasons for having multiple `HttpClient` instances including, but not limited to: + +* You want to specify different configuration parameters (for example, one instance is configured with a forward proxy while another is not). +* You want the two instances to behave like two different browsers and hence have different cookies, different authentication credentials, etc. +* You want to use <>. + +Like browsers, HTTPS requests are supported out-of-the-box (see <> for the TLS configuration), as long as the server provides a valid certificate. +In case the server does not provide a valid certificate (or in case it is self-signed) you want to customize ``HttpClient``'s TLS configuration as described in <>. + +[[stop]] +== Stopping HttpClient + +It is recommended that when your application stops, you also stop the `HttpClient` instance (or instances) that you are using. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=stop] +---- + +Stopping `HttpClient` makes sure that the memory it holds (for example, `ByteBuffer` pools, authentication credentials, cookies, etc.) is released, and that the thread pool and scheduler are properly stopped allowing all threads used by `HttpClient` to exit. + +[NOTE] +==== +You cannot call `HttpClient.stop()` from one of its own threads, as it would cause a deadlock. +It is recommended that you stop `HttpClient` from an unrelated thread, or from a newly allocated thread, for example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=stopFromOtherThread] +---- +==== + +[[arch]] +== HttpClient Architecture + +A `HttpClient` instance can be thought as a browser instance, and it manages the following components: + +* A `CookieStore` (see <>). +* A `AuthenticationStore` (see <>). +* A `ProxyConfiguration` (see <>). +* A set of ``Destination``s + +A `Destination` is the client-side component that represents an _origin_ server, and manages a queue of requests for that origin, and a <> to that origin. + +An _origin_ may be simply thought as the tuple `(scheme, host, port)` and it is where the client connects to in order to communicate with the server. +However, this is not enough. + +If you use `HttpClient` to write a proxy you may have different clients that want to contact the same server. +In this case, you may not want to use the same proxy-to-server connection to proxy requests for both clients, for example for authentication reasons: the server may associate the connection with authentication credentials, and you do not want to use the same connection for two different users that have different credentials. +Instead, you want to use different connections for different clients and this can be achieved by "tagging" a destination with a tag object that represents the remote client (for example, it could be the remote client IP address). + +Two origins with the same `(scheme, host, port)` but different `tag` create two different destinations and therefore two different connection pools. +However, also this is not enough. + +It is possible for a server to speak different protocols on the same `port`. +A connection may start by speaking one protocol, for example HTTP/1.1, but then be upgraded to speak a different protocol, for example HTTP/2. After a connection has been upgraded to a second protocol, it cannot speak the first protocol anymore, so it can only be used to communicate using the second protocol. + +Two origins with the same `(scheme, host, port, tag)` but different `protocol` create two different destinations and therefore two different connection pools. + +Finally, it is possible for a server to speak the same protocol over different xref:client/io-arch.adoc#transport[low-level transports] (represented by `Transport`), for example TCP and Unix-Domain. + +Two origins with the same `(scheme, host, port, tag, protocol)` but different low-level transports create two different destinations and therefore two different connection pools. + +Therefore, an origin is identified by the tuple `(scheme, host, port, tag, protocol, transport)`. + +[[connection-pool]] +== HttpClient Connection Pooling + +A `Destination` manages a `org.eclipse.jetty.client.ConnectionPool`, where connections to a particular origin are pooled for performance reasons: opening a connection is a costly operation, and it's better to reuse them for multiple requests. + +NOTE: Remember that to select a specific `Destination` you must select a specific origin, and that an origin is identified by the tuple `(scheme, host, port, tag, protocol, transport)`, so you can have multiple ``Destination``s for the same `host` and `port`, and therefore multiple ``ConnectionPool``s + +You can access the `ConnectionPool` in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=getConnectionPool] +---- + +Jetty's client library provides the following `ConnectionPool` implementations: + +* `DuplexConnectionPool`, historically the first implementation, only used by the HTTP/1.1 transport. +* `MultiplexConnectionPool`, the generic implementation valid for any transport where connections are reused with a most recently used algorithm (that is, the connections most recently returned to the connection pool are the more likely to be used again). +* `RoundRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with a round-robin algorithm. +* `RandomRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with an algorithm that chooses them randomly. + +The `ConnectionPool` implementation can be customized for each destination in by setting a `ConnectionPool.Factory` on the `HttpClientTransport`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=setConnectionPool] +---- + +[[request-processing]] +== HttpClient Request Processing + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant Application +participant Request +participant HttpClient +participant Destination +participant ConnectionPool +participant Connection + +Application -> HttpClient : newRequest() +HttpClient -> Request ** +Application -> Request : send() +Request -> HttpClient : send() +HttpClient -> Destination ** : get or create +Destination -> ConnectionPool ** : create +HttpClient -> Destination : send(Request) +Destination -> Destination : enqueue(Request) +Destination -> ConnectionPool : acquire() +ConnectionPool -> Connection ** : create +Destination -> Destination : dequeue(Request) +Destination -> Connection : send(Request) +---- + +When a request is sent, an origin is computed from the request; `HttpClient` uses that origin to find (or create if it does not exist) the correspondent destination. +The request is then queued onto the destination, and this causes the destination to ask its connection pool for a free connection. +If a connection is available, it is returned, otherwise a new connection is created. +Once the destination has obtained the connection, it dequeues the request and sends it over the connection. + +The first request to a destination triggers the opening of the first connection. +A second request with the same origin sent _after_ the first request/response cycle is completed may reuse the same connection, depending on the connection pool implementation. +A second request with the same origin sent _concurrently_ with the first request will likely cause the opening of a second connection, depending on the connection pool implementation. +The configuration parameter `HttpClient.maxConnectionsPerDestination` (see also the <>) controls the max number of connections that can be opened for a destination. + +NOTE: If opening connections to a given origin takes a long time, then requests for that origin will queue up in the corresponding destination until the connections are established. + +Each connection can handle a limited number of concurrent requests. +For HTTP/1.1, this number is always `1`: there can only be one outstanding request for each connection. +For HTTP/2 this number is determined by the server `max_concurrent_stream` setting (typically around `100`, i.e. there can be up to `100` outstanding requests for every connection). + +When a destination has maxed out its number of connections, and all connections have maxed out their number of outstanding requests, more requests sent to that destination will be queued. +When the request queue is full, the request will be failed. +The configuration parameter `HttpClient.maxRequestsQueuedPerDestination` (see also the <>) controls the max number of requests that can be queued for a destination. + +[[api]] +== HttpClient API Usage + +`HttpClient` provides two types of APIs: a blocking API and a non-blocking API. + +[[blocking]] +=== HttpClient Blocking APIs + +The simpler way to perform a HTTP request is the following: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=simpleBlockingGet] +---- + +The method `HttpClient.GET(\...)` performs a HTTP `GET` request to the given URI and returns a `ContentResponse` when the request/response conversation completes successfully. + +The `ContentResponse` object contains the HTTP response information: status code, headers and possibly content. +The content length is limited by default to 2 MiB; for larger content see <>. + +If you want to customize the request, for example by issuing a `HEAD` request instead of a `GET`, and simulating a browser user agent, you can do it in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=headFluent] +---- + +This is a shorthand for: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=headNonFluent] +---- + +You first create a request object using `httpClient.newRequest(\...)`, and then you customize it using the fluent API style (that is, a chained invocation of methods on the request object). +When the request object is customized, you call `request.send()` that produces the `ContentResponse` when the request/response conversation is complete. + +IMPORTANT: The `Request` object, despite being mutable, cannot be reused for other requests. +This is true also when trying to send two or more identical requests: you have to create two or more `Request` objects. + +Simple `POST` requests also have a shortcut method: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=postFluent] +---- + +The `POST` parameter values added via the `param()` method are automatically URL-encoded. + +Jetty's `HttpClient` automatically follows redirects, so it handles the typical web pattern http://en.wikipedia.org/wiki/Post/Redirect/Get[POST/Redirect/GET], and the response object contains the content of the response of the `GET` request. +Following redirects is a feature that you can enable/disable on a per-request basis or globally. + +File uploads also require one line, and make use of `java.nio.file` classes: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=fileFluent] +---- + +It is possible to impose a total timeout for the request/response conversation using the `Request.timeout(\...)` method as follows: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=totalTimeout] +---- + +In the example above, when the 5 seconds expire, the request/response cycle is aborted and a `java.util.concurrent.TimeoutException` is thrown. + +[[non-blocking]] +=== HttpClient Non-Blocking APIs + +So far we have shown how to use Jetty HTTP client in a blocking style -- that is, the thread that issues the request blocks until the request/response conversation is complete. + +This section will look at Jetty's `HttpClient` non-blocking, asynchronous APIs that are perfectly suited for large content downloads, for parallel processing of requests/responses and in cases where performance and efficient thread and resource utilization is a key factor. + +The asynchronous APIs rely heavily on listeners that are invoked at various stages of request and response processing. +These listeners are implemented by applications and may perform any kind of logic. +The implementation invokes these listeners in the same thread that is used to process the request or response. +Therefore, if the application code in these listeners takes a long time to execute, the request or response processing is delayed until the listener returns. + +If you need to execute application code that takes long time inside a listener, it is typically better to spawn your own thread to execute the code that takes long time. +In this way you return from the listener as soon as possible and allow the implementation to resume the processing of the request or response (or of other requests/responses). + +Request and response processing are executed by two different threads and therefore may happen concurrently. +A typical example of this concurrent processing is an echo server, where a large upload may be concurrent with the large download echoed back. + +NOTE: Remember that responses may be processed and completed _before_ requests; a typical example is a large upload that triggers a quick response, for example an error, by the server: the response may arrive and be completed while the request content is still being uploaded. + +The application thread that calls `Request.send(Response.CompleteListener)` performs the <> until either the request is fully sent over the network or until it would block on I/O, then it returns (and therefore never blocks). +If it would block on I/O, the thread asks the I/O system to emit an event when the I/O will be ready to continue, then returns. +When such an event is fired, a thread taken from the `HttpClient` thread pool will resume the processing of the request. + +Response are processed from the I/O thread taken from the `HttpClient` thread pool that processes the event that bytes are ready to be read. +Response processing continues until either the response is fully processed or until it would block for I/O. +If it would block for I/O, the thread asks the I/O system to emit an event when the I/O will be ready to continue, then returns. +When such an event is fired, a (possibly different) thread taken from the `HttpClient` thread pool will resume the processing of the response. + +When the request and the response are both fully processed, the thread that finished the last processing (usually the thread that processes the response, but may also be the thread that processes the request -- if the request takes more time than the response to be processed) is used to dequeue the next request for the same destination and to process it. + +A simple non-blocking `GET` request that discards the response content can be written in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=simpleNonBlocking] +---- + +Method `Request.send(Response.CompleteListener)` returns `void` and does not block; the `Response.CompleteListener` lambda provided as a parameter is notified when the request/response conversation is complete, and the `Result` parameter allows you to access the request and response objects as well as failures, if any. + +You can impose a total timeout for the request/response conversation in the same way used by the synchronous API: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=nonBlockingTotalTimeout] +---- + +The example above will impose a total timeout of 3 seconds on the request/response conversation. + +The HTTP client APIs use listeners extensively to provide hooks for all possible request and response events: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=listeners] +---- + +This makes Jetty HTTP client suitable for HTTP load testing because, for example, you can accurately time every step of the request/response conversation (thus knowing where the request/response time is really spent). + +[IMPORTANT] +==== +The code in request and response listeners __should not__ block. + +It is allowed to call other blocking APIs, such as the Java file-system APIs. +You should not call blocking APIs that: + +* Wait for other request or response events, such as receiving other request or response content chunks. +* Use wait/notify primitives such as those available in `java.lang.Object` or `java.util.concurrent.locks.Condition`. + +If the listener code blocks, the implementation also will be blocked and will not be able to advance the processing of the request or response that the listener code is likely waiting for, causing a deadlock. +==== + +Have a look at the link:{javadoc-url}/org/eclipse/jetty/client/api/Request.Listener.html[`Request.Listener`] class to know about request events, and to the link:{javadoc-url}/org/eclipse/jetty/client/api/Response.Listener.html[`Response.Listener`] class to know about response events. + +[[content-request]] +=== Request Content Handling + +Jetty's `HttpClient` provides a number of utility classes off the shelf to handle request content. + +You can provide request content as `String`, `byte[]`, `ByteBuffer`, `java.nio.file.Path`, `InputStream`, and provide your own implementation of `org.eclipse.jetty.client.Request.Content`. +Here’s an example that provides the request content using `java.nio.file.Paths`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=pathRequestContent] +---- + +Alternatively, you can use `FileInputStream` via the `InputStreamRequestContent` utility class: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=inputStreamRequestContent] +---- + +Since `InputStream` is blocking, then also the send of the request will block if the input stream blocks, even in case of usage of the non-blocking `HttpClient` APIs. + +If you have already read the content in memory, you can pass it as a `byte[]` (or a `String`) using the `BytesRequestContent` (or `StringRequestContent`) utility class: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=bytesStringRequestContent] +---- + +If the request content is not immediately available, but your application will be notified of the content to send, you can use `AsyncRequestContent` in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=asyncRequestContent] +---- + +While the request content is awaited and consequently uploaded by the client application, the server may be able to respond (at least with the response headers) completely asynchronously. +In this case, `Response.Listener` callbacks will be invoked before the request is fully sent. +This allows fine-grained control of the request/response conversation: for example the server may reject contents that are too big, send a response to the client, which in turn may stop the content upload. + +Another way to provide request content is by using an `OutputStreamRequestContent`, which allows applications to write request content when it is available to the `OutputStream` provided by `OutputStreamRequestContent`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=outputStreamRequestContent] +---- + +[[content-response]] +=== Response Content Handling + +Jetty's `HttpClient` allows applications to handle response content in different ways. + +You can buffer the response content in memory; this is done when using the <> and the content is buffered within a `ContentResponse` up to 2 MiB. + +If you want to control the length of the response content (for example limiting to values smaller than the default of 2 MiB), then you can use a `org.eclipse.jetty.client.CompletableResponseListener` in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=completableResponseListener] +---- + +If the response content length is exceeded, the response will be aborted, and an exception will be thrown by method `get(\...)`. + +You can buffer the response content in memory also using the <>, via the `BufferingResponseListener` utility class: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=bufferingResponseListener] +---- + +If you want to avoid buffering, you can wait for the response and then stream the content using the `InputStreamResponseListener` utility class: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=inputStreamResponseListener] +---- + +Finally, let's look at the advanced usage of the response content handling. + +The response content is provided by the `HttpClient` implementation to application listeners following the read/demand model of `org.eclipse.jetty.io.Content.Source`. + +The listener that follows this model is `Response.ContentSourceListener`. + +After the response headers have been processed by the `HttpClient` implementation, `Response.ContentSourceListener.onContentSource(response, contentSource)` is invoked once and only once. +This allows the application to control precisely the read/demand loop: when to read a chunk, how to process it and when to demand the next one. + +You must provide a `ContentSourceListener` whose implementation reads a `Content.Chunk` from the provided `Content.Source`, as explained in xref:arch/io.adoc#content-source[this section]. + +The invocation of `onContentSource(Request, Content.Source)` and of the demand callback passed to `contentSource.demand(Runnable)` are serialized with respect to asynchronous events such as timeouts or an asynchronous call to `Request.abort(Throwable)`. +This means that these asynchronous events are not processed until the invocation of `onContentSource(Request, Content.Source)` returns, or until the invocation of the demand callback returns. +With this model, applications should not worry too much about concurrent asynchronous events happening during response content handling, because they will eventually see the events as failures while reading the response content. + +Demanding for content and consuming the content are orthogonal activities. + +An application can read, store aside the `Content.Chunk` objects without releasing them (to consume them later), and demand for more chunks, but it must call `Chunk.retain()` on the stored chunks, and arrange to release them after they have been consumed later. + +If not done carefully, this may lead to excessive memory consumption, since the `ByteBuffer` bytes are not consumed. +Releasing the ``Content.Chunk``s will result in the ``ByteBuffer``s to be disposed/recycled and may be performed at any time. + +An application can also read one chunk of content, consume it, release it, and then _not_ demand for more content until a later time. + +Subclass `Response.AsyncContentListener` overrides the behavior of `Response.ContentSourceListener`; when an application implements `AsyncContentListener.onContent(response, chunk, demander)`, it can control the disposing/recycling of the `ByteBuffer` by releasing the chunk _and_ it can control when to demand one more chunk by calling `demander.run()`. + +Subclass `Response.ContentListener` overrides the behavior of `Response.AsyncContentListener`; when an application implementing its `onContent(response, buffer)` returns from the method itself, it will _both_ the effect of disposing/recycling the `buffer` _and_ the effect of demanding one more chunk of content. + +An application that implements a forwarder between two servers can be implemented efficiently by handling the response content without copying the `ByteBuffer` bytes as in the following example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=forwardContent] +---- + +[[api-transport]] +=== Request `Transport` + +The communication between client and server happens over a xref:client/io-arch.adoc#transport[low-level transport], and applications can specify the low-level transport to use for each request. + +This gives client applications great flexibility, because they can use the same `HttpClient` instance to communicate, for example, with an external third party web application via TCP, to a different process via Unix-Domain sockets, and efficiently to the same process via memory. + +Client application can also choose more esoteric configurations such as using QUIC, typically used to transport HTTP/3, to transport HTTP/1.1 or HTTP/2, because QUIC provides reliable and ordered communication like TCP does. + +Provided you have configured a xref:server/http.adoc#connector[`UnixDomainServerConnector`] on the server, this is how you can configure a request to use Unix-Domain sockets: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=unixDomain] +---- + +In the same way, if you have configured a xref:server/http.adoc#connector[`MemoryConnector`] on the server, this is how you can configure a request to use memory for communication: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=memory] +---- + +This is a fancy example of how to mix HTTP versions and low-level transports: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=mixedTransports] +---- + +[[configuration]] +== HttpClient Configuration + +`HttpClient` has a quite large number of configuration parameters. +Please refer to the `HttpClient` link:{javadoc-url}/org/eclipse/jetty/client/HttpClient.html[javadocs] for the complete list of configurable parameters. + +The most common parameters are: + +* `HttpClient.idleTimeout`: same as `ClientConnector.idleTimeout` described in xref:client/io-arch.adoc#transport[this section]. +* `HttpClient.connectBlocking`: same as `ClientConnector.connectBlocking` described in xref:client/io-arch.adoc#transport[this section]. +* `HttpClient.connectTimeout`: same as `ClientConnector.connectTimeout` described in xref:client/io-arch.adoc#transport[this section]. +* `HttpClient.maxConnectionsPerDestination`: the max number of TCP connections that are opened for a particular destination (defaults to 64). +* `HttpClient.maxRequestsQueuedPerDestination`: the max number of requests queued (defaults to 1024). + +[[configuration-tls]] +=== HttpClient TLS Configuration + +`HttpClient` supports HTTPS requests out-of-the-box like a browser does. + +The support for HTTPS request is provided by a `SslContextFactory.Client` instance, typically configured in the `ClientConnector`. +If not explicitly configured, the `ClientConnector` will allocate a default one when started. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=tlsExplicit] +---- + +The default `SslContextFactory.Client` verifies the certificate sent by the server by verifying the validity of the certificate with respect to the certificate chain, the expiration date, the server host name, etc. +This means that requests to public websites that have a valid certificate (such as `+https://google.com+`) will work out-of-the-box, without the need to specify a KeyStore or a TrustStore. + +However, requests made to sites that return an invalid or a self-signed certificate will fail (like they will in a browser). +An invalid certificate may be expired or have the wrong server host name; a self-signed certificate has a certificate chain that cannot be verified. + +The validation of the server host name present in the certificate is important, to guarantee that the client is connected indeed with the intended server. + +The validation of the server host name is performed at two levels: at the TLS level (in the JDK) and, optionally, at the application level. + +By default, the validation of the server host name at the TLS level is enabled, while it is disabled at the application level. + +You can configure the `SslContextFactory.Client` to skip the validation of the server host name at the TLS level: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=tlsNoValidation] +---- + +When you disable the validation of the server host name at the TLS level, you are strongly recommended to enable it at the application level. +Failing to do so puts you at risk of connecting to a server different from the one you intend to connect to: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=tlsAppValidation] +---- + +Enabling server host name validation at both the TLS level and application level allow you to further restrict the set of server hosts the client can connect to, among those allowed in the certificate sent by the server. + +Entirely disabling server host name validation is not recommended, but may be done in controlled environments. + +Even with server host name validation disabled, the validation of the certificate chain, by validating cryptographic signatures and validity dates is still performed. + +Please refer to the `SslContextFactory.Client` link:{javadoc-url}/org/eclipse/jetty/util/ssl/SslContextFactory.Client.html[javadocs] for the complete list of configurable parameters. + +[[configuration-tls-listener]] +==== HttpClient `SslHandshakeListener` + +Applications may register a `org.eclipse.jetty.io.ssl.SslHandshakeListener` to be notified of TLS handshakes success or failure, by adding the `SslHandshakeListener` as a bean to `HttpClient`: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=sslHandshakeListener] +---- + +[[configuration-tls-truststore]] +==== HttpClient TLS TrustStore Configuration +TODO + +[[configuration-tls-client-certs]] +==== HttpClient TLS Client Certificates Configuration +TODO + +[[cookie]] +== HttpClient Cookie Support + +Jetty's `HttpClient` supports cookies out of the box. + +The `HttpClient` instance receives cookies from HTTP responses and stores them in a `java.net.CookieStore`, a class that is part of the JDK. +When new requests are made, the cookie store is consulted and if there are matching cookies (that is, cookies that are not expired and that match domain and path of the request) then they are added to the requests. + +Applications can programmatically access the cookie store to find the cookies that have been set: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=getCookies] +---- + +Applications can also programmatically set cookies as if they were returned from a HTTP response: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=setCookie] +---- + +Cookies may be added explicitly only for a particular request: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=requestCookie] +---- + +You can remove cookies that you do not want to be sent in future HTTP requests: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=removeCookie] +---- + +If you want to totally disable cookie handling, you can install a `HttpCookieStore.Empty`. +This must be done when `HttpClient` is used in a proxy application, in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=emptyCookieStore] +---- + +You can enable cookie filtering by installing a cookie store that performs the filtering logic in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=filteringCookieStore] +---- + +The example above will retain only cookies that come from the `google.com` domain or sub-domains. + +// TODO: move this section to server-side +=== Special Characters in Cookies + +Jetty is compliant with https://tools.ietf.org/html/rfc6265[RFC6265], and as such care must be taken when setting a cookie value that includes special characters such as `;`. + +Previously, `Version=1` cookies defined in https://tools.ietf.org/html/rfc2109[RFC2109] (and continued in https://tools.ietf.org/html/rfc2965[RFC2965]) allowed for special/reserved characters to be enclosed within double quotes when declared in a `Set-Cookie` response header: + +[,screen] +---- +Set-Cookie: foo="bar;baz";Version=1;Path="/secure" +---- + +This was added to the HTTP Response as follows: + +[,java] +---- +protected void service(HttpServletRequest request, HttpServletResponse response) +{ + jakarta.servlet.http.Cookie cookie = new Cookie("foo", "bar;baz"); + cookie.setPath("/secure"); + response.addCookie(cookie); +} +---- + +The introduction of RFC6265 has rendered this approach no longer possible; users are now required to encode cookie values that use these special characters. +This can be done utilizing `jakarta.servlet.http.Cookie` as follows: + +[,java] +---- +jakarta.servlet.http.Cookie cookie = new Cookie("foo", URLEncoder.encode("bar;baz", "UTF-8")); +---- + +Jetty validates all cookie names and values being added to the `HttpServletResponse` via the `addCookie(Cookie)` method. +If an illegal value is discovered Jetty will throw an `IllegalArgumentException` with the details. + +[[authentication]] +== HttpClient Authentication Support + +Jetty's `HttpClient` supports the `BASIC` and `DIGEST` authentication mechanisms defined by https://tools.ietf.org/html/rfc7235[RFC 7235], as well as the SPNEGO authentication mechanism defined in https://tools.ietf.org/html/rfc4559[RFC 4559]. + +The HTTP _conversation_, the sequence of related HTTP requests, for a request that needs authentication is the following: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant Application +participant HttpClient +participant Server + +Application -> Server : GET /path +Server -> HttpClient : 401 + WWW-Authenticate +HttpClient -> Server : GET + Authentication +Server -> Application : 200 OK +---- + +Upon receiving a HTTP 401 response code, `HttpClient` looks at the `WWW-Authenticate` response header (the server _challenge_) and then tries to match configured authentication credentials to produce an `Authentication` header that contains the authentication credentials to access the resource. + +You can configure authentication credentials in the `HttpClient` instance as follows: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=addAuthentication] +---- + +``Authentication``s are matched against the server challenge first by mechanism (e.g. `BASIC` or `DIGEST`), then by realm and then by URI. + +If an `Authentication` match is found, the application does not receive events related to the HTTP 401 response. +These events are handled internally by `HttpClient` which produces another (internal) request similar to the original request but with an additional `Authorization` header. + +If the authentication is successful, the server responds with a HTTP 200 and `HttpClient` caches the `Authentication.Result` so that subsequent requests for a matching URI will not incur in the additional rountrip caused by the HTTP 401 response. + +It is possible to clear ``Authentication.Result``s in order to force authentication again: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=clearResults] +---- + +Authentication results may be preempted to avoid the additional roundtrip due to the server challenge in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=preemptedResult] +---- + +In this way, requests for the given URI are enriched immediately with the `Authorization` header, and the server should respond with HTTP 200 (and the resource content) rather than with the 401 and the challenge. + +It is also possible to preempt the authentication for a single request only, in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=requestPreemptedResult] +---- + +See also the <> for further information about how authentication works with HTTP proxies. + +[[authentication-spnego]] +=== HttpClient SPNEGO Authentication Support +TODO + +[[proxy]] +== HttpClient Proxy Support + +Jetty's `HttpClient` can be configured to use proxies to connect to destinations. + +These types of proxies are available out of the box: + +* HTTP proxy (provided by class `org.eclipse.jetty.client.HttpProxy`) +* SOCKS 4 proxy (provided by class `org.eclipse.jetty.client.Socks4Proxy`) +* <> (provided by class `org.eclipse.jetty.client.Socks5Proxy`) + +Other implementations may be written by subclassing `ProxyConfiguration.Proxy`. + +The following is a typical configuration: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=proxy] +---- + +You specify the proxy host and proxy port, and optionally also the addresses that you do not want to be proxied, and then add the proxy configuration on the `ProxyConfiguration` instance. + +Configured in this way, `HttpClient` makes requests to the HTTP proxy (for plain-text HTTP requests) or establishes a tunnel via HTTP `CONNECT` (for encrypted HTTPS requests). + +Proxying is supported for any version of the HTTP protocol. + +The communication between the client and the proxy may be encrypted, so that it would not be possible for another party on the same network as the client to know what servers the client connects to. + +[[proxy-socks5]] +=== SOCKS5 Proxy Support + +SOCKS 5 (defined in https://datatracker.ietf.org/doc/html/rfc1928[RFC 1928]) offers choices for authentication methods and supports IPv6 (things that SOCKS 4 does not support). + +A typical SOCKS 5 proxy configuration with the username/password authentication method is the following: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=proxySocks5] +---- + +[[proxy-authentication]] +=== HTTP Proxy Authentication Support + +Jetty's `HttpClient` supports HTTP proxy authentication in the same way it supports <>. + +In the example below, the HTTP proxy requires `BASIC` authentication, but the server requires `DIGEST` authentication, and therefore: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=proxyAuthentication] +---- + +The HTTP conversation for successful authentications on both the proxy and the server is the following: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant Application +participant HttpClient +participant Proxy +participant Server + +Application -> Proxy : GET /path +Proxy -> HttpClient : 407 + Proxy-Authenticate +HttpClient -> Proxy : GET /path + Proxy-Authorization +Proxy -> Server : GET /path +Server -> Proxy : 401 + WWW-Authenticate +Proxy -> HttpClient : 401 + WWW-Authenticate +HttpClient -> Proxy : GET /path + Proxy-Authorization + Authorization +Proxy -> Server : GET /path + Authorization +Server -> Proxy : 200 OK +Proxy -> HttpClient : 200 OK +HttpClient -> Application : 200 OK +---- + +The application does not receive events related to the responses with code 407 and 401 since they are handled internally by `HttpClient`. + +Similarly to the <>, the proxy authentication result and the server authentication result can be preempted to avoid, respectively, the 407 and 401 roundtrips. + +[[transport]] +== HttpClient Pluggable Transports + +Jetty's `HttpClient` can be configured to use different HTTP formats to carry the semantic of HTTP requests and responses, by specifying different `HttpClientTransport` implementations. + +This means that the intention of a client to request resource `/index.html` using the `GET` method can be carried over a <> in different formats. + +An `HttpClientTransport` is the component that is in charge of converting a high-level, semantic, HTTP requests such as " ``GET`` resource ``/index.html`` " into the specific format understood by the server (for example, HTTP/2 or HTTP/3), and to convert the server response from the specific format (HTTP/2 or HTTP/3) into high-level, semantic objects that can be used by applications. + +The most common protocol format is HTTP/1.1, a textual protocol with lines separated by `\r\n`: + +[,screen] +---- +GET /index.html HTTP/1.1\r\n +Host: domain.com\r\n +... +\r\n +---- + +However, the same request can be made using FastCGI, a binary protocol: + +[,screen] +---- +x01 x01 x00 x01 x00 x08 x00 x00 +x00 x01 x01 x00 x00 x00 x00 x00 +x01 x04 x00 x01 xLL xLL x00 x00 +x0C x0B D O C U M E + N T _ U R I / i + n d e x . h t m + l +... +---- + +Similarly, HTTP/2 is a binary protocol that transports the same information in a yet different format via TCP, while HTTP/3 is a binary protocol that transports the same information in yet another format via QUIC. + +The HTTP protocol version may be _negotiated_ between client and server. +A request for a resource may be sent using one protocol (for example, HTTP/1.1), but the response may arrive in a different protocol (for example, HTTP/2). + +`HttpClient` supports these `HttpClientTransport` implementations, each speaking only one protocol: + +* `HttpClientTransportOverHTTP`, for <> (both clear-text and TLS encrypted) +* `HttpClientTransportOverHTTP2`, for <> (both clear-text and TLS encrypted) +* `HttpClientTransportOverHTTP3`, for <> (only encrypted via QUIC) +* `HttpClientTransportOverFCGI`, for <> (both clear-text and TLS encrypted) + +`HttpClient` also supports `HttpClientTransportDynamic`, a <> that can speak different HTTP formats and can select the right protocol by negotiating it with the server or by explicit indication from applications. + +Furthermore, every HTTP format can be sent over different <> such as TCP, Unix-Domain, QUIC or memory. +Supports for Unix-Domain sockets requires Java 16 or later, since Unix-Domain sockets support has been introduced in OpenJDK with https://openjdk.java.net/jeps/380[JEP 380]. + +Applications are typically not aware of the actual HTTP format or low-level transport being used. +This allows them to write their logic against a high-level API that hides the details of the specific HTTP format and low-level transport being used. + +[[transport-http11]] +=== HTTP/1.1 Transport + +HTTP/1.1 is the default transport. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=defaultTransport] +---- + +If you want to customize the HTTP/1.1 transport, you can explicitly configure it in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=http11Transport] +---- + +[[transport-http2]] +=== HTTP/2 Transport + +The HTTP/2 transport can be configured in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=http2Transport] +---- + +`HTTP2Client` is the lower-level client that provides an API based on HTTP/2 concepts such as _sessions_, _streams_ and _frames_ that are specific to HTTP/2. See xref:client/http2.adoc[the HTTP/2 client section] for more information. + +`HttpClientTransportOverHTTP2` uses `HTTP2Client` to format high-level semantic HTTP requests into the HTTP/2 specific format. + +[[transport-http3]] +=== HTTP/3 Transport + +The HTTP/3 transport can be configured in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=http3Transport] +---- + +`HTTP3Client` is the lower-level client that provides an API based on HTTP/3 concepts such as _sessions_, _streams_ and _frames_ that are specific to HTTP/3. See xref:client/http3.adoc[the HTTP/3 client section] for more information. + +`HttpClientTransportOverHTTP3` uses `HTTP3Client` to format high-level semantic HTTP requests into the HTTP/3 specific format. + +[[transport-fcgi]] +=== FastCGI Transport + +The FastCGI transport can be configured in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=fcgiTransport] +---- + +In order to make requests using the FastCGI transport, you need to have a FastCGI server such as https://en.wikipedia.org/wiki/PHP#PHPFPM[PHP-FPM] (see also link:http://php.net/manual/en/install.fpm.php). + +The FastCGI transport is primarily used by Jetty's xref:server/fastcgi.adoc[FastCGI support] to serve PHP pages (WordPress for example). + +[[transport-dynamic]] +=== Dynamic Transport + +The static `HttpClientTransport` implementations work well if you know in advance the protocol you want to speak with the server, or if the server only supports one protocol (such as FastCGI). + +With the advent of HTTP/2 and HTTP/3, however, servers are now able to support multiple protocols. + +The HTTP/2 protocol is typically negotiated between client and server. +This negotiation can happen via ALPN, a TLS extension that allows the client to tell the server the list of protocol that the client supports, so that the server can pick one of the client supported protocols that also the server supports; or via HTTP/1.1 upgrade by means of the `Upgrade` header. + +Applications can configure the dynamic transport with one or more HTTP versions such as HTTP/1.1, HTTP/2 or HTTP/3. +The implementation will take care of using TLS for HTTPS URIs, using ALPN if necessary, negotiating protocols, upgrading from one protocol to another, etc. + +By default, the dynamic transport only speaks HTTP/1.1: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicDefault] +---- + +The dynamic transport can be configured with just one protocol, making it equivalent to the corresponding static transport: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicOneProtocol] +---- + +The dynamic transport, however, has been implemented to support multiple transports, in particular HTTP/1.1, HTTP/2 and HTTP/3: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicH1H2H3] +---- + +The order in which the protocols are specified to `HttpClientTransportDynamic` indicates what is the client preference (first the most preferred). + +When clear-text communication is used (i.e. URIs with the `http` scheme) there is no HTTP protocol version negotiation, and therefore the application must know _a priori_ whether the server supports the HTTP version or not. +For example, if the server only supports clear-text HTTP/2, and `HttpClientTransportDynamic` is configured as in the example above, where HTTP/1.1 has precedence over HTTP/2, the client will send, by default, a clear-text HTTP/1.1 request to a clear-text HTTP/2 only server, which will result in a communication failure. + +When using TLS (i.e. URIs with the `https` scheme), the HTTP protocol version is _negotiated_ between client and server via ALPN, and it is the server that decides what is the application protocol to use for the communication, regardless of the client preference. + +[IMPORTANT] +==== +HTTP/1.1 and HTTP/2 are _compatible_ because they both use TCP, while HTTP/3 is incompatible with previous HTTP versions because it uses QUIC. + +Only compatible HTTP versions can negotiate the HTTP protocol version to use via ALPN, and only compatible HTTP versions can be upgraded from an older version to a newer version. +==== + +Provided that the server supports HTTP/1.1, HTTP/2 and HTTP/3, client applications can explicitly hint the version they want to use: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicExplicitVersion] +---- + +If the client application explicitly specifies the HTTP version, then ALPN is not used by the client. +By specifying the HTTP version explicitly, the client application has prior-knowledge of what HTTP version the server supports, and therefore ALPN is not needed. +If the server does not support the HTTP version chosen by the client, then the communication will fail. + +If the client application does not explicitly specify the HTTP version, then ALPN will be used by the client, but only for compatible protocols. +If the server also supports ALPN, then the protocol will be negotiated via ALPN and the server will choose the protocol to use. +If the server does not support ALPN, the client will try to use the first protocol configured in `HttpClientTransportDynamic`, and the communication may succeed or fail depending on whether the server supports the protocol chosen by the client. + +For example, HTTP/3 is not compatible with previous HTTP version; if `HttpClientTransportDynamic` is configured to prefer HTTP/3, it will be the only protocol attempted by the client: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicPreferH3] +---- + +When the client application configures `HttpClientTransportDynamic` to prefer HTTP/2, there could be ALPN negotiation between HTTP/2 and HTTP/1.1 (but not HTTP/3 because it is incompatible); HTTP/3 will only be possible by specifying the HTTP version explicitly: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tag=dynamicPreferH2] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/client/http2.adoc b/documentation/jetty/modules/programming-guide/pages/client/http2.adoc new file mode 100644 index 000000000000..ece4ef5ed8f1 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/client/http2.adoc @@ -0,0 +1,238 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += HTTP/2 Client Library + +In the vast majority of cases, client applications should use the generic, high-level, xref:client/http.adoc[HTTP client library] that also provides HTTP/2 support via the pluggable xref:client/http.adoc#transport-http2[HTTP/2 transport] or the xref:client/http.adoc#transport-dynamic[dynamic transport]. + +The high-level HTTP library supports cookies, authentication, redirection, connection pooling and a number of other features that are absent in the low-level HTTP/2 library. + +The HTTP/2 client library has been designed for those applications that need low-level access to HTTP/2 features such as _sessions_, _streams_ and _frames_, and this is quite a rare use case. + +See also the correspondent xref:server/http2.adoc[HTTP/2 server library]. + +[[intro]] +== Introducing HTTP2Client + +The Maven artifact coordinates for the HTTP/2 client library are the following: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.http2 + jetty-http2-client + {version} + +---- + +The main class is named `org.eclipse.jetty.http2.client.HTTP2Client`, and must be created, configured and started before use: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=start] +---- + +When your application stops, or otherwise does not need `HTTP2Client` anymore, it should stop the `HTTP2Client` instance (or instances) that were started: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=stop] +---- + +`HTTP2Client` allows client applications to connect to an HTTP/2 server. +A _session_ represents a single TCP connection to an HTTP/2 server and is defined by class `org.eclipse.jetty.http2.api.Session`. +A _session_ typically has a long life -- once the TCP connection is established, it remains open until it is not used anymore (and therefore it is closed by the idle timeout mechanism), until a fatal error occurs (for example, a network failure), or if one of the peers decides unilaterally to close the TCP connection. + +HTTP/2 is a multiplexed protocol: it allows multiple HTTP/2 requests to be sent on the same TCP connection, or _session_. +Each request/response cycle is represented by a _stream_. +Therefore, a single _session_ manages multiple concurrent _streams_. +A _stream_ has typically a very short life compared to the _session_: a _stream_ only exists for the duration of the request/response cycle and then disappears. + +[[flow-control]] +== HTTP/2 Flow Control + +The HTTP/2 protocol is _flow controlled_ (see https://tools.ietf.org/html/rfc7540#section-5.2[the specification]). +This means that a sender and a receiver maintain a _flow control window_ that tracks the number of data bytes sent and received, respectively. +When a sender sends data bytes, it reduces its flow control window. +When a receiver receives data bytes, it also reduces its flow control window, and then passes the received data bytes to the application. +The application consumes the data bytes and tells back the receiver that it has consumed the data bytes. +The receiver then enlarges the flow control window, and the implementation arranges to send a message to the sender with the number of bytes consumed, so that the sender can enlarge its flow control window. + +A sender can send data bytes up to its whole flow control window, then it must stop sending. +The sender may resume sending data bytes when it receives a message from the receiver that the data bytes sent previously have been consumed. +This message enlarges the sender flow control window, which allows the sender to send more data bytes. + +HTTP/2 defines _two_ flow control windows: one for each _session_, and one for each _stream_. +Let's see with an example how they interact, assuming that in this example the session flow control window is 120 bytes and the stream flow control window is 100 bytes. + +The sender opens a session, and then opens `stream_1` on that session, and sends `80` data bytes. +At this point the session flow control window is `40` bytes (`120 - 80`), and ``stream_1``'s flow control window is `20` bytes (`100 - 80`). +The sender now opens `stream_2` on the same session and sends `40` data bytes. +At this point, the session flow control window is `0` bytes (`40 - 40`), while ``stream_2``'s flow control window is `60` (`100 - 40`). +Since now the session flow control window is `0`, the sender cannot send more data bytes, neither on `stream_1` nor on `stream_2`, nor on other streams, despite all the streams having their stream flow control windows greater than `0`. + +The receiver consumes ``stream_2``'s `40` data bytes and sends a message to the sender with this information. +At this point, the session flow control window is `40` (``0 + 40``), ``stream_1``'s flow control window is still `20` and ``stream_2``'s flow control window is `100` (``60 + 40``). +If the sender opens `stream_3` and would like to send `50` data bytes, it would only be able to send `40` because that is the maximum allowed by the session flow control window at this point. + +It is therefore very important that applications notify the fact that they have consumed data bytes as soon as possible, so that the implementation (the receiver) can send a message to the sender (in the form of a `WINDOW_UPDATE` frame) with the information to enlarge the flow control window, therefore reducing the possibility that sender stalls due to the flow control windows being reduced to `0`. + +How a client application should handle HTTP/2 flow control is discussed in details in <>. + +[[connect]] +== Connecting to the Server + +The first thing an application should do is to connect to the server and obtain a `Session`. +The following example connects to the server on a clear-text port: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=clearTextConnect] +---- + +The following example connects to the server on an encrypted port: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=encryptedConnect] +---- + +IMPORTANT: Applications must know in advance whether they want to connect to a clear-text or encrypted port, and pass the `SslContextFactory` parameter accordingly to the `connect(\...)` method. + +[[configure]] +== Configuring the Session + +The `connect(\...)` method takes a `Session.Listener` parameter. +This listener's `onPreface(\...)` method is invoked just before establishing the connection to the server to gather the client configuration to send to the server. +Client applications can override this method to change the default configuration: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=configure] +---- + +The `Session.Listener` is notified of session events originated by the server such as receiving a `SETTINGS` frame from the server, or the server closing the connection, or the client timing out the connection due to idleness. +Please refer to the `Session.Listener` link:{javadoc-url}/org/eclipse/jetty/http2/api/Session.Listener.html[javadocs] for the complete list of events. + +Once a `Session` has been established, the communication with the server happens by exchanging _frames_, as specified in the https://tools.ietf.org/html/rfc7540#section-4[HTTP/2 specification]. + +[[request]] +== Sending a Request + +Sending an HTTP request to the server, and receiving a response, creates a _stream_ that encapsulates the exchange of HTTP/2 frames that compose the request and the response. + +In order to send an HTTP request to the server, the client must send a `HEADERS` frame. +`HEADERS` frames carry the request method, the request URI and the request headers. +Sending the `HEADERS` frame opens the `Stream`: + +[,java,indent=0,subs=attributes+] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=newStream] +---- + +Note how `Session.newStream(\...)` takes a `Stream.Listener` parameter. +This listener is notified of stream events originated by the server such as receiving `HEADERS` or `DATA` frames that are part of the response, discussed in more details in the <>. +Please refer to the `Stream.Listener` link:{javadoc-url}/org/eclipse/jetty/http2/api/Stream.Listener.html[javadocs] for the complete list of events. + +HTTP requests may have content, which is sent using the `Stream` APIs: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=newStreamWithData] +---- + +IMPORTANT: When sending two `DATA` frames consecutively, the second call to `Stream.data(\...)` must be done only when the first is completed, or a `WritePendingException` will be thrown. +Use the `Callback` APIs or `CompletableFuture` APIs to ensure that the second `Stream.data(\...)` call is performed when the first completed successfully. + +[[response]] +== Receiving a Response + +Response events are delivered to the `Stream.Listener` passed to `Session.newStream(\...)`. + +An HTTP response is typically composed of a `HEADERS` frame containing the HTTP status code and the response headers, and optionally one or more `DATA` frames containing the response content bytes. + +The HTTP/2 protocol also supports response trailers (that is, headers that are sent after the response content) that also are sent using a `HEADERS` frame. + +A client application can therefore receive the HTTP/2 frames sent by the server by implementing the relevant methods in `Stream.Listener`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=responseListener] +---- + +[NOTE] +==== +When `onDataAvailable(Stream stream)` is invoked, the demand is implicitly cancelled. + +Just returning from the `onDataAvailable(Stream stream)` method does _not_ implicitly demand for more `DATA` frames. + +Applications must call `Stream.demand()` to explicitly require that `onDataAvailable(Stream stream)` is invoked again when more `DATA` frames are available. +==== + +Applications that consume the content buffer within `onDataAvailable(Stream stream)` (for example, writing it to a file, or copying the bytes to another storage) should call `Data.release()` as soon as they have consumed the content buffer. +This allows the implementation to reuse the buffer, reducing the memory requirements needed to handle the content buffers. + +Alternatively, an application may store away the `Data` object to consume the buffer bytes later, or pass the `Data` object to another asynchronous API (this is typical in proxy applications). + +[IMPORTANT] +==== +The call to `Stream.readData()` tells the implementation to enlarge the stream and session flow control windows so that the sender will be able to send more `DATA` frames without stalling. +==== + +Applications can unwrap the `Data` object into some other object that may be used later, provided that the _release_ semantic is maintained: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/HTTP2Docs.java[tags=dataUnwrap] +---- + +[IMPORTANT] +==== +Applications that implement `onDataAvailable(Stream stream)` must remember to call `Stream.demand()` eventually. + +If they do not call `Stream.demand()`, the implementation will not invoke `onDataAvailable(Stream stream)` to deliver more `DATA` frames and the application will stall threadlessly until an idle timeout fires to close the stream or the session. +==== + +[[reset]] +== Resetting a Request or Response + +In HTTP/2, clients and servers have the ability to tell to the other peer that they are not interested anymore in either the request or the response, using a `RST_STREAM` frame. + +The `HTTP2Client` APIs allow client applications to send and receive this "reset" frame: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=reset] +---- + +[[push]] +== Receiving HTTP/2 Pushes + +HTTP/2 servers have the ability to push resources related to a primary resource. +When an HTTP/2 server pushes a resource, it sends to the client a `PUSH_PROMISE` frame that contains the request URI and headers that a client would use to request explicitly that resource. + +Client applications can be configured to tell the server to never push resources, see <>. + +Client applications can listen to the push events, and act accordingly: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=push] +---- + +If a client application does not want to handle a particular HTTP/2 push, it can just reset the pushed stream to tell the server to stop sending bytes for the pushed stream: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http2/HTTP2ClientDocs.java[tags=pushReset] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/client/http3.adoc b/documentation/jetty/modules/programming-guide/pages/client/http3.adoc new file mode 100644 index 000000000000..17f4b47927ba --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/client/http3.adoc @@ -0,0 +1,155 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += HTTP/3 Client Library + +In the vast majority of cases, client applications should use the generic, high-level, xref:client/http.adoc[HTTP client library] that also provides HTTP/3 support via the pluggable xref:client/http.adoc#transport-http3[HTTP/3 transport] or the xref:client/http.adoc#transport-dynamic[dynamic transport]. + +The high-level HTTP library supports cookies, authentication, redirection, connection pooling and a number of other features that are absent in the low-level HTTP/3 library. + +The HTTP/3 client library has been designed for those applications that need low-level access to HTTP/3 features such as _sessions_, _streams_ and _frames_, and this is quite a rare use case. + +See also the correspondent xref:server/http3.adoc[HTTP/3 server library]. + +[[intro]] +== Introducing HTTP3Client + +The Maven artifact coordinates for the HTTP/3 client library are the following: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.http3 + jetty-http3-client + {version} + +---- + +The main class is named `org.eclipse.jetty.http3.client.HTTP3Client`, and must be created, configured and started before use: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java[tags=start] +---- + +When your application stops, or otherwise does not need `HTTP3Client` anymore, it should stop the `HTTP3Client` instance (or instances) that were started: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java[tags=stop] +---- + +`HTTP3Client` allows client applications to connect to an HTTP/3 server. +A _session_ represents a single connection to an HTTP/3 server and is defined by class `org.eclipse.jetty.http3.api.Session`. +A _session_ typically has a long life -- once the connection is established, it remains active until it is not used anymore (and therefore it is closed by the idle timeout mechanism), until a fatal error occurs (for example, a network failure), or if one of the peers decides unilaterally to close the connection. + +HTTP/3 is a multiplexed protocol because it relies on the multiplexing capabilities of QUIC, the protocol based on UDP that transports HTTP/3 frames. +Thanks to multiplexing, multiple HTTP/3 requests are sent on the same QUIC connection, or _session_. +Each request/response cycle is represented by a _stream_. +Therefore, a single _session_ manages multiple concurrent _streams_. +A _stream_ has typically a very short life compared to the _session_: a _stream_ only exists for the duration of the request/response cycle and then disappears. + +// TODO: flow control? +//[[pg-client-http3-flow-control]] +//==== HTTP/3 Flow Control + +//include::../../http3.adoc[tag=flowControl] + +//How a client application should handle HTTP/3 flow control is discussed in details in <>. + +[[connect]] +== Connecting to the Server + +The first thing an application should do is to connect to the server and obtain a `Session`. +The following example connects to the server: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java[tags=connect] +---- + +[[configure]] +== Configuring the Session + +The `connect(\...)` method takes a `Session.Client.Listener` parameter. +This listener's `onPreface(\...)` method is invoked just before establishing the connection to the server to gather the client configuration to send to the server. +Client applications can override this method to change the default configuration: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java[tags=configure] +---- + +The `Session.Client.Listener` is notified of session events originated by the server such as receiving a `SETTINGS` frame from the server, or the server closing the connection, or the client timing out the connection due to idleness. +Please refer to the `Session.Client.Listener` link:{javadoc-url}/org/eclipse/jetty/http3/api/Session.Client.Listener.html[javadocs] for the complete list of events. + +Once a `Session` has been established, the communication with the server happens by exchanging _frames_. + +[[request]] +== Sending a Request + +Sending an HTTP request to the server, and receiving a response, creates a _stream_ that encapsulates the exchange of HTTP/3 frames that compose the request and the response. + +In order to send an HTTP request to the server, the client must send a `HEADERS` frame. +`HEADERS` frames carry the request method, the request URI and the request headers. +Sending the `HEADERS` frame opens the `Stream`: + +[,java,indent=0,subs=attributes+] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java[tags=newStream] +---- + +Note how `Session.newRequest(\...)` takes a `Stream.Client.Listener` parameter. +This listener is notified of stream events originated by the server such as receiving `HEADERS` or `DATA` frames that are part of the response, discussed in more details in the <>. +Please refer to the `Stream.Client.Listener` link:{javadoc-url}/org/eclipse/jetty/http3/api/Stream.Client.Listener.html[javadocs] for the complete list of events. + +HTTP requests may have content, which is sent using the `Stream` APIs: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java[tags=newStreamWithData] +---- + +IMPORTANT: When sending two `DATA` frames consecutively, the second call to `Stream.data(\...)` must be done only when the first is completed, or a `WritePendingException` will be thrown. +Use the `CompletableFuture` APIs to ensure that the second `Stream.data(\...)` call is performed when the first completed successfully. + +[[response]] +== Receiving a Response + +Response events are delivered to the `Stream.Client.Listener` passed to `Session.newRequest(\...)`. + +An HTTP response is typically composed of a `HEADERS` frame containing the HTTP status code and the response headers, and optionally one or more `DATA` frames containing the response content bytes. + +The HTTP/3 protocol also supports response trailers (that is, headers that are sent after the response content) that also are sent using a `HEADERS` frame. + +A client application can therefore receive the HTTP/3 frames sent by the server by implementing the relevant methods in `Stream.Client.Listener`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java[tags=responseListener] +---- + +// TODO: flow control? +//include::../../http3.adoc[tag=apiFlowControl] + +[[reset]] +== Resetting a Request or Response + +In HTTP/3, clients and servers have the ability to tell to the other peer that they are not interested anymore in either the request or the response, by resetting the stream. + +The `HTTP3Client` APIs allow client applications to send and receive this "reset" event: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http3/HTTP3ClientDocs.java[tags=reset] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/client/index.adoc b/documentation/jetty/modules/programming-guide/pages/client/index.adoc new file mode 100644 index 000000000000..66507e1e021b --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/client/index.adoc @@ -0,0 +1,30 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Client Libraries + +The Eclipse Jetty Project provides client-side libraries that allow you to embed an HTTP or WebSocket client in your applications. +A typical example is a client application that needs to contact a third party service via HTTP (for example a REST service). +Another example is a proxy application that receives HTTP requests and forwards them as FCGI requests to a PHP application such as WordPress, or receives HTTP/1.1 requests and converts them to HTTP/2 or HTTP/3. +Yet another example is a client application that needs to receive events from a WebSocket server. + +The client libraries are designed to be non-blocking and offer both synchronous and asynchronous APIs and come with many configuration options. + +These are the available client libraries: + +* xref:client/http.adoc[The High-Level HTTP Client Library] for HTTP/1.1, HTTP/2, HTTP/3 and FastCGI +* xref:client/http2.adoc[The Low-Level HTTP/2 Client Library] for low-level HTTP/2 +* xref:client/http3.adoc[The Low-Level HTTP/3 Client Library] for low-level HTTP/3 +* xref:client/websocket.adoc[The WebSocket client library] + +If you are interested in the low-level details of how the Jetty client libraries work, or are interested in writing a custom protocol, look at the xref:client/io-arch.adoc[Client I/O Architecture]. diff --git a/documentation/jetty/modules/programming-guide/pages/client/io-arch.adoc b/documentation/jetty/modules/programming-guide/pages/client/io-arch.adoc new file mode 100644 index 000000000000..36751e9b424b --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/client/io-arch.adoc @@ -0,0 +1,178 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += I/O Architecture + +The Jetty client libraries provide the basic components and APIs to implement a client application. + +They build on the common xref:arch/io.adoc[Jetty I/O Architecture] and provide client specific concepts (such as establishing a connection to a server). + +There are conceptually two layers that compose the Jetty client libraries: + +. <>, that handles the low-level communication with the server, and deals with buffers, threads, etc. +. <>, that handles the high-level protocol by parsing the bytes read from the transport layer and by generating the bytes to write to the transport layer. + +[[transport]] +== Transport Layer + +The transport layer is the low-level layer that communicates with the server. + +Protocols such as HTTP/1.1 and HTTP/2 are typically transported over TCP, while the newer HTTP/3 is transported over QUIC, which is itself transported over UDP. + +However, there are other means of communication supported by the Jetty client libraries, in particular over <> (for inter-process communication), and over <> (for intra-process communication). + +The same high-level protocol can be carried by different low-level transports. +For example, the high-level HTTP/1.1 protocol can be transported over either TCP (the default), or QUIC, or Unix-Domain sockets, or memory, because all these low-level transport provide reliable and ordered communication between client and server. + +Similarly, the high-level HTTP/3 protocol can be transported over either QUIC (the default) or memory. +It would be possible to transport HTTP/3 also over Unix-Domain sockets, but the current version of Java only supports Unix-Domain sockets for ``SocketChannel``s and not for ``DatagramChannel``s. + +The Jetty client libraries use the common I/O design described in xref:arch/io.adoc[this section]. + +The common I/O components and concepts are used for all low-level transports. +The only partial exception is the <>, which is not based on network components; as such it does not need a `SelectorManager`, but it exposes `EndPoint` so that high-level protocols have a common interface to interact with the low-level transport. + +The client-side abstraction for the low-level transport is `org.eclipse.jetty.io.Transport`. + +`Transport` represents how high-level protocols can be transported; there is `Transport.TCP_IP` that represents communication over TCP, but also `Transport.TCPUnix` for Unix-Domain sockets, `QuicTransport` for QUIC and `MemoryTransport` for memory. + +Applications can specify the `Transport` to use for each request as described in xref:client/http.adoc#api-transport[this section]. + +When the `Transport` implementation uses the network, it delegates to `org.eclipse.jetty.io.ClientConnector`. + +`ClientConnector` primarily wraps `org.eclipse.jetty.io.SelectorManager` to provide network functionalities, and aggregates other four components: + +* a thread pool (in form of an `java.util.concurrent.Executor`) +* a scheduler (in form of `org.eclipse.jetty.util.thread.Scheduler`) +* a byte buffer pool (in form of `org.eclipse.jetty.io.ByteBufferPool`) +* a TLS factory (in form of `org.eclipse.jetty.util.ssl.SslContextFactory.Client`) + +The `ClientConnector` is where you want to set those components after you have configured them. +If you don't explicitly set those components on the `ClientConnector`, then appropriate defaults will be chosen when the `ClientConnector` starts. + +`ClientConnector` manages all network-related components, and therefore it is used for TCP, UDP, QUIC and <>. + +The simplest example that creates and starts a `ClientConnector` is the following: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java[tags=simplest] +---- + +A more typical example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java[tags=typical] +---- + +A more advanced example that customizes the `ClientConnector` by overriding some of its methods: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java[tags=advanced] +---- + +Since `ClientConnector` is the component that handles the low-level network transport, it is also the component where you want to configure the low-level network configuration. + +The most common parameters are: + +* `ClientConnector.selectors`: the number of ``java.nio.Selector``s components (defaults to `1`) that are present to handle the ``SocketChannel``s and ``DatagramChannel``s opened by the `ClientConnector`. +You typically want to increase the number of selectors only for those use cases where each selector should handle more than few hundreds _concurrent_ socket events. +For example, one selector typically runs well for `250` _concurrent_ socket events; as a rule of thumb, you can multiply that number by `10` to obtain the number of opened sockets a selector can handle (`2500`), based on the assumption that not all the `2500` sockets will be active _at the same time_. +* `ClientConnector.idleTimeout`: the duration of time after which `ClientConnector` closes a socket due to inactivity (defaults to `30` seconds). +This is an important parameter to configure, and you typically want the client idle timeout to be shorter than the server idle timeout, to avoid race conditions where the client attempts to use a socket just before the client-side idle timeout expires, but the server-side idle timeout has already expired and the is already closing the socket. +* `ClientConnector.connectBlocking`: whether the operation of connecting a socket to the server (i.e. `SocketChannel.connect(SocketAddress)`) must be a blocking or a non-blocking operation (defaults to `false`). +For `localhost` or same datacenter hosts you want to set this parameter to +`true` because DNS resolution will be immediate (and likely never fail). +For generic Internet hosts (e.g. when you are implementing a web spider) you want to set this parameter to `false`. +* `ClientConnector.connectTimeout`: the duration of time after which `ClientConnector` aborts a connection attempt to the server (defaults to `5` seconds). +This time includes the DNS lookup time _and_ the TCP connect time. + +Please refer to the `ClientConnector` link:{javadoc-url}/org/eclipse/jetty/io/ClientConnector.html[javadocs] for the complete list of configurable parameters. + +[[unix-domain]] +=== Unix-Domain Support + +https://openjdk.java.net/jeps/380[JEP 380] introduced Unix-Domain sockets support in Java 16, on all operative systems, but only for ``SocketChannel``s (not for ``DatagramChannel``s). + +`ClientConnector` handles Unix-Domain sockets exactly like it handles regular TCP sockets, so there is no additional configuration necessary -- Unix-Domain sockets are supported out-of-the-box. + +Applications can specify the `Transport` to use for each request as described in xref:client/http.adoc#api-transport[this section]. + +[[memory]] +=== Memory Support + +In addition to support communication between client and server via network or Unix-Domain, the Jetty client libraries also support communication between client and server via memory for intra-process communication. +This means that the client and server must be in the same JVM process. + +This functionality is provided by `org.eclipse.jetty.server.MemoryTransport`, which does not delegate to `ClientConnector`, but instead delegates to the server-side `MemoryConnector` and its related classes. + +Applications can specify the `Transport` to use for each request as described in xref:client/http.adoc#api-transport[this section]. + +[[protocol]] +== Protocol Layer + +The protocol layer builds on top of the transport layer to generate the bytes to be written to the low-level transport and to parse the bytes read from the low-level transport. + +Recall from xref:arch/io.adoc#connection[this section] that Jetty uses the `Connection` abstraction to produce and interpret the low-level transport bytes. + +On the client side, a `ClientConnectionFactory` implementation is the component that creates `Connection` instances based on the protocol that the client wants to "speak" with the server. + +Applications may use `ClientConnector.connect(SocketAddress, Map)` to establish a TCP connection to the server, and must provide `ClientConnector` with the following information in the context map: + +* A `Transport` instance that specifies the low-level transport to use. +* A `ClientConnectionFactory` that creates `Connection` instances for the high-level protocol. +* A `Promise` that is notified when the connection creation succeeds or fails. + +For example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java[tags=connect] +---- + +When a `Connection` is created successfully, its `onOpen()` method is invoked, and then the promise is completed successfully. + +It is now possible to write a super-simple `telnet` client that reads and writes string lines: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java[tags=telnet] +---- + +Note how a very basic "telnet" API that applications could use is implemented in the form of the `onLine(Consumer)` for the non-blocking receiving side and `writeLine(String, Callback)` for the non-blocking sending side. +Note also how the `onFillable()` method implements some basic "parsing" by looking up the `\n` character in the buffer. + +NOTE: The "telnet" client above looks like a super-simple HTTP client because HTTP/1.0 can be seen as a line-based protocol. +HTTP/1.0 was used just as an example, but we could have used any other line-based protocol such as https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol[SMTP], provided that the server was able to understand it. + +This is very similar to what the Jetty client implementation does for real network protocols. +Real network protocols are of course more complicated and so is the implementation code that handles them, but the general ideas are similar. + +The Jetty client implementation provides a number of `ClientConnectionFactory` implementations that can be composed to produce and interpret the network bytes. + +For example, it is simple to modify the above example to use the TLS protocol so that you will be able to connect to the server on port `443`, typically reserved for the secure HTTP protocol. + +The differences between the clear-text version and the TLS encrypted version are minimal: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ClientConnectorDocs.java[tags=tlsTelnet] +---- + +The differences with the clear-text version are only: + +* Change the port from `80` to `443`. +* Wrap the `ClientConnectionFactory` with `SslClientConnectionFactory`. +* Unwrap the `SslConnection` to access `TelnetConnection`. diff --git a/documentation/jetty/modules/programming-guide/pages/client/websocket.adoc b/documentation/jetty/modules/programming-guide/pages/client/websocket.adoc new file mode 100644 index 000000000000..b8cf5c0991f6 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/client/websocket.adoc @@ -0,0 +1,530 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += WebSocket Client + +Jetty's `WebSocketClient` is a more powerful alternative to the WebSocket client provided by the standard JSR 356 `javax.websocket` APIs. + +Similarly to Jetty's xref:client/http.adoc[`HttpClient`], the `WebSocketClient` is non-blocking and asynchronous, making it very efficient in resource utilization. +A synchronous, blocking, API is also offered for simpler cases. + +Since the first step of establishing a WebSocket communication is an HTTP request, `WebSocketClient` makes use of `HttpClient` and therefore depends on it. + +The Maven artifact coordinates are the following: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.websocket + jetty-websocket-jetty-client + {version} + +---- + +[[start]] +== Starting WebSocketClient + +The main class is `org.eclipse.jetty.websocket.client.WebSocketClient`; you instantiate it, configure it, and then start it like many other Jetty components. +This is a minimal example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=start] +---- + +However, it is recommended that you explicitly pass an `HttpClient` instance to `WebSocketClient` so that you can have control over the HTTP configuration as well: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=startWithHttpClient] +---- + +You may create multiple instances of `WebSocketClient`, but typically one instance is enough for most applications. +Creating multiple instances may be necessary for example when you need to specify different configuration parameters for different instances. +For example, you may need different instances when you need to configure the `HttpClient` differently: different transports, different proxies, different cookie stores, different authentications, etc. + +The configuration that is not WebSocket specific (such as idle timeout, etc.) should be directly configured on the associated `HttpClient` instance. + +The WebSocket specific configuration can be configured directly on the `WebSocketClient` instance. +Configuring the `WebSocketClient` allows to give default values to various parameters, whose values may be overridden more specifically, as described in <>. + +Refer to the `WebSocketClient` link:{javadoc-url}/org/eclipse/jetty/websocket/client/WebSocketClient.html[javadocs] for the setter methods available to customize the WebSocket specific configuration. + +[[stop]] +== Stopping WebSocketClient + +It is recommended that when your application stops, you also stop the `WebSocketClient` instance (or instances) that you are using. + +Similarly to xref:client/http.adoc#stop[stopping `HttpClient`], you want to stop `WebSocketClient` from a thread that is not owned by `WebSocketClient` itself, for example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=stop] +---- + +[[connect]] +== Connecting to a Remote Host + +A WebSocket client may initiate the communication with the server either <> or <>. +The two mechanism are quite different and detailed in the following sections. + +[[connect-http11]] +=== Using HTTP/1.1 + +Initiating a WebSocket communication with a server using HTTP/1.1 is detailed in https://tools.ietf.org/html/rfc6455#section-1.8[RFC 6455]. + +A WebSocket client first establishes a TCP connection to the server, then sends an HTTP/1.1 _upgrade_ request. + +If the server supports upgrading to WebSocket, it responds with HTTP status code `101`, and then switches the communication over that connection, either incoming or outgoing, to happen using the WebSocket protocol. + +When the client receives the HTTP status code `101`, it switches the communication over that connection, either incoming or outgoing, to happen using the WebSocket protocol. + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant ClientEndPoint +participant WebSocketClient +participant HttpClient +participant Server +participant ServerEndPoint + +WebSocketClient -> HttpClient : connect() +HttpClient -> Server : TCP/TLS connect +HttpClient -> Server : GET / HTTP/1.1\nUpgrade: websocket +Server -> ServerEndPoint ** : create +Server -> HttpClient : HTTP/1.1 101\nUpgrade: websocket +HttpClient -> WebSocketClient +WebSocketClient -> ClientEndPoint ** : create +ClientEndPoint -> WebSocketClient : WebSocket Frame A +WebSocketClient -> Server : WebSocket Frame A +Server -> ServerEndPoint : WebSocket Frame A +ServerEndPoint -> Server : WebSocket Frame B +Server -> WebSocketClient : WebSocket Frame B +WebSocketClient -> ClientEndPoint : WebSocket Frame B +---- + +In code: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=connectHTTP11] +---- + +`WebSocketClient.connect()` links the client-side WebSocket _endpoint_ to a specific server URI, and returns a `CompletableFuture` of an `org.eclipse.jetty.websocket.api.Session`. + +The endpoint offers APIs to _receive_ WebSocket data (or errors) from the server, while the session offers APIs to _send_ WebSocket data to the server. + +[[connect-http2]] +=== Using HTTP/2 + +Initiating a WebSocket communication with a server using HTTP/1.1 is detailed in https://tools.ietf.org/html/rfc8441[RFC 8441]. + +A WebSocket client establishes a TCP connection to the server or reuses an existing one currently used for HTTP/2, then sends an HTTP/2 _CONNECT_ request over an HTTP/2 stream. + +If the server supports upgrading to WebSocket, it responds with HTTP status code `200`, then switches the communication over that stream, either incoming or outgoing, to happen using HTTP/2 `DATA` frames wrapping WebSocket frames. + +When the client receives the HTTP status code `200`, it switches the communication over that stream, either incoming or outgoing, to happen using HTTP/2 `DATA` frames wrapping WebSocket frames. + +From an external point of view, it will look like client is sending chunks of an infinite HTTP/2 request upload, and the server is sending chunks of an infinite HTTP/2 response download, as they will exchange HTTP/2 `DATA` frames; but the HTTP/2 `DATA` frames will contain each one or more WebSocket frames that both client and server know how to deliver to the respective WebSocket endpoints. + +When either WebSocket endpoint decides to terminate the communication, the HTTP/2 stream will be closed as well. + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant ClientEndPoint +participant WebSocketClient +participant HttpClient +participant Server +participant ServerEndPoint + +WebSocketClient -> HttpClient : connect() +HttpClient --> Server : TCP/TLS connect +HttpClient -> Server : HEADERS\n:method: CONNECT\n:protocol: websocket +Server -> ServerEndPoint ** : create +Server -> HttpClient : HEADERS\n:status: 200\n: websocket +HttpClient -> WebSocketClient +WebSocketClient -> ClientEndPoint ** : create +ClientEndPoint -> HttpClient : WebSocket Frame A +HttpClient -> Server : DATA\nWebSocket Frame A +Server -> ServerEndPoint : WebSocket Frame A +ServerEndPoint -> Server : WebSocket Frame B +Server -> HttpClient : DATA\nWebSocket Frame B +HttpClient -> ClientEndPoint : WebSocket Frame B +---- + +In code: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=connectHTTP2] +---- + +Alternatively, you can use the xref:client/http.adoc#transport-dynamic[dynamic `HttpClient` transport]: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=connectHTTP2Dynamic] +---- + +[[connect-custom-http-request]] +=== Customizing the Initial HTTP Request + +Sometimes you need to add custom cookies, or other HTTP headers, or specify a WebSocket sub-protocol to the HTTP request that initiates the WebSocket communication. + +You can do this by using overloaded versions of the `WebSocketClient.connect(...)` method: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=customHTTPRequest] +---- + +[[connect-inspect-http-response]] +=== Inspecting the Initial HTTP Response + +If you want to inspect the HTTP response returned by the server as a reply to the HTTP request that initiates the WebSocket communication, you may provide a `JettyUpgradeListener`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/websocket/WebSocketClientDocs.java[tags=inspectHTTPResponse] +---- + +[[architecture]] +== Jetty WebSocket Architecture + +The Jetty WebSocket architecture is organized around the concept of a logical _connection_ between the client and the server. + +The connection may be physical, when connecting to the server using HTTP/1.1, as the WebSocket bytes are carried directly by the TCP connection. + +The connection may be virtual, when connecting to the server using HTTP/2, as the WebSocket bytes are wrapped into HTTP/2 `DATA` frames of an HTTP/2 stream. +In this case, a single TCP connection may carry several WebSocket virtual connections, each wrapped in its own HTTP/2 stream. + +Each side of a WebSocket connection, either client or server, is made of two entities: + +* A <>, the entity that _receives_ WebSocket events. +* A <>, the entity that offers an API to _send_ WebSocket data (and to close the WebSocket connection), as well as to configure WebSocket connection parameters. + +[[endpoints]] +== WebSocket Endpoints + +A WebSocket endpoint is the entity that receives WebSocket events. + +The WebSocket events are the following: + +* The _open_ event. +This event is emitted when the WebSocket communication has been successfully established. +Applications interested in the open event receive the WebSocket _session_ so that they can use it to send data to the remote peer. +* The _close_ event. +This event is emitted when the WebSocket communication has been closed. +Applications interested in the close event receive a WebSocket status code and an optional close reason message. +* The _error_ event. +This event is emitted when the WebSocket communication encounters a fatal error, such as an I/O error (for example, the network connection has been broken), or a protocol error (for example, the remote peer sends an invalid WebSocket frame). +Applications interested in the error event receive a `Throwable` that represent the error. +* The _frame_ events. +The frame events are emitted when a WebSocket frame is received, either a control frame such as PING, PONG or CLOSE, or a data frame such as BINARY or TEXT. +One or more data frames of the same type define a _message_. +* The _message_ events. +The message event are emitted when a WebSocket message is received. +The message event can be of two types: +** TEXT. +Applications interested in this type of messages receive a `String` representing the UTF-8 bytes received. +** BINARY. +Applications interested in this type of messages receive a `ByteBuffer` representing the raw bytes received. + +<> are notified of events by invoking the correspondent method defined by the `org.eclipse.jetty.websocket.api.Session.Listener` interface. + +<> are notified of events by invoking the correspondent method annotated with the correspondent annotation from the `+org.eclipse.jetty.websocket.api.annotations.*+` package. + +Jetty uses ``MethodHandle``s to instantiate WebSocket endpoints and invoke WebSocket event methods, so WebSocket endpoint classes and WebSocket event methods must be `public`. + +When using JPMS, your classes must be `public` and must be exported using the `exports` directive in your `module-info.java`. +It is not recommended to use the `opens` directive in your `module-info.java` for your classes, as it would expose your classes to deep reflection, which is unnecessary, as the `exports` directive is sufficient. + +This guarantees that WebSocket endpoints can be accessed by the Jetty implementation without additional configuration, no matter whether you are using only the class-path, or the module-path. + +For both types of WebSocket endpoints, only one thread at a time will be delivering frame or message events to the corresponding methods; the next frame or message event will not be delivered until the previous call to the corresponding method has exited, and if there is <> for it. +Endpoints will always be notified of message events in the same order they were received over the network. + +[[endpoints-demand]] +=== WebSocket Events Demand + +In order to receive WebSocket events, you must _demand_ for them; the only exception is the _open_ event, because it is the initial event that applications can interact with. + +When a WebSocket event is received by an endpoint, the demand for WebSocket events (for that endpoint) is reset, so that no more WebSocket events will be received by the endpoint. +It is responsibility of the endpoint to demand to receive more WebSocket events. + +For simple cases, you can just annotate your WebSocket endpoint with `@WebSocket(autoDemand = true)`, or implement `Session.Listener.AutoDemanding`. +In these two cases, when a method that receives a WebSocket event returns, the Jetty implementation automatically demands for another WebSocket event. + +For example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=autoDemand] +---- + +While auto-demand works for simple cases, it may not work in all cases, especially those where the method that receives the WebSocket event performs asynchronous operations. + +The following example shows the problem: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=autoDemandWrong] +---- + +Note how, in the example above, auto-demanding has the problem that receiving WebSocket text messages may happen faster than echoing them back, because the call to `sendText(\...)` may return almost immediately but be slow to complete because it is asynchronous. + +In the example above, if another WebSocket text message arrives, and the `sendText(\...)` operation is not complete, a `WritePendingException` will be thrown. + +In other cases, this may lead to infinite buffering of data, eventually causing ``OutOfMemoryError``s, and in general excessive resource consumption that may be difficult to diagnose and troubleshoot. + +For more information, see also the <>. + +[CAUTION] +==== +Always be careful when using auto-demand. + +Analyze the operations that your endpoint performs and make sure they complete synchronously within the method. +==== + +To solve the problem outlined above, you must explicitly demand for the next WebSocket event, only when the processing of the previous events is complete. + +For example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=explicitDemand] +---- + +Note how it is necessary to invoke `Session.demand()` from the _open_ event, in order to receive _message_ events. + +Furthermore, note how every time a text message is received, a possibly slow asynchronous operation is initiated (which returns almost immediately, although it may not be completed yet) and then the method returns. + +Because there is no demand when the method returns (because the asynchronous operation is not completed yet), the implementation will not notify any other WebSocket event (not even _frame_, _close_ or _error_ events). + +When the asynchronous operation completes successfully the callback is notified; this, in turn, invokes `Session.demand()`, and the implementation may notify another WebSocket event (if any) to the WebSocket endpoint. + +[[endpoints-listener]] +=== Listener Endpoints + +A WebSocket endpoint may implement the `org.eclipse.jetty.websocket.api.Session.Listener` interface to receive WebSocket events: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=listenerEndpoint] +---- + +==== Message Streaming Reads + +If you need to deal with large WebSocket messages, you may reduce the memory usage by streaming the message content. +For large WebSocket messages, the memory usage may be large due to the fact that the text or the bytes must be accumulated until the message is complete before delivering the message event. + +To stream textual or binary messages, you override either `org.eclipse.jetty.websocket.api.Session.Listener.onWebSocketPartialText(\...)` or `org.eclipse.jetty.websocket.api.Session.Listener.onWebSocketPartialBinary(\...)`. + +These methods receive _chunks_ of, respectively, text and bytes that form the whole WebSocket message. + +You may accumulate the chunks yourself, or process each chunk as it arrives, or stream the chunks elsewhere, for example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=streamingListenerEndpoint] +---- + +[[endpoints-annotated]] +=== Annotated Endpoints + +A WebSocket endpoint may annotate methods with `+org.eclipse.jetty.websocket.api.annotations.*+` annotations to receive WebSocket events. + +Each annotated event method may take an optional `Session` argument as its first parameter: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=annotatedEndpoint] +---- +<1> Use the `@WebSocket` annotation at the class level to make it a WebSocket endpoint, and disable auto-demand. +<2> Use the `@OnWebSocketOpen` annotation for the _open_ event. +As this is the first event notified to the endpoint, you can configure the `Session` object. +<3> Use the `@OnWebSocketMessage` annotation for the _message_ event, both for textual and binary messages. +<4> Use the `@OnWebSocketError` annotation for the _error_ event. +<5> Use the `@OnWebSocketClose` annotation for the _close_ event. + +[[endpoints-annotated-streaming]] +==== Message Streaming Reads + +If you need to deal with large WebSocket messages, you may reduce the memory usage by streaming the message content. + +To stream textual or binary messages, you still use the `@OnWebSocketMessage` annotation, but you change the signature of the method to take an additional `boolean` parameter: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=partialAnnotatedEndpoint] +---- + +Alternatively, but less efficiently, you can use the `@OnWebSocketMessage` annotation, but you change the signature of the method to take, respectively, a `Reader` and an `InputStream`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=streamingAnnotatedEndpoint] +---- + +[CAUTION] +==== +`Reader` or `InputStream` only offer blocking APIs, so if the remote peers are slow in sending the large WebSocket messages, reading threads may be blocked in `Reader.read(char[])` or `InputStream.read(byte[])`, possibly exhausting the thread pool. +==== + +Note that when you use blocking APIs, the invocations to `Session.demand()` are now performed by the `Reader` or `InputStream` implementations (as well as the `ByteBuffer` lifecycle management). +You indirectly control the demand by deciding when to read from `Reader` or `InputStream`. + +[[session]] +== WebSocket Session + +A WebSocket session is the entity that offers an API to send data to the remote peer, to close the WebSocket connection, and to configure WebSocket connection parameters. + +[[session-configure]] +=== Configuring the Session + +You may configure the WebSocket session behavior using the `org.eclipse.jetty.websocket.api.Session` APIs. +You want to do this as soon as you have access to the `Session` object, typically from the <> handler: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=sessionConfigure] +---- + +The settings that can be configured include: + +maxBinaryMessageSize:: +the maximum size in bytes of a binary message (which may be composed of multiple frames) that can be received. + +maxTextMessageSize:: +the maximum size in bytes of a text message (which may be composed of multiple frames) that can be received. + +maxFrameSize:: +the maximum payload size in bytes of any WebSocket frame that can be received. + +inputBufferSize:: +the input (read from network/transport layer) buffer size in bytes; it has no relationship with the WebSocket frame size or message size. + +outputBufferSize:: +the output (write to network/transport layer) buffer size in bytes; it has no relationship to the WebSocket frame size or message size. + +autoFragment:: +whether WebSocket frames are automatically fragmented to respect the maximum frame size. + +idleTimeout:: +the duration that a WebSocket connection may remain idle (that is, there is no network traffic, neither in read nor in write) before being closed by the implementation. + +Please refer to the `Session` link:{javadoc-url}/org/eclipse/jetty/websocket/api/Session.html[javadocs] for the complete list of configuration APIs. + +[[session-send]] +=== Sending Data + +To send data to the remote peer, you can use the non-blocking APIs offered by `Session`. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=sendNonBlocking] +---- +<1> Non-blocking APIs require a `Callback` parameter. +<2> Note how the second send must be performed from inside the callback. +<3> Sequential sends may throw `WritePendingException`. + +[IMPORTANT] +==== +Non-blocking APIs are more difficult to use since you are required to meet the following condition: + +* You cannot initiate another send of any kind until the previous send is completed. + +For example, if you have initiated a text send, you cannot initiate another text or binary send, until the previous send has completed. +==== + +This requirement is necessary to avoid unbounded buffering that could lead to ``OutOfMemoryError``s. + +[CAUTION] +==== +We strongly recommend that you follow the condition above. + +However, there may be cases where you want to explicitly control the number of outgoing buffered messages using `RemoteEndpoint.setMaxOutgoingFrames(int)`. + +Remember that trying to control the number of outgoing frames is very difficult and tricky; you may set `maxOutgoingFrames=4` and have a situation where 6 threads try to concurrently send messages: threads 1 to 4 will be able to successfully buffer their messages, thread 5 may fail, but thread 6 may succeed because one of the previous threads completed its send. +At this point you have an out-of-order message delivery that could be unexpected and very difficult to troubleshoot because it will happen non-deterministically. +==== + +While non-blocking APIs are more difficult to use, they don't block the sender thread and therefore use less resources, which in turn typically allows for greater scalability under load: with respect to blocking APIs, non-blocking APIs need less resources to cope with the same load. + +[[session-send-stream]] +==== Streaming Send APIs + +If you need to send large WebSocket messages, you may reduce the memory usage by streaming the message content. + +The Jetty WebSocket APIs offer `sendPartial*(\...)` methods that allow you to send a chunk of the whole message at a time, therefore reducing the memory usage since it is not necessary to have the whole message `String` or `ByteBuffer` in memory to send it. + +The Jetty WebSocket APIs for streaming the message content are non-blocking and therefore you should wait (without blocking!) for the callbacks to complete. + +Fortunately, Jetty provides the `IteratingCallback` utility class (described in more details xref:arch/io.adoc#echo[in this section]) which greatly simplify the use of non-blocking APIs: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=streamSendNonBlocking] +---- +<1> Implementing `Callback` allows to pass `this` to `sendPartialBinary(\...)`. +<2> The `process()` method is called iteratively when each `sendPartialBinary(\...)` is completed. +<3> Sends the message chunks. +<4> When the last chunk as been sent, complete successfully the `IteratingCallback`. +<5> Only when the `IteratingCallback` is completed successfully, demand for more WebSocket events. + +[[session-ping]] +=== Sending Ping/Pong + +The WebSocket protocol defines two special frame, named `PING` and `PONG` that may be interesting to applications for these use cases: + +* Calculate the round-trip time with the remote peer. +* Keep the connection from being closed due to idle timeout -- a heartbeat-like mechanism. + +To handle `PING`/`PONG` events, you may implement methods `Session.Listener.onWebSocketPing(ByteBuffer)` and/or `Session.Listener.onWebSocketPong(ByteBuffer)`. + +[NOTE] +==== +`PING`/`PONG` events are also supported when using annotations via the `OnWebSocketFrame` annotation. +==== + +`PING` frames may contain opaque application bytes, and the WebSocket implementation replies to them with a `PONG` frame containing the same bytes: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=pingPongListener] +---- + +[[session-close]] +=== Closing the Session + +When you want to terminate the communication with the remote peer, you close the `Session`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/WebSocketDocs.java[tags=sessionClose] +---- + +Closing a WebSocket `Session` carries a status code and a reason message that the remote peer can inspect in the _close_ event handler (see <>). + +[NOTE] +==== +The reason message is optional, and may be truncated to fit into the WebSocket frame sent to the client. +It is best to use short tokens such as `"shutdown"`, or `"idle_timeout"`, etc. or even application specific codes such as `"0001"` or `"00AF"` that can be converted by the application into more meaningful messages. +==== diff --git a/documentation/jetty/modules/programming-guide/pages/index.adoc b/documentation/jetty/modules/programming-guide/pages/index.adoc new file mode 100644 index 000000000000..85c011b3f08f --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/index.adoc @@ -0,0 +1,25 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +[reftext=Programming Guide] += Jetty {page-version} Programming Guide + +The Eclipse Jetty Programming Guide targets developers who want to use the Jetty libraries in their applications. + +The Jetty libraries provide the client-side and server-side APIs to work with various web protocols such as HTTP/1.1, HTTP/2, HTTP/3, WebSocket and FastCGI. + +You may use the xref:client/index.adoc[Jetty client-side library] in your application to make calls to third party REST services, or to other REST microservices in your system. + +Likewise, you may use the xref:server/index.adoc[Jetty server-side library] to quickly create an HTTP or REST service without having to create a web application archive file (a `+*.war+` file) and without having to deploy it to a Jetty standalone server that you would have to download and install. + +This guide will walk you through the design of the Jetty libraries and how to use its classes to write your applications. diff --git a/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-jspc-maven-plugin.adoc b/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-jspc-maven-plugin.adoc new file mode 100644 index 000000000000..ee7868521d9e --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-jspc-maven-plugin.adoc @@ -0,0 +1,242 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Jetty Jspc Maven Plugin + +This plugin will pre-compile your JSP and works in conjunction with the Maven war plugin to put them inside an assembled war. + +[[jspc-config]] +== Configuration + +Here's the basic setup required to put the JSPC plugin into your build for the Jakarta EE {ee-current} environment: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-jspc-maven-plugin + {version} + + + jspc + + jspc + + + + + + +---- + +The configurable parameters are as follows: + +webXmlFragment:: +Default value: `${project.basedir}/target/webfrag.xml` ++ +File into which to generate the servlet declarations. +Will be merged with an existing `web.xml`. +webAppSourceDirectory:: +Default value: `${project.basedir}/src/main/webapp` ++ +Root of resources directory where jsps, tags etc are located. +webXml:: +Default value: `${project.basedir}/src/main/webapp/WEB-INF/web.xml` ++ +The web.xml file to use to merge with the generated fragments. +includes:: +Default value: `+**/*.jsp, **/*.jspx+` ++ +The comma separated list of patterns for file extensions to be processed. +excludes:: +Default value: `+**/.svn/**+` ++ +The comma separated list of patterns for file extensions to be skipped. +classesDirectory:: +Default value: `${project.build.outputDirectory}` ++ +Location of classes for the webapp. +generatedClasses:: +Default value: `${project.build.outputDirectory}` ++ +Location to put the generated classes for the jsps. +insertionMarker:: +Default value: _none_ ++ +A marker string in the src `web.xml` file which indicates where to merge in the generated web.xml fragment. +Note that the marker string will NOT be preserved during the insertion. +Can be left blank, in which case the generated fragment is inserted just before the line containing ``. +useProvidedScope:: +Default value: false ++ +If true, jars of dependencies marked with provided will be placed on the compilation classpath. +mergeFragment:: +Default value: true ++ +Whether or not to merge the generated fragment file with the source web.xml. +The merged file will go into the same directory as the webXmlFragment. +keepSources:: +Default value: false ++ +If true, the generated .java files are not deleted at the end of processing. +scanAllDirectories:: +Default value: true ++ +Determines if dirs on the classpath should be scanned as well as jars. +If true, this allows scanning for tlds of dependent projects that +are in the reactor as unassembled jars. +scanManifest:: +Default value: true ++ +Determines if the manifest of JAR files found on the classpath should be scanned. +sourceVersion:: +Java version of jsp source files. +The default value depends on the version of the `jetty-{ee-current}-jspc-maven-plugin`. + +targetVersion:: +Java version of class files generated from jsps. +The default value depends on the version of the `jetty-{ee-current}-jspc-maven-plugin`. + +tldJarNamePatterns:: +Default value: `+.*taglibs[^/]*\.jar|.*jstl[^/]*\.jar$+` ++ +Patterns of jars on the 'system' (ie container) path that contain tlds. +Use | to separate each pattern. +jspc:: +Default value: the `org.apache.jasper.JspC` instance being configured. ++ +The JspC class actually performs the pre-compilation. +All setters on the JspC class are available. + +Taking all the default settings, here's how to configure the war plugin to use the generated `web.xml` that includes all of the jsp servlet declarations: + +[,xml] +---- + + org.apache.maven.plugins + maven-war-plugin + + ${project.basedir}/target/web.xml + + +---- + +[[jspc-production-precompile]] +== Precompiling only for Production Build + +As compiling jsps is usually done during preparation for a production release and not usually done during development, it is more convenient to put the plugin setup inside a which which can be deliberately invoked during prep for production. + +For example, the following profile will only be invoked if the flag `-Dprod` is present on the run line: + +[,xml,subs=attributes+] +---- + + + prod + + prod + + + + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-jspc-maven-plugin + {version} + + + + org.apache.maven.plugins + maven-war-plugin + + + + + + +---- + +The following invocation would cause your code to be compiled, the jsps to be compiled, the and s inserted in the `web.xml` and your webapp assembled into a war: + +---- +$ mvn -Dprod package +---- + +[[jspc-overlay-precompile]] +== Precompiling Jsps with Overlaid Wars + +Precompiling jsps with an overlaid war requires a bit more configuration. +This is because you need to separate the steps of unpacking the overlaid war and then repacking the final target war so the `jetty-{ee-current}-jspc-maven-plugin` has the opportunity to access the overlaid resources. + +In the following example the overlaid war will provide the `web.xml` file but the jsps will be in `src/main/webapp` (i.e. part of the project that uses the overlay). +The overlaid war file will be unpacked, the jsps compiled and their servlet definitions merged into the extracted `web.xml`, and everything packed into a war. + +An example configuration of the war plugin that separates those phases into an unpack phase, and then a packing phase: + +[,xml] +---- + + maven-war-plugin + + + unpack + exploded + generate-resources + + target/foo + + + + org.eclipse.jetty.{ee-current}.demos + jetty-{ee-current}-demo-jetty-webapp + + + + + + pack + war + package + + target/foo + target/web.xml + + + + +---- + +Now you also need to configure the `jetty-{ee-current}-jspc-maven-plugin` so that it can use the web.xml that was extracted by the war unpacking and merge in the generated definitions of the servlets. +This is in `target/foo/WEB-INF/web.xml`. +Using the default settings, the `web.xml` merged with the jsp servlet definitions will be put into `target/web.xml`. + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-jspc-maven-plugin + {version} + + + jspc + + jspc + + + target/foo/WEB-INF/web.xml + **/*.foo + **/*.fff + + + + +---- diff --git a/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-helloworld.adoc b/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-helloworld.adoc new file mode 100644 index 000000000000..64a3b8f3c791 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-helloworld.adoc @@ -0,0 +1,276 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Using Maven + +http://maven.apache.org/[Apache Maven] is a software project management and comprehension tool. +Based on the concept of a project object model (POM), Maven can manage a project's build, reporting and documentation from a central piece of information. + +It is an ideal tool to build a web application project, and such projects can use the xref:maven-jetty/jetty-maven-plugin.adoc[jetty-maven-plugin] to easily run the web application and save time in development. +You can also use Maven to build, test and run a project which embeds Jetty. + +[NOTE] +==== +Use of Maven and the jetty-maven-plugin is *not* required. +Using Maven for Jetty implementations is a popular choice, but users encouraged to manage their projects in whatever way suits their needs. +Other popular tools include Ant and Gradle. +==== + +[[configuring-embedded-jetty-with-maven]] +== Using Embedded Jetty with Maven + +Maven uses convention over configuration, so it is best to use the project structure Maven recommends. +You can use _http://maven.apache.org/guides/introduction/introduction-to-archetypes.html[archetypes]_ to quickly setup Maven projects, but we will set up the structure manually for this simple tutorial example: + +---- +> mkdir JettyMavenHelloWorld +> cd JettyMavenHelloWorld +> mkdir -p src/main/java/org/example +---- + +[[creating-helloworld-class]] +=== Creating the HelloWorld Class + +Use an editor to create the file `src/main/java/org/example/HelloWorld.java` with the following contents: + +[,java] +---- +package org.example; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import java.io.IOException; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +public class HelloWorld extends AbstractHandler +{ + public void handle(String target, + Request baseRequest, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, ServletException + { + response.setContentType("text/html;charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + baseRequest.setHandled(true); + response.getWriter().println("

      Hello World

      "); + } + + public static void main(String[] args) throws Exception + { + Server server = new Server(8080); + server.setHandler(new HelloWorld()); + + server.start(); + server.join(); + } +} +---- + +[[creating-embedded-pom-descriptor]] +=== Creating the POM Descriptor + +The `pom.xml` file declares the project name and its dependencies. +Use an editor to create the file `pom.xml` in the `JettyMavenHelloWorld` directory with the following contents: + +[,xml,subs=attributes+] +---- + + + 4.0.0 + org.example + hello-world + 0.1-SNAPSHOT + jar + Jetty HelloWorld + + + {version} + + + + + org.eclipse.jetty + jetty-server + $\{jettyVersion} + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.1 + + java + + + org.example.HelloWorld + + + + + +---- + +[[buildng-and-running-embedded-helloworld]] +=== Building and Running Embedded HelloWorld + +You can now compile and execute the HelloWorld class by using these commands: + +---- +> mvn clean compile exec:java +---- + +Point your browser to `+http://localhost:8080+` to see the _Hello World_ page. + +[[developing-standard-webapp-with-jetty-and-maven]] +== Developing a Standard WebApp with Jetty and Maven + +The previous section demonstrated how to use Maven with an application that embeds Jetty. +We can instead develop a standard webapp using Maven and Jetty. +First create the Maven structure (you can use the maven webapp archetype instead if you prefer): + +---- +> mkdir JettyMavenHelloWarApp +> cd JettyMavenHelloWebApp +> mkdir -p src/main/java/org/example +> mkdir -p src/main/webapp/WEB-INF +---- + +[[creating-servlet]] +=== Creating a Servlet + +Use an editor to create the file `src/main/java/org/example/HelloServlet.java` with the following contents: + +[,java] +---- +package org.example; + +import java.io.IOException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class HelloServlet extends HttpServlet +{ + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + response.setContentType("text/html"); + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().println("

      Hello Servlet

      "); + response.getWriter().println("session=" + request.getSession(true).getId()); + } +} +---- + +This servlet must be declared in the web deployment descriptor, so create the file `src/main/webapp/WEB-INF/web.xml` and add the following contents: + +[,xml] +---- + + + + + Hello + org.example.HelloServlet + + + Hello + /hello/* + + + +---- + +[[creating-plugin-pom-descriptor]] +=== Creating the POM Descriptor + +The `pom.xml` file declares the project name and its dependencies. +Use an editor to create the file `pom.xml` with the following contents in the `JettyMavenHelloWarApp` directory, noting particularly the declaration of the xref:maven-jetty/jetty-maven-plugin.adoc[jetty-maven-plugin] for the Jakarta {ee-current-caps} environment: + +[,xml,subs=attributes+] +---- + + + 4.0.0 + org.example + hello-world + 0.1-SNAPSHOT + war + Jetty HelloWorld WebApp + + + {version} + + + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + provided + + + + + + + org.eclipse.jetty.ee10 + jetty-ee10-maven-plugin + $\{jettyVersion} + + + + + +---- + +[[building-and-running-web-application]] +=== Building and Running the Web Application + +The web application can now be built and run without first needing to assemble it into a war by using the xref:maven-jetty/jetty-maven-plugin.adoc[jetty-maven-plugin] via the command: + +---- +> mvn jetty:run +---- + +You can see the static and dynamic content at `+http://localhost:8080/hello+` + +There are a great deal of configuration options available for the jetty-maven-plugin to help you build and run your webapp. +The full reference is at xref:maven-jetty/jetty-maven-plugin.adoc[Configuring the Jetty Maven Plugin]. + +[[building-war-file]] +=== Building a WAR file + +A Web Application Archive (WAR) file can be produced from the project with the command: + +---- +> mvn package +---- + +The resulting war file is in the `target` directory and may be deployed on any standard servlet server, including xref:operations-guide:deploy/index.adoc[Jetty]. diff --git a/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-plugin.adoc b/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-plugin.adoc new file mode 100644 index 000000000000..f15bd1bc8124 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/maven-jetty/jetty-maven-plugin.adoc @@ -0,0 +1,1162 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Using the Jetty Maven Plugin + +The Jetty Maven plugin is useful for rapid development and testing. +It can optionally periodically scan a project for changes and automatically redeploy the webapp if any are found. +This makes the development cycle more productive by eliminating the build and deploy steps: use an IDE to make changes to the project, and the running web container automatically picks them up, allowing them to be tested straight away. + +There are only 4 goals to run a webapp in Jetty: + +* <> +* <> +* <> +* <> + +Plus two utility goals: + +* <> +* <> + +`jetty:run` and `jetty:start` are alike in that they both run an _unassembled_ webapp in Jetty,however `jetty:run` is designed to be used at the command line, whereas `jetty:start` is specifically designed to be bound to execution phases in the build lifecycle. +`jetty:run` will pause Maven while jetty is running, echoing all output to the console, and then stop Maven when jetty exits. +`jetty:start` will not pause Maven, will write all its output to a file, and will not stop Maven when jetty exits. + +`jetty:run-war` and `jetty:start-war` are similar in that they both run an _assembled_ war file in Jetty. +However, `jetty:run-war` is designed to be run at the command line, whereas `jetty:start-war` is specifically designed to be bound to execution phases in the build lifecycle. +`jetty:run-war` will pause Maven while Jetty is running, echoing all output to the console, and then stop Maven when Jetty exits. +`jetty:start-war` will not pause Maven, will write all its output to a file, and will not stop Maven when Jetty exits. + +[IMPORTANT] +==== +While the Jetty Maven Plugin can be very useful for development we do not recommend its use in a _production capacity_. +In order for the plugin to work it needs to leverage many internal Maven APIs and Maven itself it not a production deployment tool. +We recommend either the traditional xrefr:og-deploy[distribution] deployment approach or using xref:operations-guide:arch/index.adoc[embedded Jetty]. +==== + +[[get-up-and-running]] +== Get Up and Running + +Since Jetty 12, Jetty Maven plugin is repackaged for the corresponding Jakarta EE version with an `eeX` classifier in the groupId and artifactId. + +First, add `jetty-{ee-all}-maven-plugin` to your `pom.xml` definition. Here's an example of how to do that for Jakarta {ee-current-caps}: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + {version} + +---- + +Then, from the same directory as the project's root `pom.xml`, type: + +---- +mvn jetty:run +---- + +This starts Jetty and serves up the project on `+http://localhost:8080/+`. + +Jetty will continue to run until you stop it. +By default, it will not automatically restart your webapp. +Set a non-zero `` value to have Jetty scan your webapp for changes and automatically redeploy, or set `` to `0` to cause manual redeployment by hitting the kbd:[Enter] key. + +Terminate the plugin with a kbd:[Ctrl+c] in the terminal window where it is running. + +[NOTE] +==== +The classpath of the running Jetty instance and its deployed webapp are managed by Maven, and may not be exactly what you expect. +For example: a webapp's dependent jars might be referenced via the local repository, or other projects in the reactor, not the `WEB-INF/lib` directory. +==== + +[[supported-goals]] +== Supported Goals + +The goals prefixed with `"run-"` are designed to be used at the _command line_. +They first run a Maven build on your project to ensure at least the classes are all built. +They then start Jetty and pause the Maven build process until Jetty is manually terminated, at which time the build will also be terminated. +Jetty can scan various files in your project for changes and redeploy the webapp as necessary, or you can choose to manually trigger a redeploy if you prefer. +All output from Jetty is echoed to the console. + +The goals prefixed with `"start-"` are designed to be used with _build lifecycle bindings in the pom_, and _not_ at the command line. +No part of your project will be rebuilt by invoking these goals - you should ensure that your bind the execution to a build phase where all necessary parts of your project have been built. +Maven will start and terminate Jetty at the appropriate points in the build lifecycle, continuing with the build. +Jetty will _not_ scan any files in your project for changes, and your webapp will _not_ be redeployed either automatically or manually. +Output from Jetty is directed to a file in the `target` directory. + +To see a list of all goals supported by the Jetty Maven plugin, do: + +---- +mvn jetty:help +---- + +To see the detailed list of parameters that can be configured for a particular goal, in addition to its description, do: + +---- +mvn jetty:help -Ddetail=true -Dgoal= +---- + +[[deployment-modes]] +== Deployment Modes +All of the `"run-"` and `"start-"` goals can deploy your webapp either into the running maven process, or forked into a new child process, or forked into a Jetty distribution on disk. + +This is controlled by setting the `deployMode` configuration parameter in the pom, but can also be set by defining the Maven property 'jetty.deployMode'. + +=== Embedded + +`deployMode` of `EMBED`. +This is the "classic" Jetty Maven plugin deployment mode, running in-process with Maven. +This is the _default_ mode. + +These extra configuration parameters are available: + +httpConnector:: +Optional. +Note that to configure a https connector, you will need to use xml configuration files instead, setting the `jettyXmls` parameter. +This parameter can only be used to configure a standard http connector. +If not specified, Jetty will create a link:{javadoc-url}/org/eclipse/jetty/server/ServerConnector.html[ServerConnector] instance listening on port 8080. +You can change this default port number by using the system property `jetty.http.port` on the command line, for example, `mvn -Djetty.http.port=9999 jetty:run`. +Alternatively, you can use this configuration element to set up the information for the ServerConnector. +The following are the valid configuration sub-elements: +port::: +The port number for the connector to listen on. +By default it is 8080. +host::: +The particular interface for the connector to listen on. +By default, all interfaces. +name::: +The name of the connector, which is useful for configuring contexts to respond only on particular connectors. +idleTimeout::: +Maximum idle time for a connection. +You could instead configure the connectors in a standard xref:operations-guide:xml/index.adoc[jetty xml config file] and put its location into the `jettyXml` parameter. +Note that since Jetty 9.0 it is no longer possible to configure a https connector directly in the pom.xml: you need to use jetty xml config files to do it. +loginServices:: +Optional. +A list of `org.eclipse.jetty.security.LoginService` implementations. Note that there is no default realm. +If you use a realm in your `web.xml` you can specify a corresponding realm here. +You could instead configure the login services in a jetty xml file and add its location to the `jettyXml` parameter. +See <>. +requestLog:: +Optional. +An implementation of the `org.eclipse.jetty.server.RequestLog` request log interface. +There are three other ways to configure the `RequestLog`: ++ + * In a Jetty xml config file, as specified in the `jettyXml` parameter. + * In a context xml config file, as specified in the `contextXml` parameter. + * In the `webApp` element. ++ +See xref:server/http.adoc#request-logging[Configuring Request Logs] for more information. +server:: +Optional as of Jetty 9.3.1. +This would configure an instance of `org.eclipse.jetty.server.Server` for the plugin to use, however it is usually _not_ necessary to configure this, as the plugin will automatically configure one for you. +In particular, if you use the `jettyXmls` element, then you generally _don't_ want to define this element, as you are probably using the `jettyXmls` file/s to configure up a Server with a special constructor argument, such as a custom threadpool. +If you define both a `server` element and use a `jettyXmls` element which points to a config file that has a line like `` then the the xml configuration will override what you configure for the `server` in the `pom.xml`. +useProvidedScope:: +Default value is `false`. +If true, the dependencies with `provided` are placed onto the __container classpath__. +Be aware that this is _not_ the webapp classpath, as `provided` indicates that these dependencies would normally be expected to be provided by the container. +You should very rarely ever need to use this. +See <>. + +=== Forked + +`deployMode` of `FORK`. +This is similar to the old "jetty:run-forked" goal - a separate process is forked to run your webapp embedded into jetty. +These extra configuration parameters are available: + +env:: +Optional. +Map of key/value pairs to pass as environment to the forked JVM. +jvmArgs:: +Optional. +A space separated string representing arbitrary arguments to pass to the forked JVM. +forkWebXml:: +Optional. +Defaults to `target/fork-web.xml`. +This is the location of a quickstart web xml file that will be _generated_ during the forking of the jetty process. +You should not need to set this parameter, but it is available if you wish to control the name and location of that file. +useProvidedScope:: +Default value is `false`. +If true, the dependencies with `provided` are placed onto the __container classpath__. +Be aware that this is NOT the webapp classpath, as "provided" indicates that these dependencies would normally be expected to be provided by the container. +You should very rarely ever need to use this. +See <>. + +=== In a jetty distribution + +`deployMode` of `EXTERNAL`. +This is similar to the old "jetty:run-distro" goal - your webapp is deployed into a dynamically downloaded, unpacked and configured Jetty distribution. +A separate process is forked to run it. +These extra configuration parameters are available: + +jettyBase:: +Optional. +The location of an existing Jetty base directory to use to deploy the webapp. +The existing base will be copied to the `target/` directory before the webapp is deployed. +If there is no existing jetty base, a fresh one will be made in `target/jetty-base`. +jettyHome:: +Optional. +The location of an existing unpacked Jetty distribution. +If one does not exist, a fresh Jetty distribution will be downloaded from Maven and installed to the `target` directory. +jettyOptions:: +Optional. +A space separated string representing extra arguments to the synthesized Jetty command line. +Values for these arguments can be found in the section titled "Options" in the output of `java -jar $jetty.home/start.jar --help`. +jvmArgs:: +Optional. +A space separated string representing arguments that should be passed to the jvm of the child process running the distro. +modules:: +Optional. +An array of names of additional Jetty modules that the Jetty child process will activate. +Use this to change the <> instead of `useProvidedScope`. +These modules are enabled by default: `server,http,webapp,deploy`. + + +[[common-configuration]] +== Common Configuration + +The following configuration parameters are common to all of the `"run-"` and `"start-"` goals: + +deployMode:: +One of `EMBED`, `FORK` or `EXTERNAL`. +Default `EMBED`. +Can also be configured by setting the Maven property `jetty.deployMode`. +This parameter determines whether the webapp will run in Jetty in-process with Maven, forked into a new process, or deployed into a Jetty distribution. +See <>. +jettyXmls:: +Optional. +A comma separated list of locations of Jetty xml files to apply in addition to any plugin configuration parameters. +You might use it if you have other webapps, handlers, specific types of connectors etc., to deploy, or if you have other Jetty objects that you cannot configure from the plugin. +skip:: +Default is false. +If true, the execution of the plugin exits. +Same as setting the SystemProperty `-Djetty.skip` on the command line. +This is most useful when configuring Jetty for execution during integration testing and you want to skip the tests. +excludedGoals:: +Optional. +A list of Jetty plugin goal names that will cause the plugin to print an informative message and exit. +Useful if you want to prevent users from executing goals that you know cannot work with your project. +supportedPackagings:: +Optional. +Defaults to `war`. +This is a list of maven <packaging> types that can work with the jetty plugin. +Usually, only `war` projects are suitable, however, you may configure other types. +The plugin will refuse to start if the <packaging> type in the pom is not in list of `supportedPackagings`. +systemProperties:: +Optional. +Allows you to configure System properties for the execution of the plugin. +For more information, see <>. +systemPropertiesFile:: +Optional. +A file containing System properties to set for the execution of the plugin. +By default, settings that you make here *do not* override any system properties already set on the command line, by the JVM, or in the POM via `systemProperties`. +Read <> for how to force overrides. +jettyProperties:: +Optional. +A map of property name, value pairs. +Allows you to configure standard jetty properties. + +[[container-classpath]] +== Container Classpath vs WebApp Classpath + +The Servlet Specification makes a strong distinction between the classpath for a _webapp_, and the classpath of the _container_. +When running in Maven, the plugin's classpath is equivalent to the _container_ classpath. +It will make a classpath for the _webapp_ to be deployed comprised of <dependencies> specified in the pom. + +If your production environment places specific jars onto the container's classpath, the equivalent way to do this with Maven is to define these as <dependencies> for the _plugin_ itself, not the _project_. See http://maven.apache.org/pom.html#Plugins[configuring maven plugins]. +This is suitable if you are using either `EMBED` or `FORK` mode. +If you are using `EXTERNAL` mode, then you should configure the `modules` parameter with the names of the Jetty modules that place these jars onto the container classpath. + +Note that in `EMBED` or `FORK` mode, you could also influence the container classpath by setting the `useProvidedScope` parameter to `true`: this will place any dependencies with <scope>provided<scope> onto the plugin's classpath. +Use this very cautiously: as the plugin already automatically places most Jetty jars onto the classpath, you could wind up with duplicate jars. + + +[[jetty-run-goal]] +== jetty:run + +The `run` goal deploys a webapp that is _not_ first built into a WAR. +A virtual webapp is constructed from the project's sources and its dependencies. +It looks for the constituent parts of a webapp in the Maven default project locations, although you can override these in the plugin configuration. +For example, by default it looks for: + +* resources in `${project.basedir}/src/main/webapp` +* classes in `${project.build.outputDirectory}` +* `web.xml` in `${project.basedir}/src/main/webapp/WEB-INF/` + +The plugin first runs a Maven parallel build to ensure that the classes are built and up-to-date before deployment. +If you change the source of a class and your IDE automatically compiles it in the background, the plugin picks up the changed class (note you need to configure a non-zero `scan` interval for automatic redeployment). + +If the plugin is invoked in a multi-module build, any dependencies that are also in the Maven reactor are used from their compiled classes. + +Once invoked, you can configure the plugin to run continuously, scanning for changes in the project and automatically performing a hot redeploy when necessary. +Any changes you make are immediately reflected in the running instance of Jetty, letting you quickly jump from coding to testing, rather than going through the cycle of: code, compile, reassemble, redeploy, test. + +The Maven build will be paused until Jetty exits, at which time Maven will also exit. + +Stopping Jetty is accomplished by typing `cntrl-c` at the command line. + +Output from Jetty will be logged to the console. + +Here is an example, which turns on scanning for changes every ten seconds, and sets the webapp context path to `/test`: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + {version} + + 10 + + /test + + + +---- + +=== Configuration + +webApp:: +This is an instance of link:{javadoc-url}/org/eclipse/jetty/{ee-current}}/maven/plugin/MavenWebAppContext.html[org.eclipse.jetty.{ee-current}.maven.plugin.MavenWebAppContext], which is an extension to the class link:{javadoc-url}/org/eclipse/jetty/{ee-current}/webapp/WebAppContext.hml[`org.eclipse.jetty.{ee-current}.webapp.WebAppContext`]. +You can use any of the setter methods on this object to configure your webapp. +Here are a few of the most useful ones: ++ +contextPath;; +The context path for your webapp. By default, this is set to `/`. +If using a custom value for this parameter, you should include the leading `/`, example `/mycontext`. +descriptor;; +The path to the `web.xml` file for your webapp. +By default, the plugin will look in `src/main/webapp/WEB-INF/web.xml`. +defaultsDescriptor;; +The path to a `webdefault.xml` file that will be applied to your webapp before the `web.xml`. +If you don't supply one, Jetty uses a default file baked into the `jetty-{ee-current}-webapp.jar`. +overrideDescriptor;; +The path to a `web.xml` file that Jetty applies after reading your `web.xml`. +You can use this to replace or add configuration. +jettyEnvXml;; +Optional. +Location of a `jetty-env.xml` file, which allows you to make JNDI bindings that satisfy `env-entry`, `resource-env-ref`, and `resource-ref` linkages in the `web.xml` that are scoped only to the webapp and not shared with other webapps that you might be deploying at the same time (for example, by using a `jettyXml` file). +tempDirectory;; +The path to a dir that Jetty can use to expand or copy jars and jsp compiles when your webapp is running. +The default is `${project.build.outputDirectory}/tmp`. +baseResource;; +The path from which Jetty serves static resources. +Defaults to `src/main/webapp`. +If this location does not exist (because, for example, your project does not use static content), then the plugin will synthesize a virtual static resource location of `target/webapp-synth`. +resourceBases;; +Use instead of `baseResource` if you have multiple directories from which you want to serve static content. +This is an array of directory locations, either as urls or file paths. +baseAppFirst;; +Defaults to "true". +Controls whether any overlaid wars are added before or after the original base resource(s) of the webapp. +See the section on <> for more information. +containerIncludeJarPattern;; +Defaults to `.*/jetty-jakarta-servlet-api-[^/]*\.jar$|.*jakarta.servlet.jsp.jstl-[^/]*\.jar|.*taglibs-standard-impl-.*\.jar`. +This is a pattern that is applied to the names of the jars on the container's classpath (ie the classpath of the plugin, not that of the webapp) that should be scanned for fragments, tlds, annotations etc. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[here]. +You can define extra patterns of jars that will be included in the scan. +webInfIncludeJarPattern;; +Defaults to matching _all_ of the dependency jars for the webapp (ie the equivalent of WEB-INF/lib). +You can make this pattern more restrictive to only match certain jars by using this setter. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[here]. +contextXml:: +The path to a context xml file that is applied to your webapp AFTER the `webApp` element. +classesDirectory:: +Location of your compiled classes for the webapp. +You should rarely need to set this parameter. +Instead, you should set `` in your `pom.xml`. +testClassesDirectory:: +Location of the compiled test classes for your webapp. By default this is `${project.build.testOutputDirectory}`. +useTestScope:: +If true, the classes from `testClassesDirectory` and dependencies of scope "test" are placed first on the classpath. +By default this is false. +scan:: +The pause in seconds between sweeps of the webapp to check for changes and automatically hot redeploy if any are detected. +*By default this is `-1`, which disables hot redeployment scanning.* +A value of `0` means no hot redeployment is done, and that you must use the kbd:[Enter] key to manually force a redeploy. +Any positive integer will enable hot redeployment, using the number as the sweep interval in seconds. +scanTargetPatterns:: +Optional. +List of extra directories with glob-style include/excludes patterns (see http://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html#getPathMatcher-java.lang.String-[javadoc] for http://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html#getPathMatcher-java.lang.String-[FileSystem.getPathMatcher]) to specify other files to periodically scan for changes. +scanClassesPattern:: +Optional. +Include and exclude patterns that can be applied to the classesDirectory for the purposes of scanning, it does *not* affect the classpath. +If a file or directory is excluded by the patterns then a change in that file (or subtree in the case of a directory) is ignored and will not cause the webapp to redeploy. +Patterns are specified as a relative path using a glob-like syntax as described in the http://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html#getPathMatcher-java.lang.String-[javadoc] for http://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html#getPathMatcher-java.lang.String-[FileSystem.getPathMatcher]. +scanTestClassesPattern:: +Optional. +Include and exclude patterns that can be applied to the testClassesDirectory for the purposes of scanning, it does *not* affect the classpath. +If a file or directory is excluded by the patterns then a change in that file (or subtree in the case of a directory) is ignored and will not cause the webapp to redeploy. +Patterns are specified as a relative path using a glob-like syntax as described in the http://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html#getPathMatcher-java.lang.String-[javadoc] for http://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html#getPathMatcher-java.lang.String-[FileSystem.getPathMatcher]. + +See <> for other configuration parameters available when using the `run` goal in EMBED, FORK or EXTERNAL modes. + +Here is an example of a pom configuration for the plugin with the `run` goal: + +[,xml,subs=attributes+] +---- + + ... + + ... + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + {version} + + + / + ${project.basedir}/src/over/here/web.xml + ${project.basedir}/src/over/here/jetty-env.xml + ${project.basedir}/src/staticfiles + + ${project.basedir}/somewhere/else + + + **/Foo.class + + + + + src/other-resources + + **/*.xml + **/*.properties + + + **/myspecial.xml + **/myspecial.properties + + + + + + + ... + +---- + +If, for whatever reason, you cannot run on an unassembled webapp, the goal `run-war` works on assembled webapps. + +[[jetty-run-war-goal]] +== jetty:run-war + +When invoked at the command line this goal first executes a maven build of your project to the package phase. + +By default it then deploys the resultant war to Jetty, but you can use this goal instead to deploy _any_ war file by simply setting the `<webApp><war>` configuration parameter to its location. + +If you set a non-zero `scan`, Jetty watches your `pom.xml` and the WAR file; if either changes, it redeploys the war. + +The maven build is held up until Jetty exits, which is achieved by typing `cntrl-c` at the command line. + +All Jetty output is directed to the console. + +=== Configuration + +Configuration parameters are: + +webApp:: +war::: +The location of the built WAR file. This defaults to `${project.build.directory}/${project.build.finalName}.war`. +You can set it to the location of any pre-built war file. +contextPath::: +The context path for your webapp. By default, this is set to `/`. +If using a custom value for this parameter, you should include the leading `/`, example `/mycontext`. +defaultsDescriptor::: +The path to a `webdefault.xml` file that will be applied to your webapp before the `web.xml`. +If you don't supply one, Jetty uses a default file baked into the `jetty-{ee-current}-webapp.jar`. +overrideDescriptor::: +The path to a `web.xml` file that Jetty applies after reading your `web.xml`. +You can use this to replace or add configuration. +containerIncludeJarPattern::: +Defaults to `.*/jetty-jakarta-servlet-api-[^/]*\.jar$|.*jakarta.servlet.jsp.jstl-[^/]*\.jar|.*taglibs-standard-impl-.*\.jar`. +This is a pattern that is applied to the names of the jars on the container's classpath (ie the classpath of the plugin, not that of the webapp) that should be scanned for fragments, tlds, annotations etc. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[here]. +You can define extra patterns of jars that will be included in the scan. +webInfIncludeJarPattern::: +Defaults to matching _all_ of the dependency jars for the webapp (ie the equivalent of WEB-INF/lib). +You can make this pattern more restrictive to only match certain jars by using this setter. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[here]. +tempDirectory::: +The path to a dir that Jetty can use to expand or copy jars and jsp compiles when your webapp is running. +The default is `${project.build.outputDirectory}/tmp`. +contextXml::: +The path to a context xml file that is applied to your webapp AFTER the `webApp` element. +scan:: +The pause in seconds between sweeps of the webapp to check for changes and automatically hot redeploy if any are detected. +*By default this is `-1`, which disables hot redeployment scanning.* +A value of `0` means no hot redeployment is done, and that you must use the kbd:[Enter] key to manually force a redeploy. +Any positive integer will enable hot redeployment, using the number as the sweep interval in seconds. +scanTargetPatterns:: +Optional. +List of directories with ant-style include/excludes patterns to specify other files to periodically scan for changes. + +See <> for other configuration parameters available when using the `run-war` goal in EMBED, FORK or EXTERNAL modes. + +[[jetty-start-goal]] +== jetty:start + +This is similar to the `jetty:run` goal, however it is _not_ designed to be run from the command line and does _not_ first execute the build up until the `test-compile` phase to ensure that all necessary classes and files of the webapp have been generated. +It will _not_ scan your project for changes and restart your webapp. +It does _not_ pause maven until Jetty is stopped. + +Instead, it is designed to be used with build phase bindings in your pom. +For example to you can have Maven start your webapp at the beginning of your tests and stop at the end. + +If the plugin is invoked as part of a multi-module build, any dependencies that are also in the maven reactor are used from their compiled classes. + +Here's an example of using the `pre-integration-test` and `post-integration-test` Maven build phases to trigger the execution and termination of Jetty: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + {version} + + foo + 9999 + + + + start-jetty + pre-integration-test + + start + + + + stop-jetty + post-integration-test + + stop + + + + +---- + +This goal will generate output from jetty into the `target/jetty-start.out` file. + +=== Configuration + +These configuration parameters are available: + +webApp:: +This is an instance of link:{javadoc-url}/org/eclipse/jetty/{ee-current}/maven/plugin/MavenWebAppContext.html[org.eclipse.jetty.{ee-current}.maven.plugin.MavenWebAppContext], which is an extension to the class link:{javadoc-url}/org/eclipse/jetty/webapp/WebAppContext.hml[`org.eclipse.jetty.ee9.webapp.WebAppContext`]. +You can use any of the setter methods on this object to configure your webapp. +Here are a few of the most useful ones: ++ +contextPath;; +The context path for your webapp. By default, this is set to `/`. +If using a custom value for this parameter, you should include the leading `/`, example `/mycontext`. +descriptor;; +The path to the `web.xml` file for your webapp. +The default is `src/main/webapp/WEB-INF/web.xml`. +defaultsDescriptor;; +The path to a `webdefault.xml` file that will be applied to your webapp before the `web.xml`. +If you don't supply one, Jetty uses a default file baked into the `jetty-{ee-current}-webapp.jar`. +overrideDescriptor;; +The path to a `web.xml` file that Jetty applies after reading your `web.xml`. +You can use this to replace or add configuration. +jettyEnvXml;; +Optional. +Location of a `jetty-env.xml` file, which allows you to make JNDI bindings that satisfy `env-entry`, `resource-env-ref`, and `resource-ref` linkages in the `web.xml` that are scoped only to the webapp and not shared with other webapps that you might be deploying at the same time (for example, by using a `jettyXml` file). +tempDirectory;; +The path to a dir that Jetty can use to expand or copy jars and jsp compiles when your webapp is running. +The default is `${project.build.outputDirectory}/tmp`. +baseResource;; +The path from which Jetty serves static resources. +Defaults to `src/main/webapp`. +resourceBases;; +Use instead of `baseResource` if you have multiple directories from which you want to serve static content. +This is an array of directory names. +baseAppFirst;; +Defaults to "true". +Controls whether any overlaid wars are added before or after the original base resource(s) of the webapp. +See the section on <> for more information. +containerIncludeJarPattern;; +Defaults to `.*/jetty-jakarta-servlet-api-[^/]*\.jar$|.*jakarta.servlet.jsp.jstl-[^/]*\.jar|.*taglibs-standard-impl-.*\.jar`. +This is a pattern that is applied to the names of the jars on the container's classpath (ie the classpath of the plugin, not that of the webapp) that should be scanned for fragments, tlds, annotations etc. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[here]. +You can define extra patterns of jars that will be included in the scan. +webInfIncludeJarPattern;; +Defaults to matching _all_ of the dependency jars for the webapp (ie the equivalent of WEB-INF/lib). +You can make this pattern more restrictive to only match certain jars by using this setter. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[here]. +contextXml:: +The path to a context xml file that is applied to your webapp AFTER the `webApp` element. +classesDirectory:: +Location of your compiled classes for the webapp. +You should rarely need to set this parameter. +Instead, you should set `build outputDirectory` in your `pom.xml`. +testClassesDirectory:: +Location of the compiled test classes for your webapp. By default this is `${project.build.testOutputDirectory}`. +useTestScope:: +If true, the classes from `testClassesDirectory` and dependencies of scope "test" are placed first on the classpath. +By default this is false. +stopPort:: +Optional. +Port to listen on for stop commands. +Useful to use in conjunction with the <> and <> goals. +stopKey:: +Optional. +Used in conjunction with stopPort for stopping jetty. +Useful to use in conjunction with the <> and <> goals. + +These additional configuration parameters are available when running in `FORK` or `EXTERNAL` mode: + +maxChildStartChecks:: +Default is `10`. +This is maximum number of times the parent process checks to see if the forked jetty process has started correctly +maxChildStartCheckMs:: +Default is `200`. +This is the time in milliseconds between checks on the startup of the forked jetty process. + + +[[jetty-start-war-goal]] +== jetty:start-war + +Similarly to the `jetty:start` goal, `jetty:start-war` is designed to be bound to build lifecycle phases in your pom. + +It will _not_ scan your project for changes and restart your webapp. +It does _not_ pause maven until Jetty is stopped. + +By default, if your pom is for a webapp project, it will deploy the war file for the project to jetty. +However, like the `jetty:run-war` project, you can nominate any war file to deploy by defining its location in the `<webApp><war>` parameter. + +If the plugin is invoked as part of a multi-module build, any dependencies that are also in the Maven reactor are used from their compiled classes. + +This goal will generate output from jetty into the `target/jetty-start-war.out` file. + +=== Configuration + +These configuration parameters are available: + +webApp:: +war::: +The location of the built WAR file. This defaults to `${project.build.directory}/${project.build.finalName}.war`. +You can set it to the location of any pre-built war file. +contextPath::: +The context path for your webapp. By default, this is set to `/`. +If using a custom value for this parameter, you should include the leading `/`, example `/mycontext`. +defaultsDescriptor::: +The path to a `webdefault.xml` file that will be applied to your webapp before the `web.xml`. +If you don't supply one, Jetty uses a default file baked into the `jetty-{ee-current}-webapp.jar`. +overrideDescriptor::: +The path to a `web.xml` file that Jetty applies after reading your `web.xml`. +You can use this to replace or add configuration. +containerIncludeJarPattern::: +Defaults to `.*/jetty-jakarta-servlet-api-[^/]*\.jar$|.*jakarta.servlet.jsp.jstl-[^/]*\.jar|.*taglibs-standard-impl-.*\.jar`. +This is a pattern that is applied to the names of the jars on the container's classpath (ie the classpath of the plugin, not that of the webapp) that should be scanned for fragments, tlds, annotations etc. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[here]. +You can define extra patterns of jars that will be included in the scan. +webInfIncludeJarPattern::: +Defaults to matching _all_ of the dependency jars for the webapp (ie the equivalent of WEB-INF/lib). +You can make this pattern more restrictive to only match certain jars by using this setter. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[here]. +tempDirectory::: +The path to a dir that Jetty can use to expand or copy jars and jsp compiles when your webapp is running. +The default is `${project.build.outputDirectory}/tmp`. +contextXml::: +The path to a context xml file that is applied to your webapp AFTER the `webApp` element. +stopPort:: +Optional. +Port to listen on for stop commands. +Useful to use in conjunction with the <>. +stopKey:: +Optional. +Used in conjunction with stopPort for stopping jetty. +Useful to use in conjunction with the <>. + +These additional configuration parameters are available when running in FORK or EXTERNAL mode: + +maxChildStartChecks:: +Default is `10`. +This is maximum number of times the parent process checks to see if the forked Jetty process has started correctly +maxChildStartCheckMs:: +Default is `200`. +This is the time in milliseconds between checks on the startup of the forked Jetty process. + + +[[jetty-stop-goal]] +== jetty:stop + +The stop goal stops a FORK or EXTERNAL mode running instance of Jetty. +To use it, you need to configure the plugin with a special port number and key. +That same port number and key will also be used by the other goals that start Jetty. + +=== Configuration + +stopPort:: +A port number for Jetty to listen on to receive a stop command to cause it to shutdown. +stopKey:: +A string value sent to the `stopPort` to validate the stop command. +stopWait:: +The maximum time in seconds that the plugin will wait for confirmation that Jetty has stopped. +If false or not specified, the plugin does not wait for confirmation but exits after issuing the stop command. + +Here's a configuration example: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + {version} + + 9966 + foo + 10 + + +---- + +Then, while Jetty is running (in another window), type: + +---- +mvn jetty:stop +---- + +The `stopPort` must be free on the machine you are running on. +If this is not the case, you will get an "Address already in use" error message after the "Started ServerConnector ..." message. + +[[jetty-effective-web-xml-goal]] +== jetty:effective-web-xml + +This goal calculates a synthetic `web.xml` (the "effective web.xml") according to the rules of the Servlet Specification taking into account all sources of discoverable configuration of web components in your application: descriptors (`webdefault.xml`, `web.xml`, `web-fragment.xml`s, `web-override.xml`) and discovered annotations (`@WebServlet`, `@WebFilter`, `@WebListener`). +No programmatic declarations of servlets, filters and listeners can be taken into account. + +You can calculate the effective web.xml for any pre-built war file by setting the `<webApp><war>` parameter, or you can calculate it for the unassembled webapp by setting all of the usual `<webApp>` parameters as for `jetty:run`. + +Other useful information about your webapp that is produced as part of the analysis is also stored as context parameters in the effective-web.xml. +The effective-web.xml can be used in conjunction with the xref:operations-guide:quickstart/index.adoc[Quickstart] feature to quickly start your webapp (note that Quickstart is not appropriate for the mvn jetty goals). + +The effective web.xml from these combined sources is generated into a file, which by default is `target/effective-web.xml`, but can be changed by setting the `effectiveWebXml` configuration parameter. + +=== Configuration + +effectiveWebXml:: +The full path name of a file into which you would like the effective web xml generated. +webApp:: +war::: +The location of the built WAR file. This defaults to `${project.build.directory}/${project.build.finalName}.war`. +You can set it to the location of any pre-built war file. +Or you can leave it blank and set up the other `webApp` parameters as per <>, as well as the `webAppSourceDirectory`, `classes` and `testClasses` parameters. +contextPath::: +The context path for your webapp. By default, this is set to `/`. +If using a custom value for this parameter, you should include the leading `/`, example `/mycontext`. +defaultsDescriptor::: +The path to a `webdefault.xml` file that will be applied to your webapp before the `web.xml`. +If you don't supply one, Jetty uses a default file baked into the `jetty-{ee-current}-webapp.jar`. +overrideDescriptor::: +The path to a `web.xml` file that Jetty applies after reading your `web.xml`. +You can use this to replace or add configuration. +containerIncludeJarPattern::: +Defaults to `.*/jetty-jakarta-servlet-api-[^/]*\.jar$|.*jakarta.servlet.jsp.jstl-[^/]*\.jar|.*taglibs-standard-impl-.*\.jar`. +This is a pattern that is applied to the names of the jars on the container's classpath (ie the classpath of the plugin, not that of the webapp) that should be scanned for fragments, tlds, annotations etc. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-container-include-jar-pattern[here]. +You can define extra patterns of jars that will be included in the scan. +webInfIncludeJarPattern::: +Defaults to matching _all_ of the dependency jars for the webapp (ie the equivalent of WEB-INF/lib). +You can make this pattern more restrictive to only match certain jars by using this setter. +This is analogous to the context attribute xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern] that is documented xref:operations-guide:annotations/index.adoc#og-web-inf-include-jar-pattern[here]. +tempDirectory::: +The path to a dir that Jetty can use to expand or copy jars and jsp compiles when your webapp is running. +The default is `${project.build.outputDirectory}/tmp`. +contextXml::: +The path to a context xml file that is applied to your webapp AFTER the `webApp` element. + + +You can also generate the origin of each element into the effective web.xml file. +The origin is either a descriptor eg web.xml,web-fragment.xml,override-web.xml file, or an annotation eg @WebServlet. +Some examples of elements with origin attribute information are: + +[,xml] +---- + + + + +---- + +To generate origin information, use the following configuration parameters on the `webApp` element: + +originAttribute:: +The name of the attribute that will contain the origin. +By default it is `origin`. +generateOrigin:: +False by default. If true, will force the generation of the `originAttribute` onto each element. + + +[[using-overlaid-wars]] +== Using Overlaid wars + +If your webapp depends on other war files, the <> and <> goals are able to merge resources from all of them. +It can do so based on the settings of the http://maven.apache.org/plugins/maven-war-plugin/[maven-war-plugin], or if your project does not use the http://maven.apache.org/plugins/maven-war-plugin/[maven-war-plugin] to handle the overlays, it can fall back to a simple algorithm to determine the ordering of resources. + +=== With maven-war-plugin + +The `maven-war-plugin` has a rich set of capabilities for merging resources. +The `jetty:run` and `jetty:start` goals are able to interpret most of them and apply them during execution of your unassembled webapp. +This is probably best seen by looking at a concrete example. + +Suppose your webapp depends on the following wars: + +[,xml] +---- + + com.acme + X + war + + + com.acme + Y + war + +---- + +Containing: + +---- +WebAppX: + + /foo.jsp + /bar.jsp + /WEB-INF/web.xml + +WebAppY: + + /bar.jsp + /baz.jsp + /WEB-INF/web.xml + /WEB-INF/special.xml +---- + +They are configured for the http://maven.apache.org/plugins/maven-war-plugin/overlays.html[maven-war-plugin]: + +[,xml,subs=attributes+] +---- + + org.apache.maven.plugins + maven-war-plugin + {version} + + + + com.acme + X + + bar.jsp + + + + com.acme + Y + + baz.jsp + + + + + + + +---- + +Then executing jetty:run would yield the following ordering of resources: `com.acme.X.war : com.acme.Y.war: ${project.basedir}/src/main/webapp`. +Note that the current project's resources are placed last in the ordering due to the empty element in the `maven-war-plugin`. +You can either use that, or specify the `false` parameter to the `jetty-{ee-current}-maven-plugin`. + +Moreover, due to the `exclusions` specified above, a request for the resource ` bar.jsp` would only be satisfied from `com.acme.Y.war.` +Similarly as `baz.jsp` is excluded, a request for it would result in a 404 error. + +=== Without maven-war-plugin + +The algorithm is fairly simple, is based on the ordering of declaration of the dependent wars, and does not support exclusions. +The configuration parameter `` (see for example <> for more information) can be used to control whether your webapp's resources are placed first or last on the resource path at runtime. + +For example, suppose our webapp depends on these two wars: + +[,xml] +---- + + com.acme + X + war + + + com.acme + Y + war + +---- + +Suppose the webapps contain: + +---- +WebAppX: + + /foo.jsp + /bar.jsp + /WEB-INF/web.xml + +WebAppY: + + /bar.jsp + /baz.jsp + /WEB-INF/web.xml + /WEB-INF/special.xml + +---- + +Then our webapp has available these additional resources: + +---- +/foo.jsp (X) +/bar.jsp (X) +/baz.jsp (Y) +/WEB-INF/web.xml (X) +/WEB-INF/special.xml (Y) +---- + +[[configuring-security-settings]] +== Configuring Security Settings + +You can configure LoginServices in the plugin. +Here's an example of setting up the `HashLoginService` for a webapp: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + {version} + + 10 + + /test + + + + Test Realm + + ${project.basedir}/src/etc/realm.properties + + + + + +---- + +[[using-multiple-webapp-root-directories]] +== Using Multiple Webapp Root Directories + +If you have external resources that you want to incorporate in the execution of a webapp, but which are not assembled into war files, you can't use the overlaid wars method described above, but you can tell Jetty the directories in which these external resources are located. +At runtime, when Jetty receives a request for a resource, it searches all the locations to retrieve the resource. +It's a lot like the overlaid war situation, but without the war. + +Here is a configuration example: + +[,xml] +---- + + + /${build.finalName} + + src/main/webapp + /home/johndoe/path/to/my/other/source + /yet/another/folder + + + +---- + +[[running-more-than-one-webapp]] +== Running More than One Webapp + +=== With jetty:run + +You can use either a `jetty.xml` file to configure extra (pre-compiled) webapps that you want to deploy, or you can use the `` configuration element to do so. +If you want to deploy webapp A, and webapps B and C in the same Jetty instance: + +Putting the configuration in webapp A's `pom.xml`: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + {version} + + 10 + + /test + + + + ${project.basedir}../../B.war + /B + + + ${project.basedir}../../C.war + /C + + + + +---- + +[IMPORTANT] +==== +If the `ContextHandler` you are deploying is a webapp, it is *essential* that you use an `org.eclipse.jetty.{ee-current}.maven.plugin.MavenWebAppContext` instance rather than a standard `org.eclipse.jetty.{ee-current}.webapp.WebAppContext` instance. +Only the former will allow the webapp to function correctly in the Maven environment. +==== + +Alternatively, add a `jetty.xml` file to webapp A. +Copy the `jetty.xml` file from the Jetty distribution, and then add WebAppContexts for the other 2 webapps: + +[,xml] +---- + + + + + /B + ../../B.war + + + + + + + /C + ../../C.war + + + + +---- + +Then configure the location of this `jetty.xml` file into webapp A's Jetty plugin: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + {version} + + 10 + + /test + + src/main/etc/jetty.xml + + +---- + +For either of these solutions, the other webapps must already have been built, and they are not automatically monitored for changes. +You can refer either to the packed WAR file of the pre-built webapps or to their expanded equivalents. + +[[setting-system-properties]] +== Setting System Properties + +You can specify property name/value pairs that Jetty sets as System properties for the execution of the plugin. +This feature is useful to tidy up the command line and save a lot of typing. + +However, *sometimes it is not possible to use this feature to set System properties* - sometimes the software component using the System property is already initialized by the time that maven runs (in which case you will need to provide the System property on the command line), or by the time that Jetty runs. +In the latter case, you can use the http://www.mojohaus.org/[maven properties plugin] to define the system properties instead. Here's an example that configures the logback logging system as the Jetty logger: + +[,xml] +---- + + org.codehaus.mojo + properties-maven-plugin + + + + set-system-properties + + + + + logback.configurationFile + ${project.baseUri}/resources/logback.xml + + + + + + +---- + +[NOTE] +==== +If a System property is already set (for example, from the command line or by the JVM itself), then by default these configured properties *DO NOT* override them. +However, they can override system properties set from a file instead, see <>. +==== + +[[specifying-properties-in-pom]] +=== Specifying System Properties in the POM + +Here's an example of how to specify System properties in the POM: + +[,xml] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + + + 222 + + + /test + + + +---- + +[[specifying-properties-in-file]] +=== Specifying System Properties in a File + +You can also specify your System properties in a file. +System properties you specify in this way *do not* override System properties that set on the command line, by the JVM, or directly in the POM via `systemProperties`. + +Suppose we have a file called `mysys.props` which contains the following: + +---- +fooprop=222 +---- + +This can be configured on the plugin like so: + +[,xml] +---- + + org.eclipse.jetty.{ee-current} + jetty-{ee-current}-maven-plugin + + ${project.basedir}/mysys.props + + /test + + + +---- + +You can instead specify the file by setting the System property `jetty.systemPropertiesFile` on the command line. diff --git a/documentation/jetty/modules/programming-guide/pages/migration/11-to-12.adoc b/documentation/jetty/modules/programming-guide/pages/migration/11-to-12.adoc new file mode 100644 index 000000000000..4e27583344b2 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/migration/11-to-12.adoc @@ -0,0 +1,185 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Migrating from Jetty 11.0.x to Jetty 12.0.x + +[[java-version]] +== Required Java Version Changes + +[cols="1,1", options="header"] +|=== +| Jetty 11.0.x | Jetty 12.0.x +| Java 11 | Java 17 +|=== + +[[maven-artifacts]] +== Maven Artifacts Changes + +[cols="1a,1a", options="header"] +|=== +| Jetty 11.0.x | Jetty 12.0.x +| org.eclipse.jetty.fcgi:**fcgi-client** | org.eclipse.jetty.fcgi:**jetty-fcgi-client** +| org.eclipse.jetty.fcgi:**fcgi-server** | org.eclipse.jetty.fcgi:**jetty-fcgi-server** +| org.eclipse.jetty.http2:**http2-client** | org.eclipse.jetty.http2:**jetty-http2-client** +| org.eclipse.jetty.http2:**http2-common** | org.eclipse.jetty.http2:**jetty-http2-common** +| org.eclipse.jetty.http2:**http2-hpack** | org.eclipse.jetty.http2:**jetty-http2-hpack** +| org.eclipse.jetty.http2:**http2-http-client-transport** | org.eclipse.jetty.http2:**jetty-http2-client-transport** +| org.eclipse.jetty.http2:**http2-server** | org.eclipse.jetty.http2:**jetty-http2-server** +| org.eclipse.jetty.http3:**http3-client** | org.eclipse.jetty.http3:**jetty-http3-client** +| org.eclipse.jetty.http3:**http3-common** | org.eclipse.jetty.http3:**jetty-http3-common** +| org.eclipse.jetty.http3:**http3-http-client-transport** | org.eclipse.jetty.http3:**jetty-http3-client-transport** +| org.eclipse.jetty.http3:**http3-qpack** | org.eclipse.jetty.http3:**jetty-http3-qpack** +| org.eclipse.jetty.http3:**http3-server** | org.eclipse.jetty.http3:**jetty-http3-server** +| org.eclipse.jetty:**jetty-osgi.*** | * org.eclipse.jetty:**jetty-osgi** +* org.eclipse.jetty.{ee-all}:**jetty-{ee-all}-osgi-*** +| org.eclipse.jetty:**jetty-proxy** | * org.eclipse.jetty:**jetty-proxy** +* org.eclipse.jetty.{ee-all}:**jetty-{ee-all}-proxy** +| org.eclipse.jetty.quic:**quic-client** | org.eclipse.jetty.quic:**jetty-quic-client** +| org.eclipse.jetty.quic:**quic-common** | org.eclipse.jetty.quic:**jetty-quic-common** +| org.eclipse.jetty.quic:**quic-quiche** | org.eclipse.jetty.quic:**jetty-quic-quiche** +| org.eclipse.jetty.quic:**quic-server** | org.eclipse.jetty.quic:**jetty-quic-server** +| org.eclipse.jetty:**jetty-unixsocket.*** | Removed -- Use org.eclipse.jetty:jetty-unixdomain-server +| org.eclipse.jetty.websocket:**websocket-core-client** | org.eclipse.jetty.websocket:**jetty-websocket-core-client** +| org.eclipse.jetty.websocket:**websocket-core-common** | org.eclipse.jetty.websocket:**jetty-websocket-core-common** +| org.eclipse.jetty.websocket:**websocket-core-server** | org.eclipse.jetty.websocket:**jetty-websocket-core-server** +| org.eclipse.jetty.websocket:**websocket-jetty-api** | org.eclipse.jetty.websocket:**jetty-websocket-jetty-api** +| org.eclipse.jetty.websocket:**websocket-jetty-client** | * org.eclipse.jetty.websocket:**jetty-websocket-jetty-client** +* org.eclipse.jetty.**{ee-all}.websocket**:jetty-**{ee-all}-websocket-jetty-client** +| org.eclipse.jetty.websocket:**websocket-jetty-common** | * org.eclipse.jetty.websocket:**jetty-websocket-jetty-common** +* org.eclipse.jetty.**{ee-all}.websocket**:jetty-**{ee-all}-websocket-jetty-common** +| org.eclipse.jetty.websocket:**websocket-jetty-server** | * org.eclipse.jetty.websocket:**jetty-websocket-jetty-server** +* org.eclipse.jetty.**{ee-all}.websocket**:jetty-**{ee-all}-websocket-jetty-server** +| org.eclipse.jetty.**websocket**:**websocket-jakarta-client** | org.eclipse.jetty.**{ee-all}.websocket**:**jetty-{ee-all}-websocket-jakarta-client** +| org.eclipse.jetty.**websocket**:**websocket-jakarta-common** | org.eclipse.jetty.**{ee-all}.websocket**:**jetty-{ee-all}-websocket-jakarta-common** +| org.eclipse.jetty.**websocket**:**websocket-jakarta-server** | org.eclipse.jetty.**{ee-all}.websocket**:**jetty-{ee-all}-websocket-jakarta-server** +| org.eclipse.jetty.**websocket**:**websocket-servlet** | org.eclipse.jetty.**{ee-all}.websocket**:**jetty-{ee-all}-websocket-servlet** +| org.eclipse.jetty:**apache-jsp** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-apache-jsp** +| org.eclipse.jetty:**jetty-annotations** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-annotations** +| org.eclipse.jetty:**jetty-ant** | Removed -- No Replacement +| org.eclipse.jetty:**jetty-cdi** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-cdi** +| org.eclipse.jetty:**glassfish-jstl** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-glassfish-jstl** +| org.eclipse.jetty:**jetty-jaspi** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-jaspi** +| org.eclipse.jetty:**jetty-jndi** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-jndi** +| org.eclipse.jetty:**jetty-jspc-maven-plugin** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-jspc-maven-plugin** +| org.eclipse.jetty:**jetty-maven-plugin** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-maven-plugin** +| org.eclipse.jetty:**jetty-plus** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-plus** +| org.eclipse.jetty:**jetty-quickstart** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-quickstart** +| org.eclipse.jetty:**jetty-runner** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-runner** +| org.eclipse.jetty:**jetty-servlet** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-servlet** +| org.eclipse.jetty:**jetty-servlets** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-servlets** +| org.eclipse.jetty:**jetty-webapp** | org.eclipse.jetty.**{ee-all}**:**jetty-{ee-all}-webapp** +|=== + +[[class-packages-names]] +== Class Packages/Names Changes + +[cols="1a,1a", options="header"] +|=== +| Jetty 11.0.x | Jetty 12.0.x +| `org.eclipse.jetty.client.**api**.*` | `org.eclipse.jetty.client.*` +| `org.eclipse.jetty.client.**util**.*` | `org.eclipse.jetty.client.*` +| `org.eclipse.jetty.client.**util**.*` | `org.eclipse.jetty.client.*` +| `org.eclipse.jetty.client.**http**.*` | `org.eclipse.jetty.client.**transport**.*` +| `org.eclipse.jetty.http2.client.**http**.*` | `org.eclipse.jetty.http2.client.**transport**.*` + +| `org.eclipse.jetty.websocket.api.annotation.**OnWebSocketConnect**` | `org.eclipse.jetty.websocket.api.annotation.**OnWebSocketOpen**` +| `org.eclipse.jetty.websocket.api.**WriteCallback**` | `org.eclipse.jetty.websocket.api.**Callback**` +| `org.eclipse.jetty.websocket.api.**WebSocket*Listener**` | `org.eclipse.jetty.websocket.api.**Session.Listener.AutoDemanding**` +| `org.eclipse.jetty.websocket.api.**RemoteEndpoint**` | `org.eclipse.jetty.websocket.api.**Session**` +| `org.eclipse.jetty.websocket.api.**WebSocketPolicy**` | `org.eclipse.jetty.websocket.api.**Configurable**` +|=== + +[[servlet-to-handler]] +== Migrate Servlets to Jetty Handlers + +Web applications written using the Servlet APIs may be re-written using the Jetty `Handler` APIs. +The sections below outline the Jetty `Handler` APIs that correspond to the Servlet APIs. +For more information about why using the Jetty `Handler` APIs instead of the Servlet APIs, refer to xref:server/http.adoc[this section]. + +For more information about replacing ``HttpServlet``s or Servlet ``Filter``s with Jetty ``Handler``s, refer to xref:server/http.adoc#handler[this section]. + +=== Handler Request APIs +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=request] +---- + +=== Handler Request Content APIs +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-string] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-buffer] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-stream] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-source] +---- + +=== Handler Response APIs +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=response] +---- + +=== Handler Response Content APIs +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-implicit] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-implicit-status] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-explicit] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-content] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-string] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-echo] + +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-trailers] +---- + +[[api-changes]] +== APIs Changes + +=== `HttpClient` + +The Jetty 11 `Request.onResponseContentDemanded(Response.DemandedContentListener)` API has been replaced by `Request.onResponseContentSource(Response.ContentSourceListener)` in Jetty 12. + +However, also look at `Request.onResponseContentAsync(Response.AsyncContentListener)` and `Request.onResponseContent(Response.ContentListener)` for simpler usages. + +The Jetty 11 model was a "demand+push" model: the application was demanding content; when the content was available, the implementation was pushing content to the application by calling `DemandedContentListener.onContent(Response, LongConsumer, ByteBuffer, Callback)` for every content chunk. + +The Jetty 12 model is a "demand+pull" model: when the content is available, the implementation calls once `Response.ContentSourceListener.onContentSource(Content.Source)`; the application can then pull the content chunks from the `Content.Source`. + +For more information about the new model, see xref:arch/io.adoc#content-source[this section]. + +=== WebSocket + +The Jetty WebSocket APIs have been vastly simplified, and brought in line with the style of other APIs. + +The Jetty 12 WebSocket APIs are now fully asynchronous, so the Jetty 11 `SuspendToken` class has been removed in favor of an explicit (or automatic) demand mechanism in Jetty 12 (for more information, refer to xref:client/websocket.adoc#endpoints-demand[this section]). + +The various Jetty 11 `WebSocket*Listener` interfaces have been replaced by a single interface in Jetty 12, `Session.Listener.AutoDemanding` (for more information, refer to xref:client/websocket.adoc#endpoints-listener[this section]). + +The Jetty 11 `RemoteEndpoint` APIs have been merged into the `Session` APIs in Jetty 12. + +The Jetty 11 `WriteCallback` class has been renamed to just `Callback` in Jetty 12, because it is now also used when receiving binary data. +Note that this `Callback` interface is a different interface from the `org.eclipse.jetty.util.Callback` interface, which cannot be used in the Jetty WebSocket APIs due to class loader visibility issues. + +On the server-side, the Jetty WebSocket APIs have been made independent of the Servlet APIs. + +Jetty 11 `JettyWebSocketServerContainer` has been replaced by `ServerWebSocketContainer` in Jetty 12, with similar APIs (for more information, refer to xref:server/websocket.adoc#jetty[this section]). + +On the client-side the `WebSocketClient` APIs are practically unchanged, as most of the changes come from the `HttpClient` changes described above. diff --git a/documentation/jetty/modules/programming-guide/pages/migration/94-to-10.adoc b/documentation/jetty/modules/programming-guide/pages/migration/94-to-10.adoc new file mode 100644 index 000000000000..7378bfa13f79 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/migration/94-to-10.adoc @@ -0,0 +1,130 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Migrating from Jetty 9.4.x to Jetty 10.0.x + +[[java-version]] +== Required Java Version Changes + +[cols="1,1", options="header"] +|=== +| Jetty 9.4.x | Jetty 10.0.x +| Java 8 | Java 11 +|=== + +[[websocket]] +== WebSocket Migration Guide + +Migrating from Jetty 9.4.x to Jetty 10.0.x requires changes in the coordinates of the Maven artifact dependencies for WebSocket. Some of these classes have also changed name and package. This is not a comprehensive list of changes but should cover the most common changes encountered during migration. + +[[websocket-maven-artifact-changes]] +=== Maven Artifacts Changes + +[cols="1a,1a", options="header"] +|=== +| Jetty 9.4.x | Jetty 10.0.x + +| `org.eclipse.jetty.websocket:**websocket-api**` +| `org.eclipse.jetty.websocket:**websocket-jetty-api**` + +| `org.eclipse.jetty.websocket:**websocket-server**` +| `org.eclipse.jetty.websocket:**websocket-jetty-server**` + +| `org.eclipse.jetty.websocket:**websocket-client**` +| `org.eclipse.jetty.websocket:**websocket-jetty-client**` + +| `org.eclipse.jetty.websocket:**javax-websocket-server-impl**` +| `org.eclipse.jetty.websocket:**websocket-javax-server**` + +| `org.eclipse.jetty.websocket:**javax-websocket-client-impl**` +| `org.eclipse.jetty.websocket:**websocket-javax-client**` + +|=== + +[[websocket-class-name-changes]] +=== Class Names Changes + +[cols="1a,1a", options="header"] +|=== +| Jetty 9.4.x | Jetty 10.0.x + +| `org.eclipse.jetty.websocket.**server.NativeWebSocketServletContainerInitializer**` +| `org.eclipse.jetty.websocket.**server.config.JettyWebSocketServletContainerInitializer**` + +| `org.eclipse.jetty.websocket.**jsr356.server.deploy.WebSocketServerContainerInitializer**` +| `org.eclipse.jetty.websocket.**javax.server.config.JavaxWebSocketServletContainerInitializer**` + +| `org.eclipse.jetty.websocket.**servlet.WebSocketCreator**` +| `org.eclipse.jetty.websocket.**server.JettyWebSocketCreator**` + +| `org.eclipse.jetty.websocket.**servlet.ServletUpgradeRequest**` +| `org.eclipse.jetty.websocket.**server.JettyServerUpgradeRequest**` + +| `org.eclipse.jetty.websocket.**servlet.ServletUpgradeResponse**` +| `org.eclipse.jetty.websocket.**server.JettyServerUpgradeResponse**` + +| `org.eclipse.jetty.websocket.**servlet.WebSocketServlet**` +| `org.eclipse.jetty.websocket.**server.JettyWebSocketServlet**` + +| `org.eclipse.jetty.websocket.**servlet.WebSocketServletFactory**` +| `org.eclipse.jetty.websocket.**server.JettyWebSocketServletFactory**` +|=== + +[[websocket-example-code]] +=== Example Code + +[cols="1a,1a", options="header"] +|=== +| Jetty 9.4.x +| Jetty 10.0.x + +| +[,java] +---- +public class ExampleWebSocketServlet extends WebSocketServlet +{ + @Override + public void configure(WebSocketServletFactory factory) + { + factory.setCreator(new WebSocketCreator() + { + @Override + public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) + { + return new ExampleEndpoint(); + } + }); + } +} +---- + +| +[,java] +---- +public class ExampleWebSocketServlet extends JettyWebSocketServlet +{ + @Override + public void configure(JettyWebSocketServletFactory factory) + { + factory.setCreator(new JettyWebSocketCreator() + { + @Override + public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) + { + return new ExampleEndpoint(); + } + }); + } +} +---- +|=== diff --git a/documentation/jetty/modules/programming-guide/pages/server/compliance.adoc b/documentation/jetty/modules/programming-guide/pages/server/compliance.adoc new file mode 100644 index 000000000000..6707b836b86b --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/compliance.adoc @@ -0,0 +1,121 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Server Compliance Modes + +The Jetty server strives to keep up with the latest https://en.wikipedia.org/wiki/Request_for_Comments[IETF RFC]s for compliance with internet specifications, which are periodically updated. + +When possible, Jetty will support backwards compatibility by providing compliance modes that can be configured to allow violations of the current specifications that may have been allowed in obsoleted specifications. + +There are compliance modes provided for: + +* <> +* <> +* <> + +Compliance modes can be configured to allow violations from the RFC requirements, or in some cases to allow additional behaviors that Jetty has implemented in excess of the RFC (for example, to allow <>). + +For example, the HTTP RFCs require that request HTTP methods are https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1[case sensitive], however Jetty can allow case-insensitive HTTP methods by including the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.Violation.html#CASE_INSENSITIVE_METHOD[`HttpCompliance.Violation.CASE_INSENSITIVE_METHOD`] in the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] set of allowed violations. + +[[http]] +== HTTP Compliance Modes + +In 1995, when Jetty was first implemented, there were no RFC specification of HTTP, only a W3C specification for https://www.w3.org/Protocols/HTTP/AsImplemented.html[HTTP/0.9], which has since been obsoleted or augmented by: + + * https://datatracker.ietf.org/doc/html/rfc1945[RFC 1945] for HTTP/1.0 in 1996 + * https://datatracker.ietf.org/doc/html/rfc2068[RFC 2068] for HTTP/1.1 in 1997 + * https://datatracker.ietf.org/doc/html/rfc2616[RFC 2616] for HTTP/1.1 bis in 1999 + * https://datatracker.ietf.org/doc/html/rfc7230[RFC 7230], https://datatracker.ietf.org/doc/html/rfc7231[RFC 7231], https://datatracker.ietf.org/doc/html/rfc7232[RFC 7232], https://datatracker.ietf.org/doc/html/rfc7233[RFC 7233], https://datatracker.ietf.org/doc/html/rfc7234[RFC 7234], https://datatracker.ietf.org/doc/html/rfc7235[RFC 7235] again for HTTP/1.1 in 2014 + * https://datatracker.ietf.org/doc/html/rfc7540[RFC 7540] for HTTP/2.0 in 2015 + +In addition to these evolving requirements, some earlier version of Jetty did not completely or strictly implement the RFC at the time (for example, case-insensitive HTTP methods). +Therefore, upgrading to a newer Jetty version may cause runtime behavior differences that may break your applications. + +The link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.Violation.html[`HttpCompliance.Violation`] enumeration defines the RFC requirements that may be optionally enforced by Jetty, to support legacy deployments. These possible violations are grouped into modes by the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html[`HttpCompliance`] class, which also defines several named modes that support common deployed sets of violations (with the default being link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html#RFC7230[`HttpCompliance.RFC7230`]). + +For example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=httpCompliance] +---- + +If you want to customize the violations that you want to allow, you can create your own mode using the link:{javadoc-url}/org/eclipse/jetty/http/HttpCompliance.html#from(java.lang.String)[`HttpCompliance.from(String)`] method: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=httpComplianceCustom] +---- + +[[uri]] +== URI Compliance Modes + +Universal Resource Locators (URLs) where initially formalized in 1994 in https://datatracker.ietf.org/doc/html/rfc1738[RFC 1738] and then refined in 1995 with relative URLs by https://datatracker.ietf.org/doc/html/rfc1808[RFC 1808]. + +In 1998, URLs were generalized to Universal Resource Identifiers (URIs) by https://datatracker.ietf.org/doc/html/rfc2396[RFC 2396], which also introduced features such a https://datatracker.ietf.org/doc/html/rfc2396#section-3.3[path parameter]s. + +This was then obsoleted in 2005 by https://datatracker.ietf.org/doc/html/rfc3986[RFC 3986] which removed the definition for path parameters. + +Unfortunately by this stage the existence and use of such parameters had already been codified in the Servlet specification. +For example, the relative URI `/foo/bar;JSESSIONID=a8b38cd02b1c` would define the path parameter `JSESSIONID` for the path segment `bar`, but the most recent RFC does not specify a formal definition of what this relative URI actually means. + +The current situation is that there may be URIs that are entirely valid for https://datatracker.ietf.org/doc/html/rfc3986[RFC 3986], but are ambiguous when handled by the Servlet APIs: + +* A URI with `..` _and_ path parameters such as `/some/..;/path` is not https://datatracker.ietf.org/doc/html/rfc3986#section-5.2[_resolved_] by RFC 3986, since the resolution process only applies to the exact segment `..`, not to `..;`. +However, once the path parameters are removed by the Servlet APIs, the resulting `/some/../path` can easily be resolved to `/path`, rather than be treated as a path that has `..;` as a segment. +* A URI such as `/some/%2e%2e/path` is not resolved by RFC 3986, yet when URL-decoded by the Servlet APIs will result in `/some/../path` which can easily be resolved to `/path`, rather than be treated as a path that has `..` as a segment. +* A URI with empty segments like `/some//../path` may be correctly resolved to `/some/path` (the `..` removes the previous empty segment) by the Servlet APIs. +However, if the URI raw path is passed to some other APIs (for example, file system APIs) it can be interpreted as `/path` because the empty segment `//` is discarded and treated as `/`, and the `..` thus removes the `/some` segment. + +In order to avoid ambiguous URIs, Jetty imposes additional URI requirements in excess of what is required by https://datatracker.ietf.org/doc/html/rfc3986[RFC 3986] compliance. + +These additional requirements may optionally be violated and are defined by the link:{javadoc-url}/org/eclipse/jetty/http/UriCompliance.Violation.html[`UriCompliance.Violation`] enumeration. + +These violations are then grouped into modes by the link:{javadoc-url}/org/eclipse/jetty/http/UriCompliance.html[`UriCompliance`] class, which also defines several named modes that support common deployed sets of violations, with the default being link:{javadoc-url}/org/eclipse/jetty/http/UriCompliance.html#DEFAULT[`UriCompliance.DEFAULT`]. + +For example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=uriCompliance] +---- + +If you want to customize the violations that you want to allow, you can create your own mode using the link:{javadoc-url}/org/eclipse/jetty/http/UriCompliance.html#from(java.lang.String)[`UriCompliance.from(String)`] method: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=uriComplianceCustom] +---- + +[[cookie]] +== Cookie Compliance Modes + +The standards for Cookies have varied greatly over time from a non-specified but de-facto standard (implemented by the first browsers), through https://tools.ietf.org/html/rfc2965[RFC 2965] and currently to https://tools.ietf.org/html/rfc6265[RFC 6265]. + +The link:{javadoc-url}/org/eclipse/jetty/http/CookieCompliance.Violation.html[CookieCompliance.Violation] enumeration defines the RFC requirements that may be optionally enforced by Jetty when parsing the `Cookie` HTTP header in requests and when generating the `Set-Cookie` HTTP header in responses. + +These violations are then grouped into modes by the link:{javadoc-url}/org/eclipse/jetty/http/CookieCompliance.html[`CookieCompliance`] class, which also defines several named modes that support common deployed sets of violations, with the default being link:{javadoc-url}/org/eclipse/jetty/http/CookieCompliance.html#RFC6265[`CookieCompliance.RFC6265`]. + +For example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=cookieCompliance] +---- + +If you want to customize the violations that you want to allow, you can create your own mode using the link:{javadoc-url}/org/eclipse/jetty/http/CookieCompliance.html#from(java.lang.String)[`CookieCompliance.from(String)`] method: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=cookieComplianceCustom] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/server/fastcgi.adoc b/documentation/jetty/modules/programming-guide/pages/server/fastcgi.adoc new file mode 100644 index 000000000000..205252c32f45 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/fastcgi.adoc @@ -0,0 +1,16 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += FastCGI Server Libraries + +TODO diff --git a/documentation/jetty/modules/programming-guide/pages/server/http.adoc b/documentation/jetty/modules/programming-guide/pages/server/http.adoc new file mode 100644 index 000000000000..840a2d76b199 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/http.adoc @@ -0,0 +1,1694 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += HTTP Server Libraries + +Web application development typically involves writing your web applications, packaging them into a web application archive, the `+*.war+` file, and then deploy the `+*.war+` file into a standalone Servlet Container that you have previously installed. + +The Jetty server libraries allow you to write web applications components using either the Jetty APIs (by writing <>) or using the standard <> (by writing ``Servlet``s and Servlet ``Filter``s). +These components can then be programmatically assembled together, without the need of creating a `+*.war+` file, added to a Jetty ``Server`` instance that is then started. +This result in your web applications to be available to HTTP clients as if you deployed your `+*.war+` files in a standalone Jetty server. + +Jetty `Handler` APIs pros: + +* Simple minimalist asynchronous APIs. +* Very low overhead, only configure the features you use. +* Faster turnaround to implement new APIs or new standards. +* Normal classloading behavior (web application classloading isolation also available). + +Servlet APIs pros: + +* Standard, well known, APIs. + +The Maven artifact coordinates are: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty + jetty-server + {version} + +---- + +An `org.eclipse.jetty.server.Server` instance is the central component that links together a collection of ``Connector``s and a collection of ``Handler``s, with threads from a `ThreadPool` doing the work. + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam padding 5 + +scale 1.5 + +hide members +hide circle + +Server - ThreadPool +Connectors - Server +Server -- Handlers +---- + +The components that accept connections from clients are `org.eclipse.jetty.server.Connector` implementations. + +When a Jetty server interprets the HTTP protocol (HTTP/1.1, HTTP/2 or HTTP/3), it uses `org.eclipse.jetty.server.Handler` instances to process incoming requests and eventually produce responses. + +A `Server` must be created, configured and started: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=simple] +---- + +The example above shows the simplest HTTP/1.1 server; it has no support for HTTP sessions, nor for HTTP authentication, nor for any of the features required by the Servlet specification. + +These features (HTTP session support, HTTP authentication support, etc.) are provided by the Jetty server libraries, but not all of them may be necessary in your web application. +You need to put together the required Jetty components to provide the features required by your web applications. +The advantage is that you do not pay the cost for features that you do not use, saving resources and likely increasing performance. + +The built-in ``Handler``s provided by the Jetty server libraries allow you to write web applications that have functionalities similar to Apache HTTPD or Nginx (for example: URL redirection, URL rewriting, serving static content, reverse proxying, etc.), as well as generating content dynamically by processing incoming requests. +Read <> for further details about ``Handler``s. + +If you are interested in writing your web application based on the Servlet APIs, jump to <>. + +[[request-processing]] +== Request Processing + +The Jetty HTTP request processing is outlined below in the diagram below. +You may want to refer to the xref:arch/io.adoc[Jetty I/O architecture] for additional information about the classes mentioned below. + +Request handing is slightly different for each protocol; in HTTP/2 Jetty takes into account multiplexing, something that is not present in HTTP/1.1. + +However, the diagram below captures the essence of request handling that is common among all protocols that carry HTTP requests. + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant ManagedSelector +participant EndPoint +participant Connection +participant Parser +participant HttpStream +participant HttpChannel +participant Server +participant Handlers + +ManagedSelector -> EndPoint : read ready +EndPoint -> Connection : onFillable() +Connection -> EndPoint : fill() +EndPoint --> Connection +Connection -> Parser : parse() +Parser -> HttpStream : events +Connection -> HttpChannel : onRequest() +HttpChannel -> Server : handle() +Server -> Handlers : handle() +---- + +First, the Jetty I/O layer emits an event that a socket has data to read. +This event is converted to a call to `AbstractConnection.onFillable()`, where the `Connection` first reads from the `EndPoint` into a `ByteBuffer`, and then calls a protocol specific parser to parse the bytes in the `ByteBuffer`. + +The parser emit events that are protocol specific; the HTTP/2 parser, for example, emits events for each HTTP/2 frame that has been parsed, and similarly does the HTTP/3 parser. +The parser events are then converted to protocol independent events such as _"request start"_, _"request headers"_, _"request content chunk"_, etc. detailed in <>. + +When enough of the HTTP request is arrived, the `Connection` calls `HttpChannel.onRequest()`. + +`HttpChannel.onRequest()` calls the <>, that allow to customize the request and/or the response headers on a per-``Connector`` basis. + +After request customization, if any, the `Handler` chain is invoked, starting from the `Server` instance, and eventually your web application code is invoked. + +[[request-processing-events]] +=== Request Processing Events + +Advanced web applications may be interested in the progress of the processing of an HTTP request/response. +A typical case is to know exactly _when_ the HTTP request/response processing starts and when it is complete, for example to monitor processing times. + +This is conveniently implemented by `org.eclipse.jetty.server.handler.EventsHandler`, described in more details in <>. + +[[request-logging]] +== Request Logging + +HTTP requests and responses can be logged to provide data that can be later analyzed with other tools. +These tools can provide information such as the most frequently accessed request URIs, the response status codes, the request/response content lengths, geographical information about the clients, etc. + +The default request/response log line format is the https://en.wikipedia.org/wiki/Common_Log_Format[NCSA Format] extended with referrer data and user-agent data. + +[NOTE] +==== +Typically, the extended NCSA format is the is enough and it's the standard used and understood by most log parsing tools and monitoring tools. + +To customize the request/response log line format see the link:{javadoc-url}/org/eclipse/jetty/server/CustomRequestLog.html[`CustomRequestLog` javadocs]. +==== + +Request logging can be enabled at the `Server` level. + +The request logging output can be directed to an SLF4J logger named `"org.eclipse.jetty.server.RequestLog"` at `INFO` level, and therefore to any logging library implementation of your choice (see also xref:troubleshooting/logging.adoc[this section] about logging). + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=serverRequestLogSLF4J] +---- + +Alternatively, the request logging output can be directed to a daily rolling file of your choice, and the file name must contain `yyyy_MM_dd` so that rolled over files retain their date: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=serverRequestLogFile] +---- + +For maximum flexibility, you can log to multiple ``RequestLog``s using class `RequestLog.Collection`, for example by logging with different formats or to different outputs. + +You can use `CustomRequestLog` with a custom `RequestLog.Writer` to direct the request logging output to your custom targets (for example, an RDBMS). +You can implement your own `RequestLog` if you want to have functionalities that are not implemented by `CustomRequestLog`. + +[[request-customizers]] +== Request Customizers + +A request customizer is an instance of `HttpConfiguration.Customizer`, that can customize the HTTP request and/or the HTTP response headers _before_ the `Handler` chain is invoked. + +Request customizers are added to a particular `HttpConfiguration` instance, and therefore are specific to a `Connector` instance: you can have two different ``Connector``s configured with different request customizers. + +For example, it is common to configure a secure `Connector` with the `SecureRequestCustomizer` that customizes the HTTP request by adding attributes that expose TLS data associated with the secure communication. + +A request customizer may: + +* Inspect the received HTTP request method, URI, version and headers. +* Wrap the `Request` object to allow any method to be overridden and customized. Typically this is done to synthesize additional HTTP request headers, or to change the return value of overridden methods. +* Add or modify the HTTP response headers. + +The out-of-the-box request customizers include: + +* `ForwardedRequestCustomizer` -- to interpret the `Forwarded` (or the the obsolete ``+X-Forwarded-*+``) HTTP header added by a reverse proxy; see <>. +* `HostHeaderCustomizer` -- to customize, or synthesize it when original absent, the HTTP `Host` header; see <>. +* `ProxyCustomizer` -- to expose as `Request` attributes the `ip:port` information carried by the PROXY protocol; see <>. +* `RewriteCustomizer` -- to rewrite the request URI; see <>. +* `SecureRequestCustomizer` -- to expose TLS data via `Request` attributes; see <>. + +You can also write your own request customizers and add them to the `HttpConfiguration` instance along existing request customizers. +Multiple request customizers will be invoked in the order they have been added. + +Below you can find an example of how to add a request customizer: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=requestCustomizer] +---- + +[[request-customizer-forwarded]] +=== `ForwardedRequestCustomizer` + +`ForwardedRequestCustomizer` should be added when Jetty receives requests from a reverse proxy on behalf of a remote client, and web applications need to access the remote client information. + +The reverse proxy adds the `Forwarded` (or the obsolete ``+X-Forwarded-*+``) HTTP header to the request, and may offload TLS so that the request arrives in clear-text to Jetty. + +Applications deployed in Jetty may need to access information related to the remote client, for example the remote IP address and port, or whether the request was sent through a secure communication channel. +However, the request is forwarded by the reverse proxy, so the direct information about the remote IP address is that of the proxy, not of the remote client. +Furthermore, the proxy may offload TLS and forward the request in clear-text, so that the URI scheme would be `http` as forwarded by the reverse proxy, not `https` as sent by the remote client. + +`ForwardedRequestCustomizer` reads the `Forwarded` header where the reverse proxy saved the remote client information, and wraps the original `Request` so that applications will transparently see the remote client information when calling methods such as `Request.isSecure()`, or `Request.getConnectionMetaData().getRemoteSocketAddress()`, etc. + +For more information about how to configure `ForwardedRequestCustomizer`, see also link:{javadoc-url}/org/eclipse/jetty/server/ForwardedRequestCustomizer.html[the javadocs]. + +[[request-customizer-host]] +=== `HostHeaderCustomizer` + +`HostHeaderCustomizer` should be added when Jetty receives requests that may lack the `Host` HTTP header, such as HTTP/1.0, HTTP/2 or HTTP/3 requests, and web applications have logic that depends on the value of the `Host` HTTP header. + +For HTTP/2 and HTTP/3, the `Host` HTTP header is missing because the authority information is carried by the `:authority` pseudo-header, as per the respective specifications. + +`HostHeaderCustomizer` will look at the `:authority` pseudo-header, then wrap the original `Request` adding a `Host` HTTP header synthesized from the `:authority` pseudo-header. +In this way, web applications that rely on the presence of the `Host` HTTP header will work seamlessly in any HTTP protocol version. + +`HostHeaderCustomizer` works also for the WebSocket protocol. + +WebSocket over HTTP/2 or over HTTP/3 initiate the WebSocket communication with an HTTP request that only has the `:authority` pseudo-header. +`HostHeaderCustomizer` synthesizes the `Host` HTTP header for such requests, so that WebSocket web applications that inspect the initial HTTP request before the WebSocket communication will work seamlessly in any HTTP protocol version. + +For more information about how to configure `HostHeaderCustomizer`, see also link:{javadoc-url}/org/eclipse/jetty/server/HostHeaderCustomizer.html[the javadocs]. + +[[request-customizer-proxy]] +=== `ProxyCustomizer` + +`ProxyCustomizer` should be added when Jetty receives requests from a reverse proxy on behalf of a remote client, prefixed by the PROXY protocol (see also this section about the <>). + +`ProxyCustomizer` adds the reverse proxy IP address and port as `Request` attributes. +Web applications may use these attributes in conjunction with the data exposed by `ForwardedRequestCustomizer` (see <>). + +For more information about how to configure `ProxyCustomizer`, see also link:{javadoc-url}/org/eclipse/jetty/server/ProxyCustomizer.html[the javadocs]. + +[[request-customizer-rewrite]] +=== `RewriteCustomizer` + +`RewriteCustomizer` is similar to `RewriteHandler` (see <>), but a `RewriteCustomizer` cannot send a response or otherwise complete the request/response processing. + +A `RewriteCustomizer` is mostly useful if you want to rewrite the request URI _before_ the `Handler` chain is invoked. +However, a very similar effect can be achieved by having the `RewriteHandler` as the first `Handler` (the child `Handler` of the `Server` instance). + +Since `RewriteCustomizer` cannot send a response or complete the request/response processing, ``Rule``s that do so such as redirect rules have no effect and are ignored; only ``Rule``s that modify or wrap the `Request` will have effect and be applied. + +Due to this limitation, it is often a better choice to use `RewriteHandler` instead of `RewriteCustomizer`. + +For more information about how to configure `RewriteCustomizer`, see also link:{javadoc-url}/org/eclipse/jetty/rewrite/RewriteCustomizer.html[the javadocs]. + +[[request-customizer-secure]] +=== `SecureRequestCustomizer` + +`SecureRequestCustomizer` should be added when Jetty receives requests over a secure `Connector`. + +`SecureRequestCustomizer` adds TLS information as request attributes, in particular an instance of `EndPoint.SslSessionData` that contains information about the negotiated TLS cipher suite and possibly client certificates, and an instance of `org.eclipse.jetty.util.ssl.X509` that contains information about the server certificate. + +`SecureRequestCustomizer` also adds, if configured so, the `Strict-Transport-Security` HTTP response header (for more information about this header, see https://datatracker.ietf.org/doc/html/rfc6797[its specification]). + +For more information about how to configure `SecureRequestCustomizer`, see also link:{javadoc-url}/org/eclipse/jetty/server/SecureRequestCustomizer.html[the javadocs]. + +[[connector]] +== Server Connectors + +A `Connector` is the component that handles incoming requests from clients, and works in conjunction with `ConnectionFactory` instances. + +The available implementations are: + +* `org.eclipse.jetty.server.ServerConnector`, for TCP/IP sockets. +* `org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector` for Unix-Domain sockets (requires Java 16 or later). +* `org.eclipse.jetty.quic.server.QuicServerConnector`, for the low-level QUIC protocol and HTTP/3. +* `org.eclipse.jetty.server.MemoryConnector`, for memory communication between client and server. + +`ServerConnector` and `UnixDomainServerConnector` use a `java.nio.channels.ServerSocketChannel` to listen to a socket address and to accept socket connections. +`QuicServerConnector` uses a `java.nio.channels.DatagramChannel` to listen to incoming UDP packets. +`MemoryConnector` uses memory for the communication between client and server, avoiding the use of sockets. + +Since `ServerConnector` wraps a `ServerSocketChannel`, it can be configured in a similar way, for example the TCP port to listen to, the IP address to bind to, etc.: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnector] +---- + +`UnixDomainServerConnector` also wraps a `ServerSocketChannel` and can be configured with the Unix-Domain path to listen to: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectorUnix] +---- + +[IMPORTANT] +==== +You can use Unix-Domain sockets only when you run your server with Java 16 or later. +==== + +`QuicServerConnector` wraps a `DatagramChannel` and can be configured in a similar way, as shown in the example below. +Since the communication via UDP does not require to "accept" connections like TCP does, the number of <> is set to `0` and there is no API to configure their number. + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectorQuic] +---- + +`MemoryConnector` uses in-process memory, not sockets, for the communication between client and server, that therefore must be in the same process. + +Typical usage of `MemoryConnector` is the following: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=memoryConnector] +---- + +[[connector-acceptors]] +=== Acceptors + +The _acceptors_ are threads (typically only one) that compete to accept TCP socket connections. +The connectors for the QUIC or HTTP/3 protocol, based on UDP, have no acceptors. + +When a TCP connection is accepted, `ServerConnector` wraps the accepted `SocketChannel` and passes it to the xref:arch/io.adoc#selector-manager[`SelectorManager`]. +Therefore, there is a little moment where the acceptor thread is not accepting new connections because it is busy wrapping the just accepted connection to pass it to the `SelectorManager`. +Connections that are ready to be accepted but are not accepted yet are queued in a bounded queue (at the OS level) whose capacity can be configured with the `acceptQueueSize` parameter. + +If your application must withstand a very high rate of connection opening, configuring more than one acceptor thread may be beneficial: when one acceptor thread accepts one connection, another acceptor thread can take over accepting connections. + +[[connector-selectors]] +=== Selectors + +The _selectors_ are components that manage a set of accepted TCP sockets, implemented by xref:arch/io.adoc#selector-manager[`ManagedSelector`]. +For QUIC or HTTP/3, there are no accepted TCP sockets, but only one `DatagramChannel` and therefore there is only one selector. + +Each selector requires one thread and uses the Java NIO mechanism to efficiently handle a set of registered channels. + +As a rule of thumb, a single selector can easily manage up to 1000-5000 TCP sockets, although the number may vary greatly depending on the application. + +For example, web applications for websites tend to use TCP sockets for one or more HTTP requests to retrieve resources and then the TCP socket is idle for most of the time. +In this case a single selector may be able to manage many TCP sockets because chances are that they will be idle most of the time. +On the contrary, web messaging applications or REST applications tend to send many small messages at a very high frequency so that the TCP sockets are rarely idle. +In this case a single selector may be able to manage less TCP sockets because chances are that many of them will be active at the same time, so you may need more than one selector. + +[[connector-multiple]] +=== Multiple Connectors + +It is possible to configure more than one `Connector` per `Server`. +Typical cases are a `ServerConnector` for clear-text HTTP, and another `ServerConnector` for secure HTTP. +Another case could be a publicly exposed `ServerConnector` for secure HTTP, and an internally exposed `UnixDomainServerConnector` or `MemoryConnector` for clear-text HTTP. +Yet another example could be a `ServerConnector` for clear-text HTTP, a `ServerConnector` for secure HTTP/2, and an `QuicServerConnector` for QUIC+HTTP/3. + +For example: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=configureConnectors] +---- + +If you do not specify the port the connector listens to explicitly, the OS will allocate one randomly when the connector starts. + +You may need to use the randomly allocated port to configure other components. +One example is to use the randomly allocated port to configure secure redirects (when redirecting from a URI with the `http` scheme to the `https` scheme). +Another example is to bind both the HTTP/2 connector and the HTTP/3 connector to the same randomly allocated port. +It is possible that the HTTP/2 connector and the HTTP/3 connector share the same port, because one uses TCP, while the other uses UDP. + +For example: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=sameRandomPort] +---- + +[[connector-protocol]] +=== Configuring Protocols + +A server `Connector` can be configured with one or more ``ConnectionFactory``s, and this list of ``ConnectionFactory``s represents the protocols that the `Connector` can understand. +If no `ConnectionFactory` is specified then `HttpConnectionFactory` is implicitly configured. + +For each accepted connection, the server `Connector` asks a `ConnectionFactory` to create a `Connection` object that handles the traffic on that connection, parsing and generating bytes for a specific protocol (see xref:arch/io.adoc[this section] for more details about `Connection` objects). + +TIP: You can listen for `Connection` open and close events as detailed in xref:arch/io.adoc#connection-listener[this section]. + +Secure protocols like secure HTTP/1.1, secure HTTP/2 or HTTP/3 (HTTP/3 is intrinsically secure -- there is no clear-text HTTP/3) require an `SslContextFactory.Server` to be configured with a KeyStore. + +For HTTP/1.1 and HTTP/2, `SslContextFactory.Server` is used in conjunction with `SSLEngine`, which drives the TLS handshake that establishes the secure communication. + +Applications may register a `org.eclipse.jetty.io.ssl.SslHandshakeListener` to be notified of TLS handshakes success or failure, by adding the `SslHandshakeListener` as a bean to the `Connector`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=sslHandshakeListener] +---- + +[[connector-protocol-http11]] +==== Clear-Text HTTP/1.1 + +`HttpConnectionFactory` creates `HttpConnection` objects that parse bytes and generate bytes for the HTTP/1.1 protocol. + +This is how you configure Jetty to support clear-text HTTP/1.1: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=http11] +---- + +[[connector-protocol-http11-tls]] +==== Encrypted HTTP/1.1 (https) + +Supporting encrypted HTTP/1.1 (that is, requests with the `https` scheme) is supported by configuring an `SslContextFactory` that has access to the KeyStore containing the private server key and public server certificate, in this way: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=tlsHttp11] +---- + +You can customize the SSL/TLS provider as explained in <>. + +[[connector-protocol-http2]] +==== Clear-Text HTTP/2 + +It is well known that the HTTP ports are `80` (for clear-text HTTP) and `443` for encrypted HTTP. +By using those ports, a client had _prior knowledge_ that the server would speak, respectively, the HTTP/1.x protocol and the TLS protocol (and, after decryption, the HTTP/1.x protocol). + +HTTP/2 was designed to be a smooth transition from HTTP/1.1 for users and as such the HTTP ports were not changed. +However the HTTP/2 protocol is, on the wire, a binary protocol, completely different from HTTP/1.1. +Therefore, with HTTP/2, clients that connect to port `80` (or to a specific Unix-Domain path) may speak either HTTP/1.1 or HTTP/2, and the server must figure out which version of the HTTP protocol the client is speaking. + +Jetty can support both HTTP/1.1 and HTTP/2 on the same clear-text port by configuring both the HTTP/1.1 and the HTTP/2 ``ConnectionFactory``s: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=http11H2C] +---- + +Note how the ``ConnectionFactory``s passed to `ServerConnector` are in order: first HTTP/1.1, then HTTP/2. +This is necessary to support both protocols on the same port: Jetty will start parsing the incoming bytes as HTTP/1.1, but then realize that they are HTTP/2 bytes and will therefore _upgrade_ from HTTP/1.1 to HTTP/2. + +This configuration is also typical when Jetty is installed in backend servers behind a load balancer that also takes care of offloading TLS. +When Jetty is behind a load balancer, you can always prepend the PROXY protocol as described in <>. + +[[connector-protocol-http2-tls]] +==== Encrypted HTTP/2 + +When using encrypted HTTP/2, the unencrypted protocol is negotiated by client and server using an extension to the TLS protocol called ALPN. + +Jetty supports ALPN and encrypted HTTP/2 with this configuration: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=tlsALPNHTTP] +---- + +Note how the ``ConnectionFactory``s passed to `ServerConnector` are in order: TLS, ALPN, HTTP/2, HTTP/1.1. + +Jetty starts parsing TLS bytes so that it can obtain the ALPN extension. +With the ALPN extension information, Jetty can negotiate a protocol and pick, among the ``ConnectionFactory``s supported by the `ServerConnector`, the `ConnectionFactory` correspondent to the negotiated protocol. + +The fact that the HTTP/2 protocol comes before the HTTP/1.1 protocol indicates that HTTP/2 is the preferred protocol for the server. + +Note also that the default protocol set in the ALPN ``ConnectionFactory``, which is used in case ALPN is not supported by the client, is HTTP/1.1 -- if the client does not support ALPN is probably an old client so HTTP/1.1 is the safest choice. + +You can customize the SSL/TLS provider as explained in <>. + +[[connector-protocol-http3]] +==== HTTP/3 + +The HTTP/3 protocol is layered on top of the https://datatracker.ietf.org/doc/html/rfc9000[QUIC] protocol, which is based on UDP. +This is rather different with respect to HTTP/1 and HTTP/2, that are based on TCP. + +Jetty only implements the HTTP/3 layer in Java; the QUIC implementation is provided by the https://github.com/cloudflare/quiche[Quiche] native library, that Jetty calls via https://github.com/java-native-access/jna[JNA] (and possibly, in the future, via the https://openjdk.org/jeps/454[Foreign APIs]). + +NOTE: Jetty's HTTP/3 support can only be used on the platforms (OS and CPU) supported by the Quiche native library. + +HTTP/3 clients may not know in advance if the server supports QUIC (over UDP), but the server typically supports either HTTP/1 or HTTP/2 (over TCP) on the default HTTP secure port `443`, and advertises the availability HTTP/3 as an https://datatracker.ietf.org/doc/html/rfc7838[_HTTP alternate service_], possibly on a different port and/or on a different host. + +For example, an HTTP/2 response may include the following header: + +[source] +---- +Alt-Svc: h3=":843" +---- + +The presence of this header indicates that protocol `h3` is available on the same host (since no host is defined before the port), but on port `843` (although it may be the same port `443`). +The HTTP/3 client may now initiate a QUIC connection on port `843` and make HTTP/3 requests. + +NOTE: It is nowadays common to use the same port `443` for both HTTP/2 and HTTP/3. This does not cause problems because HTTP/2 listens on the TCP port `443`, while QUIC listens on the UDP port `443`. + +It is therefore common for HTTP/3 clients to initiate connections using the HTTP/2 protocol over TCP, and if the server supports HTTP/3 switch to HTTP/3 as indicated by the server. + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false + +participant client +participant "server:443" as h2server +participant "server:843" as h3server + +client -> h2server : HTTP/2 request +h2server -> client : HTTP/2 response\nAlt-Svc: h3=":843" +client -> h3server : HTTP/3 requests +h3server -> client : HTTP/3 responses +... +---- + +The code necessary to configure HTTP/2 is described in <>. + +To setup HTTP/3, for example on port `843`, you need the following code (some of which could be shared with other connectors such as HTTP/2's): + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=h3] +---- + +[IMPORTANT] +==== +The use of the Quiche native library requires the private key and public certificate present in the KeyStore to be exported as PEM files for Quiche to use them. + +It is therefore mandatory to configure the PEM directory as shown above. + +The PEM directory must also be adequately protected using file system permissions, because it stores the private key PEM file. +You want to grant as few permissions as possible, typically the equivalent of POSIX `rwx` only to the user that runs the Jetty process. Using `/tmp` or any other directory accessible by any user is not a secure choice. +==== + +[[connector-protocol-tls-conscrypt]] +==== Using Conscrypt as SSL/TLS Provider + +If not explicitly configured, the TLS implementation is provided by the JDK you are using at runtime. + +OpenJDK's vendors may replace the default TLS provider with their own, but you can also explicitly configure an alternative TLS provider. + +The standard TLS provider from OpenJDK is implemented in Java (no native code), and its performance is not optimal, both in CPU usage and memory usage. + +A faster alternative, implemented natively, is Google's https://github.com/google/conscrypt/[Conscrypt], which is built on https://boringssl.googlesource.com/boringssl/[BoringSSL], which is Google's fork of https://www.openssl.org/[OpenSSL]. + +CAUTION: As Conscrypt eventually binds to a native library, there is a higher risk that a bug in Conscrypt or in the native library causes a JVM crash, while the Java implementation will not cause a JVM crash. + +To use Conscrypt as TLS provider, you must have the Conscrypt jar and the Jetty dependency `jetty-alpn-conscrypt-server-{version}.jar` in the class-path or module-path. + +Then, you must configure the JDK with the Conscrypt provider, and configure Jetty to use the Conscrypt provider, in this way: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=conscrypt] +---- + +[[connector-protocol-proxy-http11]] +==== Jetty Behind a Load Balancer + +It is often the case that Jetty receives connections from a load balancer configured to distribute the load among many Jetty backend servers. + +From the Jetty point of view, all the connections arrive from the load balancer, rather than the real clients, but is possible to configure the load balancer to forward the real client IP address and IP port to the backend Jetty server using the https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt[PROXY protocol]. + +NOTE: The PROXY protocol is widely supported by load balancers such as http://cbonte.github.io/haproxy-dconv/2.2/configuration.html#5.2-send-proxy[HAProxy] (via its `send-proxy` directive), https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol[Nginx](via its `proxy_protocol on` directive) and others. + +To support this case, Jetty can be configured in this way: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=proxyHTTP] +---- + +Note how the ``ConnectionFactory``s passed to `ServerConnector` are in order: first PROXY, then HTTP/1.1. +Note also how the PROXY `ConnectionFactory` needs to know its _next_ protocol (in this example, HTTP/1.1). + +Each `ConnectionFactory` is asked to create a `Connection` object for each accepted TCP connection; the `Connection` objects will be chained together to handle the bytes, each for its own protocol. +Therefore the `ProxyConnection` will handle the PROXY protocol bytes and `HttpConnection` will handle the HTTP/1.1 bytes producing a request object and response object that will be processed by ``Handler``s. + +The load balancer may be configured to communicate with Jetty backend servers via Unix-Domain sockets (requires Java 16 or later). +For example: + +[,java,indent=0,options=nowrap] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=proxyHTTPUnix] +---- + +Note that the only difference when using Unix-Domain sockets is instantiating `UnixDomainServerConnector` instead of `ServerConnector` and configuring the Unix-Domain path instead of the IP port. + +[[handler]] +== Server Handlers + +An `org.eclipse.jetty.server.Handler` is the component that processes incoming HTTP requests and eventually produces HTTP responses. + +``Handler``s can process the HTTP request themselves, or they can be ``Handler.Container``s that delegate HTTP request processing to one or more contained ``Handler``s. +This allows ``Handler``s to be organized as a tree comprised of: + +* Leaf ``Handler``s that generate a response, complete the `Callback`, and return `true` from the `handle(\...)` method. +* A `Handler.Wrapper` can be used to form a chain of ``Handler``s where request, response or callback objects may be wrapped in the `handle(\...)` method before being passed down the chain. +* A `Handler.Sequence` that contains a sequence of ``Handler``s, with each `Handler` being called in sequence until one returns `true` from its `handle(\...)` method. +* A specialized `Handler.Container` that may use properties of the request (for example, the URI, or a header, etc.) to select from one or more contained ``Handler``s to delegate the HTTP request processing to, for example link:{javadoc-url}/org/eclipse/jetty/server/handler/PathMappingsHandler.html[`PathMappingsHandler`]. + +A `Handler` tree is created by composing ``Handler``s together: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerTree] +---- + +The corresponding `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── GzipHandler + └── Handler.Sequence + ├── App1Handler + └── App2Handler +---- + +You should prefer using existing ``Handler``s provided by the Jetty server libraries for managing web application contexts, security, HTTP sessions and Servlet support. +Refer to <> for more information about how to use the ``Handler``s provided by the Jetty server libraries. + +You should write your own leaf ``Handler``s to implement your web application logic. +Refer to <> for more information about how to write your own ``Handler``s. + +A `Handler` may be declared as non-blocking (by extending `Handler.Abstract.NonBlocking`) or as blocking (by extending `Handler.Abstract`), to allow interaction with the xref:arch/threads.adoc[Jetty threading architecture] for more efficient thread and CPU utilization during the request/response processing. + +Container ``Handler``s typically inherit whether they are blocking or non-blocking from their child or children. + +Furthermore, container ``Handler``s may be declared as dynamic: they allow adding/removing child ``Handler``s after they have been started (see link:{javadoc-url}/org/eclipse/jetty/server/Handler.AbstractContainer.html[Handler.AbstractContainer] for more information). +Dynamic container ``Handler``s are therefore always blocking, as it is not possible to know if a child `Handler` added in the future will be blocking or non-blocking. + +If the `Handler` tree is not dynamic, then it is possible to create a non-blocking `Handler` tree, for example: + +[,screen] +---- +Server +└── RewriteHandler + └── GzipHandler + └── ContextHandler + └── AppHandler extends Handler.Abstract.NonBlocking +---- + +When the `Handler` tree is non-blocking, Jetty may use the xref:arch/threads.adoc#execution-strategy-pc[`Produce-Consume`] mode to invoke the `Handler` tree, therefore avoiding a thread hand-off, and saving the cost of being scheduled on a different CPU with cold caches. + +The `Produce-Consume` mode is equivalent to what other servers call "event loop" or "selector thread loop" architectures. + +This mode has the benefit of reducing OS context switches and CPU cache misses, using fewer threads, and it is overall very efficient. +On the other hand, it requires writing quick, non-blocking code, and partially sequentializes the request/response processing, so that the Nth request in the sequence pays the latency of the processing of the N-1 requests in front of it. + +[IMPORTANT] +==== +If you declare your `Handler` to be non-blocking by extending `Handler.Abstract.NonBlocking`, the code you write in `handle(\...)` (and recursively all the code called from there) must truly be non-blocking, and possibly execute quickly. + +If the code blocks, you risk a server lock-up. +If the code takes a long time to execute, requests from other connections may be delayed. +==== + +[[handler-use]] +=== Jetty Handlers + +Web applications are the unit of deployment in an HTTP server or Servlet container such as Jetty. + +Two different web applications are typically deployed on different __context path__s, where a _context path_ is the initial segment of the URI path. +For example, web application `webappA` that implements a web user interface for an e-commerce site may be deployed to context path `/shop`, while web application `webappB` that implements a REST API for the e-commerce business may be deployed to `/api`. + +A client making a request to URI `/shop/cart` is directed by Jetty to `webappA`, while a request to URI `/api/products` is directed to `webappB`. + +An alternative way to deploy the two web applications of the example above is to use _virtual hosts_. +A _virtual host_ is a subdomain of the primary domain that shares the same IP address with the primary domain. +If the e-commerce business primary domain is `domain.com`, then a virtual host for `webappA` could be `shop.domain.com`, while a virtual host for `webappB` could be `api.domain.com`. + +Web application `webappA` can now be deployed to virtual host `shop.domain.com` and context path `/`, while web application `webappB` can be deployed to virtual host `api.domain.com` and context path `/`. +Both applications have the same context path `/`, but they can be distinguished by the subdomain. + +A client making a request to `+https://shop.domain.com/cart+` is directed by Jetty to `webappA`, while a request to `+https://api.domain.com/products+` is directed to `webappB`. + +Therefore, in general, a web application is deployed to a _context_ which can be seen as the pair `(virtual_host, context_path)`. +In the first case the contexts were `(domain.com, /shop)` and `(domain.com, /api)`, while in the second case the contexts were `(shop.domain.com, /)` and `(api.domain.com, /)`. +Server applications using the Jetty Server Libraries create and configure a _context_ for each web application. +Many __context__s can be deployed together to enrich the web application offering -- for example a catalog context, a shop context, an API context, an administration context, etc. + +Web applications can be written using exclusively the Servlet APIs, since developers know well the Servlet API and because they guarantee better portability across Servlet container implementations, as described in <>. + +On the other hand, web applications can be written using the Jetty APIs, for better performance, or to be able to access to Jetty specific APIs, or to use features such as redirection from HTTP to HTTPS, support for `gzip` content compression, URI rewriting, etc. +The Jetty Server Libraries provides a number of out-of-the-box ``Handler``s that implement the most common functionalities and are described in the next sections. + +[[handler-use-context]] +==== ContextHandler + +`ContextHandler` is a `Handler` that represents a _context_ for a web application. +It is a `Handler.Wrapper` that performs some action before and after delegating to the nested `Handler`. +// TODO: expand on what the ContextHandler does, e.g. ServletContext. + +The simplest use of `ContextHandler` is the following: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=contextHandler] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── ContextHandler /shop + └── ShopHandler +---- + +[[handler-use-context-collection]] +==== ContextHandlerCollection + +Server applications may need to deploy to Jetty more than one web application. + +Recall from the <> that Jetty offers `Handler.Collection` that contains a sequence of child ``Handler``s. +However, this has no knowledge of the concept of _context_ and just iterates through the sequence of ``Handler``s. + +A better choice for multiple web application is `ContextHandlerCollection`, that matches a _context_ from either its _context path_ or _virtual host_, without iterating through the ``Handler``s. + +If `ContextHandlerCollection` does not find a match, it just returns `false` from its `handle(\...)` method. +What happens next depends on the `Handler` tree structure: other ``Handler``s may be invoked after `ContextHandlerCollection`, for example `DefaultHandler` (see <>). +Eventually, if no `Handler` returns `true` from their own `handle(\...)` method, then Jetty returns an HTTP `404` response to the client. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=contextHandlerCollection] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── ContextHandlerCollection + ├── ContextHandler /shop + │ └── ShopHandler + └── ContextHandler /api + └── RESTHandler +---- + +[[handler-use-resource]] +==== ResourceHandler -- Static Content + +Static content such as images or files (HTML, JavaScript, CSS) can be sent by Jetty very efficiently because Jetty can write the content asynchronously, using direct ``ByteBuffer``s to minimize data copy, and using a memory cache for faster access to the data to send. + +Being able to write content asynchronously means that if the network gets congested (for example, the client reads the content very slowly) and the server stalls the send of the requested data, then Jetty will wait to resume the send _without_ blocking a thread to finish the send. + +`ResourceHandler` supports the following features: + +* Welcome files, for example serving `/index.html` for request URI `/` +* Precompressed resources, serving a precompressed `/document.txt.gz` for request URI `/document.txt` +* https://tools.ietf.org/html/rfc7233[Range requests], for requests containing the `Range` header, which allows clients to pause and resume downloads of large files +* Directory listing, serving a HTML page with the file list of the requested directory +* Conditional headers, for requests containing the `If-Match`, `If-None-Match`, `If-Modified-Since`, `If-Unmodified-Since` headers. + +The number of features supported and the efficiency in sending static content are on the same level as those of common front-end servers used to serve static content such as Nginx or Apache. +Therefore, the traditional architecture where Nginx/Apache was the front-end server used only to send static content and Jetty was the back-end server used only to send dynamic content is somehow obsolete as Jetty can perform efficiently both tasks. +This leads to simpler systems (less components to configure and manage) and more performance (no need to proxy dynamic requests from front-end servers to back-end servers). + +NOTE: It is common to use Nginx/Apache as load balancers, or as rewrite/redirect servers. +We typically recommend https://haproxy.org[HAProxy] as load balancer, and Jetty has <> as well. + +This is how you configure a `ResourceHandler` to create a simple file server: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=resourceHandler] +---- + +If you need to serve static resources from multiple directories: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=multipleResourcesHandler] +---- + +If the resource is not found, `ResourceHandler` will not return `true` from the `handle(\...)` method, so what happens next depends on the `Handler` tree structure. +See also <> `DefaultHandler`. + +[[handler-use-gzip]] +==== GzipHandler + +`GzipHandler` provides supports for automatic decompression of compressed request content and automatic compression of response content. + +`GzipHandler` is a `Handler.Wrapper` that inspects the request and, if the request matches the `GzipHandler` configuration, just installs the required components to eventually perform decompression of the request content or compression of the response content. +The decompression/compression is not performed until the web application reads request content or writes response content. + +`GzipHandler` can be configured at the server level in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=serverGzipHandler] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── GzipHandler + └── ContextHandlerCollection + ├── ContextHandler 1 + :── ... + └── ContextHandler N +---- + +However, in less common cases, you can configure `GzipHandler` on a per-context basis, for example because you want to configure `GzipHandler` with different parameters for each context, or because you want only some contexts to have compression support: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=contextGzipHandler] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── ContextHandlerCollection + └── ContextHandlerCollection + ├── GzipHandler + │ └── ContextHandler /shop + │ └── ShopHandler + └── ContextHandler /api + └── RESTHandler +---- + +// TODO: does ServletContextHandler really need a special configuration? + +[[handler-use-rewrite]] +==== RewriteHandler + +`RewriteHandler` provides support for URL rewriting, very similarly to https://httpd.apache.org/docs/current/mod/mod_rewrite.html[Apache's mod_rewrite] or https://nginx.org/en/docs/http/ngx_http_rewrite_module.html[Nginx rewrite module]. + +The Maven artifact coordinates are: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty + jetty-rewrite + {version} + +---- + +`RewriteHandler` can be configured with a set of __rule__s; a _rule_ inspects the request and when it matches it performs some change to the request (for example, changes the URI path, adds/removes headers, etc.). + +The Jetty Server Libraries provide rules for the most common usages, but you can write your own rules by extending the `org.eclipse.jetty.rewrite.handler.Rule` class. + +Please refer to the `jetty-rewrite` module link:{javadoc-url}/org/eclipse/jetty/rewrite/handler/package-summary.html[javadocs] for the complete list of available rules. + +You typically want to configure `RewriteHandler` at the server level, although it is possible to configure it on a per-context basis. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=rewriteHandler] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── RewriteHandler + └── ContextHandlerCollection + ├── ContextHandler 1 + :── ... + └── ContextHandler N +---- + +[[handler-use-sizelimit]] +==== SizeLimitHandler + +`SizeLimitHandler` tracks the sizes of request content and response content, and fails the request processing with an HTTP status code of https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large[`413 Content Too Large`]. + +Server applications can set up the `SizeLimitHandler` before or after handlers that modify the request content or response content such as <>. +When `SizeLimitHandler` is before `GzipHandler` in the `Handler` tree, it will limit the compressed content; when it is after, it will limit the uncompressed content. + +The `Handler` tree structure look like the following, to limit uncompressed content: + +[,screen] +---- +Server +└── GzipHandler + └── SizeLimitHandler + └── ContextHandlerCollection + ├── ContextHandler 1 + :── ... + └── ContextHandler N +---- + +[[handler-use-statistics]] +==== StatisticsHandler + +`StatisticsHandler` gathers and exposes a number of statistic values related to request processing such as: + +* Total number of requests +* Current number of concurrent requests +* Minimum, maximum, average and standard deviation of request processing times +* Number of responses grouped by HTTP code (i.e. how many `2xx` responses, how many `3xx` responses, etc.) +* Total response content bytes + +Server applications can read these values and use them internally, or expose them via some service, or xref:arch/jmx.adoc[export them to JMX]. + +`StatisticsHandler` can be configured at the server level or at the context level. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=statisticsHandler] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── StatisticsHandler + └── ContextHandlerCollection + ├── ContextHandler 1 + :── ... + └── ContextHandler N +---- + +It is possible to act on those statistics by subclassing `StatisticsHandler`. +For instance, `StatisticsHandler.MinimumDataRateHandler` can be used to enforce a minimum read rate and a minimum write rate based of the figures collected by the `StatisticsHandler`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=dataRateHandler] +---- + +[[handler-use-events]] +==== EventsHandler + +`EventsHandler` allows applications to be notified of <> events. + +`EventsHandler` must be subclassed, and the relevant `onXYZ()` methods overridden to capture the request processing events you are interested in. +The request processing events can be used in conjunction with `Request` APIs that provide the information you may be interested in. + +For example, if you want to use `EventsHandler` to record processing times, you can use the request processing events with the following `Request` APIs: + +* `Request.getBeginNanoTime()`, which returns the earliest possible nanoTime the request was received. +* `Request.getHeadersNanoTime()`, which returns the nanoTime at which the parsing of the HTTP headers was completed. + +[CAUTION] +==== +The `Request` and `Response` objects may be inspected during events, but it is recommended to avoid modifying them, for example by adding/removing headers or by reading/writing content, because any modification may interfere with the processing performed by other ``Handler``s. +==== + +`EventsHandler` emits the following events: + +`beforeHandling`:: +Emitted just before `EventsHandler` invokes the `Handler.handle(\...)` method of the next `Handler` in the `Handler` tree. +`afterHandling`:: +Emitted just after the invocation to the `Handler.handle(\...)` method of the next `Handler` in the `Handler` tree returns, either normally or by throwing. +`requestRead`:: +Emitted every time a chunk of content is read from the `Request`. +`responseBegin`:: +Emitted when the response first write happens. +`responseWrite`:: +Emitted every time the write of some response content is initiated. +`responseWriteComplete`:: +Emitted every time the write of some response content is completed, either successfully or with a failure. +`responseTrailersComplete`:: +Emitted when the write of the response trailers is completed. +`complete`:: +Emitted when both request and the response have been completely processed. + +Your `EventsHandler` subclass should then be linked in the `Handler` tree in the relevant position, typically as the outermost `Handler` after `Server`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=eventsHandler] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── MyEventsHandler + └── ContextHandler /app +---- + +You can link the `EventsHandler` at any point in the `Handler` tree structure, and even have multiple ``EventsHandler``s to be notified of the request processing events at the different stages of the `Handler` tree, for example: + +[,screen] +---- +Server +└── TotalEventsHandler + └── SlowHandler + └── AppEventsHandler + └── ContextHandler /app +---- + +In the example above, `TotalEventsHandler` may record the total times of request processing, from `SlowHandler` all the way to the `ContextHandler`. +On the other hand, `AppEventsHandler` may record both the time it takes for the request to flow from `TotalEventsHandler` to `AppEventsHandler`, therefore effectively measuring the processing time due to `SlowHandler`, and the time it takes to process the request by the `ContextHandler`. + +Refer to the `EventsHandler` link:{javadoc-url}/org/eclipse/jetty/server/handler/EventsHandler.html[javadocs] for further information. + +[[handler-use-qos]] +==== QoSHandler + +`QoSHandler` allows web applications to limit the number of concurrent requests, therefore implementing a quality of service (QoS) mechanism for end users. + +Web applications may need to access resources with limited capacity, for example a relational database accessed through a JDBC connection pool. + +Consider the case where each HTTP request results in a JDBC query, and the capacity of the database is of 400 queries/s. +Allowing more than 400 HTTP requests/s into the system, for example 500 requests/s, results in 100 requests blocking waiting for a JDBC connection _for every second_. +It is evident that even a short load spike of few seconds eventually results in consuming all the server threads: some will be processing requests and queries, the remaining will be blocked waiting for a JDBC connection. +When no more threads are available, additional requests will queue up as tasks in the thread pool, consuming more memory and potentially causing a complete server failure. +This situation affects the whole server, so one bad behaving web application may affect other well behaving web applications. +From the end user perspective the quality of service is terrible, because requests will take a lot of time to be served and eventually time out. + +In cases of load spikes, caused for example by popular events (weather or social events), usage bursts (Black Friday sales), or even denial of service attacks, it is desirable to give priority to certain requests rather than others. +For example, in an e-commerce site requests that lead to the checkout and to the payments should have higher priorities than requests to browse the products. +Another example is to prioritize requests for certain users such as paying users or administrative users. + +`QoSHandler` allows you to configure the maximum number of concurrent requests; by extending `QoSHandler` you can prioritize suspended requests for faster processing. + +A simple example that just limits the number of concurrent requests: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=simpleQoSHandler] +---- + +This is an example of a `QoSHandler` subclass where you can implement a custom prioritization logic: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=advancedQoSHandler] +---- + +[[handler-use-secured]] +==== SecuredRedirectHandler -- Redirect from HTTP to HTTPS + +`SecuredRedirectHandler` allows to redirect requests made with the `http` scheme (and therefore to the clear-text port) to the `https` scheme (and therefore to the encrypted port). + +For example a request to `+http://domain.com:8080/path?param=value+` is redirected to `+https://domain.com:8443/path?param=value+`. + +Server applications must configure a `HttpConfiguration` object with the secure scheme and secure port so that `SecuredRedirectHandler` can build the redirect URI. + +`SecuredRedirectHandler` is typically configured at the server level, although it can be configured on a per-context basis. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=securedHandler] +---- + +[[handler-use-cross-origin]] +==== CrossOriginHandler + +`CrossOriginHandler` supports the server-side requirements of the https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[CORS protocol] implemented by browsers when performing cross-origin requests. + +An example of a cross-origin request is when a script downloaded from the origin domain `+http://domain.com+` uses `fetch()` or `XMLHttpRequest` to make a request to a cross domain such as `+http://cross.domain.com+` (a subdomain of the origin domain) or to `+http://example.com+` (a completely different domain). + +This is common, for example, when you embed reusable components such as a chat component into a web page: the web page and the chat component files are downloaded from `+http://domain.com+`, but the chat server is at `+http://chat.domain.com+`, so the chat component must make cross-origin requests to the chat server. + +This kind of setup exposes to https://owasp.org/www-community/attacks/csrf[cross-site request forgery (CSRF) attacks], and the CORS protocol has been established to protect against this kind of attacks. + +For security reasons, browsers by default do not allow cross-origin requests, unless the response from the cross domain contains the right CORS headers. + +`CrossOriginHandler` relieves server-side web applications from handling CORS headers explicitly. +You can set up your `Handler` tree with the `CrossOriginHandler`, configure it, and it will take care of the CORS headers separately from your application, where you can concentrate on the business logic. + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── CrossOriginHandler + └── ContextHandler /app + └── AppHandler +---- + +The most important `CrossOriginHandler` configuration parameter that must be configured is `allowedOrigins`, which by default is the empty set, therefore disallowing all origins. + +You want to restrict requests to your cross domain to only origins you trust. +From the chat example above, the chat server at `+http://chat.domain.com+` knows that the chat component is downloaded from the origin server at `+http://domain.com+`, so the `CrossOriginHandler` is configured in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=crossOriginAllowedOrigins] +---- + +Browsers send cross-origin requests in two ways: + +* Directly, if the cross-origin request meets some simple criteria. +* By issuing a hidden _preflight_ request before the actual cross-origin request, to verify with the server if it is willing to reply properly to the actual cross-origin request. + +Both preflight requests and cross-origin requests will be handled by `CrossOriginHandler`, which will analyze the request and possibly add appropriate CORS response headers. + +By default, preflight requests are not delivered to the `CrossOriginHandler` child `Handler`, but it is possible to configure `CrossOriginHandler` by setting `deliverPreflightRequests=true` so that the web application can fine-tune the CORS response headers. + +Another important `CrossOriginHandler` configuration parameter is `allowCredentials`, which controls whether cookies and authentication headers that match the cross-origin request to the cross domain are sent in the cross-origin requests. +By default, `allowCredentials=false` so that cookies and authentication headers are not sent in cross-origin requests. + +If the application deployed in the cross domain requires cookies or authentication, then you must set `allowCredentials=true`, but you also need to restrict the allowed origins only to the ones your trust, otherwise your cross domain application will be vulnerable to CSRF attacks. + +For more `CrossOriginHandler` configuration options, refer to the link:{javadoc-url}/org/eclipse/jetty/server/handler/CrossOriginHandler.html[`CrossOriginHandler` javadocs]. + +[[handler-use-state-tracking]] +==== StateTrackingHandler + +`StateTrackingHandler` is a xref:troubleshooting/index.adoc[troubleshooting] `Handler` that tracks whether `Handler`/`Request`/`Response` asynchronous APIs are properly used by applications.

      + +Asynchronous APIs are notoriously more difficult to troubleshoot than blocking APIs, and may be subject to restrictions that applications need to respect (a typical case is that they cannot perform blocking operations). + +For example, a `Handler` implementation whose `handle(\...)` method returns `true` _must_ eventually complete the callback passed to `handle(\...)` (for more details on the `Handler` APIs, see <>). + +When an application forgets to complete the callback passed to `handle(\...)`, the HTTP response may not be sent to the client, but it will be difficult to troubleshoot why the client is not receiving responses. + +`StateTrackingHandler` helps with this troubleshooting because it tracks the callback passed to `handle(\...)` and emits an event if the callback is not completed within a configurable timeout: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=stateTrackingHandle] +---- + +By default, events are logged at warning level, but it is possible to specify a listener to be notified of the events tracked by `StateTrackingHandler`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=stateTrackingListener] +---- + +Other events tracked by `StateTrackingHandler` are demand callbacks that block, writes that do not complete their callbacks, or write callbacks that block. +The complete list of events is specified by the `StateTrackingHandler.Listener` class (link:{javadoc-url}/org/eclipse/jetty/server/handler/StateTrackingHandler.Listener.html[javadocs]). + +[[handler-use-default]] +==== DefaultHandler + +`DefaultHandler` is a terminal `Handler` that always returns `true` from its `handle(\...)` method and performs the following: + +* Serves the `favicon.ico` Jetty icon when it is requested +* Sends a HTTP `404` response for any other request +* The HTTP `404` response content nicely shows a HTML table with all the contexts deployed on the `Server` instance + +`DefaultHandler` is set directly on the server, for example: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=defaultHandler] +---- + +The `Handler` tree structure looks like the following: + +[,screen] +---- +Server + ├── ContextHandlerCollection + │ ├── ContextHandler 1 + │ :── ... + │ └── ContextHandler N + └── DefaultHandler +---- + +In the example above, `ContextHandlerCollection` will try to match a request to one of the contexts; if the match fails, `Server` will call the `DefaultHandler` that will return a HTTP `404` with an HTML page showing the existing contexts deployed on the `Server`. + +NOTE: `DefaultHandler` just sends a nicer HTTP `404` response in case of wrong requests from clients. +Jetty will send an HTTP `404` response anyway if `DefaultHandler` has not been set. + +[[handler-use-servlet]] +=== Servlet API Handlers + +[[handler-use-servlet-context]] +==== ServletContextHandler + +``Handler``s are easy to write, but often web applications have already been written using the Servlet APIs, using ``Servlet``s and ``Filter``s. + +`ServletContextHandler` is a `ContextHandler` that provides support for the Servlet APIs and implements the behaviors required by the Servlet specification. + +However, differently from <>, it does not require web application to be packaged as a `+*.war+`, nor it requires a `web.xml` for configuration. + +With `ServletContextHandler` you can just put all your Servlet components in a `+*.jar+` and configure each component using the `ServletContextHandler` APIs, in a way equivalent to what you would write in a `web.xml`. + +The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-all} + jetty-{ee-all}-servlet + {version} + +---- + +For example, for Jakarta {ee-current-caps} the coordinates are: `org.eclipse.jetty.{ee-current}:jetty-{ee-current}-servlet:{version}`. + +Below you can find an example of how to set up a Jakarta {ee-current-caps} `ServletContextHandler`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=servletContextHandler-servlet] +---- +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=servletContextHandler-setup] +---- + +The `Handler` and Servlet components tree structure looks like the following: + +[,screen,subs=+quotes] +---- +Server +└── ServletContextHandler /shop + ├── _ShopCartServlet /cart/*_ + └── _CrossOriginFilter /*_ +---- + +Note how the Servlet components (they are not ``Handler``s) are represented in _italic_. + +Note also how adding a `Servlet` or a `Filter` returns a _holder_ object that can be used to specify additional configuration for that particular `Servlet` or `Filter`, for example initialization parameters (equivalent to `` in `web.xml`). + +When a request arrives to `ServletContextHandler` the request URI will be matched against the ``Filter``s and ``Servlet`` mappings and only those that match will process the request, as dictated by the Servlet specification. + +IMPORTANT: `ServletContextHandler` is a terminal `Handler`, that is it always returns `true` from its `handle(\...)` method when invoked. +Server applications must be careful when creating the `Handler` tree to put ``ServletContextHandler``s as last ``Handler``s in any `Handler.Collection` or as children of a `ContextHandlerCollection`. + +// TODO: revise what above, as ServletContextHandler is not a terminal handler. +// TODO: introduce the fact that ServletContextHandler can have a class loader that may be used to "isolate" web application classes. + +[[handler-use-webapp-context]] +==== WebAppContext + +`WebAppContext` is a `ServletContextHandler` that autoconfigures itself by reading a `web.xml` Servlet configuration file. + +The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-all} + jetty-{ee-all}-webapp + {version} + +---- + +Server applications can specify a `+*.war+` file or a directory with the structure of a `+*.war+` file to `WebAppContext` to deploy a standard Servlet web application packaged as a `war` (as defined by the Servlet specification). + +Where server applications using `ServletContextHandler` must manually invoke methods to add ``Servlet``s and ``Filter``s as described in <>, `WebAppContext` reads `WEB-INF/web.xml` to add ``Servlet``s and ``Filter``s, and also enforces a number of restrictions defined by the Servlet specification, in particular related to class loading. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=webAppContextHandler] +---- + +[[handler-use-webapp-context-class-loading]] +.WebAppContext Class Loading + +The Servlet specification requires that a web application class loader must load the web application classes from `WEB-INF/classes` and `WEB_INF/lib`. +The web application class loader is special because it behaves differently from typical class loaders: where typical class loaders first delegate to their parent class loader and then try to find the class locally, the web application class loader first tries to find the class locally and then delegates to the parent class loader. +The typical class loading model, parent-first, is _inverted_ for web application class loaders, as they use a child-first model. + +Furthermore, the Servlet specification requires that web applications cannot load or otherwise access the Servlet container implementation classes, also called _server classes_. +Web applications receive the HTTP request object as an instance of the `jakarta.servlet.http.HttpServletRequest` interface, and cannot downcast it to the Jetty specific implementation of that interface to access Jetty specific features -- this ensures maximum web application portability across Servlet container implementations. + +Lastly, the Servlet specification requires that other classes, also called _system classes_, such as `jakarta.servlet.http.HttpServletRequest` or JDK classes such as `java.lang.String` or `java.sql.Connection` cannot be modified by web applications by putting, for example, modified versions of those classes in `WEB-INF/classes` so that they are loaded first by the web application class loader (instead of the class-path class loader where they are normally loaded from). + +`WebAppContext` implements this class loader logic using a single class loader, `WebAppClassLoader`, with filtering capabilities: when it loads a class, it checks whether the class is a _system class_ or a _server class_ and acts according to the Servlet specification. + +When `WebAppClassLoader` is asked to load a class, it first tries to find the class locally (since it must use the inverted child-first model); if the class is found, and it is not a _system class_, the class is loaded; otherwise the class is not found locally. +If the class is not found locally, the parent class loader is asked to load the class; the parent class loader uses the standard parent-first model, so it delegates the class loading to its parent, and so on. +If the class is found, and it is not a _server class_, the class is loaded; otherwise the class is not found and a `ClassNotFoundException` is thrown. + +Unfortunately, the Servlet specification does not define exactly which classes are _system classes_ and which classes are _server classes_. +However, Jetty picks good defaults and allows server applications to customize _system classes_ and _server classes_ in `WebAppContext`. + +// TODO: add a section on parentLoaderPriority. +// TODO: add a code example about how to set system/server classes. +// TODO: add a code example about how to configure extra classpath +// TODO: add a section on ClassLoading (see old docs) + +// TODO: add a section on Configuration (system/server classes) +// TODO: add a section about how to setup JSP support + +[[handler-use-default-servlet]] +==== DefaultServlet -- Static Content for Servlets + +If you have a <>, you may want to use a `DefaultServlet` instead of `ResourceHandler`. +The features are similar, but `DefaultServlet` is more commonly used to serve static files for Servlet web applications. + +The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-all} + jetty-{ee-all}-servlet + {version} + +---- + +Below you can find an example of how to setup `DefaultServlet`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=defaultServlet] +---- + +[[handler-impl]] +=== Implementing Handler + +The `Handler` API consist fundamentally of just one method: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerAPI] +---- + +The code that implements the `handle(\...)` method must respect the following contract: + +* It may inspect `Request` immutable information such as URI and headers, typically to decide whether to return `true` or `false` (see below). +* Returning `false` means that the implementation will not handle the request, and it **must not** complete the `callback` parameter, nor read the request content, nor write response content. +* Returning `true` means that the implementation will handle the request, and it **must** eventually complete the `callback` parameter. +The completion of the `callback` parameter may happen synchronously within the invocation to `handle(\...)`, or at a later time, asynchronously, possibly from another thread. +If the response has not been explicitly written when the `callback` has been completed, the Jetty implementation will write a `200` response with no content if the `callback` has been succeeded, or an error response if the `callback` has been failed. + +[CAUTION] +==== +Violating the contract above may result in undefined or unexpected behavior, and possibly leak resources. + +For example, returning `true` from `handle(\...)`, but not completing the `callback` parameter may result in the request or the response never be completed, likely causing the client to time out. + +Similarly, returning `false` from `handle(\...)` but then either writing the response or completing the `callback` parameter will likely result in a garbled response be sent to the client, as the implementation will either invoke another `Handler` (that may write a response) or write a default response. +==== + +Applications may wrap the request, the response, or the callback and forward the wrapped request, response and callback to a child `Handler`. + +[[handler-impl-hello]] +==== Hello World `Handler` + +A simple "Hello World" `Handler` is the following: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerHello] +---- + +Such a simple `Handler` can access the request and response main features, such as <>, or <>. + +Note how `HelloWorldHandler` extends from `Handler.Abstract.NonBlocking`. +This is a declaration that `HelloWorldHandler` does not use blocking APIs (of any kind) to perform its logic, allowing Jetty to apply optimizations (see <>) that are not applied to ``Handler``s that declare themselves as blocking. + +If your `Handler` implementation uses blocking APIs (of any kind), extend from `Handler.Abstract`. + +Note how the `callback` parameter is passed to `Content.Sink.write(\...)` -- a utility method that eventually calls `Response.write(\...)`, so that when the write completes, also the `callback` parameter is completed. +Note also that because the `callback` parameter will eventually be completed, the value returned from `handle(\...)` is `true`. + +In this way the <> is fully respected: when `true` is returned, the `callback` will eventually be completed. + +[[handler-impl-filter]] +==== Filtering `Handler` + +A filtering `Handler` is a handler that perform some modification to the request or response, and then either forwards the request to another `Handler` or produces an error response: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerFilter] +---- + +Note how a filtering `Handler` extends from `Handler.Wrapper` and as such needs another handler to forward the request processing to, and how the two ``Handler``s needs to be linked together to work properly. + +[[handler-impl-request]] +==== Using the `Request` + +The `Request` object can be accessed by web applications to inspect the HTTP request URI, the HTTP request headers and read the HTTP request content. + +Since the `Request` object may be wrapped by <>, the design decision for the `Request` APIs was to keep the number of virtual methods at a minimum. +This minimizes the effort necessary to write `Request` wrapper implementations and provides a single source for the data carried by `Request` objects. + +To use the `Request` APIs, you should look up the relevant methods in the following order: + +1. `Request` virtual methods. +For example, `Request.getMethod()` returns the HTTP method used in the request, such as `GET`, `POST`, etc. +2. `Request` `static` methods. +These are utility methods that provide more convenient access to request features. +For example, the HTTP URI query is a string and can be directly accessed via the non-``static`` method `request.getHttpURI().getQuery()`; however, the query string typically holds key/value parameters and applications should not have the burden to parse the query string, so the `static Request.extractQueryParameters(Request)` method is provided. +3. Super class `static` methods. +Since `Request` _is-a_ `Content.Source`, look also for `static` methods in `Content.Source` that take a `Content.Source` as a parameter, so that you can pass the `Request` object as a parameter. + +Below you can find a list of the most common `Request` features and how to access them. +Refer to the `Request` link:{javadoc-url}/org/eclipse/jetty/server/Request.html[javadocs] for the complete list. + +`Request` URI:: +The `Request` URI is accessed via `Request.getHttpURI()` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpURI.html[`HttpURI`] APIs. + +`Request` HTTP headers:: +The `Request` HTTP headers are accessed via `Request.getHeaders()` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpFields.html[`HttpFields`] APIs. + +`Request` cookies:: +The `Request` cookies are accessed via `static Request.getCookies(Request)` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpCookie.html[`HttpCookie`] APIs. + +`Request` parameters:: +The `Request` parameters are accessed via `static Request.extractQueryParameters(Request)` for those present in the HTTP URI query, and via `static Request.getParametersAsync(Request)` for both query parameters and request content parameters received via form upload with `Content-Type: application/x-www-url-form-encoded`, and the link:{javadoc-url}/org/eclipse/jetty/util/Fields.html[`Fields`] APIs. +If you are only interested in the request content parameters received via form upload, you can use `static FormFields.from(Request)`, see also <>. + +`Request` HTTP session:: +The `Request` HTTP session is accessed via `Request.getSession(boolean)` and the link:{javadoc-url}/org/eclipse/jetty/server/Session.html[`Session`] APIs. +For more information about how to set up support for HTTP sessions, see <>. + +[[handler-impl-request-content]] +==== Reading the `Request` Content + +Since `Request` _is-a_ `Content.Source`, the xref:arch/io.adoc#content-source[section] about reading from `Content.Source` applies to `Request` as well. +The `static Content.Source` utility methods will allow you to read the request content as a string, or as an `InputStream`, for example. + +There are two special cases that are specific to HTTP for the request content: form parameters (sent when submitting an HTML form) and multipart form data (sent when submitting an HTML form with file upload). + +For form parameters, typical of HTML form submissions, you can use the `FormFields` APIs as shown here: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerForm] +---- +<1> If the `Content-Type` is `x-www-form-urlencoded`, read the request content with `FormFields`. +<2> When all the request content has arrived, process the `Fields`. + +[WARNING] +==== +The `Handler` returns `true`, so the `callback` parameter **must** be completed. + +It is therefore mandatory to use `CompletableFuture` APIs that are invoked even when reading the request content failed, such as `whenComplete(BiConsumer)`, `handle(BiFunction)`, `exceptionally(Function)`, etc. + +Failing to do so may result in the `Handler` `callback` parameter to never be completed, causing the request processing to hang forever. +==== + +For multipart form data, typical for HTML form file uploads, you can use the `MultiPartFormData.Parser` APIs as shown here: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=handlerMultiPart] +---- +<1> If the `Content-Type` is `multipart/form-data`, read the request content with `MultiPartFormData.Parser`. +<2> When all the request content has arrived, process the `MultiPartFormData.Parts`. + +[WARNING] +==== +The `Handler` returns `true`, so the `callback` parameter **must** be completed. + +It is therefore mandatory to use `CompletableFuture` APIs that are invoked even when reading the request content failed, such as `whenComplete(BiConsumer)`, `handle(BiFunction)`, `exceptionally(Function)`, etc. + +Failing to do so may result in the `Handler` `callback` parameter to never be completed, causing the request processing to hang forever. +==== + +[[handler-impl-request-listeners]] +==== `Request` Listeners + +Application may add listeners to the `Request` object to be notified of particular events happening during the request/response processing. + +`Request.addIdleTimeoutListener(Predicate)` allows you to add an idle timeout listener, which is invoked when an idle timeout period elapses during the request/response processing, if the idle timeout event is not notified otherwise. + +When an idle timeout event happens, it is delivered to the application as follows: + +* If there is pending demand (via `Request.demand(Runnable)`), then the demand `Runnable` is invoked and the application may see the idle timeout failure by reading from the `Request`, obtaining a xref:arch/io.adoc#content-source[transient failure chunk]. +* If there is a pending response write (via `Response.write(boolean, ByteBuffer, Callback)`), the response write `Callback` is failed. +* If neither of the above, the idle timeout listeners are invoked, in the same order they have been added. +The first idle timeout listener that returns `true` stops the Jetty implementation from invoking the idle timeout listeners that follow. + +The idle timeout listeners are therefore invoked only when the application is really idle, neither trying to read nor trying to write. + +An idle timeout listener may return `true` to indicate that the idle timeout should be treated as a fatal failure of the request/response processing; otherwise the listener may return `false` to indicate that no further handling of the idle timeout is needed from the Jetty implementation. + +When idle timeout listeners return `false`, then any subsequent idle timeouts are handled as above. +In the case that the application does not initiate any read or write, then the idle timeout listeners are invoked again after another idle timeout period. + +`Request.addFailureListener(Consumer)` allows you to add a failure listener, which is invoked when a failure happens during the request/response processing. + +When a failure happens during the request/response processing, then: + +* The pending demand for request content, if any, is invoked; that is, the `Runnable` passed to `Request.demand(Runnable)` is invoked. +* The callback of an outstanding call to `Response.write(boolean, ByteBuffer, Callback)`, if any, is failed. +* The failure listeners are invoked, in the same order they have been added. + +Failure listeners are invoked also in case of idle timeouts, in the following cases: + +* At least one idle timeout listener returned `true` to indicate to the Jetty implementation to treat the idle timeout as a fatal failure. +* There are no idle timeout listeners. + +Failures reported to a failure listener are always fatal failures; see also xref:arch/io.adoc#content-source[this section] about fatal versus transient failures. +This means that it is not possible to read or write from a failure listener: the read returns a fatal failure chunk, and the write will immediately fail the write callback. + +[NOTE] +==== +Applications are always required to complete the `Handler` callback, as described <>. +In case of asynchronous failures, failure listeners are a good place to complete (typically by failing it) the `Handler` callback. +==== + +`Request.addCompletionListener(Consumer)` allows you to add a completion listener, which is invoked at the very end of the request/response processing. +This is equivalent to adding an `HttpStream` wrapper and overriding both `HttpStream.succeeded()` and `HttpStream.failed(Throwable)`. + +Completion listeners are typically (but not only) used to recycle or dispose resources used during the request/response processing, or get a precise timing for when the request/response processing finishes, to be paired with `Request.getBeginNanoTime()`. + +Note that while failure listeners are invoked as soon as the failure happens, completion listeners are invoked only at the very end of the request/response processing: after the `Callback` passed to `Handler.handle(Request, Response, Callback)` has been completed, all container dispatched threads have returned, and all the response writes have been completed. + +In case of many completion listeners, they are invoked in the reverse order they have been added. + +[[handler-impl-response]] +==== Using the `Response` + +The `Response` object can be accessed by web applications to set the HTTP response status code, the HTTP response headers and write the HTTP response content. + +The design of the `Response` APIs is similar to that of the `Request` APIs, described in <>. + +To use the `Response` APIs, you should look up the relevant methods in the following order: + +1. `Response` virtual methods. +For example, `Response.setStatus(int)` to set the HTTP response status code. +2. `Request` `static` methods. +These are utility methods that provide more convenient access to response features. +For example, adding an HTTP cookie could be done by adding a `Set-Cookie` response header, but it would be extremely error-prone. +The utility method `static Response.addCookie(Response, HttpCookie)` is provided instead. +3. Super class `static` methods. +Since `Response` _is-a_ `Content.Sink`, look also for `static` methods in `Content.Sink` that take a `Content.Sink` as a parameter, so that you can pass the `Response` object as a parameter. + +Below you can find a list of the most common `Response` features and how to access them. +Refer to the `Response` link:{javadoc-url}/org/eclipse/jetty/server/Response.html[javadocs] for the complete list. + +`Response` status code:: +The `Response` HTTP status code is accessed via `Response.getStatus()` and `Response.setStatus(int)`. + +`Response` HTTP headers:: +The `Response` HTTP headers are accessed via `Response.getHeaders()` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpFields.Mutable.html[`HttpFields.Mutable`] APIs. +The response headers are mutable until the response is _committed_, as defined in <>. + +`Response` cookies:: +The `Response` cookies are accessed via `static Response.addCookie(Response, HttpCookie)`, `static Response.replaceCookie(Response, HttpCookie)` and the link:{javadoc-url}/org/eclipse/jetty/http/HttpCookie.html[`HttpCookie`] APIs. +Since cookies translate to HTTP headers, they can be added/replaces until the response is _committed_, as defined in <>. + +[[handler-impl-response-content]] +==== Writing the `Response` Content + +Since `Response` _is-a_ `Content.Sink`, the xref:arch/io.adoc#content-sink[section] about writing to `Content.Sink` applies to `Response` as well. +The `static Content.Sink` utility methods will allow you to write the response content as a string, or as an `OutputStream`, for example. + +IMPORTANT: The first call to `Response.write(boolean, ByteBuffer, Callback)` _commits_ the response. + +Committing the response means that the response status code and response headers are sent to the other peer, and therefore cannot be modified anymore. +Trying to modify them may result in an `IllegalStateException` to be thrown, as it is an application mistake to commit the response and then try to modify the headers. + +You can explicitly commit the response by performing an empty, non-last write: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=flush] +---- + +[WARNING] +==== +The `Handler` returns `true`, so the `callback` parameter **must** be completed. + +It is therefore mandatory to use `CompletableFuture` APIs that are invoked even when writing the response content failed, such as `whenComplete(BiConsumer)`, `handle(BiFunction)`, `exceptionally(Function)`, etc. + +Failing to do so may result in the `Handler` `callback` parameter to never be completed, causing the request processing to hang forever. +==== + +Jetty can perform important optimizations for the HTTP/1.1 protocol if the response content length is known before the response is committed: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=contentLength] +---- + +NOTE: Setting the response content length is an _optimization_; Jetty will work well even without it. +If you set the response content length, however, remember that it must specify the number of _bytes_, not the number of characters. + +[[handler-impl-response-interim]] +==== Sending Interim ``Response``s + +The HTTP protocol (any version) allows applications to write https://www.rfc-editor.org/rfc/rfc9110#name-status-codes[interim responses]. + +An interim response has a status code in the `1xx` range (but not `101`), and an application may write zero or more interim response before the final response. + +This is an example of writing an interim `100 Continue` response: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=continue] +---- +<1> Using `Response.writeInterim(\...)` to send the interim response. +<2> The completion of the callback must take into account both success and failure. + +Note how writing an interim response is as asynchronous operation. +As such you must perform subsequent operations using the `CompletableFuture` APIs, and remember to complete the `Handler` `callback` parameter both in case of success or in case of failure. + +This is an example of writing an interim `103 Early Hints` response: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=earlyHints103] +---- +<1> Using `Response.writeInterim(\...)` to send the interim response. +<2> The completion of the callback must take into account both success and failure. + +An interim response may or may not have its own HTTP headers (this depends on the interim response status code), and they are typically different from the final response HTTP headers. + +[[session]] +=== HTTP Session Support + +Some web applications (but not all of them) have the concept of a _user_, that is a way to identify a specific client that is interacting with the web application. + +The HTTP session is a feature offered by servers that allows web applications to maintain a temporary, per-user, storage for user-specific data. + +The storage can be accessed by the web application across multiple request/response interactions with the client. +This makes the web application stateful, because a computation performed by a previous request may be stored in the HTTP session and used in subsequent requests without the need to perform again the computation. + +Since not all web applications need support for the HTTP session, Jetty offers this feature optionally. + +The Maven coordinates for the Jetty HTTP session support are: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty + jetty-session + {version} + +---- + +The HTTP session support is provided by the `org.eclipse.jetty.session.SessionHandler`, that must be set up in the `Handler` tree between a `ContextHandler` and your `Handler` implementation: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/SessionHandlerDocs.java[tags=session] +---- + +The corresponding `Handler` tree structure looks like the following: + +[,screen] +---- +Server +└── ContextHandler /myApp + └── SessionHandler + └── MyAppHandler +---- + +With the ``Handler``s set up in this way, you can access the HTTP session from your `MyAppHandler` using `Request.getSession(boolean)`, and then use the link:{javadoc-url}/org/eclipse/jetty/server/Session.html[`Session` APIs]. + +The support provided by Jetty for HTTP sessions is advanced and completely pluggable, providing features such as first-level and second-level caching, eviction, etc. + +You can configure the HTTP session support from a very simple local in-memory configuration, to a replicated (across nodes in a cluster), persistent (for example over file system, database or https://memcached.org/[memcached]) configuration for the most advanced use cases. +The advanced configuration of Jetty's HTTP session support is discussed in more details in xref:server/session.adoc[this section]. + +[[security]] +=== Securing HTTP Server Applications + +// TODO: ConstraintSecurityHandler and Authenticators and LoginServices +TODO + +[[application]] +=== Writing HTTP Server Applications + +Writing HTTP applications is typically simple, especially when using blocking APIs. +However, there are subtle cases where it is worth clarifying what a server application should do to obtain the desired results when run by Jetty. + +[[application-1xx]] +==== Sending 1xx Responses + +The https://tools.ietf.org/html/rfc7231#section-5.1.1[HTTP/1.1 RFC] allows for `1xx` informational responses to be sent before a real content response. +Unfortunately the servlet specification does not provide a way for these to be sent, so Jetty has had to provide non-standard handling of these headers. + +[[application-100]] +==== 100 Continue + +The `100 Continue` response should be sent by the server when a client sends a request with an `Expect: 100-continue` header, as the client will not send the body of the request until the `100 Continue` response has been sent. + +The intent of this feature is to allow a server to inspect the headers and to tell the client to not send a request body that might be too large or insufficiently private or otherwise unable to be handled. + +Jetty achieves this by waiting until the input stream or reader is obtained by the filter/servlet, before sending the `100 Continue` response. +Thus a filter/servlet may inspect the headers of a request before getting the input stream and send an error response (or redirect etc.) rather than the 100 continues. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=continue100] +---- + +[[jetty-102-processing]] +==== 102 Processing + +https://tools.ietf.org/html/rfc2518[RFC 2518] defined the `102 Processing` status code that can be sent: + +[quote,RFC 2518 section 10.1] +when the server has a reasonable expectation that the request will take significant time to complete. +As guidance, if a method is taking longer than 20 seconds (a reasonable, but arbitrary value) to process the server SHOULD return a `102 Processing` response. + +However, a later update of RFC 2518, https://tools.ietf.org/html/rfc4918[RFC 4918], removed the `102 Processing` status code for https://tools.ietf.org/html/rfc4918#appendix-F.3["lack of implementation"]. + +Jetty supports the `102 Processing` status code. +If a request is received with the `Expect: 102-processing` header, then a filter/servlet may send a `102 Processing` response (without terminating further processing) by calling `response.sendError(102)`. diff --git a/documentation/jetty/modules/programming-guide/pages/server/http2.adoc b/documentation/jetty/modules/programming-guide/pages/server/http2.adoc new file mode 100644 index 000000000000..342f2b4a9965 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/http2.adoc @@ -0,0 +1,200 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += HTTP/2 Server Library + +In the vast majority of cases, server applications should use the generic, high-level, xref:server/http.adoc[HTTP server library] that also provides HTTP/2 support via the HTTP/2 ``ConnectionFactory``s as described in details xref:server/http.adoc#connector-protocol-http2[here]. + +The low-level HTTP/2 server library has been designed for those applications that need low-level access to HTTP/2 features such as _sessions_, _streams_ and _frames_, and this is quite a rare use case. + +See also the correspondent xref:client/http2.adoc[HTTP/2 client library]. + +[[intro]] +== Introduction + +The Maven artifact coordinates for the HTTP/2 client library are the following: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.http2 + jetty-http2-server + {version} + +---- + +HTTP/2 is a multiplexed protocol: it allows multiple HTTP/2 requests to be sent on the same TCP connection, or _session_. +Each request/response cycle is represented by a _stream_. +Therefore, a single _session_ manages multiple concurrent _streams_. +A _stream_ has typically a very short life compared to the _session_: a _stream_ only exists for the duration of the request/response cycle and then disappears. + +[[flow-control]] +== HTTP/2 Flow Control + +The HTTP/2 protocol is _flow controlled_ (see https://tools.ietf.org/html/rfc7540#section-5.2[the specification]). +This means that a sender and a receiver maintain a _flow control window_ that tracks the number of data bytes sent and received, respectively. +When a sender sends data bytes, it reduces its flow control window. +When a receiver receives data bytes, it also reduces its flow control window, and then passes the received data bytes to the application. +The application consumes the data bytes and tells back the receiver that it has consumed the data bytes. +The receiver then enlarges the flow control window, and the implementation arranges to send a message to the sender with the number of bytes consumed, so that the sender can enlarge its flow control window. + +A sender can send data bytes up to its whole flow control window, then it must stop sending. +The sender may resume sending data bytes when it receives a message from the receiver that the data bytes sent previously have been consumed. +This message enlarges the sender flow control window, which allows the sender to send more data bytes. + +HTTP/2 defines _two_ flow control windows: one for each _session_, and one for each _stream_. +Let's see with an example how they interact, assuming that in this example the session flow control window is 120 bytes and the stream flow control window is 100 bytes. + +The sender opens a session, and then opens `stream_1` on that session, and sends `80` data bytes. +At this point the session flow control window is `40` bytes (`120 - 80`), and ``stream_1``'s flow control window is `20` bytes (`100 - 80`). +The sender now opens `stream_2` on the same session and sends `40` data bytes. +At this point, the session flow control window is `0` bytes (`40 - 40`), while ``stream_2``'s flow control window is `60` (`100 - 40`). +Since now the session flow control window is `0`, the sender cannot send more data bytes, neither on `stream_1` nor on `stream_2`, nor on other streams, despite all the streams having their stream flow control windows greater than `0`. + +The receiver consumes ``stream_2``'s `40` data bytes and sends a message to the sender with this information. +At this point, the session flow control window is `40` (``0 + 40``), ``stream_1``'s flow control window is still `20` and ``stream_2``'s flow control window is `100` (``60 + 40``). +If the sender opens `stream_3` and would like to send `50` data bytes, it would only be able to send `40` because that is the maximum allowed by the session flow control window at this point. + +It is therefore very important that applications notify the fact that they have consumed data bytes as soon as possible, so that the implementation (the receiver) can send a message to the sender (in the form of a `WINDOW_UPDATE` frame) with the information to enlarge the flow control window, therefore reducing the possibility that sender stalls due to the flow control windows being reduced to `0`. + +How a server application should handle HTTP/2 flow control is discussed in details in <>. + +[[setup]] +== Server Setup + +The low-level HTTP/2 support is provided by `org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory` and `org.eclipse.jetty.http2.api.server.ServerSessionListener`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java[tags=setup] +---- + +Where server applications using the xref:server/http.adoc[high-level server library] deal with HTTP requests and responses in ``Handler``s, server applications using the low-level HTTP/2 server library deal directly with HTTP/2 __session__s, __stream__s and __frame__s in a `ServerSessionListener` implementation. + +The `ServerSessionListener` interface defines a number of methods that are invoked by the implementation upon the occurrence of HTTP/2 events, and that server applications can override to react to those events. + +Please refer to the `ServerSessionListener` link:{javadoc-url}/org/eclipse/jetty/http2/api/server/ServerSessionListener.html[javadocs] for the complete list of events. + +The first event is the _accept_ event and happens when a client opens a new TCP connection to the server and the server accepts the connection. +This is the first occasion where server applications have access to the HTTP/2 `Session` object: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java[tags=accept] +---- + +After connecting to the server, a compliant HTTP/2 client must send the https://tools.ietf.org/html/rfc7540#section-3.5[HTTP/2 client preface], and when the server receives it, it generates the _preface_ event on the server. +This is where server applications can customize the connection settings by returning a map of settings that the implementation will send to the client: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java[tags=preface] +---- + +[[request]] +== Receiving a Request + +Receiving an HTTP request from the client, and sending a response, creates a _stream_ that encapsulates the exchange of HTTP/2 frames that compose the request and the response. + +An HTTP request is made of a `HEADERS` frame, that carries the request method, the request URI and the request headers, and optional `DATA` frames that carry the request content. + +Receiving the `HEADERS` frame opens the `Stream`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java[tags=request] +---- + +Server applications should return a `Stream.Listener` implementation from `onNewStream(\...)` to be notified of events generated by the client, such as `DATA` frames carrying request content, or a `RST_STREAM` frame indicating that the client wants to _reset_ the request, or an idle timeout event indicating that the client was supposed to send more frames but it did not. + +The example below shows how to receive request content: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java[tags=requestContent] +---- + +[NOTE] +==== +When `onDataAvailable(Stream stream)` is invoked, the demand is implicitly cancelled. + +Just returning from the `onDataAvailable(Stream stream)` method does _not_ implicitly demand for more `DATA` frames. + +Applications must call `Stream.demand()` to explicitly require that `onDataAvailable(Stream stream)` is invoked again when more `DATA` frames are available. +==== + +Applications that consume the content buffer within `onDataAvailable(Stream stream)` (for example, writing it to a file, or copying the bytes to another storage) should call `Data.release()` as soon as they have consumed the content buffer. +This allows the implementation to reuse the buffer, reducing the memory requirements needed to handle the content buffers. + +Alternatively, an application may store away the `Data` object to consume the buffer bytes later, or pass the `Data` object to another asynchronous API (this is typical in proxy applications). + +[IMPORTANT] +==== +The call to `Stream.readData()` tells the implementation to enlarge the stream and session flow control windows so that the sender will be able to send more `DATA` frames without stalling. +==== + +Applications can unwrap the `Data` object into some other object that may be used later, provided that the _release_ semantic is maintained: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/HTTP2Docs.java[tags=dataUnwrap] +---- + +[IMPORTANT] +==== +Applications that implement `onDataAvailable(Stream stream)` must remember to call `Stream.demand()` eventually. + +If they do not call `Stream.demand()`, the implementation will not invoke `onDataAvailable(Stream stream)` to deliver more `DATA` frames and the application will stall threadlessly until an idle timeout fires to close the stream or the session. +==== + +[[response]] +== Sending a Response + +After receiving an HTTP request, a server application must send an HTTP response. + +An HTTP response is typically composed of a `HEADERS` frame containing the HTTP status code and the response headers, and optionally one or more `DATA` frames containing the response content bytes. + +The HTTP/2 protocol also supports response trailers (that is, headers that are sent after the response content) that also are sent using a `HEADERS` frame. + +A server application can send a response in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java[tags=response;!exclude] +---- + +[[reset]] +== Resetting a Request + +A server application may decide that it does not want to accept the request. +For example, it may throttle the client because it sent too many requests in a time window, or the request is invalid (and does not deserve a proper HTTP response), etc. + +A request can be reset in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java[tags=reset;!exclude] +---- + +[[push]] +== HTTP/2 Push of Resources + +A server application may _push_ secondary resources related to a primary resource. + +A client may inform the server that it does not accept pushed resources(see https://tools.ietf.org/html/rfc7540#section-8.2[this section] of the specification) via a `SETTINGS` frame. +Server applications must track `SETTINGS` frames and verify whether the client supports HTTP/2 push, and only push if the client supports it: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http2/HTTP2ServerDocs.java[tags=push] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/server/http3.adoc b/documentation/jetty/modules/programming-guide/pages/server/http3.adoc new file mode 100644 index 000000000000..8727ddd7c33e --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/http3.adoc @@ -0,0 +1,135 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += HTTP/3 Server Library + +In the vast majority of cases, server applications should use the generic, high-level, xref:server/http.adoc[HTTP server library] that also provides HTTP/3 support via the HTTP/3 connector and ``ConnectionFactory``s as described in details xref:server/http.adoc#connector-protocol-http3[here]. + +The low-level HTTP/3 server library has been designed for those applications that need low-level access to HTTP/3 features such as _sessions_, _streams_ and _frames_, and this is quite a rare use case. + +See also the correspondent xref:client/http3.adoc[HTTP/3 client library]. + +[[intro]] +== Introduction + +The Maven artifact coordinates for the HTTP/3 client library are the following: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.http3 + jetty-http3-server + {version} + +---- + +HTTP/3 is a multiplexed protocol because it relies on the multiplexing capabilities of QUIC, the protocol based on UDP that transports HTTP/3 frames. +Thanks to multiplexing, multiple HTTP/3 requests are sent on the same QUIC connection, or _session_. +Each request/response cycle is represented by a _stream_. +Therefore, a single _session_ manages multiple concurrent _streams_. +A _stream_ has typically a very short life compared to the _session_: a _stream_ only exists for the duration of the request/response cycle and then disappears. + +// TODO: flow control? +//[[pg-server-http3-flow-control]] +//==== HTTP/3 Flow Control +// +//include::../../http3.adoc[tag=flowControl] +// +//How a server application should handle HTTP/3 flow control is discussed in details in <>. + +[[setup]] +== Server Setup + +The low-level HTTP/3 support is provided by `org.eclipse.jetty.http3.server.RawHTTP3ServerConnectionFactory` and `org.eclipse.jetty.http3.api.Session.Server.Listener`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java[tags=setup] +---- + +Where server applications using the xref:server/http.adoc[high-level server library] deal with HTTP requests and responses in ``Handler``s, server applications using the low-level HTTP/3 server library deal directly with HTTP/3 __session__s, __stream__s and __frame__s in a `Session.Server.Listener` implementation. + +The `Session.Server.Listener` interface defines a number of methods that are invoked by the implementation upon the occurrence of HTTP/3 events, and that server applications can override to react to those events. + +Please refer to the `Session.Server.Listener` link:{javadoc-url}/org/eclipse/jetty/http3/api/Session.Server.Listener.html[javadocs] for the complete list of events. + +The first event is the _accept_ event and happens when a client opens a new QUIC connection to the server and the server accepts the connection. +This is the first occasion where server applications have access to the HTTP/3 `Session` object: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java[tags=accept] +---- + +After the QUIC connection has been established, both client and server send an HTTP/3 `SETTINGS` frame to exchange their HTTP/3 configuration. +This generates the _preface_ event, where applications can customize the HTTP/3 settings by returning a map of settings that the implementation will send to the other peer: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java[tags=preface] +---- + +[[request]] +== Receiving a Request + +Receiving an HTTP request from the client, and sending a response, creates a _stream_ that encapsulates the exchange of HTTP/3 frames that compose the request and the response. + +An HTTP request is made of a `HEADERS` frame, that carries the request method, the request URI and the request headers, and optional `DATA` frames that carry the request content. + +Receiving the `HEADERS` frame opens the `Stream`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java[tags=request] +---- + +Server applications should return a `Stream.Server.Listener` implementation from `onRequest(...)` to be notified of events generated by the client, such as `DATA` frames carrying request content, or a reset event indicating that the client wants to _reset_ the request, or an idle timeout event indicating that the client was supposed to send more frames but it did not. + +The example below shows how to receive request content: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java[tags=requestContent] +---- + +// TODO: flow control? +//include::../../http3.adoc[tag=apiFlowControl] + +[[response]] +== Sending a Response + +After receiving an HTTP request, a server application must send an HTTP response. + +An HTTP response is typically composed of a `HEADERS` frame containing the HTTP status code and the response headers, and optionally one or more `DATA` frames containing the response content bytes. + +The HTTP/3 protocol also supports response trailers (that is, headers that are sent after the response content) that also are sent using a `HEADERS` frame. + +A server application can send a response in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java[tags=response;!exclude] +---- + +[[reset]] +== Resetting a Request + +A server application may decide that it does not want to accept the request. +For example, it may throttle the client because it sent too many requests in a time window, or the request is invalid (and does not deserve a proper HTTP response), etc. + +A request can be reset in this way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/http3/HTTP3ServerDocs.java[tags=reset;!exclude] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/server/index.adoc b/documentation/jetty/modules/programming-guide/pages/server/index.adoc new file mode 100644 index 000000000000..3dd8b0582736 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/index.adoc @@ -0,0 +1,29 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Server Libraries + +The Eclipse Jetty Project provides server-side libraries that allow you to configure and start programmatically an HTTP or WebSocket server from a main class, or embed it in your existing application. +A typical example is a HTTP server that needs to expose a REST endpoint. +Another example is a proxy application that receives HTTP requests, processes them, and then forwards them to third party services, for example using the Jetty xref:client/index.adoc[client libraries]. + +While historically Jetty is an HTTP server, it is possible to use the Jetty server-side libraries to write a generic network server that interprets any network protocol (not only HTTP). +If you are interested in the low-level details of how the Eclipse Jetty server libraries work, or are interested in writing a custom protocol, look at the xref:server/io-arch.adoc[Server I/O Architecture]. + +The Jetty server-side libraries provide: + +* HTTP high-level support for HTTP/1.0, HTTP/1.1, HTTP/2, clear-text or encrypted, HTTP/3, for applications that want to embed Jetty as a generic HTTP server or proxy (no matter the HTTP version), via the xref:server/http.adoc[HTTP libraries] +* HTTP/2 low-level support, for applications that want to explicitly handle low-level HTTP/2 _sessions_, _streams_ and _frames_, via the xref:server/http2.adoc[HTTP/2 libraries] +* HTTP/3 low-level support, for applications that want to explicitly handle low-level HTTP/3 _sessions_, _streams_ and _frames_, via the xref:server/http3.adoc[HTTP/3 libraries] +* WebSocket support, for applications that want to embed a WebSocket server, via the xref:server/websocket.adoc[WebSocket libraries] +* FCGI support, to delegate requests to PHP, Python, Ruby or similar scripting languages. diff --git a/documentation/jetty/modules/programming-guide/pages/server/io-arch.adoc b/documentation/jetty/modules/programming-guide/pages/server/io-arch.adoc new file mode 100644 index 000000000000..86b8b06a54dd --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/io-arch.adoc @@ -0,0 +1,223 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Server I/O Architecture + +The Jetty server libraries provide the basic components and APIs to implement a network server. + +They build on the common xref:arch/io.adoc[Jetty I/O Architecture] and provide server specific concepts. + +The Jetty server libraries provide I/O support for TCP/IP sockets (for both IPv4 and IPv6) and, when using Java 16 or later, for Unix-Domain sockets. + +Support for Unix-Domain sockets is interesting when Jetty is deployed behind a proxy or a load-balancer: it is possible to configure the proxy or load balancer to communicate with Jetty via Unix-Domain sockets, rather than via the loopback network interface. + +The central I/O server-side component are `org.eclipse.jetty.server.ServerConnector`, that handles the TCP/IP socket traffic, and `org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector`, that handles the Unix-Domain socket traffic. + +`ServerConnector` and `UnixDomainServerConnector` are very similar, and while in the following sections `ServerConnector` is used, the same concepts apply to `UnixDomainServerConnector`, unless otherwise noted. + +A `ServerConnector` manages a list of ``ConnectionFactory``s, that indicate what protocols the connector is able to speak. + +[[connection-factory]] +== Creating Connections with `ConnectionFactory` + +Recall from the xref:arch/io.adoc#connection[`Connection` section] of the Jetty I/O architecture that `Connection` instances are responsible for parsing bytes read from a socket and generating bytes to write to that socket. + +On the server-side, a `ConnectionFactory` creates `Connection` instances that know how to parse and generate bytes for the specific protocol they support -- it can be either HTTP/1.1, or TLS, or FastCGI, or the https://www.haproxy.org/download/2.1/doc/proxy-protocol.txt[PROXY protocol]. + +For example, this is how clear-text HTTP/1.1 is configured for TCP/IP sockets: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=http] +---- + +With this configuration, the `ServerConnector` will listen on port `8080`. + +Similarly, this is how clear-text HTTP/1.1 is configured for Unix-Domain sockets: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=httpUnix] +---- + +With this configuration, the `UnixDomainServerConnector` will listen on file `/tmp/jetty.sock`. + +[NOTE] +==== +`ServerConnector` and `UnixDomainServerConnector` only differ by how they are configured -- for `ServerConnector` you specify the IP port it listens to, for `UnixDomainServerConnector` you specify the Unix-Domain path it listens to. + +Both configure ``ConnectionFactory``s in exactly the same way. +==== + +When a new socket connection is established, `ServerConnector` delegates to the `ConnectionFactory` the creation of the `Connection` instance for that socket connection, that is linked to the corresponding `EndPoint`: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam padding 5 + +hide members +hide circle + +scale 1.5 + +circle network +circle application + +network - SocketChannelEndPoint +SocketChannelEndPoint - HttpConnection +HttpConnection - application +---- + +For every socket connection there will be an `EndPoint` + `Connection` pair. + +[[connection-factory-wrapping]] +== Wrapping a `ConnectionFactory` + +A `ConnectionFactory` may wrap another `ConnectionFactory`; for example, the TLS protocol provides encryption for any other protocol. +Therefore, to support encrypted HTTP/1.1 (also known as `https`), you need to configure the `ServerConnector` with two ``ConnectionFactory``s -- one for the TLS protocol and one for the HTTP/1.1 protocol, like in the example below: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=tlsHttp] +---- + +With this configuration, the `ServerConnector` will listen on port `8443`. +When a new socket connection is established, the first `ConnectionFactory` configured in `ServerConnector` is invoked to create a `Connection`. +In the example above, `SslConnectionFactory` creates a `SslConnection` and then asks to its wrapped `ConnectionFactory` (in the example, `HttpConnectionFactory`) to create the wrapped `Connection` (an `HttpConnection`) and will then link the two ``Connection``s together, in this way: + +[plantuml] +---- +skinparam backgroundColor transparent +skinparam monochrome true +skinparam shadowing false +skinparam padding 5 + +hide members +hide circle + +scale 1.5 + +circle network +circle application + +network - SocketChannelEndPoint +SocketChannelEndPoint - SslConnection +SslConnection -- SslEndPoint +SslEndPoint - HttpConnection +HttpConnection - application +---- + +Bytes read by the `SocketChannelEndPoint` will be interpreted as TLS bytes by the `SslConnection`, then decrypted and made available to the `SslEndPoint` (a component part of `SslConnection`), which will then provide them to `HttpConnection`. + +The application writes bytes through the `HttpConnection` to the `SslEndPoint`, which will encrypt them through the `SslConnection` and write the encrypted bytes to the `SocketChannelEndPoint`. + +[[connection-factory-detecting]] +== Choosing `ConnectionFactory` via Bytes Detection + +Typically, a network port is associated with a specific protocol. +For example, port `80` is associated with clear-text HTTP, while port `443` is associated with encrypted HTTP (that is, the TLS protocol wrapping the HTTP protocol, also known as `https`). + +In certain cases, applications need to listen to the same port for two or more protocols, or for different but incompatible versions of the same protocol, which can only be distinguished by reading the initial bytes and figuring out to what protocol they belong to. + +The Jetty server libraries support this case by placing a `DetectorConnectionFactory` in front of other ``ConnectionFactory``s. +`DetectorConnectionFactory` accepts a list of ``ConnectionFactory``s that implement `ConnectionFactory.Detecting`, which will be called to see if one of them recognizes the initial bytes. + +In the example below you can see how to support both clear-text and encrypted HTTP/1.1 (i.e. both `http` and `https`) _on the same network port_: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=detector] +---- + +<1> Creates the `DetectorConnectionFactory` with the `SslConnectionFactory` as the only detecting `ConnectionFactory`. +With this configuration, the detector will delegate to `SslConnectionFactory` to recognize the initial bytes, which will detect whether the bytes are TLS protocol bytes. +<2> Creates the `ServerConnector` with `DetectorConnectionFactory` as the first `ConnectionFactory`, and `HttpConnectionFactory` as the next `ConnectionFactory` to invoke if the detection fails. + +In the example above `ServerConnector` will listen on port 8181. +When a new socket connection is established, `DetectorConnectionFactory` is invoked to create a `Connection`, because it is the first `ConnectionFactory` specified in the `ServerConnector` list. +`DetectorConnectionFactory` reads the initial bytes and asks to its detecting ``ConnectionFactory``s if they recognize the bytes. +In the example above, the detecting ``ConnectionFactory`` is `SslConnectionFactory` which will therefore detect whether the initial bytes are TLS bytes. +If one of the detecting ``ConnectionFactory``s recognizes the bytes, it creates a `Connection`; otherwise `DetectorConnectionFactory` will try the next `ConnectionFactory` after itself in the `ServerConnector` list. +In the example above, the next `ConnectionFactory` after `DetectorConnectionFactory` is `HttpConnectionFactory`. + +The final result is that when new socket connection is established, the initial bytes are examined: if they are TLS bytes, a `SslConnectionFactory` will create a `SslConnection` that wraps an `HttpConnection` as explained <>, therefore supporting `https`; otherwise they are not TLS bytes and an `HttpConnection` is created, therefore supporting `http`. + +[[connection-factory-custom]] +== Writing a Custom `ConnectionFactory` + +This section explains how to use the Jetty server-side libraries to write a generic network server able to parse and generate any protocol.. + +Let's suppose that we want to write a custom protocol that is based on JSON but has the same semantic as HTTP; let's call this custom protocol `JSONHTTP`, so that a request would look like this: + +[,json] +---- +{ + "type": "request", + "method": "GET", + "version": "HTTP/1.1", + "uri": "http://localhost/path", + "fields": { + "content-type": "text/plain;charset=ASCII" + }, + "content": "HELLO" +} +---- + +In order to implement this custom protocol, we need to: + +* implement a `JSONHTTPConnectionFactory` +* implement a `JSONHTTPConnection` +* parse bytes and generate bytes in the `JSONHTTP` format +* design an easy to use API that applications use to process requests and respond + +First, the `JSONHTTPConnectionFactory`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=jsonHttpConnectionFactory] +---- + +Note how `JSONHTTPConnectionFactory` extends `AbstractConnectionFactory` to inherit facilities common to all `ConnectionFactory` implementations. + +Second, the `JSONHTTPConnection`. +Recall from the xref:arch/io.adoc#echo[echo `Connection` example] that you need to override `onOpen()` to call `fillInterested()` so that the Jetty I/O system will notify your `Connection` implementation when there are bytes to read by calling `onFillable()`. +Furthermore, because the Jetty libraries are non-blocking and asynchronous, you need to use `IteratingCallback` to implement `onFillable()`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=jsonHttpConnection] +---- + +Again, note how `JSONHTTPConnection` extends `AbstractConnection` to inherit facilities that you would otherwise need to re-implement from scratch. + +When `JSONHTTPConnection` receives a full JSON object it calls `invokeApplication(...)` to allow the application to process the incoming request and produce a response. + +At this point you need to design a non-blocking asynchronous API that takes a `Callback` parameter so that applications can signal to the implementation when the request processing is complete (either successfully or with a failure). + +A simple example of this API design could be the following: + +* Wrap the JSON `Map` into a `JSONHTTPRequest` parameter so that applications may use more specific HTTP APIs such as `JSONHTTPRequest.getMethod()` rather than a generic `Map.get("method")` +* Provide an equivalent `JSONHTTPResponse` parameter so that applications may use more specific APIs such as `JSONHTTPResponse.setStatus(int)` rather than a generic `Map.put("status", 200)` +* Provide a `Callback` (or a `CompletableFuture`) parameter so that applications may indicate when the request processing is complete + +This results in the following API: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/ServerDocs.java[tags=jsonHttpAPI] +---- + +The important part of this simple API example is the `Callback` parameter that makes the API non-blocking and asynchronous. diff --git a/documentation/jetty/modules/programming-guide/pages/server/session.adoc b/documentation/jetty/modules/programming-guide/pages/server/session.adoc new file mode 100644 index 000000000000..22cda501555b --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/session.adoc @@ -0,0 +1,1107 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += HTTP Session Management + +Sessions are a concept within the Servlet API which allow requests to store and retrieve information across the time a user spends in an application. + +[[architecture]] +== Session Architecture + +Jetty session support has been architected to provide a core implementation that is independent of the Servlet specification. +This allows programmers who use core Jetty - without the Servlet API - to still have classic Servlet session-like support for their ``Request``s and ``Handler``s. + +These core classes are adapted to each of the various Servlet specification environments to deliver classic ``HttpSession``s for ``Servlet``s,`Filter``s, etc + +Full support for the session lifecycle is supported, in addition to L1 and L2 caching, and a number of pluggable options for persisting session data. + +Here are some of the most important concepts that will be referred to throughout the documentation: + +SessionIdManager:: +responsible for allocation of unique session ids. +HouseKeeper:: +responsible for orchestrating the detection and removal of expired sessions. +SessionManager:: +responsible for managing the lifecycle of sessions. +SessionHandler:: +an implementation of `SessionManager` that adapts sessions to either the core or Servlet specification environment. +SessionCache:: +an L1 cache of in-use `ManagedSession` objects +Session:: +a session consisting of `SessionData` that can be associated with a `Request` +ManagedSession:: +a `Session` that supports caching and lifecycle management +SessionData:: +encapsulates the attributes and metadata associated with a `Session` +SessionDataStore:: +responsible for creating, persisting and reading `SessionData` +CachingSessionDataStore:: +an L2 cache of `SessionData` + +Diagrammatically, these concepts can be represented as: + +[plantuml] +---- +title Session Composition Diagram +class Server + +interface SessionIdManager + +class HouseKeeper + +interface SessionManager + +class SessionHandler + +interface SessionCache + +interface SessionDataStore + +class CachingSessionDataStore + +interface Session + +class ManagedSession + +class SessionData + +class Request + +Server "1" *-down- "1" SessionIdManager +SessionIdManager "1" *-left- "1" HouseKeeper +Server "1" *-down- "n" SessionHandler +Request "1" *-down- "0/1" Session +SessionManager "1" *-down- "1" SessionCache +SessionManager <|-- SessionHandler +SessionCache "1" *-down- "1" SessionDataStore +SessionCache o-down- ManagedSession +ManagedSession "1" *-- "1" SessionData +Session <|-- ManagedSession +SessionDataStore --> SessionData: CRUD +SessionDataStore <|-- CachingSessionDataStore +CachingSessionDataStore o- SessionData +---- + +[[idmgr]] +== The SessionIdManager + +There is a maximum of one `SessionIdManager` per `Server` instance. +Its purpose is to generate fresh, unique session ids and to coordinate the re-use of session ids amongst co-operating contexts. + +The `SessionIdManager` is agnostic with respect to the type of clustering technology chosen. + +Jetty provides a default implementation - the link:{javadoc-url}/org/eclipse/jetty/session/DefaultSessionIdManager.html[DefaultSessionIdManager] - which should meet the needs of most users. + +[[defaultidmgr]] +=== The DefaultSessionIdManager + +[[workername]] +A single instance of the `DefaultSessionIdManager` should be created and registered as a bean on the `Server` instance so that all ``SessionHandler``'s share the same instance. +This is done by the Jetty `session` module, but can be done programmatically instead. +As a fallback, when an individual `SessionHandler` starts up, if it does not find the `SessionIdManager` already present for the `Server` it will create and register a bean for it. +That instance will be shared by the other ``SessionHandler``s. + +The most important configuration parameter for the `DefaultSessionIdManager` is the `workerName`, which uniquely identifies the server in a cluster. +If a `workerName` has not been explicitly set, then the value is derived as follows: + + node[JETTY_WORKER_NAME] + +where `JETTY_WORKER_NAME` is an environment variable whose value can be an integer or string. +If the environment variable is not set, then it defaults to `0`, yielding the default `workerName` of `"node0"`. +It is _essential_ to change this default if you have more than one `Server`. + +Here is an example of explicitly setting up a `DefaultSessionIdManager` with a `workerName` of `server3` in code: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=default] +---- + +[[housekeeper]] +=== The HouseKeeper + +The `DefaultSessionIdManager` creates a link:{javadoc-url}/org/eclipse/jetty/session/HouseKeeper.html[HouseKeeper], which periodically scans for, and eliminates, expired sessions (referred to as "scavenging"). +The period of the scan is controlled by the `setIntervalSec(int)` method, defaulting to 600secs. +Setting a negative or 0 value prevents scavenging occurring. + + +[IMPORTANT] +==== +The `HouseKeeper` semi-randomly adds 10% to the configured `intervalSec`. +This is to help prevent sync-ing up of servers in a cluster that are all restarted at once, and slightly stagger their scavenge cycles to ensure any load on the persistent storage mechanism is spread out. +==== + +Here is an example of creating and configuring a `HouseKeeper` for the `DefaultSessionIdManager` in code: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=housekeeper] +---- + +=== Implementing a Custom SessionIdManager + +If the `DefaultSessionIdManager` does not meet your needs, you can extend it, or implement the `SessionIdManager` interface directly. + +When implementing a `SessionIdManager` pay particular attention to the following: + +* the `getWorkerName()` method _must_ return a name that is unique to the `Server` instance. +The `workerName` becomes important in clustering scenarios because sessions can migrate from node to node: the `workerName` identifies which node was last managing a `Session`. +* the contract of the `isIdInUse(String id)` method is very specific: a session id may _only_ be reused _iff_ it is already in use by another context. +This restriction is important to support cross-context dispatch. +* you should be _very_ careful to ensure that the `newSessionId(HttpServletRequest request, long created)` method does not return duplicate or predictable session ids. + +[[handler]] +== The SessionHandler + +A `SessionHandler` is a `Handler` that implements the `SessionManager`, and is thus responsible for the creation, maintenance and propagation of sessions. +There are `SessionHandlers` for both the core and the various Servlet environments. + +Note that in the Servlet environments, each `ServletContextHandler` or `WebAppContext` has at most a single `SessionHandler`. + +Both core and Servlet environment `SessionHandlers` can be configured programmatically. +Here are some of the most important methods that you may call to customize your session setup. +Note that in Servlet environments, some of these methods also have analogous Servlet API methods and/or analogous `web.xml` declarations and also equivalent context init params. +These alternatives are noted below. + +setCheckingRemoteSessionIdEncoding(boolean) _[Default:false]_ :: +This controls whether response urls will be encoded with the session id as a path parameter when the URL is destined for a remote node. + +_Servlet environment alternatives:_ +* `org.eclipse.jetty.session.CheckingRemoteSessionIdEncoding` context init parameter + +setMaxInactiveInterval(int) _[Default:-1]_ :: +This is the amount of time in seconds after which an unused session may be scavenged. + +_Servlet environment alternatives:_ +* `` element in `web.xml` (NOTE! this element is specified in _minutes_ but this method uses _seconds_). +* `ServletContext.setSessionTimeout(int)` where the timeout is configured in _minutes_. + +setHttpOnly(boolean) _[Default:false]_ :: +If `true`, the session cookie will not be exposed to client-side scripting code. + +_Servlet environment alternatives:_ +* `SessionCookieConfig.setHttpOnly(boolean)` +* `` element in `web.xml` + +[[handler-refreshcookie]] +setRefreshCookieAge(int) _[Default:-1]_ :: +Value in seconds that controls resetting the session cookie when `SessionCookieConfig.setMaxAge(int)` is non-zero. +See also <>. +If the amount of time since the session cookie was last set exceeds this time, the session cookie is regenerated to keep the session cookie valid. + +setSameSite(HttpCookie.SameSite) _[Default:null]_ :: +The values are `HttpCookie.SameSite.NONE`, `HttpCookie.SameSite.STRICT`, `HttpCookie.SameSite.LAX`. + +setSecureRequestOnly(boolean) _[Default:true]_:: +If `true` and the request is HTTPS, the set session cookie will be marked as `secure`, meaning the client will only send the session cookie to the server on subsequent requests over HTTPS. + +_Servlet environment alternatives:_ +* `SessionCookieConfig.setSecure(true)`, in which case the set session cookie will _always_ be marked as `secure`, even if the request triggering the creation of the cookie was not over HTTPS. +* `` element in `web.xml` + +setSessionCookie(String) _[Default:"JSESSIONID"]_:: +This is the name of the session cookie. + +_Servlet environment alternatives:_ +* `SessionCookieConfig.setName(String)` +* `` element in `web.xml` +* `org.eclipse.jetty.session.SessionCookie` context init parameter. + +setSessionIdPathParameterName(String) _[Default:"jsessionid"]_:: +This is the name of the path parameter used to transmit the session id on request URLs, and on encoded URLS in responses. + +_Servlet environment alternatives:_ +* `org.eclipse.jetty.session.SessionIdPathParameterName` context init parameter + +setSessionTrackingModes(Set) _[Default:{`SessionTrackingMode.COOKIE`, `SessionTrackingMode.URL`}]_:: +_Servlet environment alternatives:_ +* `ServletContext.setSessionTrackingModes)` +* defining up to three ````s for the `` element in `web.xml` + +setUsingCookies(boolean) _[Default:true]_ :: +Determines whether the `SessionHandler` will look for session cookies on requests, and will set session cookies on responses. +If `false` session ids must be transmitted as path params on URLs. + +[[handler-maxAge]] +setMaxAge(int) _[Default:-1]_:: +This is the maximum number of seconds that the session cookie will be considered to be valid. +By default, the cookie has no maximum validity time. +See also <>. + +_Servlet environment alternatives:_ +* `ServletContext.getSessionCookieConfig().setMaxAge(int)` +* `org.eclipse.jetty.session.MaxAge` context init parameter + +setSessionDomain(String) _[Default:null]_ :: +This is the domain of the session cookie. + +_Servlet environment alternatives:_ +* `ServletContext.getSessionCookieConfig().setDomain(String)` +* `` element in `web.xml` +* `org.eclipse.jetty.session.SessionDomain` context init parameter + +setSessionPath(String) _[Default:null]_:: +This is used when creating a new session cookie. +If nothing is configured, the context path is used instead, defaulting to `/`. + +_Servlet environment alternatives:_ +* `ServletContext.getSessionCookieConfig().setPath(String)` +* `` element in `web.xml` +* `org.eclipse.jetty.session.SessionPath` context init parameter + +=== Statistics + +Some statistics about the sessions for a context can be obtained from the `SessionHandler`, either by calling the methods directly or via JMX: + +getSessionsCreated():: +This is the total number of sessions that have been created for this context since Jetty started. + +getSessionTimeMax():: +The longest period of time a session was valid in this context before being invalidated. + +getSessionTimeMean():: +The average period of time a session in this context was valid. + +getSessionTimeStdDev():: +The standard deviation of the session validity times for this context. + +getSessionTimeTotal():: +The total time that all sessions in this context have remained valid. + +[[cache]] +== The SessionCache + +There is one `SessionCache` per `SessionManager`, and thus one per context. +Its purpose is to provide an L1 cache of `ManagedSession` objects. +Having a working set of `ManagedSession` objects in memory allows multiple simultaneous requests for the same session (ie the _same_ session id in the _same_ context) to share the same `ManagedSession` object. +A `SessionCache` uses a `SessionDataStore` to create, read, store, and delete the `SessionData` associated with the `ManagedSession`. + +There are two ways to create a `SessionCache` for a `SessionManager`: + +. allow the `SessionManager` to create one lazily at startup. +The `SessionManager` looks for a `SessionCacheFactory` bean on the `Server` to produce the `SessionCache` instance. +It then looks for a `SessionDataStoreFactory` bean on the `Server` to produce a `SessionDataStore` instance to use with the `SessionCache`. +If no `SessionCacheFactory` is present, it defaults to creating a `DefaultSessionCache`. +If no `SessionDataStoreFactory` is present, it defaults to creating a `NullSessionDataStore`. + +. pass a fully configured `SessionCache` instance to the `SessionManager`. +You are responsible for configuring both the `SessionCache` instance and its `SessionDataStore` + +More on ``SessionDataStore``s <>, this section concentrates on the `SessionCache` and `SessionCacheFactory`. + + +The link:{javadoc-url}/org/eclipse/jetty/session/AbstractSessionCache.html[AbstractSessionCache] provides most of the behaviour of ``SessionCache``s. +If you are implementing a custom `SessionCache` it is strongly recommended that you extend this class because it implements the numerous subtleties of the Servlet specification. + +Some of the important behaviours of ``SessionCache``s are: + +eviction:: +By default, ``ManagedSession``s remain in a cache until they are expired or invalidated. +If you have many or large sessions that are infrequently referenced you can use eviction to reduce the memory consumed by the cache. +When a session is evicted, it is removed from the cache but it is _not_ invalidated. +If you have configured a `SessionDataStore` that persists or distributes the session in some way, it will continue to exist, and can be read back in when it needs to be referenced again. +The eviction strategies are: + NEVER_EVICT::: + This is the default, sessions remain in the cache until expired or invalidated. + EVICT_ON_SESSION_EXIT::: + When the last simultaneous request for a session finishes, the session will be evicted from the cache. + EVICT_ON_INACTIVITY::: + If a session has not been referenced for a configurable number of seconds, then it will be evicted from the cache. + +saveOnInactiveEviction:: +This controls whether a session will be persisted to the `SessionDataStore` if it is being evicted due to the EVICT_ON_INACTIVITY policy. +Usually sessions are written to the `SessionDataStore` whenever the last simultaneous request exits the session. +However, as ``SessionDataStore``s` can be configured to <>, this option ensures that the session will be written out. + +saveOnCreate:: +Usually a session will be written through to the configured `SessionDataStore` when the last request for it finishes. +In the case of a freshly created session, this means that it will not be persisted until the request is fully finished. +If your application uses context forwarding or including, the newly created session id will not be available in the subsequent contexts. +You can enable this feature to ensure that a freshly created session is immediately persisted after creation: in this way the session id will be available for use in other contexts accessed during the same request. + +removeUnloadableSessions:: +If a session becomes corrupted in the persistent store, it cannot be re-loaded into the `SessionCache`. +This can cause noisy log output during scavenge cycles, when the same corrupted session fails to load over and over again. +To prevent his, enable this feature and the `SessionCache` will ensure that if a session fails to be loaded, it will be deleted. + +invalidateOnShutdown:: +Some applications want to ensure that all cached sessions are removed when the server shuts down. +This option will ensure that all cached sessions are invalidated. +The `AbstractSessionCache` does not implement this behaviour, a subclass must implement the link:{javadoc-url}/org/eclipse/jetty/session/SessionCache.html#shutdown()[SessionCache.shutdown()] method. + +flushOnResponseCommit:: +This forces a "dirty" session to be written to the `SessionDataStore` just before a response is returned to the client, rather than waiting until the request is finished. +A "dirty" session is one whose attributes have changed, or it has been freshly created. +Using this option ensures that all subsequent requests - either to the same or a different node - will see the latest changes to the session. + +Jetty provides two `SessionCache` implementations: the link:{javadoc-url}/org/eclipse/jetty/session/DefaultSessionCache.html[DefaultSessionCache] and the link:{javadoc-url}/org/eclipse/jetty/session/NullSessionCache.html[NullSessionCache]. + +[[hash]] +=== The DefaultSessionCache + +The link:{javadoc-url}/org/eclipse/jetty/session/DefaultSessionCache.html[DefaultSessionCache] retains `ManagedSession` objects in memory in a `ConcurrentHashMap`. +It is suitable for non-clustered and clustered deployments. +For clustered deployments, a sticky load balancer is *strongly* recommended, otherwise you risk indeterminate session state as the session bounces around multiple nodes. + +It implements the link:{javadoc-url}/org/eclipse/jetty/session/SessionCache.html#shutdown()[SessionCache.shutdown()] method. + +It also provides some statistics on sessions, which are convenient to access either directly in code or remotely via JMX: + +current sessions:: +The link:{javadoc-url}/org/eclipse/jetty/session/DefaultSessionCache.html#getSessionsCurrent()[DefaultSessionCache.getSessionsCurrent()] method reports the number of sessions in the cache at the time of the method call. + +max sessions:: +The link:{javadoc-url}/org/eclipse/jetty/session/DefaultSessionCache.html#getSessionsCurrent()[DefaultSessionCache.getSessionsMax()] method reports the highest number of sessions in the cache at the time of the method call. + +total sessions:: +The link:{javadoc-url}/org/eclipse/jetty/session/DefaultSessionCache.html#getSessionsTotal()[DefaultSessionCache.getSessionsTotal()] method reports the cumulative total of the number of sessions in the cache at the time of the method call. + +If you create a link:{javadoc-url}/org/eclipse/jetty/session/DefaultSessionCacheFactory.html[DefaultSessionFactory] and register it as a `Server` bean, a `SessionManger` will be able to lazily create a `DefaultSessionCache`. +The `DefaultSessionCacheFactory` has all of the same configuration setters as a `DefaultSessionCache`. +Alternatively, if you only have a single `SessionManager`, or you need to configure a `DefaultSessionCache` differently for every `SessionManager`, then you could dispense with the `DefaultSessionCacheFactory` and simply instantiate, configure, and pass in the `DefaultSessionCache` yourself. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=defaultsessioncache] +---- + +NOTE: If you don't configure any `SessionCache` or `SessionCacheFactory`, a `SessionManager` will automatically create its own `DefaultSessionCache`. + +[[null]] +=== The NullSessionCache + +The link:{javadoc-url}/org/eclipse/jetty/session/NullSessionCache.html[NullSessionCache] does not actually cache any objects: each request uses a fresh `ManagedSession` object. +It is suitable for clustered deployments without a sticky load balancer and non-clustered deployments when purely minimal support for sessions is needed. + +As no sessions are actually cached, of course functions like `invalidateOnShutdown` and all of the eviction strategies have no meaning for the `NullSessionCache`. + +There is a link:{javadoc-url}/org/eclipse/jetty/session/NullSessionCacheFactory.html[NullSessionCacheFactory] which you can instantiate, configure and set as a `Server` bean to enable a `SessionManager` to automatically create new ``NullSessionCache``s as needed. +All of the same configuration options are available on the `NullSessionCacheFactory` as the `NullSessionCache` itself. +Alternatively, if you only have a single `SessionManager`, or you need to configure a `NullSessionCache` differently for every `SessionManager`, then you could dispense with the `NullSessionCacheFactory` and simply instantiate, configure, and pass in the `NullSessionCache` yourself. + + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=nullsessioncache] +---- + +[[customcache]] +=== Implementing a custom SessionCache + +As previously mentioned, it is strongly recommended that you extend the link:{javadoc-url}/org/eclipse/jetty/session/AbstractSessionCache.html[AbstractSessionCache]. + +=== Heterogeneous caching + +Using one of the ``SessionCacheFactory``s will ensure that every time a `SessionManager` starts it will create a new instance of the corresponding type of `SessionCache`. + +But, what if you deploy multiple webapps, and for one of them, you don't want to use sessions? +Or alternatively, you don't want to use sessions, but you have one webapp that now needs them? +In that case, you can configure the `SessionCacheFactory` appropriate to the majority, and then specifically create the right type of `SessionCache` for the others. +Here's an example where we configure the `DefaultSessionCacheFactory` to handle most webapps, but then specifically use a `NullSessionCache` for another: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=mixedsessioncache] +---- + +[[datastore]] +== The SessionDataStore + +A link:{javadoc-url}/org/eclipse/jetty/session/SessionDataStore.html[SessionDataStore] mediates the storage, retrieval and deletion of `SessionData`. +There is one `SessionDataStore` per `SessionCache` and thus one per context. +Jetty provides a number of alternative `SessionDataStore` implementations: + +[plantuml] +---- +title SessionDataStores + +interface SessionDataStore +class AbstractSessionDataStore +class NullSessionDataStore +class FileSessionDataStore +class GCloudSessionDataStore +class HazelcastSessionDataStore +class InfinispanSessionDataStore +class JDBCSessionDataStore +class MongoSessionDataStore +class CachingSessionDataStore + + +SessionDataStore <|-- AbstractSessionDataStore +AbstractSessionDataStore <|-- NullSessionDataStore +AbstractSessionDataStore <|-- FileSessionDataStore +AbstractSessionDataStore <|-- GCloudSessionDataStore +AbstractSessionDataStore <|-- HazelcastSessionDataStore +AbstractSessionDataStore <|-- InfinispanSessionDataStore +AbstractSessionDataStore <|-- JDBCSessionDataStore +AbstractSessionDataStore <|-- MongoSessionDataStore +SessionDataStore <|-- CachingSessionDataStore +---- + +NullSessionDataStore:: +Does not store `SessionData`, meaning that sessions will exist in-memory only. +See <> + +FileSessionDataStore:: +Uses the file system to persist `SessionData`. +See <> for more information. + +GCloudSessionDataStore:: +Uses GCloud Datastore for persisting `SessionData`. +See <> for more information. + +HazelcastSessionDataStore:: +Uses Hazelcast for persisting `SessionData`. + +InfinispanSessionDataStore:: +Uses http://infinispan.org[Infinispan] for persisting `SessionData`. +See <> for more information. + +JDBCSessionDataStore:: +Uses a relational database via JDBC API to persist `SessionData`. +See <> for more information. + +MongoSessionDataStore:: +Uses http://www.mongodb.com[MongoDB] document database to persist `SessionData`. +See <> for more information. + +CachingSessionDataStore:: +Uses http://memcached.org[memcached] to provide an L2 cache of `SessionData` while delegating to another `SessionDataStore` for persistence of `SessionData`. +See <> for more information. + +Most of the behaviour common to ``SessionDataStore``s is provided by the link:{javadoc-url}/org/eclipse/jetty/session/AbstractSessionDataStore.html[AbstractSessionDataStore] class. +You are strongly encouraged to use this as the base class for implementing your custom `SessionDataStore`. + +Some important methods are: + +isPassivating():: +Boolean. "True" means that session data is _serialized_. +Some persistence mechanisms serialize, such as JDBC, GCloud Datastore etc. +Others can store an object in shared memory, e.g. Infinispan and thus don't serialize session data. +In Servlet environments, whether a `SessionDataStore` reports that it is capable of passivating controls whether ``HttpSessionActivationListener``s will be called. +When implementing a custom `SessionDataStore` you need to decide whether you will support passivation or not. + +[[datastore-skip]] +//tag::common-datastore-config[] +setSavePeriodSec(int) _[Default:0]_ :: +This is an interval defined in seconds. +It is used to reduce the frequency with which `SessionData` is written. +Normally, whenever the last concurrent request leaves a `Session`, the `SessionData` for that `Session` is always persisted, even if the only thing that changed is the `lastAccessTime`. +If the `savePeriodSec` is non-zero, the `SessionData` will not be persisted if no session attributes changed, _unless_ the time since the last save exceeds the `savePeriod`. +Setting a non-zero value can reduce the load on the persistence mechanism, but in a clustered environment runs the risk that other nodes will see the session as expired because it has not been persisted sufficiently recently. + +setGracePeriodSec(int) _[Default:3600]_ :: +The `gracePeriod` is an interval defined in seconds. +It is an attempt to deal with the non-transactional nature of sessions with regard to finding sessions that have expired. +In a clustered configuration - even with a sticky load balancer - it is always possible that a session is "live" on a node but not yet updated in the persistent store. +This means that it can be hard to determine at any given moment whether a clustered session has truly expired. +Thus, we use the `gracePeriod` to provide a bit of leeway around the moment of expiry during <>: + +* on every <> cycle an `AbstractSessionDataStore` searches for sessions that belong to the context that expired at least one `gracePeriod` ago +* infrequently the `AbstractSessionDataStore` searches for and summarily deletes sessions - from any context - that expired at least 10 ``gracePeriod``s ago +//end::common-datastore-config[] + +=== Custom SessionDataStores +When implementing a `SessionDataStore` for a particular persistence technology, you should base it off the `AbstractSessionDataStore` class. + +Firstly, it is important to understand the components of a unique key for a session suitable for storing in a persistence mechanism. +Consider that although multiple contexts may share the _same_ session id (ie cross-context dispatch), the data in those sessions must be distinct. +Therefore, when storing session data in a persistence mechanism that is shared by many nodes in a cluster, the session must be identified by a combination of the id _and_ the context. + +The ``SessionDataStore``s use the following information to synthesize a unique key for session data that is suitable to the particular persistence mechanism : +[[key]] +id:: +This is the id as generated by the `SessionIdManager` +context:: +The path of the context associated with the session. +virtual host:: +The first virtual host - if any - associated with the context. + +The link:{javadoc-url}/org/eclipse/jetty/session/SessionContext.html[SessionContext] class, of which every `AbstractSessionDataStore` has an instance, will provide these components to you in a canonicalized form. + +Then you will need to implement the following methods: + +public boolean doExists(String id):: +Check if data for the given session exists in your persistence mechanism. +The id is always relative to the context, see <>. + +public void doStore(String id, SessionData data, long lastSaveTime):: +Store the session data into your persistence mechanism. +The id is always relative to the context, see <>. + +public SessionData doLoad(String id):: +Load the session from your persistent mechanism. +The id is always relative to the context, see <>. + +public Set doCheckExpired(Set candidates, long time):: +Verify which of the suggested session ids have expired since the time given, according to the data stored in your persistence mechanism. +This is used during scavenging to ensure that a session that is a candidate for expiry according to _this_ node is not in-use on _another_ node. +The sessions matching these ids will be loaded as ``ManagedSession``s and have their normal expiration lifecycle events invoked. +The id is always relative to the context, see <>. + +public Set doGetExpired(long before):: +Find the ids of sessions that expired at or before the time given. +The sessions matching these ids will be loaded as ``ManagedSession``s and have their normal expiration lifecycle events invoked. +The id is always relative to the context, see <>. + +public void doCleanOrphans(long time):: +Find the ids of sessions that expired at or before the given time, _independent of the context they are in_. +The purpose is to find sessions that are no longer being managed by any node. +These sessions may even belong to contexts that no longer exist. +Thus, any such sessions must be summarily deleted from the persistence mechanism and cannot have their normal expiration lifecycle events invoked. + +=== The SessionDataStoreFactory + +Every `SessionDataStore` has a factory class that creates instances based on common configuration. + +All `SessionDataStoreFactory` implementations support configuring: + +setSavePeriodSec(int):: +setGracePeriodSec(int):: + +[[datastore-null]] +=== The NullSessionDataStore + +The `NullSessionDataStore` is a trivial implementation of `SessionDataStore` that does not persist `SessionData`. +Use it when you want your sessions to remain in memory _only_. +Be careful of your `SessionCache` when using the `NullSessionDataStore`: + +* if using a `NullSessionCache` then your sessions are neither shared nor saved +* if using a `DefaultSessionCache` with eviction settings, your session will cease to exist when it is evicted from the cache + +If you have not configured any other <>, when a `SessionHandler` aka `AbstractSessionManager` starts up, it will instantiate a `NullSessionDataStore`. + +[[datastore-file]] +=== The FileSessionDataStore + +The `FileSessionDataStore` supports persistent storage of session data in a filesystem. + +IMPORTANT: Persisting sessions to the local file system should *never* be used in a clustered environment. + +One file represents one session in one context. + +File names follow this pattern: + + [expiry]_[contextpath]_[virtualhost]_[id] + +expiry:: +This is the expiry time in milliseconds since the epoch. + +contextpath:: +This is the context path with any special characters, including `/`, replaced by the `_` underscore character. +For example, a context path of `/catalog` would become `_catalog`. +A context path of simply `/` becomes just `__`. + +virtualhost:: +This is the first virtual host associated with the context and has the form of 4 digits separated by `.` characters. +If there are no virtual hosts associated with a context, then `0.0.0.0` is used: + + [digit].[digit].[digit].[digit] + +id:: +This is the unique id of the session. + +Putting all of the above together as an example, a session with an id of `node0ek3vx7x2y1e7pmi3z00uqj1k0` for the context with path `/test` with no virtual hosts and an expiry of `1599558193150` would have a file name of: + +`1599558193150__test_0.0.0.0_node0ek3vx7x2y1e7pmi3z00uqj1k0` + + +You can configure either a link:{javadoc-url}/org/eclipse/jetty/session/FileSessionDataStore.html[FileSessionDataStore] individually, or a `FileSessionDataStoreFactory` if you want multiple ``SessionHandler``s to use ``FileSessionDataStore``s that are identically configured. +The configuration methods are: + +setStoreDir(File) _[Default:null]_ :: +This is the location for storage of session files. +If the directory does not exist at startup, it will be created. +If you use the same `storeDir` for multiple `SessionHandlers`, then the sessions for all of those contexts are stored in the same directory. +This is not a problem, as the name of the file is unique because it contains the context information. +You _must_ supply a value for this, otherwise startup of the `FileSessionDataStore` will fail. + +deleteUnrestorableFiles(boolean) _[Default:false]_ :: + +If set to `true`, unreadable files will be deleted. +This is useful to prevent repeated logging of the same error when the <> periodically (re-)attempts to load the corrupted information for a session in order to expire it. + +setSavePeriodSec(int) _[Default:0]_ :: +This is an interval defined in seconds. +It is used to reduce the frequency with which `SessionData` is written. +Normally, whenever the last concurrent request leaves a `Session`, the `SessionData` for that `Session` is always persisted, even if the only thing that changed is the `lastAccessTime`. +If the `savePeriodSec` is non-zero, the `SessionData` will not be persisted if no session attributes changed, _unless_ the time since the last save exceeds the `savePeriod`. +Setting a non-zero value can reduce the load on the persistence mechanism, but in a clustered environment runs the risk that other nodes will see the session as expired because it has not been persisted sufficiently recently. + +setGracePeriodSec(int) _[Default:3600]_ :: +The `gracePeriod` is an interval defined in seconds. +It is an attempt to deal with the non-transactional nature of sessions with regard to finding sessions that have expired. +In a clustered configuration - even with a sticky load balancer - it is always possible that a session is "live" on a node but not yet updated in the persistent store. +This means that it can be hard to determine at any given moment whether a clustered session has truly expired. +Thus, we use the `gracePeriod` to provide a bit of leeway around the moment of expiry during <>: + +* on every <> cycle an `AbstractSessionDataStore` searches for sessions that belong to the context that expired at least one `gracePeriod` ago +* infrequently the `AbstractSessionDataStore` searches for and summarily deletes sessions - from any context - that expired at least 10 ``gracePeriod``s ago + +Here's an example of configuring a `FileSessionDataStoreFactory`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=filesessiondatastorefactory] +---- + +Here's an alternate example, configuring a `FileSessionDataStore` directly: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=filesessiondatastore] +---- + +[[datastore-jdbc]] +=== The JDBCSessionDataStore + +The link:{javadoc-url}/org/eclipse/jetty/session/JDBCSessionDataStore.html[JDBCSessionDataStore] supports persistent storage of session data in a relational database. +To do that, it requires a `DatabaseAdaptor` that handles the differences between databases (eg Oracle, Postgres etc), and a `SessionTableSchema` that allows for the customization of table and column names. + +[plantuml] +---- +class JDBCSessionDataStore +class DatabaseAdaptor +class SessionTableSchema + +JDBCSessionDataStore "1" *-- "1" DatabaseAdaptor +JDBCSessionDataStore "1" *-- "1" SessionTableSchema +---- + + +The link:{javadoc-url}/org/eclipse/jetty/session/JDBCSessionDataStore.html[JDBCSessionDataStore] and corresponding link:{javadoc-url}/org/eclipse/jetty/session/JDBCSessionDataStoreFactory.html[JDBCSessionDataStoreFactory] support the following configuration: + +setSavePeriodSec(int) _[Default:0]_ :: +This is an interval defined in seconds. +It is used to reduce the frequency with which `SessionData` is written. +Normally, whenever the last concurrent request leaves a `Session`, the `SessionData` for that `Session` is always persisted, even if the only thing that changed is the `lastAccessTime`. +If the `savePeriodSec` is non-zero, the `SessionData` will not be persisted if no session attributes changed, _unless_ the time since the last save exceeds the `savePeriod`. +Setting a non-zero value can reduce the load on the persistence mechanism, but in a clustered environment runs the risk that other nodes will see the session as expired because it has not been persisted sufficiently recently. + +setGracePeriodSec(int) _[Default:3600]_ :: +The `gracePeriod` is an interval defined in seconds. +It is an attempt to deal with the non-transactional nature of sessions with regard to finding sessions that have expired. +In a clustered configuration - even with a sticky load balancer - it is always possible that a session is "live" on a node but not yet updated in the persistent store. +This means that it can be hard to determine at any given moment whether a clustered session has truly expired. +Thus, we use the `gracePeriod` to provide a bit of leeway around the moment of expiry during <>: + +* on every <> cycle an `AbstractSessionDataStore` searches for sessions that belong to the context that expired at least one `gracePeriod` ago +* infrequently the `AbstractSessionDataStore` searches for and summarily deletes sessions - from any context - that expired at least 10 ``gracePeriod``s ago + +setDatabaseAdaptor(DatabaseAdaptor):: +A `JDBCSessionDataStore` requires a `DatabaseAdapter`, otherwise an `Exception` is thrown at start time. + +setSessionTableSchema(SessionTableSchema):: +If a `SessionTableSchema` has not been explicitly set, one with all values defaulted is created at start time. + +==== The DatabaseAdaptor + +Many databases use different keywords for types such as `long`, `blob` and `varchar`. +Jetty will detect the type of the database at runtime by interrogating the metadata associated with a database connection. +Based on that metadata Jetty will try to select that database's preferred keywords. +However, you may need to instead explicitly configure these as described below. + +setDatasource(String):: +setDatasource(Datasource):: +Either the JNDI name of a `Datasource` to look up, or the `Datasource` itself. +Alternatively you can set the *driverInfo*, see below. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=dbaDatasource] +---- + +setDriverInfo(String, String):: +setDriverInfo(Driver, String):: +This is the name or instance of a `Driver` class and a connection URL. +Alternatively you can set the *datasource*, see above. +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=dbaDriver] +---- + +setBlobType(String) _[Default: "blob" or "bytea" for Postgres]_ :: +The type name used to represent "blobs" by the database. + +setLongType(String) _[Default: "bigint" or "number(20)" for Oracle]_ :: +The type name used to represent large integers by the database. + +setStringType(String) _[Default: "varchar"]_:: +The type name used to represent character data by the database. + + +==== The SessionTableSchema + +`SessionData` is stored in a table with one row per session. +This is the definition of the table with the table name, column names, and type keywords all at their default settings: + +[caption="Table:"] +.JettySessions +[frame=all] +[cols=12*,options="header"] +|=== +|sessionId +|contextPath +|virtualHost +|lastNode +|accessTime +|lastAccessTime +|createTime +|cookieTime +|lastSavedTime +|expiryTime +|maxInterval +|map +|120 varchar|60 varchar|60 varchar|60 varchar|long|long|long|long|long|long|long|blob +|=== + +Use the `SessionTableSchema` class to customize these names. + +setSchemaName(String), setCatalogName(String) _[Default: null]_ :: +The exact meaning of these two are dependent on your database vendor, but can broadly be described as further scoping for the session table name. +See https://en.wikipedia.org/wiki/Database_schema and https://en.wikipedia.org/wiki/Database_catalog. +These extra scoping names come into play at startup time when Jetty determines if the session table already exists, or creates it on-the-fly. +If your database is not using schema or catalog name scoping, leave these unset. +If your database is configured with a schema or catalog name, use the special value "INFERRED" and Jetty will extract them from the database metadata. +Alternatively, set them explicitly using these methods. + +setTableName(String) _[Default:"JettySessions"]_ :: +This is the name of the table in which session data is stored. + +setAccessTimeColumn(String) _[Default: "accessTime"]_ :: +This is the name of the column that stores the time - in ms since the epoch - at which a session was last accessed + +setContextPathColumn(String) _[Default: "contextPath"]_ :: +This is the name of the column that stores the `contextPath` of a session. + +setCookieTimeColumn(String) _[Default: "cookieTime"]_:: +This is the name of the column that stores the time - in ms since the epoch - that the cookie was last set for a session. + +setCreateTimeColumn(String) _[Default: "createTime"]_ :: +This is the name of the column that stores the time - in ms since the epoch - at which a session was created. + +setExpiryTimeColumn(String) _[Default: "expiryTime"]_ :: +This is name of the column that stores - in ms since the epoch - the time at which a session will expire. + +setLastAccessTimeColumn(String) _[Default: "lastAccessTime"]_ :: +This is the name of the column that stores the time - in ms since the epoch - that a session was previously accessed. + +setLastSavedTimeColumn(String) _[Default: "lastSavedTime"]_ :: +This is the name of the column that stores the time - in ms since the epoch - at which a session was last written. + +setIdColumn(String) _[Default: "sessionId"]_ :: +This is the name of the column that stores the id of a session. + +setLastNodeColumn(String) _[Default: "lastNode"]_ :: +This is the name of the column that stores the `workerName` of the last node to write a session. + +setVirtualHostColumn(String) _[Default: "virtualHost"]_ :: +This is the name of the column that stores the first virtual host of the context of a session. + +setMaxIntervalColumn(String) _[Default: "maxInterval"]_ :: +This is the name of the column that stores the interval - in ms - during which a session can be idle before being considered expired. + +setMapColumn(String) _[Default: "map"]_ :: +This is the name of the column that stores the serialized attributes of a session. + +[[datastore-mongo]] +=== The MongoSessionDataStore + +The `MongoSessionDataStore` supports persistence of `SessionData` in a nosql database. + +The best description for the document model for session information is found in the javadoc for the link:{javadoc-url}/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.html[MongoSessionDataStore]. +In overview, it can be represented thus: + +[plantuml] +---- +database HttpSessions { + folder jettySessions { + file session { + file "context" { + rectangle attributes + } + } + } +} +---- + +The database contains a document collection for the sessions. +Each document represents a session id, and contains one nested document per context in which that session id is used. +For example, the session id `abcd12345` might be used by two contexts, one with path `/contextA` and one with path `/contextB`. +In that case, the outermost document would refer to `abcd12345` and it would have a nested document for `/contextA` containing the session attributes for that context, and another nested document for `/contextB` containing the session attributes for that context. +Remember, according to the Servlet Specification, a session id can be shared by many contexts, but the attributes must be unique per context. + +The outermost document contains these fields: + +id:: +The session id. +created:: +The time (in ms since the epoch) at which the session was first created in any context. +maxIdle:: +The time (in ms) for which an idle session is regarded as valid. +As maxIdle times can be different for ``Session``s from different contexts, this is the _shortest_ maxIdle time. +expiry:: +The time (in ms since the epoch) at which the session will expire. +As the expiry time can be different for ``Session``s from different contexts, this is the _shortest_ expiry time. + +Each nested context-specific document contains: + +attributes:: +The session attributes as a serialized map. +lastSaved:: +The time (in ms since the epoch) at which the session in this context was saved. +lastAccessed:: +The time (in ms since the epoch) at which the session in this context was previously accessed. +accessed:: +The time (in ms since the epoch) at which this session was most recently accessed. +lastNode:: +The <> of the last server that saved the session data. +version:: +An object that is updated every time a session is written for a context. + +You can configure either a link:{javadoc-url}/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStore.html[MongoSessionDataStore] individually, or a link:{javadoc-url}/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreFactory.html[MongoSessionDataStoreFactory] if you want multiple ``SessionHandler``s to use ``MongoSessionDataStore``s that are identically configured. +The configuration methods for the `MongoSessionDataStoreFactory` are: + +setSavePeriodSec(int) _[Default:0]_ :: +This is an interval defined in seconds. +It is used to reduce the frequency with which `SessionData` is written. +Normally, whenever the last concurrent request leaves a `Session`, the `SessionData` for that `Session` is always persisted, even if the only thing that changed is the `lastAccessTime`. +If the `savePeriodSec` is non-zero, the `SessionData` will not be persisted if no session attributes changed, _unless_ the time since the last save exceeds the `savePeriod`. +Setting a non-zero value can reduce the load on the persistence mechanism, but in a clustered environment runs the risk that other nodes will see the session as expired because it has not been persisted sufficiently recently. + +setGracePeriodSec(int) _[Default:3600]_ :: +The `gracePeriod` is an interval defined in seconds. +It is an attempt to deal with the non-transactional nature of sessions with regard to finding sessions that have expired. +In a clustered configuration - even with a sticky load balancer - it is always possible that a session is "live" on a node but not yet updated in the persistent store. +This means that it can be hard to determine at any given moment whether a clustered session has truly expired. +Thus, we use the `gracePeriod` to provide a bit of leeway around the moment of expiry during <>: + +* on every <> cycle an `AbstractSessionDataStore` searches for sessions that belong to the context that expired at least one `gracePeriod` ago +* infrequently the `AbstractSessionDataStore` searches for and summarily deletes sessions - from any context - that expired at least 10 ``gracePeriod``s ago + +setDbName(String):: +This is the name of the database. +setCollectionName(String):: +The name of the document collection. + +setConnectionString(String):: +a mongodb url, eg "mongodb://localhost". +Alternatively, you can specify the *host,port* combination instead, see below. +setHost(String):: +setPort(int):: +the hostname and port number of the mongodb instance to contact. +Alternatively, you can specify the *connectionString* instead, see above. + +This is an example of configuring a `MongoSessionDataStoreFactory`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=mongosdfactory] +---- + +[[datastore-infinispan]] +=== The InfinispanSessionDataStore + +The `InfinispanSessionDataStore` supports persistent storage of session data via the https://infinispan.org/[Infinispan] data grid. + +You may use Infinispan in either _embedded mode_, where it runs in the same process as Jetty, or in _remote mode_ mode, where your Infinispan instance is on another node. + +For more information on Infinispan, including some code examples, consult the https://infinispan.org/[Infinispan documentation]. +See below for some code examples of configuring the link:{javadoc-url}/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStore.html[InfinispanSessionDataStore] in Jetty. +Note that the configuration options are the same for both the `InfinispanSessionDataStore` and the link:{javadoc-url}/org/eclipse/jetty/session/infinispan/InfinispanSessionDataStoreFactory.html[InfinispanSessionDataStoreFactory]. +Use the latter to apply the same configuration to multiple ``InfinispanSessionDataStore``s. + +setSavePeriodSec(int) _[Default:0]_ :: +This is an interval defined in seconds. +It is used to reduce the frequency with which `SessionData` is written. +Normally, whenever the last concurrent request leaves a `Session`, the `SessionData` for that `Session` is always persisted, even if the only thing that changed is the `lastAccessTime`. +If the `savePeriodSec` is non-zero, the `SessionData` will not be persisted if no session attributes changed, _unless_ the time since the last save exceeds the `savePeriod`. +Setting a non-zero value can reduce the load on the persistence mechanism, but in a clustered environment runs the risk that other nodes will see the session as expired because it has not been persisted sufficiently recently. + +setGracePeriodSec(int) _[Default:3600]_ :: +The `gracePeriod` is an interval defined in seconds. +It is an attempt to deal with the non-transactional nature of sessions with regard to finding sessions that have expired. +In a clustered configuration - even with a sticky load balancer - it is always possible that a session is "live" on a node but not yet updated in the persistent store. +This means that it can be hard to determine at any given moment whether a clustered session has truly expired. +Thus, we use the `gracePeriod` to provide a bit of leeway around the moment of expiry during <>: + +* on every <> cycle an `AbstractSessionDataStore` searches for sessions that belong to the context that expired at least one `gracePeriod` ago +* infrequently the `AbstractSessionDataStore` searches for and summarily deletes sessions - from any context - that expired at least 10 ``gracePeriod``s ago + +setCache(BasicCache cache):: +Infinispan uses a cache API as the interface to the data grid and this method configures Jetty with the cache instance. +This cache can be either an _embedded_ cache - also called a "local" cache in Infinispan parlance - or a _remote_ cache. + +setSerialization(boolean) _[Default: false]_ :: +When the `InfinispanSessionDataStore` starts, if it detects the Infinispan classes for remote caches on the classpath, it will automatically assume `serialization` is true, and thus that `SessionData` will be serialized over-the-wire to a remote cache. +You can use this parameter to override this. +If this parameter is `true`, the `InfinispanSessionDataStore` returns true for the `isPassivating()` method, but false otherwise. + +setInfinispanIdleTimeoutSec(int) _[Default: 0]_ :: +This controls the Infinispan option whereby it can detect and delete entries that have not been referenced for a configurable amount of time. +A value of 0 disables it. + +NOTE: If you use this option, expired sessions will be summarily deleted from Infinispan _without_ the normal session invalidation handling (eg calling of lifecycle listeners). +Only use this option if you do not have session lifecycle listeners that must be called when a session is invalidated. + +setQueryManager(QueryManager):: +If this parameter is not set, the `InfinispanSessionDataStore` will be unable to scavenge for unused sessions. +In that case, you can use the `infinispanIdleTimeoutSec` option instead to prevent the accumulation of expired sessions. +When using Infinispan in _embedded_ mode, configure the link:{javadoc-url}/org/eclipse/jetty/session/infinispan/EmbeddedQueryManager.html[EmbeddedQueryManager] to enable Jetty to query for expired sessions so that they may be property invalidated and lifecycle listeners called. +When using Infinispan in _remote_ mode, configure the link:{javadoc-url}/org/eclipse/jetty/session/infinispan/RemoteQueryManager.html[RemoteQueryManager] instead. + +Here is an example of configuring an `InfinispanSessionDataStore` in code using an _embedded_ cache: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=infinispanembed] +---- + +Here is an example of configuring an `InfinispanSessionDataStore` in code using a _remote_ cache: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=infinispanremote] +---- + +[[datastore-gcloud]] +=== The GCloudSessionDataStore + +The `GCloudSessionDataStore` supports persistent storage of session data into https://cloud.google.com/datastore[Google Cloud DataStore]. + +[[datastore-gcloud-prep]] +==== Preparation + +You will first need to create a project and enable the Google Cloud API: https://cloud.google.com/docs/authentication#preparation[]. +Take note of the `project id` that you create in this step as you need to supply it in later steps. + +You can choose to use Jetty either inside or outside of Google infrastructure. + +. Outside of Google infrastructure ++ +Before running Jetty, you will need to choose one of the following methods to set up the local environment to enable remote GCloud DataStore communications: + +.. Using the GCloud SDK + * Ensure you have the GCloud SDK installed: https://cloud.google.com/sdk/?hl=en[] + * Use the GCloud tool to set up the project you created in the preparation step: `gcloud config set project PROJECT_ID` + * Use the GCloud tool to authenticate a Google account associated with the project created in the preparation step: `gcloud auth login ACCOUNT` + + .. Using environment variables + * Define the environment variable `GCLOUD_PROJECT` with the project id you created in the preparation step. + * Generate a JSON https://cloud.google.com/storage/docs/authentication?hl=en#service_accounts[service account key] and then define the environment variable `GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/key.json` + +. Inside of Google infrastructure ++ +The Google deployment tools will automatically configure the project and authentication information for you. + +Jetty GCloud session support provides some indexes as optimizations that can speed up session searches. +This will particularly benefit session scavenging, although it may make write operations slower. +By default, indexes will _not_ be used. +You will see a log `WARNING` message informing you about the absence of indexes: + + WARN: Session indexes not uploaded, falling back to less efficient queries + +In order to use them, you will need to manually upload the file to GCloud that defines the indexes. +This file is named `index.yaml` and you can find it in your distribution in `$JETTY_BASE/etc/sessions/gcloud/index.yaml`. + +Follow the instructions https://cloud.google.com/datastore/docs/tools/#the_development_workflow_using_gcloud[here] to upload the pre-generated `index.yaml` file. + +==== Configuration + +The following configuration options apply to both the link:{javadoc-url}/org/eclipse/jetty/session/GCloudSessionDataStore.html[GCloudSessionDataStore] and the link:{javadoc-url}/org/eclipse/jetty/session/GCloudSessionDataStoreFactory.html[GCloudSessionDataStoreFactory]. +Use the latter if you want multiple ``SessionHandler``s to use ``GCloudSessionDataStore``s that are identically configured. + +setSavePeriodSec(int) _[Default:0]_ :: +This is an interval defined in seconds. +It is used to reduce the frequency with which `SessionData` is written. +Normally, whenever the last concurrent request leaves a `Session`, the `SessionData` for that `Session` is always persisted, even if the only thing that changed is the `lastAccessTime`. +If the `savePeriodSec` is non-zero, the `SessionData` will not be persisted if no session attributes changed, _unless_ the time since the last save exceeds the `savePeriod`. +Setting a non-zero value can reduce the load on the persistence mechanism, but in a clustered environment runs the risk that other nodes will see the session as expired because it has not been persisted sufficiently recently. + +setGracePeriodSec(int) _[Default:3600]_ :: +The `gracePeriod` is an interval defined in seconds. +It is an attempt to deal with the non-transactional nature of sessions with regard to finding sessions that have expired. +In a clustered configuration - even with a sticky load balancer - it is always possible that a session is "live" on a node but not yet updated in the persistent store. +This means that it can be hard to determine at any given moment whether a clustered session has truly expired. +Thus, we use the `gracePeriod` to provide a bit of leeway around the moment of expiry during <>: + +* on every <> cycle an `AbstractSessionDataStore` searches for sessions that belong to the context that expired at least one `gracePeriod` ago +* infrequently the `AbstractSessionDataStore` searches for and summarily deletes sessions - from any context - that expired at least 10 ``gracePeriod``s ago + +setProjectId(String) _[Default: null]_ :: +Optional. +The `project id` of your project. +You don't need to set this if you carried out the instructions in the <> section, but you might want to set this - along with the `host` and/or `namespace` parameters - if you want more explicit control over connecting to GCloud. + +setHost(String) _[Default: null]_ :: +Optional. +This is the name of the host for the GCloud DataStore. +If you leave it unset, then the GCloud DataStore library will work out the host to contact. +You might want to use this - along with `projectId` and/or `namespace` parameters - if you want more explicit control over connecting to GCloud. + +setNamespace(String) _[Default: null]_ :: +Optional. +If set, partitions the visibility of session data in multi-tenant deployments. +More information can be found https://cloud.google.com/datastore/docs/concepts/multitenancy[here.] + +setMaxRetries(int) _[Default: 5]_ :: +This is the maximum number of retries to connect to GCloud DataStore in order to write a session. +This is used in conjunction with the `backoffMs` parameter to control the frequency with which Jetty will retry to contact GCloud to write out a session. + +setBackoffMs(int) _[Default: 1000]_ :: +This is the interval that Jetty will wait in between retrying failed writes. +Each time a write fails, Jetty doubles the previous backoff. +Used in conjunction with the `maxRetries` parameter. + +setEntityDataModel(EntityDataModel):: +The `EntityDataModel` encapsulates the type (called "kind" in GCloud DataStore) of stored session objects and the names of its fields. +If you do not set this parameter, `GCloudSessionDataStore` uses all default values, which should be sufficient for most needs. +Should you need to customize this, the methods and their defaults are: + * *setKind(String)* _[Default: "GCloudSession"]_ this is the type of the session object. + * *setId(String)* _[Default: "id"]_ this is the name of the field storing the session id. + * *setContextPath(String)* _[Default: "contextPath"]_ this is name of the field storing the canonicalized context path of the context to which the session belongs. + * *setVhost(String)* _[Default: "vhost"]_ this the name of the field storing the canonicalized virtual host of the context to which the session belongs. + * *setAccessed(String)* _[Default: "accessed"]_ this is the name of the field storing the current access time of the session. + * *setLastAccessed(String)* _[Default: "lastAccessed"]_ this is the name of the field storing the last access time of the session. + * *setCreateTime(String)* _[Default: "createTime"]_ this is the name of the field storing the time in ms since the epoch, at which the session was created. + * *setCookieSetTime(String)* _[Default: "cookieSetTime"]_ this is the name of the field storing time at which the session cookie was last set. + * *setLastNode(String)* _[Default: "lastNode"]_ this is the name of the field storing the `workerName` of the last node to manage the session. + * *setExpiry(String)* _[Default: "expiry"]_ this is the name of the field storing the time, in ms since the epoch, at which the session will expire. + * *setMaxInactive(String)* _[Default: "maxInactive"]_ this is the name of the field storing the session timeout in ms. + * *setAttributes(String)* _[Default: "attributes"]_ this is the name of the field storing the session attribute map. + + +Here's an example of configuring a `GCloudSessionDataStoreFactory`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=gcloudsessiondatastorefactory] +---- + +[[cachingsessiondatastore]] +=== The CachingSessionDataStore + +[plantuml] +---- +interface SessionDataMap +class CachingSessionDataStore +interface SessionDataStore + +CachingSessionDataStore "1" *-down- "1" SessionDataMap +CachingSessionDataStore "1" *-down- "1" SessionDataStore +SessionDataMap <|-- MemcachedSessionDataMap +---- + +The link:{javadoc-url}/org/eclipse/jetty/session/CachingSessionDataStore.html[CachingSessionDataStore] is a special type of `SessionDataStore` that checks an L2 cache for `SessionData` before checking a delegate `SessionDataStore`. +This can improve the performance of slow stores. + +The L2 cache is an instance of a link:{javadoc-url}/org/eclipse/jetty/session/SessionDataMap.html[SessionDataMap]. +Jetty provides one implementation of this L2 cache based on `memcached`, link:{javadoc-url}/org/eclipse/jetty/memcached/session/MemcachedSessionDataMap.html[MemcachedSessionDataMap]. + +This is an example of how to programmatically configure ``CachingSessionDataStore``s, using a <> as a delegate, and `memcached` as the L2 cache: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/session/SessionDocs.java[tags=cachingsds] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/server/websocket.adoc b/documentation/jetty/modules/programming-guide/pages/server/websocket.adoc new file mode 100644 index 000000000000..fe3371e6da01 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/server/websocket.adoc @@ -0,0 +1,343 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += WebSocket Server + +Jetty provides different implementations of the WebSocket protocol: + +* A Jakarta EE 8 (`javax.websocket`) implementation, based on the https://jakarta.ee/specifications/websocket/1.1/[Jakarta WebSocket 1.1 Specification]. +* A Jakarta EE 9 (`jakarta.websocket`) implementation, based on the https://jakarta.ee/specifications/websocket/2.0/[Jakarta WebSocket 2.0 Specification]. +* A Jakarta EE 10 (`jakarta.websocket`) implementation, based on the https://jakarta.ee/specifications/websocket/2.1/[Jakarta WebSocket 2.1 Specification]. +* A Jetty specific implementation, based on the Jetty WebSocket APIs, that does not depend on any Jakarta EE APIs. + +The Jakarta EE implementations and APIs are described in <>. + +Using the standard Jakarta EE WebSocket APIs allows your applications to depend only on standard APIs, and your applications may be deployed in any compliant WebSocket Container that supports Jakarta WebSocket. +The standard Jakarta EE WebSocket APIs provide these features that are not present in the Jetty WebSocket APIs: + +* Encoders and Decoders for automatic conversion of text or binary messages to objects. + +The Jetty specific WebSocket implementation and APIs are described in <>. + +Using the Jetty WebSocket APIs allows your applications to be more efficient and offer greater and more fine-grained control, and provide these features that are not present in the Jakarta EE WebSocket APIs: + +* A demand mechanism to control backpressure. +* Remote socket address (IP address and port) information. +* Advanced request URI matching with regular expressions, in addition to Servlet patterns and URI template patterns. +* More configuration options, for example the network buffer capacity. +* Programmatic WebSocket upgrade, in addition to WebSocket upgrade based on URI matching, for maximum flexibility. + +If your application needs specific features that are not provided by the standard APIs, the Jetty WebSocket APIs may provide such features. + +TIP: If the feature you are looking for is not present, you may ask for these features by https://github.com/eclipse/jetty.project/issues[submitting an issue] to the Jetty Project without waiting for the standard Jakarta EE process to approve them and release a new version of the Jakarta EE WebSocket specification. + +[[standard]] +== Standard APIs Implementation + +When you write a WebSocket application using the standard `jakarta.websocket` APIs, your code typically need to depend on just the APIs to compile your application. +However, at runtime you need to have an implementation of the standard APIs in your class-path (or module-path). + +The standard `jakarta.websocket` APIs, for example for Jakarta {ee-current-caps}, are provided by the following Maven artifact: + +[,xml] +---- + + jakarta.websocket + jakarta.websocket-api + 2.1.0 + +---- + +At runtime, you also need an implementation of the standard Jakarta {ee-current-caps} WebSocket APIs, that Jetty provides with the following Maven artifact (and its transitive dependencies): + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.{ee-current}.websocket + jetty-{ee-current}-websocket-jakarta-server + {version} + +---- + +[NOTE] +==== +The `jakarta.websocket-api` artifact and the `jetty-{ee-current}-websocket-jakarta-server` artifact (and their transitive dependencies) should be present in the server class-path (or module-path), and never in the web application's `/WEB-INF/lib` directory. +==== + +To configure correctly your WebSocket application based on the standard Jakarta {ee-current-caps} WebSocket APIs, you need two steps: + +. Make sure that Jetty sets up an instance of `jakarta.websocket.server.ServerContainer`, described in <>. +. Configure the WebSocket endpoints that implement your application logic, either by annotating their classes with the standard `jakarta.websocket` annotations, or by using the `ServerContainer` APIs to register them in your code, described in <>. + +[[standard-container]] +=== Setting Up `ServerContainer` + +Jetty sets up a `ServerContainer` instance using `JakartaWebSocketServletContainerInitializer`. + +When you deploy web applications using xref:server/http.adoc#handler-use-webapp-context[`WebAppContext`], then `JakartaWebSocketServletContainerInitializer` is automatically discovered and initialized by Jetty when the web application starts, so that it sets up the `ServerContainer`. +In this way, you do not need to write any additional code: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=standardContainerWebAppContext] +---- + +On the other hand, when you deploy web applications using xref:server/http.adoc#handler-use-servlet-context[`ServletContextHandler`], you have to write the code to ensure that the `JakartaWebSocketServletContainerInitializer` is initialized, so that it sets up the `ServerContainer`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=standardContainerServletContextHandler] +---- + +Calling `JakartaWebSocketServletContainerInitializer.configure(\...)` must be done _before_ the `ServletContextHandler` is started, and configures the Jakarta {ee-current-caps} WebSocket implementation for that web application context, making `ServerContainer` available to web applications. + +[[standard-endpoints]] +=== Configuring Endpoints + +Once you have <> the `ServerContainer`, you can configure your WebSocket endpoints. + +The WebSocket endpoints classes may be either annotated with the standard `jakarta.websocket` annotations, extend the `jakarta.websocket.Endpoint` abstract class, or implement the `jakarta.websocket.server.ServerApplicationConfig` interface. + +When you deploy web applications using xref:server/http.adoc#handler-use-webapp-context[`WebAppContext`], then annotated WebSocket endpoint classes are automatically discovered and registered. +In this way, you do not need to write any additional code; you just need to ensure that your WebSocket endpoint classes are present in the web application's `/WEB-INF/classes` directory, or in a `*.jar` file in `/WEB-INF/lib`. + +On the other hand, when you deploy web applications using xref:server/http.adoc#handler-use-webapp-context[`WebAppContext`] but you need to perform more advanced configuration of the `ServerContainer` or of the WebSocket endpoints, or when you deploy web applications using xref:server/http.adoc#handler-use-servlet-context[`ServletContextHandler`], you need to access the `ServerContainer` APIs. + +The `ServerContainer` instance is stored as a `ServletContext` attribute, so it can be retrieved when the `ServletContext` is initialized, either from a `ServletContextListener`, or from a Servlet `Filter`, or from an `HttpServlet`: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=standardEndpointsInitialization] +---- + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=standardWebSocketInitializerServlet] +---- + +When you deploy web applications using xref:server/http.adoc#handler-use-servlet-context[`ServletContextHandler`], you can alternatively use the code below to set up the `ServerContainer` and configure the WebSocket endpoints in one step: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=standardContainerAndEndpoints] +---- + +When the `ServletContextHandler` is started, the `Configurator` lambda (the second parameter passed to `JakartaWebSocketServletContainerInitializer.configure(\...)`) is invoked and allows you to explicitly configure the WebSocket endpoints using the standard APIs provided by `ServerContainer`. + +[[standard-upgrade]] +==== Upgrade to WebSocket + +Under the hood, `JakartaWebSocketServletContainerInitializer` installs the `org.eclipse.jetty.{ee-current}.websocket.servlet.WebSocketUpgradeFilter`, which is the component that intercepts HTTP requests to upgrade to WebSocket, and performs the upgrade from the HTTP protocol to the WebSocket protocol. + +[NOTE] +==== +The `WebSocketUpgradeFilter` is installed under the filter name corresponding to its class name (that is, the string `"org.eclipse.jetty.{ee-current}.websocket.servlet.WebSocketUpgradeFilter"`) and with a filter mapping of `/*`. + +Refer to the <> for more information. +==== + +With the default configuration, every HTTP request flows first through the `WebSocketUpgradeFilter`. + +If the HTTP request is a valid upgrade to WebSocket, then `WebSocketUpgradeFilter` tries to find a matching WebSocket endpoint for the request URI path; if the match is found, `WebSocketUpgradeFilter` performs the upgrade and does not invoke any other Filter or Servlet. +From this point on, the communication happens with the WebSocket protocol, and HTTP components such as Filters and Servlets are not relevant anymore. + +If the HTTP request is not an upgrade to WebSocket, or `WebSocketUpgradeFilter` did not find a matching WebSocket endpoint for the request URI path, then the request is passed to the Filter chain of your web application, and eventually the request arrives to a Servlet to be processed (otherwise a `404 Not Found` response is returned to client). + +[[configure-filter]] +== Advanced `WebSocketUpgradeFilter` Configuration + +The `WebSocketUpgradeFilter` that handles the HTTP requests that upgrade to WebSocket is installed by the `JakartaWebSocketServletContainerInitializer`, as described in <>. + +Typically, the `WebSocketUpgradeFilter` is not present in the `web.xml` configuration, and therefore the mechanisms above create a new `WebSocketUpgradeFilter` and install it _before_ any other Filter declared in `web.xml`, under the default name of `"org.eclipse.jetty.{ee-current}.websocket.servlet.WebSocketUpgradeFilter"` and with path mapping `/*`. + +However, if the `WebSocketUpgradeFilter` is already present in `web.xml` under the default name, then the ``ServletContainerInitializer``s will use that declared in `web.xml` instead of creating a new one. + +This allows you to customize: + +* The filter order; for example, by configuring filters for increased security or authentication _before_ the `WebSocketUpgradeFilter`. +* The `WebSocketUpgradeFilter` configuration via ``init-param``s, that affects all `Session` instances created by this filter. +* The `WebSocketUpgradeFilter` path mapping. Rather than the default mapping of `+/*+`, you can map the `WebSocketUpgradeFilter` to a more specific path such as `+/ws/*+`. +* The possibility to have multiple ``WebSocketUpgradeFilter``s, mapped to different paths, each with its own configuration. + +For example: + +[,xml,subs=attributes+] +---- + + + My WebSocket WebApp + + + + security + com.acme.SecurityFilter + true + + + security + /* + + + + + + org.eclipse.jetty.{ee-current}.websocket.servlet.WebSocketUpgradeFilter + org.eclipse.jetty.{ee-current}.websocket.servlet.WebSocketUpgradeFilter + + + maxTextMessageSize + 1048576 + + true + + + org.eclipse.jetty.{ee-current}.websocket.servlet.WebSocketUpgradeFilter + + /ws/* + + + +---- +<1> The custom `SecurityFilter` is the first, to apply custom security. +<2> The configuration for the _default_ `WebSocketUpgradeFilter`. +<3> Note the use of the _default_ `WebSocketUpgradeFilter` name. +<4> Specific configuration for `WebSocketUpgradeFilter` parameters. +<5> Use a more specific path mapping for `WebSocketUpgradeFilter`. + +Note that using a more specific path mapping for WebSocket requests is also beneficial to the performance of normal HTTP requests: they do not go through the `WebSocketUpgradeFilter` (as they will not match its path mapping), saving the cost of analyzing them to see whether they are WebSocket upgrade requests or not. + +[[jetty]] +== Jetty APIs Implementation + +When you write a WebSocket application using the Jetty WebSocket APIs, your code typically needs to depend on just the Jetty WebSocket APIs to compile your application. +However, at runtime you need to have the _implementation_ of the Jetty WebSocket APIs in your class-path (or module-path). + +Jetty's WebSocket APIs are provided by the following Maven artifact: + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.websocket + jetty-websocket-jetty-api + {version} + +---- + +Jetty's implementation of the Jetty WebSocket APIs is provided by the following Maven artifact (and its transitive dependencies): + +[,xml,subs=attributes+] +---- + + org.eclipse.jetty.websocket + jetty-websocket-jetty-server + {version} + +---- + +[NOTE] +==== +The `jetty-websocket-jetty-api` artifact and the `jetty-websocket-jetty-server` artifact (and its transitive dependencies) should be present in the server class-path (or module-path), and never in a web application's `/WEB-INF/lib` directory. +==== + +To configure correctly your WebSocket application based on the Jetty WebSocket APIs, you need two steps: + +. Make sure to <> an instance of `org.eclipse.jetty.websocket.server.ServerWebSocketContainer`. +. Use the `ServerWebSocketContainer` APIs in your applications to <> that implement your application logic. + +You can read more about the xref:client/websocket.adoc#architecture[Jetty WebSocket architecture], which is common to both client-side and server-side, to get familiar with the terminology used in the following sections. + +[[jetty-container]] +=== Setting up `ServerWebSocketContainer` + +You need Jetty to set up a `ServerWebSocketContainer` instance to make your WebSocket applications based on the Jetty WebSocket APIs work. + +Your WebSocket web application is represented by a `ContextHandler`. +The WebSocket upgrade is performed in a descendant (typically the only child) of the `ContextHandler`, either by the `org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler`, or by a custom `Handler` that you write and is part of your web application. + +In both cases, you need to set up a `ServerWebSocketContainer`, and this can be done <> by using `WebSocketUpgradeHandler`, or <> by creating the `ServerWebSocketContainer` instance. + +[[jetty-container-websocket-handler]] +==== Implicit setup using `WebSocketUpgradeHandler` + +Using `WebSocketUpgradeHandler` is the most common way to set up your WebSocket applications. + +You can use the `WebSocketUpgradeHandler` and the `ServerWebSocketContainer` APIs to map HTTP request URIs to WebSocket endpoints. + +When an HTTP request arrives, `WebSocketUpgradeHandler` tests whether it is a WebSocket upgrade request, whether it matches a mapped URI, and if so upgrades the protocol to WebSocket. + +From this point on, the communication on the upgraded connection happens with the WebSocket protocol. +This is very similar to what <> does when using the Jakarta EE WebSocket APIs. + +Once you have set up the `WebSocketUpgradeHandler`, you can use the `ServerWebSocketContainer` APIs to configure the WebSocket endpoints. +The example below shows how to set up the `WebSocketUpgradeHandler` and use the `ServerWebSocketContainer` APIs: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=jettyContainerWithUpgradeHandler] +---- + +The mapping of request URIs to WebSocket endpoints is further explained in <>. + +[[jetty-container-websocket-container]] +==== Explicit setup using `ServerWebSocketContainer` + +A more advanced way to set up your WebSocket applications is to explicitly create the `ServerWebSocketContainer` instance programmatically. + +This gives you more flexibility when deciding whether an HTTP request should be upgraded to WebSocket, because you do not need to match request URIs (although you can), nor you need to use `WebSocketUpgradeHandler` (although you can). + +Once you have created the `ServerWebSocketContainer`, you can use its APIs to configure the WebSocket endpoints as shown in the example below. + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=jettyContainerWithContainer] +---- + +Note how the call to `ServerWebSocketContainer.upgrade(\...)` allows you to perform a direct WebSocket upgrade programmatically. + +[[jetty-endpoints]] +=== WebSocket Endpoints + +When using the Jetty WebSocket APIs, the WebSocket endpoint classes must be either annotated with the Jetty WebSocket annotations from the `org.eclipse.jetty.websocket.api.annotations` package, or implement the `org.eclipse.jetty.websocket.api.Session.Listener` interface. + +In the case you want to implement the `Session.Listener` interface, remember that you have to explicitly demand to receive the next WebSocket event. +Use `Session.Listener.AutoDemanding` to automate the demand for simple use cases. + +Refer to the Jetty WebSocket architecture xref:client/websocket.adoc#endpoints[section] for more information about Jetty WebSocket endpoints and how to correctly deal with the demand for WebSocket events. + +There is no automatic discovery of WebSocket endpoints; all the WebSocket endpoints of your application must be returned by a `org.eclipse.jetty.websocket.server.WebSocketCreator` that is either mapped to a request URI via `ServerWebSocketContainer.addMapping(\...)`, or directly upgraded via `ServerWebSocketContainer.upgrade(\...)`. + +In the call to `ServerWebSocketContainer.addMapping(\...)`, you can specify a _path spec_ (the first parameter) that can specified as discussed in <>. + +When the `Server` is started, the lambda passed to `ServerWebSocketContainer.configure(\...)`) is invoked and allows you to explicitly configure the WebSocket endpoints using the Jetty WebSocket APIs provided by `ServerWebSocketContainer`. + +[[jetty-pathspec]] +==== Custom PathSpec Mappings + +The `ServerWebSocketContainer.addMapping(\...)` API maps a _path spec_ to a `WebSocketCreator` instance (typically a lambda expression). +The path spec is matched against the WebSocket upgrade request URI to select the correspondent `WebSocketCreator` to invoke. + +The path spec can have these forms: + +* Servlet syntax, specified with `servlet|`, where the `servlet|` prefix can be omitted if the path spec begins with `/` or `+*.+` (for example, `/ws`, `/ws/chat` or `+*.ws+`). +* Regex syntax, specified with `regex|`, where the `regex|` prefix can be omitted if the path spec begins with `^` (for example, `+^/ws/[0-9]++`). +* URI template syntax, specified with `uri-template|` (for example `+uri-template|/ws/chat/{room}+`). + +Within the `WebSocketCreator`, it is possible to access the path spec and, for example in case of URI templates, extract additional information in the following way: + +[,java,indent=0] +---- +include::code:example$src/main/java/org/eclipse/jetty/docs/programming/server/websocket/WebSocketServerDocs.java[tags=uriTemplatePathSpec] +---- diff --git a/documentation/jetty/modules/programming-guide/pages/troubleshooting/component-dump.adoc b/documentation/jetty/modules/programming-guide/pages/troubleshooting/component-dump.adoc new file mode 100644 index 000000000000..c705673803af --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/troubleshooting/component-dump.adoc @@ -0,0 +1,26 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Component Tree Dump + +Jetty components are organized in a xref:arch/bean.adoc[component tree]. + +At the root of the component tree there is typically a `ContainerLifeCycle` instance -- typically a `Server` instance on the server and an `HttpClient` instance on the client. + +`ContainerLifeCycle` has built-in _dump_ APIs that can be invoked either directly on the `Server` instance, or xref:arch/jmx.adoc[via JMX]. + +You can invoke `Server.dump()` via JMX using a JMX console such as https://adoptium.net/jmc.html[Java Mission Control (JMC)]: + +image::jmc-server-dump.png[] + +TIP: You can get more details from a Jetty's `QueuedThreadPool` dump by enabling detailed dumps via `queuedThreadPool.setDetailedDump(true)`. diff --git a/documentation/jetty/modules/programming-guide/pages/troubleshooting/debugging.adoc b/documentation/jetty/modules/programming-guide/pages/troubleshooting/debugging.adoc new file mode 100644 index 000000000000..5f2e81f682da --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/troubleshooting/debugging.adoc @@ -0,0 +1,29 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Remote Debugging + +Sometimes, in order to figure out a problem, enabling xref:troubleshooting/logging.adoc[DEBUG logging] is not enough and you really need to debug the code with a debugger. + +Debugging an embedded Jetty application is most easily done from your preferred IDE, so refer to your IDE instruction for how to debug Java applications. + +Remote debugging can be enabled in a Jetty application via command line options: + +[,screen] +---- +java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000 --class-path ... +---- + +The example above enables remote debugging so that debuggers (for example, your preferred IDE) can connect to port `8000` on the host running the Jetty application to receive debugging events. + +NOTE: More technically, remote debugging exchanges JVM Tools Interface (JVMTI) events and commands via the Java Debug Wire Protocol (JDWP). diff --git a/documentation/jetty/modules/programming-guide/pages/troubleshooting/index.adoc b/documentation/jetty/modules/programming-guide/pages/troubleshooting/index.adoc new file mode 100644 index 000000000000..ea63d07e5ebc --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/troubleshooting/index.adoc @@ -0,0 +1,21 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Troubleshooting Jetty + +TODO: introduction +// TODO: explain the process to troubleshoot Jetty: +// TODO: #1 enable JMX +// TODO: #2 enable GC logs +// TODO: #3 take jvm/component dumps +// TODO: #4 enable debug logging if you can diff --git a/documentation/jetty/modules/programming-guide/pages/troubleshooting/logging.adoc b/documentation/jetty/modules/programming-guide/pages/troubleshooting/logging.adoc new file mode 100644 index 000000000000..1ffece87acb5 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/troubleshooting/logging.adoc @@ -0,0 +1,57 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += Logging + +The Jetty libraries (both client and server) use http://slf4j.org/[SLF4J] as logging APIs. +You can therefore plug in any SLF4J logging implementation, and configure the logging category `org.eclipse.jetty` at the desired level. + +When you have problems with Jetty, the first thing that you want to do is to enable DEBUG logging. +This is helpful because by reading the DEBUG logs you get a better understanding of what is going on in the system (and that alone may give you the answers you need to fix the problem), and because Jetty developers will probably need the DEBUG logs to help you. + +== Jetty SLF4J Binding + +The Jetty artifact `jetty-slf4j-impl` is a SLF4J binding, that is the Jetty implementation of the SLF4J APIs, and provides a number of easy-to-use features to configure logging. + +The Jetty SLF4J binding only provides an appender that writes to `System.err`. +For more advanced configurations (for example, logging to a file), use http://logback.qos.ch[LogBack], or https://logging.apache.org/log4j/2.x/[Log4j2], or your preferred SLF4J binding. + +CAUTION: Only one binding can be present in the class-path or module-path. If you use the LogBack SLF4J binding or the Log4j2 SLF4J binding, remember to remove the Jetty SLF4J binding. + +The Jetty SLF4J binding reads a file in the class-path (or module-path) called `jetty-logging.properties` that can be configured with the logging levels for various logger categories: + +.jetty-logging.properties +[,screen] +---- +# By default, log at INFO level all Jetty classes. +org.eclipse.jetty.LEVEL=INFO + +# However, the Jetty client classes are logged at DEBUG level. +org.eclipse.jetty.client.LEVEL=DEBUG +---- + +Similarly to how you configure the `jetty-logging.properties` file, you can set the system property `org.eclipse.jetty[.].LEVEL=DEBUG` to quickly change the logging level to DEBUG without editing any file. +The system property can be set on the command line, or in your IDE when you run your tests or your Jetty-based application and will override the `jetty-logging.properties` file configuration. +For example to enable DEBUG logging for all the Jetty classes (_very_ verbose): + +[,screen] +---- +java -Dorg.eclipse.jetty.LEVEL=DEBUG --class-path ... +---- + +If you want to enable DEBUG logging but only for the HTTP/2 classes: + +[,screen] +---- +java -Dorg.eclipse.jetty.http2.LEVEL=DEBUG --class-path ... +---- diff --git a/documentation/jetty/modules/programming-guide/pages/troubleshooting/state-tracking.adoc b/documentation/jetty/modules/programming-guide/pages/troubleshooting/state-tracking.adoc new file mode 100644 index 000000000000..ba5858c2b36f --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/troubleshooting/state-tracking.adoc @@ -0,0 +1,22 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += `StateTrackingHandler` + +`StateTrackingHandler` (described xref:server/http.adoc#handler-use-state-tracking[here]) is a troubleshooting `Handler` that can be inserted in the `Handler` chain to track usages of `Handler`/`Request`/`Response` asynchronous APIs. + +xref:troubleshooting/component-dump.adoc[Dumping the Jetty component tree] will dump the `StateTrackingHandler`, which will dump the state of the current requests. + +This will help detecting whether requests are not completed due to callbacks not being completed, or whether callback code is stuck while invoking blocking APIs, etc. + +Thread stack traces (including virtual threads) of operations that have been started but not completed, or are stuck in blocking code are provided in the component tree dump. diff --git a/documentation/jetty/modules/programming-guide/pages/troubleshooting/thread-dump.adoc b/documentation/jetty/modules/programming-guide/pages/troubleshooting/thread-dump.adoc new file mode 100644 index 000000000000..4684c409bff9 --- /dev/null +++ b/documentation/jetty/modules/programming-guide/pages/troubleshooting/thread-dump.adoc @@ -0,0 +1,16 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + += JVM Thread Dump + +TODO diff --git a/documentation/jetty/pom.xml b/documentation/jetty/pom.xml new file mode 100644 index 000000000000..657601753fb7 --- /dev/null +++ b/documentation/jetty/pom.xml @@ -0,0 +1,163 @@ + + + + 4.0.0 + + org.eclipse.jetty.documentation + documentation + 12.0.10-SNAPSHOT + + jetty + pom + Documentation :: Content Root + + + + true + + + + + + org.antora + antora-maven-plugin + 1.0.0-alpha.4 + true + + + + + + jetty/jetty.website + main:lib/playbook-templates/per-branch-antora-playbook.yml + + + + + + + + + collector + + + collector + + + + ${project.build.directory}/jetty-home-${project.version} + ${project.build.directory} + true + + + + org.eclipse.jetty + jetty-home + ${project.version} + zip + provided + + + org.eclipse.jetty.tests + jetty-testers + runtime + + + org.eclipse.jetty.toolchain + jetty-test-helper + runtime + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + build-jetty-home + + unpack + + generate-resources + + + + org.eclipse.jetty + jetty-home + ${project.version} + zip + + + ${jetty.home.output.directory} + + + + capture-run-jetty-classpath + + build-classpath + + generate-resources + + run.jetty.classpath + runtime + + jetty-testers + true + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + pass-jetty-home + + copy-resources + + process-resources + + ${project.build.directory}/collector + + + ${basedir} + + antora.yml + + true + + + + + + + + + + + collector-with-antora-cache-dir + + + env.ANTORA_CACHE_DIR + + + + ${env.ANTORA_CACHE_DIR}/jetty/jetty-home-${project.version} + ${env.ANTORA_CACHE_DIR}/jetty + + + + examples + + + !collector + + + + modules/code/examples + + + + diff --git a/documentation/pom.xml b/documentation/pom.xml index b0b5d5845001..27e16b450794 100644 --- a/documentation/pom.xml +++ b/documentation/pom.xml @@ -13,7 +13,6 @@ Documentation - jetty-asciidoctor-extensions - jetty-documentation + jetty diff --git a/tests/jetty-testers/src/main/java/org/eclipse/jetty/tests/testers/RunJetty.java b/tests/jetty-testers/src/main/java/org/eclipse/jetty/tests/testers/RunJetty.java new file mode 100644 index 000000000000..9ed4d3be7e30 --- /dev/null +++ b/tests/jetty-testers/src/main/java/org/eclipse/jetty/tests/testers/RunJetty.java @@ -0,0 +1,262 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.testers; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + *

      A Java application that captures the output of starting the Jetty server with + * a given set of parameters, filters the output, and prints it back to STDOUT.

      + *

      This application is used in the Jetty documentation to show the output from starting + * the Jetty server.

      + *

      Example usage:

      + *
      + * java -cp $RUN_JETTY_CLASSPATH org.eclipse.jetty.tests.testers.RunJetty --jetty-home $JETTY_HOME
      + * 
      + *

      Available options are:

      + *
      + *
      jetty-home
      + *
      Required, the path to where Jetty is installed.
      + *
      jetty-base
      + *
      Optional, the path to the Jetty base directory where customizations are located. + * If not provided, will be automatically created.
      + *
      setup-args
      + *
      Optional, specifies the arguments to use in a Jetty server setup run. + * If missing, no Jetty server setup run will be executed. + * The output produced by this run is ignored.
      + *
      args
      + *
      Optional, specifies the arguments to use in a Jetty server run. + * If missing, a Jetty server run will be executed with no arguments. + * The output produced by this run is included in the Asciidoc document.
      + *
      replace
      + *
      Optional, specifies a comma-separated pair where the first element is a regular + * expression and the second is the string replacement.
      + *
      delete
      + *
      Optional, specifies a regular expression that when matched deletes the line
      + *
      highlight
      + *
      Optional, specifies a regular expression that matches lines that should be highlighted. + * If missing, no line will be highlighted. + * If the regular expression contains capturing groups, only the text matching + * the groups is highlighted, not the whole line. + *
      + *
      callouts
      + *
      Optional, specifies a comma-separated pair where the first element is a callout + * pattern, and the second element is a comma-separated list of regular expressions, + * each matching a single line, that get a callout added at the end of the line.
      + *
      + * + * @see JettyHomeTester + */ +public class RunJetty +{ + public static void main(String[] args) + { + Map config = new HashMap(); + String name = null; + for (String arg : args) + { + if (arg.startsWith("--") && arg.contains("=")) + { + String[] nameAndValue = arg.split("=", 2); + config.put(nameAndValue[0].substring(2), nameAndValue[1]); + } + } + if (!config.containsKey("jetty-home")) + { + throw new RuntimeException("--jetty-home argument is required"); + } + new RunJetty().run(config); + } + + public RunJetty() + { + } + + public void run(Map config) + { + try + { + Path jettyHome = Path.of(config.get("jetty-home")); + Path jettyBase = Path.of(config.getOrDefault("jetty-base", jettyHome.toString() + "-base")); + + JettyHomeTester jetty = JettyHomeTester.Builder.newInstance() + .jettyHome(jettyHome) + .jettyBase(jettyBase) + .mavenLocalRepository(config.get("maven-local-repo")) + .build(); + + String setupArgs = config.get("setup-args"); + if (setupArgs != null) + { + try (JettyHomeTester.Run setupRun = jetty.start(setupArgs.split(" "))) + { + setupRun.awaitFor(15, TimeUnit.SECONDS); + } + } + + String args = config.get("args"); + args = args == null ? "" : args + " "; + args += jettyHome.resolve("etc/jetty-halt.xml"); + try (JettyHomeTester.Run run = jetty.start(args.split(" "))) + { + run.awaitFor(15, TimeUnit.SECONDS); + System.out.println(captureOutput(config, run)); + } + } + catch (Throwable x) + { + throw new RuntimeException(x); + } + } + + private String captureOutput(Map config, JettyHomeTester.Run run) + { + final String actualVersion = (String)config.get("jetty-version"); + final String stableVersion = actualVersion == null ? null : actualVersion.replace("-SNAPSHOT", ""); + Stream lines = run.getLogs().stream() + .map(line -> redact(line, System.getProperty("java.home"), "/path/to/java.home")) + .map(line -> redact(line, run.getConfig().getMavenLocalRepository(), "/path/to/maven.repository")) + .map(line -> redact(line, run.getConfig().getJettyHome().toString(), "/path/to/jetty.home")) + .map(line -> redact(line, run.getConfig().getJettyBase().toString(), "/path/to/jetty.base")) + .map(line -> redact(line, actualVersion, stableVersion)) + .map(line -> regexpRedact(line, "(^| )[^ ]+/etc/jetty-halt\\.xml", "")); + lines = replace(lines, config.get("replace")); + lines = delete(lines, config.get("delete")); + lines = denoteLineStart(lines); + lines = highlight(lines, config.get("highlight")); + lines = callouts(lines, config.get("callouts")); + return lines.collect(Collectors.joining(System.lineSeparator())); + } + + private String redact(String line, String target, String replacement) + { + if (target != null && replacement != null) + return line.replace(target, replacement); + return line; + } + + private String regexpRedact(String line, String regexp, String replacement) + { + if (regexp != null && replacement != null) + return line.replaceAll(regexp, replacement); + return line; + } + + private Stream replace(Stream lines, String replace) + { + if (replace == null) + return lines; + + // Format is: (regexp,replacement). + String[] parts = replace.split(","); + String regExp = parts[0]; + String replacement = parts[1].replace("\\n", "\n"); + + return lines.flatMap(line -> Stream.of(line.replaceAll(regExp, replacement).split("\n"))); + } + + private Stream delete(Stream lines, String delete) + { + if (delete == null) + return lines; + Pattern regExp = Pattern.compile(delete); + return lines.filter(line -> !regExp.matcher(line).find()); + } + + private Stream denoteLineStart(Stream lines) + { + // Matches lines that start with a date such as "2020-01-01 00:00:00.000:". + Pattern regExp = Pattern.compile("(^\\d{4}[^:]+:[^:]+:[^:]+:)"); + return lines.map(line -> + { + Matcher matcher = regExp.matcher(line); + if (!matcher.find()) + return line; + return "**" + matcher.group(1) + "**" + line.substring(matcher.end(1)); + }); + } + + private Stream highlight(Stream lines, String highlight) + { + if (highlight == null) + return lines; + + Pattern regExp = Pattern.compile(highlight); + return lines.map(line -> + { + Matcher matcher = regExp.matcher(line); + if (!matcher.find()) + return line; + + int groupCount = matcher.groupCount(); + + // No capturing groups, highlight the whole line. + if (groupCount == 0) + return "##" + line + "##"; + + // Highlight the capturing groups. + StringBuilder result = new StringBuilder(line.length() + 4 * groupCount); + int start = 0; + for (int groupIndex = 1; groupIndex <= groupCount; ++groupIndex) + { + int matchBegin = matcher.start(groupIndex); + result.append(line, start, matchBegin); + result.append("##"); + int matchEnd = matcher.end(groupIndex); + result.append(line, matchBegin, matchEnd); + result.append("##"); + start = matchEnd; + } + result.append(line, start, line.length()); + return result.toString(); + }); + } + + private Stream callouts(Stream lines, String callouts) + { + if (callouts == null) + return lines; + + // Format is (prefix$Nsuffix,regExp...). + String[] parts = callouts.split(","); + String calloutPattern = parts[0]; + List regExps = Stream.of(parts) + .skip(1) + .map(Pattern::compile) + .collect(Collectors.toList()); + + AtomicInteger index = new AtomicInteger(); + + return lines.map(line -> + { + int regExpIndex = index.get(); + if (regExpIndex == regExps.size()) + return line; + Pattern regExp = regExps.get(regExpIndex); + if (!regExp.matcher(line).find()) + return line; + int calloutIndex = index.incrementAndGet(); + return line + calloutPattern.replace("$N", String.valueOf(calloutIndex)); + }); + } +}