/*
* Copyright 2013-2014 TORCH GmbH
*
* This file is part of Graylog2.
*
* Graylog2 is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Graylog2 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Graylog2. If not, see <http://www.gnu.org/licenses/>.
*/
package lib;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.collect.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.name.Names;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import lib.security.PlayAuthenticationListener;
import lib.security.RedirectAuthenticator;
import lib.security.RethrowingFirstSuccessfulStrategy;
import lib.security.ServerRestInterfaceRealm;
import models.LocalAdminUser;
import models.ModelFactoryModule;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationListener;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.graylog2.logback.appender.AccessLog;
import org.graylog2.restclient.lib.ApiClient;
import org.graylog2.restclient.lib.DateTools;
import org.graylog2.restclient.lib.Graylog2MasterUnavailableException;
import org.graylog2.restclient.lib.ServerNodes;
import org.graylog2.restclient.lib.ServerNodesRefreshService;
import org.graylog2.restclient.lib.Tools;
import org.graylog2.restclient.lib.Version;
import org.graylog2.restclient.models.Node;
import org.graylog2.restclient.models.SessionService;
import org.graylog2.restclient.models.UserService;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.Application;
import play.Configuration;
import play.GlobalSettings;
import play.api.mvc.EssentialFilter;
import play.libs.F;
import play.libs.Json;
import play.mvc.Http;
import play.mvc.Result;
import java.io.File;
import java.net.URI;
import java.util.List;
import static play.mvc.Results.internalServerError;
@SuppressWarnings("unused")
public class Global extends GlobalSettings {
private static final Logger log = LoggerFactory.getLogger(Global.class);
private static Injector injector;
/**
* Retrieve the application's global Guice injector.
* <p/>
* Unfortunately there seems to be no supported way to store custom objects in the application,
* thus we need to make this accessor static. However, running more than one Play application in
* the same JVM won't work anyway, so we are on the safe side here.
*
* @return the Guice injector for this application.
*/
public static Injector getInjector() {
return injector;
}
@Override
public void onStart(Application app) {
log.info("Graylog2 web interface version {} starting up.", Version.VERSION);
final String appSecret = app.configuration().getString("application.secret");
if (appSecret == null || appSecret.isEmpty()) {
log.error("Please configure application.secret in your conf/graylog2-web-interface.conf");
throw new IllegalStateException("No application.secret configured.");
}
if (appSecret.length() < 16) {
log.error("Please configure application.secret in your conf/graylog2-web-interface.conf to be longer than 16 characters. Suggested is using pwgen -N 1 -s 96 or similar");
throw new IllegalStateException("application.secret is too short, use at least 16 characters! Suggested is to use pwgen -N 1 -s 96 or similar");
}
final String graylog2ServerUris = app.configuration().getString("graylog2-server.uris", "");
if (graylog2ServerUris.isEmpty()) {
log.error("graylog2-server.uris is not set!");
throw new IllegalStateException("graylog2-server.uris is empty");
}
final String[] uris = graylog2ServerUris.split(",");
if (uris.length == 0) {
log.error("graylog2-server.uris is empty!");
throw new IllegalStateException("graylog2-server.uris is empty");
}
final URI[] initialNodes = new URI[uris.length];
int i = 0;
for (String uri : uris) {
initialNodes[i++] = URI.create(uri);
}
final String timezone = app.configuration().getString("timezone", "");
if (!timezone.isEmpty()) {
try {
DateTools.setApplicationTimeZone(DateTimeZone.forID(timezone));
} catch (IllegalArgumentException e) {
log.error("Invalid timezone {} specified!", timezone);
throw new IllegalStateException(e);
}
}
log.info("Using application default timezone {}", DateTools.getApplicationTimeZone());
final ObjectMapper objectMapper = buildObjectMapper();
Json.setObjectMapper(objectMapper);
final List<Module> modules = Lists.newArrayList();
modules.add(new AbstractModule() {
@Override
protected void configure() {
bind(URI[].class).annotatedWith(Names.named("Initial Nodes")).toInstance(initialNodes);
bind(Long.class).annotatedWith(Names.named("Default Timeout"))
.toInstance(org.graylog2.restclient.lib.Configuration.apiTimeout("DEFAULT"));
bind(ObjectMapper.class).toInstance(objectMapper);
}
});
modules.add(new ModelFactoryModule());
injector = Guice.createInjector(modules);
// start the services that need starting
final ApiClient api = injector.getInstance(ApiClient.class);
api.start();
injector.getInstance(ServerNodesRefreshService.class).start();
// TODO replace with custom AuthenticatedAction filter
RedirectAuthenticator.userService = injector.getInstance(UserService.class);
RedirectAuthenticator.sessionService = injector.getInstance(SessionService.class);
// temporarily disabled for preview to prevent confusion.
// LocalAdminUserRealm localAdminRealm = new LocalAdminUserRealm("local-accounts");
// localAdminRealm.setCredentialsMatcher(new HashedCredentialsMatcher("SHA2"));
// setupLocalUser(api, localAdminRealm, app);
Realm serverRestInterfaceRealm = injector.getInstance(ServerRestInterfaceRealm.class);
final DefaultSecurityManager securityManager =
new DefaultSecurityManager(
Lists.newArrayList(serverRestInterfaceRealm)
);
// disable storing sessions (TODO we might want to write a session store bridge to play's session cookie)
final DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
final DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
final Authenticator authenticator = securityManager.getAuthenticator();
if (authenticator instanceof ModularRealmAuthenticator) {
ModularRealmAuthenticator a = (ModularRealmAuthenticator) authenticator;
a.setAuthenticationStrategy(new RethrowingFirstSuccessfulStrategy());
a.setAuthenticationListeners(
Lists.<AuthenticationListener>newArrayList(new PlayAuthenticationListener())
);
}
SecurityUtils.setSecurityManager(securityManager);
}
private ObjectMapper buildObjectMapper() {
return new ObjectMapper()
.registerModules(new GuavaModule(), new JodaModule())
.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES)
.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
@Override
public void onStop(Application app) {
injector.getInstance(ApiClient.class).stop();
injector.getInstance(ServerNodesRefreshService.class).stop();
}
@Override
@SuppressWarnings("unchecked")
public <T extends EssentialFilter> Class<T>[] filters() {
return new Class[]{AccessLog.class, NoCacheHeader.class};
}
@Override
public <A> A getControllerInstance(Class<A> controllerClass) throws Exception {
return injector.getInstance(controllerClass);
}
@Override
public F.Promise<Result> onError(Http.RequestHeader request, Throwable t) {
if (t.getCause() instanceof Graylog2MasterUnavailableException) {
final ServerNodes serverNodes = injector.getInstance(ServerNodes.class);
final List<Node> configuredNodes = serverNodes.getConfiguredNodes();
final List<Node> nodesEverConnectedTo = serverNodes.all(true);
return F.Promise.<Result>pure(internalServerError(
views.html.disconnected.no_master.render(Http.Context.current(), configuredNodes, nodesEverConnectedTo, serverNodes))
);
}
return super.onError(request, t);
}
@Override
public Configuration onLoadConfig(Configuration configuration, File file, ClassLoader classLoader) {
boolean isTest = false;
if (System.getProperty("skip.config.check", "false").equals("true")) {
log.info("In test mode, not performing config file checks.");
isTest = true;
}
final String configOverrideLocation = Tools.firstNonNull("",
System.getProperty("config.file"),
System.getProperty("config.url"),
System.getProperty("config.resource"));
if (!configOverrideLocation.isEmpty()) {
log.warn("Using configuration from overridden location at {}", configOverrideLocation);
return configuration;
}
final File configFile = new File(file, "conf/graylog2-web-interface.conf");
if (!isTest) {
if (!configFile.exists()) {
log.error("Your configuration should be at {} but does not exist, cannot continue without it.", configFile.getAbsoluteFile());
throw new IllegalStateException("Missing configuration file " + configFile.getAbsolutePath());
} else if (!configFile.canRead()) {
log.error("Your configuration at {} is not readable, cannot continue without it.", configFile.getAbsoluteFile());
throw new IllegalStateException("Unreadable configuration file " + configFile.getAbsolutePath());
}
}
final Config config = ConfigFactory.parseFileAnySyntax(configFile);
if (config.isEmpty() && !isTest) {
log.error("Your configuration file at {} is empty, cannot continue without content.", configFile.getAbsoluteFile());
throw new IllegalStateException("Empty configuration file " + configFile.getAbsolutePath());
/*
*
* This is merging the standard bundled application.conf with our graylog2-web-interface.conf.
* The application.conf must always be empty when packaged so there is nothing hidden from the user.
* We are merging, because the Configuration object already contains some information the web-interface needs.
*
*/
}
return new Configuration(
config.withFallback(configuration.getWrappedConfiguration().underlying())
);
}
private void setupLocalUser(ApiClient api, SimpleAccountRealm realm, Application app) {
final Configuration config = app.configuration();
final String username = config.getString("local-user.name", "localadmin");
final String passwordHash = config.getString("local-user.password-sha2");
if (passwordHash == null) {
log.warn("No password hash for local user {} set. " +
"If you lose connection to the graylog2-server at {}, you will be unable to log in!",
username, config.getString("graylog2-server"));
return;
}
realm.addAccount(
username,
passwordHash,
"local-admin"
);
LocalAdminUser.createSharedInstance(api, username, passwordHash);
}
}