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