Java tutorial
/* * 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 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(); } } }