1 package org
.argeo
.cms
.jetty
;
3 import java
.io
.IOException
;
4 import java
.net
.InetSocketAddress
;
5 import java
.security
.NoSuchAlgorithmException
;
7 import java
.util
.TreeMap
;
8 import java
.util
.concurrent
.Executor
;
9 import java
.util
.concurrent
.ThreadPoolExecutor
;
11 import javax
.net
.ssl
.SSLContext
;
12 import javax
.servlet
.ServletException
;
13 import javax
.websocket
.server
.ServerContainer
;
15 import org
.argeo
.api
.cms
.CmsLog
;
16 import org
.argeo
.api
.cms
.CmsState
;
17 import org
.argeo
.cms
.CmsDeployProperty
;
18 import org
.argeo
.cms
.http
.server
.HttpServerUtils
;
19 import org
.eclipse
.jetty
.ee8
.servlet
.ServletContextHandler
;
20 import org
.eclipse
.jetty
.http
.UriCompliance
;
21 import org
.eclipse
.jetty
.server
.HttpConfiguration
;
22 import org
.eclipse
.jetty
.server
.HttpConnectionFactory
;
23 import org
.eclipse
.jetty
.server
.SecureRequestCustomizer
;
24 import org
.eclipse
.jetty
.server
.Server
;
25 import org
.eclipse
.jetty
.server
.ServerConnector
;
26 import org
.eclipse
.jetty
.server
.SslConnectionFactory
;
27 import org
.eclipse
.jetty
.server
.handler
.ContextHandlerCollection
;
28 import org
.eclipse
.jetty
.util
.ssl
.SslContextFactory
;
29 import org
.eclipse
.jetty
.util
.thread
.ExecutorThreadPool
;
30 import org
.eclipse
.jetty
.util
.thread
.QueuedThreadPool
;
31 import org
.eclipse
.jetty
.util
.thread
.ThreadPool
;
33 import com
.sun
.net
.httpserver
.HttpContext
;
34 import com
.sun
.net
.httpserver
.HttpHandler
;
35 import com
.sun
.net
.httpserver
.HttpServer
;
36 import com
.sun
.net
.httpserver
.HttpsConfigurator
;
37 import com
.sun
.net
.httpserver
.HttpsServer
;
39 /** An {@link HttpServer} implementation based on Jetty. */
40 public class JettyHttpServer
extends HttpsServer
{
41 private final static CmsLog log
= CmsLog
.getLog(JettyHttpServer
.class);
43 /** Long timeout since our users may have poor connections. */
44 private static final int DEFAULT_IDLE_TIMEOUT
= 120 * 1000;
46 private Server server
;
48 protected ServerConnector httpConnector
;
49 protected ServerConnector httpsConnector
;
51 private InetSocketAddress httpAddress
;
52 private InetSocketAddress httpsAddress
;
54 private ThreadPoolExecutor executor
;
56 private HttpsConfigurator httpsConfigurator
;
58 private final Map
<String
, JettyHttpContext
> contexts
= new TreeMap
<>();
60 private ServletContextHandler rootContextHandler
;
61 protected final ContextHandlerCollection contextHandlerCollection
= new ContextHandlerCollection();
63 private boolean started
;
65 private CmsState cmsState
;
68 public void bind(InetSocketAddress addr
, int backlog
) throws IOException
{
69 throw new UnsupportedOperationException();
74 String httpPortStr
= getDeployProperty(CmsDeployProperty
.HTTP_PORT
);
75 String httpsPortStr
= getDeployProperty(CmsDeployProperty
.HTTPS_PORT
);
76 if (httpPortStr
!= null && httpsPortStr
!= null)
77 throw new IllegalArgumentException("Either an HTTP or an HTTPS port should be configured, not both");
78 if (httpPortStr
== null && httpsPortStr
== null) {
79 log
.warn("Neither an HTTP or an HTTPS port was configured, not starting Jetty");
82 /// TODO make it more generic
83 String httpHost
= getDeployProperty(CmsDeployProperty
.HOST
);
87 ThreadPool threadPool
= null;
88 if (executor
!= null) {
89 threadPool
= new ExecutorThreadPool(executor
);
91 // TODO make it configurable
92 threadPool
= new QueuedThreadPool(10, 1);
95 server
= new Server(threadPool
);
97 configureConnectors(httpPortStr
, httpsPortStr
, httpHost
);
99 if (httpConnector
!= null) {
100 httpConnector
.open();
101 server
.addConnector(httpConnector
);
104 if (httpsConnector
!= null) {
105 httpsConnector
.open();
106 server
.addConnector(httpsConnector
);
112 rootContextHandler
= createRootContextHandler();
113 // httpContext.addServlet(holder, "/*");
114 if (rootContextHandler
!= null)
115 configureRootContextHandler(rootContextHandler
);
117 if (rootContextHandler
!= null && !contexts
.containsKey("/"))
118 contextHandlerCollection
.addHandler(rootContextHandler
);
120 server
.setHandler(contextHandlerCollection
);
128 String fallBackHostname
= cmsState
!= null ? cmsState
.getHostname() : "::1";
129 if (httpConnector
!= null) {
130 httpAddress
= new InetSocketAddress(httpHost
!= null ? httpHost
: fallBackHostname
,
131 httpConnector
.getLocalPort());
132 } else if (httpsConnector
!= null) {
133 httpsAddress
= new InetSocketAddress(httpHost
!= null ? httpHost
: fallBackHostname
,
134 httpsConnector
.getLocalPort());
137 Runtime
.getRuntime().addShutdownHook(new Thread(() -> stop(), "Jetty shutdown"));
139 log
.info(httpPortsMsg());
141 } catch (Exception e
) {
143 throw new IllegalStateException("Cannot start Jetty HTTP server", e
);
147 protected void configureConnectors(String httpPortStr
, String httpsPortStr
, String httpHost
) {
150 if (httpPortStr
!= null || httpsPortStr
!= null) {
151 // TODO deal with hostname resolving taking too much time
152 // String fallBackHostname = InetAddress.getLocalHost().getHostName();
154 boolean httpEnabled
= httpPortStr
!= null;
155 boolean httpsEnabled
= httpsPortStr
!= null;
158 HttpConfiguration httpConfiguration
= new HttpConfiguration();
160 if (httpsEnabled
) {// not supported anymore to have both http and https, but it may change again
161 int httpsPort
= Integer
.parseInt(httpsPortStr
);
162 httpConfiguration
.setSecureScheme("https");
163 httpConfiguration
.setSecurePort(httpsPort
);
166 int httpPort
= Integer
.parseInt(httpPortStr
);
167 httpConnector
= new ServerConnector(server
, new HttpConnectionFactory(httpConfiguration
));
168 httpConnector
.setPort(httpPort
);
169 httpConnector
.setHost(httpHost
);
170 httpConnector
.setIdleTimeout(DEFAULT_IDLE_TIMEOUT
);
175 if (httpsConfigurator
== null) {
176 // we make sure that an HttpSConfigurator is set, so that clients can detect
177 // whether this server is HTTP or HTTPS
179 httpsConfigurator
= new HttpsConfigurator(SSLContext
.getDefault());
180 } catch (NoSuchAlgorithmException e
) {
181 throw new IllegalStateException("Cannot initalise SSL Context", e
);
185 SslContextFactory
.Server sslContextFactory
= new SslContextFactory
.Server();
186 // sslContextFactory.setKeyStore(KeyS)
188 sslContextFactory
.setKeyStoreType(getDeployProperty(CmsDeployProperty
.SSL_KEYSTORETYPE
));
189 sslContextFactory
.setKeyStorePath(getDeployProperty(CmsDeployProperty
.SSL_KEYSTORE
));
190 sslContextFactory
.setKeyStorePassword(getDeployProperty(CmsDeployProperty
.SSL_PASSWORD
));
191 // sslContextFactory.setKeyManagerPassword(getFrameworkProp(CmsDeployProperty.SSL_KEYPASSWORD));
192 sslContextFactory
.setProtocol("TLS");
194 sslContextFactory
.setTrustStoreType(getDeployProperty(CmsDeployProperty
.SSL_TRUSTSTORETYPE
));
195 sslContextFactory
.setTrustStorePath(getDeployProperty(CmsDeployProperty
.SSL_TRUSTSTORE
));
196 sslContextFactory
.setTrustStorePassword(getDeployProperty(CmsDeployProperty
.SSL_TRUSTSTOREPASSWORD
));
198 String wantClientAuth
= getDeployProperty(CmsDeployProperty
.SSL_WANTCLIENTAUTH
);
199 if (wantClientAuth
!= null && wantClientAuth
.equals(Boolean
.toString(true)))
200 sslContextFactory
.setWantClientAuth(true);
201 String needClientAuth
= getDeployProperty(CmsDeployProperty
.SSL_NEEDCLIENTAUTH
);
202 if (needClientAuth
!= null && needClientAuth
.equals(Boolean
.toString(true)))
203 sslContextFactory
.setNeedClientAuth(true);
205 // HTTPS Configuration
206 HttpConfiguration httpsConfiguration
= new HttpConfiguration();
207 httpsConfiguration
.addCustomizer(new SecureRequestCustomizer());
208 httpsConfiguration
.setUriCompliance(UriCompliance
.LEGACY
);
211 httpsConnector
= new ServerConnector(server
, new SslConnectionFactory(sslContextFactory
, "http/1.1"),
212 new HttpConnectionFactory(httpsConfiguration
));
213 int httpsPort
= Integer
.parseInt(httpsPortStr
);
214 httpsConnector
.setPort(httpsPort
);
215 httpsConnector
.setHost(httpHost
);
216 httpsConnector
.setIdleTimeout(DEFAULT_IDLE_TIMEOUT
);
222 public void stop(int delay
) {
223 // TODO wait for processing to complete
231 // TODO delete temp dir
233 log
.debug(() -> "Stopped Jetty server");
234 } catch (Exception e
) {
235 log
.error("Cannot stop Jetty HTTP server", e
);
241 public void setExecutor(Executor executor
) {
242 if (!(executor
instanceof ThreadPoolExecutor
))
243 throw new IllegalArgumentException("Only " + ThreadPoolExecutor
.class.getName() + " are supported");
244 this.executor
= (ThreadPoolExecutor
) executor
;
248 public Executor
getExecutor() {
253 public synchronized HttpContext
createContext(String path
, HttpHandler handler
) {
254 HttpContext httpContext
= createContext(path
);
255 httpContext
.setHandler(handler
);
260 public synchronized HttpContext
createContext(String path
) {
261 if (!path
.endsWith("/"))
263 if (contexts
.containsKey(path
))
264 throw new IllegalArgumentException("Context " + path
+ " already exists");
266 JettyHttpContext httpContext
= new ServletHttpContext(this, path
);
267 contexts
.put(path
, httpContext
);
269 contextHandlerCollection
.addHandler(httpContext
.getServletContextHandler());
274 public synchronized void removeContext(String path
) throws IllegalArgumentException
{
275 if (!path
.endsWith("/"))
277 if (!contexts
.containsKey(path
))
278 throw new IllegalArgumentException("Context " + path
+ " does not exist");
279 JettyHttpContext httpContext
= contexts
.remove(path
);
280 if (httpContext
instanceof ContextHandlerHttpContext contextHandlerHttpContext
) {
281 // TODO stop handler first?
282 // FIXME understand compatibility with Jetty 12
283 // contextHandlerCollection.removeHandler(contextHandlerHttpContext.getServletContextHandler());
285 // FIXME apparently servlets cannot be removed in Jetty, we should replace the
291 public synchronized void removeContext(HttpContext context
) {
292 removeContext(context
.getPath());
296 public InetSocketAddress
getAddress() {
297 InetSocketAddress res
= httpAddress
!= null ? httpAddress
: httpsAddress
;
299 throw new IllegalStateException("Neither an HTTP nor and HTTPS address is available");
304 public void setHttpsConfigurator(HttpsConfigurator config
) {
305 this.httpsConfigurator
= config
;
309 public HttpsConfigurator
getHttpsConfigurator() {
310 return httpsConfigurator
;
313 protected String
getDeployProperty(CmsDeployProperty deployProperty
) {
314 return cmsState
!= null ? cmsState
.getDeployProperty(deployProperty
.getProperty())
315 : System
.getProperty(deployProperty
.getProperty());
318 private String
httpPortsMsg() {
319 String hostStr
= getHost();
320 hostStr
= hostStr
== null ?
"*:" : hostStr
+ ":";
321 return (httpConnector
!= null ?
"# HTTP " + hostStr
+ getHttpPort() + " " : "")
322 + (httpsConnector
!= null ?
"# HTTPS " + hostStr
+ getHttpsPort() : "");
325 public String
getHost() {
326 if (httpConnector
== null)
328 return httpConnector
.getHost();
331 public Integer
getHttpPort() {
332 if (httpConnector
== null)
334 return httpConnector
.getLocalPort();
337 public Integer
getHttpsPort() {
338 if (httpsConnector
== null)
340 return httpsConnector
.getLocalPort();
343 protected ServletContextHandler
createRootContextHandler() {
347 protected void configureRootContextHandler(ServletContextHandler servletContextHandler
) throws ServletException
{
351 public void setCmsState(CmsState cmsState
) {
352 this.cmsState
= cmsState
;
355 boolean isStarted() {
359 ServletContextHandler
getRootContextHandler() {
360 return rootContextHandler
;
363 ServerContainer
getRootServerContainer() {
364 throw new UnsupportedOperationException();
367 public static void main(String
... args
) {
368 JettyHttpServer httpServer
= new JettyHttpServer();
369 System
.setProperty("argeo.http.port", "8080");
370 httpServer
.createContext("/", (exchange
) -> {
371 exchange
.getResponseBody().write("Hello World!".getBytes());
374 httpServer
.createContext("/sub/context", (exchange
) -> {
375 final String key
= "count";
376 Integer count
= (Integer
) exchange
.getHttpContext().getAttributes().get(key
);
378 exchange
.getHttpContext().getAttributes().put(key
, 0);
380 exchange
.getHttpContext().getAttributes().put(key
, count
+ 1);
381 StringBuilder sb
= new StringBuilder();
382 sb
.append("Subcontext:");
383 sb
.append(" " + key
+ "=" + exchange
.getHttpContext().getAttributes().get(key
));
384 sb
.append(" relativePath=" + HttpServerUtils
.relativize(exchange
));
385 exchange
.getResponseBody().write(sb
.toString().getBytes());