diff --git a/jetty-documentation/src/main/asciidoc/administration/jmx/using-jmx.adoc b/jetty-documentation/src/main/asciidoc/administration/jmx/using-jmx.adoc
index 4113599a59c..b14cb261df3 100644
--- a/jetty-documentation/src/main/asciidoc/administration/jmx/using-jmx.adoc
+++ b/jetty-documentation/src/main/asciidoc/administration/jmx/using-jmx.adoc
@@ -17,93 +17,93 @@
[[using-jmx]]
=== Using JMX with Jetty
-Jetty JMX integration uses the platform MBean server implementation that Java VM provides.
-The integration is based on the `ObjectMBean` implementation of `DynamicMBean`.
-This implementation allows you to wrap an arbitrary POJO in an MBean and annotate it appropriately to expose it via JMX.
-See xref:jetty-jmx-annotations[].
+Jetty's architecture is based on POJO components (see xref:basic-architecture[]).
+These components are organized in a tree and each component may have a lifecycle
+that spans the `Server` lifetime, or a web application lifetime, or even shorter
+lifetimes such as that of a TCP connection.
-The `MBeanContainer` implementation of the `Container.Listener` interface coordinates creation of MBeans.
-The Jetty Server and it's components use a link:{JDURL}/org/eclipse/jetty/util/component/Container.html[Container] to maintain a containment tree of components and to support notification of changes to that tree.
-The `MBeanContainer` class listens for Container events and creates and destroys MBeans as required to wrap all Jetty components.
+Every time a component is added or removed from the component tree, an event is
+emitted, and link:{JDURL}/org/eclipse/jetty/util/component/Container.html[`Container.Listener`]
+implementations can listen to those events and perform additional actions.
-You can access the MBeans that Jetty publishes both through built-in Java VM connector via JConsole or JMC, or by registering a remote JMX connector and using a remote JMX agent to monitor Jetty.
+One such `Container.Listener` is `MBeanContainer` that uses `ObjectMBean` to
+create an MBean from an arbitrary POJO, and register/unregister the MBean to/from
+the platform `MBeanServer`.
+
+Jetty components are annotated with <>
+and provide specific JMX details so that `ObjectMBean` can build a more
+precise representation of the JMX metadata associated with the component POJO.
+
+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 Jetty MBeans can be accessed via any JMX console such as Java Mission Control
+(JMC), VisualVM, JConsole or others.
[[configuring-jmx]]
==== Configuring JMX
-This guide describes how to initialize and configure the Jetty JMX integration.
+This guide describes the various ways to initialize and configure the Jetty JMX integration.
+Configuring the Jetty JMX integration only registers the Jetty MBeans into the platform
+`MBeanServer`, and therefore the MBeans can only be accessed locally (from the same machine),
+not from remote machines.
-To monitor an application using JMX, perform the following steps:
-
-* Configure the application to instantiate an MBean container.
-* Instrument objects to be MBeans.
-* Provide access for JMX agents to MBeans.
-
-[[accessing-jetty-mbeans]]
-===== Using JConsole to Access Jetty MBeans
-
-The simplest way to access the MBeans that Jetty publishes is to use the http://java.sun.com/developer/technicalArticles/J2SE/jconsole.html[JConsole utility] the Java Virtual Machine supplies.
-See xref:jetty-jconsole[] for instructions on how to configure JVM for use with JConsole or JMC.
-
-To access Jetty MBeans via JConsole or JMC, you must:
-
-* Enable the registration of Jetty MBeans into the platform MBeanServer.
-* Enable a `JMXConnectorServer` so that JConsole/JMC can connect and visualize the MBeans.
-
-[[registering-jetty-mbeans]]
-===== Registering Jetty MBeans
-
-Configuring Jetty JMX integration differs for standalone and embedded Jetty.
+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 Jetty where runs is remote, or only accessible via SSH or otherwise
+without graphical user interface support.
+In these cases, you have to enable <>.
[[jmx-standalone-jetty]]
-====== Standalone Jetty
+===== Standalone Jetty Server
JMX is not enabled by default in the Jetty distribution.
-To enable JMX in the Jetty distribution, run the following, where `{$jetty.home}` is the directory where you have the Jetty distribution located (see link:#startup-base-and-home[the documentation for Jetty base vs. home examples]):
+To enable JMX in the Jetty distribution run the following, where `{$jetty.home}`
+is the directory where you have the Jetty distribution installed, and
+`${jetty.base}` is the directory where you have your Jetty configuration
+(see link:#startup-base-and-home[the documentation for Jetty base vs. home examples]):
[source, screen, subs="{sub-order}"]
-....
+----
+$ cd ${jetty.base}
$ java -jar {$jetty.home}/start.jar --add-to-start=jmx
-....
+----
-Running the above command will append the available configurable elements of the JMX module to the `{$jetty.base}/start.ini` file.
-If you are managing separate ini files for your modules in the distribution, use `--add-to-start.d=jmx` instead.
-
-If you wish to add remote access for JMX, you will also need to enable the JMX-Remote module:
-
-[source, screen, subs="{sub-order}"]
-....
-$ java -jar {$jetty.home}/start.jar --add-to-start=jmx-remote
-....
+Running the above command will append the available configurable elements of the `jmx` module
+to the `{$jetty.base}/start.ini` file, or create the `${jetty.base}/start.d/jmx.ini` file.
[[jmx-embedded-jetty]]
-====== Embedded Jetty
+===== Embedded Jetty Server
-When running Jetty embedded into an application, create and configure an MBeanContainer instance as follows:
+When running Jetty embedded into an application, create and configure an `MBeanContainer`
+instance as follows:
[source, java]
----
-
Server server = new Server();
-// Setup JMX
-MBeanContainer mbContainer=new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
-server.addEventListener(mbContainer);
-server.addBean(mbContainer);
+// Setup JMX.
+MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
+server.addBean(mbeanContainer);
-// Add loggers MBean to server (will be picked up by MBeanContainer above)
+// Export the loggers as MBeans.
server.addBean(Log.getLog());
-
----
-Notice that Jetty creates the `MBeanContainer` immediately after creating the Server, and immediately after registering it as an `EventListener` of the Server object (which is also a Container object).
+Because logging is initialized prior to the `MBeanContainer` (even before the `Server` itself),
+it is necessary to register the logger manually via `server.addBean()` so that the loggers may
+show up in the JMX tree as MBeans.
-Because logging is initialized prior to the `MBeanContainer` (even before the Server itself), it is necessary to register the logger manually via `server.addBean()` so that the loggers may show up in the JMX tree.
-
-[[jmx-using-jetty-maven-plugin]]
+[[jmx-jetty-maven-plugin]]
===== Using the Jetty Maven Plugin with JMX
-If you are using the link:#jetty-maven-plugin[Jetty Maven plugin] you should copy the `/etc/jetty-jmx.xml` file into your webapp project somewhere, such as `/src/etc,` then add a `` element to the plugin ``:
+If you are using the link:#jetty-maven-plugin[Jetty Maven plugin] you should copy the
+`${jetty.home}/etc/jetty-jmx.xml` file into your webapp project somewhere, such as
+`src/main/config/etc/`, then add a
+`` element to the `` element of the Jetty Maven Plugin:
[source, xml, subs="{sub-order}"]
----
@@ -113,21 +113,153 @@ If you are using the link:#jetty-maven-plugin[Jetty Maven plugin] you should cop
{VERSION}
10
- src/etc/jetty-jmx.xml
+ src/main/config/etc/jetty-jmx.xml
----
+[[accessing-jetty-mbeans]]
+==== Using JConsole or Java Mission Control to Access Jetty MBeans
-[[enabling-jmxconnectorserver-for-remote-access]]
-==== Enabling JMXConnectorServer for Remote Access
+The simplest way to access the MBeans that Jetty publishes is to use
+<>.
-There are two ways of enabling remote connectivity so that JConsole or JMC can connect to visualize MBeans.
+Both these tools can connect to local or remote JVMs to display the MBeans.
+
+For local access, you just need to start JConsole or JMC and then choose
+from their user interface the local JVM you want to connect to.
+
+For remote access, you need first to enable <>
+in Jetty.
+
+[[jmx-remote-access]]
+==== Enabling JMX Remote Access
+
+There are two ways of enabling remote connectivity so that JConsole or 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.
-To enable use of this class, uncomment the correspondent portion in `/etc/jetty-jmx.xml,` like this:
+* Use Jetty's `jmx-remote` module or - equivalently - the `ConnectorServer` class.
+
+`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 configuration for the RMI registry and the RMI server is specified by a `JMXServiceURL`.
+The string format of an RMI `JMXServiceURL` is:
+
+[source, screen, subs="{sub-order}"]
+----
+service:jmx:rmi://:/jndi/rmi://:/jmxrmi
+----
+
+Default values are:
+
+[source, screen, subs="{sub-order}"]
+----
+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.
+
+Examples:
+
+[source, screen, subs="{sub-order}"]
+----
+service:jmx:rmi:///jndi/rmi:///jmxrmi
+ rmi_server_host = any address
+ rmi_server_port = randomly chosen
+ 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
+<>.
+====
+
+===== Enabling JMX Remote Access in Standalone Jetty Server
+
+Similarly to <>, you
+enable the `jmx-remote` module:
+
+[source, screen, subs="{sub-order}"]
+----
+$ cd ${jetty.base}
+$ java -jar {$jetty.home}/start.jar --add-to-start=jmx-remote
+----
+
+===== Enabling JMX Remote Access in Embedded Jetty
+
+When running Jetty embedded into an application, create and configure a `ConnectorServer`:
+
+[source, java, subs="{sub-order}"]
+----
+Server server = new Server();
+
+// Setup JMX
+MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
+server.addBean(mbeanContainer);
+
+// Setup ConnectorServer
+JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1999, "/jndi/rmi:///jmxrmi");
+ConnectorServer jmxServer = new ConnectorServer(jmxURL, "org.eclipse.jetty.jmx:name=rmiconnectorserver");
+server.addBean(jmxServer);
+----
+
+The `JMXServiceURL` above specifies that the RMI server binds to the wildcard address
+on port 1999, while the RMI registry binds to the wildcard address on port 1099 (the
+default RMI registry port).
+
+[[jmx-remote-access-authorization]]
+===== JMX Remote Access Authorization
+
+The standard `JMXConnectorServer` provides several options to authorize access.
+For a complete guide to controlling authentication and authorization in JMX, see
+https://blogs.oracle.com/lmalventosa/entry/jmx_authentication_authorization[Authentication and Authorization in JMX RMI connectors].
+
+To authorize access to the `JMXConnectorServer` you can use this configuration,
+where the `jmx.password` and `jmx.access` files have the format specified in the blog entry above:
[source, xml, subs="{sub-order}"]
----
@@ -136,50 +268,22 @@ To enable use of this class, uncomment the correspondent portion in `/etc/jetty-
rmi
-
- /jndi/rmi://:/jmxrmi
-
-
- org.eclipse.jetty.jmx:name=rmiconnectorserver
-
-
-
-----
-
-This configuration snippet starts an `RMIRegistry` and a `JMXConnectorServer` both on port 1099 (by default), so that firewalls should open just that one port to allow connections from JConsole or JMC.
-
-[[securing-remote-access]]
-==== Securing Remote Access
-
-`JMXConnectorServer` several options to restrict access.
-For a complete guide to controlling authentication and authorization in JMX, see https://blogs.oracle.com/lmalventosa/entry/jmx_authentication_authorization[Authentication and Authorization in JMX RMI connectors] in Luis-Miguel Alventosa's blog.
-
-To restrict access to the `JMXConnectorServer`, you can use this configuration, where the `jmx.password` and `jmx.access` files have the format specified in the blog entry above:
-
-[source, xml, subs="{sub-order}"]
-----
-
-
-
-
- rmi
-
-
- /jndi/rmi://:/jmxrmi
+ 1099
+ /jndi/rmi:///jmxrmi
@@ -187,17 +291,141 @@ To restrict access to the `JMXConnectorServer`, you can use this configuration,
org.eclipse.jetty.jmx:name=rmiconnectorserver
-
-
----
-[[custom-monitor-applcation]]
-==== Custom Monitor Application
+Similarly, in code:
-Using the JMX API, you can also write a custom application to monitor your Jetty server.
-To allow this application to connect to your Jetty server, you need to uncomment the last section of the `/etc/jetty-jmx.xml` configuration file and optionally modify the endpoint name.
-Doing so creates a JMX HTTP connector and registers a JMX URL that outputs to the `Stderr` log.
+[source, java, subs="{sub-order}"]
+----
+JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1099, "/jndi/rmi:///jmxrmi");
+Map env = new HashMap<>();
+env.put("jmx.remote.x.access.file", "resources/jmx.access");
+env.put("jmx.remote.x.password.file", "resources/jmx.password");
+ConnectorServer jmxServer = new ConnectorServer(jmxURL, env, "org.eclipse.jetty.jmx:name=rmiconnectorserver");
+jmxServer.start();
+----
-You should provide the URL that appears in the log to your monitor application in order to create an `MBeanServerConnection.`
-You can use the same URL to connect to your Jetty instance from a remote machine using JConsole or JMC.
-See the link:{GITBROWSEURL}/jetty-jmx/src/main/config/etc/jetty-jmx.xml[configuration file] for more details.
+Calling `ConnectorServer.start()` may be explicit as in the examples above,
+or can be skipped when adding the `ConnectorServer` as a bean to the `Server`,
+so that starting the `Server` will also start the `ConnectorServer`.
+
+===== 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, xml, subs="{sub-order}"]
+----
+
+
+
+ rmi
+
+ 1099
+ /jndi/rmi:///jmxrmi
+
+
+
+ org.eclipse.jetty.jmx:name=rmiconnectorserver
+
+
+----
+
+Similarly, in code:
+
+[source, java, subs="{sub-order}"]
+----
+SslContextFactory sslContextFactory = new SslContextFactory();
+sslContextFactory.setKeyStorePath();
+sslContextFactory.setKeyStorePassword("secret");
+
+JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1099, "/jndi/rmi:///jmxrmi");
+ConnectorServer jmxServer = new ConnectorServer(jmxURL, null, "org.eclipse.jetty.jmx:name=rmiconnectorserver", sslContextFactory);
+----
+
+It is possible to use the same `SslContextFactory` used to configure the
+Jetty `ServerConnector` that supports TLS for the HTTP protocol.
+This is used in the XML example above: the `SslContextFactory` configured
+for the TLS `ServerConnector` is registered with an id of `sslContextFactory`
+which is referenced in the XML via the `Ref` element.
+
+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 to `domain.com`:
+
+[source, java, subs="{sub-order}"]
+----
+// 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);
+}
+----
+
+Similarly, to launch JMC:
+
+[source, java, subs="{sub-order}"]
+----
+$ jmc -vmargs -Djavax.net.ssl.trustStore=/path/to/trustStore -Djavax.net.ssl.trustStorePassword=secret
+----
+
+Note that 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.
+
+[[jmx-remote-access-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 a 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, subs="{sub-order}"]
+----
+$ 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.
diff --git a/jetty-jmx/src/main/config/etc/jetty-jmx-remote.xml b/jetty-jmx/src/main/config/etc/jetty-jmx-remote.xml
index bd6cbc6e7b8..1003ad7e261 100644
--- a/jetty-jmx/src/main/config/etc/jetty-jmx-remote.xml
+++ b/jetty-jmx/src/main/config/etc/jetty-jmx-remote.xml
@@ -13,14 +13,14 @@
-->
-
@@ -28,9 +28,9 @@
rmi
-
-
- /jndi/rmi://:/jmxrmi
+
+
+ /jndi/rmi://:/jmxrmi
org.eclipse.jetty.jmx:name=rmiconnectorserver
diff --git a/jetty-jmx/src/main/config/etc/jetty-jmx.xml b/jetty-jmx/src/main/config/etc/jetty-jmx.xml
index e07ea744355..ac906d8172f 100644
--- a/jetty-jmx/src/main/config/etc/jetty-jmx.xml
+++ b/jetty-jmx/src/main/config/etc/jetty-jmx.xml
@@ -4,13 +4,13 @@
-
+
-
+
@@ -25,7 +25,7 @@
-
+
diff --git a/jetty-jmx/src/main/config/modules/jmx-remote.mod b/jetty-jmx/src/main/config/modules/jmx-remote.mod
index 7a10a018144..438f3368ef9 100644
--- a/jetty-jmx/src/main/config/modules/jmx-remote.mod
+++ b/jetty-jmx/src/main/config/modules/jmx-remote.mod
@@ -8,8 +8,14 @@ jmx
etc/jetty-jmx-remote.xml
[ini-template]
-## The host/address to bind RMI to
-# jetty.jmxremote.rmihost=localhost
+## The host/address to bind the RMI server to.
+# jetty.jmxremote.rmiserverhost=localhost
-## The port RMI listens to
-# jetty.jmxremote.rmiport=1099
+## The port the RMI server listens to (0 means a random port is chosen).
+# jetty.jmxremote.rmiserverport=1099
+
+## The host/address to bind the RMI registry to.
+# jetty.jmxremote.rmiregistryhost=localhost
+
+## The port the RMI registry listens to.
+# jetty.jmxremote.rmiregistryport=1099
diff --git a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ConnectorServer.java b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ConnectorServer.java
index 9dea1d260eb..1901bd57252 100644
--- a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ConnectorServer.java
+++ b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ConnectorServer.java
@@ -18,160 +18,185 @@
package org.eclipse.jetty.jmx;
+import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.net.ServerSocket;
+import java.net.UnknownHostException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
+import java.rmi.server.RMIClientSocketFactory;
+import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
+import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
+import java.util.function.IntConsumer;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
+import javax.management.remote.rmi.RMIConnectorServer;
+import javax.rmi.ssl.SslRMIClientSocketFactory;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ShutdownThread;
-
-/* ------------------------------------------------------------ */
/**
- * AbstractLifeCycle wrapper for JMXConnector Server
+ * LifeCycle wrapper for JMXConnectorServer.
+ * This class provides the following facilities:
+ *
+ * - participates in the {@code Server} lifecycle
+ * - starts the RMI registry if not there already
+ * - allows to bind the RMI registry and the RMI server to the loopback interface
+ * - makes it easy to use TLS for the JMX communication
+ *
*/
public class ConnectorServer extends AbstractLifeCycle
{
+ public static final String RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE = "com.sun.jndi.rmi.factory.socket";
private static final Logger LOG = Log.getLogger(ConnectorServer.class);
- JMXConnectorServer _connectorServer;
- Registry _registry;
+ private JMXServiceURL _jmxURL;
+ private final Map _environment;
+ private final String _objectName;
+ private final SslContextFactory _sslContextFactory;
+ private int _registryPort;
+ private int _rmiPort;
+ private JMXConnectorServer _connectorServer;
+ private Registry _registry;
- /* ------------------------------------------------------------ */
/**
- * Constructs connector server
+ * Constructs a ConnectorServer
*
- * @param serviceURL the address of the new connector server.
- * The actual address of the new connector server, as returned
- * by its getAddress method, will not necessarily be exactly the same.
- * @param name object name string to be assigned to connector server bean
- * @throws Exception if unable to setup connector server
+ * @param serviceURL the address of the new ConnectorServer
+ * @param name object name string to be assigned to ConnectorServer bean
*/
public ConnectorServer(JMXServiceURL serviceURL, String name)
- throws Exception
{
this(serviceURL, null, name);
}
- /* ------------------------------------------------------------ */
/**
- * Constructs connector server
+ * Constructs a ConnectorServer
*
- * @param svcUrl the address of the new connector server.
- * The actual address of the new connector server, as returned
- * by its getAddress method, will not necessarily be exactly the same.
- * @param environment a set of attributes to control the new connector
- * server's behavior. This parameter can be null. Keys in this map must
- * be Strings. The appropriate type of each associated value depends on
- * the attribute. The contents of environment are not changed by this call.
- * @param name object name string to be assigned to connector server bean
- * @throws Exception if unable to create connector server
+ * @param svcUrl the address of the new ConnectorServer
+ * @param environment a set of attributes to control the new ConnectorServer's behavior.
+ * This parameter can be null. Keys in this map must
+ * be Strings. The appropriate type of each associated value depends on
+ * the attribute. The contents of environment are not changed by this call.
+ * @param name object name string to be assigned to ConnectorServer bean
*/
- public ConnectorServer(JMXServiceURL svcUrl, Map environment, String name)
- throws Exception
+ public ConnectorServer(JMXServiceURL svcUrl, Map environment, String name)
{
- String urlPath = svcUrl.getURLPath();
- int idx = urlPath.indexOf("rmi://");
- if (idx > 0)
+ this(svcUrl, environment, name, null);
+ }
+
+ public ConnectorServer(JMXServiceURL svcUrl, Map environment, String name, SslContextFactory sslContextFactory)
+ {
+ this._jmxURL = svcUrl;
+ this._environment = environment == null ? new HashMap<>() : new HashMap<>(environment);
+ this._objectName = name;
+ this._sslContextFactory = sslContextFactory;
+ }
+
+ public JMXServiceURL getAddress()
+ {
+ return _jmxURL;
+ }
+
+ @Override
+ public void doStart() throws Exception
+ {
+ boolean rmi = "rmi".equals(_jmxURL.getProtocol());
+ if (rmi)
{
- String hostPort = urlPath.substring(idx+6, urlPath.indexOf('/', idx+6));
- String regHostPort = startRegistry(hostPort);
- if (regHostPort != null) {
- urlPath = urlPath.replace(hostPort,regHostPort);
- svcUrl = new JMXServiceURL(svcUrl.getProtocol(), svcUrl.getHost(), svcUrl.getPort(), urlPath);
+ if (!_environment.containsKey(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE))
+ _environment.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new JMXRMIServerSocketFactory(_jmxURL.getHost(), port -> _rmiPort = port));
+ if (_sslContextFactory != null)
+ {
+ SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
+ if (!_environment.containsKey(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE))
+ _environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
+ if (!_environment.containsKey(RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE))
+ _environment.put(RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
}
}
- MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
- _connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(svcUrl, environment, mbeanServer);
- mbeanServer.registerMBean(_connectorServer,new ObjectName(name));
- }
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart()
- */
- @Override
- public void doStart()
- throws Exception
- {
+ String urlPath = _jmxURL.getURLPath();
+ String jndiRMI = "/jndi/rmi://";
+ if (urlPath.startsWith(jndiRMI))
+ {
+ int startIndex = jndiRMI.length();
+ int endIndex = urlPath.indexOf('/', startIndex);
+ HostPort hostPort = new HostPort(urlPath.substring(startIndex, endIndex));
+ String registryHost = startRegistry(hostPort);
+ // If the RMI registry was already started, use the existing port.
+ if (_registryPort == 0)
+ _registryPort = hostPort.getPort();
+ urlPath = jndiRMI + registryHost + ":" + _registryPort + urlPath.substring(endIndex);
+ // Rebuild JMXServiceURL to use it for the creation of the JMXConnectorServer.
+ _jmxURL = new JMXServiceURL(_jmxURL.getProtocol(), _jmxURL.getHost(), _jmxURL.getPort(), urlPath);
+ }
+
+ MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
+ _connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(_jmxURL, _environment, mbeanServer);
+ mbeanServer.registerMBean(_connectorServer, new ObjectName(_objectName));
_connectorServer.start();
+ String rmiHost = normalizeHost(_jmxURL.getHost());
+ // If _rmiPort is still zero, it's using the same port as the RMI registry.
+ if (_rmiPort == 0)
+ _rmiPort = _registryPort;
+ _jmxURL = new JMXServiceURL(_jmxURL.getProtocol(), rmiHost, _rmiPort, urlPath);
+
ShutdownThread.register(0, this);
- LOG.info("JMX Remote URL: {}", _connectorServer.getAddress().toString());
+ LOG.info("JMX URL: {}", _jmxURL);
}
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop()
- */
@Override
- public void doStop()
- throws Exception
+ public void doStop() throws Exception
{
ShutdownThread.deregister(this);
_connectorServer.stop();
+ MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
+ mbeanServer.unregisterMBean(new ObjectName(_objectName));
stopRegistry();
}
- /**
- * Check that local RMI registry is used, and ensure it is started. If local RMI registry is being used and not started, start it.
- *
- * @param hostPath
- * hostname and port number of RMI registry
- * @throws Exception
- */
- private String startRegistry(String hostPath) throws Exception
+ private String startRegistry(HostPort hostPort) throws Exception
{
- HostPort hostPort = new HostPort(hostPath);
+ String host = hostPort.getHost();
+ int port = hostPort.getPort(1099);
- String rmiHost = hostPort.getHost();
- int rmiPort = hostPort.getPort(1099);
-
- // Verify that local registry is being used
- InetAddress hostAddress = InetAddress.getByName(rmiHost);
- if(hostAddress.isLoopbackAddress())
+ try
{
- if (rmiPort == 0)
- {
- ServerSocket socket = new ServerSocket(0);
- rmiPort = socket.getLocalPort();
- socket.close();
- }
- else
- {
- try
- {
- // Check if a local registry is already running
- LocateRegistry.getRegistry(rmiPort).list();
- return null;
- }
- catch (Exception ex)
- {
- LOG.ignore(ex);
- }
- }
-
- _registry = LocateRegistry.createRegistry(rmiPort);
- Thread.sleep(1000);
-
- rmiHost = HostPort.normalizeHost(InetAddress.getLocalHost().getCanonicalHostName());
- return rmiHost + ':' + Integer.toString(rmiPort);
+ // Check if a local registry is already running.
+ LocateRegistry.getRegistry(host, port).list();
+ return normalizeHost(host);
+ }
+ catch (Throwable ex)
+ {
+ LOG.ignore(ex);
}
- return null;
+ RMIClientSocketFactory csf = _sslContextFactory == null ? null : new SslRMIClientSocketFactory();
+ RMIServerSocketFactory ssf = new JMXRMIServerSocketFactory(host, p -> _registryPort = p);
+ _registry = LocateRegistry.createRegistry(port, csf, ssf);
+
+ return normalizeHost(host);
+ }
+
+ private String normalizeHost(String host) throws UnknownHostException
+ {
+ return host == null || host.isEmpty() ? InetAddress.getLocalHost().getHostName() : host;
}
private void stopRegistry()
@@ -180,12 +205,69 @@ public class ConnectorServer extends AbstractLifeCycle
{
try
{
- UnicastRemoteObject.unexportObject(_registry,true);
+ UnicastRemoteObject.unexportObject(_registry, true);
}
catch (Exception ex)
{
LOG.ignore(ex);
}
+ finally
+ {
+ _registry = null;
+ }
+ }
+ }
+
+ private class JMXRMIServerSocketFactory implements RMIServerSocketFactory
+ {
+ private final String _host;
+ private final IntConsumer _portConsumer;
+
+ private JMXRMIServerSocketFactory(String host, IntConsumer portConsumer)
+ {
+ this._host = host;
+ this._portConsumer = portConsumer;
+ }
+
+ @Override
+ public ServerSocket createServerSocket(int port) throws IOException
+ {
+ InetAddress address = _host == null || _host.isEmpty() ? null : InetAddress.getByName(_host);
+ ServerSocket server = createServerSocket(address, port);
+ _portConsumer.accept(server.getLocalPort());
+ return server;
+ }
+
+ private ServerSocket createServerSocket(InetAddress address, int port) throws IOException
+ {
+ // A null address binds to the wildcard address.
+ if (_sslContextFactory == null)
+ {
+ ServerSocket server = new ServerSocket();
+ server.bind(new InetSocketAddress(address, port));
+ return server;
+ }
+ else
+ {
+ return _sslContextFactory.newSslServerSocket(address == null ? null : address.getHostName(), port, 0);
+ }
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return _host != null ? _host.hashCode() : 0;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null || getClass() != obj.getClass())
+ return false;
+ JMXRMIServerSocketFactory that = (JMXRMIServerSocketFactory)obj;
+ return Objects.equals(_host, that._host);
}
}
}
diff --git a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java
index d8504b7be84..9bee1f6eb91 100644
--- a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java
+++ b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java
@@ -18,95 +18,217 @@
package org.eclipse.jetty.jmx;
+import java.net.ConnectException;
import java.net.InetAddress;
-import java.rmi.registry.LocateRegistry;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThat;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
+import javax.rmi.ssl.SslRMIClientSocketFactory;
+
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
-import static org.hamcrest.core.Is.is;
-import static org.hamcrest.core.AnyOf.anyOf;
+/**
+ * Running the tests of this class in the same JVM results often in
+ *
+ * Caused by: java.rmi.NoSuchObjectException: no such object in table
+ * at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:276)
+ * at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:253)
+ * at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:379)
+ * at sun.rmi.registry.RegistryImpl_Stub.bind(Unknown Source)
+ *
+ * Running each test method in a forked JVM makes these tests all pass,
+ * therefore the issue is likely caused by use of stale stubs cached by the JDK.
+ */
+@Ignore
public class ConnectorServerTest
{
-
+ private String objectName = "org.eclipse.jetty:name=rmiconnectorserver";
private ConnectorServer connectorServer;
@After
public void tearDown() throws Exception
{
if (connectorServer != null)
- {
- connectorServer.doStop();
- }
+ connectorServer.stop();
}
@Test
- public void randomPortTest() throws Exception
+ public void testAddressAfterStart() throws Exception
{
- // given
- connectorServer = new ConnectorServer(new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:0/jettytest"),
- "org.eclipse.jetty:name=rmiconnectorserver");
- // if port is not available then the server value is null
- if (connectorServer != null)
- {
- // when
- connectorServer.start();
-
- // then
- assertThat("Server status must be in started or starting",connectorServer.getState(),
- anyOf(is(ConnectorServer.STARTED),is(ConnectorServer.STARTING)));
- }
- }
-
- @Test
- @Ignore // collides on ci server
- public void testConnServerWithRmiDefaultPort() throws Exception
- {
- // given
- LocateRegistry.createRegistry(1099);
- JMXServiceURL serviceURLWithOutPort = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi");
- connectorServer = new ConnectorServer(serviceURLWithOutPort," domain: key3 = value3");
-
- // when
+ connectorServer = new ConnectorServer(new JMXServiceURL("service:jmx:rmi:///jndi/rmi:///jmxrmi"), objectName);
connectorServer.start();
- // then
- assertThat("Server status must be in started or starting",connectorServer.getState(),anyOf(is(ConnectorServer.STARTED),is(ConnectorServer.STARTING)));
+ JMXServiceURL address = connectorServer.getAddress();
+ Assert.assertTrue(address.toString().matches("service:jmx:rmi://[^:]+:\\d+/jndi/rmi://[^:]+:\\d+/jmxrmi"));
}
@Test
- public void testConnServerWithRmiRandomPort() throws Exception
+ public void testNoRegistryHostBindsToAny() throws Exception
{
- // given
- JMXServiceURL serviceURLWithOutPort = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1199/jmxrmi");
- connectorServer = new ConnectorServer(serviceURLWithOutPort," domain: key4 = value4");
- // if port is not available then the server value is null
- if (connectorServer != null)
- {
- // when
- connectorServer.start();
- connectorServer.stop();
+ connectorServer = new ConnectorServer(new JMXServiceURL("service:jmx:rmi:///jndi/rmi:///jmxrmi"), objectName);
+ connectorServer.start();
- // then
- assertThat("Server status must be in started or starting",connectorServer.getState(),
- anyOf(is(ConnectorServer.STOPPING),is(ConnectorServer.STOPPED)));
+ // Verify that I can connect to the RMI registry using a non-loopback address.
+ new Socket(InetAddress.getLocalHost(), 1099).close();
+ // Verify that I can connect to the RMI registry using the loopback address.
+ new Socket(InetAddress.getLoopbackAddress(), 1099).close();
+ }
+
+ @Test
+ public void testNoRegistryHostNonDefaultRegistryPort() throws Exception
+ {
+ int registryPort = 1299;
+ connectorServer = new ConnectorServer(new JMXServiceURL("service:jmx:rmi:///jndi/rmi://:" + registryPort + "/jmxrmi"), objectName);
+ connectorServer.start();
+
+ // Verify that I can connect to the RMI registry using a non-loopback address.
+ new Socket(InetAddress.getLocalHost(), registryPort).close();
+ // Verify that I can connect to the RMI registry using the loopback address.
+ new Socket(InetAddress.getLoopbackAddress(), registryPort).close();
+ }
+
+ @Test
+ public void testNoRMIHostBindsToAny() throws Exception
+ {
+ connectorServer = new ConnectorServer(new JMXServiceURL("service:jmx:rmi:///jndi/rmi:///jmxrmi"), objectName);
+ connectorServer.start();
+
+ // Verify that I can connect to the RMI server using a non-loopback address.
+ new Socket(InetAddress.getLocalHost(), connectorServer.getAddress().getPort()).close();
+ // Verify that I can connect to the RMI server using the loopback address.
+ new Socket(InetAddress.getLoopbackAddress(), connectorServer.getAddress().getPort()).close();
+ }
+
+ @Test
+ public void testLocalhostRegistryBindsToLoopback() throws Exception
+ {
+ connectorServer = new ConnectorServer(new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi"), objectName);
+ connectorServer.start();
+
+ InetAddress localHost = InetAddress.getLocalHost();
+ if (!localHost.isLoopbackAddress())
+ {
+ try
+ {
+ // Verify that I cannot connect to the RMIRegistry using a non-loopback address.
+ new Socket(localHost, 1099);
+ Assert.fail();
+ }
+ catch (ConnectException ignored)
+ {
+ // Ignored.
+ }
+ }
+
+ InetAddress loopback = InetAddress.getLoopbackAddress();
+ new Socket(loopback, 1099).close();
+ }
+
+ @Test
+ public void testLocalhostRMIBindsToLoopback() throws Exception
+ {
+ connectorServer = new ConnectorServer(new JMXServiceURL("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"), objectName);
+ connectorServer.start();
+ JMXServiceURL address = connectorServer.getAddress();
+
+ InetAddress localHost = InetAddress.getLocalHost();
+ if (!localHost.isLoopbackAddress())
+ {
+ try
+ {
+ // Verify that I cannot connect to the RMIRegistry using a non-loopback address.
+ new Socket(localHost, address.getPort());
+ Assert.fail();
+ }
+ catch (ConnectException ignored)
+ {
+ // Ignored.
+ }
+ }
+
+ InetAddress loopback = InetAddress.getLoopbackAddress();
+ new Socket(loopback, address.getPort()).close();
+ }
+
+ @Test
+ public void testRMIServerPort() throws Exception
+ {
+ ServerSocket server = new ServerSocket(0);
+ int port = server.getLocalPort();
+ server.close();
+
+ connectorServer = new ConnectorServer(new JMXServiceURL("service:jmx:rmi://localhost:" + port + "/jndi/rmi:///jmxrmi"), objectName);
+ connectorServer.start();
+
+ JMXServiceURL address = connectorServer.getAddress();
+ Assert.assertEquals(port, address.getPort());
+
+ InetAddress loopback = InetAddress.getLoopbackAddress();
+ new Socket(loopback, port).close();
+ }
+
+ @Test
+ public void testRMIServerAndRMIRegistryOnSameHostAndSamePort() throws Exception
+ {
+ // RMI can multiplex connections on the same address and port for different
+ // RMI objects, in this case the RMI registry and the RMI server. In this
+ // case, the RMIServerSocketFactory will be invoked only once.
+ // The case with different address and same port is already covered by TCP,
+ // that can listen to 192.168.0.1:1099 and 127.0.0.1:1099 without problems.
+
+ String host = "localhost";
+ int port = 1399;
+ connectorServer = new ConnectorServer(new JMXServiceURL("rmi", host, port, "/jndi/rmi://" + host + ":" + port + "/jmxrmi"), objectName);
+ connectorServer.start();
+
+ JMXServiceURL address = connectorServer.getAddress();
+ Assert.assertEquals(port, address.getPort());
+ }
+
+ @Test
+ public void testJMXOverTLS() throws Exception
+ {
+ SslContextFactory sslContextFactory = new SslContextFactory();
+ String keyStorePath = MavenTestingUtils.getTestResourcePath("keystore.jks").toString();
+ String keyStorePassword = "storepwd";
+ sslContextFactory.setKeyStorePath(keyStorePath);
+ sslContextFactory.setKeyStorePassword(keyStorePassword);
+ sslContextFactory.start();
+
+ // The RMIClientSocketFactory is stored within the RMI stub.
+ // When using TLS, the stub is deserialized in a possibly different
+ // JVM that does not have access to the server keystore, and there
+ // is no way to provide TLS configuration during the deserialization
+ // of the stub. Therefore the client must provide system properties
+ // to specify the TLS configuration. For this test it needs the
+ // trustStore because the server certificate is self-signed.
+ // The server needs to contact the RMI registry and therefore also
+ // needs these system properties.
+ System.setProperty("javax.net.ssl.trustStore", keyStorePath);
+ System.setProperty("javax.net.ssl.trustStorePassword", keyStorePassword);
+
+ connectorServer = new ConnectorServer(new JMXServiceURL("rmi", null, 1100, "/jndi/rmi://localhost:1100/jmxrmi"), null, objectName, sslContextFactory);
+ connectorServer.start();
+
+ // The client needs to talk TLS to the RMI registry to download
+ // the RMI server stub, and this is independent from JMX.
+ // The RMI server stub then contains the SslRMIClientSocketFactory
+ // needed to talk to the RMI server.
+ Map clientEnv = new HashMap<>();
+ clientEnv.put(ConnectorServer.RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory());
+ try (JMXConnector client = JMXConnectorFactory.connect(connectorServer.getAddress(), clientEnv))
+ {
+ client.getMBeanServerConnection().queryNames(null, null);
}
}
-
- @Test
- @Ignore
- public void testIsLoopbackAddressWithWrongValue() throws Exception
- {
- // given
- JMXServiceURL serviceURLWithOutPort = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + InetAddress.getLocalHost() + ":1199/jmxrmi");
-
- // when
- connectorServer = new ConnectorServer(serviceURLWithOutPort," domain: key5 = value5");
-
- // then
- assertNull("As loopback address returns false...registry must be null",connectorServer._registry);
- }
}
diff --git a/jetty-jmx/src/test/resources/jetty-logging.properties b/jetty-jmx/src/test/resources/jetty-logging.properties
index 3d40866a4f6..f0392ad78c1 100644
--- a/jetty-jmx/src/test/resources/jetty-logging.properties
+++ b/jetty-jmx/src/test/resources/jetty-logging.properties
@@ -1,2 +1,2 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
-org.eclipse.jetty.jmx.LEVEL=WARN
+org.eclipse.jetty.jmx.LEVEL=INFO
diff --git a/jetty-jmx/src/test/resources/keystore.jks b/jetty-jmx/src/test/resources/keystore.jks
new file mode 100644
index 00000000000..428ba54776e
Binary files /dev/null and b/jetty-jmx/src/test/resources/keystore.jks differ
diff --git a/jetty-quickstart/src/test/java/org/eclipse/jetty/quickstart/TestQuickStart.java b/jetty-quickstart/src/test/java/org/eclipse/jetty/quickstart/TestQuickStart.java
index 35344441fce..c414bacaed5 100644
--- a/jetty-quickstart/src/test/java/org/eclipse/jetty/quickstart/TestQuickStart.java
+++ b/jetty-quickstart/src/test/java/org/eclipse/jetty/quickstart/TestQuickStart.java
@@ -22,14 +22,11 @@ package org.eclipse.jetty.quickstart;
import static org.junit.Assert.*;
import java.io.File;
-import java.nio.file.Path;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
-import org.eclipse.jetty.util.resource.Resource;
-import org.eclipse.jetty.webapp.WebAppContext;
import org.junit.Before;
import org.junit.Test;
@@ -90,7 +87,7 @@ public class TestQuickStart
server.start();
//verify that FooServlet is now mapped to / and not the DefaultServlet
- ServletHolder sh = webapp.getServletHandler().getHolderEntry("/").getResource();
+ ServletHolder sh = webapp.getServletHandler().getMappedServlet("/").getResource();
assertNotNull(sh);
assertEquals("foo", sh.getName());
server.stop();
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java
index e1ee2cf4d09..3c9d2a7fea8 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/HashLoginService.java
@@ -50,8 +50,7 @@ public class HashLoginService extends AbstractLoginService
{
private static final Logger LOG = Log.getLogger(HashLoginService.class);
- private File _configFile;
- private Resource _configResource;
+ private String _config;
private boolean hotReload = false; // default is not to reload
private UserStore _userStore;
private boolean _userStoreAutoCreate = false;
@@ -78,28 +77,15 @@ public class HashLoginService extends AbstractLoginService
/* ------------------------------------------------------------ */
public String getConfig()
{
- if(_configFile == null)
- {
- return null;
- }
- return _configFile.getAbsolutePath();
+ return _config;
}
+
/* ------------------------------------------------------------ */
-
- /**
- * @deprecated use {@link #setConfig(String)} instead
- */
@Deprecated
- public void getConfig(String config)
- {
- setConfig(config);
- }
-
- /* ------------------------------------------------------------ */
public Resource getConfigResource()
{
- return _configResource;
+ return null;
}
/* ------------------------------------------------------------ */
@@ -109,12 +95,11 @@ public class HashLoginService extends AbstractLoginService
* The property file maps usernames to password specs followed by an optional comma separated list of role names.
*
*
- * @param configFile
- * Filename of user properties file.
+ * @param config uri or url or path to realm properties file
*/
- public void setConfig(String configFile)
+ public void setConfig(String config)
{
- _configFile = new File(configFile);
+ _config=config;
}
/**
@@ -200,11 +185,10 @@ public class HashLoginService extends AbstractLoginService
if (_userStore == null)
{
if(LOG.isDebugEnabled())
- LOG.debug("doStart: Starting new PropertyUserStore. PropertiesFile: " + _configFile + " hotReload: " + hotReload);
-
+ LOG.debug("doStart: Starting new PropertyUserStore. PropertiesFile: " + _config + " hotReload: " + hotReload);
PropertyUserStore propertyUserStore = new PropertyUserStore();
propertyUserStore.setHotReload(hotReload);
- propertyUserStore.setConfigPath(_configFile);
+ propertyUserStore.setConfigPath(_config);
propertyUserStore.start();
_userStore = propertyUserStore;
_userStoreAutoCreate = true;
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/PropertyUserStore.java b/jetty-security/src/main/java/org/eclipse/jetty/security/PropertyUserStore.java
index ddd1268179c..c3bdd1c5b3e 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/PropertyUserStore.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/PropertyUserStore.java
@@ -29,6 +29,7 @@ import org.eclipse.jetty.util.security.Credential;
import java.io.File;
import java.io.IOException;
+import java.net.MalformedURLException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
@@ -73,18 +74,30 @@ public class PropertyUserStore extends UserStore implements PathWatcher.Listener
@Deprecated
public String getConfig()
{
- return _configPath.toString();
+ if (_configPath != null)
+ return _configPath.toString();
+ return null;
}
/**
* Set the Config Path from a String reference to a file
- * @param configFile the config file
- * @deprecated use {@link #setConfigPath(String)} instead
+ * @param config the config file
*/
- @Deprecated
- public void setConfig(String configFile)
+ public void setConfig(String config)
{
- setConfigPath(configFile);
+ try
+ {
+ Resource configResource = Resource.newResource(config);
+ if (configResource.getFile() != null)
+ setConfigPath(configResource.getFile());
+ else
+ throw new IllegalArgumentException(config+" is not a file");
+ }
+ catch (Exception e)
+ {
+ throw new IllegalStateException(e);
+ }
+
}
/**
diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java
index ff36a22c1f5..51f7d8abb5c 100644
--- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java
+++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java
@@ -513,7 +513,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory, Welc
if ((_welcomeServlets || _welcomeExactServlets) && welcome_servlet==null)
{
- MappedResource entry=_servletHandler.getHolderEntry(welcome_in_context);
+ MappedResource entry=_servletHandler.getMappedServlet(welcome_in_context);
if (entry!=null && entry.getResource()!=_defaultHolder &&
(_welcomeServlets || (_welcomeExactServlets && entry.getPathSpec().getDeclaration().equals(welcome_in_context))))
welcome_servlet=welcome_in_context;
diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/Invoker.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/Invoker.java
index 1d1a2d016df..82e58e6c3ea 100644
--- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/Invoker.java
+++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/Invoker.java
@@ -32,7 +32,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
-import org.eclipse.jetty.http.PathMap.MappedEntry;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.server.Dispatcher;
import org.eclipse.jetty.server.Handler;
@@ -66,6 +65,7 @@ import org.eclipse.jetty.util.log.Logger;
* @version $Id: Invoker.java 4780 2009-03-17 15:36:08Z jesse $
*
*/
+@SuppressWarnings("serial")
public class Invoker extends HttpServlet
{
private static final Logger LOG = Log.getLogger(Invoker.class);
@@ -168,11 +168,11 @@ public class Invoker extends HttpServlet
synchronized(_servletHandler)
{
// find the entry for the invoker (me)
- _invokerEntry=_servletHandler.getHolderEntry(servlet_path);
+ _invokerEntry=_servletHandler.getMappedServlet(servlet_path);
// Check for existing mapping (avoid threaded race).
String path=URIUtil.addPaths(servlet_path,servlet);
- MappedResource entry = _servletHandler.getHolderEntry(path);
+ MappedResource entry = _servletHandler.getMappedServlet(path);
if (entry!=null && !entry.equals(_invokerEntry))
{
diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java
index c6c553630c7..bc2bfd2b6ad 100644
--- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java
+++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java
@@ -24,7 +24,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
@@ -53,8 +52,6 @@ import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
-import org.eclipse.jetty.http.pathmap.PathSpec;
-import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.server.Request;
@@ -361,14 +358,16 @@ public class ServletHandler extends ScopedHandler
/**
* ServletHolder matching path.
*
- * @param pathInContext Path within _context.
+ * @param target Path within _context or servlet name
* @return PathMap Entries pathspec to ServletHolder
+ * @deprecated Use {@link #getMappedServlet(String)}
*/
- public MappedResource getHolderEntry(String pathInContext)
+ @Deprecated
+ public MappedResource getHolderEntry(String target)
{
- if (_servletPathMap==null)
- return null;
- return _servletPathMap.getMatch(pathInContext);
+ if (target.startsWith("/"))
+ return getMappedServlet(target);
+ return null;
}
/* ------------------------------------------------------------ */
@@ -438,16 +437,14 @@ public class ServletHandler extends ScopedHandler
ServletHolder servlet_holder=null;
UserIdentity.Scope old_scope=null;
- // find the servlet
- if (target.startsWith("/"))
+ MappedResource mapping=getMappedServlet(target);
+ if (mapping!=null)
{
- // Look for the servlet by path
- MappedResource entry=getHolderEntry(target);
- if (entry!=null)
+ servlet_holder = mapping.getResource();
+
+ if (mapping.getPathSpec()!=null)
{
- PathSpec pathSpec = entry.getPathSpec();
- servlet_holder=entry.getResource();
-
+ PathSpec pathSpec = mapping.getPathSpec();
String servlet_path=pathSpec.getPathMatch(target);
String path_info=pathSpec.getPathInfo(target);
@@ -464,12 +461,7 @@ public class ServletHandler extends ScopedHandler
}
}
}
- else
- {
- // look for a servlet by name!
- servlet_holder= _servletNameMap.get(target);
- }
-
+
if (LOG.isDebugEnabled())
LOG.debug("servlet {}|{}|{} -> {}",baseRequest.getContextPath(),baseRequest.getServletPath(),baseRequest.getPathInfo(),servlet_holder);
@@ -550,7 +542,33 @@ public class ServletHandler extends ScopedHandler
baseRequest.setHandled(true);
}
}
+
+ /* ------------------------------------------------------------ */
+ /**
+ * ServletHolder matching path.
+ *
+ * @param target Path within _context or servlet name
+ * @return MappedResource to the ServletHolder. Named servlets have a null PathSpec
+ */
+ public MappedResource getMappedServlet(String target)
+ {
+ if (target.startsWith("/"))
+ {
+ if (_servletPathMap==null)
+ return null;
+ return _servletPathMap.getMatch(target);
+ }
+
+ if (_servletNameMap==null)
+ return null;
+ ServletHolder holder = _servletNameMap.get(target);
+ if (holder==null)
+ return null;
+ return new MappedResource<>(null,holder);
+ }
+
+ /* ------------------------------------------------------------ */
protected FilterChain getFilterChain(Request baseRequest, String pathInContext, ServletHolder servletHolder)
{
String key=pathInContext==null?servletHolder.getName():pathInContext;
@@ -704,8 +722,6 @@ public class ServletHandler extends ScopedHandler
return _startWithUnavailable;
}
-
-
/* ------------------------------------------------------------ */
/** Initialize filters and load-on-startup servlets.
* @throws Exception if unable to initialize
@@ -1766,6 +1782,7 @@ public class ServletHandler extends ScopedHandler
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
+ @SuppressWarnings("serial")
public static class Default404Servlet extends HttpServlet
{
@Override
diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletHandlerTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletHandlerTest.java
index 22edca076e9..c7710b49427 100644
--- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletHandlerTest.java
+++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletHandlerTest.java
@@ -267,7 +267,7 @@ public class ServletHandlerTest
handler.updateMappings();
- MappedResource entry=handler.getHolderEntry("/foo/*");
+ MappedResource entry=handler.getMappedServlet("/foo/*");
assertNotNull(entry);
assertEquals("s1", entry.getResource().getName());
}
@@ -312,7 +312,7 @@ public class ServletHandlerTest
handler.addServletMapping(sm2);
handler.updateMappings();
- MappedResource entry=handler.getHolderEntry("/foo/*");
+ MappedResource entry=handler.getMappedServlet("/foo/*");
assertNotNull(entry);
assertEquals("s2", entry.getResource().getName());
}
diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JettyWebXmlConfiguration.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JettyWebXmlConfiguration.java
index dea75058d21..6439cc289c1 100644
--- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JettyWebXmlConfiguration.java
+++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JettyWebXmlConfiguration.java
@@ -41,6 +41,13 @@ public class JettyWebXmlConfiguration extends AbstractConfiguration
{
private static final Logger LOG = Log.getLogger(JettyWebXmlConfiguration.class);
+ /** The value of this property points to the WEB-INF directory of
+ * the web-app currently installed.
+ * it is passed as a property to the jetty-web.xml file */
+ @Deprecated
+ public static final String PROPERTY_THIS_WEB_INF_URL = "this.web-inf.url";
+ public static final String PROPERTY_WEB_INF_URI = "web-inf.uri";
+ public static final String PROPERTY_WEB_INF = "web-inf";
public static final String XML_CONFIGURATION = "org.eclipse.jetty.webapp.JettyWebXmlConfiguration";
public static final String JETTY_WEB_XML = "jetty-web.xml";
@@ -104,12 +111,8 @@ public class JettyWebXmlConfiguration extends AbstractConfiguration
private void setupXmlConfiguration(XmlConfiguration jetty_config, Resource web_inf) throws IOException
{
Map props = jetty_config.getProperties();
- props.put("this.web-inf.url", web_inf.getURI().toURL().toExternalForm());
- String webInfPath = web_inf.getFile().getAbsolutePath();
- if (!webInfPath.endsWith(File.separator))
- {
- webInfPath += File.separator;
- }
- props.put("this.web-inf.path", webInfPath);
+ props.put(PROPERTY_THIS_WEB_INF_URL, web_inf.getURI().toString());
+ props.put(PROPERTY_WEB_INF_URI, web_inf.getURI().toString());
+ props.put(PROPERTY_WEB_INF, web_inf.toString());
}
}
diff --git a/tests/test-webapps/test-jetty-webapp/src/main/assembly/embedded-jetty-web-for-webbundle.xml b/tests/test-webapps/test-jetty-webapp/src/main/assembly/embedded-jetty-web-for-webbundle.xml
index 41dfeed4e6a..eefda45fb50 100644
--- a/tests/test-webapps/test-jetty-webapp/src/main/assembly/embedded-jetty-web-for-webbundle.xml
+++ b/tests/test-webapps/test-jetty-webapp/src/main/assembly/embedded-jetty-web-for-webbundle.xml
@@ -51,7 +51,9 @@ detected.
Test Realm
- realm.properties
+
+ /realm.properties
+
+