/**
* Yobi, Project Hosting SW
*
* Copyright 2013 NAVER Corp.
* http://yobi.io
*
* @author Wansoon Park, Yi EungJun, Suwon Chae
*
* Licensed 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.
*/
import com.avaje.ebean.Ebean;
import com.typesafe.config.ConfigFactory;
import controllers.SvnApp;
import controllers.UserApp;
import controllers.routes;
import mailbox.MailboxService;
import models.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.impl.cookie.DateUtils;
import play.Application;
import play.Configuration;
import play.GlobalSettings;
import play.Play;
import play.api.mvc.Handler;
import play.data.Form;
import play.libs.F.Promise;
import play.mvc.Action;
import play.mvc.Http;
import play.mvc.Http.RequestHeader;
import play.mvc.Result;
import play.mvc.Results;
import utils.*;
import views.html.welcome.restart;
import views.html.welcome.secret;
import javax.annotation.Nonnull;
import javax.persistence.PersistenceException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.util.Date;
import static play.data.Form.form;
import static play.mvc.Results.badRequest;
public class Global extends GlobalSettings {
private static final String[] INITIAL_ENTITY_NAME = {"users", "roles", "siteAdmins"};
private final String DEFAULT_SECRET = "VA2v:_I=h9>?FYOH:@ZhW]01P<mWZAKlQ>kk>Bo`mdCiA>pDw64FcBuZdDh<47Ew";
private boolean isSecretInvalid = false;
private boolean isRestartRequired = false;
private MailboxService mailboxService = new MailboxService();
private boolean hasFailedToUpdateSecretKey = false;
private ConfigFile configFile = new ConfigFile("config", "application.conf");
private ConfigFile loggerConfigFile = new ConfigFile("logger", "application-logger.xml");
@Override
public Configuration onLoadConfig(play.Configuration config, File path, ClassLoader classloader) {
initLoggerConfig();
return initConfig(classloader);
}
/**
* Creates application.conf by default if necessary
*
* @param classloader
* @return the configuration read from the created file,
* or null if this method didn't create the file.
*/
private Configuration initConfig(ClassLoader classloader) {
if (configFile.isLocationSpecified()) {
return null;
}
try {
if (configFile.getPath().toFile().exists()) {
return null;
}
} catch (URISyntaxException e) {
play.Logger.error("Failed to check whether the config file exists", e);
return null;
}
try {
configFile.createByDefault();
return new Configuration(ConfigFactory.load(classloader,
ConfigFactory.parseFileAnySyntax(configFile.getPath().toFile())));
} catch (Exception e) {
play.Logger.error("Failed to initialize configuration", e);
return null;
}
}
/**
* Creates application-logger.xml by default if necessary
*
* Note: This method creates application-logger.xml even if logger.xml exists.
*/
private void initLoggerConfig() {
try {
if (!loggerConfigFile.isLocationSpecified() && !loggerConfigFile.getPath().toFile().exists()) {
try {
loggerConfigFile.createByDefault();
} catch (Exception e) {
play.Logger.error("Failed to initialize logger configuration", e);
}
}
} catch (URISyntaxException e) {
play.Logger.error("Failed to check whether the logger config file exists", e);
}
}
@Override
public void onStart(Application app) {
isSecretInvalid = equalsDefaultSecret();
insertInitialData();
Config.onStart();
Property.onStart();
PullRequest.onStart();
NotificationMail.onStart();
NotificationEvent.onStart();
Attachment.onStart();
AccessControl.onStart();
if (!isSecretInvalid) {
YobiUpdate.onStart();
mailboxService.start();
}
}
private boolean equalsDefaultSecret() {
return DEFAULT_SECRET.equals(play.Configuration.root().getString("application.secret"));
}
private static void insertInitialData() {
if (Ebean.find(User.class).findRowCount() == 0) {
YamlUtil.insertDataFromYaml("initial-data.yml", INITIAL_ENTITY_NAME);
}
}
@Override
public Action<Void> onRequest(final Http.Request request, Method actionMethod) {
if (isSecretInvalid) {
if (isRestartRequired) {
return getRestartAction();
} else {
return getConfigSecretAction();
}
} else {
return getDefaultAction(request);
}
}
@SuppressWarnings("rawtypes")
private Action<Void> getDefaultAction(final Http.Request request) {
final long start = System.currentTimeMillis();
return new Action.Simple() {
public Promise<Result> call(Http.Context ctx) throws Throwable {
UserApp.initTokenUser();
try {
UserApp.updatePreferredLanguage();
} catch (Exception e) {
play.Logger.warn("Failed to update the preferred language", e);
}
ctx.response().setHeader("Date", DateUtils.formatDate(new Date()));
ctx.response().setHeader("Cache-Control", "no-cache");
Promise<Result> promise = delegate.call(ctx);
AccessLogger.log(request, promise, start);
return promise;
}
};
}
private Action<Void> getRestartAction() {
return new Action.Simple() {
@Override
public Promise<Result> call(Http.Context ctx) throws Throwable {
return Promise.pure((Result) ok(restart.render(hasFailedToUpdateSecretKey)));
}
};
}
private Action<Void> getConfigSecretAction() {
return new Action.Simple() {
@Override
public Promise<Result> call(Http.Context ctx) throws Throwable {
if( ctx.request().method().toLowerCase().equals("post") ) {
Form<User> newSiteAdminUserForm = form(User.class).bindFromRequest();
if (hasError(newSiteAdminUserForm)) {
return Promise.pure((Result) badRequest(secret.render(SiteAdmin.SITEADMIN_DEFAULT_LOGINID, newSiteAdminUserForm)));
}
User siteAdmin = SiteAdmin.updateDefaultSiteAdmin(newSiteAdminUserForm.get());
try {
updateSiteSecretKey(createSeed(siteAdmin.loginId + ":" + siteAdmin.password));
} catch (Exception e) {
play.Logger.warn("Failed to update secret key", e);
hasFailedToUpdateSecretKey = true;
}
isRestartRequired = true;
return Promise.pure((Result) ok(restart.render(hasFailedToUpdateSecretKey)));
} else {
return Promise.pure((Result) ok(secret.render(SiteAdmin.SITEADMIN_DEFAULT_LOGINID, new Form<>(User.class))));
}
}
private String createSeed(String basicSeed) {
String seed = basicSeed;
try {
seed += InetAddress.getLocalHost();
} catch (Exception e) {
play.Logger.warn("Failed to get localhost address", e);
}
return seed;
}
private void updateSiteSecretKey(String seed) throws Exception {
SecureRandom random = new SecureRandom(seed.getBytes(Config.getCharset()));
String secret = new BigInteger(130, random).toString(32);
if (configFile.isExternal()) {
throw new Exception("Cowardly refusing to update an external file: " + configFile.getPath());
}
byte[] bytes = Files.readAllBytes(configFile.getPath());
String config = new String(bytes, Config.getCharset());
config = config.replace(DEFAULT_SECRET, secret);
Files.write(configFile.getPath(), config.getBytes(Config.getCharset()));
}
private boolean hasError(Form<User> newUserForm) {
if (StringUtils.isBlank(newUserForm.field("loginId").value())) {
newUserForm.reject("loginId", "user.wrongloginId.alert");
}
if (!newUserForm.field("loginId").value().equals("admin")) {
newUserForm.reject("loginId", "user.wrongloginId.alert");
}
if (StringUtils.isBlank(newUserForm.field("password").value())) {
newUserForm.reject("password", "user.wrongPassword.alert");
}
if (!newUserForm.field("password").value().equals(newUserForm.field("retypedPassword").value())) {
newUserForm.reject("retypedPassword", "user.confirmPassword.alert");
}
if (StringUtils.isBlank(newUserForm.field("email").value())) {
newUserForm.reject("email", "validation.invalidEmail");
}
if (User.isEmailExist(newUserForm.field("email").value())) {
newUserForm.reject("email", "user.email.duplicate");
}
return newUserForm.hasErrors();
}
};
}
@Override
public Handler onRouteRequest(RequestHeader request) {
// If request method is webdav method, SvnApp serves this request
// because Play2 cannot route them.
if (SvnApp.isWebDavMethod(request.method())) {
return routes.ref.SvnApp.service().handler();
} else {
return super.onRouteRequest(request);
}
}
public void onStop(Application app) {
mailboxService.stop();
}
@Override
public Promise<Result> onHandlerNotFound(RequestHeader request) {
AccessLogger.log(request, null, Http.Status.NOT_FOUND);
return Promise.pure((Result) Results.notFound(ErrorViews.NotFound.render()));
}
@Override
public Promise<Result> onError(RequestHeader request, Throwable t) {
AccessLogger.log(request, null, Http.Status.INTERNAL_SERVER_ERROR);
if (Play.isProd()) {
String messageKey;
if (t.getCause() instanceof PersistenceException && StringUtils.contains(t.getMessage(), "timed out")){
messageKey = "error.timeout";
} else {
messageKey = "error.internalServerError";
}
return Promise.pure((Result) Results.internalServerError(views.html.error.internalServerError_default.render(messageKey)));
} else {
return super.onError(request, t);
}
}
@Override
public Promise<Result> onBadRequest(RequestHeader request, String error) {
AccessLogger.log(request, null, Http.Status.BAD_REQUEST);
return Promise.pure((Result) badRequest(ErrorViews.BadRequest.render()));
}
private static class ConfigFile {
private static final String CONFIG_DIRNAME = "conf";
private final String fileName;
private final String defaultFileName;
private final String propertyGroup;
ConfigFile(String propertyGroup, String fileName) {
this.propertyGroup = propertyGroup;
this.fileName = fileName;
this.defaultFileName = fileName + ".default";
}
String getProperty(@Nonnull String key) {
return System.getProperty(propertyGroup + "." + key);
}
String getProperty(@Nonnull String key, String defaultValue) {
return System.getProperty(propertyGroup + "." + key, defaultValue);
}
/**
* The location of the config file is specified by user
*/
boolean isLocationSpecified() {
return (getProperty("resource") != null)
|| (getProperty("file") != null)
|| (getProperty("url") != null);
}
void createByDefault() throws IOException, URISyntaxException {
InputStream stream = Config.class.getClassLoader().getResourceAsStream(defaultFileName);
getPath().toFile().getParentFile().mkdirs();
if (stream != null) {
Files.copy(stream, getPath());
} else {
Files.copy(getDirectoryPath().resolve(defaultFileName), getPath());
}
}
/**
* @return the path to the configuration file
* @throws java.lang.IllegalStateException
*/
Path getPath() throws URISyntaxException {
if (getProperty("url") != null) {
return Paths.get(new URI(getProperty("url")));
}
if (getProperty("file") != null) {
return Paths.get(getProperty("file"));
}
String filename = getProperty("resource", fileName);
return getDirectoryPath().resolve(filename);
}
/**
* @return the path to the directory to store configuration files
*/
static Path getDirectoryPath() {
return Paths.get(Config.getYobiHome(""), CONFIG_DIRNAME);
}
boolean isExternal() throws IOException, URISyntaxException {
return !FileUtil.isSubpathOf(getPath(), getDirectoryPath()) &&
!FileUtil.isSubpathOf(getPath(), Paths.get(Config.getYobiHome()));
}
}
}