Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-11.0.x

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2020-05-05 11:12:29 +10:00
commit 9aeef3d5a9
33 changed files with 1589 additions and 264 deletions

View File

@ -33,6 +33,26 @@
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-jakarta-servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-rewrite</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
@ -48,36 +68,21 @@
<artifactId>jetty-servlets</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-rewrite</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-http-client-transport</artifactId>

View File

@ -31,8 +31,9 @@ The end result is that an application based on the Jetty libraries is a _tree_ o
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 JMX MBeans (TODO: xref the JMX section) so that a JMX console can look at the internal state of the components.
It also makes possible to dump the component tree (and therefore each component's internal state) to a log file or to the console for troubleshooting purposes (TODO: xref troubleshooting section).
It makes possible to register the components in the tree as xref:eg-arch-jmx[JMX MBeans] so that a JMX console can look at the internal state of the components.
It also makes possible to xref:eg-troubleshooting-component-dump[dump the component tree] (and therefore each component's internal state) to a log file or to the console for xref:eg-troubleshooting[troubleshooting purposes].
// TODO: add a section on Dumpable?
[[eg-arch-bean-lifecycle]]
==== Jetty Component Lifecycle
@ -40,7 +41,7 @@ It also makes possible to dump the component tree (and therefore each component'
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 extend the `org.eclipse.jetty.util.component.ContainerLifeCycle` class.
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
@ -94,8 +95,72 @@ include::{doc_code}/embedded/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:
[source,java,indent=0]
----
include::{doc_code}/embedded/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:eg-arch-jmx[export to JMX] or xref:eg-troubleshooting-component-dump[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.
====
[[eg-arch-bean-listener]]
==== Jetty Component Listeners
// TODO: LifeCycle.Listener
// TODO: Container.Listener + InheritedListener
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.
[[eg-arch-bean-listener-lifecycle]]
===== LifeCycle.Listener
A `LifeCycle.Listener` emits events for life cycle events such as starting, stopping and failures:
[source,java,indent=0]
----
include::{doc_code}/embedded/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`.
[[eg-arch-bean-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:
[source,java,indent=0]
----
include::{doc_code}/embedded/ComponentDocs.java[tags=containerListener]
----
A `Container.Listener` added as a bean will also be registered as a listener:
[source,java,indent=0]
----
include::{doc_code}/embedded/ComponentDocs.java[tags=containerSiblings]
----
[[eg-arch-bean-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:eg-arch-jmx[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.

View File

@ -0,0 +1,306 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
[[eg-arch-jmx]]
== 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 link:https://openjdk.java.net/projects/jmc/[Java Mission Control] can interact with a running application for these purposes.
Jetty architecture is based on xref:eg-arch-bean[components] organized in a tree. Every time a component is added to or removed from the component tree, an event is emitted, and xref:eg-arch-bean-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 xref:eg-arch-jmx-annotation[Jetty JMX annotations] 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:
[source,xml,subs=normal]
----
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
<version>{version}</version>
</dependency>
----
=== 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:
[source,java,indent=0]
----
include::{doc_code}/embedded/JMXDocs.java[tags=server]
----
Similarly on the client:
[source,java,indent=0]
----
include::{doc_code}/embedded/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 xref:eg-arch-jmx-remote[JMX Remote Access].
====
// TODO: add a section about how to expose logging once #4830 is fixed.
[[eg-arch-jmx-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:
[source,screen]
----
service:jmx:rmi://<rmi_server_host>:<rmi_server_port>/jndi/rmi://<rmi_registry_host>:<rmi_registry_port>/jmxrmi
----
Default values are:
[source,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 xref:eg-arch-jmx-remote-ssh-tunnel[SSH tunnel].
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:
[source,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 xref:eg-arch-jmx-remote-ssh-tunnel[JMX Remote Access via SSH Tunnel.]
====
To allow JMX remote access, create and configure a `ConnectorServer`:
[source,java,indent=0]
----
include::{doc_code}/embedded/JMXDocs.java[tags=remote]
----
[[eg-arch-jmx-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:
[source,java,indent=0]
----
include::{doc_code}/embedded/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
[source,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
[source,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:
[source,java,indent=0]
----
include::{doc_code}/embedded/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:
[source,java,indent=0]
----
include::{doc_code}/embedded/JMXDocs.java[tags=tlsJMXConnector]
----
Similarly, to launch JMC:
[source,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.
[[eg-arch-jmx-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:
[source,screen]
----
$ ssh -L 1099:localhost:1099 <user>@<machine_host>
----
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.
[[eg-arch-jmx-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.
[source,java,indent=0]
----
include::{doc_code}/embedded/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: `<package>.jmx.<class>MBean`.
For example, class `com.acme.Foo` may have a custom `ObjectMBean` subclass named `com.acme.**jmx**.Foo**MBean**`.
[source,java,indent=0]
----
include::{doc_code}/embedded/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.
[source,java,indent=0]
----
include::{doc_code}/embedded/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.

View File

@ -21,5 +21,6 @@
== Jetty Architecture
include::arch-bean.adoc[]
include::arch-jmx.adoc[]
include::arch-listener.adoc[]
include::arch-io.adoc[]

View File

@ -245,8 +245,7 @@ Server
* 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 export them via JMX.
// TODO: xref to the JMX section.
Server applications can read these values and use them internally, or expose them via some service, or xref:eg-arch-jmx[export them to JMX].
`StatisticsHandler` can be configured at the server level or at the context level.

View File

@ -20,6 +20,13 @@
[[eg-troubleshooting]]
== 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
[[eg-troubleshooting-logging]]
=== Logging
@ -66,6 +73,24 @@ If you want to enable DEBUG logging but only for the HTTP/2 classes:
java -Dorg.eclipse.jetty.http2.LEVEL=DEBUG --class-path ...
----
[[eg-troubleshooting-thread-dump]]
=== JVM Thread Dump
TODO
[[eg-troubleshooting-component-dump]]
=== Jetty Component Tree Dump
Jetty components are organized in a xref:eg-arch-bean[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 or xref:eg-arch-jmx[via JMX].
// TODO: images from JMC?
// TODO: Command line JMX will be in JMX section.
TIP: You can get more details from a Jetty's `QueuedThreadPool` dump by enabling detailed dumps via `queuedThreadPool.setDetailedDump(true)`.
[[eg-troubleshooting-debugging]]
=== Debugging

View File

@ -18,11 +18,19 @@
package embedded;
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
@ -140,4 +148,142 @@ public class ComponentDocs
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<Object> children = root.getBeans();
// children contains only service
// Find all descendants of root that are instance of a particular class.
Collection<ScheduledExecutorService> 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<Object> 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[]
}
}

View File

@ -0,0 +1,276 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package embedded;
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<String, Object> 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<String, Object> 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<ObjectName> 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<String, Object> 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[]
}
}

View File

@ -34,6 +34,7 @@ import org.eclipse.jetty.io.SelectorManager;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
@SuppressWarnings("unused")
public class SelectorManagerDocs
{
// tag::connect[]

View File

@ -66,7 +66,7 @@ public class ConnectorServer extends AbstractLifeCycle
private JMXServiceURL _jmxURL;
private final Map<String, Object> _environment;
private final String _objectName;
private final SslContextFactory _sslContextFactory;
private final SslContextFactory.Server _sslContextFactory;
private int _registryPort;
private int _rmiPort;
private JMXConnectorServer _connectorServer;
@ -98,7 +98,7 @@ public class ConnectorServer extends AbstractLifeCycle
this(svcUrl, environment, name, null);
}
public ConnectorServer(JMXServiceURL svcUrl, Map<String, ?> environment, String name, SslContextFactory sslContextFactory)
public ConnectorServer(JMXServiceURL svcUrl, Map<String, ?> environment, String name, SslContextFactory.Server sslContextFactory)
{
this._jmxURL = svcUrl;
this._environment = environment == null ? new HashMap<>() : new HashMap<>(environment);

View File

@ -60,7 +60,7 @@ public class MBeanContainer implements Container.InheritedListener, Dumpable, De
private final MBeanServer _mbeanServer;
private final boolean _useCacheForOtherClassLoaders;
private final ConcurrentMap<Class, MetaData> _metaData = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, MetaData> _metaData = new ConcurrentHashMap<>();
private final ConcurrentMap<Object, Container> _beans = new ConcurrentHashMap<>();
private final ConcurrentMap<Object, ObjectName> _mbeans = new ConcurrentHashMap<>();
private String _domain = null;

View File

@ -231,7 +231,7 @@ public class ConnectorServerTest
@Test
public void testJMXOverTLS() throws Exception
{
SslContextFactory sslContextFactory = new SslContextFactory.Server();
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
String keyStorePath = MavenTestingUtils.getTestResourcePath("keystore.p12").toString();
String keyStorePassword = "storepwd";
sslContextFactory.setKeyStorePath(keyStorePath);

View File

@ -18,6 +18,8 @@
package org.eclipse.jetty.server;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import jakarta.servlet.http.PushBuilder;
@ -27,6 +29,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -38,7 +41,14 @@ public class PushBuilderImpl implements PushBuilder
{
private static final Logger LOG = LoggerFactory.getLogger(PushBuilderImpl.class);
private static final HttpField JettyPush = new HttpField("x-http2-push", "PushBuilder");
private static final HttpField JETTY_PUSH = new HttpField("x-http2-push", "PushBuilder");
private static EnumSet<HttpMethod> UNSAFE_METHODS = EnumSet.of(
HttpMethod.POST,
HttpMethod.PUT,
HttpMethod.DELETE,
HttpMethod.CONNECT,
HttpMethod.OPTIONS,
HttpMethod.TRACE);
private final Request _request;
private final HttpFields.Mutable _fields;
@ -56,7 +66,7 @@ public class PushBuilderImpl implements PushBuilder
_method = method;
_queryString = queryString;
_sessionId = sessionId;
_fields.add(JettyPush);
_fields.add(JETTY_PUSH);
if (LOG.isDebugEnabled())
LOG.debug("PushBuilder({} {}?{} s={} c={})", _method, _request.getRequestURI(), _queryString, _sessionId);
}
@ -70,6 +80,10 @@ public class PushBuilderImpl implements PushBuilder
@Override
public PushBuilder method(String method)
{
Objects.requireNonNull(method);
if (StringUtil.isBlank(method) || UNSAFE_METHODS.contains(HttpMethod.fromString(method)))
throw new IllegalArgumentException("Method not allowed for push: " + method);
_method = method;
return this;
}
@ -149,9 +163,6 @@ public class PushBuilderImpl implements PushBuilder
@Override
public void push()
{
if (HttpMethod.POST.is(_method) || HttpMethod.PUT.is(_method))
throw new IllegalStateException("Bad Method " + _method);
if (_path == null || _path.length() == 0)
throw new IllegalStateException("Bad Path " + _path);

View File

@ -71,6 +71,7 @@ import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HostPortHttpField;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
@ -393,6 +394,11 @@ public class Request implements HttpServletRequest
HttpFields.Mutable fields = HttpFields.build(getHttpFields(), NOT_PUSHED_HEADERS);
HttpField authField = getHttpFields().getField(HttpHeader.AUTHORIZATION);
//TODO check what to do for digest etc etc
if (getUserPrincipal() != null && authField.getValue().startsWith("Basic"))
fields.add(authField);
String id;
try
{
@ -410,12 +416,47 @@ public class Request implements HttpServletRequest
id = getRequestedSessionId();
}
Map<String,String> cookies = new HashMap<>();
Cookie[] existingCookies = getCookies();
if (existingCookies != null)
{
for (Cookie c: getCookies())
{
cookies.put(c.getName(), c.getValue());
}
}
//Any Set-Cookies that were set on the response must be set as Cookies on the
//PushBuilder, unless the max-age of the cookie is <= 0
HttpFields responseFields = getResponse().getHttpFields();
for (HttpField field : responseFields)
{
HttpHeader header = field.getHeader();
if (header == HttpHeader.SET_COOKIE)
{
HttpCookie cookie = ((SetCookieHttpField)field).getHttpCookie();
if (cookie.getMaxAge() > 0)
cookies.put(cookie.getName(), cookie.getValue());
else
cookies.remove(cookie.getName());
}
}
if (!cookies.isEmpty())
{
StringBuilder buff = new StringBuilder();
for (Map.Entry<String,String> entry : cookies.entrySet())
{
if (buff.length() > 0)
buff.append("; ");
buff.append(entry.getKey()).append('=').append(entry.getValue());
}
fields.add(new HttpField(HttpHeader.COOKIE, buff.toString()));
}
PushBuilder builder = new PushBuilderImpl(this, fields, getMethod(), getQueryString(), id);
builder.addHeader("referer", getRequestURL().toString());
// TODO process any set cookies
// TODO process any user_identity
return builder;
}

View File

@ -27,6 +27,7 @@ import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@ -46,10 +47,18 @@ import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.Part;
import jakarta.servlet.http.PushBuilder;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpCookie;
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.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.http.tools.HttpTester;
@ -58,6 +67,9 @@ import org.eclipse.jetty.server.LocalConnector.LocalEndPoint;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.session.Session;
import org.eclipse.jetty.server.session.SessionData;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
@ -1861,11 +1873,128 @@ public class RequestTest
assertNotNull(request.getParameterMap());
assertEquals(0, request.getParameterMap().size());
}
@Test
public void testPushBuilder() throws Exception
{
String uri = "/foo/something";
Request request = new TestRequest(null, null);
request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(new HttpCookie("good","thumbsup", 100), CookieCompliance.RFC6265));
request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(new HttpCookie("bonza","bewdy", 1), CookieCompliance.RFC6265));
request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(new HttpCookie("bad", "thumbsdown", 0), CookieCompliance.RFC6265));
HttpFields.Mutable fields = HttpFields.build();
fields.add(HttpHeader.AUTHORIZATION, "Basic foo");
request.setMetaData(new MetaData.Request("GET", HttpURI.from(uri), HttpVersion.HTTP_1_0, fields));
assertTrue(request.isPushSupported());
PushBuilder builder = request.newPushBuilder();
assertNotNull(builder);
assertEquals("GET", builder.getMethod());
assertThrows(NullPointerException.class, () ->
{
builder.method(null);
});
assertThrows(IllegalArgumentException.class, () ->
{
builder.method("");
});
assertThrows(IllegalArgumentException.class, () ->
{
builder.method(" ");
});
assertThrows(IllegalArgumentException.class, () ->
{
builder.method("POST");
});
assertThrows(IllegalArgumentException.class, () ->
{
builder.method("PUT");
});
assertThrows(IllegalArgumentException.class, () ->
{
builder.method("DELETE");
});
assertThrows(IllegalArgumentException.class, () ->
{
builder.method("CONNECT");
});
assertThrows(IllegalArgumentException.class, () ->
{
builder.method("OPTIONS");
});
assertThrows(IllegalArgumentException.class, () ->
{
builder.method("TRACE");
});
assertEquals(TestRequest.TEST_SESSION_ID, builder.getSessionId());
builder.path("/foo/something-else.txt");
assertEquals("/foo/something-else.txt", builder.getPath());
assertEquals("Basic foo", builder.getHeader("Authorization"));
assertThat(builder.getHeader("Cookie"), containsString("bonza"));
assertThat(builder.getHeader("Cookie"), containsString("good"));
assertThat(builder.getHeader("Cookie"), containsString("maxpos"));
assertThat(builder.getHeader("Cookie"), not(containsString("bad")));
}
interface RequestTester
{
boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException;
}
private class TestRequest extends Request
{
public static final String TEST_SESSION_ID = "abc123";
Response _response = new Response(null, null);
Cookie c1;
Cookie c2;
public TestRequest(HttpChannel channel, HttpInput input)
{
super(channel, input);
c1 = new Cookie("maxpos", "xxx");
c1.setMaxAge(1);
c2 = new Cookie("maxneg", "yyy");
c2.setMaxAge(-1);
}
@Override
public boolean isPushSupported()
{
return true;
}
@Override
public HttpSession getSession()
{
return new Session(new SessionHandler(), new SessionData(TEST_SESSION_ID, "", "0.0.0.0", 0, 0, 0, 300));
}
@Override
public Principal getUserPrincipal()
{
return new Principal()
{
@Override
public String getName()
{
return "user";
}
};
}
@Override
public Response getResponse()
{
return _response;
}
@Override
public Cookie[] getCookies()
{
return new Cookie[] {c1,c2};
}
}
private class RequestHandler extends AbstractHandler
{

View File

@ -40,12 +40,15 @@ import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient;
import org.eclipse.jetty.websocket.core.exception.UpgradeException;
import org.eclipse.jetty.websocket.core.exception.WebSocketTimeoutException;
import org.eclipse.jetty.websocket.jakarta.client.internal.JsrUpgradeListener;
import org.eclipse.jetty.websocket.jakarta.common.ConfiguredEndpoint;
import org.eclipse.jetty.websocket.jakarta.common.JakartaWebSocketContainer;
import org.eclipse.jetty.websocket.jakarta.common.JakartaWebSocketExtensionConfig;
import org.eclipse.jetty.websocket.jakarta.common.JakartaWebSocketFrameHandler;
import org.eclipse.jetty.websocket.jakarta.common.JakartaWebSocketFrameHandlerFactory;
import org.eclipse.jetty.websocket.util.InvalidWebSocketException;
/**
* Container for Client use of the jakarta.websocket API.
@ -132,7 +135,7 @@ public class JakartaWebSocketClientContainer extends JakartaWebSocketContainer i
{
if (error != null)
{
futureSession.completeExceptionally(error);
futureSession.completeExceptionally(convertCause(error));
return;
}
@ -148,6 +151,18 @@ public class JakartaWebSocketClientContainer extends JakartaWebSocketContainer i
return futureSession;
}
public static Throwable convertCause(Throwable error)
{
if (error instanceof UpgradeException ||
error instanceof WebSocketTimeoutException)
return new IOException(error);
if (error instanceof InvalidWebSocketException)
return new DeploymentException(error.getMessage(), error);
return error;
}
private Session connect(ConfiguredEndpoint configuredEndpoint, URI destURI) throws IOException
{
Objects.requireNonNull(configuredEndpoint, "WebSocket configured endpoint cannot be null");

View File

@ -22,6 +22,7 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import jakarta.websocket.server.PathParam;
import org.eclipse.jetty.websocket.util.InvalidSignatureException;
import org.eclipse.jetty.websocket.util.InvokerUtils;
/**
@ -40,6 +41,7 @@ public class PathParamIdentifier implements InvokerUtils.ParamIdentifier
{
if (anno.annotationType().equals(PathParam.class))
{
validateType(paramType);
PathParam pathParam = (PathParam)anno;
return new InvokerUtils.Arg(paramType, pathParam.value());
}
@ -47,4 +49,22 @@ public class PathParamIdentifier implements InvokerUtils.ParamIdentifier
}
return new InvokerUtils.Arg(paramType);
}
/**
* The JSR356 rules for @PathParam only support
* String, Primitive Types (and their Boxed version)
*/
public static void validateType(Class<?> type)
{
if (!String.class.isAssignableFrom(type) &&
!Integer.TYPE.isAssignableFrom(type) &&
!Long.TYPE.isAssignableFrom(type) &&
!Short.TYPE.isAssignableFrom(type) &&
!Float.TYPE.isAssignableFrom(type) &&
!Double.TYPE.isAssignableFrom(type) &&
!Boolean.TYPE.isAssignableFrom(type) &&
!Character.TYPE.isAssignableFrom(type) &&
!Byte.TYPE.isAssignableFrom(type))
throw new InvalidSignatureException("Unsupported PathParam Type: " + type);
}
}

View File

@ -35,7 +35,6 @@ import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -51,112 +50,121 @@ import static org.hamcrest.Matchers.notNullValue;
public class WSServer extends LocalServer implements LocalFuzzer.Provider
{
private static final Logger LOG = LoggerFactory.getLogger(WSServer.class);
private final Path contextDir;
private final String contextPath;
private ContextHandlerCollection contexts;
private Path webinf;
private Path classesDir;
private final Path testDir;
private ContextHandlerCollection contexts = new ContextHandlerCollection();
public WSServer(File testdir, String contextName)
public WSServer(Path testDir)
{
this(testdir.toPath(), contextName);
this.testDir = testDir;
}
public WSServer(Path testdir, String contextName)
public WebApp createWebApp(String contextName)
{
this.contextDir = testdir.resolve(contextName);
this.contextPath = "/" + contextName;
FS.ensureEmpty(contextDir);
}
public void copyClass(Class<?> clazz) throws Exception
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
String endpointPath = TypeUtil.toClassReference(clazz);
URL classUrl = cl.getResource(endpointPath);
assertThat("Class URL for: " + clazz, classUrl, notNullValue());
Path destFile = classesDir.resolve(endpointPath);
FS.ensureDirExists(destFile.getParent());
File srcFile = new File(classUrl.toURI());
IO.copy(srcFile, destFile.toFile());
}
public void copyEndpoint(Class<?> endpointClass) throws Exception
{
copyClass(endpointClass);
}
public void copyLib(Class<?> clazz, String jarFileName) throws URISyntaxException, IOException
{
webinf = contextDir.resolve("WEB-INF");
FS.ensureDirExists(webinf);
Path libDir = webinf.resolve("lib");
FS.ensureDirExists(libDir);
Path jarFile = libDir.resolve(jarFileName);
URL codeSourceURL = clazz.getProtectionDomain().getCodeSource().getLocation();
assertThat("Class CodeSource URL is file scheme", codeSourceURL.getProtocol(), is("file"));
File sourceCodeSourceFile = new File(codeSourceURL.toURI());
if (sourceCodeSourceFile.isDirectory())
{
LOG.info("Creating " + jarFile + " from " + sourceCodeSourceFile);
JAR.create(sourceCodeSourceFile, jarFile.toFile());
}
else
{
LOG.info("Copying " + sourceCodeSourceFile + " to " + jarFile);
IO.copy(sourceCodeSourceFile, jarFile.toFile());
}
}
public void copyWebInf(String testResourceName) throws IOException
{
webinf = contextDir.resolve("WEB-INF");
FS.ensureDirExists(webinf);
classesDir = webinf.resolve("classes");
FS.ensureDirExists(classesDir);
Path webxml = webinf.resolve("web.xml");
File testWebXml = MavenTestingUtils.getTestResourceFile(testResourceName);
IO.copy(testWebXml, webxml.toFile());
}
public WebAppContext createWebAppContext() throws IOException
{
WebAppContext context = new WebAppContext();
context.setContextPath(this.contextPath);
context.setBaseResource(new PathResource(this.contextDir));
context.setAttribute("org.eclipse.jetty.websocket.jakarta", Boolean.TRUE);
context.addConfiguration(new JakartaWebSocketConfiguration());
return context;
}
public void createWebInf() throws IOException
{
copyWebInf("empty-web.xml");
}
public void deployWebapp(WebAppContext webapp) throws Exception
{
contexts.addHandler(webapp);
contexts.manage(webapp);
webapp.setThrowUnavailableOnStartupException(true);
webapp.start();
if (LOG.isDebugEnabled())
{
LOG.debug("{}", webapp.dump());
}
}
public Path getWebAppDir()
{
return this.contextDir;
return new WebApp(contextName);
}
@Override
protected Handler createRootHandler(Server server) throws Exception
protected Handler createRootHandler(Server server)
{
contexts = new ContextHandlerCollection();
return contexts;
}
public class WebApp
{
private final WebAppContext context;
private final Path contextDir;
private final Path webInf;
private final Path classesDir;
private final Path libDir;
private WebApp(String contextName)
{
// Ensure context directory.
contextDir = testDir.resolve(contextName);
FS.ensureEmpty(contextDir);
// Ensure WEB-INF directories.
webInf = contextDir.resolve("WEB-INF");
FS.ensureDirExists(webInf);
classesDir = webInf.resolve("classes");
FS.ensureDirExists(classesDir);
libDir = webInf.resolve("lib");
FS.ensureDirExists(libDir);
// Configure the WebAppContext.
context = new WebAppContext();
context.setContextPath("/" + contextName);
context.setBaseResource(new PathResource(contextDir));
context.setAttribute("org.eclipse.jetty.websocket.jakarta", Boolean.TRUE);
context.addConfiguration(new JakartaWebSocketConfiguration());
}
public WebAppContext getWebAppContext()
{
return context;
}
public String getContextPath()
{
return context.getContextPath();
}
public Path getContextDir()
{
return contextDir;
}
public void createWebInf() throws IOException
{
copyWebInf("empty-web.xml");
}
public void copyWebInf(String testResourceName) throws IOException
{
File testWebXml = MavenTestingUtils.getTestResourceFile(testResourceName);
Path webXml = webInf.resolve("web.xml");
IO.copy(testWebXml, webXml.toFile());
}
public void copyClass(Class<?> clazz) throws Exception
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
String endpointPath = TypeUtil.toClassReference(clazz);
URL classUrl = cl.getResource(endpointPath);
assertThat("Class URL for: " + clazz, classUrl, notNullValue());
Path destFile = classesDir.resolve(endpointPath);
FS.ensureDirExists(destFile.getParent());
File srcFile = new File(classUrl.toURI());
IO.copy(srcFile, destFile.toFile());
}
public void copyLib(Class<?> clazz, String jarFileName) throws URISyntaxException, IOException
{
Path jarFile = libDir.resolve(jarFileName);
URL codeSourceURL = clazz.getProtectionDomain().getCodeSource().getLocation();
assertThat("Class CodeSource URL is file scheme", codeSourceURL.getProtocol(), is("file"));
File sourceCodeSourceFile = new File(codeSourceURL.toURI());
if (sourceCodeSourceFile.isDirectory())
{
LOG.info("Creating " + jarFile + " from " + sourceCodeSourceFile);
JAR.create(sourceCodeSourceFile, jarFile.toFile());
}
else
{
LOG.info("Copying " + sourceCodeSourceFile + " to " + jarFile);
IO.copy(sourceCodeSourceFile, jarFile.toFile());
}
}
public void deploy()
{
contexts.addHandler(context);
contexts.manage(context);
context.setThrowUnavailableOnStartupException(true);
if (LOG.isDebugEnabled())
LOG.debug("{}", context.dump());
}
}
}

View File

@ -24,7 +24,6 @@ import java.util.List;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.core.CloseStatus;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
@ -50,22 +49,21 @@ public class AltFilterTest
@Test
public void testEcho() throws Exception
{
WSServer wsb = new WSServer(testdir.getPath(), "app");
wsb.copyWebInf("alt-filter-web.xml");
WSServer wsb = new WSServer(testdir.getPath());
WSServer.WebApp app = wsb.createWebApp("app");
app.copyWebInf("alt-filter-web.xml");
// the endpoint (extends jakarta.websocket.Endpoint)
wsb.copyClass(BasicEchoSocket.class);
app.copyClass(BasicEchoSocket.class);
app.deploy();
try
{
wsb.start();
WebAppContext webapp = wsb.createWebAppContext();
wsb.deployWebapp(webapp);
FilterHolder filterWebXml = webapp.getServletHandler().getFilter("wsuf-test");
FilterHolder filterWebXml = app.getWebAppContext().getServletHandler().getFilter("wsuf-test");
assertThat("Filter[wsuf-test]", filterWebXml, notNullValue());
FilterHolder filterSCI = webapp.getServletHandler().getFilter("Jetty_WebSocketUpgradeFilter");
FilterHolder filterSCI = app.getWebAppContext().getServletHandler().getFilter("Jetty_WebSocketUpgradeFilter");
assertThat("Filter[Jetty_WebSocketUpgradeFilter]", filterSCI, nullValue());
List<Frame> send = new ArrayList<>();

View File

@ -28,7 +28,6 @@ import jakarta.websocket.Session;
import jakarta.websocket.WebSocketContainer;
import jakarta.websocket.server.ServerEndpoint;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.jakarta.tests.EventSocket;
import org.eclipse.jetty.websocket.jakarta.tests.WSServer;
import org.junit.jupiter.api.AfterEach;
@ -61,12 +60,13 @@ public class ContainerProviderServerTest
public void startServer() throws Exception
{
Path testdir = MavenTestingUtils.getTargetTestingPath(ContainerProviderServerTest.class.getName());
server = new WSServer(testdir, "app");
server.createWebInf();
server.copyEndpoint(MySocket.class);
server = new WSServer(testdir);
WSServer.WebApp app = server.createWebApp("app");
app.createWebInf();
app.copyClass(MySocket.class);
app.deploy();
server.start();
WebAppContext webapp = server.createWebAppContext();
server.deployWebapp(webapp);
}
@AfterEach

View File

@ -27,7 +27,6 @@ import com.acme.websocket.BasicEchoEndpointConfigContextListener;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
@ -56,21 +55,19 @@ public class EndpointViaConfigTest
@Test
public void testEcho() throws Exception
{
WSServer wsb = new WSServer(testdir.getPath(), "app");
wsb.copyWebInf("basic-echo-endpoint-config-web.xml");
WSServer wsb = new WSServer(testdir.getPath());
WSServer.WebApp app = wsb.createWebApp("app");
// the endpoint (extends jakarta.websocket.Endpoint)
wsb.copyClass(BasicEchoEndpoint.class);
app.copyClass(BasicEchoEndpoint.class);
// the configuration (adds the endpoint)
wsb.copyClass(BasicEchoEndpointConfigContextListener.class);
app.copyClass(BasicEchoEndpointConfigContextListener.class);
app.deploy();
try
{
wsb.start();
URI uri = wsb.getWsUri();
WebAppContext webapp = wsb.createWebAppContext();
wsb.deployWebapp(webapp);
WebSocketCoreClient client = new WebSocketCoreClient();
try
{

View File

@ -28,7 +28,6 @@ import com.acme.websocket.IdleTimeoutOnOpenEndpoint;
import com.acme.websocket.IdleTimeoutOnOpenSocket;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.core.CloseStatus;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
@ -51,19 +50,17 @@ public class IdleTimeoutTest
@BeforeAll
public static void setupServer() throws Exception
{
server = new WSServer(MavenTestingUtils.getTargetTestingPath(IdleTimeoutTest.class.getName()), "app");
server.copyWebInf("idle-timeout-config-web.xml");
server = new WSServer(MavenTestingUtils.getTargetTestingPath(IdleTimeoutTest.class.getName()));
WSServer.WebApp app = server.createWebApp("app");
// the endpoint (extends jakarta.websocket.Endpoint)
server.copyClass(IdleTimeoutOnOpenEndpoint.class);
app.copyClass(IdleTimeoutOnOpenEndpoint.class);
// the configuration that adds the endpoint
server.copyClass(IdleTimeoutContextListener.class);
app.copyClass(IdleTimeoutContextListener.class);
// the annotated socket
server.copyClass(IdleTimeoutOnOpenSocket.class);
app.copyClass(IdleTimeoutOnOpenSocket.class);
app.deploy();
server.start();
WebAppContext webapp = server.createWebAppContext();
server.deployWebapp(webapp);
}
@AfterAll

View File

@ -29,7 +29,6 @@ import jakarta.websocket.server.ServerEndpoint;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
@ -63,18 +62,17 @@ public class LargeAnnotatedTest
@Test
public void testEcho() throws Exception
{
WSServer wsb = new WSServer(testdir.getPath(), "app");
wsb.createWebInf();
wsb.copyEndpoint(LargeEchoConfiguredSocket.class);
WSServer wsb = new WSServer(testdir.getPath());
WSServer.WebApp app = wsb.createWebApp("app");
app.createWebInf();
app.copyClass(LargeEchoConfiguredSocket.class);
app.deploy();
try
{
wsb.start();
URI uri = wsb.getWsUri();
WebAppContext webapp = wsb.createWebAppContext();
wsb.deployWebapp(webapp);
WebSocketCoreClient client = new WebSocketCoreClient();
try
{

View File

@ -28,7 +28,6 @@ import com.acme.websocket.LargeEchoDefaultSocket;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
@ -53,18 +52,17 @@ public class LargeContainerTest
@Test
public void testEcho() throws Exception
{
WSServer wsb = new WSServer(testdir.getPath(), "app");
wsb.copyWebInf("large-echo-config-web.xml");
wsb.copyEndpoint(LargeEchoDefaultSocket.class);
WSServer wsb = new WSServer(testdir.getPath());
WSServer.WebApp app = wsb.createWebApp("app");
app.copyWebInf("large-echo-config-web.xml");
app.copyClass(LargeEchoDefaultSocket.class);
app.deploy();
try
{
wsb.start();
URI uri = wsb.getWsUri();
WebAppContext webapp = wsb.createWebAppContext();
wsb.deployWebapp(webapp);
WebSocketCoreClient client = new WebSocketCoreClient();
try
{

View File

@ -31,7 +31,6 @@ import jakarta.websocket.server.ServerEndpoint;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
@ -87,18 +86,17 @@ public class OnMessageReturnTest
@Test
public void testEchoReturn() throws Exception
{
WSServer wsb = new WSServer(testdir.getPath(), "app");
wsb.copyWebInf("empty-web.xml");
wsb.copyClass(EchoReturnEndpoint.class);
WSServer wsb = new WSServer(testdir.getPath());
WSServer.WebApp app = wsb.createWebApp("app");
app.copyWebInf("empty-web.xml");
app.copyClass(EchoReturnEndpoint.class);
app.deploy();
try
{
wsb.start();
URI uri = wsb.getWsUri();
WebAppContext webapp = wsb.createWebAppContext();
wsb.deployWebapp(webapp);
WebSocketCoreClient client = new WebSocketCoreClient();
try
{

View File

@ -30,7 +30,6 @@ import com.acme.websocket.PongMessageEndpoint;
import com.acme.websocket.PongSocket;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.core.CoreSession;
import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode;
@ -55,17 +54,16 @@ public class PingPongTest
public static void startServer() throws Exception
{
Path testdir = MavenTestingUtils.getTargetTestingPath(PingPongTest.class.getName());
server = new WSServer(testdir, "app");
server.copyWebInf("pong-config-web.xml");
server = new WSServer(testdir);
server.copyClass(PongContextListener.class);
server.copyClass(PongMessageEndpoint.class);
server.copyClass(PongSocket.class);
WSServer.WebApp app = server.createWebApp("app");
app.copyWebInf("pong-config-web.xml");
app.copyClass(PongContextListener.class);
app.copyClass(PongMessageEndpoint.class);
app.copyClass(PongSocket.class);
app.deploy();
server.start();
WebAppContext webapp = server.createWebAppContext();
server.deployWebapp(webapp);
}
@BeforeAll

View File

@ -93,12 +93,13 @@ public class WebAppClassLoaderTest
public void startServer() throws Exception
{
Path testdir = MavenTestingUtils.getTargetTestingPath(WebAppClassLoaderTest.class.getName());
server = new WSServer(testdir, "app");
server.createWebInf();
server.copyEndpoint(MySocket.class);
server = new WSServer(testdir);
WSServer.WebApp app = server.createWebApp("app");
app.createWebInf();
app.copyClass(MySocket.class);
app.deploy();
webapp = app.getWebAppContext();
server.start();
webapp = server.createWebAppContext();
server.deployWebapp(webapp);
}
@AfterEach

View File

@ -0,0 +1,195 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.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.websocket.jakarta.tests.server;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import jakarta.websocket.CloseReason;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.DecodeException;
import jakarta.websocket.Decoder;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.WebSocketContainer;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.eclipse.jetty.annotations.ServletContainerInitializersStarter;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.websocket.jakarta.tests.EventSocket;
import org.eclipse.jetty.websocket.jakarta.tests.WSServer;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnJre;
import org.junit.jupiter.api.condition.JRE;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class DeploymentTest
{
private WSServer server;
@BeforeEach
public void startServer() throws Exception
{
Path testdir = MavenTestingUtils.getTargetTestingPath(DeploymentTest.class.getName());
server = new WSServer(testdir);
}
@AfterEach
public void stopServer() throws Exception
{
server.stop();
}
@Test
public void testBadPathParamSignature() throws Exception
{
WSServer.WebApp app1 = server.createWebApp("test1");
app1.createWebInf();
app1.copyClass(BadPathParamEndpoint.class);
app1.copyClass(DecodedString.class);
app1.copyClass(DeploymentTest.class);
app1.deploy();
app1.getWebAppContext().setThrowUnavailableOnStartupException(false);
try (StacklessLogging ignore = new StacklessLogging(ServletContainerInitializersStarter.class, WebAppContext.class))
{
server.start();
}
WebSocketContainer client = ContainerProvider.getWebSocketContainer();
EventSocket clientSocket = new EventSocket();
Throwable error = assertThrows(Throwable.class, () ->
client.connectToServer(clientSocket, server.getWsUri().resolve(app1.getContextPath() + "/badonclose/a")));
assertThat(error, Matchers.instanceOf(IOException.class));
assertThat(error.getMessage(), Matchers.containsString("503 Service Unavailable"));
}
@Test
@DisabledOnJre(JRE.JAVA_14) // TODO: Waiting on JDK14 bug at https://bugs.openjdk.java.net/browse/JDK-8244090.
public void testDifferentWebAppsWithSameClassInSignature() throws Exception
{
WSServer.WebApp app1 = server.createWebApp("test1");
app1.createWebInf();
app1.copyClass(DecodedEndpoint.class);
app1.copyClass(StringDecoder.class);
app1.copyClass(DecodedString.class);
app1.copyClass(DeploymentTest.class);
app1.deploy();
WSServer.WebApp app2 = server.createWebApp("test2");
app2.createWebInf();
app2.copyClass(DecodedEndpoint.class);
app2.copyClass(StringDecoder.class);
app2.copyClass(DecodedString.class);
app2.copyClass(DeploymentTest.class);
app2.deploy();
server.start();
WebSocketContainer client = ContainerProvider.getWebSocketContainer();
EventSocket clientSocket = new EventSocket();
// Test echo and close to endpoint at /test1.
Session session = client.connectToServer(clientSocket, server.getWsUri().resolve("/test1"));
session.getAsyncRemote().sendText("hello world");
assertThat(clientSocket.textMessages.poll(5, TimeUnit.SECONDS), is("hello world"));
session.close();
assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(clientSocket.closeReason.getCloseCode(), is(CloseReason.CloseCodes.NORMAL_CLOSURE));
// Test echo and close to endpoint at /test2.
session = client.connectToServer(clientSocket, server.getWsUri().resolve("/test2"));
session.getAsyncRemote().sendText("hello world");
assertThat(clientSocket.textMessages.poll(5, TimeUnit.SECONDS), is("hello world"));
session.close();
assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(clientSocket.closeReason.getCloseCode(), is(CloseReason.CloseCodes.NORMAL_CLOSURE));
}
@ServerEndpoint("/badonopen/{arg}")
public static class BadPathParamEndpoint
{
@OnOpen
public void onOpen(Session session, @PathParam("arg") DecodedString arg)
{
}
}
@ServerEndpoint(value = "/", decoders = {StringDecoder.class})
public static class DecodedEndpoint
{
@OnMessage
public void onMessage(Session session, DecodedString message)
{
session.getAsyncRemote().sendText(message.getString());
}
}
public static class DecodedString
{
public String string = "";
public DecodedString(String hold)
{
string = hold;
}
public String getString()
{
return string;
}
}
public static class StringDecoder implements Decoder.Text<DecodedString>
{
@Override
public DecodedString decode(String s) throws DecodeException
{
return new DecodedString(s);
}
@Override
public void init(EndpointConfig config)
{
}
@Override
public void destroy()
{
}
@Override
public boolean willDecode(String s)
{
return true;
}
}
}

View File

@ -113,6 +113,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-jetty-client</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-felix-webapp</artifactId>
@ -120,6 +126,13 @@
<scope>test</scope>
<type>war</type>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-websocket-webapp</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<type>war</type>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-bad-websocket-webapp</artifactId>

View File

@ -20,12 +20,14 @@ package org.eclipse.jetty.tests.distribution;
import java.io.BufferedWriter;
import java.io.File;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpClient;
@ -37,10 +39,14 @@ import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.unixsocket.client.HttpClientTransportOverUnixSockets;
import org.eclipse.jetty.unixsocket.server.UnixSocketConnector;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.Disabled;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnJre;
import org.junit.jupiter.api.condition.JRE;
@ -49,6 +55,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -410,6 +417,7 @@ public class DistributionTests extends AbstractDistributionTest
"",
"--jpms",
})
@DisabledOnJre(JRE.JAVA_14) // TODO: Waiting on JDK14 bug at https://bugs.openjdk.java.net/browse/JDK-8244090.
public void testSimpleWebAppWithWebsocket(String arg) throws Exception
{
String jettyVersion = System.getProperty("jettyVersion");
@ -427,9 +435,13 @@ public class DistributionTests extends AbstractDistributionTest
assertTrue(run1.awaitFor(5, TimeUnit.SECONDS));
assertEquals(0, run1.getExitValue());
File war = distribution.resolveArtifact("org.eclipse.jetty.tests:test-bad-websocket-webapp:war:" + jettyVersion);
distribution.installWarFile(war, "test1");
distribution.installWarFile(war, "test2");
File webApp = distribution.resolveArtifact("org.eclipse.jetty.tests:test-websocket-webapp:war:" + jettyVersion);
File badWebApp = distribution.resolveArtifact("org.eclipse.jetty.tests:test-bad-websocket-webapp:war:" + jettyVersion);
distribution.installWarFile(webApp, "test1");
distribution.installWarFile(badWebApp, "test2");
distribution.installWarFile(badWebApp, "test3");
distribution.installWarFile(webApp, "test4");
int port = distribution.freePort();
String[] args2 = {
@ -437,26 +449,64 @@ public class DistributionTests extends AbstractDistributionTest
"jetty.http.port=" + port//,
//"jetty.server.dumpAfterStart=true"
};
try (DistributionTester.Run run2 = distribution.start(args2))
{
assertTrue(run2.awaitConsoleLogsFor("Started Server@", 10, TimeUnit.SECONDS));
// we do not test that anymore because it doesn't work for java14
//assertFalse(run2.getLogs().stream().anyMatch(s -> s.contains("LinkageError")));
assertFalse(run2.getLogs().stream().anyMatch(s -> s.contains("LinkageError")));
startHttpClient();
ContentResponse response = client.GET("http://localhost:" + port + "/test1/index.jsp");
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getContentAsString(), containsString("Hello"));
assertThat(response.getContentAsString(), not(containsString("<%")));
WebSocketClient wsClient = new WebSocketClient(client);
wsClient.start();
URI serverUri = URI.create("ws://localhost:" + port);
client.GET("http://localhost:" + port + "/test2/index.jsp");
assertEquals(HttpStatus.OK_200, response.getStatus());
assertThat(response.getContentAsString(), containsString("Hello"));
assertThat(response.getContentAsString(), not(containsString("<%")));
// Verify /test1 is able to establish a WebSocket connection.
WsListener webSocketListener = new WsListener();
Session session = wsClient.connect(webSocketListener, serverUri.resolve("/test1")).get(5, TimeUnit.SECONDS);
session.getRemote().sendString("echo message");
assertThat(webSocketListener.textMessages.poll(5, TimeUnit.SECONDS), is("echo message"));
session.close();
assertTrue(webSocketListener.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(webSocketListener.closeCode, is(StatusCode.NO_CODE));
// Verify that /test2 and /test3 could not be started.
ContentResponse response = client.GET(serverUri.resolve("/test2/badonopen/a"));
assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus());
client.GET("http://localhost:" + port + "/test3/badonopen/a");
assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus());
// Verify /test4 is able to establish a WebSocket connection.
webSocketListener = new WsListener();
session = wsClient.connect(webSocketListener, serverUri.resolve("/test4")).get(5, TimeUnit.SECONDS);
session.getRemote().sendString("echo message");
assertThat(webSocketListener.textMessages.poll(5, TimeUnit.SECONDS), is("echo message"));
session.close();
assertTrue(webSocketListener.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(webSocketListener.closeCode, is(StatusCode.NO_CODE));
}
}
}
public static class WsListener implements WebSocketListener
{
BlockingArrayQueue<String> textMessages = new BlockingArrayQueue<>();
private CountDownLatch closeLatch = new CountDownLatch(1);
private int closeCode;
@Override
public void onWebSocketClose(int statusCode, String reason)
{
this.closeCode = statusCode;
closeLatch.countDown();
}
@Override
public void onWebSocketText(String message)
{
textMessages.add(message);
}
}
@Test
public void testStartStopLog4j2Modules() throws Exception
{

View File

@ -18,27 +18,22 @@
package org.eclipse.jetty.tests.webapp.websocket;
import java.io.IOException;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ServerEndpoint("/onopen/{arg}")
public class OnOpenServerEndpoint
@ServerEndpoint(value = "/", decoders = {StringSequenceDecoder.class})
public class EchoEndpoint
{
private static final Logger LOGGER = LoggerFactory.getLogger(OnOpenServerEndpoint.class);
private static String open = "";
private static final Logger LOGGER = LoggerFactory.getLogger(EchoEndpoint.class);
@OnMessage
public String echo(String echo)
public String echo(StringSequence echo)
{
return open + echo;
return echo.toString();
}
@OnOpen
@ -46,12 +41,4 @@ public class OnOpenServerEndpoint
{
LOGGER.info("Session opened");
}
@OnError
public void onError(Session session, Throwable t)
throws IOException
{
String message = "Error happened:" + t.getMessage();
session.getBasicRemote().sendText(message);
}
}

View File

@ -0,0 +1,54 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.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.webapp.websocket;
public class StringSequence
implements CharSequence
{
public String stringBuffer;
public StringSequence(String hold)
{
stringBuffer = hold;
}
@Override
public int length()
{
return stringBuffer.length();
}
@Override
public char charAt(int index)
{
return stringBuffer.charAt(index);
}
@Override
public CharSequence subSequence(int start, int end)
{
return stringBuffer.subSequence(start, end);
}
@Override
public String toString()
{
return stringBuffer;
}
}

View File

@ -18,40 +18,33 @@
package org.eclipse.jetty.tests.webapp.websocket;
import java.io.IOException;
import jakarta.websocket.DecodeException;
import jakarta.websocket.Decoder;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ServerEndpoint("/onclose/{arg}")
public class OnCloseServerEndpoint
public class StringSequenceDecoder implements Decoder.Text<StringSequence>
{
private static final Logger LOGGER = LoggerFactory.getLogger(OnCloseServerEndpoint.class);
private static String close = "";
@OnMessage
public String echo(String echo)
@Override
public StringSequence decode(String s) throws DecodeException
{
return close + echo;
return new StringSequence(s);
}
@OnClose
public void onClose(Session session)
@Override
public void init(EndpointConfig config)
{
LOGGER.info("Session close");
}
@OnError
public void onError(Session session, Throwable t)
throws IOException
@Override
public void destroy()
{
String message = "Error happened:" + t.getMessage();
session.getBasicRemote().sendText(message);
}
@Override
public boolean willDecode(String s)
{
return true;
}
}