//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2018 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import java.io.File;
import java.io.FileInputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.*;
import com.threerings.getdown.util.StringUtil;
import jalview.util.HttpUtils;
import com.threerings.getdown.data.Application;
/** Configuration that comes from our "environment" (command line args, sys props, etc.). */
public final class EnvConfig {
/** Used to report problems or feedback by {@link #create}. */
public static final class Note {
public static enum Level { INFO, WARN, ERROR };
public static Note info (String msg) { return new Note(Level.INFO, msg); }
public static Note warn (String msg) { return new Note(Level.WARN, msg); }
public static Note error (String msg) { return new Note(Level.ERROR, msg); }
public final Level level;
public final String message;
public Note (Level level, String message) {
this.level = level;
this.message = message;
}
}
/**
* Creates an environment config, obtaining information (in order) from the following sources:
*
*
* - A {@code bootstrap.properties} file bundled with the jar.
* - System properties supplied to the JVM.
* - The supplied command line arguments ({@code argv}).
*
*
* If a later source supplies a configuration already provided by a prior source, a warning
* message will be logged to indicate the conflict, and the prior source will be used.
*
* @param notes a list into which notes are added, to be logged after the logging system has
* been initialized (which cannot happen until the appdir is known). If any {@code ERROR} notes
* are included, the app should terminate after reporting them.
* @return an env config instance, or {@code null} if no appdir could be located via any
* configuration source.
*/
public static EnvConfig create (String[] argv, List notes) {
String appDir = null, appDirProv = null;
String appId = null, appIdProv = null;
String appBase = null, appBaseProv = null;
// start with bootstrap.properties config, if avaialble
try {
ResourceBundle bundle = ResourceBundle.getBundle("bootstrap");
if (bundle.containsKey("appdir")) {
appDir = bundle.getString("appdir");
appDir = appDir.replace(USER_HOME_KEY, System.getProperty("user.home"));
appDirProv = "bootstrap.properties";
}
if (bundle.containsKey("appid")) {
appId = bundle.getString("appid");
appIdProv = "bootstrap.properties";
}
if (bundle.containsKey("appbase")) {
appBase = bundle.getString("appbase");
appBaseProv = "bootstrap.properties";
}
// if any system properties are specified (keys prefixed with sys.), set those up
for (String key : bundle.keySet()) {
if (key.startsWith("sys.")) {
String skey = key.substring(4);
String svalue = bundle.getString(key);
notes.add(Note.info("Setting system property from bundle: " +
skey + "='" + svalue + "'"));
System.setProperty(skey, svalue);
}
}
} catch (MissingResourceException e) {
// bootstrap.properties is optional; no need for a warning
}
// next seek config from system properties
String spropsAppDir = SysProps.appDir();
if (!StringUtil.isBlank(spropsAppDir)) {
if (appDir == null) {
appDir = spropsAppDir;
appDirProv = "system property";
} else {
notes.add(Note.warn("Ignoring 'appdir' system property, have appdir via '" +
appDirProv + "'"));
}
}
String spropsAppId = SysProps.appId();
if (!StringUtil.isBlank(spropsAppId)) {
if (appId == null) {
appId = spropsAppId;
appIdProv = "system property";
} else {
notes.add(Note.warn("Ignoring 'appid' system property, have appid via '" +
appIdProv + "'"));
}
}
String spropsAppBase = SysProps.appBase();
if (!StringUtil.isBlank(spropsAppBase)) {
if (appBase == null) {
appBase = spropsAppBase;
appBaseProv = "system property";
} else {
notes.add(Note.warn("Ignoring 'appbase' system property, have appbase via '" +
appBaseProv + "'"));
}
}
// finally obtain config from command line arguments
String argvAppDir = argv.length > 0 ? argv[0] : null;
if (!StringUtil.isBlank(argvAppDir)) {
if (appDir == null) {
appDir = argvAppDir;
appDirProv = "command line";
} else {
notes.add(Note.warn("Ignoring 'appdir' command line arg, have appdir via '" +
appDirProv + "'"));
}
}
String argvAppId = argv.length > 1 ? argv[1] : null;
if (!StringUtil.isBlank(argvAppId)) {
if (appId == null) {
appId = argvAppId;
appIdProv = "command line";
} else {
notes.add(Note.warn("Ignoring 'appid' command line arg, have appid via '" +
appIdProv + "'"));
}
}
int skipArgs = 2;
// Look for locator file, pass to Application and remove from appArgs
String argvLocatorFilename = argv.length > 2 ? argv[2] : null;
if (!StringUtil.isBlank(argvLocatorFilename)
&& argvLocatorFilename.toLowerCase(Locale.ROOT).endsWith("."+Application.LOCATOR_FILE_EXTENSION)) {
argvLocatorFilename = HttpUtils.equivalentJalviewUrl(argvLocatorFilename);
notes.add(Note.info("locatorFilename in args: '"+argv[2]+"'"));
Application.setLocatorFile(argvLocatorFilename);
skipArgs++;
}
// ensure that we were able to find an app dir
if (appDir == null) {
return null; // caller will report problem to user
}
notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir));
if (appId != null) notes.add(Note.info("Using appid from " + appIdProv + ": " + appId));
if (appBase != null) notes.add(
Note.info("Using appbase from " + appBaseProv + ": " + appBase));
// ensure that the appdir refers to a directory that exists
File appDirFile = new File(appDir);
if (!appDirFile.exists()) {
// if we have a bootstrap URL then we auto-create the app dir; this enables an
// installer to simply place a getdown.jar file somewhere and create an OS shortcut
// that runs getdown with an appdir and appbase specified, and have getdown create the
// appdir and download the app into it
if (!StringUtil.isBlank(appBase)) {
if (appDirFile.mkdirs()) {
notes.add(Note.info("Auto-created app directory '" + appDir + "'"));
} else {
notes.add(Note.warn("Unable to auto-create app dir: '" + appDir + "'"));
}
} else {
notes.add(Note.error("Invalid appdir '" + appDir + "': directory does not exist"));
return null;
}
} else if (!appDirFile.isDirectory()) {
notes.add(Note.error("Invalid appdir '" + appDir + "': refers to non-directory"));
return null;
}
// pass along anything after the first two (or three) args as extra app args
List appArgs = argv.length > skipArgs ?
Arrays.asList(argv).subList(skipArgs, argv.length) :
Collections.emptyList();
// load X.509 certificate if it exists
File crtFile = new File(appDirFile, Digest.digestFile(Digest.VERSION) + ".crt");
List certs = new ArrayList<>();
if (crtFile.exists()) {
try (FileInputStream fis = new FileInputStream(crtFile)) {
X509Certificate certificate = (X509Certificate)
CertificateFactory.getInstance("X.509").generateCertificate(fis);
certs.add(certificate);
} catch (Exception e) {
notes.add(Note.error("Certificate error: " + e.getMessage()));
}
}
return new EnvConfig(appDirFile, appId, appBase, certs, appArgs);
}
/** The directory in which the application and metadata is stored. */
public final File appDir;
/** Either {@code null} or an identifier for a secondary application that should be
* launched. That app will use {@code appid.class} and {@code appid.apparg} to configure
* itself but all other parameters will be the same as the primary app. */
public final String appId;
/** Either {@code null} or fallback {@code appbase} to use if one cannot be read from a
* {@code getdown.txt} file during startup. */
public final String appBase;
/** Zero or more signing certificates used to verify the digest file. */
public final List certs;
/** Additional arguments to pass on to launched application. These will be added after the
* args in the getdown.txt file. */
public final List appArgs;
public EnvConfig (File appDir) {
this(appDir, null, null, Collections.emptyList(),
Collections.emptyList());
}
private EnvConfig (File appDir, String appId, String appBase, List certs,
List appArgs) {
this.appDir = appDir;
this.appId = appId;
this.appBase = appBase;
this.certs = certs;
this.appArgs = appArgs;
}
private static final String USER_HOME_KEY = "${user.home}";
}