All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.calcite.avatica.server.HttpServer Maven / Gradle / Ivy

The newest version!
/*
 * 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.calcite.avatica.server;

import org.apache.calcite.avatica.metrics.MetricsSystemConfiguration;
import org.apache.calcite.avatica.remote.AuthenticationType;
import org.apache.calcite.avatica.remote.Driver.Serialization;
import org.apache.calcite.avatica.remote.Service;
import org.apache.calcite.avatica.remote.Service.RpcMetadataResponse;

import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConfigurableSpnegoLoginService;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.authentication.AuthorizationService;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.security.authentication.ConfigurableSpnegoAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.session.DefaultSessionIdManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

/**
 * Avatica HTTP server.
 *
 * 

If you need to change the server's configuration, override the * {@link #configureConnector(ServerConnector, int)} method in a derived class. */ public class HttpServer { private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class); private static final int MAX_ALLOWED_HEADER_SIZE = 1024 * 64; private static final String DEFAULT_KEYSTORE_TYPE = "JKS"; private Server server; private int port = -1; private final AvaticaHandler handler; private final AvaticaServerConfiguration config; private final Subject subject; private final SslContextFactory.Server sslFactory; private final List> serverCustomizers; private final int maxAllowedHeaderSize; @Deprecated public HttpServer(Handler handler) { this(wrapJettyHandler(handler)); } /** * Constructs an {@link HttpServer} which binds to an ephemeral port. * @param handler The Handler to run */ public HttpServer(AvaticaHandler handler) { this(0, handler); } @Deprecated public HttpServer(int port, Handler handler) { this(port, wrapJettyHandler(handler)); } /** * Constructs an {@link HttpServer} with no additional configuration. * @param port The listen port * @param handler The Handler to run */ public HttpServer(int port, AvaticaHandler handler) { this(port, handler, null); } /** * Constructs an {@link HttpServer}. * @param port The listen port * @param handler The Handler to run * @param config Optional configuration for the server */ public HttpServer(int port, AvaticaHandler handler, AvaticaServerConfiguration config) { this(port, handler, config, null); } /** * Constructs an {@link HttpServer}. * @param port The listen port * @param handler The Handler to run * @param config Optional configuration for the server * @param subject The javax.security Subject for the server, or null */ public HttpServer(int port, AvaticaHandler handler, AvaticaServerConfiguration config, Subject subject) { this(port, handler, config, subject, null); } /** * Constructs an {@link HttpServer}. * @param port The listen port * @param handler The Handler to run * @param config Optional configuration for the server * @param subject The javax.security Subject for the server, or null * @param sslFactory A configured SslContextFactory.Server, or null */ public HttpServer(int port, AvaticaHandler handler, AvaticaServerConfiguration config, Subject subject, SslContextFactory.Server sslFactory) { this(port, handler, config, subject, sslFactory, Collections.>emptyList(), MAX_ALLOWED_HEADER_SIZE); } /** * Constructs an {@link HttpServer}. * @param port The listen port * @param handler The Handler to run * @param config Optional configuration for the server * @param subject The javax.security Subject for the server, or null * @param sslFactory A configured SslContextFactory.Server, or null * @param maxAllowedHeaderSize A maximum size in bytes that are allowed in an HTTP header */ public HttpServer(int port, AvaticaHandler handler, AvaticaServerConfiguration config, Subject subject, SslContextFactory.Server sslFactory, int maxAllowedHeaderSize) { this(port, handler, config, subject, sslFactory, Collections.>emptyList(), maxAllowedHeaderSize); } /** * Constructs an {@link HttpServer}. * @param port The listen port * @param handler The Handler to run * @param config Optional configuration for the server * @param subject The javax.security Subject for the server, or null * @param sslFactory A configured SslContextFactory.Server, or null * @param maxAllowedHeaderSize A maximum size in bytes that are allowed in an HTTP header */ private HttpServer(int port, AvaticaHandler handler, AvaticaServerConfiguration config, Subject subject, SslContextFactory.Server sslFactory, List> serverCustomizers, int maxAllowedHeaderSize) { this.port = port; this.handler = handler; this.config = config; this.subject = subject; this.sslFactory = sslFactory; this.serverCustomizers = serverCustomizers; this.maxAllowedHeaderSize = maxAllowedHeaderSize; } static AvaticaHandler wrapJettyHandler(Handler handler) { if (handler instanceof AvaticaHandler) { return (AvaticaHandler) handler; } // Backwards compatibility, noop's the AvaticaHandler interface return new DelegatingAvaticaHandler(handler); } public void start() { if (null != subject) { // Run the start in the privileged block (as the kerberos-identified user) Subject.doAs(subject, new PrivilegedAction() { @Override public Void run() { internalStart(); return null; } }); } else { internalStart(); } } protected void internalStart() { if (server != null) { throw new RuntimeException("Server is already started"); } final SubjectPreservingPrivilegedThreadFactory subjectPreservingPrivilegedThreadFactory = new SubjectPreservingPrivilegedThreadFactory(); //The constructor parameters are the Jetty defaults, except for the ThreadFactory final QueuedThreadPool threadPool = new QueuedThreadPool(200, 8, 60000, -1, null, null, subjectPreservingPrivilegedThreadFactory); server = new Server(threadPool); server.manage(threadPool); ServerConnector serverConnector = null; HandlerList handlerList = null; if (null != this.config && AuthenticationType.CUSTOM == config.getAuthenticationType()) { if (null != handler || null != sslFactory) { throw new IllegalStateException("Handlers and SSLFactory cannot be configured with " + "the HTTPServer Builder when using CUSTOM Authentication Type."); } } else { serverConnector = configureServerConnector(); handlerList = configureHandlers(); } // Apply server customizers for (ServerCustomizer customizer : this.serverCustomizers) { LOG.info("Customizing server with customizer: " + customizer.getClass()); customizer.customize(server); } try { server.start(); } catch (Exception e) { throw new RuntimeException(e); } if (null != serverConnector && null != handlerList) { port = serverConnector.getLocalPort(); LOG.info("Service listening on port {}.", getPort()); // Set the information about the address for this server try { this.handler.setServerRpcMetadata(createRpcServerMetadata(serverConnector)); } catch (UnknownHostException e) { // Failed to do the DNS lookup, bail out. throw new RuntimeException(e); } } else if (0 == server.getConnectors().length) { String error = "No server connectors have been configured for this Avatica server"; LOG.error(error); throw new RuntimeException(error); } } private ServerConnector configureServerConnector() { final ServerConnector connector = getServerConnector(); connector.setIdleTimeout(60 * 1000); connector.setPort(port); server.setConnectors(new Connector[] { connector }); return connector; } private HandlerList configureHandlers() { final HandlerList handlerList = new HandlerList(); Handler avaticaHandler = handler; // Wrap the provided handler for security if we made one if (null != config) { ConstraintSecurityHandler securityHandler = getSecurityHandler(); securityHandler.setHandler(handler); // SPNEGO requires a session SessionHandler sessionHandler = new SessionHandler(); sessionHandler.setHandler(securityHandler); avaticaHandler = sessionHandler; } handlerList.setHandlers(new Handler[] {avaticaHandler, new DefaultHandler()}); server.setHandler(handlerList); return handlerList; } private ConstraintSecurityHandler getSecurityHandler() { ConstraintSecurityHandler securityHandler = null; switch (config.getAuthenticationType()) { case SPNEGO: // Get the Handler for SPNEGO authentication securityHandler = configureSpnego(server, this.config); break; case BASIC: securityHandler = configureBasicAuthentication(server, config); break; case DIGEST: securityHandler = configureDigestAuthentication(server, config); break; default: // Pass break; } return securityHandler; } protected ServerConnector getServerConnector() { HttpConnectionFactory factory = new HttpConnectionFactory(); HttpConfiguration httpConfiguration = factory.getHttpConfiguration(); httpConfiguration.setSendServerVersion(false); httpConfiguration.setRequestHeaderSize(maxAllowedHeaderSize); if (null == sslFactory) { return new ServerConnector(server, factory); } return new ServerConnector(server, AbstractConnectionFactory.getFactories(sslFactory, factory)); } private RpcMetadataResponse createRpcServerMetadata(ServerConnector connector) throws UnknownHostException { String host = connector.getHost(); if (null == host) { // "null" means binding to all interfaces, we need to pick one so the client gets a real // address and not "0.0.0.0" or similar. host = InetAddress.getLocalHost().getHostName(); } final int port = connector.getLocalPort(); return new RpcMetadataResponse( String.format(Locale.ROOT, "%s:%d", host, port)); } /** * Configures the connector given the config for using SPNEGO. * * @param config The configuration */ protected ConstraintSecurityHandler configureSpnego(Server server, AvaticaServerConfiguration config) { final String realm = Objects.requireNonNull(config.getKerberosRealm()); // DefaultSessionIdManager uses SecureRandom, but we can be explicit about that. server.setSessionIdManager(new DefaultSessionIdManager(server, new SecureRandom())); // We rely on SPNEGO to authenticate the users with valid Kerberos identities. We // do not require a _specific_ Kerberos identity in order to authenticate with // Avatica. AvaticaUserStore will assign the role "avatica-user" to every SPNEGO-authenticated // user, and then ConfigurableSpnegoAuthenticator will check that role. // // This setup adds nothing but complexity to Avatica, but Jetty removed the // functionality to not have this layer of indirection. It paves the way for // flexibility in having "user" centric HTTP endpoints and "admin" centric // HTTP endpoints which Avatica can authorize appropriately. final AvaticaUserStore userStore = new AvaticaUserStore(); LOG.info("Instantiating HashLoginService with {}", realm); // Passing the Kerberos Realm here was previously important, but is not critical any longer. final HashLoginService authz = new HashLoginService(realm); authz.setUserStore(userStore); // A customization of SpnegoLoginService to explicitly set the server's principal, otherwise // we would have to require a custom file to set the server's principal. ConfigurableSpnegoLoginService spnegoLoginService = new ConfigurableSpnegoLoginService(realm, AuthorizationService.from(authz, "")); // Why? The Jetty unit test does it. spnegoLoginService.addBean(authz); spnegoLoginService.setServiceName(config.getKerberosServiceName()); spnegoLoginService.setHostName(config.getKerberosHostName()); spnegoLoginService.setKeyTabPath(config.getKerberosKeytab().toPath()); // The Authenticator independently validates what role(s) the authenticated // user has and authorizes them to access the HTTP resources. We use "avatica-user" // as the role to check. final String[] allowedRealms = new String[] {AvaticaUserStore.AVATICA_USER_ROLE}; final ConfigurableSpnegoAuthenticator spnegoAuthn = new ConfigurableSpnegoAuthenticator(); spnegoAuthn.setAuthenticationDuration(Duration.ofMinutes(5)); return configureCommonAuthentication(Constraint.__SPNEGO_AUTH, allowedRealms, spnegoAuthn, realm, spnegoLoginService); } protected ConstraintSecurityHandler configureBasicAuthentication(Server server, AvaticaServerConfiguration config) { final String[] allowedRoles = config.getAllowedRoles(); final String realm = config.getHashLoginServiceRealm(); final String loginServiceProperties = config.getHashLoginServiceProperties(); HashLoginService loginService = new HashLoginService(realm, loginServiceProperties); server.addBean(loginService); return configureCommonAuthentication(Constraint.__BASIC_AUTH, allowedRoles, new BasicAuthenticator(), null, loginService); } protected ConstraintSecurityHandler configureDigestAuthentication(Server server, AvaticaServerConfiguration config) { final String[] allowedRoles = config.getAllowedRoles(); final String realm = config.getHashLoginServiceRealm(); final String loginServiceProperties = config.getHashLoginServiceProperties(); HashLoginService loginService = new HashLoginService(realm, loginServiceProperties); server.addBean(loginService); return configureCommonAuthentication(Constraint.__DIGEST_AUTH, allowedRoles, new DigestAuthenticator(), null, loginService); } protected ConstraintSecurityHandler configureCommonAuthentication(String constraintName, String[] allowedRoles, Authenticator authenticator, String realm, LoginService loginService) { Constraint constraint = new Constraint(); constraint.setName(constraintName); constraint.setRoles(allowedRoles); // This is telling Jetty to not allow unauthenticated requests through (very important!) constraint.setAuthenticate(true); ConstraintMapping cm = new ConstraintMapping(); cm.setConstraint(constraint); cm.setPathSpec("/*"); ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); securityHandler.setAuthenticator(authenticator); securityHandler.setLoginService(loginService); securityHandler.setConstraintMappings(new ConstraintMapping[]{cm}); return securityHandler; } /** * Configures the server connector. * *

The default configuration sets a timeout of 1 minute and disables * TCP linger time. * *

To change the configuration, override this method in a derived class. * The overriding method must call its super method. * * @param connector connector to be configured * @param port port number handed over in constructor */ protected ServerConnector configureConnector(ServerConnector connector, int port) { connector.setIdleTimeout(60 * 1000); connector.setPort(port); return connector; } protected AvaticaServerConfiguration getConfig() { return this.config; } public void stop() { if (server == null) { throw new RuntimeException("Server is already stopped"); } LOG.info("Service terminating."); try { final Server server1 = server; port = -1; server = null; server1.stop(); } catch (Exception e) { throw new RuntimeException(e); } } public void join() throws InterruptedException { server.join(); } public int getPort() { return port; } /** * Builder class for creating instances of {@link HttpServer}. * @param element type */ public static class Builder { private int port; private Service service; private Serialization serialization; private AvaticaHandler handler = null; private MetricsSystemConfiguration metricsConfig; private AuthenticationType authenticationType = AuthenticationType.NONE; private String kerberosPrincipal; private String kerberosRealm; private File keytab; private DoAsRemoteUserCallback remoteUserCallback; private RemoteUserExtractor remoteUserExtractor = new HttpRequestRemoteUserExtractor(); private String loginServiceRealm; private String loginServiceProperties; private String[] loginServiceAllowedRoles; private boolean usingTLS = false; private File keystore; private String keystorePassword; private File truststore; private String truststorePassword; private String keystoreType; private List> serverCustomizers = Collections.emptyList(); // The maximum size in bytes of an http header the server will read (64KB) private int maxAllowedHeaderSize = MAX_ALLOWED_HEADER_SIZE; private AvaticaServerConfiguration serverConfig; private Subject subject; public Builder() {} /** * Creates a typed Builder for Server customization. * @param The type of HttpServer * @return A typed Builder */ public static Builder newBuilder() { return new Builder<>(); } public Builder withPort(int port) { this.port = port; return this; } /** * Sets the {@link Service} and {@link Serialization} information necessary to construct * the appropriate {@link AvaticaHandler}. * * @param service The Avatica service * @param serialization The serialization method * @return this */ public Builder withHandler(Service service, Serialization serialization) { this.service = Objects.requireNonNull(service); this.serialization = Objects.requireNonNull(serialization); return this; } /** * Sets an {@link AvaticaHandler} directly on the builder. Most users will not want to use * this method and should instead use {@link #withHandler(Service, Serialization)}. * * @param handler The handler * @return this */ public Builder withHandler(AvaticaHandler handler) { this.handler = Objects.requireNonNull(handler); return this; } /** * Sets the given configuration to enable metrics collection in the server. * * @param metricsConfig Configuration object for metrics. * @return this */ public Builder withMetricsConfiguration(MetricsSystemConfiguration metricsConfig) { this.metricsConfig = Objects.requireNonNull(metricsConfig); return this; } /** * Configures the server to use SPNEGO authentication. This method requires that the * principal contains the Kerberos realm. Invoking this method overrides any * previous call which configures authentication. * * @param principal A kerberos principal with the realm required. * @return this */ public Builder withSpnego(String principal) { return withSpnego(principal, (String[]) null); } /** * Configures the server to use SPNEGO authentication. This method requires that the * principal contains the Kerberos realm. Invoking this method overrides any * previous call which configures authentication. Invoking this method overrides any previous * call which configures authentication. By default, only principals from the server's realm are * permitted, but additional realms can be allowed using additionalAllowedRealms. * * @param principal A kerberos principal with the realm required. * @param additionalAllowedRealms Any additional realms, other than the server's realm, which * should be allowed to authenticate against the server. Can be null. * @return this * @deprecated Since 1.20.0, because {@code additionalAllowedRealms} is no longer considered. */ @Deprecated public Builder withSpnego(String principal, String[] additionalAllowedRealms) { int index = Objects.requireNonNull(principal).lastIndexOf('@'); if (-1 == index) { throw new IllegalArgumentException("Could not find '@' symbol in '" + principal + "' to parse the Kerberos realm from the principal"); } final String realm = principal.substring(index + 1); return withSpnego(principal, realm, additionalAllowedRealms); } /** * Configures the server to use SPNEGO authentication. It is required that callers are logged * in via Kerberos already or have provided the necessary configuration to automatically log * in via JAAS (using the java.security.auth.login.config system property) before * starting the {@link HttpServer}. Invoking this method overrides any previous call which * configures authentication. * * @param principal The kerberos principal * @param realm The kerberos realm * @return this */ public Builder withSpnego(String principal, String realm) { return this.withSpnego(principal, realm, null); } /** * Configures the server to use SPNEGO authentication. It is required that callers are logged * in via Kerberos already or have provided the necessary configuration to automatically log * in via JAAS (using the java.security.auth.login.config system property) before * starting the {@link HttpServer}. Invoking this method overrides any previous call which * configures authentication. By default, only principals from the server's realm are permitted, * but additional realms can be allowed using additionalAllowedRealms. * * @param principal The kerberos principal * @param realm The kerberos realm * @param additionalAllowedRealms Any additional realms, other than the server's realm, which * should be allowed to authenticate against the server. Can be null. * @return this * @deprecated since 1.20.0 because {@code additionalAllowedRealms} is no longer considered. */ @Deprecated public Builder withSpnego(String principal, String realm, String[] additionalAllowedRealms) { this.authenticationType = AuthenticationType.SPNEGO; this.kerberosPrincipal = Objects.requireNonNull(principal); this.kerberosRealm = Objects.requireNonNull(realm); if (additionalAllowedRealms != null) { LOG.warn("Avatica no longer support additionalAllowedRealms as the Jetty SPNEGO" + " implementation does not adhere to it. All authenticateable realms are allowed: {}", Arrays.toString(additionalAllowedRealms)); } this.loginServiceAllowedRoles = additionalAllowedRealms; return this; } /** * Sets a keytab to be used to perform a Kerberos login automatically (without the use of JAAS). * * @param keytab A KeyTab file for the server's login. * @return this */ public Builder withAutomaticLogin(File keytab) { this.keytab = Objects.requireNonNull(keytab); return this; } /** * Sets a callback implementation to defer the logic on how to run an action as a given user and * if the action should be permitted for that user. * * @param remoteUserCallback User-provided implementation of the callback * @return this */ public Builder withImpersonation(DoAsRemoteUserCallback remoteUserCallback) { this.remoteUserCallback = Objects.requireNonNull(remoteUserCallback); return this; } /** * Sets a callback implementation to defer the logic on how to use the right remoteUserExtractor * to extract remote user. * * @param remoteUserExtractor User-provided remoteUserExtractor * @return this */ public Builder withRemoteUserExtractor(RemoteUserExtractor remoteUserExtractor) { this.remoteUserExtractor = Objects.requireNonNull(remoteUserExtractor); return this; } /** * Configures the server to use HTTP Basic authentication. The properties must * be in a form consumable by Jetty. Invoking this method overrides any previous call which * configures authentication. This authentication is supplementary to the JDBC-provided user * authentication interfaces and should only be used when those interfaces are not used. * * @param properties Location of a properties file parseable by Jetty which contains users and * passwords. * @param allowedRoles An array of allowed roles in the properties file * @return this */ public Builder withBasicAuthentication(String properties, String[] allowedRoles) { return withAuthentication(AuthenticationType.BASIC, properties, allowedRoles); } /** * Configures the server to use HTTP Digest authentication. The properties must * be in a form consumable by Jetty. Invoking this method overrides any previous call which * configures authentication. This authentication is supplementary to the JDBC-provided user * authentication interfaces and should only be used when those interfaces are not used. * * @param properties Location of a properties file parseable by Jetty which contains users and * passwords. * @param allowedRoles An array of allowed roles in the properties file * @return this */ public Builder withDigestAuthentication(String properties, String[] allowedRoles) { return withAuthentication(AuthenticationType.DIGEST, properties, allowedRoles); } /** * Configures the server to use CUSTOM authentication mechanism, which can allow users to * combine benefits of multiple auth methods. See CustomAuthHttpServerTest for * examples on how to use it. * Note: Default ServerConnectors and Handlers will NOT be used. * Customize them directly using instances {@link ServerCustomizer} * @param config AvaticaServerConfiguration implementation that configures various details * about the authentication mechanism for {@link HttpServer} * @return this */ public Builder withCustomAuthentication(AvaticaServerConfiguration config) { this.authenticationType = AuthenticationType.CUSTOM; this.serverConfig = config; return this; } private Builder withAuthentication(AuthenticationType authType, String properties, String[] allowedRoles) { this.loginServiceRealm = "Avatica"; this.authenticationType = authType; this.loginServiceProperties = Objects.requireNonNull(properties); this.loginServiceAllowedRoles = Objects.requireNonNull(allowedRoles); return this; } /** * Configures the server to use TLS for wire encryption. * * @param keystore The server's keystore * @param keystorePassword The keystore's password * @param truststore The truststore containing the key used to generate the server's key * @param truststorePassword The truststore's password * @return this */ public Builder withTLS(File keystore, String keystorePassword, File truststore, String truststorePassword) { this.usingTLS = true; this.keystore = Objects.requireNonNull(keystore); this.keystorePassword = Objects.requireNonNull(keystorePassword); this.truststore = Objects.requireNonNull(truststore); this.truststorePassword = Objects.requireNonNull(truststorePassword); return this; } /** * Configures the server to use TLS for wire encryption. * * @param keystore The server's keystore * @param keystorePassword The keystore's password * @param truststore The truststore containing the key used to generate the server's key * @param truststorePassword The truststore's password * @param keystoreType The keystore's type * @return this */ public Builder withTLS(File keystore, String keystorePassword, File truststore, String truststorePassword, String keystoreType) { this.withTLS(keystore, keystorePassword, truststore, truststorePassword); this.keystoreType = Objects.requireNonNull(keystoreType); return this; } /** * Adds customizers to configure a Server before startup. * * @param serverCustomizers The customizers to use * @param clazz The type of server to customize * @return this */ public Builder withServerCustomizers(List> serverCustomizers, Class clazz) { Objects.requireNonNull(clazz); if (!clazz.isAssignableFrom(Server.class)) { throw new IllegalArgumentException("Only Jetty Server customizers are supported"); } this.serverCustomizers = Objects.requireNonNull(serverCustomizers); return this; } /** * Configures the maximum size, in bytes, of an HTTP header that the server will read. * * @param maxHeaderSize Maximums HTTP header size in bytes * @return this */ public Builder withMaxHeaderSize(int maxHeaderSize) { this.maxAllowedHeaderSize = maxHeaderSize; return this; } /** * Builds the HttpServer instance from this. * @return An HttpServer. */ @SuppressWarnings("unchecked") public HttpServer build() { switch (authenticationType) { case NONE: serverConfig = null; subject = null; handler = buildHandler(this, serverConfig); break; case BASIC: case DIGEST: // Build the configuration for BASIC or DIGEST authentication. serverConfig = buildUserAuthenticationConfiguration(this); subject = null; handler = buildHandler(this, serverConfig); break; case SPNEGO: LOG.debug("Not performing Kerberos login, Jetty does this now"); subject = null; serverConfig = buildSpnegoConfiguration(this); handler = buildHandler(this, serverConfig); break; case CUSTOM: // We don't need to build any Config here since // serverConfig is already assigned the required AvaticaServerConfiguration serverConfig = buildCustomConfiguration(this); subject = null; break; default: throw new IllegalArgumentException("Unhandled AuthenticationType"); } SslContextFactory.Server sslFactory = buildSSLContextFactory(); List> jettyCustomizers = new ArrayList<>(); for (ServerCustomizer customizer : this.serverCustomizers) { // Type checked in withServerCustomizers jettyCustomizers.add((ServerCustomizer) customizer); } return new HttpServer(port, handler, serverConfig, subject, sslFactory, jettyCustomizers, maxAllowedHeaderSize); } protected SslContextFactory.Server buildSSLContextFactory() { SslContextFactory.Server sslFactory = null; if (usingTLS) { sslFactory = new SslContextFactory.Server(); sslFactory.setKeyStorePath(this.keystore.getAbsolutePath()); sslFactory.setKeyStorePassword(keystorePassword); sslFactory.setTrustStorePath(truststore.getAbsolutePath()); sslFactory.setTrustStorePassword(truststorePassword); if (keystoreType != null && !keystoreType.equals(DEFAULT_KEYSTORE_TYPE)) { sslFactory.setKeyStoreType(keystoreType); } } return sslFactory; } private AvaticaServerConfiguration buildCustomConfiguration(Builder tBuilder) { return tBuilder.serverConfig; } /** * Creates the appropriate {@link AvaticaHandler}. * * @param b The {@link Builder}. * @param config The Avatica server configuration * @return An {@link AvaticaHandler}. */ private AvaticaHandler buildHandler(Builder b, AvaticaServerConfiguration config) { // The user provided a handler explicitly. if (null != b.handler) { return b.handler; } // Normal case, we create the handler for the user. HandlerFactory factory = new HandlerFactory(); return factory.getHandler(b.service, b.serialization, b.metricsConfig, config); } /** * Builds an {@link AvaticaServerConfiguration} implementation for SPNEGO-based authentication. * @param b The {@link Builder}. * @return A configuration instance. */ private AvaticaServerConfiguration buildSpnegoConfiguration(Builder b) { final String principal = b.kerberosPrincipal; final int separatorIndex = principal.indexOf('/'); if (separatorIndex < 1) { throw new RuntimeException("Expected principal to be of the form primary/instance" + " but got " + principal); } final String primary = principal.substring(0, separatorIndex); final int atSignIndex = principal.indexOf('@'); final String instance; // Trim off the @REALM if it's present if (atSignIndex == -1) { instance = principal.substring(separatorIndex + 1); } else { instance = principal.substring(separatorIndex + 1, atSignIndex); } final String realm = b.kerberosRealm; final File keytab = b.keytab; final String[] additionalAllowedRealms = b.loginServiceAllowedRoles; final DoAsRemoteUserCallback callback = b.remoteUserCallback; final RemoteUserExtractor remoteUserExtractor = b.remoteUserExtractor; return new AvaticaServerConfiguration() { @Override public AuthenticationType getAuthenticationType() { return AuthenticationType.SPNEGO; } @Override public String getKerberosRealm() { return realm; } @Override public String getKerberosPrincipal() { return principal; } @Override public String getKerberosServiceName() { return primary; } @Override public String getKerberosHostName() { return instance; } @Override public File getKerberosKeytab() { return keytab; } @Override public boolean supportsImpersonation() { return null != callback; } @Override public T doAsRemoteUser(String remoteUserName, String remoteAddress, Callable action) throws Exception { return callback.doAsRemoteUser(remoteUserName, remoteAddress, action); } @Override public RemoteUserExtractor getRemoteUserExtractor() { return remoteUserExtractor; } @Override public String[] getAllowedRoles() { return additionalAllowedRealms; } @Override public String getHashLoginServiceRealm() { return null; } @Override public String getHashLoginServiceProperties() { return null; } }; } private AvaticaServerConfiguration buildUserAuthenticationConfiguration(Builder b) { final AuthenticationType authType = b.authenticationType; final String[] allowedRoles = b.loginServiceAllowedRoles; final String realm = b.loginServiceRealm; final String properties = b.loginServiceProperties; final RemoteUserExtractor remoteUserExtractor = b.remoteUserExtractor; return new AvaticaServerConfiguration() { @Override public AuthenticationType getAuthenticationType() { return authType; } @Override public String[] getAllowedRoles() { return allowedRoles; } @Override public String getHashLoginServiceRealm() { return realm; } @Override public String getHashLoginServiceProperties() { return properties; } // Unused @Override public String getKerberosRealm() { return null; } @Override public String getKerberosPrincipal() { return null; } @Override public String getKerberosServiceName() { return null; } @Override public String getKerberosHostName() { return null; } @Override public boolean supportsImpersonation() { return false; } @Override public T doAsRemoteUser(String remoteUserName, String remoteAddress, Callable action) throws Exception { return null; } @Override public RemoteUserExtractor getRemoteUserExtractor() { return remoteUserExtractor; } }; } private Subject loginViaKerberos(Builder b) { Set principals = new HashSet(); principals.add(new KerberosPrincipal(b.kerberosPrincipal)); Subject subject = new Subject(false, principals, new HashSet(), new HashSet()); ServerKeytabJaasConf conf = new ServerKeytabJaasConf(b.kerberosPrincipal, b.keytab.toString()); String confName = "NotUsed"; try { LoginContext loginContext = new LoginContext(confName, subject, null, conf); loginContext.login(); return loginContext.getSubject(); } catch (LoginException e) { throw new RuntimeException(e); } } } } // End HttpServer.java