]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java
Prepare next development cycle
[lgpl/argeo-commons.git] / org.argeo.cms.lib.jetty / src / org / argeo / cms / jetty / JettyHttpServer.java
1 package org.argeo.cms.jetty;
2
3 import java.io.IOException;
4 import java.net.InetSocketAddress;
5 import java.security.NoSuchAlgorithmException;
6 import java.util.Map;
7 import java.util.TreeMap;
8 import java.util.concurrent.Executor;
9 import java.util.concurrent.ThreadPoolExecutor;
10
11 import javax.net.ssl.SSLContext;
12 import javax.servlet.ServletException;
13 import javax.websocket.server.ServerContainer;
14
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;
32
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;
38
39 /** An {@link HttpServer} implementation based on Jetty. */
40 public class JettyHttpServer extends HttpsServer {
41 private final static CmsLog log = CmsLog.getLog(JettyHttpServer.class);
42
43 /** Long timeout since our users may have poor connections. */
44 private static final int DEFAULT_IDLE_TIMEOUT = 120 * 1000;
45
46 private Server server;
47
48 protected ServerConnector httpConnector;
49 protected ServerConnector httpsConnector;
50
51 private InetSocketAddress httpAddress;
52 private InetSocketAddress httpsAddress;
53
54 private ThreadPoolExecutor executor;
55
56 private HttpsConfigurator httpsConfigurator;
57
58 private final Map<String, JettyHttpContext> contexts = new TreeMap<>();
59
60 private ServletContextHandler rootContextHandler;
61 protected final ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
62
63 private boolean started;
64
65 private CmsState cmsState;
66
67 @Override
68 public void bind(InetSocketAddress addr, int backlog) throws IOException {
69 throw new UnsupportedOperationException();
70 }
71
72 @Override
73 public void start() {
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");
80 }
81
82 /// TODO make it more generic
83 String httpHost = getDeployProperty(CmsDeployProperty.HOST);
84
85 try {
86
87 ThreadPool threadPool = null;
88 if (executor != null) {
89 threadPool = new ExecutorThreadPool(executor);
90 } else {
91 // TODO make it configurable
92 threadPool = new QueuedThreadPool(10, 1);
93 }
94
95 server = new Server(threadPool);
96
97 configureConnectors(httpPortStr, httpsPortStr, httpHost);
98
99 if (httpConnector != null) {
100 httpConnector.open();
101 server.addConnector(httpConnector);
102 }
103
104 if (httpsConnector != null) {
105 httpsConnector.open();
106 server.addConnector(httpsConnector);
107 }
108
109 // holder
110
111 // context
112 rootContextHandler = createRootContextHandler();
113 // httpContext.addServlet(holder, "/*");
114 if (rootContextHandler != null)
115 configureRootContextHandler(rootContextHandler);
116
117 if (rootContextHandler != null && !contexts.containsKey("/"))
118 contextHandlerCollection.addHandler(rootContextHandler);
119
120 server.setHandler(contextHandlerCollection);
121
122 //
123 // START
124 server.start();
125 //
126
127 // Addresses
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());
135 }
136 // Clean up
137 Runtime.getRuntime().addShutdownHook(new Thread(() -> stop(), "Jetty shutdown"));
138
139 log.info(httpPortsMsg());
140 started = true;
141 } catch (Exception e) {
142 stop();
143 throw new IllegalStateException("Cannot start Jetty HTTP server", e);
144 }
145 }
146
147 protected void configureConnectors(String httpPortStr, String httpsPortStr, String httpHost) {
148
149 // try {
150 if (httpPortStr != null || httpsPortStr != null) {
151 // TODO deal with hostname resolving taking too much time
152 // String fallBackHostname = InetAddress.getLocalHost().getHostName();
153
154 boolean httpEnabled = httpPortStr != null;
155 boolean httpsEnabled = httpsPortStr != null;
156
157 if (httpEnabled) {
158 HttpConfiguration httpConfiguration = new HttpConfiguration();
159
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);
164 }
165
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);
171
172 }
173
174 if (httpsEnabled) {
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
178 try {
179 httpsConfigurator = new HttpsConfigurator(SSLContext.getDefault());
180 } catch (NoSuchAlgorithmException e) {
181 throw new IllegalStateException("Cannot initalise SSL Context", e);
182 }
183 }
184
185 SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
186 // sslContextFactory.setKeyStore(KeyS)
187
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");
193
194 sslContextFactory.setTrustStoreType(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORETYPE));
195 sslContextFactory.setTrustStorePath(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORE));
196 sslContextFactory.setTrustStorePassword(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD));
197
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);
204
205 // HTTPS Configuration
206 HttpConfiguration httpsConfiguration = new HttpConfiguration();
207 httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
208 httpsConfiguration.setUriCompliance(UriCompliance.LEGACY);
209
210 // HTTPS connector
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);
217 }
218 }
219 }
220
221 @Override
222 public void stop(int delay) {
223 // TODO wait for processing to complete
224 stop();
225
226 }
227
228 public void stop() {
229 try {
230 server.stop();
231 // TODO delete temp dir
232 started = false;
233 log.debug(() -> "Stopped Jetty server");
234 } catch (Exception e) {
235 log.error("Cannot stop Jetty HTTP server", e);
236 }
237
238 }
239
240 @Override
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;
245 }
246
247 @Override
248 public Executor getExecutor() {
249 return executor;
250 }
251
252 @Override
253 public synchronized HttpContext createContext(String path, HttpHandler handler) {
254 HttpContext httpContext = createContext(path);
255 httpContext.setHandler(handler);
256 return httpContext;
257 }
258
259 @Override
260 public synchronized HttpContext createContext(String path) {
261 if (!path.endsWith("/"))
262 path = path + "/";
263 if (contexts.containsKey(path))
264 throw new IllegalArgumentException("Context " + path + " already exists");
265
266 JettyHttpContext httpContext = new ServletHttpContext(this, path);
267 contexts.put(path, httpContext);
268
269 contextHandlerCollection.addHandler(httpContext.getServletContextHandler());
270 return httpContext;
271 }
272
273 @Override
274 public synchronized void removeContext(String path) throws IllegalArgumentException {
275 if (!path.endsWith("/"))
276 path = path + "/";
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());
284 } else {
285 // FIXME apparently servlets cannot be removed in Jetty, we should replace the
286 // handler
287 }
288 }
289
290 @Override
291 public synchronized void removeContext(HttpContext context) {
292 removeContext(context.getPath());
293 }
294
295 @Override
296 public InetSocketAddress getAddress() {
297 InetSocketAddress res = httpAddress != null ? httpAddress : httpsAddress;
298 if (res == null)
299 throw new IllegalStateException("Neither an HTTP nor and HTTPS address is available");
300 return res;
301 }
302
303 @Override
304 public void setHttpsConfigurator(HttpsConfigurator config) {
305 this.httpsConfigurator = config;
306 }
307
308 @Override
309 public HttpsConfigurator getHttpsConfigurator() {
310 return httpsConfigurator;
311 }
312
313 protected String getDeployProperty(CmsDeployProperty deployProperty) {
314 return cmsState != null ? cmsState.getDeployProperty(deployProperty.getProperty())
315 : System.getProperty(deployProperty.getProperty());
316 }
317
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() : "");
323 }
324
325 public String getHost() {
326 if (httpConnector == null)
327 return null;
328 return httpConnector.getHost();
329 }
330
331 public Integer getHttpPort() {
332 if (httpConnector == null)
333 return null;
334 return httpConnector.getLocalPort();
335 }
336
337 public Integer getHttpsPort() {
338 if (httpsConnector == null)
339 return null;
340 return httpsConnector.getLocalPort();
341 }
342
343 protected ServletContextHandler createRootContextHandler() {
344 return null;
345 }
346
347 protected void configureRootContextHandler(ServletContextHandler servletContextHandler) throws ServletException {
348
349 }
350
351 public void setCmsState(CmsState cmsState) {
352 this.cmsState = cmsState;
353 }
354
355 boolean isStarted() {
356 return started;
357 }
358
359 ServletContextHandler getRootContextHandler() {
360 return rootContextHandler;
361 }
362
363 ServerContainer getRootServerContainer() {
364 throw new UnsupportedOperationException();
365 }
366
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());
372 });
373 httpServer.start();
374 httpServer.createContext("/sub/context", (exchange) -> {
375 final String key = "count";
376 Integer count = (Integer) exchange.getHttpContext().getAttributes().get(key);
377 if (count == null)
378 exchange.getHttpContext().getAttributes().put(key, 0);
379 else
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());
386 });
387 }
388 }