]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java
a469a87c4ec8b22ca4b1f7a0dafc095502f613de
[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.util.Map;
6 import java.util.TreeMap;
7 import java.util.concurrent.Executor;
8 import java.util.concurrent.ThreadPoolExecutor;
9
10 import javax.servlet.ServletException;
11 import javax.websocket.server.ServerContainer;
12
13 import org.argeo.api.cms.CmsLog;
14 import org.argeo.api.cms.CmsState;
15 import org.argeo.cms.CmsDeployProperty;
16 import org.argeo.cms.http.server.HttpServerUtils;
17 import org.eclipse.jetty.http.UriCompliance;
18 import org.eclipse.jetty.server.HttpConfiguration;
19 import org.eclipse.jetty.server.HttpConnectionFactory;
20 import org.eclipse.jetty.server.SecureRequestCustomizer;
21 import org.eclipse.jetty.server.Server;
22 import org.eclipse.jetty.server.ServerConnector;
23 import org.eclipse.jetty.server.SslConnectionFactory;
24 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
25 import org.eclipse.jetty.servlet.ServletContextHandler;
26 import org.eclipse.jetty.util.ssl.SslContextFactory;
27 import org.eclipse.jetty.util.thread.ExecutorThreadPool;
28 import org.eclipse.jetty.util.thread.QueuedThreadPool;
29 import org.eclipse.jetty.util.thread.ThreadPool;
30
31 import com.sun.net.httpserver.HttpContext;
32 import com.sun.net.httpserver.HttpHandler;
33 import com.sun.net.httpserver.HttpServer;
34 import com.sun.net.httpserver.HttpsConfigurator;
35 import com.sun.net.httpserver.HttpsServer;
36
37 /** An {@link HttpServer} implementation based on Jetty. */
38 public class JettyHttpServer extends HttpsServer {
39 private final static CmsLog log = CmsLog.getLog(JettyHttpServer.class);
40
41 /** Long timeout since our users may have poor connections. */
42 private static final int DEFAULT_IDLE_TIMEOUT = 120 * 1000;
43
44 private Server server;
45
46 protected ServerConnector httpConnector;
47 protected ServerConnector httpsConnector;
48
49 private InetSocketAddress httpAddress;
50 private InetSocketAddress httpsAddress;
51
52 private ThreadPoolExecutor executor;
53
54 private HttpsConfigurator httpsConfigurator;
55
56 private final Map<String, JettyHttpContext> contexts = new TreeMap<>();
57
58 private ServletContextHandler rootContextHandler;
59 protected final ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
60
61 private boolean started;
62
63 private CmsState cmsState;
64
65 @Override
66 public void bind(InetSocketAddress addr, int backlog) throws IOException {
67 throw new UnsupportedOperationException();
68 }
69
70 @Override
71 public void start() {
72 String httpPortStr = getDeployProperty(CmsDeployProperty.HTTP_PORT);
73 String httpsPortStr = getDeployProperty(CmsDeployProperty.HTTPS_PORT);
74 if (httpPortStr != null && httpsPortStr != null)
75 throw new IllegalArgumentException("Either an HTTP or an HTTPS port should be configured, not both");
76 if (httpPortStr == null && httpsPortStr == null) {
77 log.warn("Neither an HTTP or an HTTPS port was configured, not starting Jetty");
78 }
79
80 /// TODO make it more generic
81 String httpHost = getDeployProperty(CmsDeployProperty.HOST);
82
83 try {
84
85 ThreadPool threadPool = null;
86 if (executor != null) {
87 threadPool = new ExecutorThreadPool(executor);
88 } else {
89 // TODO make it configurable
90 threadPool = new QueuedThreadPool(10, 1);
91 }
92
93 server = new Server(threadPool);
94
95 configureConnectors(httpPortStr, httpsPortStr, httpHost);
96
97 if (httpConnector != null) {
98 httpConnector.open();
99 server.addConnector(httpConnector);
100 }
101
102 if (httpsConnector != null) {
103 httpsConnector.open();
104 server.addConnector(httpsConnector);
105 }
106
107 // holder
108
109 // context
110 rootContextHandler = createRootContextHandler();
111 // httpContext.addServlet(holder, "/*");
112 if (rootContextHandler != null)
113 configureRootContextHandler(rootContextHandler);
114
115 if (rootContextHandler != null && !contexts.containsKey("/"))
116 contextHandlerCollection.addHandler(rootContextHandler);
117
118 server.setHandler(contextHandlerCollection);
119
120 //
121 // START
122 server.start();
123 //
124
125 // Addresses
126 String fallBackHostname = cmsState != null ? cmsState.getHostname() : "::1";
127 if (httpConnector != null) {
128 httpAddress = new InetSocketAddress(httpHost != null ? httpHost : fallBackHostname,
129 httpConnector.getLocalPort());
130 } else if (httpsConnector != null) {
131 httpsAddress = new InetSocketAddress(httpHost != null ? httpHost : fallBackHostname,
132 httpsConnector.getLocalPort());
133 }
134 // Clean up
135 Runtime.getRuntime().addShutdownHook(new Thread(() -> stop(), "Jetty shutdown"));
136
137 log.info(httpPortsMsg());
138 started = true;
139 } catch (Exception e) {
140 stop();
141 throw new IllegalStateException("Cannot start Jetty HTTP server", e);
142 }
143 }
144
145 protected void configureConnectors(String httpPortStr, String httpsPortStr, String httpHost) {
146
147 // try {
148 if (httpPortStr != null || httpsPortStr != null) {
149 // TODO deal with hostname resolving taking too much time
150 // String fallBackHostname = InetAddress.getLocalHost().getHostName();
151
152 boolean httpEnabled = httpPortStr != null;
153 boolean httpsEnabled = httpsPortStr != null;
154
155 if (httpEnabled) {
156 HttpConfiguration httpConfiguration = new HttpConfiguration();
157
158 if (httpsEnabled) {// not supported anymore to have both http and https, but it may change again
159 int httpsPort = Integer.parseInt(httpsPortStr);
160 httpConfiguration.setSecureScheme("https");
161 httpConfiguration.setSecurePort(httpsPort);
162 }
163
164 int httpPort = Integer.parseInt(httpPortStr);
165 httpConnector = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
166 httpConnector.setPort(httpPort);
167 httpConnector.setHost(httpHost);
168 httpConnector.setIdleTimeout(DEFAULT_IDLE_TIMEOUT);
169
170 }
171
172 if (httpsEnabled) {
173 SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
174 // sslContextFactory.setKeyStore(KeyS)
175
176 sslContextFactory.setKeyStoreType(getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE));
177 sslContextFactory.setKeyStorePath(getDeployProperty(CmsDeployProperty.SSL_KEYSTORE));
178 sslContextFactory.setKeyStorePassword(getDeployProperty(CmsDeployProperty.SSL_PASSWORD));
179 // sslContextFactory.setKeyManagerPassword(getFrameworkProp(CmsDeployProperty.SSL_KEYPASSWORD));
180 sslContextFactory.setProtocol("TLS");
181
182 sslContextFactory.setTrustStoreType(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORETYPE));
183 sslContextFactory.setTrustStorePath(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORE));
184 sslContextFactory.setTrustStorePassword(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD));
185
186 String wantClientAuth = getDeployProperty(CmsDeployProperty.SSL_WANTCLIENTAUTH);
187 if (wantClientAuth != null && wantClientAuth.equals(Boolean.toString(true)))
188 sslContextFactory.setWantClientAuth(true);
189 String needClientAuth = getDeployProperty(CmsDeployProperty.SSL_NEEDCLIENTAUTH);
190 if (needClientAuth != null && needClientAuth.equals(Boolean.toString(true)))
191 sslContextFactory.setNeedClientAuth(true);
192
193 // HTTPS Configuration
194 HttpConfiguration httpsConfiguration = new HttpConfiguration();
195 httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
196 httpsConfiguration.setUriCompliance(UriCompliance.LEGACY);
197
198 // HTTPS connector
199 httpsConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"),
200 new HttpConnectionFactory(httpsConfiguration));
201 int httpsPort = Integer.parseInt(httpsPortStr);
202 httpsConnector.setPort(httpsPort);
203 httpsConnector.setHost(httpHost);
204 httpsConnector.setIdleTimeout(DEFAULT_IDLE_TIMEOUT);
205 }
206 }
207 }
208
209 @Override
210 public void stop(int delay) {
211 // TODO wait for processing to complete
212 stop();
213
214 }
215
216 public void stop() {
217 try {
218 server.stop();
219 // TODO delete temp dir
220 started = false;
221 } catch (Exception e) {
222 log.error("Cannot stop Jetty HTTP server", e);
223 }
224
225 }
226
227 @Override
228 public void setExecutor(Executor executor) {
229 if (!(executor instanceof ThreadPoolExecutor))
230 throw new IllegalArgumentException("Only " + ThreadPoolExecutor.class.getName() + " are supported");
231 this.executor = (ThreadPoolExecutor) executor;
232 }
233
234 @Override
235 public Executor getExecutor() {
236 return executor;
237 }
238
239 @Override
240 public synchronized HttpContext createContext(String path, HttpHandler handler) {
241 HttpContext httpContext = createContext(path);
242 httpContext.setHandler(handler);
243 return httpContext;
244 }
245
246 @Override
247 public synchronized HttpContext createContext(String path) {
248 if (!path.endsWith("/"))
249 path = path + "/";
250 if (contexts.containsKey(path))
251 throw new IllegalArgumentException("Context " + path + " already exists");
252
253 JettyHttpContext httpContext = new ServletHttpContext(this, path);
254 contexts.put(path, httpContext);
255
256 contextHandlerCollection.addHandler(httpContext.getServletContextHandler());
257 return httpContext;
258 }
259
260 @Override
261 public synchronized void removeContext(String path) throws IllegalArgumentException {
262 if (!path.endsWith("/"))
263 path = path + "/";
264 if (!contexts.containsKey(path))
265 throw new IllegalArgumentException("Context " + path + " does not exist");
266 JettyHttpContext httpContext = contexts.remove(path);
267 if (httpContext instanceof ContextHandlerHttpContext contextHandlerHttpContext) {
268 // TODO stop handler first?
269 contextHandlerCollection.removeHandler(contextHandlerHttpContext.getServletContextHandler());
270 }
271 }
272
273 @Override
274 public synchronized void removeContext(HttpContext context) {
275 removeContext(context.getPath());
276 }
277
278 @Override
279 public InetSocketAddress getAddress() {
280 InetSocketAddress res = httpAddress != null ? httpAddress : httpsAddress;
281 if (res == null)
282 throw new IllegalStateException("Neither an HTTP nor and HTTPS address is available");
283 return res;
284 }
285
286 @Override
287 public void setHttpsConfigurator(HttpsConfigurator config) {
288 this.httpsConfigurator = config;
289 }
290
291 @Override
292 public HttpsConfigurator getHttpsConfigurator() {
293 return httpsConfigurator;
294 }
295
296 protected String getDeployProperty(CmsDeployProperty deployProperty) {
297 return cmsState != null ? cmsState.getDeployProperty(deployProperty.getProperty())
298 : System.getProperty(deployProperty.getProperty());
299 }
300
301 private String httpPortsMsg() {
302
303 return (httpConnector != null ? "HTTP " + getHttpPort() + " " : "")
304 + (httpsConnector != null ? "HTTPS " + getHttpsPort() : "");
305 }
306
307 public Integer getHttpPort() {
308 if (httpConnector == null)
309 return null;
310 return httpConnector.getLocalPort();
311 }
312
313 public Integer getHttpsPort() {
314 if (httpsConnector == null)
315 return null;
316 return httpsConnector.getLocalPort();
317 }
318
319 protected ServletContextHandler createRootContextHandler() {
320 return null;
321 }
322
323 protected void configureRootContextHandler(ServletContextHandler servletContextHandler) throws ServletException {
324
325 }
326
327 public void setCmsState(CmsState cmsState) {
328 this.cmsState = cmsState;
329 }
330
331 boolean isStarted() {
332 return started;
333 }
334
335 ServletContextHandler getRootContextHandler() {
336 return rootContextHandler;
337 }
338
339 ServerContainer getRootServerContainer() {
340 throw new UnsupportedOperationException();
341 }
342
343 public static void main(String... args) {
344 JettyHttpServer httpServer = new JettyHttpServer();
345 System.setProperty("argeo.http.port", "8080");
346 httpServer.createContext("/", (exchange) -> {
347 exchange.getResponseBody().write("Hello World!".getBytes());
348 });
349 httpServer.start();
350 httpServer.createContext("/sub/context", (exchange) -> {
351 final String key = "count";
352 Integer count = (Integer) exchange.getHttpContext().getAttributes().get(key);
353 if (count == null)
354 exchange.getHttpContext().getAttributes().put(key, 0);
355 else
356 exchange.getHttpContext().getAttributes().put(key, count + 1);
357 StringBuilder sb = new StringBuilder();
358 sb.append("Subcontext:");
359 sb.append(" " + key + "=" + exchange.getHttpContext().getAttributes().get(key));
360 sb.append(" relativePath=" + HttpServerUtils.relativize(exchange));
361 exchange.getResponseBody().write(sb.toString().getBytes());
362 });
363 }
364 }