]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java
Extract last part of a path
[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.http.UriCompliance;
20 import org.eclipse.jetty.server.HttpConfiguration;
21 import org.eclipse.jetty.server.HttpConnectionFactory;
22 import org.eclipse.jetty.server.SecureRequestCustomizer;
23 import org.eclipse.jetty.server.Server;
24 import org.eclipse.jetty.server.ServerConnector;
25 import org.eclipse.jetty.server.SslConnectionFactory;
26 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
27 import org.eclipse.jetty.servlet.ServletContextHandler;
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 } catch (Exception e) {
234 log.error("Cannot stop Jetty HTTP server", e);
235 }
236
237 }
238
239 @Override
240 public void setExecutor(Executor executor) {
241 if (!(executor instanceof ThreadPoolExecutor))
242 throw new IllegalArgumentException("Only " + ThreadPoolExecutor.class.getName() + " are supported");
243 this.executor = (ThreadPoolExecutor) executor;
244 }
245
246 @Override
247 public Executor getExecutor() {
248 return executor;
249 }
250
251 @Override
252 public synchronized HttpContext createContext(String path, HttpHandler handler) {
253 HttpContext httpContext = createContext(path);
254 httpContext.setHandler(handler);
255 return httpContext;
256 }
257
258 @Override
259 public synchronized HttpContext createContext(String path) {
260 if (!path.endsWith("/"))
261 path = path + "/";
262 if (contexts.containsKey(path))
263 throw new IllegalArgumentException("Context " + path + " already exists");
264
265 JettyHttpContext httpContext = new ServletHttpContext(this, path);
266 contexts.put(path, httpContext);
267
268 contextHandlerCollection.addHandler(httpContext.getServletContextHandler());
269 return httpContext;
270 }
271
272 @Override
273 public synchronized void removeContext(String path) throws IllegalArgumentException {
274 if (!path.endsWith("/"))
275 path = path + "/";
276 if (!contexts.containsKey(path))
277 throw new IllegalArgumentException("Context " + path + " does not exist");
278 JettyHttpContext httpContext = contexts.remove(path);
279 if (httpContext instanceof ContextHandlerHttpContext contextHandlerHttpContext) {
280 // TODO stop handler first?
281 contextHandlerCollection.removeHandler(contextHandlerHttpContext.getServletContextHandler());
282 } else {
283 // FIXME apparently servlets cannot be removed in Jetty, we should replace the
284 // handler
285 }
286 }
287
288 @Override
289 public synchronized void removeContext(HttpContext context) {
290 removeContext(context.getPath());
291 }
292
293 @Override
294 public InetSocketAddress getAddress() {
295 InetSocketAddress res = httpAddress != null ? httpAddress : httpsAddress;
296 if (res == null)
297 throw new IllegalStateException("Neither an HTTP nor and HTTPS address is available");
298 return res;
299 }
300
301 @Override
302 public void setHttpsConfigurator(HttpsConfigurator config) {
303 this.httpsConfigurator = config;
304 }
305
306 @Override
307 public HttpsConfigurator getHttpsConfigurator() {
308 return httpsConfigurator;
309 }
310
311 protected String getDeployProperty(CmsDeployProperty deployProperty) {
312 return cmsState != null ? cmsState.getDeployProperty(deployProperty.getProperty())
313 : System.getProperty(deployProperty.getProperty());
314 }
315
316 private String httpPortsMsg() {
317
318 return (httpConnector != null ? "HTTP " + getHttpPort() + " " : "")
319 + (httpsConnector != null ? "HTTPS " + getHttpsPort() : "");
320 }
321
322 public Integer getHttpPort() {
323 if (httpConnector == null)
324 return null;
325 return httpConnector.getLocalPort();
326 }
327
328 public Integer getHttpsPort() {
329 if (httpsConnector == null)
330 return null;
331 return httpsConnector.getLocalPort();
332 }
333
334 protected ServletContextHandler createRootContextHandler() {
335 return null;
336 }
337
338 protected void configureRootContextHandler(ServletContextHandler servletContextHandler) throws ServletException {
339
340 }
341
342 public void setCmsState(CmsState cmsState) {
343 this.cmsState = cmsState;
344 }
345
346 boolean isStarted() {
347 return started;
348 }
349
350 ServletContextHandler getRootContextHandler() {
351 return rootContextHandler;
352 }
353
354 ServerContainer getRootServerContainer() {
355 throw new UnsupportedOperationException();
356 }
357
358 public static void main(String... args) {
359 JettyHttpServer httpServer = new JettyHttpServer();
360 System.setProperty("argeo.http.port", "8080");
361 httpServer.createContext("/", (exchange) -> {
362 exchange.getResponseBody().write("Hello World!".getBytes());
363 });
364 httpServer.start();
365 httpServer.createContext("/sub/context", (exchange) -> {
366 final String key = "count";
367 Integer count = (Integer) exchange.getHttpContext().getAttributes().get(key);
368 if (count == null)
369 exchange.getHttpContext().getAttributes().put(key, 0);
370 else
371 exchange.getHttpContext().getAttributes().put(key, count + 1);
372 StringBuilder sb = new StringBuilder();
373 sb.append("Subcontext:");
374 sb.append(" " + key + "=" + exchange.getHttpContext().getAttributes().get(key));
375 sb.append(" relativePath=" + HttpServerUtils.relativize(exchange));
376 exchange.getResponseBody().write(sb.toString().getBytes());
377 });
378 }
379 }