org.apache.drill.yarn.appMaster.http.WebServer.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.drill.yarn.appMaster.http.WebServer.java

Source

/*
 * 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.Set;

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.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.ConstraintMapping;
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
     */
    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/*");

        // Static resources (CSS, images, etc.)

        setupStaticResources(servletContextHandler);

        // Security, if requested.

        if (AMSecurityManagerImpl.isEnabled()) {
            servletContextHandler.setSecurityHandler(createSecurityHandler(config));
            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 {
        public final String userName;

        public AMUserPrincipal(String userName) {
            this.userName = userName;
        }

        @Override
        public String getName() {
            return userName;
        }
    }

    public static class AmLoginService implements LoginService {
        private 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) {
            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) {
        }

        // @Override
        // protected UserIdentity loadUser(String username) {
        // // TODO Auto-generated method stub
        // return null;
        // }
        //
        // @Override
        // protected void loadUsers() throws IOException {
        // putUser( "fred", new Password( "wilma" ), new String[] { ADMIN_ROLE } );
        // }

    }

    /**
     * @return
     * @return
     * @see http://www.eclipse.org/jetty/documentation/current/embedded-examples.html
     */

    private ConstraintSecurityHandler createSecurityHandler(Config config) {
        ConstraintSecurityHandler security = new ConstraintSecurityHandler();

        Set<String> knownRoles = ImmutableSet.of(ADMIN_ROLE);
        security.setConstraintMappings(Collections.<ConstraintMapping>emptyList(), knownRoles);

        security.setAuthenticator(new FormAuthenticator("/login", "/login", true));
        security.setLoginService(new AmLoginService(AMSecurityManagerImpl.instance()));

        return security;
    }

    /**
     * @return A {@link SessionHandler} which contains a
     *         {@link HashSessionManager}
     */
    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.
     * @throws Exception
     */
    private ServerConnector createHttpConnector(Config config) throws Exception {
        LOG.info("Setting up HTTP connector for web server");
        final HttpConfiguration httpConfig = new HttpConfiguration();
        final ServerConnector httpConnector = new ServerConnector(jettyServer,
                new HttpConnectionFactory(httpConfig));
        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
     * {@link org.apache.drill.exec.server.rest.Webserver#createHttpsConnector( )}.
     * 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
     */

    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 = new HttpConfiguration();
        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;
    }

    @Override
    public void close() throws Exception {
        if (jettyServer != null) {
            jettyServer.stop();
        }
    }
}