1 package eu.fbk.knowledgestore.server.http;
2
3 import ch.qos.logback.access.jetty.RequestLogImpl;
4 import com.google.common.base.Charsets;
5 import com.google.common.base.MoreObjects;
6 import com.google.common.base.Preconditions;
7 import com.google.common.collect.ImmutableList;
8 import com.google.common.collect.Sets;
9 import com.google.common.io.Resources;
10 import eu.fbk.knowledgestore.ForwardingKnowledgeStore;
11 import eu.fbk.knowledgestore.KnowledgeStore;
12 import eu.fbk.knowledgestore.Session;
13 import eu.fbk.knowledgestore.data.Data;
14 import eu.fbk.knowledgestore.runtime.Component;
15 import eu.fbk.knowledgestore.server.http.jaxrs.*;
16 import org.eclipse.jetty.jmx.MBeanContainer;
17 import org.eclipse.jetty.security.HashLoginService;
18 import org.eclipse.jetty.server.*;
19 import org.eclipse.jetty.server.handler.HandlerCollection;
20 import org.eclipse.jetty.server.handler.RequestLogHandler;
21 import org.eclipse.jetty.server.handler.StatisticsHandler;
22 import org.eclipse.jetty.util.resource.Resource;
23 import org.eclipse.jetty.util.ssl.SslContextFactory;
24 import org.eclipse.jetty.util.thread.ExecutorThreadPool;
25 import org.eclipse.jetty.webapp.WebAppContext;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
29 import javax.annotation.Nullable;
30 import java.io.File;
31 import java.io.IOException;
32 import java.lang.management.ManagementFactory;
33 import java.net.URL;
34 import java.util.Set;
35 import java.util.UUID;
36 import java.util.concurrent.ExecutorService;
37
38
39
40
41
42
43
44
45 public class HttpServer extends ForwardingKnowledgeStore implements Component {
46
47 private static final Logger LOGGER = LoggerFactory.getLogger(HttpServer.class);
48
49 private static final String DEFAULT_HOST = "0.0.0.0";
50
51 private static final String DEFAULT_PATH = "/";
52
53 private static final int DEFAULT_HTTP_PORT = 8080;
54
55 private static final int DEFAULT_HTTPS_PORT = 8443;
56
57 private static final int DEFAULT_ACCEPTORS = -1;
58
59 private static final int DEFAULT_SELECTORS = -1;
60
61 private static final KeystoreConfig DEFAULT_KEYSTORE_CONFIG = new KeystoreConfig(
62 HttpServer.class.getResource("HttpServer.jks").toString(), "kspass", null, null);
63
64 private static final String DEFAULT_REALM = "KnowledgeStore";
65
66 private static final UIConfig DEFAULT_UI_CONFIG = UIConfig.builder().build();
67
68 private static final long STOP_TIMEOUT = 1000;
69
70 private final KnowledgeStore delegate;
71
72 private final Server server;
73
74 private HttpServer(final Builder builder) {
75 this.delegate = Preconditions.checkNotNull(builder.delegate);
76 this.server = createJettyServer(builder);
77 }
78
79 @Override
80 protected KnowledgeStore delegate() {
81 return this.delegate;
82 }
83
84 @Override
85 public void init() throws IOException {
86
87 if (this.delegate instanceof Component) {
88 ((Component) this.delegate).init();
89 }
90
91 try {
92 this.server.start();
93 if (LOGGER.isInfoEnabled()) {
94 final StringBuilder builder = new StringBuilder("Jetty ").append(
95 Server.getVersion()).append(" started, listening on ");
96 builder.append(((ServerConnector) this.server.getConnectors()[0]).getHost());
97 builder.append(", port(s)");
98 for (final Connector connector : this.server.getConnectors()) {
99 builder.append(" ").append(((ServerConnector) connector).getPort());
100 }
101 builder.append(", base '").append(this.server.getAttribute("base")).append('\'');
102 LOGGER.info(builder.toString());
103 }
104
105 } catch (final Exception ex) {
106 throw ex instanceof RuntimeException ? (RuntimeException) ex
107 : new RuntimeException(ex);
108 }
109 }
110
111 @Override
112 public Session newSession() throws IllegalStateException {
113 return this.delegate.newSession();
114 }
115
116 @Override
117 public Session newSession(final String username, final String password)
118 throws IllegalStateException {
119 return this.delegate.newSession(username, password);
120 }
121
122 @Override
123 public boolean isClosed() {
124 return this.delegate.isClosed();
125 }
126
127 @Override
128 public void close() {
129 try {
130 this.server.setStopTimeout(STOP_TIMEOUT);
131 this.server.stop();
132 LOGGER.info("Jetty {} stopped", Server.getVersion());
133 } catch (final Exception ex) {
134
135 LOGGER.warn("Jetty {} stopped (with errors)", Server.getVersion());
136 } finally {
137 this.delegate.close();
138 }
139 }
140
141 private Server createJettyServer(final Builder builder) {
142
143
144 final String host = MoreObjects.firstNonNull(builder.host, DEFAULT_HOST);
145 String base = MoreObjects.firstNonNull(builder.path, DEFAULT_PATH);
146 final int acceptors = MoreObjects.firstNonNull(builder.acceptors, DEFAULT_ACCEPTORS);
147 final int selectors = MoreObjects.firstNonNull(builder.selectors, DEFAULT_SELECTORS);
148 final boolean debug = Boolean.TRUE.equals(builder.debug);
149 base = base.startsWith("/") ? base : "/" + base;
150
151
152 final ExecutorService scheduler = Data.getExecutor();
153
154
155 final Server server = new Server(new ExecutorThreadPool(scheduler) {
156
157 @Override
158 protected void doStop() throws Exception {
159
160
161 }
162
163 });
164 server.setDumpAfterStart(false);
165 server.setDumpBeforeStop(false);
166 server.setStopAtShutdown(true);
167
168
169
170
171 final MBeanContainer jmxContainer = new MBeanContainer(
172 ManagementFactory.getPlatformMBeanServer());
173 server.addBean(jmxContainer);
174
175
176 final SecurityConfig sc = builder.securityConfig;
177 String realm = DEFAULT_REALM;
178 Set<String> anonymousRoles = SecurityConfig.ALL_ROLES;
179 if (sc != null) {
180 realm = MoreObjects.firstNonNull(sc.getRealm(), realm);
181 anonymousRoles = sc.getAnonymousRoles();
182 final String userdb = sc.getUserdbURL().toString();
183 final HashLoginService login = new HashLoginService();
184 login.setName(sc.getRealm());
185 login.setConfig(userdb);
186 login.setRefreshInterval(userdb.startsWith("file://") ? 60000 : 0);
187 server.addBean(login);
188 }
189
190
191 final int httpPort = MoreObjects.firstNonNull(builder.httpPort, DEFAULT_HTTP_PORT);
192 final int httpsPort = MoreObjects.firstNonNull(builder.httpsPort, DEFAULT_HTTPS_PORT);
193 if (httpPort > 0) {
194 Preconditions.checkArgument(httpPort < 65536, "Invalid HTTP port %s", httpPort);
195 server.addConnector(createServerConnector(server, host, httpPort, acceptors,
196 selectors, createHttpConnectionFactory(httpsPort, false)));
197 }
198 if (httpsPort > 0) {
199 Preconditions.checkArgument(httpsPort < 65536 && httpsPort != httpPort,
200 "Invalid HTTPS port %s", httpsPort);
201 final KeystoreConfig kc = MoreObjects.firstNonNull(builder.keystoreConfig,
202 DEFAULT_KEYSTORE_CONFIG);
203 server.addConnector(createServerConnector(server, host, httpsPort, acceptors,
204 selectors, createSSLConnectionFactory(kc),
205 createHttpConnectionFactory(httpsPort, true)));
206 }
207
208
209 final WebAppContext webappHandler = new WebAppContext();
210 webappHandler.setThrowUnavailableOnStartupException(true);
211 webappHandler.setTempDirectory(new File(System.getProperty("java.io.tmpdir") + "/"
212 + UUID.randomUUID().toString()));
213 webappHandler.setResourceBase(getClass().getClassLoader()
214 .getResource("eu/fbk/knowledgestore/server/http/jaxrs").toExternalForm());
215 webappHandler.setParentLoaderPriority(true);
216 webappHandler.setMaxFormContentSize(Integer.MAX_VALUE);
217 webappHandler.setDescriptor(createWebXml(realm, anonymousRoles, debug).toString());
218 webappHandler.setConfigurationDiscovered(false);
219 webappHandler.setCompactPath(true);
220 webappHandler.setClassLoader(getClass().getClassLoader());
221 webappHandler.setContextPath(base);
222 webappHandler.getServletContext().setAttribute(Application.STORE_ATTRIBUTE, this);
223 webappHandler.getServletContext().setAttribute(Application.TRACING_ATTRIBUTE, debug);
224 webappHandler.getServletContext().setAttribute(Application.CUSTOM_ATTRIBUTE, builder.customConfigs);
225 webappHandler.getServletContext().setAttribute(Application.UI_ATTRIBUTE,
226 MoreObjects.firstNonNull(builder.uiConfig, DEFAULT_UI_CONFIG));
227 webappHandler.getServletContext().setAttribute(
228 Application.RESOURCE_ATTRIBUTE,
229 ImmutableList.of(
230 Root.class,
231 Files.class,
232 eu.fbk.knowledgestore.server.http.jaxrs.Resources.class,
233 Mentions.class,
234
235
236
237 Sparql.class,
238 SparqlUpdate.class,
239 SparqlDelete.class,
240 Custom.class
241 )
242 );
243
244
245 RequestLogHandler requestLogHandler = null;
246 if (builder.logLocation != null) {
247 LOGGER.info("Log location: {}", builder.logLocation);
248 NCSARequestLog requestLog = new NCSARequestLog(builder.logLocation + "ksd-yyyy_mm_dd-http.log");
249 requestLog.setAppend(true);
250 requestLog.setExtended(false);
251 requestLog.setLogTimeZone("GMT");
252 requestLogHandler = new RequestLogHandler();
253 requestLogHandler.setRequestLog(requestLog);
254 }
255 if (builder.logConfigLocation != null) {
256 final RequestLogImpl requestLog = new RequestLogImpl();
257 requestLog.setQuiet(true);
258 requestLog.setResource((builder.logConfigLocation.startsWith("/") ? "" : "/")
259 + builder.logConfigLocation);
260 requestLogHandler = new RequestLogHandler();
261 requestLogHandler.setRequestLog(requestLog);
262 }
263
264
265 final Handler handler = webappHandler;
266
267
268 final StatisticsHandler statHandler = new StatisticsHandler();
269
270
271 if (requestLogHandler == null) {
272 statHandler.setHandler(handler);
273 } else {
274 final HandlerCollection multiHandler = new HandlerCollection();
275 multiHandler.setHandlers(new Handler[] { handler, requestLogHandler });
276 statHandler.setHandler(multiHandler);
277 }
278
279 server.setHandler(statHandler);
280 server.setAttribute("base", base);
281
282
283 return server;
284 }
285
286 private ServerConnector createServerConnector(final Server server, final String host,
287 final int port, final int acceptors, final int selectors,
288 final ConnectionFactory... connectionFactories) {
289
290 final ServerConnector connector = new ServerConnector(server, null, null, null, acceptors,
291 selectors, connectionFactories);
292 connector.setHost(host);
293 connector.setPort(port);
294 connector.setReuseAddress(true);
295 connector.addBean(new ConnectorStatistics());
296 return connector;
297 }
298
299 private HttpConnectionFactory createHttpConnectionFactory(final int httpsPort,
300 final boolean customizer) {
301
302 final HttpConfiguration config = new HttpConfiguration();
303 if (httpsPort > 0) {
304 config.setSecureScheme("https");
305 config.setSecurePort(httpsPort);
306 if (customizer) {
307 config.addCustomizer(new SecureRequestCustomizer());
308 }
309 }
310
311 config.setOutputBufferSize(32 * 1024);
312 config.setRequestHeaderSize(8 * 1024);
313 config.setResponseHeaderSize(8 * 1024);
314 config.setSendServerVersion(true);
315 config.setSendDateHeader(true);
316 final HttpConnectionFactory factory = new HttpConnectionFactory(config);
317 return factory;
318 }
319
320 private SslConnectionFactory createSSLConnectionFactory(final KeystoreConfig keystore) {
321
322 final Resource keystoreResource = Resource.newResource(keystore.getURL());
323
324 final SslContextFactory contextFactory = new SslContextFactory();
325 contextFactory.setEnableCRLDP(false);
326 contextFactory.setEnableOCSP(false);
327 contextFactory.setNeedClientAuth(false);
328 contextFactory.setWantClientAuth(false);
329 contextFactory.setValidateCerts(false);
330 contextFactory.setValidatePeerCerts(false);
331 contextFactory.setRenegotiationAllowed(false);
332 contextFactory.setSessionCachingEnabled(true);
333 contextFactory.setKeyStoreResource(keystoreResource);
334 contextFactory.setKeyStorePassword(keystore.getPassword());
335 contextFactory.setCertAlias(keystore.getAlias());
336 contextFactory.setKeyStoreType(keystore.getType());
337 contextFactory.setIncludeCipherSuites("TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
338 "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA",
339 "SSL_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
340 "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA");
341
342 final SslConnectionFactory factory = new SslConnectionFactory(contextFactory, "http/1.1");
343 return factory;
344 }
345
346 private URL createWebXml(final String realmName,
347 final Iterable<? extends String> anonymousRoles, final boolean debug) {
348 try {
349
350 String webxml = Resources.toString(HttpServer.class.getResource("HttpServer.web.xml"),
351 Charsets.UTF_8);
352
353
354 if (!debug) {
355 int index = webxml.indexOf("<filter-name>TeeFilter</filter-name>");
356 final int start1 = webxml.lastIndexOf("<filter>", index);
357 final int end1 = webxml.indexOf("</filter>", index) + 9;
358 index = webxml.indexOf("<filter-name>TeeFilter</filter-name>", end1);
359 final int start2 = webxml.lastIndexOf("<filter-mapping>", index);
360 final int end2 = webxml.indexOf("</filter-mapping>", index) + 17;
361 webxml = webxml.substring(0, start1) + webxml.substring(end1, start2)
362 + webxml.substring(end2);
363 }
364
365
366 StringBuilder builder = new StringBuilder();
367 int index = 0;
368 int last = 0;
369 while ((index = webxml.indexOf("<realm-name>", last)) >= 0) {
370 index = index + 12;
371 builder.append(webxml.substring(last, index));
372 builder.append(realmName);
373 index = webxml.indexOf("</realm-name>", index);
374 last = index + 13;
375 builder.append(webxml.substring(index, last));
376 }
377 builder.append(webxml.substring(last));
378 webxml = builder.toString();
379
380
381 builder = new StringBuilder();
382 last = 0;
383 while ((index = webxml.indexOf("<security-constraint>", last)) >= 0) {
384 builder.append(webxml.substring(last, index));
385 last = webxml.indexOf("</security-constraint>", index) + 22;
386 final int start1 = index;
387 int end1 = -1;
388 final Set<String> roles = Sets.newHashSet();
389 int start2 = index + 21;
390 while (true) {
391 index = webxml.indexOf("<role-name>", start2);
392 if (index < 0 || index >= last) {
393 break;
394 }
395 end1 = end1 >= 0 ? end1 : index;
396 index += 11;
397 final int roleStart = index;
398 index = webxml.indexOf("</role-name>", index);
399 roles.add(webxml.substring(roleStart, index));
400 start2 = index + 12;
401 }
402 last = webxml.indexOf("</security-constraint>", start2) + 22;
403 for (final String role : anonymousRoles) {
404 roles.remove(role);
405 }
406 if (!roles.isEmpty()) {
407 builder.append(webxml.substring(start1, end1));
408 for (final String role : roles) {
409 builder.append("<role-name>").append(role).append("</role-name>");
410 }
411 builder.append(webxml.substring(start2, last));
412 }
413 }
414 builder.append(webxml.substring(last));
415 webxml = builder.toString();
416
417
418 final File file = File.createTempFile("knowledgestore_", ".web.xml");
419 file.deleteOnExit();
420 com.google.common.io.Files.write(webxml, file, Charsets.UTF_8);
421 return file.toURI().toURL();
422
423 } catch (final Exception ex) {
424 throw new Error("Cannot configure web.xml descriptor: " + ex.getMessage(), ex);
425 }
426 }
427
428 public static Builder builder(final KnowledgeStore delegate) {
429 return new Builder(delegate);
430 }
431
432 public static class Builder {
433
434 final KnowledgeStore delegate;
435
436 @Nullable
437 String host;
438
439 @Nullable
440 Integer httpPort;
441
442 @Nullable
443 Integer httpsPort;
444
445 @Nullable
446 String path;
447
448 @Nullable
449 String proxyHttpRoot;
450
451 @Nullable
452 String proxyHttpsRoot;
453
454 @Nullable
455 KeystoreConfig keystoreConfig;
456
457 @Nullable
458 Integer acceptors;
459
460 @Nullable
461 Integer selectors;
462
463 @Nullable
464 SecurityConfig securityConfig;
465
466 @Nullable
467 UIConfig uiConfig;
468
469 @Nullable
470 Iterable<CustomConfig> customConfigs;
471
472 @Nullable
473 Boolean debug;
474
475 @Nullable
476 String logLocation;
477
478 @Nullable
479 String logConfigLocation;
480
481 Builder(final KnowledgeStore store) {
482 this.delegate = Preconditions.checkNotNull(store);
483 }
484
485 public Builder host(@Nullable final String host) {
486 this.host = host;
487 return this;
488 }
489
490 public Builder httpPort(@Nullable final Integer httpPort) {
491 this.httpPort = httpPort;
492 return this;
493 }
494
495 public Builder httpsPort(@Nullable final Integer httpsPort) {
496 this.httpsPort = httpsPort;
497 return this;
498 }
499
500 public Builder path(@Nullable final String path) {
501 this.path = path;
502 return this;
503 }
504
505 public Builder proxyHttpRoot(@Nullable final String proxyHttpRoot) {
506 this.proxyHttpRoot = proxyHttpRoot;
507 return this;
508 }
509
510 public Builder proxyHttpsRoot(@Nullable final String proxyHttpsRoot) {
511 this.proxyHttpsRoot = proxyHttpsRoot;
512 return this;
513 }
514
515 public Builder acceptors(@Nullable final Integer acceptors) {
516 this.acceptors = acceptors;
517 return this;
518 }
519
520 public Builder selectors(@Nullable final Integer selectors) {
521 this.selectors = selectors;
522 return this;
523 }
524
525 public Builder keystoreConfig(@Nullable final KeystoreConfig keystoreConfig) {
526 this.keystoreConfig = keystoreConfig;
527 return this;
528 }
529
530 public Builder customConfigs(@Nullable final Iterable<CustomConfig> customConfigs) {
531 this.customConfigs = customConfigs;
532 return this;
533 }
534
535 public Builder securityConfig(@Nullable final SecurityConfig securityConfig) {
536 this.securityConfig = securityConfig;
537 return this;
538 }
539
540 public Builder uiConfig(@Nullable final UIConfig uiConfig) {
541 this.uiConfig = uiConfig;
542 return this;
543 }
544
545 public Builder debug(@Nullable final Boolean debug) {
546 this.debug = debug;
547 return this;
548 }
549
550 public Builder logLocation(@Nullable final String logLocation) {
551 this.logLocation = logLocation;
552 return this;
553 }
554
555 public Builder logConfigLocation(@Nullable final String logConfigLocation) {
556 this.logConfigLocation = logConfigLocation;
557 return this;
558 }
559
560 public HttpServer build() {
561 return new HttpServer(this);
562 }
563
564 }
565
566 }