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  // TODO: check
39  // https://jersey.java.net/apidocs/2.5.1/jersey/org/glassfish/jersey/server/filter/UriConnegFilter.html
40  
41  // TODO: add DOSFilter and QOSFilter from jetty-servlets
42  
43  // TODO: getters and tostring
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; // -1 = platform specific
58  
59      private static final int DEFAULT_SELECTORS = -1; // -1 = platform specific
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; // wait 1 s before forcing closure
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             // Just log a warning without additional detail, as Jetty already prints log info
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         // Retrieve common attributes
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         // Retrieve the scheduler
152         final ExecutorService scheduler = Data.getExecutor();
153 
154         // Create HTTP server
155         final Server server = new Server(new ExecutorThreadPool(scheduler) {
156 
157             @Override
158             protected void doStop() throws Exception {
159                 // Do nothing (default behaviour is shutting down the supplied scheduler, which is
160                 // something we do not want.
161             }
162 
163         });
164         server.setDumpAfterStart(false);
165         server.setDumpBeforeStop(false);
166         server.setStopAtShutdown(true);
167         // server.setSendDateHeader(true);
168         // server.setSendServerVersion(true); // custom header sent
169 
170         // enable JMX support
171         final MBeanContainer jmxContainer = new MBeanContainer(
172                 ManagementFactory.getPlatformMBeanServer());
173         server.addBean(jmxContainer);
174 
175         // configure security
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         // add HTTP and HTTPS connectors
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         // configure Webapp handler
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                         // Entities.class,
235                         // Axioms.class,
236                         // Match.class,
237                         Sparql.class,
238                         SparqlUpdate.class,
239                         SparqlDelete.class,
240                         Custom.class
241                 )
242         );
243 
244         // configure request logging using logback access
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         // add proxy pass reverse support
265         final Handler handler = webappHandler;
266 
267         // configure request statistics collector (accessible via JMX)
268         final StatisticsHandler statHandler = new StatisticsHandler();
269 
270         // configure CLF logging
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         // return configured server
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); // to avoid respawning issues with supervisord
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); // avoid vulnerability
332         contextFactory.setSessionCachingEnabled(true); // optimization
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"); // check RFC 5430
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             // load web.xml from classpath
350             String webxml = Resources.toString(HttpServer.class.getResource("HttpServer.web.xml"),
351                     Charsets.UTF_8);
352 
353             // disable TeeFilter if not in debug mode
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             // replace references to <realm-name>
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             // disable security constraints for anonymous roles
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             // save filtered web.xml to temporary file, returning associated URL
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; // TODO: handle this
450 
451         @Nullable
452         String proxyHttpsRoot; // TODO: handle this
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 }