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 - jmx.remote.x.password.file + jmx.remote.x.access.file - /resources/jmx.password + /resources/jmx.access - jmx.remote.x.access.file + jmx.remote.x.password.file - /resources/jmx.access + /resources/jmx.password @@ -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 + +