/** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.aurora.scheduler.http; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.EnumSet; import java.util.Map; import java.util.Set; import javax.annotation.Nonnegative; import javax.inject.Inject; import javax.inject.Singleton; import javax.servlet.DispatcherType; import javax.servlet.ServletContextListener; import javax.ws.rs.HttpMethod; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.net.HostAndPort; import com.google.common.util.concurrent.AbstractIdleService; import com.google.inject.AbstractModule; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.PrivateModule; import com.google.inject.Provides; import com.google.inject.TypeLiteral; import com.google.inject.name.Names; import com.google.inject.servlet.GuiceFilter; import com.google.inject.servlet.GuiceServletContextListener; import com.google.inject.util.Modules; import com.sun.jersey.guice.JerseyServletModule; import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; import org.apache.aurora.common.args.Arg; import org.apache.aurora.common.args.CmdLine; import org.apache.aurora.common.net.http.handlers.AbortHandler; import org.apache.aurora.common.net.http.handlers.ContentionPrinter; import org.apache.aurora.common.net.http.handlers.HealthHandler; import org.apache.aurora.common.net.http.handlers.QuitHandler; import org.apache.aurora.common.net.http.handlers.ThreadStackPrinter; import org.apache.aurora.common.net.http.handlers.TimeSeriesDataSource; import org.apache.aurora.common.net.http.handlers.VarsHandler; import org.apache.aurora.common.net.http.handlers.VarsJsonHandler; import org.apache.aurora.scheduler.SchedulerServicesModule; import org.apache.aurora.scheduler.app.ServiceGroupMonitor.MonitorException; import org.apache.aurora.scheduler.http.api.ApiModule; import org.apache.aurora.scheduler.http.api.security.HttpSecurityModule; import org.apache.aurora.scheduler.thrift.ThriftModule; import org.eclipse.jetty.rewrite.handler.RewriteHandler; import org.eclipse.jetty.rewrite.handler.RewriteRegexRule; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.Slf4jRequestLog; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.util.resource.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.Objects.requireNonNull; import static com.sun.jersey.api.json.JSONConfiguration.FEATURE_POJO_MAPPING; /** * Binding module for scheduler HTTP servlets. * <p> * TODO(wfarner): Continue improvements here by simplifying serving of static assets. Jetty's * DefaultServlet can take over this responsibility, and jetty-rewite can be used to rewrite * requests (for static assets) similar to what we currently do with path specs. */ public class JettyServerModule extends AbstractModule { private static final Logger LOG = LoggerFactory.getLogger(JettyServerModule.class); // The name of the request attribute where the path for the current request before it was // rewritten is stored. static final String ORIGINAL_PATH_ATTRIBUTE_NAME = "originalPath"; @CmdLine(name = "hostname", help = "The hostname to advertise in ZooKeeper instead of the locally-resolved hostname.") private static final Arg<String> HOSTNAME_OVERRIDE = Arg.create(null); @Nonnegative @CmdLine(name = "http_port", help = "The port to start an HTTP server on. Default value will choose a random port.") protected static final Arg<Integer> HTTP_PORT = Arg.create(0); @CmdLine(name = "ip", help = "The ip address to listen. If not set, the scheduler will listen on all interfaces.") protected static final Arg<String> LISTEN_IP = Arg.create(); public static final Map<String, String> GUICE_CONTAINER_PARAMS = ImmutableMap.of( FEATURE_POJO_MAPPING, Boolean.TRUE.toString()); private static final String STATIC_ASSETS_ROOT = Resource .newClassPathResource("scheduler/assets/index.html") .toString() .replace("assets/index.html", ""); private final boolean production; public JettyServerModule() { this(true); } @VisibleForTesting JettyServerModule(boolean production) { this.production = production; } @Override protected void configure() { bind(Runnable.class) .annotatedWith(Names.named(AbortHandler.ABORT_HANDLER_KEY)) .to(AbortCallback.class); bind(AbortCallback.class).in(Singleton.class); bind(Runnable.class).annotatedWith(Names.named(QuitHandler.QUIT_HANDLER_KEY)) .to(QuitCallback.class); bind(QuitCallback.class).in(Singleton.class); bind(new TypeLiteral<Supplier<Boolean>>() { }) .annotatedWith(Names.named(HealthHandler.HEALTH_CHECKER_KEY)) .toInstance(Suppliers.ofInstance(true)); final Optional<String> hostnameOverride = Optional.fromNullable(HOSTNAME_OVERRIDE.get()); if (hostnameOverride.isPresent()) { try { InetAddress.getByName(hostnameOverride.get()); } catch (UnknownHostException e) { /* Possible misconfiguration, so warn the user. */ LOG.warn("Unable to resolve name specified in -hostname. " + "Depending on your environment, this may be valid."); } } install(new PrivateModule() { @Override protected void configure() { bind(new TypeLiteral<Optional<String>>() { }).toInstance(hostnameOverride); bind(HttpService.class).to(HttpServerLauncher.class); bind(HttpServerLauncher.class).in(Singleton.class); expose(HttpServerLauncher.class); expose(HttpService.class); } }); SchedulerServicesModule.addAppStartupServiceBinding(binder()).to(HttpServerLauncher.class); bind(LeaderRedirect.class).in(Singleton.class); SchedulerServicesModule.addAppStartupServiceBinding(binder()).to(RedirectMonitor.class); if (production) { install(PRODUCTION_SERVLET_CONTEXT_LISTENER); } } private static final Module PRODUCTION_SERVLET_CONTEXT_LISTENER = new AbstractModule() { @Override protected void configure() { // Provider binding only. } @Provides @Singleton ServletContextListener provideServletContextListener(Injector parentInjector) { return makeServletContextListener( parentInjector, Modules.combine( new ApiModule(), new H2ConsoleModule(), new HttpSecurityModule(), new ThriftModule())); } }; private static final Set<String> LEADER_ENDPOINTS = ImmutableSet.of( "agents", "api", "cron", "maintenance", "mname", "offers", "pendingtasks", "quotas", "slaves", "tiers", "utilization" ); private static final Multimap<Class<?>, String> JAX_RS_ENDPOINTS = ImmutableMultimap.<Class<?>, String>builder() .put(AbortHandler.class, "abortabortabort") .put(Agents.class, "agents") .put(ContentionPrinter.class, "contention") .put(Cron.class, "cron") .put(HealthHandler.class, "health") .put(LeaderHealth.class, "leaderhealth") .put(LogConfig.class, "logconfig") .put(Maintenance.class, "maintenance") .put(Mname.class, "mname") .put(Offers.class, "offers") .put(PendingTasks.class, "pendingtasks") .put(QuitHandler.class, "quitquitquit") .put(Quotas.class, "quotas") .put(Services.class, "services") .put(StructDump.class, "structdump") .put(ThreadStackPrinter.class, "threads") .put(Tiers.class, "tiers") .put(TimeSeriesDataSource.class, "graphdata") .put(Utilization.class, "utilization") .put(VarsHandler.class, "vars") .put(VarsJsonHandler.class, "vars.json") .build(); private static String allOf(Set<String> paths) { return "^(?:" + Joiner.on("|").join(Iterables.transform(paths, path -> "/" + path)) + ").*$"; } // TODO(ksweeney): Factor individual servlet configurations to their own ServletModules. @VisibleForTesting static ServletContextListener makeServletContextListener( final Injector parentInjector, final Module childModule) { return new GuiceServletContextListener() { @Override protected Injector getInjector() { return parentInjector.createChildInjector( childModule, new JerseyServletModule() { @Override protected void configureServlets() { bind(HttpStatsFilter.class).in(Singleton.class); filter("*").through(HttpStatsFilter.class); bind(LeaderRedirectFilter.class).in(Singleton.class); filterRegex(allOf(LEADER_ENDPOINTS)) .through(LeaderRedirectFilter.class); bind(GuiceContainer.class).in(Singleton.class); filterRegex(allOf(ImmutableSet.copyOf(JAX_RS_ENDPOINTS.values()))) .through(GuiceContainer.class, GUICE_CONTAINER_PARAMS); filterRegex("/assets/scheduler(?:/.*)?").through(LeaderRedirectFilter.class); serve("/assets", "/assets/*") .with(new DefaultServlet(), ImmutableMap.of( "resourceBase", STATIC_ASSETS_ROOT, "dirAllowed", "false")); for (Class<?> jaxRsHandler : JAX_RS_ENDPOINTS.keySet()) { bind(jaxRsHandler); } } }); } }; } static class RedirectMonitor extends AbstractIdleService { private final LeaderRedirect redirector; @Inject RedirectMonitor(LeaderRedirect redirector) { this.redirector = requireNonNull(redirector); } @Override public void startUp() throws MonitorException { redirector.monitor(); } @Override protected void shutDown() throws IOException { redirector.close(); } } public static final class HttpServerLauncher extends AbstractIdleService implements HttpService { private final ServletContextListener servletContextListener; private final Optional<String> advertisedHostOverride; private volatile Server server; private volatile HostAndPort serverAddress = null; @Inject HttpServerLauncher( ServletContextListener servletContextListener, Optional<String> advertisedHostOverride) { this.servletContextListener = requireNonNull(servletContextListener); this.advertisedHostOverride = requireNonNull(advertisedHostOverride); } private static final Map<String, String> REGEX_REWRITE_RULES = ImmutableMap.<String, String>builder() .put("/(?:index.html)?", "/assets/index.html") .put("/graphview(?:/index.html)?", "/assets/graphview/graphview.html") .put("/graphview/(.*)", "/assets/graphview/$1") .put("/(?:scheduler|updates)(?:/.*)?", "/assets/scheduler/index.html") .put("/slaves", "/agents") .build(); private static Handler getRewriteHandler(Handler wrapped) { RewriteHandler rewrites = new RewriteHandler(); rewrites.setOriginalPathAttribute(ORIGINAL_PATH_ATTRIBUTE_NAME); rewrites.setRewriteRequestURI(true); rewrites.setRewritePathInfo(true); for (Map.Entry<String, String> entry : REGEX_REWRITE_RULES.entrySet()) { RewriteRegexRule rule = new RewriteRegexRule(); rule.setRegex(entry.getKey()); rule.setReplacement(entry.getValue()); rewrites.addRule(rule); } rewrites.setHandler(wrapped); return rewrites; } private static Handler getGzipHandler(Handler wrapped) { GzipHandler gzip = new GzipHandler(); gzip.addIncludedMethods(HttpMethod.POST); gzip.setHandler(wrapped); return gzip; } @Override public HostAndPort getAddress() { Preconditions.checkState(state() == State.RUNNING); return HostAndPort.fromParts( advertisedHostOverride.or(serverAddress.getHost()), serverAddress.getPort()); } @Override protected void startUp() { server = new Server(); ServletContextHandler servletHandler = new ServletContextHandler(server, "/", ServletContextHandler.NO_SESSIONS); servletHandler.addServlet(DefaultServlet.class, "/"); servletHandler.addFilter(GuiceFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); servletHandler.addEventListener(servletContextListener); HandlerCollection rootHandler = new HandlerList(); RequestLogHandler logHandler = new RequestLogHandler(); logHandler.setRequestLog(new Slf4jRequestLog()); rootHandler.addHandler(logHandler); rootHandler.addHandler(servletHandler); ServerConnector connector = new ServerConnector(server); connector.setPort(HTTP_PORT.get()); if (LISTEN_IP.hasAppliedValue()) { connector.setHost(LISTEN_IP.get()); } server.addConnector(connector); server.setHandler(getGzipHandler(getRewriteHandler(rootHandler))); try { connector.open(); server.start(); } catch (Exception e) { throw new RuntimeException(e); } String host; if (connector.getHost() == null) { // Resolve the local host name. try { host = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { throw new RuntimeException("Failed to resolve local host address: " + e, e); } } else { // If jetty was configured with a specific host to bind to, use that. host = connector.getHost(); } serverAddress = HostAndPort.fromParts(host, connector.getLocalPort()); } @Override protected void shutDown() { LOG.info("Shutting down embedded http server"); try { server.stop(); } catch (Exception e) { LOG.info("Failed to stop jetty server: " + e, e); } } } }