Do not use privileged ports with WebServer by default (#4992) Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServer.java b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServer.java index c3463dc..68d7048 100644 --- a/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServer.java +++ b/containers/grizzly2-http/src/main/java/org/glassfish/jersey/grizzly2/httpserver/GrizzlyHttpServer.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Markus KARG. All rights reserved. * * This program and the accompanying materials are made available under the @@ -61,7 +61,7 @@ this.container = container; this.httpServer = GrizzlyHttpServerFactory.createHttpServer( - configuration.uri(false), + configuration.uri(true), this.container, configuration.isHttps(), configuration.isHttps() ? new SSLEngineConfigurator(sslContext, false,
diff --git a/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpServer.java b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpServer.java index d53a758..33d0bcf 100644 --- a/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpServer.java +++ b/containers/jdk-http/src/main/java/org/glassfish/jersey/jdkhttp/JdkHttpServer.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Markus KARG. All rights reserved. * * This program and the accompanying materials are made available under the @@ -57,7 +57,7 @@ this.container = container; this.httpServer = JdkHttpServerFactory.createHttpServer( - configuration.uri(false), + configuration.uri(true), this.container, configuration.sslContext(), sslClientAuthentication == OPTIONAL,
diff --git a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java index 9f014f4..64af030 100644 --- a/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java +++ b/containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpServer.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Markus KARG. All rights reserved. * * This program and the accompanying materials are made available under the @@ -68,7 +68,7 @@ } this.container = container; this.httpServer = JettyHttpContainerFactory.createServer( - configuration.uri(false), + configuration.uri(true), sslContextFactory, this.container, configuration.autoStart());
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpServer.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpServer.java index fece127..7b28779 100644 --- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpServer.java +++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/NettyHttpServer.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Markus KARG. All rights reserved. * * This program and the accompanying materials are made available under the @@ -64,7 +64,7 @@ final SeBootstrap.Configuration.SSLClientAuthentication sslClientAuthentication = configuration .sslClientAuthentication(); - final URI uri = configuration.uri(false); + final URI uri = configuration.uri(true); this.port = NettyHttpContainerProvider.getPort(uri); this.container = container;
diff --git a/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleHttpServer.java b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleHttpServer.java index e63d3f4..1b9ac18 100644 --- a/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleHttpServer.java +++ b/containers/simple-http/src/main/java/org/glassfish/jersey/simple/SimpleHttpServer.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Markus KARG. All rights reserved. * * This program and the accompanying materials are made available under the @@ -50,7 +50,7 @@ SimpleHttpServer(final SimpleContainer container, final JerseySeBootstrapConfiguration configuration) { this.container = container; this.simpleServer = SimpleContainerFactory.create( - configuration.uri(false), + configuration.uri(true), configuration.sslContext(), configuration.sslClientAuthentication(), this.container,
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/config/SystemPropertiesConfigurationModel.java b/core-common/src/main/java/org/glassfish/jersey/internal/config/SystemPropertiesConfigurationModel.java index 6374f73..23b127a 100644 --- a/core-common/src/main/java/org/glassfish/jersey/internal/config/SystemPropertiesConfigurationModel.java +++ b/core-common/src/main/java/org/glassfish/jersey/internal/config/SystemPropertiesConfigurationModel.java
@@ -86,7 +86,7 @@ } @Override public <T> Optional<T> getOptionalProperty(String name, Class<T> clazz) { - return Optional.of(as(name, clazz)); + return Optional.ofNullable(as(name, clazz)); } @Override
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/JerseySeBootstrapConfiguration.java b/core-server/src/main/java/org/glassfish/jersey/server/JerseySeBootstrapConfiguration.java index e433adb..3bfe5db 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/JerseySeBootstrapConfiguration.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/JerseySeBootstrapConfiguration.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -18,19 +18,27 @@ import jakarta.ws.rs.SeBootstrap; import jakarta.ws.rs.core.UriBuilder; +import org.glassfish.jersey.internal.config.ExternalPropertiesConfigurationFactory; +import org.glassfish.jersey.internal.config.SystemPropertiesConfigurationModel; +import org.glassfish.jersey.internal.util.PropertiesClass; import org.glassfish.jersey.server.internal.LocalizationMessages; import org.glassfish.jersey.server.spi.Container; import org.glassfish.jersey.server.spi.WebServer; import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.ServerSocket; import java.net.URI; import java.security.NoSuchAlgorithmException; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Random; import java.util.function.BiFunction; import java.util.logging.Logger; +import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; /** @@ -40,6 +48,7 @@ */ public final class JerseySeBootstrapConfiguration implements SeBootstrap.Configuration { private static final Logger LOGGER = Logger.getLogger(JerseySeBootstrapConfiguration.class.getName()); + protected static final Random RANDOM = new Random(); private final SeBootstrap.Configuration configuration; private JerseySeBootstrapConfiguration(SeBootstrap.Configuration configuration) { @@ -61,16 +70,60 @@ public URI uri(boolean resolveDefaultPort) { final String protocol = configuration.protocol(); final String host = configuration.host(); - final int configPort = configuration.port(); - final int port = (configPort < 0 && resolveDefaultPort) - ? isHttps() ? Container.DEFAULT_HTTPS_PORT : Container.DEFAULT_HTTP_PORT - : configPort; + final int port = resolveDefaultPort ? resolvePort() : configuration.port(); final String rootPath = configuration.rootPath(); final URI uri = UriBuilder.newInstance().scheme(protocol.toLowerCase()).host(host).port(port).path(rootPath) .build(); return uri; } + private int resolvePort() { + final int configPort = configuration.port(); + final int basePort = allowPrivilegedPorts() ? 0 : 8000; + final int port; + switch (configPort) { + case SeBootstrap.Configuration.DEFAULT_PORT: + port = basePort + (isHttps() ? Container.DEFAULT_HTTPS_PORT : Container.DEFAULT_HTTP_PORT); + break; + case SeBootstrap.Configuration.FREE_PORT: + port = _resolvePort(basePort == 0); + break; + default: + port = configPort; + break; + } + return port; + } + + private int _resolvePort(boolean allowPrivilegedPort) { + final int basePort = allowPrivilegedPort ? 0 : 1023; + // Get the initial range parameters + final int lower = basePort; + final int range = 0xFFFF; + + // Select a start point in the range + final int initialOffset = RANDOM.nextInt(range - lower); + + // Loop the offset through all ports in the range and attempt + // to bind to each + int offset = initialOffset; + ServerSocket socket; + do { + final int port = lower + offset; + try { + socket = new ServerSocket(port); + socket.close(); + return port; + } catch (IOException caught) { + // Swallow exceptions until the end + } + offset = (offset + 1) % range; + } while (offset != initialOffset); + + // If a port can't be bound, throw the exception + throw new IllegalArgumentException(LocalizationMessages.COULD_NOT_BIND_TO_ANY_PORT()); + } + /** * Return {@link SSLContext} in the configuration if the protocol scheme is {@code HTTPS}. * @return the SSLContext in the configuration. @@ -101,6 +154,16 @@ } /** + * Defines if the {@link WebServer} should start on a privileged port when port is not set. + * @return true if {@link ServerProperties#WEBSERVER_AUTO_START} is {@code true}, {@code false} otherwise. + */ + public boolean allowPrivilegedPorts() { + return Optional.ofNullable( + (Boolean) configuration.property(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS)) + .orElse(FALSE); + } + + /** * Factory method creating {@code JerseySeBootstrapConfiguration} wrapper around {@link SeBootstrap.Configuration}. * @param configuration wrapped configuration * @return {@code JerseySeBootstrapConfiguration} wrapper around {@link SeBootstrap.Configuration}. @@ -129,8 +192,9 @@ PROPERTY_TYPES.put(SeBootstrap.Configuration.ROOT_PATH, String.class); PROPERTY_TYPES.put(SeBootstrap.Configuration.SSL_CONTEXT, SSLContext.class); PROPERTY_TYPES.put(SeBootstrap.Configuration.SSL_CLIENT_AUTHENTICATION, SSLClientAuthentication.class); - PROPERTY_TYPES.put(ServerProperties.WEBSERVER_CLASS, Class.class); + PROPERTY_TYPES.put(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS, Boolean.class); PROPERTY_TYPES.put(ServerProperties.WEBSERVER_AUTO_START, Boolean.class); + PROPERTY_TYPES.put(ServerProperties.WEBSERVER_CLASS, Class.class); } private final Map<String, Object> properties = new HashMap<>(); @@ -138,7 +202,7 @@ private Builder() { this.properties.put(SeBootstrap.Configuration.PROTOCOL, "HTTP"); // upper case mandated by javadoc this.properties.put(SeBootstrap.Configuration.HOST, "localhost"); - this.properties.put(SeBootstrap.Configuration.PORT, -1); // Auto-select port 80 for HTTP or 443 for HTTPS + this.properties.put(SeBootstrap.Configuration.PORT, -1); // Auto-select port 8080 for HTTP or 8443 for HTTPS this.properties.put(SeBootstrap.Configuration.ROOT_PATH, "/"); this.properties.put(ServerProperties.WEBSERVER_CLASS, WebServer.class); // Auto-select first provider try { @@ -149,6 +213,15 @@ this.properties.put(SeBootstrap.Configuration.SSL_CLIENT_AUTHENTICATION, SeBootstrap.Configuration.SSLClientAuthentication.NONE); this.properties.put(ServerProperties.WEBSERVER_AUTO_START, TRUE); + this.properties.put(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS, FALSE); + + SystemPropertiesConfigurationModel propertiesConfigurationModel = new SystemPropertiesConfigurationModel( + Collections.singletonList(Properties.class.getName()) + ); + from((name, aClass) -> String.class.equals(aClass) || Integer.class.equals(aClass) || Boolean.class.equals(aClass) + ? propertiesConfigurationModel.getOptionalProperty(name, aClass) + : Optional.empty() + ); } @Override @@ -208,4 +281,41 @@ return this; } } + + /** + * Name the properties to be internally read from System properties by {@link ExternalPropertiesConfigurationFactory}. + * This is required just when SecurityManager is on, otherwise all system properties are read. + */ + @PropertiesClass + private static class Properties { + /** + * See {@link SeBootstrap.Configuration#PROTOCOL} property. + */ + public static final String SE_BOOTSTRAP_CONFIGURATION_PROTOCOL = SeBootstrap.Configuration.PROTOCOL; + + /** + * See {@link SeBootstrap.Configuration#HOST} property. + */ + public static final String SE_BOOTSTRAP_CONFIGURATION_HOST = SeBootstrap.Configuration.HOST; + + /** + * See {@link SeBootstrap.Configuration#PORT} property. + */ + public static final String SE_BOOTSTRAP_CONFIGURATION_PORT = SeBootstrap.Configuration.PORT; + + /** + * See {@link SeBootstrap.Configuration#ROOT_PATH} property. + */ + public static final String SE_BOOTSTRAP_CONFIGURATION_ROOT_PATH = SeBootstrap.Configuration.ROOT_PATH; + + /** + * See {@link ServerProperties#WEBSERVER_ALLOW_PRIVILEGED_PORTS} property. + */ + public static final String WEBSERVER_ALLOW_PRIVILEGED_PORTS = ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS; + + /** + * See {@link ServerProperties#WEBSERVER_AUTO_START} property. + */ + public static final String WEBSERVER_AUTO_START = ServerProperties.WEBSERVER_AUTO_START; + } }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ServerProperties.java b/core-server/src/main/java/org/glassfish/jersey/server/ServerProperties.java index 25b5e9e..099ce70 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/ServerProperties.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/ServerProperties.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -24,6 +24,7 @@ import org.glassfish.jersey.internal.util.PropertiesClass; import org.glassfish.jersey.internal.util.PropertiesHelper; import org.glassfish.jersey.internal.util.PropertyAlias; +import org.glassfish.jersey.server.spi.Container; import org.glassfish.jersey.server.spi.WebServer; @@ -39,25 +40,6 @@ public final class ServerProperties { /** - * Defines the implementation of {@link WebServer} to bootstrap. - * <p> - * By default auto-selects the first server provider found. - * </p> - * @since 3.1.0 - */ - public static final String WEBSERVER_CLASS = "jersey.config.server.webserver.class"; - - /** - * Whether to automatically startup {@link WebServer} at bootstrap. - * <p> - * By default, servers are immediately listening to connections after bootstrap, - * so no explicit invocation of {@link WebServer#start()} is needed. - * </p> - * @since 3.1.0 - */ - public static final String WEBSERVER_AUTO_START = "jersey.config.server.webserver.autostart"; - - /** * Defines one or more packages that contain application-specific resources and * providers. * @@ -784,6 +766,50 @@ "jersey.config.server.empty.request.media.matches.any.consumes"; /** + * Defines whether to allow privileged ports (0-1023) to be used to start the {@link WebServer} implementation + * to be chosen from the unused ports when the {@link jakarta.ws.rs.SeBootstrap.Configuration#PORT} is set to {@code -1} + * or unset. + * <p> + * The default ports are {@link Container#DEFAULT_HTTP_PORT} for HTTP and {@link Container#DEFAULT_HTTPS_PORT} + * for HTTPS when {@code WEBSERVER_ALLOW_PRIVILEGED_PORTS} is {@code true} or 8080 for HTTP and 8443 for HTTPS when + * {@code WEBSERVER_ALLOW_PRIVILEGED_PORTS} is {@code false}. + * </p> + * <p> + * If {@link jakarta.ws.rs.SeBootstrap.Configuration#PORT} is set to {@code 0}, the implementation chooses random ports + * (0-65535) when {@code WEBSERVER_ALLOW_PRIVILEGED_PORTS} is {@code true}, or (1024-65535) when + * {@code WEBSERVER_ALLOW_PRIVILEGED_PORTS} is {@code false.} + * </p> + * <p> + * The default this is {@code false}. Use {@code true} to allow a restricted port number. The name of the configuration + * property is <tt>{@value}</tt>. + * </p> + * @since 3.1.0 + */ + public static final String WEBSERVER_ALLOW_PRIVILEGED_PORTS = + "jersey.config.server.bootstrap.webserver.allow.privileged.ports"; + + /** + * Whether to automatically startup {@link WebServer} at bootstrap. + * <p> + * By default, servers are immediately listening to connections after bootstrap, + * so no explicit invocation of {@link WebServer#start()} is needed. The name of the configuration + * property is <tt>{@value}</tt>. + * </p> + * @since 3.1.0 + */ + public static final String WEBSERVER_AUTO_START = "jersey.config.server.bootstrap.webserver.autostart"; + + /** + * Defines the implementation of {@link WebServer} to bootstrap. + * <p> + * By default auto-selects the first server provider found. The name of the configuration + * property is <tt>{@value}</tt>. + * </p> + * @since 3.1.0 + */ + public static final String WEBSERVER_CLASS = "jersey.config.server.bootstrap.webserver.class"; + + /** * JVM argument to define the value of * {@link org.glassfish.jersey.server.internal.monitoring.core.ReservoirConstants#COLLISION_BUFFER_POWER}. * Lower values reduce the memory footprint.
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/spi/WebServer.java b/core-server/src/main/java/org/glassfish/jersey/server/spi/WebServer.java index 1de9d68..731e4eb 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/spi/WebServer.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/spi/WebServer.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 Markus KARG. All rights reserved. * * This program and the accompanying materials are made available under the @@ -28,8 +28,11 @@ /** * Jersey service contract for self-contained servers. * <p> - * Runs a self-contained {@link Application} in a {@link Container} using a - * Web Server implicitly started and stopped together with the application. + * Runs a self-contained {@link Application} in a {@link Container} using a + * Web Server implicitly started and stopped together with the application. + * </p> + * <p> + * The WebServer instance is wrapped by the implementation of {@link jakarta.ws.rs.SeBootstrap.Instance}. * </p> * * @author Markus KARG (markus@headcrashing.eu)
diff --git a/core-server/src/main/resources/org/glassfish/jersey/server/internal/localization.properties b/core-server/src/main/resources/org/glassfish/jersey/server/internal/localization.properties index a17fb8d..7c18301 100644 --- a/core-server/src/main/resources/org/glassfish/jersey/server/internal/localization.properties +++ b/core-server/src/main/resources/org/glassfish/jersey/server/internal/localization.properties
@@ -1,5 +1,5 @@ # -# Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License v. 2.0, which is available at @@ -33,6 +33,7 @@ closeable.unable.to.close=Error while closing {0}. collection.extractor.type.unsupported=Unsupported collection type. contract.cannot.be.bound.to.resource.method=The given contract ({0}) of {1} provider cannot be bound to a resource method. +could.not.bind.to.any.port=Could not bind to any port. default.could.not.process.method=Default value, {0} could not be processed by method {1}. error.async.callback.failed=Callback {0} invocation failed. error.committing.output.stream=Error while committing the output stream.
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapPropertiesTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapPropertiesTest.java new file mode 100644 index 0000000..089994f --- /dev/null +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapPropertiesTest.java
@@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.server.sebootstrap; + +import jakarta.ws.rs.SeBootstrap; +import org.glassfish.jersey.server.JerseySeBootstrapConfiguration; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.server.internal.RuntimeDelegateImpl; +import org.glassfish.jersey.server.spi.Container; +import org.junit.Assert; +import org.junit.Test; + +import java.net.URI; + +public class SeBootstrapPropertiesTest { + @Test + public void testRandomPortScanning() { + JerseySeBootstrapConfiguration configuration = (JerseySeBootstrapConfiguration) RuntimeDelegateImpl.getInstance() + .createConfigurationBuilder().port(SeBootstrap.Configuration.FREE_PORT).build(); + + URI uri = configuration.uri(true); + Assert.assertTrue(uri.getPort() > 0); + } + + @Test + public void testDefaultUnprivilegedPort() { + JerseySeBootstrapConfiguration configuration = (JerseySeBootstrapConfiguration) RuntimeDelegateImpl.getInstance() + .createConfigurationBuilder().port(SeBootstrap.Configuration.DEFAULT_PORT).build(); + + URI uri = configuration.uri(true); + Assert.assertEquals(Container.DEFAULT_HTTP_PORT + 8000, uri.getPort()); + } + + @Test + public void testDefaultPrivilegedPort() { + JerseySeBootstrapConfiguration configuration = (JerseySeBootstrapConfiguration) RuntimeDelegateImpl.getInstance() + .createConfigurationBuilder() + .property(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS, Boolean.TRUE) + .port(SeBootstrap.Configuration.DEFAULT_PORT).build(); + + URI uri = configuration.uri(true); + Assert.assertEquals(Container.DEFAULT_HTTP_PORT, uri.getPort()); + } + + @Test + public void testDefaultUnprivilegedSecuredPort() { + JerseySeBootstrapConfiguration configuration = (JerseySeBootstrapConfiguration) RuntimeDelegateImpl.getInstance() + .createConfigurationBuilder().protocol("HTTPS").port(SeBootstrap.Configuration.DEFAULT_PORT).build(); + + URI uri = configuration.uri(true); + Assert.assertEquals(Container.DEFAULT_HTTPS_PORT + 8000, uri.getPort()); + } + + @Test + public void testDefaultPrivilegedSecuredPort() { + JerseySeBootstrapConfiguration configuration = (JerseySeBootstrapConfiguration) RuntimeDelegateImpl.getInstance() + .createConfigurationBuilder() + .protocol("HTTPS") + .property(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS, Boolean.TRUE) + .port(SeBootstrap.Configuration.DEFAULT_PORT).build(); + + URI uri = configuration.uri(true); + Assert.assertEquals(Container.DEFAULT_HTTPS_PORT, uri.getPort()); + } +}
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapSystemPropertiesTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapSystemPropertiesTest.java new file mode 100644 index 0000000..1cb540a --- /dev/null +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapSystemPropertiesTest.java
@@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.server.sebootstrap; + +import jakarta.ws.rs.SeBootstrap; +import org.glassfish.jersey.CommonProperties; +import org.glassfish.jersey.server.JerseySeBootstrapConfiguration; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.server.internal.RuntimeDelegateImpl; +import org.glassfish.jersey.server.spi.Container; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.URI; + +public class SeBootstrapSystemPropertiesTest { + + @BeforeClass + public static void setUp() { + System.setProperty(CommonProperties.ALLOW_SYSTEM_PROPERTIES_PROVIDER, Boolean.TRUE.toString()); + System.getProperties().put(SeBootstrap.Configuration.PORT, "9998"); + System.getProperties().put(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS, Boolean.TRUE.toString()); + } + + @AfterClass + public static void tearDown() { + System.clearProperty(CommonProperties.ALLOW_SYSTEM_PROPERTIES_PROVIDER); + System.clearProperty(SeBootstrap.Configuration.PORT); + System.clearProperty(ServerProperties.WEBSERVER_ALLOW_PRIVILEGED_PORTS); + } + + @Test + public void testDefaultPrivilegedPortSystemProperty() { + JerseySeBootstrapConfiguration configuration = (JerseySeBootstrapConfiguration) RuntimeDelegateImpl.getInstance() + .createConfigurationBuilder() + .port(SeBootstrap.Configuration.DEFAULT_PORT).build(); + + URI uri = configuration.uri(true); + Assert.assertEquals(Container.DEFAULT_HTTP_PORT, uri.getPort()); + } + + @Test + public void testPortSystemProperty() { + JerseySeBootstrapConfiguration configuration = (JerseySeBootstrapConfiguration) RuntimeDelegateImpl.getInstance() + .createConfigurationBuilder() + .build(); + + URI uri = configuration.uri(true); + Assert.assertEquals(9998, uri.getPort()); + } +}
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapTest.java new file mode 100644 index 0000000..18b15e7 --- /dev/null +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/sebootstrap/SeBootstrapTest.java
@@ -0,0 +1,367 @@ +/* + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020 Markus Karg. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package org.glassfish.jersey.tests.e2e.server.sebootstrap; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.SeBootstrap; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.UriBuilder; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Compliance Test for Java SE Bootstrap API of Jakarta REST API + * + * @author Markus KARG (markus@headcrashing.eu) + * @since 3.1 + */ +public class SeBootstrapTest { + /** + * Verifies that an instance will boot using default configuration. + * + * @throws ExecutionException if the instance didn't boot correctly + * @throws InterruptedException if the test took much longer than usually + * expected + */ + @Test + public final void shouldBootInstanceUsingDefaults() throws InterruptedException, ExecutionException { + // given + final int expectedResponse = mockInt(); + final Application application = new StaticApplication(expectedResponse); + final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder(); + final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.build(); + + // when + final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application, requestedConfiguration); + final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get(); + final SeBootstrap.Configuration actualConfiguration = instance.configuration(); + final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol()) + .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath()) + .path("application/resource")).request().get(int.class); + + // then + assertThat(actualResponse, is(expectedResponse)); + assertThat(actualConfiguration.protocol(), is("HTTP")); + assertThat(actualConfiguration.host(), is("localhost")); + assertThat(actualConfiguration.port(), is(greaterThan(0))); + assertThat(actualConfiguration.rootPath(), is("/")); + instance.stop().toCompletableFuture().get(); + } + + /** + * Verifies that an instance will boot using explicit configuration given by + * properties. + * + * @throws ExecutionException if the instance didn't boot correctly + * @throws InterruptedException if the test took much longer than usually + * expected + * @throws IOException if no IP port was free + */ + @Test + public final void shouldBootInstanceUsingProperties() throws InterruptedException, ExecutionException, IOException { + // given + final int expectedResponse = mockInt(); + final Application application = new StaticApplication(expectedResponse); + final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder(); + final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder + .property(SeBootstrap.Configuration.PROTOCOL, "HTTP") + .property(SeBootstrap.Configuration.HOST, "localhost") + .property(SeBootstrap.Configuration.PORT, someFreeIpPort()) + .property(SeBootstrap.Configuration.ROOT_PATH, "/root/path").build(); + + // when + final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application, requestedConfiguration); + final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get(); + final SeBootstrap.Configuration actualConfiguration = instance.configuration(); + final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol()) + .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath()) + .path("application/resource")).request().get(int.class); + + // then + assertThat(actualResponse, is(expectedResponse)); + assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol())); + assertThat(actualConfiguration.host(), is(requestedConfiguration.host())); + assertThat(actualConfiguration.port(), is(requestedConfiguration.port())); + assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath())); + instance.stop().toCompletableFuture().get(); + } + + /** + * Verifies that an instance will boot using explicit configuration given by + * convenience methods. + * + * @throws ExecutionException if the instance didn't boot correctly + * @throws InterruptedException if the test took much longer than usually + * expected + * @throws IOException if no IP port was free + */ + @Test + public final void shouldBootInstanceUsingConvenienceMethods() throws InterruptedException, ExecutionException, IOException { + // given + final int expectedResponse = mockInt(); + final Application application = new StaticApplication(expectedResponse); + final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder(); + final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.protocol("HTTP").host("localhost") + .port(someFreeIpPort()).rootPath("/root/path").build(); + + // when + final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application, requestedConfiguration); + final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get(); + final SeBootstrap.Configuration actualConfiguration = instance.configuration(); + final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol()) + .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath()) + .path("application/resource")).request().get(int.class); + + // then + assertThat(actualResponse, is(expectedResponse)); + assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol())); + assertThat(actualConfiguration.host(), is(requestedConfiguration.host())); + assertThat(actualConfiguration.port(), is(requestedConfiguration.port())); + assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath())); + instance.stop().toCompletableFuture().get(); + } + + /** + * Verifies that an instance will boot using external configuration. + * + * @throws ExecutionException if the instance didn't boot correctly + * @throws InterruptedException if the test took much longer than usually + * expected + * @throws IOException if no IP port was free + */ + @Test + public final void shouldBootInstanceUsingExternalConfiguration() throws Exception { + // given + final int someFreeIpPort = someFreeIpPort(); + final int expectedResponse = mockInt(); + final Application application = new StaticApplication(expectedResponse); + final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder(); + final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.from((property, type) -> { + switch (property) { + case SeBootstrap.Configuration.PROTOCOL: + return Optional.of("HTTP"); + case SeBootstrap.Configuration.HOST: + return Optional.of("localhost"); + case SeBootstrap.Configuration.PORT: + return Optional.of(someFreeIpPort); + case SeBootstrap.Configuration.ROOT_PATH: + return Optional.of("/root/path"); + default: + return Optional.empty(); + } + }).build(); + + // when + final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application, requestedConfiguration); + final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get(); + final SeBootstrap.Configuration actualConfiguration = instance.configuration(); + final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol()) + .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath()) + .path("application/resource")).request().get(int.class); + + // then + assertThat(actualResponse, is(expectedResponse)); + assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol())); + assertThat(actualConfiguration.host(), is(requestedConfiguration.host())); + assertThat(actualConfiguration.port(), is(requestedConfiguration.port())); + assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath())); + instance.stop().toCompletableFuture().get(); + } + + /** + * Verifies that an instance will ignore unknown configuration parameters. + * + * @throws ExecutionException if the instance didn't boot correctly + * @throws InterruptedException if the test took much longer than usually + * expected + * @throws IOException if no IP port was free + */ + @Test + public final void shouldBootInstanceDespiteUnknownConfigurationParameters() throws Exception { + // given + final int expectedResponse = mockInt(); + final Application application = new StaticApplication(expectedResponse); + final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder(); + final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.protocol("HTTP").host("localhost") + .port(someFreeIpPort()).rootPath("/root/path").from((property, type) -> { + switch (property) { + case "jakarta.ws.rs.tck.sebootstrap.SeBootstrapIT$Unknown_1": + return Optional.of("Silently ignored value A"); + default: + return Optional.empty(); + } + }).property("jakarta.ws.rs.tck.sebootstrap.SeBootstrapIT$Unknown_2", "Silently ignored value B") + .from(new Object()).build(); + + // when + final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application, requestedConfiguration); + final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get(); + final SeBootstrap.Configuration actualConfiguration = instance.configuration(); + final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol()) + .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath()) + .path("application/resource")).request().get(int.class); + + // then + assertThat(actualResponse, is(expectedResponse)); + assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol())); + assertThat(actualConfiguration.host(), is(requestedConfiguration.host())); + assertThat(actualConfiguration.port(), is(requestedConfiguration.port())); + assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath())); + instance.stop().toCompletableFuture().get(); + } + + /** + * Verifies that an instance will boot using a self-detected free IP port. + * + * @throws ExecutionException if the instance didn't boot correctly + * @throws InterruptedException if the test took much longer than usually + * expected + */ + @Test + public final void shouldBootInstanceUsingSelfDetectedFreeIpPort() throws InterruptedException, ExecutionException { + // given + final int expectedResponse = mockInt(); + final Application application = new StaticApplication(expectedResponse); + final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder(); + final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.protocol("HTTP").host("localhost") + .port(SeBootstrap.Configuration.FREE_PORT).rootPath("/root/path").build(); + + // when + final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application, requestedConfiguration); + final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get(); + final SeBootstrap.Configuration actualConfiguration = instance.configuration(); + final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol()) + .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath()) + .path("application/resource")).request().get(int.class); + + // then + assertThat(actualResponse, is(expectedResponse)); + assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol())); + assertThat(actualConfiguration.host(), is(requestedConfiguration.host())); + assertThat(actualConfiguration.port(), is(greaterThan(0))); + assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath())); + instance.stop().toCompletableFuture().get(); + } + + /** + * Verifies that an instance will boot using the implementation's default IP + * port. + * + * @throws ExecutionException if the instance didn't boot correctly + * @throws InterruptedException if the test took much longer than usually + * expected + */ + @Test + public final void shouldBootInstanceUsingImplementationsDefaultIpPort() throws InterruptedException, ExecutionException { + // given + final int expectedResponse = mockInt(); + final Application application = new StaticApplication(expectedResponse); + final SeBootstrap.Configuration.Builder bootstrapConfigurationBuilder = SeBootstrap.Configuration.builder(); + final SeBootstrap.Configuration requestedConfiguration = bootstrapConfigurationBuilder.protocol("HTTP").host("localhost") + .port(SeBootstrap.Configuration.DEFAULT_PORT).rootPath("/root/path").build(); + + // when + final CompletionStage<SeBootstrap.Instance> completionStage = SeBootstrap.start(application, requestedConfiguration); + final SeBootstrap.Instance instance = completionStage.toCompletableFuture().get(); + final SeBootstrap.Configuration actualConfiguration = instance.configuration(); + final int actualResponse = client.target(UriBuilder.newInstance().scheme(actualConfiguration.protocol()) + .host(actualConfiguration.host()).port(actualConfiguration.port()).path(actualConfiguration.rootPath()) + .path("application/resource")).request().get(int.class); + + // then + assertThat(actualResponse, is(expectedResponse)); + assertThat(actualConfiguration.protocol(), is(requestedConfiguration.protocol())); + assertThat(actualConfiguration.host(), is(requestedConfiguration.host())); + assertThat(actualConfiguration.port(), is(greaterThan(0))); + assertThat(actualConfiguration.rootPath(), is(requestedConfiguration.rootPath())); + instance.stop().toCompletableFuture().get(); + } + + private static Client client; + + @BeforeClass + public static void createClient() { + SeBootstrapTest.client = ClientBuilder.newClient(); + } + + @AfterClass + public static void disposeClient() { + SeBootstrapTest.client.close(); + } + + @ApplicationPath("application") + public static final class StaticApplication extends Application { + + private final StaticResource staticResource; + + private StaticApplication(final long staticResponse) { + this.staticResource = new StaticResource(staticResponse); + } + + @Override + public final Set<Object> getSingletons() { + return Collections.<Object>singleton(staticResource); + } + + @Path("resource") + public static final class StaticResource { + + private final long staticResponse; + + private StaticResource(final long staticResponse) { + this.staticResponse = staticResponse; + } + + @GET + public final long staticResponse() { + return this.staticResponse; + } + } + } + + private static final int someFreeIpPort() throws IOException { + int port = 0; + int cnt = 0; + while (port < 1024 && cnt++ < 1025) { + try (final ServerSocket serverSocket = new ServerSocket(0)) { + port = serverSocket.getLocalPort(); + } + } + return port; + } + + private static final int mockInt() { + return (int) Math.round(Integer.MAX_VALUE * Math.random()); + } +}