/**
* Copyright 2015 StreamSets Inc.
*
* Licensed under 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 com.streamsets.datacollector.http;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.streamsets.datacollector.main.BuildInfo;
import com.streamsets.datacollector.main.RuntimeInfo;
import com.streamsets.datacollector.task.AbstractTask;
import com.streamsets.datacollector.util.Configuration;
import com.streamsets.lib.security.http.DisconnectedSSOManager;
import com.streamsets.lib.security.http.DisconnectedSSOService;
import com.streamsets.lib.security.http.FailoverSSOService;
import com.streamsets.lib.security.http.ProxySSOService;
import com.streamsets.lib.security.http.RemoteSSOService;
import com.streamsets.lib.security.http.SSOAuthenticator;
import com.streamsets.lib.security.http.SSOConstants;
import com.streamsets.lib.security.http.SSOService;
import com.streamsets.lib.security.http.SSOUtils;
import com.streamsets.pipeline.api.impl.Utils;
import org.eclipse.jetty.jaas.JAASLoginService;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.rewrite.handler.RewriteRegexRule;
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.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.Handler;
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.SslConnectionFactory;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
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 javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Automatic security configuration based on URL paths:
*
* public /*
* public /public-rest/*
* protected /rest/*
* public /<APP>/*
* public /<APP>/public-rest/*
* protected /<APP>/rest/*
*
* public means authentication IS NOT required.
* protected means authentication IS required.
*
*/
public abstract class WebServerTask extends AbstractTask {
public static final String HTTP_BIND_HOST = "http.bindHost";
private static final String HTTP_BIND_HOST_DEFAULT = "0.0.0.0";
public static final String HTTP_MAX_THREADS = "http.maxThreads";
private static final int HTTP_MAX_THREADS_DEFAULT = 200;
public static final String HTTP_PORT_KEY = "http.port";
private static final int HTTP_PORT_DEFAULT = 0;
public static final String HTTPS_PORT_KEY = "https.port";
private static final int HTTPS_PORT_DEFAULT = -1;
public static final String HTTPS_KEYSTORE_PATH_KEY = "https.keystore.path";
private static final String HTTPS_KEYSTORE_PATH_DEFAULT = "keystore.jks";
public static final String HTTPS_KEYSTORE_PASSWORD_KEY = "https.keystore.password";
private static final String HTTPS_KEYSTORE_PASSWORD_DEFAULT = "${file(\"keystore-password.txt\")}";
static final String HTTPS_TRUSTSTORE_PATH_KEY = "https.truststore.path";
private static final String HTTPS_TRUSTSTORE_PATH_DEFAULT = null;
private static final String HTTPS_TRUSTSTORE_PASSWORD_KEY = "https.truststore.password";
private static final String HTTPS_TRUSTSTORE_PASSWORD_DEFAULT = null;
public static final String HTTP_SESSION_MAX_INACTIVE_INTERVAL_CONFIG = "http.session.max.inactive.interval";
public static final int HTTP_SESSION_MAX_INACTIVE_INTERVAL_DEFAULT = 86400; // in seconds = 24 hours
public static final String HTTP_ENABLE_FORWARDED_REQUESTS_KEY = "http.enable.forwarded.requests";
private static final boolean HTTP_ENABLE_FORWARDED_REQUESTS_DEFAULT = false;
public static final String AUTHENTICATION_KEY = "http.authentication";
public static final String AUTHENTICATION_DEFAULT = "none";
private static final String DIGEST_REALM_KEY = "http.digest.realm";
private static final String REALM_POSIX_DEFAULT = "-realm";
public static final String REALM_FILE_PERMISSION_CHECK = "http.realm.file.permission.check";
private static final boolean REALM_FILE_PERMISSION_CHECK_DEFAULT = true;
public static final String HTTP_AUTHENTICATION_LOGIN_MODULE = "http.authentication.login.module";
public static final String FILE = "file";
public static final String HTTP_AUTHENTICATION_LOGIN_MODULE_DEFAULT = "file";
public static final String HTTP_AUTHENTICATION_LDAP_ROLE_MAPPING = "http.authentication.ldap.role.mapping";
private static final String HTTP_AUTHENTICATION_LDAP_ROLE_MAPPING_DEFAULT = "";
private static final String JSESSIONID_COOKIE = "JSESSIONID_";
private static final Set<String> AUTHENTICATION_MODES = ImmutableSet.of("none", "digest", "basic", "form");
private static final Logger LOG = LoggerFactory.getLogger(WebServerTask.class);
public static final String LDAP_LOGIN_CONF = "ldap-login.conf";
public static final String JAVA_SECURITY_AUTH_LOGIN_CONFIG = "java.security.auth.login.config";
public static final String LDAP = "ldap";
public static final String LDAP_LOGIN_MODULE_NAME = "ldap.login.module.name";
public static final Set<String> LOGIN_MODULES = ImmutableSet.of(FILE, LDAP);
public static final String SSO_SERVICES_ATTR = "ssoServices";
private final String serverName;
private final BuildInfo buildInfo;
private final RuntimeInfo runtimeInfo;
private final Configuration conf;
private final Set<WebAppProvider> webAppProviders;
private final Set<ContextConfigurator> contextConfigurators;
private int port;
private Server server;
private HttpConfiguration httpConf = new HttpConfiguration();
private Server redirector;
private SessionHandler sessionHandler;
Map<String, Set<String>> roleMapping;
public WebServerTask(
BuildInfo buildInfo,
RuntimeInfo runtimeInfo,
Configuration conf,
Set<ContextConfigurator> contextConfigurators,
Set<WebAppProvider> webAppProviders
) {
this("webserver", buildInfo, runtimeInfo, conf, contextConfigurators, webAppProviders);
}
public WebServerTask(
String serverName,
BuildInfo buildInfo,
RuntimeInfo runtimeInfo,
Configuration conf,
Set<ContextConfigurator> contextConfigurators,
Set<WebAppProvider> webAppProviders
) {
super("webServer");
this.serverName = serverName;
this.buildInfo = buildInfo;
this.runtimeInfo = runtimeInfo;
this.conf = conf;
this.webAppProviders = webAppProviders;
this.contextConfigurators = contextConfigurators;
}
protected RuntimeInfo getRuntimeInfo() {
return runtimeInfo;
}
protected Configuration getConfiguration() {
return conf;
}
@Override
public void initTask() {
checkValidPorts();
synchronized (getRuntimeInfo()) {
if (!getRuntimeInfo().hasAttribute(SSO_SERVICES_ATTR)) {
getRuntimeInfo().setAttribute(SSO_SERVICES_ATTR, Collections.synchronizedList(new ArrayList<Object>()));
}
}
server = createServer();
// initialize a global session manager
sessionHandler = new SessionHandler();
sessionHandler.setMaxInactiveInterval(conf.get(HTTP_SESSION_MAX_INACTIVE_INTERVAL_CONFIG,
HTTP_SESSION_MAX_INACTIVE_INTERVAL_DEFAULT));
ContextHandlerCollection appHandlers = new ContextHandlerCollection();
// load web apps
Set<String> contextPaths = new LinkedHashSet<>();
for (WebAppProvider appProvider : webAppProviders) {
Configuration appConf = appProvider.getAppConfiguration();
ServletContextHandler appHandler = appProvider.get();
String contextPath = appHandler.getContextPath();
if (contextPath.equals("/")) {
throw new RuntimeException("Webapps cannot be registered at the root context");
}
if (contextPaths.contains(contextPath)) {
throw new RuntimeException(Utils.format("Webapp already registered at '{}' context", contextPath));
}
// all webapps must have a session manager
appHandler.setSessionHandler(new SessionHandler());
appHandler.setSecurityHandler(createSecurityHandler(server, appConf, appHandler, contextPath));
contextPaths.add(contextPath);
appHandlers.addHandler(appHandler);
}
ServletContextHandler appHandler = configureRootContext(sessionHandler);
appHandler.setSecurityHandler(createSecurityHandler(server, conf, appHandler, "/"));
Handler handler = configureRedirectionRules(appHandler);
appHandlers.addHandler(handler);
server.setHandler(appHandlers);
if (isRedirectorToSSLEnabled()) {
redirector = createRedirectorServer();
}
addToPostStart(new Runnable() {
@Override
public void run() {
for (WebAppProvider appProvider : webAppProviders) {
appProvider.postStart();
}
}
});
}
@VisibleForTesting
Server getServer() {
return server;
}
private ServletContextHandler configureRootContext(SessionHandler sessionHandler) {
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setSessionHandler(sessionHandler);
context.setContextPath("/");
for (ContextConfigurator cc : contextConfigurators) {
cc.init(context);
}
return context;
}
private Handler configureRedirectionRules(Handler appHandler) {
RewriteHandler handler = new RewriteHandler();
handler.setRewriteRequestURI(false);
handler.setRewritePathInfo(false);
handler.setOriginalPathAttribute("requestedPath");
RewriteRegexRule uiRewriteRule = new RewriteRegexRule();
uiRewriteRule.setRegex("^/collector/.*");
uiRewriteRule.setReplacement("/");
handler.addRule(uiRewriteRule);
handler.setHandler(appHandler);
HandlerCollection handlerCollection = new HandlerCollection();
handlerCollection.setHandlers(new Handler[] {handler, appHandler});
return handlerCollection;
}
private List<ConstraintMapping> createConstraintMappings() {
// everything under /* public
Constraint noAuthConstraint = new Constraint();
noAuthConstraint.setName("auth");
noAuthConstraint.setAuthenticate(false);
noAuthConstraint.setRoles(new String[]{"user"});
ConstraintMapping noAuthMapping = new ConstraintMapping();
noAuthMapping.setPathSpec("/*");
noAuthMapping.setConstraint(noAuthConstraint);
// everything under /public-rest/* public
Constraint publicRestConstraint = new Constraint();
publicRestConstraint.setName("auth");
publicRestConstraint.setAuthenticate(false);
publicRestConstraint.setRoles(new String[] { "user"});
ConstraintMapping publicRestMapping = new ConstraintMapping();
publicRestMapping.setPathSpec("/public-rest/*");
publicRestMapping.setConstraint(publicRestConstraint);
// everything under /rest/* restricted
Constraint restConstraint = new Constraint();
restConstraint.setName("auth");
restConstraint.setAuthenticate(true);
restConstraint.setRoles(new String[] { "user"});
ConstraintMapping restMapping = new ConstraintMapping();
restMapping.setPathSpec("/rest/*");
restMapping.setConstraint(restConstraint);
// /logout is restricted
Constraint logoutConstraint = new Constraint();
logoutConstraint.setName("auth");
logoutConstraint.setAuthenticate(true);
logoutConstraint.setRoles(new String[] { "user"});
ConstraintMapping logoutMapping = new ConstraintMapping();
logoutMapping.setPathSpec("/logout");
logoutMapping.setConstraint(logoutConstraint);
// index page is restricted to trigger login correctly when using form authentication
Constraint indexConstraint = new Constraint();
indexConstraint.setName("auth");
indexConstraint.setAuthenticate(true);
indexConstraint.setRoles(new String[] { "user"});
ConstraintMapping indexMapping = new ConstraintMapping();
indexMapping.setPathSpec("");
indexMapping.setConstraint(indexConstraint);
// docs is restricted
ConstraintMapping docMapping = new ConstraintMapping();
docMapping.setPathSpec("/docs/*");
docMapping.setConstraint(indexConstraint);
// Disable TRACE method
Constraint disableTraceConstraint = new Constraint();
disableTraceConstraint.setName("Disable TRACE");
disableTraceConstraint.setAuthenticate(true);
ConstraintMapping disableTraceMapping = new ConstraintMapping();
disableTraceMapping.setPathSpec("/*");
disableTraceMapping.setMethod("TRACE");
disableTraceMapping.setConstraint(disableTraceConstraint);
return ImmutableList.of(
disableTraceMapping,
restMapping,
indexMapping,
docMapping,
logoutMapping,
noAuthMapping,
publicRestMapping
);
}
protected SecurityHandler createSecurityHandler(
Server server, Configuration appConf, ServletContextHandler appHandler, String appContext
) {
ConstraintSecurityHandler securityHandler;
String auth = conf.get(AUTHENTICATION_KEY, AUTHENTICATION_DEFAULT);
boolean isDPMEnabled = runtimeInfo.isDPMEnabled();
if (isDPMEnabled) {
securityHandler = configureSSO(appConf, appHandler, appContext);
} else {
switch (auth) {
case "none":
securityHandler = null;
break;
case "digest":
case "basic":
securityHandler = configureDigestBasic(appConf, server, auth);
break;
case "form":
securityHandler = configureForm(appConf, server, auth);
break;
default:
throw new RuntimeException(Utils.format("Invalid authentication mode '{}', must be one of '{}'",
auth, AUTHENTICATION_MODES));
}
}
if (securityHandler != null) {
List<ConstraintMapping> constraintMappings = new ArrayList<>();
constraintMappings.addAll(createConstraintMappings());
securityHandler.setConstraintMappings(constraintMappings);
}
return securityHandler;
}
public static final Set<PosixFilePermission> OWNER_PERMISSIONS = ImmutableSet.of(PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE);
private void validateRealmFile(File realmFile) {
boolean checkRealmFilePermission = conf.get(REALM_FILE_PERMISSION_CHECK, REALM_FILE_PERMISSION_CHECK_DEFAULT);
if(!checkRealmFilePermission) {
return;
}
if (!realmFile.exists()) {
throw new RuntimeException(Utils.format("Realm file '{}' does not exists", realmFile));
}
if (!realmFile.isFile()) {
throw new RuntimeException(Utils.format("Realm file '{}' is not a file", realmFile));
}
try {
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(realmFile.toPath());
permissions.removeAll(OWNER_PERMISSIONS);
if (!permissions.isEmpty()) {
throw new RuntimeException(Utils.format("The permissions of the realm file '{}' should be owner only",
realmFile));
}
} catch (IOException ex) {
throw new RuntimeException(Utils.format("Could not get the permissions of the realm file '{}', {}", realmFile,
ex.toString()), ex);
}
}
RemoteSSOService createRemoteSSOService(Configuration appConf) {
RemoteSSOService remoteSsoService = new RemoteSSOService();
remoteSsoService.setConfiguration(appConf);
return remoteSsoService;
}
protected boolean isDisconnectedSSOModeEnabled() {
return false;
}
@SuppressWarnings("unchecked")
private ConstraintSecurityHandler configureSSO(
final Configuration appConf, ServletContextHandler appHandler, final String appContext
) {
final String componentId = getComponentId(appConf);
final String appToken = getAppAuthToken(appConf);
Utils.checkArgument(appToken != null && !appToken.trim().isEmpty(),
Utils.format("{} cannot be NULL or empty", RemoteSSOService.SECURITY_SERVICE_APP_AUTH_TOKEN_CONFIG));
LOG.debug("Initializing DPM componentId '{}'", componentId);
ConstraintSecurityHandler security = new ConstraintSecurityHandler();
final SSOService ssoService;
RemoteSSOService remoteSsoService = createRemoteSSOService(appConf);
remoteSsoService.setComponentId(componentId);
remoteSsoService.setApplicationAuthToken(appToken);
LOG.info("DPM component ID '{}' application authentication token '{}'", componentId, SSOUtils.tokenForLog
(appToken));
if (isDisconnectedSSOModeEnabled()) {
LOG.info("Support for DPM disconnected mode is enabled");
DisconnectedSSOManager disconnectedSSOManager =
new DisconnectedSSOManager(getRuntimeInfo().getDataDir(), appConf);
disconnectedSSOManager.setEnabled(true);
disconnectedSSOManager.registerResources(appHandler);
DisconnectedSSOService disconnectedSSOService = disconnectedSSOManager.getSsoService();
ssoService = new FailoverSSOService(remoteSsoService, disconnectedSSOService);
} else {
LOG.debug("Support for DPM disconnected mode is disabled");
ssoService = remoteSsoService;
}
addToPostStart(new Runnable() {
@Override
public void run() {
LOG.debug("Validating application token for DPM component ID '{}'", componentId);
ssoService.register(getRegistrationAttributes());
runtimeInfo.setRemoteRegistrationStatus(true);
}
});
SSOService proxySsoService = new ProxySSOService(ssoService);
// registering ssoService with runtime, to enable cache flushing
((List)getRuntimeInfo().getAttribute(SSO_SERVICES_ATTR)).add(proxySsoService);
appHandler.getServletContext().setAttribute(SSOService.SSO_SERVICE_KEY, proxySsoService);
security.setAuthenticator(new SSOAuthenticator(appContext, proxySsoService, appConf));
return security;
}
private ConstraintSecurityHandler configureDigestBasic(Configuration conf, Server server, String mode) {
LoginService loginService = getLoginService(conf, mode);
server.addBean(loginService);
ConstraintSecurityHandler security = new ConstraintSecurityHandler();
switch (mode) {
case "digest":
security.setAuthenticator(new ProxyAuthenticator(new DigestAuthenticator(), runtimeInfo, conf));
break;
case "basic":
security.setAuthenticator(new ProxyAuthenticator(new BasicAuthenticator(), runtimeInfo, conf));
break;
default:
// no action
break;
}
security.setLoginService(loginService);
return security;
}
private ConstraintSecurityHandler configureForm(Configuration conf, Server server, String mode) {
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
LoginService loginService = getLoginService(conf, mode);
server.addBean(loginService);
securityHandler.setLoginService(loginService);
FormAuthenticator authenticator = new FormAuthenticator("/login.html", "/login.html?error=true", true);
securityHandler.setAuthenticator(new ProxyAuthenticator(authenticator, runtimeInfo, conf));
return securityHandler;
}
private boolean isSSLEnabled() {
return conf.get(HTTPS_PORT_KEY, HTTPS_PORT_DEFAULT) != -1;
}
// Currently if http or https is random (set to 0), the other should be unused (set to -1)
// We have this restriction as currently we are not exposing API's for publishing the redirector
// port.
private void checkValidPorts() {
if ((conf.get(HTTP_PORT_KEY, HTTP_PORT_DEFAULT) == 0 && conf.get(HTTPS_PORT_KEY,
HTTPS_PORT_DEFAULT) != -1)
|| (conf.get(HTTPS_PORT_KEY, HTTPS_PORT_DEFAULT) == 0 && conf.get(HTTP_PORT_KEY,
HTTP_PORT_DEFAULT) != -1)) {
throw new IllegalArgumentException(
"Invalid port combination for http and https, If http port is set to 0 (random), then https should be "
+ "set to -1 or vice versa");
}
}
private boolean isRedirectorToSSLEnabled() {
return conf.get(HTTPS_PORT_KEY, HTTPS_PORT_DEFAULT) != -1 && conf.get(HTTP_PORT_KEY, HTTP_PORT_DEFAULT) != -1;
}
private Server createServer() {
port = isSSLEnabled() ?
conf.get(HTTPS_PORT_KEY, HTTPS_PORT_DEFAULT) :
conf.get(HTTP_PORT_KEY, HTTP_PORT_DEFAULT);
String hostname = conf.get(HTTP_BIND_HOST, HTTP_BIND_HOST_DEFAULT);
QueuedThreadPool qtp = new QueuedThreadPool(conf.get(HTTP_MAX_THREADS, HTTP_MAX_THREADS_DEFAULT));
qtp.setName(serverName);
qtp.setDaemon(true);
Server server = new Server(qtp);
httpConf = configureForwardRequestCustomizer(httpConf);
if (!isSSLEnabled()) {
InetSocketAddress addr = new InetSocketAddress(hostname, port);
ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConf));
connector.setHost(addr.getHostName());
connector.setPort(addr.getPort());
server.setConnectors(new Connector[]{connector});
} else {
//Create a connector for HTTPS
httpConf.addCustomizer(new SecureRequestCustomizer());
SslContextFactory sslContextFactory = createSslContextFactory();
ServerConnector httpsConnector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory, "http/1.1"),
new HttpConnectionFactory(httpConf));
httpsConnector.setPort(port);
httpsConnector.setHost(hostname);
server.setConnectors(new Connector[]{httpsConnector});
}
return server;
}
protected SslContextFactory createSslContextFactory() {
SslContextFactory sslContextFactory = new SslContextFactory();
File keyStore = getHttpsKeystore(conf, runtimeInfo.getConfigDir());
if (!keyStore.exists()) {
throw new RuntimeException(Utils.format("KeyStore file '{}' does not exist", keyStore.getPath()));
}
String password = conf.get(HTTPS_KEYSTORE_PASSWORD_KEY, HTTPS_KEYSTORE_PASSWORD_DEFAULT).trim();
sslContextFactory.setKeyStorePath(keyStore.getPath());
sslContextFactory.setKeyStorePassword(password);
sslContextFactory.setKeyManagerPassword(password);
File trustStoreFile = getHttpsTruststore(conf, runtimeInfo.getConfigDir());
if (trustStoreFile != null) {
if (trustStoreFile.exists()) {
sslContextFactory.setTrustStorePath(trustStoreFile.getPath());
String trustStorePassword = Utils.checkNotNull(conf.get(HTTPS_TRUSTSTORE_PASSWORD_KEY,
HTTPS_TRUSTSTORE_PASSWORD_DEFAULT
), HTTPS_TRUSTSTORE_PASSWORD_KEY);
sslContextFactory.setTrustStorePassword(trustStorePassword.trim());
} else {
throw new IllegalStateException(Utils.format(
"Truststore file: '{}' " + "doesn't exist",
trustStoreFile.getAbsolutePath()
));
}
}
return sslContextFactory;
}
private void setSSLContext() {
for (Connector connector : server.getConnectors()) {
for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
if (connectionFactory instanceof SslConnectionFactory) {
runtimeInfo.setSSLContext(((SslConnectionFactory) connectionFactory).getSslContextFactory().getSslContext());
}
}
}
if (runtimeInfo.getSSLContext() == null) {
throw new IllegalStateException("Unexpected error, SSLContext is not set for https enabled server");
}
}
private File getHttpsTruststore(Configuration conf, String configDir) {
final String httpsTruststorePath = conf.get(HTTPS_TRUSTSTORE_PATH_KEY, HTTPS_TRUSTSTORE_PATH_DEFAULT);
if (httpsTruststorePath == null || httpsTruststorePath.trim().isEmpty()) {
LOG.info(Utils.format(
"TrustStore config '{}' is not set, will pickup" + " truststore from $JAVA_HOME/jre/lib/security/cacerts",
HTTPS_TRUSTSTORE_PATH_KEY
));
return null;
} else if (Paths.get(httpsTruststorePath).isAbsolute()) {
return new File(httpsTruststorePath).getAbsoluteFile();
} else {
return new File(configDir, httpsTruststorePath).getAbsoluteFile();
}
}
@VisibleForTesting
static File getHttpsKeystore(Configuration conf, String configDir) {
final String httpsKeystorePath = conf.get(HTTPS_KEYSTORE_PATH_KEY, HTTPS_KEYSTORE_PATH_DEFAULT);
if (Paths.get(httpsKeystorePath).isAbsolute()) {
return new File(httpsKeystorePath).getAbsoluteFile();
} else {
return new File(configDir, httpsKeystorePath).getAbsoluteFile();
}
}
private Server createRedirectorServer() {
int unsecurePort = conf.get(HTTP_PORT_KEY, HTTP_PORT_DEFAULT);
String hostname = conf.get(HTTP_BIND_HOST, HTTP_BIND_HOST_DEFAULT);
QueuedThreadPool qtp = new QueuedThreadPool(25);
qtp.setName(serverName + "Redirector");
qtp.setDaemon(true);
Server server = new Server(qtp);
InetSocketAddress addr = new InetSocketAddress(hostname, unsecurePort);
ServerConnector connector = new ServerConnector(server);
connector.setHost(addr.getHostName());
connector.setPort(addr.getPort());
server.setConnectors(new Connector[]{connector});
ServletContextHandler context = new ServletContextHandler();
context.addServlet(new ServletHolder(new RedirectorServlet()), "/*");
context.setContextPath("/");
server.setHandler(context);
return server;
}
private class RedirectorServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
StringBuffer sb = req.getRequestURL();
String qs = req.getQueryString();
if (qs != null) {
sb.append("?").append(qs);
}
URL httpUrl = new URL(sb.toString());
URL httpsUrl = new URL("https", httpUrl.getHost(), port, httpUrl.getFile());
resp.sendRedirect(httpsUrl.toString());
}
}
private final List<Runnable> postStartRunnables = new ArrayList<>();
void addToPostStart(Runnable runnable) {
postStartRunnables.add(runnable);
}
void postStart() {
for (Runnable runnable : postStartRunnables) {
runnable.run();
}
}
protected String getHttpUrl() {
return runtimeInfo.getBaseHttpUrl();
}
@Override
protected void runTask() {
for (ContextConfigurator cc : contextConfigurators) {
cc.start();
}
try {
server.start();
port = server.getURI().getPort();
sessionHandler.setSessionCookie(JSESSIONID_COOKIE + port);
if(runtimeInfo.getBaseHttpUrl().equals(RuntimeInfo.UNDEF)) {
try {
String baseHttpUrl = "http://";
if (isSSLEnabled()) {
baseHttpUrl = "https://";
}
String hostname = conf.get(HTTP_BIND_HOST, HTTP_BIND_HOST_DEFAULT);
baseHttpUrl += !"0.0.0.0".equals(hostname) ? hostname : InetAddress.getLocalHost().getCanonicalHostName();
baseHttpUrl += ":" + port;
runtimeInfo.setBaseHttpUrl(baseHttpUrl);
} catch(UnknownHostException ex) {
LOG.debug("Exception during hostname resolution: {}", ex);
runtimeInfo.setBaseHttpUrl(server.getURI().toString());
}
}
System.out.println(Utils.format("Running on URI : '{}'", getHttpUrl()));
LOG.info("Running on URI : '{}'", getHttpUrl());
for (Connector connector : server.getConnectors()) {
if (connector instanceof ServerConnector) {
port = ((ServerConnector)connector).getLocalPort();
}
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
if (redirector != null) {
try {
redirector.start();
LOG.debug("Running HTTP redirector to HTTPS on port '{}'", conf.get(HTTP_PORT_KEY, HTTP_PORT_DEFAULT));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
if (isSSLEnabled()) {
setSSLContext();
}
postStart();
}
public URI getServerURI() throws ServerNotYetRunningException {
if (!server.isStarted()) {
throw new ServerNotYetRunningException("Server has not yet started");
} else {
return server.getURI();
}
}
@Override
protected void stopTask() {
try {
server.stop();
} catch (Exception ex) {
LOG.error("Error while stopping Jetty, {}", ex.toString(), ex);
} finally {
for (ContextConfigurator cc : contextConfigurators) {
try {
cc.stop();
} catch (Exception ex) {
LOG.error("Error while stopping '{}', {}", cc.getClass().getSimpleName(), ex.toString(), ex);
}
}
}
if (redirector != null) {
try {
redirector.stop();
} catch (Exception ex) {
LOG.error("Error while stopping redirector Jetty, {}", ex.toString(), ex);
}
}
}
protected LoginService getLoginService(Configuration conf, String mode) {
LoginService loginService = null;
String loginModule = this.conf.get(HTTP_AUTHENTICATION_LOGIN_MODULE, HTTP_AUTHENTICATION_LOGIN_MODULE_DEFAULT);
switch (loginModule) {
case FILE:
String realm = conf.get(DIGEST_REALM_KEY, mode + REALM_POSIX_DEFAULT);
File realmFile = new File(runtimeInfo.getConfigDir(), realm + ".properties").getAbsoluteFile();
validateRealmFile(realmFile);
loginService = new SdcHashLoginService(realm, realmFile.getAbsolutePath());
break;
case LDAP:
// If “java.security.auth.login.config” system property is set then use that config file.
// This will allow users to include sdc ldap entry in their existing ldap login config file.
// If not, pick up login config file from location ${SDC_DIST}/etc/ldap-login.conf
String ldapConfigFileName = System.getProperty(JAVA_SECURITY_AUTH_LOGIN_CONFIG, null);
if (null == ldapConfigFileName) {
File ldapConfigFile = new File(runtimeInfo.getConfigDir(), LDAP_LOGIN_CONF).getAbsoluteFile();
System.setProperty(JAVA_SECURITY_AUTH_LOGIN_CONFIG, ldapConfigFile.getAbsolutePath());
}
roleMapping = parseRoleMapping(conf.get(HTTP_AUTHENTICATION_LDAP_ROLE_MAPPING,
HTTP_AUTHENTICATION_LDAP_ROLE_MAPPING_DEFAULT));
// Look up the login module name from sdc.properties file. Assume "ldap" if none specified.
// This helps to be backward compatible and allows for overriding the login module name if the jaas config
// file contains multiple entries
String loginModuleName = conf.get(LDAP_LOGIN_MODULE_NAME, LDAP);
if (loginModuleName.trim().isEmpty()) {
loginModuleName = LDAP;
}
// resetting it becuase it is cached and testcases fail then
javax.security.auth.login.Configuration.getConfiguration().setConfiguration(null);
// verifying that is authentication mode is DIGEST and we are using LDAP, we don't allow forceBindingLogin
// set to TRUE as it won't work
if ("digest".equals(mode)) {
AppConfigurationEntry configEntries[] =
javax.security.auth.login.Configuration.getConfiguration().getAppConfigurationEntry(loginModuleName);
if (configEntries.length == 1) {
String forceBindingLogin = (String) configEntries[0].getOptions().get("forceBindingLogin");
if (forceBindingLogin != null && Boolean.parseBoolean(forceBindingLogin.trim())) {
throw new RuntimeException(
"Digest authentication cannot be used with LDAP 'forceBindingLoging' set to true");
}
}
}
loginService = new JAASLoginService(loginModuleName);
loginService.setIdentityService(new DefaultIdentityService() {
@Override
public UserIdentity newUserIdentity(Subject subject, Principal userPrincipal, String[] roles) {
Set<String> rolesSet = new HashSet<>();
rolesSet.add("user");
for(String role: roles) {
Set<String> dcRoles = tryMappingRole(role);
if(dcRoles != null && dcRoles.size() > 0) {
rolesSet.addAll(dcRoles);
} else {
rolesSet.add(role);
}
}
return new DefaultUserIdentity(subject, userPrincipal, rolesSet.toArray(new String[rolesSet.size()]));
}
});
break;
default:
throw new RuntimeException(Utils.format("Invalid Authentication Login Module '{}', must be one of '{}'",
loginModule, LOGIN_MODULES));
}
return loginService;
}
private Map<String, Set<String>> parseRoleMapping(String option) {
if(option == null || option.trim().length() == 0) {
throw new RuntimeException(Utils.format("LDAP group to Data Collector role mapping configuration - '{}' is empty",
HTTP_AUTHENTICATION_LDAP_ROLE_MAPPING));
}
Map<String, Set<String>> roleMapping = new HashMap<>();
try {
String[] mappings = option.split(";");
for (String mapping : mappings) {
String[] map = mapping.split(":", 2);
String ldapRole = map[0].trim();
String[] streamSetsRoles = map[1].split(",");
if (roleMapping.get(ldapRole) == null) {
roleMapping.put(ldapRole, new HashSet<String>());
}
final Set<String> streamSetsRolesSet = roleMapping.get(ldapRole);
for (String streamSetsRole : streamSetsRoles) {
streamSetsRolesSet.add(streamSetsRole.trim());
}
}
} catch (Exception e) {
throw new RuntimeException(Utils.format("Invalid LDAP group to Data Collector role mapping configuration - '{}'.",
option, e.getMessage()), e);
}
return roleMapping;
}
protected Set<String> tryMappingRole(String role) {
Set<String> roles = new HashSet<String>();
if (roleMapping == null || roleMapping.isEmpty()) {
return roles;
}
Set<String> streamSetsRoles = roleMapping.get(role);
if (streamSetsRoles != null) {
// add all mapped roles
for (String streamSetsRole : streamSetsRoles) {
roles.add(streamSetsRole);
}
}
return roles;
}
protected abstract String getAppAuthToken(Configuration appConfiguration);
protected abstract String getComponentId(Configuration appConfiguration);
protected Map<String, String> getRegistrationAttributes() {
return ImmutableMap.of(SSOConstants.SERVICE_BASE_URL_ATTR, this.runtimeInfo.getBaseHttpUrl());
}
@VisibleForTesting
HttpConfiguration configureForwardRequestCustomizer(HttpConfiguration httpConf) {
if (conf.get(HTTP_ENABLE_FORWARDED_REQUESTS_KEY, HTTP_ENABLE_FORWARDED_REQUESTS_DEFAULT)) {
httpConf.addCustomizer(new ForwardedRequestCustomizer());
}
return httpConf;
}
@VisibleForTesting
HttpConfiguration getHttpConf() {
return httpConf;
}
}