blob: dd10b700458b54735c416c8b270a5cf244bd9b7b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.drill.yarn.appMaster.http;
import static org.apache.drill.exec.server.rest.auth.DrillUserPrincipal.ADMIN_ROLE;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Principal;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Set;
import javax.servlet.DispatcherType;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.drill.exec.server.rest.CsrfTokenInjectFilter;
import org.apache.drill.exec.server.rest.CsrfTokenValidateFilter;
import org.apache.drill.yarn.appMaster.Dispatcher;
import org.apache.drill.yarn.core.DrillOnYarnConfig;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.DefaultUserIdentity;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.security.authentication.SessionAuthentication;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SessionManager;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.glassfish.jersey.servlet.ServletContainer;
import org.joda.time.DateTime;
import org.apache.drill.shaded.guava.com.google.common.collect.ImmutableSet;
import com.typesafe.config.Config;
/**
* Wrapper around the Jetty web server.
* <p>
* Adapted from Drill's drill.exec.WebServer class. Would be good to create a
* common base class later, but the goal for the initial project is to avoid
* Drill code changes.
*
* @see <a href=
* "http://www.eclipse.org/jetty/documentation/current/embedding-jetty.html">
* Jetty Embedding documentation</a>
*/
public class WebServer implements AutoCloseable {
private static final Log LOG = LogFactory.getLog(WebServer.class);
private final Server jettyServer;
private Dispatcher dispatcher;
public WebServer(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
Config config = DrillOnYarnConfig.config();
if (config.getBoolean(DrillOnYarnConfig.HTTP_ENABLED)) {
jettyServer = new Server();
} else {
jettyServer = null;
}
}
/**
* Start the web server including setup.
*
* @throws Exception in case of error during start
*/
public void start() throws Exception {
if (jettyServer == null) {
return;
}
build();
jettyServer.start();
}
private void build() throws Exception {
Config config = DrillOnYarnConfig.config();
buildConnector(config);
buildServlets(config);
}
private void buildConnector(Config config) throws Exception {
final ServerConnector serverConnector;
if (config.getBoolean(DrillOnYarnConfig.HTTP_ENABLE_SSL)) {
serverConnector = createHttpsConnector(config);
} else {
serverConnector = createHttpConnector(config);
}
jettyServer.addConnector(serverConnector);
}
/**
* Build the web app with embedded servlets.
* <p>
* <b>ServletContextHandler</b>: is a Jetty-provided handler that add the
* extra bits needed to set up the context that servlets expect. Think of it
* as an adapter between the (simple) Jetty handler and the (more complex)
* servlet API.
*
*/
private void buildServlets(Config config) {
final ServletContextHandler servletContextHandler = new ServletContextHandler(
null, "/");
servletContextHandler.setErrorHandler(createErrorHandler());
jettyServer.setHandler(servletContextHandler);
// Servlet holder for the pages of the Drill AM web app. The web app is a
// javax.ws application driven from annotations. The servlet holder "does
// the right thing" to drive the application, which is rooted at "/".
// The servlet container comes from Jersey, and manages the servlet
// lifecycle.
final ServletHolder servletHolder = new ServletHolder(
new ServletContainer(new WebUiPageTree(dispatcher)));
servletHolder.setInitOrder(1);
servletContextHandler.addServlet(servletHolder, "/*");
final ServletHolder restHolder = new ServletHolder(
new ServletContainer(new AmRestApi(dispatcher)));
restHolder.setInitOrder(2);
servletContextHandler.addServlet(restHolder, "/rest/*");
// Applying filters for CSRF protection.
servletContextHandler.addFilter(CsrfTokenInjectFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
for (String path : new String[]{"/resize", "/stop", "/cancel"}) {
servletContextHandler.addFilter(CsrfTokenValidateFilter.class, path, EnumSet.of(DispatcherType.REQUEST));
}
// Static resources (CSS, images, etc.)
setupStaticResources(servletContextHandler);
// Security, if requested.
if (AMSecurityManagerImpl.isEnabled()) {
servletContextHandler.setSecurityHandler(createSecurityHandler());
servletContextHandler.setSessionHandler(createSessionHandler(config, servletContextHandler.getSecurityHandler()));
}
}
private ErrorHandler createErrorHandler() {
// Error handler to show detailed errors.
// Should probably be turned off in production.
final ErrorHandler errorHandler = new ErrorHandler();
errorHandler.setShowStacks(true);
errorHandler.setShowMessageInTitle(true);
return errorHandler;
}
private void setupStaticResources(
ServletContextHandler servletContextHandler) {
// Access to static resources (JS pages, images, etc.)
// The static resources themselves come from Drill exec sub-project
// and the Drill-on-YARN project.
//
// We handle static content this way because we want to do it
// in the context of a servlet app, so we need the Jetty "default servlet"
// that handles static content. That servlet is designed to take its
// properties
// from the web.xml, file; but can also take them programmatically as done
// here. (The Jetty manual suggests a simpler handler, but that is a
// non-Servlet
// version.)
final ServletHolder staticHolder = new ServletHolder("static",
DefaultServlet.class);
staticHolder.setInitParameter("resourceBase",
Resource.newClassPathResource("/rest/static").toString());
staticHolder.setInitParameter("dirAllowed", "false");
staticHolder.setInitParameter("pathInfoOnly", "true");
servletContextHandler.addServlet(staticHolder, "/static/*");
final ServletHolder amStaticHolder = new ServletHolder("am-static",
DefaultServlet.class);
amStaticHolder.setInitParameter("resourceBase",
Resource.newClassPathResource("/drill-am/static").toString());
amStaticHolder.setInitParameter("dirAllowed", "false");
amStaticHolder.setInitParameter("pathInfoOnly", "true");
servletContextHandler.addServlet(amStaticHolder, "/drill-am/static/*");
}
public static class AMUserPrincipal implements Principal {
private final String userName;
public AMUserPrincipal(String userName) {
this.userName = userName;
}
@Override
public String getName() {
return userName;
}
}
public static class AmLoginService implements LoginService {
private final AMSecurityManager securityMgr;
protected IdentityService identityService = new DefaultIdentityService();
public AmLoginService(AMSecurityManager securityMgr) {
this.securityMgr = securityMgr;
}
@Override
public String getName() {
return "drill-am";
}
@Override
public UserIdentity login(String username, Object credentials, ServletRequest request) {
if (!securityMgr.login(username, (String) credentials)) {
return null;
}
return new DefaultUserIdentity(null, new AMUserPrincipal(username), new String[] { ADMIN_ROLE });
}
@Override
public boolean validate(UserIdentity user) {
return true;
}
@Override
public IdentityService getIdentityService() {
return identityService;
}
@Override
public void setIdentityService(IdentityService service) {
this.identityService = service;
}
@Override
public void logout(UserIdentity user) {
}
}
/**
* It creates handler with security constraint combinations for runtime efficiency
* @see <a href="http://www.eclipse.org/jetty/documentation/current/embedded-examples.html">Eclipse Jetty Documentation</a>
*
* @return security handler with precomputed constraint combinations
*/
private ConstraintSecurityHandler createSecurityHandler() {
ConstraintSecurityHandler security = new ConstraintSecurityHandler();
Set<String> knownRoles = ImmutableSet.of(ADMIN_ROLE);
security.setConstraintMappings(Collections.emptyList(), knownRoles);
security.setAuthenticator(new FormAuthenticator("/login", "/login", true));
security
.setLoginService(new AmLoginService(AMSecurityManagerImpl.instance()));
return security;
}
/**
* It creates A {@link SessionHandler} which contains a {@link HashSessionManager}
*
* @param config Drill configs
* @param securityHandler Set of initparameters that are used by the Authentication
* @return session handler
*/
private SessionHandler createSessionHandler(Config config,
final SecurityHandler securityHandler) {
SessionManager sessionManager = new HashSessionManager();
sessionManager.setMaxInactiveInterval(
config.getInt(DrillOnYarnConfig.HTTP_SESSION_MAX_IDLE_SECS));
sessionManager.addEventListener(new HttpSessionListener() {
@Override
public void sessionCreated(HttpSessionEvent se) {
// No-op
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
final HttpSession session = se.getSession();
if (session == null) {
return;
}
final Object authCreds = session
.getAttribute(SessionAuthentication.__J_AUTHENTICATED);
if (authCreds != null) {
final SessionAuthentication sessionAuth = (SessionAuthentication) authCreds;
securityHandler.logout(sessionAuth);
session.removeAttribute(SessionAuthentication.__J_AUTHENTICATED);
}
}
});
return new SessionHandler(sessionManager);
}
/**
* Create HTTP connector.
*
* @return Initialized {@link ServerConnector} instance for HTTP connections.
*/
private ServerConnector createHttpConnector(Config config) {
LOG.info("Setting up HTTP connector for web server");
final ServerConnector httpConnector = new ServerConnector(jettyServer,
new HttpConnectionFactory(baseHttpConfig()));
httpConnector.setPort(config.getInt(DrillOnYarnConfig.HTTP_PORT));
return httpConnector;
}
/**
* Create an HTTPS connector for given jetty server instance. If the admin has
* specified keystore/truststore settings they will be used else a self-signed
* certificate is generated and used.
* <p>
* This is a shameless copy of
* org.apache.drill.exec.server.rest.WebServer#createHttpsConnector(int, int, int).
* The two should be merged at some point. The primary issue is that the Drill
* version is tightly coupled to Drillbit configuration.
*
* @return Initialized {@link ServerConnector} for HTTPS connections.
* @throws Exception when unable to create HTTPS connector
*/
private ServerConnector createHttpsConnector(Config config) throws Exception {
LOG.info("Setting up HTTPS connector for web server");
final SslContextFactory sslContextFactory = new SslContextFactory();
// if (config.hasPath(ExecConstants.HTTP_KEYSTORE_PATH) &&
// !Strings.isNullOrEmpty(config.getString(ExecConstants.HTTP_KEYSTORE_PATH)))
// {
// LOG.info("Using configured SSL settings for web server");
// sslContextFactory.setKeyStorePath(config.getString(ExecConstants.HTTP_KEYSTORE_PATH));
// sslContextFactory.setKeyStorePassword(config.getString(ExecConstants.HTTP_KEYSTORE_PASSWORD));
//
// // TrustStore and TrustStore password are optional
// if (config.hasPath(ExecConstants.HTTP_TRUSTSTORE_PATH)) {
// sslContextFactory.setTrustStorePath(config.getString(ExecConstants.HTTP_TRUSTSTORE_PATH));
// if (config.hasPath(ExecConstants.HTTP_TRUSTSTORE_PASSWORD)) {
// sslContextFactory.setTrustStorePassword(config.getString(ExecConstants.HTTP_TRUSTSTORE_PASSWORD));
// }
// }
// } else {
LOG.info("Using generated self-signed SSL settings for web server");
final SecureRandom random = new SecureRandom();
// Generate a private-public key pair
final KeyPairGenerator keyPairGenerator = KeyPairGenerator
.getInstance("RSA");
keyPairGenerator.initialize(1024, random);
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
final DateTime now = DateTime.now();
// Create builder for certificate attributes
final X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE)
.addRDN(BCStyle.OU, "Apache Drill (auth-generated)")
.addRDN(BCStyle.O, "Apache Software Foundation (auto-generated)")
.addRDN(BCStyle.CN, "Drill AM");
final Date notBefore = now.minusMinutes(1).toDate();
final Date notAfter = now.plusYears(5).toDate();
final BigInteger serialNumber = new BigInteger(128, random);
// Create a certificate valid for 5years from now.
final X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(
nameBuilder.build(), // attributes
serialNumber, notBefore, notAfter, nameBuilder.build(),
keyPair.getPublic());
// Sign the certificate using the private key
final ContentSigner contentSigner = new JcaContentSignerBuilder(
"SHA256WithRSAEncryption").build(keyPair.getPrivate());
final X509Certificate certificate = new JcaX509CertificateConverter()
.getCertificate(certificateBuilder.build(contentSigner));
// Check the validity
certificate.checkValidity(now.toDate());
// Make sure the certificate is self-signed.
certificate.verify(certificate.getPublicKey());
// Generate a random password for keystore protection
final String keyStorePasswd = RandomStringUtils.random(20);
final KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(null, null);
keyStore.setKeyEntry("DrillAutoGeneratedCert", keyPair.getPrivate(),
keyStorePasswd.toCharArray(),
new java.security.cert.Certificate[] { certificate });
sslContextFactory.setKeyStore(keyStore);
sslContextFactory.setKeyStorePassword(keyStorePasswd);
// }
final HttpConfiguration httpsConfig = baseHttpConfig();
httpsConfig.addCustomizer(new SecureRequestCustomizer());
// SSL Connector
final ServerConnector sslConnector = new ServerConnector(jettyServer,
new SslConnectionFactory(sslContextFactory,
HttpVersion.HTTP_1_1.asString()),
new HttpConnectionFactory(httpsConfig));
sslConnector.setPort(config.getInt(DrillOnYarnConfig.HTTP_PORT));
return sslConnector;
}
private HttpConfiguration baseHttpConfig() {
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSendServerVersion(false);
return httpConfig;
}
@Override
public void close() throws Exception {
if (jettyServer != null) {
jettyServer.stop();
}
}
}