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());
+ }
+}