updated control access

This commit is contained in:
Quality System Admin
2025-10-16 02:36:32 +03:00
parent 50c791e242
commit c96039542d
266 changed files with 32656 additions and 9 deletions

View File

@@ -0,0 +1,413 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.auth.Certificate;
import qz.build.provision.params.Phase;
import qz.installer.certificate.*;
import qz.installer.certificate.firefox.FirefoxCertificateInstaller;
import qz.installer.provision.ProvisionInstaller;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import qz.ws.WebsocketPorts;
import java.io.*;
import java.nio.file.*;
import java.security.cert.X509Certificate;
import java.util.*;
import static qz.common.Constants.*;
import static qz.installer.certificate.KeyPairWrapper.Type.CA;
import static qz.utils.FileUtilities.*;
/**
* Cross-platform wrapper for install steps
* - Used by CommandParser via command line
* - Used by PrintSocketServer at startup to ensure SSL is functioning
*/
public abstract class Installer {
protected static final Logger log = LogManager.getLogger(Installer.class);
// Silence prompts within our control
public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent"));
public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime";
WebsocketPorts websocketPorts;
public enum PrivilegeLevel {
USER,
SYSTEM
}
public abstract Installer removeLegacyStartup();
public abstract Installer addAppLauncher();
public abstract Installer addStartupEntry();
public abstract Installer addSystemSettings();
public abstract Installer removeSystemSettings();
public abstract void spawn(List<String> args) throws Exception;
public abstract void setDestination(String destination);
public abstract String getDestination();
private static Installer instance;
public static Installer getInstance() {
if(instance == null) {
switch(SystemUtilities.getOs()) {
case WINDOWS:
instance = new WindowsInstaller();
break;
case MAC:
instance = new MacInstaller();
break;
default:
instance = new LinuxInstaller();
}
}
return instance;
}
public static void install(String destination, boolean silent) throws Exception {
IS_SILENT |= silent; // preserve environmental variable if possible
getInstance();
if (destination != null) {
instance.setDestination(destination);
}
install();
}
public static boolean preinstall() {
getInstance();
log.info("Fixing runtime permissions...");
instance.setJrePermissions(SystemUtilities.getAppPath().toString());
log.info("Stopping running instances...");
return TaskKiller.killAll();
}
public static void install() throws Exception {
getInstance();
log.info("Installing to {}", instance.getDestination());
instance.removeLibs()
.removeProvisioning()
.deployApp()
.removeLegacyStartup()
.removeLegacyFiles()
.addSharedDirectory()
.addAppLauncher()
.addStartupEntry()
.invokeProvisioning(Phase.INSTALL)
.addSystemSettings();
}
public static void uninstall() {
log.info("Stopping running instances...");
TaskKiller.killAll();
getInstance();
log.info("Uninstalling from {}", instance.getDestination());
instance.removeSharedDirectory()
.removeSystemSettings()
.removeCerts()
.invokeProvisioning(Phase.UNINSTALL);
}
public Installer deployApp() throws IOException {
Path src = SystemUtilities.getAppPath();
Path dest = Paths.get(getDestination());
if(!Files.exists(dest)) {
Files.createDirectories(dest);
}
// Delete the JDK blindly
FileUtils.deleteDirectory(dest.resolve(JRE_LOCATION).toFile());
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
FileUtils.copyDirectory(src.toFile(), dest.toFile(), false);
FileUtilities.setPermissionsRecursively(dest, false);
// Fix permissions for provisioned files
FileUtilities.setExecutableRecursively(SystemUtilities.isMac() ?
dest.resolve("Contents/Resources").resolve(PROVISION_DIR) :
dest.resolve(PROVISION_DIR), false);
if(!SystemUtilities.isWindows()) {
setExecutable(SystemUtilities.isMac() ? "Contents/Resources/uninstall" : "uninstall");
setExecutable(SystemUtilities.isMac() ? "Contents/MacOS/" + ABOUT_TITLE : PROPS_FILE);
return setJrePermissions(getDestination());
}
return this;
}
private Installer setJrePermissions(String dest) {
File jreLocation = new File(dest, JRE_LOCATION);
File jreBin = new File(jreLocation, "bin");
File jreLib = new File(jreLocation, "lib");
// Set jre/bin/java and friends executable
File[] files = jreBin.listFiles(pathname -> !pathname.isDirectory());
if(files != null) {
for(File file : files) {
file.setExecutable(true, false);
}
}
// Set jspawnhelper executable
new File(jreLib, "jspawnhelper" + (SystemUtilities.isWindows() ? ".exe" : "")).setExecutable(true, false);
return this;
}
private void setExecutable(String relativePath) {
new File(getDestination(), relativePath).setExecutable(true, false);
}
/**
* Explicitly purge libs to notify system cache per https://github.com/qzind/tray/issues/662
*/
public Installer removeLibs() {
String[] dirs = { "libs" };
for (String dir : dirs) {
try {
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
} catch(IOException ignore) {}
}
return this;
}
public Installer cleanupLegacyLogs(int rolloverCount) {
// Convert old < 2.2.3 log file format
Path logLocation = USER_DIR;
int oldIndex = 0;
int newIndex = 0;
File oldFile;
do {
// Old: debug.log.1
oldFile = logLocation.resolve("debug.log." + ++oldIndex).toFile();
if(oldFile.exists()) {
// New: debug.1.log
File newFile;
do {
newFile = logLocation.resolve("debug." + ++newIndex + ".log").toFile();
} while(newFile.exists());
oldFile.renameTo(newFile);
log.info("Migrated log file {} to new location {}", oldFile, newFile);
}
} while(oldFile.exists() || oldIndex <= rolloverCount);
return this;
}
public Installer removeLegacyFiles() {
ArrayList<String> dirs = new ArrayList<>();
ArrayList<String> files = new ArrayList<>();
HashMap<String, String> move = new HashMap<>();
// QZ Tray 2.0 files
dirs.add("demo/js/3rdparty");
dirs.add("utils");
dirs.add("auth");
files.add("demo/js/qz-websocket.js");
files.add("windows-icon.ico");
// QZ Tray 2.2.3-SNAPSHOT accidentally wrote certs in the wrong place
dirs.add("ssl");
// QZ Tray 2.1 files
if(SystemUtilities.isMac()) {
// Moved to macOS Application Bundle standard https://developer.apple.com/go/?id=bundle-structure
dirs.add("demo");
dirs.add("libs");
files.add(PROPS_FILE + ".jar");
files.add("LICENSE.txt");
files.add("uninstall");
move.put(PROPS_FILE + ".properties", "Contents/Resources/" + PROPS_FILE + ".properties");
}
dirs.forEach(dir -> {
try {
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
} catch(IOException ignore) {}
});
files.forEach(file -> {
new File(instance.getDestination() + File.separator + file).delete();
});
move.forEach((src, dest) -> {
try {
FileUtils.moveFile(new File(instance.getDestination() + File.separator + src),
new File(instance.getDestination() + File.separator + dest));
} catch(IOException ignore) {}
});
return this;
}
public Installer addSharedDirectory() {
try {
Files.createDirectories(SHARED_DIR);
FileUtilities.setPermissionsRecursively(SHARED_DIR, true);
Path ssl = Paths.get(SHARED_DIR.toString(), "ssl");
Files.createDirectories(ssl);
FileUtilities.setPermissionsRecursively(ssl, true);
log.info("Created shared directory: {}", SHARED_DIR);
} catch(IOException e) {
log.warn("Could not create shared directory: {}", SHARED_DIR);
}
return this;
}
public Installer removeSharedDirectory() {
try {
FileUtils.deleteDirectory(SHARED_DIR.toFile());
log.info("Deleted shared directory: {}", SHARED_DIR);
} catch(IOException e) {
log.warn("Could not delete shared directory: {}", SHARED_DIR);
}
return this;
}
/**
* Checks, and if needed generates an SSL for the system
*/
public CertificateManager certGen(boolean forceNew, String... hostNames) throws Exception {
CertificateManager certificateManager = new CertificateManager(forceNew, hostNames);
boolean needsInstall = certificateManager.needsInstall();
try {
// Check that the CA cert is installed
X509Certificate caCert = certificateManager.getKeyPair(CA).getCert();
NativeCertificateInstaller installer = NativeCertificateInstaller.getInstance();
if (forceNew || needsInstall) {
// Remove installed certs per request (usually the desktop installer, or failure to write properties)
// Skip if running from IDE, this may accidentally remove sandboxed certs
if(SystemUtilities.isJar()) {
List<String> matchingCerts = installer.find();
installer.remove(matchingCerts);
}
installer.install(caCert);
FirefoxCertificateInstaller.install(caCert, hostNames);
} else {
// Make sure the certificate is recognized by the system
if(caCert == null) {
log.info("CA cert is empty, skipping installation checks. This is normal for trusted/3rd-party SSL certificates.");
} else {
File tempCert = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
CertificateManager.writeCert(caCert, tempCert); // temp cert
if (!installer.verify(tempCert)) {
installer.install(caCert);
FirefoxCertificateInstaller.install(caCert, hostNames);
}
if(!tempCert.delete()) {
tempCert.deleteOnExit();
}
}
}
}
catch(Exception e) {
log.error("Something went wrong obtaining the certificate. HTTPS will fail.", e);
}
// Add provisioning steps that come after certgen
if(SystemUtilities.isAdmin()) {
invokeProvisioning(Phase.CERTGEN);
}
return certificateManager;
}
/**
* Remove matching certs from user|system, then Firefox
*/
public Installer removeCerts() {
// System certs
NativeCertificateInstaller instance = NativeCertificateInstaller.getInstance();
instance.remove(instance.find());
// Firefox certs
FirefoxCertificateInstaller.uninstall();
return this;
}
/**
* Add user-specific settings
* Note: See override usage for platform-specific tasks
*/
public Installer addUserSettings() {
// Check for whitelisted certificates in <install>/whitelist/
Path whiteList = SystemUtilities.getJarParentPath().resolve(WHITELIST_CERT_DIR);
if(Files.exists(whiteList) && Files.isDirectory(whiteList)) {
for(File file : whiteList.toFile().listFiles()) {
try {
Certificate cert = new Certificate(FileUtilities.readLocalFile(file.getPath()));
if (!cert.isSaved()) {
FileUtilities.addToCertList(ALLOW_FILE, file);
}
} catch(Exception e) {
log.warn("Could not add {} to {}", file, ALLOW_FILE, e);
}
}
}
return instance;
}
public Installer invokeProvisioning(Phase phase) {
try {
Path provisionPath = SystemUtilities.isMac() ?
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
Paths.get(getDestination()).resolve(PROVISION_DIR);
ProvisionInstaller provisionInstaller = new ProvisionInstaller(provisionPath);
provisionInstaller.invoke(phase);
// Special case for custom websocket ports
if(phase == Phase.INSTALL) {
websocketPorts = WebsocketPorts.parseFromSteps(provisionInstaller.getSteps());
}
} catch(Exception e) {
log.warn("An error occurred invoking provision \"phase\": \"{}\"", phase, e);
}
return this;
}
public Installer removeProvisioning() {
try {
Path provisionPath = SystemUtilities.isMac() ?
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
Paths.get(getDestination()).resolve(PROVISION_DIR);
FileUtils.deleteDirectory(provisionPath.toFile());
} catch(Exception e) {
log.warn("An error occurred removing provision directory", e);
}
return this;
}
public static Properties persistProperties(File oldFile, Properties newProps) {
if(oldFile.exists()) {
Properties oldProps = new Properties();
try(Reader reader = new FileReader(oldFile)) {
oldProps.load(reader);
for(String key : PERSIST_PROPS) {
if (oldProps.containsKey(key)) {
String value = oldProps.getProperty(key);
log.info("Preserving {}={} for install", key, value);
newProps.put(key, value);
}
}
} catch(IOException e) {
log.warn("Warning, an error occurred reading the old properties file {}", oldFile, e);
}
}
return newProps;
}
public void spawn(String ... args) throws Exception {
spawn(new ArrayList(Arrays.asList(args)));
}
}

View File

@@ -0,0 +1,371 @@
package qz.installer;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.FileUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.UnixUtilities;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import static qz.common.Constants.*;
public class LinuxInstaller extends Installer {
protected static final Logger log = LogManager.getLogger(LinuxInstaller.class);
public static final String SHORTCUT_NAME = PROPS_FILE + ".desktop";
public static final String STARTUP_DIR = "/etc/xdg/autostart/";
public static final String STARTUP_LAUNCHER = STARTUP_DIR + SHORTCUT_NAME;
public static final String APP_DIR = "/usr/share/applications/";
public static final String APP_LAUNCHER = APP_DIR + SHORTCUT_NAME;
public static final String UDEV_RULES = "/lib/udev/rules.d/99-udev-override.rules";
public static final String[] CHROME_POLICY_DIRS = {"/etc/chromium/policies/managed", "/etc/opt/chrome/policies/managed" };
public static final String CHROME_POLICY = "{ \"URLAllowlist\": [\"" + DATA_DIR + "://*\"] }";
private String destination = "/opt/" + PROPS_FILE;
private String sudoer;
public LinuxInstaller() {
super();
sudoer = getSudoer();
}
public void setDestination(String destination) {
this.destination = destination;
}
public String getDestination() {
return destination;
}
public Installer addAppLauncher() {
addLauncher(APP_LAUNCHER, false);
return this;
}
public Installer addStartupEntry() {
addLauncher(STARTUP_LAUNCHER, true);
return this;
}
private void addLauncher(String location, boolean isStartup) {
HashMap<String, String> fieldMap = new HashMap<>();
// Dynamic fields
fieldMap.put("%DESTINATION%", destination);
fieldMap.put("%LINUX_ICON%", String.format("%s.svg", PROPS_FILE));
fieldMap.put("%COMMAND%", String.format("%s/%s", destination, PROPS_FILE));
fieldMap.put("%PARAM%", isStartup ? "--honorautostart" : "%u");
File launcher = new File(location);
try {
FileUtilities.configureAssetFile("assets/linux-shortcut.desktop.in", launcher, fieldMap, LinuxInstaller.class);
launcher.setReadable(true, false);
launcher.setExecutable(true, false);
} catch(IOException e) {
log.warn("Unable to write {} file: {}", isStartup ? "startup":"launcher", location, e);
}
}
public Installer removeLegacyStartup() {
log.info("Removing legacy autostart entries for all users matching {} or {}", ABOUT_TITLE, PROPS_FILE);
// assume users are in /home
String[] shortcutNames = {ABOUT_TITLE, PROPS_FILE};
for(File file : new File("/home").listFiles()) {
if (file.isDirectory()) {
File userStart = new File(file.getPath() + "/.config/autostart");
if (userStart.exists() && userStart.isDirectory()) {
for (String shortcutName : shortcutNames) {
File legacyStartup = new File(userStart.getPath() + File.separator + shortcutName + ".desktop");
if(legacyStartup.exists()) {
legacyStartup.delete();
}
}
}
}
}
return this;
}
public Installer addSystemSettings() {
// Legacy Ubuntu versions only: Patch Unity to show the System Tray
if(UnixUtilities.isUbuntu()) {
ShellUtilities.execute("gsettings", "set", "com.canonical.Unity.Panel", "systray", "-whitelist", "\"['all']\"");
if(ShellUtilities.execute("killall", "-w", "unity", "-panel")) {
ShellUtilities.execute("nohup", "unity", "-panel");
}
if(ShellUtilities.execute("killall", "-w", "unity", "-2d")) {
ShellUtilities.execute("nohup", "unity", "-2d");
}
}
// Chrome protocol handler
for (String policyDir : CHROME_POLICY_DIRS) {
log.info("Installing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
try {
FileUtilities.setPermissionsParentally(Files.createDirectories(Paths.get(policyDir)), false);
} catch(IOException e) {
log.warn("An error occurred creating {}", policyDir);
}
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
try (BufferedWriter writer = new BufferedWriter(new FileWriter(policy.toFile()))){
writer.write(CHROME_POLICY);
policy.toFile().setReadable(true, false);
}
catch(IOException e) {
log.warn("Unable to write chrome policy: {} ({}:launch will fail)", policy, DATA_DIR);
}
}
// USB permissions
try {
File udev = new File(UDEV_RULES);
if (udev.exists()) {
udev.delete();
}
FileUtilities.configureAssetFile("assets/linux-udev.rules.in", new File(UDEV_RULES), new HashMap<>(), LinuxInstaller.class);
// udev rules should be -rw-r--r--
udev.setReadable(true, false);
ShellUtilities.execute("udevadm", "control", "--reload-rules");
} catch(IOException e) {
log.warn("Could not install udev rules, usb support may fail {}", UDEV_RULES, e);
}
// Cleanup incorrectly placed files
File badFirefoxJs = new File("/usr/bin/defaults/pref/" + PROPS_FILE + ".js");
File badFirefoxCfg = new File("/usr/bin/" + PROPS_FILE + ".cfg");
if(badFirefoxCfg.exists()) {
log.info("Removing incorrectly placed Firefox configuration {}, {}...", badFirefoxJs, badFirefoxCfg);
badFirefoxCfg.delete();
new File("/usr/bin/defaults").delete();
}
// Cleanup incorrectly placed files
File badFirefoxPolicy = new File("/usr/bin/distribution/policies.json");
if(badFirefoxPolicy.exists()) {
log.info("Removing incorrectly placed Firefox policy {}", badFirefoxPolicy);
badFirefoxPolicy.delete();
// Delete the distribution folder too, as long as it's empty
File badPolicyFolder = badFirefoxPolicy.getParentFile();
if(badPolicyFolder.isDirectory() && badPolicyFolder.listFiles().length == 0) {
badPolicyFolder.delete();
}
}
// Cleanup
log.info("Cleaning up any remaining files...");
new File(destination + File.separator + "install").delete();
return this;
}
public Installer removeSystemSettings() {
// Chrome protocol handler
for (String policyDir : CHROME_POLICY_DIRS) {
log.info("Removing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
policy.toFile().delete();
}
// USB permissions
File udev = new File(UDEV_RULES);
if (udev.exists()) {
udev.delete();
}
return this;
}
// Environmental variables for spawning a task using sudo. Order is important.
static String[] SUDO_EXPORTS = {"USER", "HOME", "UPSTART_SESSION", "DISPLAY", "DBUS_SESSION_BUS_ADDRESS", "XDG_CURRENT_DESKTOP", "GNOME_DESKTOP_SESSION_ID" };
/**
* Spawns the process as the underlying regular user account, preserving the environment
*/
public void spawn(List<String> args) throws Exception {
if(!SystemUtilities.isAdmin()) {
// Not admin, just run as the existing user
ShellUtilities.execute(args.toArray(new String[args.size()]));
return;
}
// Get user's environment from dbus, etc
HashMap<String, String> env = getUserEnv(sudoer);
if(env.size() == 0) {
throw new Exception("Unable to get dbus info; can't spawn instance");
}
// Prepare the environment
String[] envp = new String[env.size() + ShellUtilities.envp.length];
int i = 0;
// Keep existing env
for(String keep : ShellUtilities.envp) {
envp[i++] = keep;
}
for(String key :env.keySet()) {
envp[i++] = String.format("%s=%s", key, env.get(key));
}
// Concat "sudo|su", sudoer, "nohup", args
ArrayList<String> argsList = sudoCommand(sudoer, true, args);
// Spawn
log.info("Executing: {}", Arrays.toString(argsList.toArray()));
Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
}
/**
* Constructs a command to help running as another user using "sudo" or "su"
*/
public static ArrayList<String> sudoCommand(String sudoer, boolean async, List<String> cmds) {
ArrayList<String> sudo = new ArrayList<>();
if(StringUtils.isEmpty(sudoer) || !userExists(sudoer)) {
throw new UnsupportedOperationException(String.format("Parameter [sudoer: %s] is empty or the provided user was not found", sudoer));
}
if(ShellUtilities.execute("which", "sudo") // check if sudo exists
|| ShellUtilities.execute("sudo", "-u", sudoer, "-v")) { // check if user can login
// Pass directly into "sudo"
log.info("Guessing that this system prefers \"sudo\" over \"su\".");
sudo.add("sudo");
// Add calling user
sudo.add("-E"); // preserve environment
sudo.add("-u");
sudo.add(sudoer);
// Add "background" task support
if(async) {
sudo.add("nohup");
}
if(cmds != null && cmds.size() > 0) {
// Add additional commands
sudo.addAll(cmds);
}
} else {
// Build and escape for "su"
log.info("Guessing that this system prefers \"su\" over \"sudo\".");
sudo.add("su");
// Add calling user
sudo.add(sudoer);
sudo.add("-c");
// Add "background" task support
if(async) {
sudo.add("nohup");
}
if(cmds != null && cmds.size() > 0) {
// Add additional commands
sudo.addAll(Arrays.asList(StringUtils.join(cmds, "\" \"") + "\""));
}
}
return sudo;
}
/**
* Gets the most likely non-root user account that the installer is running from
*/
private static String getSudoer() {
String sudoer = ShellUtilities.executeRaw("logname").trim();
if(sudoer.isEmpty() || SystemUtilities.isSolaris()) {
sudoer = System.getenv("SUDO_USER");
}
return sudoer;
}
/**
* Uses two common POSIX techniques for testing if the provided user account exists
*/
private static boolean userExists(String user) {
return ShellUtilities.execute("id", "-u", user) ||
ShellUtilities.execute("getent", "passwd", user);
}
/**
* Attempts to extract user environment variables from the dbus process to
* allow starting a graphical application as the current user.
*
* If this fails, items such as the user's desktop theme may not be known to Java
* at runtime resulting in the Swing L&F instead of the Gtk L&F.
*/
private static HashMap<String, String> getUserEnv(String matchingUser) {
if(!SystemUtilities.isAdmin()) {
throw new UnsupportedOperationException("Administrative access is required");
}
String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="};
ArrayList<String> pids = new ArrayList<>();
for(String dbusMatch : dbusMatches) {
pids.addAll(Arrays.asList(ShellUtilities.executeRaw("pgrep", "-f", dbusMatch).split("\\r?\\n")));
}
HashMap<String, String> env = new HashMap<>();
HashMap<String, String> tempEnv = new HashMap<>();
ArrayList<String> toExport = new ArrayList<>(Arrays.asList(SUDO_EXPORTS));
for(String pid : pids) {
if(pid.isEmpty()) {
continue;
}
try {
String[] vars;
if(SystemUtilities.isSolaris()) {
// Use pargs -e $$ to get environment
log.info("Reading environment info from [pargs, -e, {}]", pid);
String pargs = ShellUtilities.executeRaw("pargs", "-e", pid);
vars = pargs.split("\\r?\\n");
String delim = "]: ";
for(int i = 0; i < vars.length; i++) {
if(vars[i].contains(delim)) {
vars[i] = vars[i].substring(vars[i].indexOf(delim) + delim.length()).trim();
}
}
} else {
// Assume /proc/$$/environ
String environ = String.format("/proc/%s/environ", pid);
String delim = Pattern.compile("\0").pattern();
log.info("Reading environment info from {}", environ);
vars = new String(Files.readAllBytes(Paths.get(environ))).split(delim);
}
for(String var : vars) {
String[] parts = var.split("=", 2);
if(parts.length == 2) {
String key = parts[0].trim();
String val = parts[1].trim();
if(toExport.contains(key)) {
tempEnv.put(key, val);
}
}
}
} catch(Exception e) {
log.warn("An unexpected error occurred obtaining dbus info", e);
}
// Only add vars for the current user
if(matchingUser.trim().equals(tempEnv.get("USER"))) {
env.putAll(tempEnv);
} else {
log.debug("Expected USER={} but got USER={}, skipping results for {}", matchingUser, tempEnv.get("USER"), pid);
}
// Use gtk theme
if(env.containsKey("XDG_CURRENT_DESKTOP") && !env.containsKey("GNOME_DESKTOP_SESSION_ID")) {
if(env.get("XDG_CURRENT_DESKTOP").toLowerCase(Locale.ENGLISH).contains("gnome")) {
env.put("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated");
}
}
}
return env;
}
}

View File

@@ -0,0 +1,125 @@
package qz.installer;
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.FileUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import static qz.common.Constants.*;
public class MacInstaller extends Installer {
protected static final Logger log = LogManager.getLogger(MacInstaller.class);
private static final String PACKAGE_NAME = getPackageName();
public static final String LAUNCH_AGENT_PATH = String.format("/Library/LaunchAgents/%s.plist", MacInstaller.PACKAGE_NAME);
private String destination = "/Applications/" + ABOUT_TITLE + ".app";
public Installer addAppLauncher() {
// not needed; registered when "QZ Tray.app" is copied
return this;
}
public Installer addStartupEntry() {
File dest = new File(LAUNCH_AGENT_PATH);
HashMap<String, String> fieldMap = new HashMap<>();
// Dynamic fields
fieldMap.put("%PACKAGE_NAME%", PACKAGE_NAME);
fieldMap.put("%COMMAND%", String.format("%s/Contents/MacOS/%s", destination, ABOUT_TITLE));
fieldMap.put("%PARAM%", "--honorautostart");
try {
FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class);
// Disable service until reboot
if(SystemUtilities.isMac()) {
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
}
} catch(IOException e) {
log.warn("Unable to write startup file: {}", dest, e);
}
return this;
}
public void setDestination(String destination) {
this.destination = destination;
}
public String getDestination() {
return destination;
}
public Installer addSystemSettings() {
// Chrome protocol handler
String plist = "/Library/Preferences/com.google.Chrome.plist";
if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] {DATA_DIR + "://*" }).isEmpty()) {
ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLAllowlist", "-array-add", DATA_DIR +"://*");
}
return this;
}
public Installer removeSystemSettings() {
// Remove startup entry
File dest = new File(LAUNCH_AGENT_PATH);
dest.delete();
return this;
}
/**
* Removes legacy (<= 2.0) startup entries
*/
public Installer removeLegacyStartup() {
log.info("Removing startup entries for all users matching " + ABOUT_TITLE);
String script = "tell application \"System Events\" to delete "
+ "every login item where name is \"" + ABOUT_TITLE + "\""
+ " or name is \"" + PROPS_FILE + ".jar\"";
// Run on background thread in case System Events is hung or slow to respond
final String finalScript = script;
new Thread(() -> {
ShellUtilities.executeAppleScript(finalScript);
}).run();
return this;
}
public static String getPackageName() {
String packageName;
String[] parts = ABOUT_URL.split("\\W");
if (parts.length >= 2) {
// Parse io.qz.qz-print from Constants
packageName = String.format("%s.%s.%s", parts[parts.length - 1], parts[parts.length - 2], PROPS_FILE);
} else {
// Fallback on something sane
packageName = "local." + PROPS_FILE;
}
return packageName;
}
public void spawn(List<String> args) throws Exception {
if(SystemUtilities.isAdmin()) {
// macOS unconventionally uses "$USER" during its install process
String sudoer = System.getenv("USER");
if(sudoer == null || sudoer.isEmpty() || sudoer.equals("root")) {
// Fallback, should only fire via Terminal + sudo
sudoer = ShellUtilities.executeRaw("logname").trim();
}
// Start directly without waitFor(...), avoids deadlocking
Runtime.getRuntime().exec(new String[] { "su", sudoer, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""});
} else {
Runtime.getRuntime().exec(args.toArray(new String[args.size()]));
}
}
}

View File

@@ -0,0 +1,227 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashSet;
import static qz.common.Constants.PROPS_FILE;
public class TaskKiller {
protected static final Logger log = LogManager.getLogger(TaskKiller.class);
private static final String[] JAR_NAMES = {
PROPS_FILE + ".jar",
"qz.App", // v2.2.0...
"qz.ws.PrintSocketServer" // v2.0.0...v2.1.6
};
private static final String[] KILL_PID_CMD_POSIX = { "kill", "-9" };
private static final String[] KILL_PID_CMD_WIN32 = { "taskkill.exe", "/F", "/PID" };
private static final String[] KILL_PID_CMD = SystemUtilities.isWindows() ? KILL_PID_CMD_WIN32 : KILL_PID_CMD_POSIX;
/**
* Kills all QZ Tray processes, being careful not to kill itself
*/
public static boolean killAll() {
boolean success = true;
// Disable service until reboot
if(SystemUtilities.isMac()) {
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
}
// Use jcmd to get all java processes
HashSet<Integer> pids = findPidsJcmd();
if(!SystemUtilities.isWindows()) {
// Fallback to pgrep, needed for macOS (See JDK-8319589, JDK-8197387)
pids.addAll(findPidsPgrep());
} else if(WindowsUtilities.isSystemAccount()) {
// Fallback to powershell, needed for Windows
pids.addAll(findPidsPwsh());
}
// Careful not to kill ourselves ;)
pids.remove(SystemUtilities.getProcessId());
// Kill each PID
String[] killPid = new String[KILL_PID_CMD.length + 1];
System.arraycopy(KILL_PID_CMD, 0, killPid, 0, KILL_PID_CMD.length);
for (Integer pid : pids) {
killPid[killPid.length - 1] = pid.toString();
success = success && ShellUtilities.execute(killPid);
}
return success;
}
private static Path getJcmdPath() throws IOException {
Path jcmd;
if(SystemUtilities.isWindows()) {
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd.exe");
} else if (SystemUtilities.isMac()) {
jcmd = SystemUtilities.getJarParentPath().resolve("../PlugIns/Java.runtime/Contents/Home/bin/jcmd");
} else {
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd");
}
if(!jcmd.toFile().exists()) {
log.error("Could not find {}", jcmd);
throw new IOException("Could not find jcmd, we can't use it for detecting running instances");
}
return jcmd;
}
static final String[] PWSH_QUERY = { "powershell.exe", "-Command", "\"(Get-CimInstance Win32_Process -Filter \\\"Name = 'java.exe' OR Name = 'javaw.exe'\\\").Where({$_.CommandLine -like '*%s*'}).ProcessId\"" };
/**
* Leverage powershell.exe when run as SYSTEM to workaround https://github.com/qzind/tray/issues/1360
* TODO: Remove when jcmd is patched to work as SYSTEM account
*/
private static HashSet<Integer> findPidsPwsh() {
HashSet<Integer> foundPids = new HashSet<>();
for(String jarName : JAR_NAMES) {
String[] pwshQuery = PWSH_QUERY.clone();
int lastIndex = pwshQuery.length - 1;
// Format the last element to contain the jarName
pwshQuery[lastIndex] = String.format(pwshQuery[lastIndex], jarName);
String stdout = ShellUtilities.executeRaw(pwshQuery);
String[] lines = stdout.split("\\s*\\r?\\n");
for(String line : lines) {
if(line.trim().isEmpty()) {
// Don't try to process blank lines
continue;
}
int pid = parsePid(line);
if (pid >= 0) {
foundPids.add(pid);
} else {
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
}
}
}
return foundPids;
}
/**
* Use pgrep to fetch all PIDs to workaround https://github.com/openjdk/jdk/pull/25824
* TODO: Remove when jcmd is patched to work properly on macOS
*/
private static HashSet<Integer> findPidsPgrep() {
HashSet<Integer> foundPids = new HashSet<>();
for(String jarName : JAR_NAMES) {
String stdout = ShellUtilities.executeRaw("pgrep", "-f", jarName);
String[] lines = stdout.split("\\s*\\r?\\n");
for(String line : lines) {
if(line.trim().isEmpty()) {
// Don't try to process blank lines
continue;
}
int pid = parsePid(line);
if (pid >= 0) {
foundPids.add(pid);
} else {
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
}
}
}
return foundPids;
}
/**
* Uses jcmd to fetch all PIDs that match this product
*/
private static HashSet<Integer> findPidsJcmd() {
HashSet<Integer> foundPids = new HashSet<>();
String stdout;
String[] lines;
try {
stdout = ShellUtilities.executeRaw(getJcmdPath().toString(), "-l");
if(stdout == null) {
log.error("Error calling '{}' {}", getJcmdPath(), "-l");
return foundPids;
}
lines = stdout.split("\\r?\\n");
} catch(Exception e) {
log.error(e);
return foundPids;
}
for(String line : lines) {
if (line.trim().isEmpty()) {
// Don't try to process blank lines
continue;
}
// e.g. "35446 C:\Program Files\QZ Tray\qz-tray.jar"
String[] parts = line.split(" ", 2);
int pid = parsePid(parts);
if (pid >= 0) {
String args = parseArgs(parts);
if (args == null) {
log.warn("Found PID value '{}' but no args to match. Full line: '{}', Full output: '{}'", pid, line, stdout);
continue;
}
for(String jarName : JAR_NAMES) {
if (args.contains(jarName)) {
foundPids.add(pid);
break; // continue parent loop
}
}
} else {
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
}
}
return foundPids;
}
// Returns the second index of a String[], trimmed
private static String parseArgs(String[] input) {
if(input != null) {
if(input.length == 2) {
return input[1].trim();
}
}
return null;
}
// Parses an int value form the first index of a String[], returning -1 if something went wrong
private static int parsePid(String[] input) {
if(input != null) {
if(input.length == 2) {
return parsePid(input[0]);
}
}
return -1;
}
// Parses an int value form the provided string, returning -1 if something went wrong
private static int parsePid(String input) {
String pidString = input.trim();
if(StringUtils.isNumeric(pidString)) {
return Integer.parseInt(pidString);
}
return -1;
}
}

View File

@@ -0,0 +1,208 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer;
import com.sun.jna.platform.win32.*;
import mslinks.ShellLink;
import mslinks.ShellLinkException;
import mslinks.ShellLinkHelper;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import qz.ws.PrintSocketServer;
import javax.swing.*;
import static qz.common.Constants.*;
import static qz.installer.WindowsSpecialFolders.*;
import static com.sun.jna.platform.win32.WinReg.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class WindowsInstaller extends Installer {
protected static final Logger log = LogManager.getLogger(WindowsInstaller.class);
private String destination = getDefaultDestination();
private String destinationExe = getDefaultDestination() + File.separator + PROPS_FILE + ".exe";
public void setDestination(String destination) {
this.destination = destination;
this.destinationExe = destination + File.separator + PROPS_FILE + ".exe";
}
/**
* Cycles through registry keys removing legacy (<= 2.0) startup entries
*/
public Installer removeLegacyStartup() {
log.info("Removing legacy startup entries for all users matching " + ABOUT_TITLE);
for (String user : Advapi32Util.registryGetKeys(HKEY_USERS)) {
WindowsUtilities.deleteRegValue(HKEY_USERS, user.trim() + "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", ABOUT_TITLE);
}
try {
FileUtils.deleteQuietly(new File(STARTUP + File.separator + ABOUT_TITLE + ".lnk"));
} catch(Win32Exception ignore) {}
return this;
}
public Installer addAppLauncher() {
try {
// Delete old 2.0 launcher
FileUtils.deleteQuietly(new File(COMMON_START_MENU + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
Path loc = Paths.get(COMMON_START_MENU.toString(), "Programs", ABOUT_TITLE);
loc.toFile().mkdirs();
String lnk = loc + File.separator + ABOUT_TITLE + ".lnk";
String exe = destination + File.separator + PROPS_FILE+ ".exe";
log.info("Creating launcher \"{}\" -> \"{}\"", lnk, exe);
ShellLinkHelper.createLink(exe, lnk);
} catch(ShellLinkException | IOException | Win32Exception e) {
log.warn("Could not create launcher", e);
}
return this;
}
public Installer addStartupEntry() {
try {
String lnk = WindowsSpecialFolders.COMMON_STARTUP + File.separator + ABOUT_TITLE + ".lnk";
String exe = destination + File.separator + PROPS_FILE+ ".exe";
log.info("Creating startup entry \"{}\" -> \"{}\"", lnk, exe);
ShellLink link = ShellLinkHelper.createLink(exe, lnk).getLink();
link.setCMDArgs("--honorautostart"); // honors auto-start preferences
} catch(ShellLinkException | IOException | Win32Exception e) {
log.warn("Could not create startup launcher", e);
}
return this;
}
public Installer removeSystemSettings() {
// Cleanup registry
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + ABOUT_TITLE);
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\" + ABOUT_TITLE);
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, DATA_DIR);
// Chrome protocol handler
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
// Deprecated Chrome protocol handler
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLWhitelist", String.format("%s://*", DATA_DIR));
// Cleanup launchers
for(WindowsSpecialFolders folder : new WindowsSpecialFolders[] { START_MENU, COMMON_START_MENU, DESKTOP, PUBLIC_DESKTOP, COMMON_STARTUP, RECENT }) {
try {
new File(folder + File.separator + ABOUT_TITLE + ".lnk").delete();
// Since 2.1, start menus use subfolder
if (folder.equals(COMMON_START_MENU) || folder.equals(START_MENU)) {
FileUtils.deleteQuietly(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
FileUtils.deleteDirectory(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE));
}
} catch(InvalidPathException | IOException | Win32Exception ignore) {}
}
// Cleanup firewall rules
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
return this;
}
public Installer addSystemSettings() {
/**
* TODO: Upgrade JNA!
* 64-bit registry view is currently invoked by nsis (windows-installer.nsi.in) using SetRegView 64
* However, newer version of JNA offer direct WinNT.KEY_WOW64_64KEY registry support, safeguarding
* against direct calls to "java -jar qz-tray.jar install|keygen|etc", which will be needed moving forward
* for support and troubleshooting.
*/
// Mime-type support e.g. qz:launch
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "", String.format("URL:%s Protocol", ABOUT_TITLE));
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "URL Protocol", "");
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\DefaultIcon", DATA_DIR), "", String.format("\"%s\",1", destinationExe));
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\shell\\open\\command", DATA_DIR), "", String.format("\"%s\" \"%%1\"", destinationExe));
/// Uninstall info
String uninstallKey = String.format("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", ABOUT_TITLE);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, String.format("Software\\%s", ABOUT_TITLE), "", destination);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayName", String.format("%s %s", ABOUT_TITLE, VERSION));
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "Publisher", ABOUT_COMPANY);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "UninstallString", destination + File.separator + "uninstall.exe");
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayIcon", destinationExe);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "HelpLink", ABOUT_SUPPORT_URL );
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLUpdateInfo", ABOUT_DOWNLOAD_URL);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLInfoAbout", ABOUT_SUPPORT_URL);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayVersion", VERSION.toString());
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "EstimatedSize", FileUtils.sizeOfDirectoryAsBigInteger(new File(destination)).intValue() / 1024);
// Chrome protocol handler
WindowsUtilities.addNumberedRegValue(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
// Firewall rules
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "add", "rule", String.format("name=%s", ABOUT_TITLE),
"dir=in", "action=allow", "profile=any", String.format("localport=%s", websocketPorts.allPortsAsString()), "localip=any", "protocol=tcp");
return this;
}
@Override
public Installer addUserSettings() {
// Whitelist loopback for IE/Edge
if(ShellUtilities.execute("CheckNetIsolation.exe", "LoopbackExempt", "-a", "-n=Microsoft.MicrosoftEdge_8wekyb3d8bbwe")) {
log.warn("Could not whitelist loopback connections for IE, Edge");
}
try {
// Intranet settings; uncheck "include sites not listed in other zones"
String key = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones\\1";
String value = "Flags";
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
// remove value using bitwise XOR
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data ^ 16);
}
// Legacy Edge loopback support
key = "Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge\\ExperimentalFeatures";
value = "AllowLocalhostLoopback";
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
// remove value using bitwise OR
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data | 1);
}
} catch(Exception e) {
log.warn("An error occurred configuring the \"Local Intranet Zone\"; connections to \"localhost\" may fail", e);
}
return super.addUserSettings();
}
public static String getDefaultDestination() {
String path = System.getenv("ProgramW6432");
if (path == null || path.trim().isEmpty()) {
path = System.getenv("ProgramFiles");
}
return path + File.separator + ABOUT_TITLE;
}
public String getDestination() {
return destination;
}
public void spawn(List<String> args) throws Exception {
if(SystemUtilities.isAdmin()) {
log.warn("Spawning as user isn't implemented; starting process with elevation instead");
}
ShellUtilities.execute(args.toArray(new String[args.size()]));
}
}

View File

@@ -0,0 +1,97 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer;
import com.sun.jna.platform.win32.*;
import qz.utils.WindowsUtilities;
/**
* Windows XP-compatible special folder's wrapper for JNA
*
*/
public enum WindowsSpecialFolders {
ADMIN_TOOLS(ShlObj.CSIDL_ADMINTOOLS, KnownFolders.FOLDERID_AdminTools),
STARTUP_ALT(ShlObj.CSIDL_ALTSTARTUP, KnownFolders.FOLDERID_Startup),
ROAMING_APPDATA(ShlObj.CSIDL_APPDATA, KnownFolders.FOLDERID_RoamingAppData),
RECYCLING_BIN(ShlObj.CSIDL_BITBUCKET, KnownFolders.FOLDERID_RecycleBinFolder),
CD_BURNING(ShlObj.CSIDL_CDBURN_AREA, KnownFolders.FOLDERID_CDBurning),
COMMON_ADMIN_TOOLS(ShlObj.CSIDL_COMMON_ADMINTOOLS, KnownFolders.FOLDERID_CommonAdminTools),
COMMON_STARTUP_ALT(ShlObj.CSIDL_COMMON_ALTSTARTUP, KnownFolders.FOLDERID_CommonStartup),
PROGRAM_DATA(ShlObj.CSIDL_COMMON_APPDATA, KnownFolders.FOLDERID_ProgramData),
PUBLIC_DESKTOP(ShlObj.CSIDL_COMMON_DESKTOPDIRECTORY, KnownFolders.FOLDERID_PublicDesktop),
PUBLIC_DOCUMENTS(ShlObj.CSIDL_COMMON_DOCUMENTS, KnownFolders.FOLDERID_PublicDocuments),
COMMON_FAVORITES(ShlObj.CSIDL_COMMON_FAVORITES, KnownFolders.FOLDERID_Favorites),
COMMON_MUSIC(ShlObj.CSIDL_COMMON_MUSIC, KnownFolders.FOLDERID_PublicMusic),
COMMON_OEM_LINKS(ShlObj.CSIDL_COMMON_OEM_LINKS, KnownFolders.FOLDERID_CommonOEMLinks),
COMMON_PICTURES(ShlObj.CSIDL_COMMON_PICTURES, KnownFolders.FOLDERID_PublicPictures),
COMMON_PROGRAMS(ShlObj.CSIDL_COMMON_PROGRAMS, KnownFolders.FOLDERID_CommonPrograms),
COMMON_START_MENU(ShlObj.CSIDL_COMMON_STARTMENU, KnownFolders.FOLDERID_CommonStartMenu),
COMMON_STARTUP(ShlObj.CSIDL_COMMON_STARTUP, KnownFolders.FOLDERID_CommonStartup),
COMMON_TEMPLATES(ShlObj.CSIDL_COMMON_TEMPLATES, KnownFolders.FOLDERID_CommonTemplates),
COMMON_VIDEO(ShlObj.CSIDL_COMMON_VIDEO, KnownFolders.FOLDERID_PublicVideos),
COMPUTERS_NEAR_ME(ShlObj.CSIDL_COMPUTERSNEARME, KnownFolders.FOLDERID_NetworkFolder),
CONNECTIONS_FOLDER(ShlObj.CSIDL_CONNECTIONS, KnownFolders.FOLDERID_ConnectionsFolder),
CONTROL_PANEL(ShlObj.CSIDL_CONTROLS, KnownFolders.FOLDERID_ControlPanelFolder),
COOKIES(ShlObj.CSIDL_COOKIES, KnownFolders.FOLDERID_Cookies),
DESKTOP_VIRTUAL(ShlObj.CSIDL_DESKTOP, KnownFolders.FOLDERID_Desktop),
DESKTOP(ShlObj.CSIDL_DESKTOPDIRECTORY, KnownFolders.FOLDERID_Desktop),
COMPUTER_FOLDER(ShlObj.CSIDL_DRIVES, KnownFolders.FOLDERID_ComputerFolder),
FAVORITES(ShlObj.CSIDL_FAVORITES, KnownFolders.FOLDERID_Favorites),
FONTS(ShlObj.CSIDL_FONTS, KnownFolders.FOLDERID_Fonts),
HISTORY(ShlObj.CSIDL_HISTORY, KnownFolders.FOLDERID_History),
INTERNET_FOLDER(ShlObj.CSIDL_INTERNET, KnownFolders.FOLDERID_InternetFolder),
INTERNET_CACHE(ShlObj.CSIDL_INTERNET_CACHE, KnownFolders.FOLDERID_InternetCache),
LOCAL_APPDATA(ShlObj.CSIDL_LOCAL_APPDATA, KnownFolders.FOLDERID_LocalAppData),
MY_DOCUMENTS(ShlObj.CSIDL_MYDOCUMENTS, KnownFolders.FOLDERID_Documents),
MY_MUSIC(ShlObj.CSIDL_MYMUSIC, KnownFolders.FOLDERID_Music),
MY_PICTURES(ShlObj.CSIDL_MYPICTURES, KnownFolders.FOLDERID_Pictures),
MY_VIDEOS(ShlObj.CSIDL_MYVIDEO, KnownFolders.FOLDERID_Videos),
NETWORK_NEIGHBORHOOD(ShlObj.CSIDL_NETHOOD, KnownFolders.FOLDERID_NetHood),
NETWORK_FOLDER(ShlObj.CSIDL_NETWORK, KnownFolders.FOLDERID_NetworkFolder),
PERSONAL_FOLDDER(ShlObj.CSIDL_PERSONAL, KnownFolders.FOLDERID_Documents),
PRINTERS(ShlObj.CSIDL_PRINTERS, KnownFolders.FOLDERID_PrintersFolder),
PRINTING_NEIGHBORHOODD(ShlObj.CSIDL_PRINTHOOD, KnownFolders.FOLDERID_PrintHood),
PROFILE_FOLDER(ShlObj.CSIDL_PROFILE, KnownFolders.FOLDERID_Profile),
PROGRAM_FILES(ShlObj.CSIDL_PROGRAM_FILES, KnownFolders.FOLDERID_ProgramFiles),
PROGRAM_FILESX86(ShlObj.CSIDL_PROGRAM_FILESX86, KnownFolders.FOLDERID_ProgramFilesX86),
PROGRAM_FILES_COMMON(ShlObj.CSIDL_PROGRAM_FILES_COMMON, KnownFolders.FOLDERID_ProgramFilesCommon),
PROGRAM_FILES_COMMONX86(ShlObj.CSIDL_PROGRAM_FILES_COMMONX86, KnownFolders.FOLDERID_ProgramFilesCommonX86),
PROGRAMS(ShlObj.CSIDL_PROGRAMS, KnownFolders.FOLDERID_Programs),
RECENT(ShlObj.CSIDL_RECENT, KnownFolders.FOLDERID_Recent),
RESOURCES(ShlObj.CSIDL_RESOURCES, KnownFolders.FOLDERID_ResourceDir),
RESOURCES_LOCALIZED(ShlObj.CSIDL_RESOURCES_LOCALIZED, KnownFolders.FOLDERID_LocalizedResourcesDir),
SEND_TO(ShlObj.CSIDL_SENDTO, KnownFolders.FOLDERID_SendTo),
START_MENU(ShlObj.CSIDL_STARTMENU, KnownFolders.FOLDERID_StartMenu),
STARTUP(ShlObj.CSIDL_STARTUP, KnownFolders.FOLDERID_Startup),
SYSTEM(ShlObj.CSIDL_SYSTEM, KnownFolders.FOLDERID_System),
SYSTEMX86(ShlObj.CSIDL_SYSTEMX86, KnownFolders.FOLDERID_SystemX86),
TEMPLATES(ShlObj.CSIDL_TEMPLATES, KnownFolders.FOLDERID_Templates),
WINDOWS(ShlObj.CSIDL_WINDOWS, KnownFolders.FOLDERID_Windows);
private int csidl;
private Guid.GUID guid;
WindowsSpecialFolders(int csidl, Guid.GUID guid) {
this.csidl = csidl;
this.guid = guid;
}
public String getPath() {
if(WindowsUtilities.isWindowsXP()) {
return Shell32Util.getSpecialFolderPath(csidl, false);
}
return Shell32Util.getKnownFolderPath(guid);
}
@Override
public String toString() {
return getPath();
}
}

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=%ABOUT_TITLE%
Exec="%COMMAND%" %PARAM%
Path=%DESTINATION%
Icon=%DESTINATION%/%LINUX_ICON%
MimeType=application/x-qz;x-scheme-handler/qz;
Terminal=false

View File

@@ -0,0 +1,2 @@
# %ABOUT_TITLE% usb override settings
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>%PACKAGE_NAME%</string>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key><false/>
<key>AfterInitialDemand</key><false/>
</dict>
<key>RunAtLoad</key><true/>
<key>ProgramArguments</key>
<array>
<string>%COMMAND%</string>
<string>%PARAM%</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,147 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.util.Calendar;
import java.util.Locale;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import qz.common.Constants;
import qz.utils.SystemUtilities;
import static qz.installer.certificate.KeyPairWrapper.Type.*;
public class CertificateChainBuilder {
public static final String[] DEFAULT_HOSTNAMES = {"localhost", "localhost.qz.io" };
private static int KEY_SIZE = 2048;
public static int CA_CERT_AGE = 7305; // 20 years
public static int SSL_CERT_AGE = 825; // Per https://support.apple.com/HT210176
private String[] hostNames;
public CertificateChainBuilder(String ... hostNames) {
Security.addProvider(new BouncyCastleProvider());
if(hostNames.length > 0) {
this.hostNames = hostNames;
} else {
this.hostNames = DEFAULT_HOSTNAMES;
}
}
public KeyPairWrapper createCaCert() throws IOException, GeneralSecurityException, OperatorException {
KeyPair keyPair = createRsaKey();
X509v3CertificateBuilder builder = createX509Cert(keyPair, CA_CERT_AGE, hostNames);
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(1))
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign + KeyUsage.cRLSign))
.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
// Signing
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate());
X509CertificateHolder certHolder = builder.build(sign);
// Convert to java-friendly format
return new KeyPairWrapper(CA, keyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
}
public KeyPairWrapper createSslCert(KeyPairWrapper caKeyPairWrapper) throws IOException, GeneralSecurityException, OperatorException {
KeyPair sslKeyPair = createRsaKey();
X509v3CertificateBuilder builder = createX509Cert(sslKeyPair, SSL_CERT_AGE, hostNames);
JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils();
builder.addExtension(Extension.authorityKeyIdentifier, false, utils.createAuthorityKeyIdentifier(caKeyPairWrapper.getCert()))
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature + KeyUsage.keyEncipherment))
.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}))
.addExtension(Extension.subjectAlternativeName, false, buildSan(hostNames))
.addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(sslKeyPair.getPublic()));
// Signing
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caKeyPairWrapper.getKey());
X509CertificateHolder certHolder = builder.build(sign);
// Convert to java-friendly format
return new KeyPairWrapper(SSL, sslKeyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
}
private static KeyPair createRsaKey() throws GeneralSecurityException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
private static X509v3CertificateBuilder createX509Cert(KeyPair keyPair, int age, String ... hostNames) {
String cn = hostNames.length > 0? hostNames[0]:DEFAULT_HOSTNAMES[0];
X500Name name = new X500NameBuilder()
.addRDN(BCStyle.C, Constants.ABOUT_COUNTRY)
.addRDN(BCStyle.ST, Constants.ABOUT_STATE)
.addRDN(BCStyle.L, Constants.ABOUT_CITY)
.addRDN(BCStyle.O, Constants.ABOUT_COMPANY)
.addRDN(BCStyle.OU, Constants.ABOUT_COMPANY)
.addRDN(BCStyle.EmailAddress, Constants.ABOUT_EMAIL)
.addRDN(BCStyle.CN, cn)
.build();
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Calendar notBefore = Calendar.getInstance(Locale.ENGLISH);
Calendar notAfter = Calendar.getInstance(Locale.ENGLISH);
notBefore.add(Calendar.DAY_OF_YEAR, -1);
notAfter.add(Calendar.DAY_OF_YEAR, age - 1);
SystemUtilities.swapLocale();
X509v3CertificateBuilder x509builder = new JcaX509v3CertificateBuilder(name, serial, notBefore.getTime(), notAfter.getTime(), name, keyPair.getPublic());
SystemUtilities.restoreLocale();
return x509builder;
}
/**
* Builds subjectAlternativeName extension; iterates and detects IPv4 or hostname
*/
private static GeneralNames buildSan(String ... hostNames) {
GeneralName[] gn = new GeneralName[hostNames.length];
for (int i = 0; i < hostNames.length; i++) {
int gnType = isIp(hostNames[i]) ? GeneralName.iPAddress : GeneralName.dNSName;
gn[i] = new GeneralName(gnType, hostNames[i]);
}
return GeneralNames.getInstance(new DERSequence(gn));
}
private static boolean isIp(String ip) {
try {
String[] split = ip.split("\\.");
if (split.length != 4) return false;
for (int i = 0; i < 4; ++i) {
int p = Integer.parseInt(split[i]);
if (p > 255 || p < 0) return false;
}
return true;
} catch (Exception ignore) {}
return false;
}
}

View File

@@ -0,0 +1,478 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.OperatorException;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ArgValue;
import qz.utils.FileUtilities;
import qz.utils.MacUtilities;
import qz.utils.SystemUtilities;
import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.*;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import static qz.utils.FileUtilities.*;
import static qz.installer.certificate.KeyPairWrapper.Type.*;
/**
* Stores and maintains reading and writing of certificate related files
*/
public class CertificateManager {
static List<Path> SAVE_LOCATIONS = new ArrayList<>();
static {
// Workaround for JDK-8266929
// See also https://github.com/qzind/tray/issues/814
SystemUtilities.clearAlgorithms();
// Skip shared location if running from IDE or build directory
// Prevents corrupting the version installed per https://github.com/qzind/tray/issues/1200
if(SystemUtilities.isJar() && SystemUtilities.isInstalled()) {
// Skip install location if running from sandbox (must remain sealed)
if(!SystemUtilities.isMac() || !MacUtilities.isSandboxed()) {
SAVE_LOCATIONS.add(SystemUtilities.getJarParentPath());
}
SAVE_LOCATIONS.add(SHARED_DIR);
}
SAVE_LOCATIONS.add(USER_DIR);
}
private static final Logger log = LogManager.getLogger(CertificateManager.class);
public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12";
public static String DEFAULT_KEYSTORE_EXTENSION = ".p12";
public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt";
private static int DEFAULT_PASSWORD_BITS = 100;
private boolean needsInstall;
private SslContextFactory.Server sslContextFactory;
private KeyPairWrapper sslKeyPair;
private KeyPairWrapper caKeyPair;
private Properties properties;
private char[] password;
/**
* For internal certs
*/
public CertificateManager(boolean forceNew, String ... hostNames) throws IOException, GeneralSecurityException, OperatorException {
Security.addProvider(new BouncyCastleProvider());
sslKeyPair = new KeyPairWrapper(SSL);
caKeyPair = new KeyPairWrapper(CA);
if (!forceNew) {
// order is important: ssl, ca
properties = loadProperties(sslKeyPair, caKeyPair);
}
if(properties == null) {
log.warn("Warning, SSL properties won't be loaded from disk... we'll try to create them...");
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
caKeyPair = cb.createCaCert();
sslKeyPair = cb.createSslCert(caKeyPair);
// Create CA
properties = createKeyStore(CA)
.writeCert(CA)
.writeKeystore(null, CA);
// Create SSL
properties = createKeyStore(SSL)
.writeCert(SSL)
.writeKeystore(properties, SSL);
// Save properties
saveProperties();
}
}
/**
* For trusted PEM-formatted certs
*/
public CertificateManager(File trustedPemKey, File trustedPemCert) throws Exception {
Security.addProvider(new BouncyCastleProvider());
needsInstall = false;
sslKeyPair = new KeyPairWrapper(SSL);
// Assumes ssl/privkey.pem, ssl/fullchain.pem
properties = createTrustedKeystore(trustedPemKey, trustedPemCert)
.writeKeystore(properties, SSL);
// Save properties
saveProperties();
}
/**
* For trusted PKCS12-formatted certs
*/
public CertificateManager(File pkcs12File, char[] password) throws Exception {
Security.addProvider(new BouncyCastleProvider());
needsInstall = false;
sslKeyPair = new KeyPairWrapper(SSL);
// Assumes direct pkcs12 import
this.password = password;
sslKeyPair.init(pkcs12File, password);
// Save it back, but to a location we can find
properties = writeKeystore(null, SSL);
// Save properties
saveProperties();
}
public void renewCertChain(String ... hostNames) throws Exception {
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
sslKeyPair = cb.createSslCert(caKeyPair);
createKeyStore(SSL).writeKeystore(properties, SSL);
reloadSslContextFactory();
}
public KeyPairWrapper getSslKeyPair() {
return sslKeyPair;
}
public KeyPairWrapper getCaKeyPair() {
return caKeyPair;
}
public KeyPairWrapper getKeyPair(KeyPairWrapper.Type type) {
switch(type) {
case SSL:
return sslKeyPair;
case CA:
default:
return caKeyPair;
}
}
public KeyPairWrapper getKeyPair(String alias) {
for(KeyPairWrapper.Type type : KeyPairWrapper.Type.values()) {
if (KeyPairWrapper.getAlias(type).equalsIgnoreCase(alias)) {
return getKeyPair(type);
}
}
return getKeyPair(KeyPairWrapper.Type.CA);
}
public Properties getProperties() {
return properties;
}
private char[] getPassword() {
if (password == null) {
if(caKeyPair != null && caKeyPair.getPassword() != null) {
// Reuse existing
password = caKeyPair.getPassword();
} else {
// Create new
BigInteger bi = new BigInteger(DEFAULT_PASSWORD_BITS, new SecureRandom());
password = bi.toString(16).toCharArray();
log.info("Created a random {} bit password: {}", DEFAULT_PASSWORD_BITS, new String(password));
}
}
return password;
}
public SslContextFactory.Server configureSslContextFactory() {
sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
return sslContextFactory;
}
public void reloadSslContextFactory() throws Exception {
if(isSslActive()) {
sslContextFactory.reload(sslContextFactory -> {
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
});
} else {
log.warn("SSL isn't active, can't reload");
}
}
public boolean isSslActive() {
return sslContextFactory != null;
}
public boolean needsInstall() {
return needsInstall;
}
public CertificateManager createKeyStore(KeyPairWrapper.Type type) throws IOException, GeneralSecurityException {
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
KeyStore keyStore = KeyStore.getInstance(DEFAULT_KEYSTORE_FORMAT);
keyStore.load(null, password);
List<X509Certificate> chain = new ArrayList<>();
chain.add(keyPair.getCert());
// Add ca to ssl cert chain
if (keyPair.getType() == SSL) {
chain.add(caKeyPair.getCert());
}
keyStore.setEntry(caKeyPair.getAlias(), new KeyStore.TrustedCertificateEntry(caKeyPair.getCert()), null);
keyStore.setKeyEntry(keyPair.getAlias(), keyPair.getKey(), getPassword(), chain.toArray(new X509Certificate[chain.size()]));
keyPair.init(keyStore, getPassword());
return this;
}
public CertificateManager createTrustedKeystore(File p12Store, String password) throws Exception {
sslKeyPair = new KeyPairWrapper(SSL);
sslKeyPair.init(p12Store, password.toCharArray());
return this;
}
public CertificateManager createTrustedKeystore(File pemKey, File pemCert) throws Exception {
sslKeyPair = new KeyPairWrapper(SSL);
// Private Key
PEMParser pem = new PEMParser(new FileReader(pemKey));
Object parsedObject = pem.readObject();
PrivateKeyInfo privateKeyInfo = parsedObject instanceof PEMKeyPair ? ((PEMKeyPair)parsedObject).getPrivateKeyInfo() : (PrivateKeyInfo)parsedObject;
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
KeyFactory factory = KeyFactory.getInstance("RSA");
PrivateKey key = factory.generatePrivate(privateKeySpec);
List<X509Certificate> certs = new ArrayList<>();
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
if(certHolder != null) {
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
}
// Certificate
pem = new PEMParser(new FileReader(pemCert));
while((certHolder = (X509CertificateHolder)pem.readObject()) != null) {
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
}
// Keystore
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null);
for (int i = 0; i < certs.size(); i++) {
ks.setCertificateEntry(sslKeyPair.getAlias() + "_" + i, certs.get(i));
}
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null);
keyStore.setKeyEntry(sslKeyPair.getAlias(), key, getPassword(), certs.toArray(new X509Certificate[certs.size()]));
sslKeyPair.init(keyStore, getPassword());
return this;
}
public static void writeCert(X509Certificate data, File dest) throws IOException {
// PEMWriter doesn't always clear the file, explicitly delete it, see issue #796
if(dest.exists()) {
dest.delete();
}
JcaMiscPEMGenerator cert = new JcaMiscPEMGenerator(data);
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(dest.toPath(), StandardOpenOption.CREATE)));
writer.writeObject(cert.generate());
writer.close();
FileUtilities.inheritParentPermissions(dest.toPath());
log.info("Wrote Cert: \"{}\"", dest);
}
public CertificateManager writeCert(KeyPairWrapper.Type type) throws IOException {
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
File certFile = new File(getWritableLocation("ssl"), keyPair.getAlias() + DEFAULT_CERTIFICATE_EXTENSION);
writeCert(keyPair.getCert(), certFile);
FileUtilities.inheritParentPermissions(certFile.toPath());
if(keyPair.getType() == CA) {
needsInstall = true;
}
return this;
}
public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) throws GeneralSecurityException, IOException {
File sslDir = getWritableLocation("ssl");
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
File keyFile = new File(sslDir, keyPair.getAlias() + DEFAULT_KEYSTORE_EXTENSION);
keyPair.getKeyStore().store(Files.newOutputStream(keyFile.toPath(), StandardOpenOption.CREATE), getPassword());
FileUtilities.inheritParentPermissions(keyFile.toPath());
log.info("Wrote {} Key: \"{}\"", DEFAULT_KEYSTORE_FORMAT, keyFile);
if (props == null) {
props = new Properties();
}
props.putIfAbsent(String.format("%s.keystore", keyPair.propsPrefix()), keyFile.toString());
props.putIfAbsent(String.format("%s.storepass", keyPair.propsPrefix()), new String(getPassword()));
props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias());
if (keyPair.getType() == SSL) {
props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), ArgValue.SECURITY_WSS_HOST.getDefaultVal());
}
return props;
}
public static File getWritableLocation(String ... suffixes) throws IOException {
// Get an array of preferred directories
ArrayList<Path> locs = new ArrayList<>();
if (suffixes.length == 0) {
locs.addAll(SAVE_LOCATIONS);
// Last, fallback on a directory we won't ever see again :/
locs.add(TEMP_DIR);
} else {
// Same as above, but with suffixes added (usually "ssl"), skipping the install location
for(Path saveLocation : SAVE_LOCATIONS) {
if(!saveLocation.equals(SystemUtilities.getJarParentPath())) {
locs.add(Paths.get(saveLocation.toString(), suffixes));
}
}
// Last, fallback on a directory we won't ever see again :/
locs.add(Paths.get(TEMP_DIR.toString(), suffixes));
}
// Find a suitable write location
File path;
for(Path loc : locs) {
if (loc == null) continue;
boolean isPreferred = locs.indexOf(loc) == 0;
path = loc.toFile();
path.mkdirs();
if (path.canWrite()) {
log.debug("Writing to {}", loc);
if(!isPreferred) {
log.warn("Warning, {} isn't the preferred write location, but we'll use it anyway", loc);
}
return path;
} else {
log.debug("Can't write to {}, trying the next...", loc);
}
}
throw new IOException("Can't find a suitable write location. SSL will fail.");
}
public static Properties loadProperties(KeyPairWrapper... keyPairs) {
log.info("Try to find SSL properties file...");
Properties props = null;
for(Path loc : SAVE_LOCATIONS) {
if (loc == null) continue;
try {
for(KeyPairWrapper keyPair : keyPairs) {
props = loadKeyPair(keyPair, loc, props);
}
// We've loaded without Exception, return
log.info("Found {}/{}.properties", loc, Constants.PROPS_FILE);
return props;
} catch(Exception ignore) {
log.warn("Properties couldn't be loaded at {}, trying fallback...", loc, ignore);
}
}
log.info("Could not get SSL properties from file.");
return null;
}
public static Properties loadKeyPair(KeyPairWrapper keyPair, Path parent, Properties existing) throws Exception {
Properties props;
if (existing == null) {
FileInputStream fis = null;
try {
props = new Properties();
props.load(fis = new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties")));
} finally {
if(fis != null) fis.close();
}
} else {
props = existing;
}
String ks = props.getProperty(String.format("%s.keystore", keyPair.propsPrefix()));
String pw = props.getProperty(String.format("%s.storepass", keyPair.propsPrefix()), "");
if(ks == null || ks.trim().isEmpty()) {
if(keyPair.getType() == SSL) {
throw new IOException("Missing wss.keystore entry");
} else {
// CA is only needed for internal certs, return
return props;
}
}
File ksFile = Paths.get(ks).isAbsolute()? new File(ks):new File(parent.toFile(), ks);
if (ksFile.exists()) {
keyPair.init(ksFile, pw.toCharArray());
return props;
}
return null;
}
private void saveProperties() throws IOException {
File propsFile = new File(getWritableLocation(), Constants.PROPS_FILE + ".properties");
Installer.persistProperties(propsFile, properties); // checks for props from previous install
properties.store(new FileOutputStream(propsFile), null);
FileUtilities.inheritParentPermissions(propsFile.toPath());
log.info("Successfully created SSL properties file: {}", propsFile);
}
public static boolean emailMatches(X509Certificate cert) {
return emailMatches(cert, false);
}
public static boolean emailMatches(X509Certificate cert, boolean quiet) {
try {
X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
RDN[] emailNames = x500name.getRDNs(BCStyle.E);
for(RDN emailName : emailNames) {
AttributeTypeAndValue first = emailName.getFirst();
if (first != null && first.getValue() != null && Constants.ABOUT_EMAIL.equals(first.getValue().toString())) {
if(!quiet) {
log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, ExpiryTask.CertProvider.INTERNAL);
}
return true;
}
}
}
catch(Exception ignore) {}
if(!quiet) {
log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL);
}
return false;
}
}

View File

@@ -0,0 +1,295 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import static qz.utils.FileUtilities.*;
public class ExpiryTask extends TimerTask {
private static final Logger log = LogManager.getLogger(CertificateManager.class);
public static final int DEFAULT_INITIAL_DELAY = 60 * 1000; // 1 minute
public static final int DEFAULT_CHECK_FREQUENCY = 3600 * 1000; // 1 hour
private static final int DEFAULT_GRACE_PERIOD_DAYS = 5;
private enum ExpiryState {VALID, EXPIRING, EXPIRED, MANAGED}
public enum CertProvider {
INTERNAL(Constants.ABOUT_COMPANY + ".*"),
LETS_ENCRYPT("Let's Encrypt.*"),
CA_CERT_ORG("CA Cert Signing.*"),
UNKNOWN;
String[] patterns;
CertProvider(String ... regexPattern) {
this.patterns = regexPattern;
}
}
private Timer timer;
private CertificateManager certificateManager;
private String[] hostNames;
private CertProvider certProvider;
public ExpiryTask(CertificateManager certificateManager) {
super();
this.certificateManager = certificateManager;
this.hostNames = parseHostNames();
this.certProvider = findCertProvider();
}
@Override
public void run() {
// Check for expiration
ExpiryState state = getExpiry(certificateManager.getSslKeyPair().getCert());
switch(state) {
case EXPIRING:
case EXPIRED:
log.info("Certificate ExpiryState {}, renewing/reloading...", state);
switch(certProvider) {
case INTERNAL:
if(renewInternalCert()) {
getExpiry();
}
break;
case CA_CERT_ORG:
case LETS_ENCRYPT:
if(renewExternalCert(certProvider)) {
getExpiry();
}
break;
case UNKNOWN:
default:
log.warn("Certificate can't be renewed/reloaded; ExpiryState: {}, CertProvider: {}", state, certProvider);
}
case VALID:
default:
}
}
public boolean renewInternalCert() {
try {
log.info("Requesting a new SSL certificate from {} ...", certificateManager.getCaKeyPair().getAlias());
certificateManager.renewCertChain(hostNames);
log.info("New SSL certificate created. Reloading SslContextFactory...");
certificateManager.reloadSslContextFactory();
log.info("Reloaded SSL successfully.");
return true;
}
catch(Exception e) {
log.error("Could not reload SSL certificate", e);
}
return false;
}
public ExpiryState getExpiry() {
return getExpiry(certificateManager.getSslKeyPair().getCert());
}
/**
* Returns true if the SSL certificate is generated by QZ Tray and expires inside the GRACE_PERIOD.
* GRACE_PERIOD is preferred for scheduling the renewals in advance, such as non-peak hours
*/
public static ExpiryState getExpiry(X509Certificate cert) {
// Invalid
if (cert == null) {
log.error("Can't check for expiration, certificate is missing.");
return ExpiryState.EXPIRED;
}
Date expireDate = cert.getNotAfter();
Calendar now = Calendar.getInstance(Locale.ENGLISH);
Calendar expires = Calendar.getInstance(Locale.ENGLISH);
expires.setTime(expireDate);
// Expired
if (now.after(expires)) {
log.info("SSL certificate has expired {}. It must be renewed immediately.", SystemUtilities.toISO(expireDate));
return ExpiryState.EXPIRED;
}
// Expiring
expires.add(Calendar.DAY_OF_YEAR, -DEFAULT_GRACE_PERIOD_DAYS);
if (now.after(expires)) {
log.info("SSL certificate will expire in less than {} days: {}", DEFAULT_GRACE_PERIOD_DAYS, SystemUtilities.toISO(expireDate));
return ExpiryState.EXPIRING;
}
// Valid
int days = (int)Math.round((expireDate.getTime() - new Date().getTime()) / (double)86400000);
log.info("SSL certificate is still valid for {} more days: {}. We'll make a new one automatically when needed.", days, SystemUtilities.toISO(expireDate));
return ExpiryState.VALID;
}
public void schedule() {
schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY);
}
public void schedule(int delayMillis, int freqMillis) {
if(timer != null) {
timer.cancel();
timer.purge();
}
timer = new Timer();
timer.scheduleAtFixedRate(this, delayMillis, freqMillis);
}
public String[] parseHostNames() {
return parseHostNames(certificateManager.getSslKeyPair().getCert());
}
public CertProvider findCertProvider() {
return findCertProvider(certificateManager.getSslKeyPair().getCert());
}
public static CertProvider findCertProvider(X509Certificate cert) {
// Internal certs use CN=localhost, trust email instead
if (CertificateManager.emailMatches(cert)) {
return CertProvider.INTERNAL;
}
String providerDN;
// check registered patterns to classify certificate
if(cert.getIssuerDN() != null && (providerDN = cert.getIssuerDN().getName()) != null) {
String cn = null;
try {
// parse issuer's DN
LdapName ldapName = new LdapName(providerDN);
for(Rdn rdn : ldapName.getRdns()) {
if(rdn.getType().equalsIgnoreCase("CN")) {
cn = (String)rdn.getValue();
break;
}
}
// compare cn to our pattern
if(cn != null) {
for(CertProvider provider : CertProvider.values()) {
for(String pattern : provider.patterns) {
if (cn.matches(pattern)) {
log.warn("Cert issuer detected as {}", provider.name());
return provider;
}
}
}
}
} catch(InvalidNameException ignore) {}
}
log.warn("A valid issuer couldn't be found, we won't know how to renew this cert when it expires");
return CertProvider.UNKNOWN;
}
public static String[] parseHostNames(X509Certificate cert) {
// Cache the SAN hosts for recreation
List<String> hostNameList = new ArrayList<>();
try {
Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
if (altNames != null) {
for(List<?> altName : altNames) {
if(altName.size()< 1) continue;
switch((Integer)altName.get(0)) {
case GeneralName.dNSName:
case GeneralName.iPAddress:
Object data = altName.get(1);
if (data instanceof String) {
hostNameList.add(((String)data));
}
break;
default:
}
}
} else {
log.error("getSubjectAlternativeNames is null?");
}
log.debug("Parsed hostNames: {}", String.join(", ", hostNameList));
} catch(CertificateException e) {
log.warn("Can't parse hostNames from this cert. Cert renewals will contain default values instead");
}
return hostNameList.toArray(new String[hostNameList.size()]);
}
public boolean renewExternalCert(CertProvider externalProvider) {
switch(externalProvider) {
case LETS_ENCRYPT:
return renewLetsEncryptCert(externalProvider);
case CA_CERT_ORG:
default:
log.error("Cert renewal for {} is not implemented", externalProvider);
}
return false;
}
private boolean renewLetsEncryptCert(CertProvider externalProvider) {
try {
File storagePath = CertificateManager.getWritableLocation("ssl");
// cerbot is much simpler than acme, let's use it
Path root = Paths.get(SHARED_DIR.toString(), "letsencrypt", "config");
log.info("Attempting to renew {}. Assuming certs are installed in {}...", externalProvider, root);
List<String> cmds = new ArrayList(Arrays.asList("certbot", "--force-renewal", "certonly"));
cmds.add("--standalone");
cmds.add("--config-dir");
String config = Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "config").toString();
cmds.add(config);
cmds.add("--logs-dir");
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "logs").toString());
cmds.add("--work-dir");
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt").toString());
// append dns names
for(String hostName : hostNames) {
cmds.add("-d");
cmds.add(hostName);
}
if (ShellUtilities.execute(cmds.toArray(new String[cmds.size()]))) {
// Assume the cert is stored in a folder called "letsencrypt/config/live/<domain>"
Path keyPath = Paths.get(config, "live", hostNames[0], "privkey.pem");
Path certPath = Paths.get(config, "live", hostNames[0], "fullchain.pem"); // fullchain required
certificateManager.createTrustedKeystore(keyPath.toFile(), certPath.toFile());
log.info("Files imported, converted and saved. Reloading SslContextFactory...");
certificateManager.reloadSslContextFactory();
log.info("Reloaded SSL successfully.");
return true;
} else {
log.warn("Something went wrong renewing the LetsEncrypt certificate. Please run the certbot command manually to learn more.");
}
} catch(Exception e) {
log.error("Error renewing/reloading LetsEncrypt cert", e);
}
return false;
}
}

View File

@@ -0,0 +1,130 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import qz.common.Constants;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
/**
* Wrap handling of X509Certificate, PrivateKey and KeyStore conversion
*/
public class KeyPairWrapper {
public enum Type {CA, SSL}
private Type type;
private PrivateKey key;
private char[] password;
private X509Certificate cert;
private KeyStore keyStore; // for SSL
public KeyPairWrapper(Type type) {
this.type = type;
}
public KeyPairWrapper(Type type, KeyPair keyPair, X509Certificate cert) {
this.type = type;
this.key = keyPair.getPrivate();
this.cert = cert;
}
/**
* Load from disk
*/
public void init(File keyFile, char[] password) throws IOException, GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance(keyFile.getName().endsWith(".jks") ? "JKS" : "PKCS12");
keyStore.load(new FileInputStream(keyFile), password);
init(keyStore, password);
}
/**
* Load from memory
*/
public void init(KeyStore keyStore, char[] password) throws GeneralSecurityException {
this.keyStore = keyStore;
KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(password);
KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(getAlias(), param);
// the entry we assume is always wrong for pkcs12 imports, search for it instead
if(entry == null) {
Enumeration<String> enumerator = keyStore.aliases();
while(enumerator.hasMoreElements()) {
String alias = enumerator.nextElement();
if(keyStore.isKeyEntry(alias)) {
this.password = password;
this.key = ((KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, param)).getPrivateKey();
this.cert = (X509Certificate)keyStore.getCertificate(alias);
return;
}
}
throw new GeneralSecurityException("Could not initialize the KeyStore for internal use");
}
this.password = password;
this.key = entry.getPrivateKey();
this.cert = (X509Certificate)keyStore.getCertificate(getAlias());
}
public X509Certificate getCert() {
return cert;
}
public PrivateKey getKey() {
return key;
}
public String getPasswordString() {
return new String(password);
}
public char[] getPassword() {
return password;
}
public static String getAlias(Type type) {
switch(type) {
case SSL:
return Constants.PROPS_FILE; // "qz-tray"
case CA:
default:
return "root-ca";
}
}
public String getAlias() {
return getAlias(getType());
}
public String propsPrefix() {
switch(type) {
case SSL:
return "wss";
case CA:
default:
return "ca";
}
}
public Type getType() {
return type;
}
public KeyStore getKeyStore() {
return keyStore;
}
}

View File

@@ -0,0 +1,365 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.util.encoders.Base64;
import qz.auth.X509Constants;
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ByteUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.UnixUtilities;
import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import static qz.installer.Installer.PrivilegeLevel.*;
/**
* @author Tres Finocchiaro
*/
public class LinuxCertificateInstaller extends NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(LinuxCertificateInstaller.class);
private static final String CA_CERTIFICATES = "/usr/local/share/ca-certificates/";
private static final String CA_CERTIFICATE_NAME = Constants.PROPS_FILE + "-root.crt"; // e.g. qz-tray-root.crt
private static final String PK11_KIT_ID = "pkcs11:id=";
private static String[] NSSDB_URLS = {
// Conventional cert store
"sql:" + System.getenv("HOME") + "/.pki/nssdb/",
// Snap-specific cert stores
"sql:" + System.getenv("HOME") + "/snap/chromium/current/.pki/nssdb/",
"sql:" + System.getenv("HOME") + "/snap/brave/current/.pki/nssdb/",
"sql:" + System.getenv("HOME") + "/snap/opera/current/.pki/nssdb/",
"sql:" + System.getenv("HOME") + "/snap/opera-beta/current/.pki/nssdb/"
};
private Installer.PrivilegeLevel certType;
public LinuxCertificateInstaller(Installer.PrivilegeLevel certType) {
setInstallType(certType);
findCertutil();
}
public Installer.PrivilegeLevel getInstallType() {
return certType;
}
public void setInstallType(Installer.PrivilegeLevel certType) {
this.certType = certType;
if (this.certType == SYSTEM) {
log.warn("Command \"certutil\" (required for certain browsers) needs to run as USER. We'll try again on launch.");
}
}
public boolean remove(List<String> idList) {
boolean success = true;
if(certType == SYSTEM) {
boolean first = distrustUsingUpdateCaCertificates(idList);
boolean second = distrustUsingTrustAnchor(idList);
success = first || second;
} else {
for(String nickname : idList) {
for(String nssdb : NSSDB_URLS) {
success = success && ShellUtilities.execute("certutil", "-d", nssdb, "-D", "-n", nickname);
}
}
}
return success;
}
public List<String> find() {
ArrayList<String> nicknames = new ArrayList<>();
if(certType == SYSTEM) {
nicknames = findUsingTrustAnchor();
nicknames.addAll(findUsingUsingUpdateCaCert());
} else {
try {
for(String nssdb : NSSDB_URLS) {
Process p = Runtime.getRuntime().exec(new String[] {"certutil", "-d", nssdb, "-L"});
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while((line = in.readLine()) != null) {
if (line.startsWith(Constants.ABOUT_COMPANY + " ")) {
nicknames.add(Constants.ABOUT_COMPANY);
break; // Stop reading input; nicknames can't appear more than once
}
}
in.close();
}
}
catch(IOException e) {
log.warn("Could not get certificate nicknames", e);
}
}
return nicknames;
}
public boolean verify(File ignore) { return true; } // no easy way to validate a cert, assume it's installed
public boolean add(File certFile) {
boolean success = true;
if(certType == SYSTEM) {
// Attempt two common methods for installing the SSL certificate
File systemCertFile;
boolean first = (systemCertFile = trustUsingUpdateCaCertificates(certFile)) != null;
boolean second = trustUsingTrustAnchor(systemCertFile, certFile);
success = first || second;
} else if(certType == USER) {
// Install certificate to local profile using "certutil"
for(String nssdb : NSSDB_URLS) {
String[] parts = nssdb.split(":", 2);
if (parts.length > 1) {
File folder = new File(parts[1]);
// If .pki/nssdb doesn't exist yet, don't create it! Per https://github.com/qzind/tray/issues/1003
if(folder.exists() && folder.isDirectory()) {
if (!ShellUtilities.execute("certutil", "-d", nssdb, "-A", "-t", "TC", "-n", Constants.ABOUT_COMPANY, "-i", certFile.getPath())) {
log.warn("Something went wrong creating {}. HTTPS will fail on certain browsers which depend on it.", nssdb);
success = false;
}
}
}
}
}
return success;
}
private boolean findCertutil() {
boolean installed = ShellUtilities.execute("which", "certutil");
if (!installed) {
if (certType == SYSTEM && promptCertutil()) {
if(UnixUtilities.isUbuntu() || UnixUtilities.isDebian()) {
installed = ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools");
} else if(UnixUtilities.isFedora()) {
installed = ShellUtilities.execute("dnf", "install", "-y", "nss-tools");
}
}
}
if(!installed) {
log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on certain browsers which depend on it.");
}
return installed;
}
private boolean promptCertutil() {
// Assume silent or headless installs want certutil
if(Installer.IS_SILENT || GraphicsEnvironment.isHeadless()) {
return true;
}
try {
SystemUtilities.setSystemLookAndFeel(true);
return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(null, "A critical component, \"certutil\" wasn't found. Attempt to fetch it now?");
} catch(Throwable ignore) {}
return true;
}
/**
* Common technique for installing system-wide certificates on Debian-based systems (Ubuntu, etc.)
*
* This technique is only known to work for select browsers, such as Epiphany. Browsers such as
* Firefox and Chromium require different techniques.
*
* @return Full path to the destination file if successful, otherwise <code>null</code>
*/
private File trustUsingUpdateCaCertificates(File certFile) {
if(hasUpdateCaCertificatesCommand()) {
File destFile = new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME);
log.debug("Copying SYSTEM SSL certificate {} to {}", certFile.getPath(), destFile.getPath());
try {
if (new File(CA_CERTIFICATES).isDirectory()) {
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
FileUtils.copyFile(certFile, destFile, false);
if (destFile.isFile()) {
// Attempt "update-ca-certificates" (Debian)
if (!ShellUtilities.execute("update-ca-certificates")) {
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
} else {
return destFile;
}
}
} else {
log.warn("{} is not a valid directory, skipping", CA_CERTIFICATES);
}
}
catch(IOException e) {
log.warn("Error copying SYSTEM SSL certificate file", e);
}
} else {
log.warn("Skipping SYSTEM SSL certificate install using \"update-ca-certificates\", command missing or invalid");
}
return null;
}
/**
* Common technique for installing system-wide certificates on Fedora-based systems
*
* Uses first existing non-null file provided
*/
private boolean trustUsingTrustAnchor(File ... certFiles) {
if (hasTrustAnchorCommand()) {
for(File certFile : certFiles) {
if (certFile == null || !certFile.exists()) {
continue;
}
// Install certificate to system using "trust anchor" (Fedora)
if (ShellUtilities.execute("trust", "anchor", "--store", certFile.getPath())) {
return true;
} else {
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
}
}
} else {
log.warn("Skipping SYSTEM SSL certificate install using \"trust anchor\", command missing or invalid");
}
return false;
}
private boolean distrustUsingUpdateCaCertificates(List<String> paths) {
if(hasUpdateCaCertificatesCommand()) {
boolean deleted = false;
for(String path : paths) {
// Process files only; not "trust anchor" URIs
if(!path.startsWith(PK11_KIT_ID)) {
File certFile = new File(path);
if (certFile.isFile() && certFile.delete()) {
deleted = true;
} else {
log.warn("SYSTEM SSL certificate {} does not exist, skipping", certFile.getPath());
}
}
}
// Attempt "update-ca-certificates" (Debian)
if(deleted) {
if (ShellUtilities.execute("update-ca-certificates")) {
return true;
} else {
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
}
}
} else {
log.warn("Skipping SYSTEM SSL certificate removal using \"update-ca-certificates\", command missing or invalid");
}
return false;
}
private boolean distrustUsingTrustAnchor(List<String> idList) {
if(hasTrustAnchorCommand()) {
for(String id : idList) {
// only remove by id
if (id.startsWith(PK11_KIT_ID) && !ShellUtilities.execute("trust", "anchor", "--remove", id)) {
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
}
}
} else {
log.warn("Skipping SYSTEM SSL certificate removal using \"trust anchor\", command missing or invalid");
}
return false;
}
/**
* Check for the presence of a QZ certificate in known locations (e.g. /usr/local/share/ca-certificates/
* and return the path if found
*/
private ArrayList<String> findUsingUsingUpdateCaCert() {
ArrayList<String> found = new ArrayList<>();
File[] systemCertFiles = { new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME) };
for(File file : systemCertFiles) {
if(file.isFile()) {
found.add(file.getPath());
}
}
return found;
}
/**
* Find QZ installed certificates in the "trust anchor" by searching by email.
*
* The "trust" utility identifies certificates as URIs:
* Example:
* pkcs11:id=%7C%5D%02%84%13%D4%CC%8A%9B%81%CE%17%1C%2E%29%1E%9C%48%63%42;type=cert
* ... which is an encoded version of the cert's SubjectKeyIdentifier field
* To identify a match:
* 1. Extract all trusted certificates and look for a familiar email address
* 2. If found, construct and store a "trust" compatible URI as the nickname
*/
private ArrayList<String> findUsingTrustAnchor() {
ArrayList<String> uris = new ArrayList<>();
File tempFile = null;
try {
// Temporary location for system certificates
tempFile = File.createTempFile("trust-extract-for-qz-", ".pem");
// Delete before use: "trust extract" requires an empty file
tempFile.delete();
if(ShellUtilities.execute("trust", "extract", "--format", "pem-bundle", tempFile.getPath())) {
BufferedReader reader = new BufferedReader(new FileReader(tempFile));
String line;
StringBuilder base64 = new StringBuilder();
while ((line = reader.readLine()) != null) {
if(line.startsWith(X509Constants.BEGIN_CERT)) {
// Beginning of a new certificate
base64.setLength(0);
} else if(line.startsWith(X509Constants.END_CERT)) {
// End of the existing certificate
byte[] certBytes = Base64.decode(base64.toString());
CertificateFactory factory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes));
if(CertificateManager.emailMatches(cert, true)) {
byte[] extensionValue = cert.getExtensionValue(Extension.subjectKeyIdentifier.getId());
byte[] octets = DEROctetString.getInstance(extensionValue).getOctets();
SubjectKeyIdentifier subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets);
byte[] keyIdentifier = subjectKeyIdentifier.getKeyIdentifier();
String hex = ByteUtilities.bytesToHex(keyIdentifier, true);
String uri = PK11_KIT_ID + hex.replaceAll("(.{2})", "%$1") + ";type=cert";
log.info("Found matching cert: {}", uri);
uris.add(uri);
}
} else {
base64.append(line);
}
}
reader.close();
}
} catch(IOException | CertificateException e) {
log.warn("An error occurred finding preexisting \"trust anchor\" certificates", e);
} finally {
if(tempFile != null && !tempFile.delete()) {
tempFile.deleteOnExit();
}
}
return uris;
}
private boolean hasUpdateCaCertificatesCommand() {
return ShellUtilities.execute("which", "update-ca-certificates");
}
private boolean hasTrustAnchorCommand() {
return ShellUtilities.execute("trust", "anchor", "--help");
}
}

View File

@@ -0,0 +1,91 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ShellUtilities;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class MacCertificateInstaller extends NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(MacCertificateInstaller.class);
public static final String USER_STORE = System.getProperty("user.home") + "/Library/Keychains/login.keychain"; // aka login.keychain-db
public static final String SYSTEM_STORE = "/Library/Keychains/System.keychain";
private String certStore;
public MacCertificateInstaller(Installer.PrivilegeLevel certType) {
setInstallType(certType);
}
public boolean add(File certFile) {
if (certStore.equals(USER_STORE)) {
// This will prompt the user
return ShellUtilities.execute("security", "add-trusted-cert", "-r", "trustRoot", "-k", certStore, certFile.getPath());
} else {
return ShellUtilities.execute("security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", certStore, certFile.getPath());
}
}
public boolean remove(List<String> idList) {
boolean success = true;
for (String certId : idList) {
success = success && ShellUtilities.execute("security", "delete-certificate", "-Z", certId, certStore);
}
return success;
}
public List<String> find() {
ArrayList<String> hashList = new ArrayList<>();
try {
Process p = Runtime.getRuntime().exec(new String[] {"security", "find-certificate", "-a", "-e", Constants.ABOUT_EMAIL, "-Z", certStore});
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
if (line.contains("SHA-1") && line.contains(":")) {
hashList.add(line.split(":", 2)[1].trim());
}
}
in.close();
} catch(IOException e) {
log.warn("Could not get certificate list", e);
}
return hashList;
}
public boolean verify(File certFile) {
return ShellUtilities.execute( "security", "verify-cert", "-c", certFile.getPath());
}
public void setInstallType(Installer.PrivilegeLevel type) {
if (type == Installer.PrivilegeLevel.USER) {
certStore = USER_STORE;
} else {
certStore = SYSTEM_STORE;
}
}
public Installer.PrivilegeLevel getInstallType() {
if (certStore == USER_STORE) {
return Installer.PrivilegeLevel.USER;
} else {
return Installer.PrivilegeLevel.SYSTEM;
}
}
}

View File

@@ -0,0 +1,105 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.installer.Installer;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.security.cert.X509Certificate;
import java.util.List;
public abstract class NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(NativeCertificateInstaller.class);
protected static NativeCertificateInstaller instance;
public static NativeCertificateInstaller getInstance() {
return getInstance(SystemUtilities.isAdmin() ? Installer.PrivilegeLevel.SYSTEM : Installer.PrivilegeLevel.USER);
}
public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) {
if (instance == null) {
switch(SystemUtilities.getOs()) {
case WINDOWS:
instance = new WindowsCertificateInstaller(type);
break;
case MAC:
instance = new MacCertificateInstaller(type);
break;
case LINUX:
default:
instance = new LinuxCertificateInstaller(type);
}
}
return instance;
}
/**
* Install a certificate from memory
*/
public boolean install(X509Certificate cert) {
File certFile = null;
try {
certFile = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
JcaMiscPEMGenerator generator = new JcaMiscPEMGenerator(cert);
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(certFile.toPath(), StandardOpenOption.CREATE)));
writer.writeObject(generator.generate());
writer.close();
return install(certFile);
} catch(IOException e) {
log.warn("Could not install cert from temp file", e);
} finally {
if(certFile != null && !certFile.delete()) {
certFile.deleteOnExit();
}
}
return false;
}
/**
* Install a certificate from disk
*/
public boolean install(File certFile) {
String helper = instance.getClass().getSimpleName();
String store = instance.getInstallType().name();
if(SystemUtilities.isJar()) {
if (remove(find())) {
log.info("Certificate removed from {} store using {}", store, helper);
} else {
log.warn("Could not remove certificate from {} store using {}", store, helper);
}
} else {
log.info("Skipping {} store certificate removal, IDE detected.", store, helper);
}
if (add(certFile)) {
log.info("Certificate added to {} store using {}", store, helper);
return true;
} else {
log.warn("Could not install certificate to {} store using {}", store, helper);
}
return false;
}
public abstract boolean add(File certFile);
public abstract boolean remove(List<String> idList);
public abstract List<String> find();
public abstract boolean verify(File certFile);
public abstract void setInstallType(Installer.PrivilegeLevel certType);
public abstract Installer.PrivilegeLevel getInstallType();
}

View File

@@ -0,0 +1,236 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.Kernel32Util;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.openssl.PEMParser;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
public class WindowsCertificateInstaller extends NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(WindowsCertificateInstaller.class);
private WinCrypt.HCERTSTORE store;
private byte[] certBytes;
private Installer.PrivilegeLevel certType;
public WindowsCertificateInstaller(Installer.PrivilegeLevel certType) {
setInstallType(certType);
}
public boolean add(File certFile) {
log.info("Writing certificate {} to {} store using Crypt32...", certFile, certType);
try {
byte[] bytes = getCertBytes(certFile);
Pointer pointer = new Memory(bytes.length);
pointer.write(0, bytes, 0, bytes.length);
boolean success = Crypt32.INSTANCE.CertAddEncodedCertificateToStore(
openStore(),
WinCrypt.X509_ASN_ENCODING,
pointer,
bytes.length,
Crypt32.CERT_STORE_ADD_REPLACE_EXISTING,
null
);
if(!success) {
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
}
closeStore();
return success;
} catch(IOException e) {
log.warn("An error occurred installing the certificate", e);
} finally {
certBytes = null;
}
return false;
}
private byte[] getCertBytes(File certFile) throws IOException {
if(certBytes == null) {
PEMParser pem = new PEMParser(new FileReader(certFile));
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
certBytes = certHolder.getEncoded();
}
return certBytes;
}
private WinCrypt.HCERTSTORE openStore() {
if(store == null) {
store = openStore(certType);
}
return store;
}
private void closeStore() {
if(store != null && closeStore(store)) {
store = null;
} else {
log.warn("Unable to close {} cert store", certType);
}
}
private static WinCrypt.HCERTSTORE openStore(Installer.PrivilegeLevel certType) {
log.info("Opening {} store using Crypt32...", certType);
WinCrypt.HCERTSTORE store = Crypt32.INSTANCE.CertOpenStore(
Crypt32.CERT_STORE_PROV_SYSTEM,
0,
null,
certType == Installer.PrivilegeLevel.USER ? Crypt32.CERT_SYSTEM_STORE_CURRENT_USER : Crypt32.CERT_SYSTEM_STORE_LOCAL_MACHINE,
"ROOT"
);
if(store == null) {
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
}
return store;
}
private static boolean closeStore(WinCrypt.HCERTSTORE certStore) {
boolean isClosed = Crypt32.INSTANCE.CertCloseStore(
certStore, 0
);
if(!isClosed) {
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
}
return isClosed;
}
public boolean remove(List<String> ignore) {
boolean success = true;
WinCrypt.CERT_CONTEXT hCertContext;
WinCrypt.CERT_CONTEXT pPrevCertContext = null;
while(true) {
hCertContext = Crypt32.INSTANCE.CertFindCertificateInStore(
openStore(),
WinCrypt.X509_ASN_ENCODING,
0,
Crypt32.CERT_FIND_SUBJECT_STR,
Constants.ABOUT_EMAIL,
pPrevCertContext);
if(hCertContext == null) {
break;
}
pPrevCertContext = Crypt32.INSTANCE.CertDuplicateCertificateContext(hCertContext);
if(success = (success && Crypt32.INSTANCE.CertDeleteCertificateFromStore(hCertContext))) {
log.info("Successfully deleted certificate matching {}", Constants.ABOUT_EMAIL);
} else {
log.info("Could not delete certificate: {}", Kernel32Util.formatMessage(Native.getLastError()));
}
}
closeStore();
return success;
}
public List<String> find() {
return null;
}
public void setInstallType(Installer.PrivilegeLevel type) {
this.certType = type;
}
public Installer.PrivilegeLevel getInstallType() {
return certType;
}
public boolean verify(File certFile) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(getCertBytes(certFile));
WinCrypt.DATA_BLOB thumbPrint = new WinCrypt.DATA_BLOB(md.digest());
WinNT.HANDLE cert = Crypt32.INSTANCE.CertFindCertificateInStore(
openStore(),
WinCrypt.X509_ASN_ENCODING,
0,
Crypt32.CERT_FIND_SHA1_HASH,
thumbPrint,
null);
return cert != null;
} catch(IOException | NoSuchAlgorithmException e) {
log.warn("An error occurred verifying the cert is installed: {}", certFile, e);
}
return false;
}
/**
* The JNA's Crypt32 instance oversimplifies store handling, preventing user stores from being used
*/
interface Crypt32 extends StdCallLibrary {
int CERT_SYSTEM_STORE_CURRENT_USER = 65536;
int CERT_SYSTEM_STORE_LOCAL_MACHINE = 131072;
int CERT_STORE_PROV_SYSTEM = 10;
int CERT_STORE_ADD_REPLACE_EXISTING = 3;
int CERT_FIND_SUBJECT_STR = 524295;
int CERT_FIND_SHA1_HASH = 65536;
Crypt32 INSTANCE = Native.load("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS);
WinCrypt.HCERTSTORE CertOpenStore(int lpszStoreProvider, int dwMsgAndCertEncodingType, Pointer hCryptProv, int dwFlags, String pvPara);
boolean CertCloseStore(WinCrypt.HCERTSTORE hCertStore, int dwFlags);
boolean CertAddEncodedCertificateToStore(WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, Pointer pbCertEncoded, int cbCertEncoded, int dwAddDisposition, Pointer ppCertContext);
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, String pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, Structure pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
boolean CertDeleteCertificateFromStore(WinCrypt.CERT_CONTEXT pCertContext);
boolean CertFreeCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
WinCrypt.CERT_CONTEXT CertDuplicateCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
}
// Polyfill from JNA5+
@SuppressWarnings("UnusedDeclaration") //Library class
public static class WinCrypt {
public static int X509_ASN_ENCODING = 0x00000001;
public static class HCERTSTORE extends WinNT.HANDLE {
public HCERTSTORE() {}
public HCERTSTORE(Pointer p) {
super(p);
}
}
public static class CERT_CONTEXT extends WinNT.HANDLE {
public CERT_CONTEXT() {}
public CERT_CONTEXT(Pointer p) {
super(p);
}
}
public static class DATA_BLOB extends com.sun.jna.platform.win32.WinCrypt.DATA_BLOB {
// Wrap the constructor for code readability
public DATA_BLOB() {
super();
}
public DATA_BLOB(byte[] data) {
super(data);
}
}
}
}

View File

@@ -0,0 +1,136 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ShellUtilities;
import qz.utils.WindowsUtilities;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* Command Line technique for installing certificates on Windows
* Fallback class for when JNA is not available (e.g. Windows on ARM)
*/
@SuppressWarnings("UnusedDeclaration") //Library class
public class WindowsCertificateInstallerCli extends NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(WindowsCertificateInstallerCli.class);
private Installer.PrivilegeLevel certType;
public WindowsCertificateInstallerCli(Installer.PrivilegeLevel certType) {
setInstallType(certType);
}
public boolean add(File certFile) {
if (WindowsUtilities.isWindowsXP()) return false;
if (certType == Installer.PrivilegeLevel.USER) {
// This will prompt the user
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "-user", "Root", certFile.getPath());
} else {
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "Root", certFile.getPath());
}
}
public boolean remove(List<String> idList) {
if (WindowsUtilities.isWindowsXP()) return false;
boolean success = true;
for (String certId : idList) {
if (certType == Installer.PrivilegeLevel.USER) {
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "-user", "Root", certId);
} else {
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "Root", certId);
}
}
return success;
}
/**
* Returns a list of serials, if found
*/
public List<String> find() {
ArrayList<String> serialList = new ArrayList<>();
try {
Process p;
if (certType == Installer.PrivilegeLevel.USER) {
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "-user", "Root"});
} else {
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "Root"});
}
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
if (line.contains("================")) {
// First line is serial
String serial = parseNextLine(in);
if (serial != null) {
// Second line is issuer
String issuer = parseNextLine(in);
if (issuer.contains("OU=" + Constants.ABOUT_COMPANY)) {
serialList.add(serial);
}
}
}
}
in.close();
} catch(Exception e) {
log.info("Unable to find a Trusted Root Certificate matching \"OU={}\"", Constants.ABOUT_COMPANY);
}
return serialList;
}
public boolean verify(File certFile) {
return verifyCert(certFile);
}
public static boolean verifyCert(File certFile) {
// -user also will check the root store
String dwErrorStatus = ShellUtilities.execute( new String[] {"certutil", "-user", "-verify", certFile.getPath() }, new String[] { "dwErrorStatus=" }, false, false);
if(!dwErrorStatus.isEmpty()) {
String[] parts = dwErrorStatus.split("[\r\n\\s]+");
for(String part : parts) {
if(part.startsWith("dwErrorStatus=")) {
log.info("Certificate validity says {}", part);
String[] status = part.split("=", 2);
if (status.length == 2) {
return status[1].trim().equals("0");
}
}
}
}
log.warn("Unable to determine certificate validity, you'll be prompted on startup");
return false;
}
public void setInstallType(Installer.PrivilegeLevel type) {
this.certType = type;
}
public Installer.PrivilegeLevel getInstallType() {
return certType;
}
private static String parseNextLine(BufferedReader reader) throws IOException {
String data = reader.readLine();
if (data != null) {
String[] split = data.split(":", 2);
if (split.length == 2) {
return split[1].trim();
}
}
return null;
}
}

View File

@@ -0,0 +1,7 @@
package qz.installer.certificate.firefox;
class ConflictingPolicyException extends Exception {
ConflictingPolicyException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,282 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate.firefox;
import com.github.zafarkhaja.semver.Version;
import com.sun.jna.platform.win32.WinReg;
import org.codehaus.jettison.json.JSONException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import qz.installer.certificate.CertificateManager;
import qz.installer.certificate.firefox.locator.AppAlias;
import qz.installer.certificate.firefox.locator.AppInfo;
import qz.installer.certificate.firefox.locator.AppLocator;
import qz.utils.JsonWriter;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
/**
* Installs the Firefox Policy file via Enterprise Policy, Distribution Policy file or AutoConfig, depending on OS & version
*/
public class FirefoxCertificateInstaller {
protected static final Logger log = LogManager.getLogger(FirefoxCertificateInstaller.class);
/**
* Versions are for Mozilla's official Firefox release.
* 3rd-party/clones may adopt Enterprise Policy support under
* different version numbers, adapt as needed.
*/
private static final Version WINDOWS_POLICY_VERSION = Version.valueOf("62.0.0");
private static final Version MAC_POLICY_VERSION = Version.valueOf("63.0.0");
private static final Version LINUX_POLICY_VERSION = Version.valueOf("65.0.0");
public static final Version FIREFOX_RESTART_VERSION = Version.valueOf("60.0.0");
public static final String LINUX_GLOBAL_POLICY_LOCATION = "/etc/firefox/policies/policies.json";
public static final String LINUX_SNAP_CERT_LOCATION = "/etc/firefox/policies/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION; // See https://github.com/mozilla/policy-templates/issues/936
public static final String LINUX_GLOBAL_CERT_LOCATION = "/usr/lib/mozilla/certificates/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION;
private static String DISTRIBUTION_ENTERPRISE_ROOT_POLICY = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }";
private static String DISTRIBUTION_INSTALL_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION + "\", \"" + LINUX_SNAP_CERT_LOCATION + "\" ] } } }";
private static String DISTRIBUTION_REMOVE_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"/opt/" + Constants.PROPS_FILE + "/auth/root-ca.crt\"] } } }";
public static final String DISTRIBUTION_POLICY_LOCATION = "distribution/policies.json";
public static final String DISTRIBUTION_MAC_POLICY_LOCATION = "Contents/Resources/" + DISTRIBUTION_POLICY_LOCATION;
public static final String POLICY_AUDIT_MESSAGE = "Enterprise policy installed by " + Constants.ABOUT_TITLE + " on " + SystemUtilities.timeStamp();
public static void install(X509Certificate cert, String ... hostNames) {
// Blindly install Firefox enterprise policies to the system (macOS, Windows)
ArrayList<AppAlias.Alias> enterpriseFailed = new ArrayList<>();
for(AppAlias.Alias alias : AppAlias.FIREFOX.getAliases()) {
boolean success = false;
try {
if(alias.isEnterpriseReady() && !hasEnterprisePolicy(alias, false)) {
log.info("Installing Firefox enterprise certificate policy for {}", alias);
success = installEnterprisePolicy(alias, false);
}
} catch(ConflictingPolicyException e) {
log.warn("Conflict found installing {} enterprise cert support. We'll fallback on the distribution policy instead", alias.getName(), e);
}
if(!success) {
enterpriseFailed.add(alias);
}
}
// Search for installed instances
ArrayList<AppInfo> foundApps = AppLocator.getInstance().locate(AppAlias.FIREFOX);
ArrayList<Path> processPaths = null;
for(AppInfo appInfo : foundApps) {
boolean success = false;
if (honorsPolicy(appInfo)) {
if((SystemUtilities.isWindows()|| SystemUtilities.isMac()) && !enterpriseFailed.contains(appInfo.getAlias())) {
// Enterprise policy was already installed
success = true;
} else {
log.info("Installing Firefox distribution policy for {}", appInfo);
success = installDistributionPolicy(appInfo, cert);
}
} else {
log.info("Installing Firefox auto-config script for {}", appInfo);
try {
String certData = Base64.getEncoder().encodeToString(cert.getEncoded());
success = LegacyFirefoxCertificateInstaller.installAutoConfigScript(appInfo, certData, hostNames);
}
catch(CertificateEncodingException e) {
log.warn("Unable to install auto-config script for {}", appInfo, e);
}
}
if(success) {
issueRestartWarning(processPaths = AppLocator.getRunningPaths(foundApps, processPaths), appInfo);
}
}
}
public static void uninstall() {
ArrayList<AppInfo> appList = AppLocator.getInstance().locate(AppAlias.FIREFOX);
for(AppInfo appInfo : appList) {
if(honorsPolicy(appInfo)) {
if(SystemUtilities.isWindows() || SystemUtilities.isMac()) {
log.info("Skipping uninstall of Firefox enterprise root certificate policy for {}", appInfo);
} else {
try {
File policy = appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toFile();
if(policy.exists()) {
JsonWriter.write(appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toString(), DISTRIBUTION_INSTALL_CERT_POLICY, false, true);
}
} catch(IOException | JSONException e) {
log.warn("Unable to remove Firefox policy for {}", appInfo, e);
}
}
} else {
log.info("Uninstalling Firefox auto-config script for {}", appInfo);
LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(appInfo);
}
}
}
public static boolean honorsPolicy(AppInfo appInfo) {
if (appInfo.getVersion() == null) {
log.warn("Firefox-compatible browser found {}, but no version information is available", appInfo);
return false;
}
if(SystemUtilities.isWindows()) {
return appInfo.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION);
} else if (SystemUtilities.isMac()) {
return appInfo.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION);
} else {
return appInfo.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION);
}
}
/**
* Returns true if an alternative Firefox policy (e.g. registry, plist user or system) is installed
*/
private static boolean hasEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) throws ConflictingPolicyException {
if(SystemUtilities.isWindows()) {
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));
Integer foundPolicy = WindowsUtilities.getRegInt(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots");
if(foundPolicy != null) {
return foundPolicy == 1;
}
} else if(SystemUtilities.isMac()) {
String policyLocation = "/Library/Preferences/";
if(userOnly) {
policyLocation = System.getProperty("user.home") + policyLocation;
}
String policesEnabled = ShellUtilities.executeRaw(new String[] { "defaults", "read", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled"}, true);
String foundPolicy = ShellUtilities.executeRaw(new String[] {"defaults", "read", policyLocation + alias.getBundleId(), "Certificates"}, true);
if(!policesEnabled.isEmpty() && !foundPolicy.isEmpty()) {
// Policies exist, decide how to proceed
if(policesEnabled.trim().equals("1") && foundPolicy.contains("ImportEnterpriseRoots = 1;")) {
return true;
}
throw new ConflictingPolicyException(String.format("%s enterprise policy conflict at %s: %s", alias.getName(), policyLocation + alias.getBundleId(), foundPolicy));
}
} else {
// Linux alternate policy not yet supported
}
return false;
}
/**
* Install policy to distribution/policies.json
*/
public static boolean installDistributionPolicy(AppInfo app, X509Certificate cert) {
Path jsonPath = app.getPath().resolve(SystemUtilities.isMac() ? DISTRIBUTION_MAC_POLICY_LOCATION:DISTRIBUTION_POLICY_LOCATION);
String jsonPolicy = SystemUtilities.isWindows() || SystemUtilities.isMac() ? DISTRIBUTION_ENTERPRISE_ROOT_POLICY:DISTRIBUTION_INSTALL_CERT_POLICY;
// Special handling for snaps
if(app.getPath().toString().startsWith("/snap")) {
log.info("Snap detected, installing policy file to global location instead: {}", LINUX_GLOBAL_POLICY_LOCATION);
jsonPath = Paths.get(LINUX_GLOBAL_POLICY_LOCATION);
}
try {
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
// Linux lacks the concept of "enterprise roots", we'll write it to a known location instead
writeCertFile(cert, LINUX_SNAP_CERT_LOCATION); // so that the snap can read from it
writeCertFile(cert, LINUX_GLOBAL_CERT_LOCATION); // default location for non-snaps
}
File jsonFile = jsonPath.toFile();
// Make sure we can traverse and read
File distribution = jsonFile.getParentFile();
distribution.mkdirs();
distribution.setReadable(true, false);
distribution.setExecutable(true, false);
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
// Delete previous policy
JsonWriter.write(jsonPath.toString(), DISTRIBUTION_REMOVE_CERT_POLICY, false, true);
}
JsonWriter.write(jsonPath.toString(), jsonPolicy, false, false);
// Make sure ew can read
jsonFile.setReadable(true, false);
return true;
} catch(JSONException | IOException e) {
log.warn("Could not install distribution policy {} to {}", jsonPolicy, jsonPath.toString(), e);
}
return false;
}
public static boolean installEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) {
if(SystemUtilities.isWindows()) {
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));;
WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "Comment", POLICY_AUDIT_MESSAGE);
return WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots", 1);
} else if(SystemUtilities.isMac()) {
String policyLocation = "/Library/Preferences/";
if(userOnly) {
policyLocation = System.getProperty("user.home") + policyLocation;
}
return ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled", "-bool", "TRUE"}, true) &&
ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "Certificates", "-dict", "ImportEnterpriseRoots", "-bool", "TRUE",
"Comment", "-string", POLICY_AUDIT_MESSAGE}, true);
}
return false;
}
public static boolean issueRestartWarning(ArrayList<Path> runningPaths, AppInfo appInfo) {
boolean firefoxIsRunning = runningPaths.contains(appInfo.getExePath());
// Edge case for detecting if snap is running, since we can't compare the exact path easily
for(Path runningPath : runningPaths) {
if(runningPath.startsWith("/snap/")) {
firefoxIsRunning = true;
}
}
if (firefoxIsRunning) {
if (appInfo.getVersion().greaterThanOrEqualTo(FirefoxCertificateInstaller.FIREFOX_RESTART_VERSION)) {
try {
Installer.getInstance().spawn(appInfo.getExePath().toString(), "-private", "about:restartrequired");
return true;
}
catch(Exception ignore) {}
} else {
log.warn("{} must be restarted manually for changes to take effect", appInfo);
}
}
return false;
}
private static void writeCertFile(X509Certificate cert, String location) throws IOException {
File certFile = new File(location);
// Make sure we can traverse and read
File certs = new File(location).getParentFile();
certs.mkdirs();
certs.setReadable(true, false);
certs.setExecutable(true, false);
File mozilla = certs.getParentFile();
mozilla.setReadable(true, false);
mozilla.setExecutable(true, false);
// Make sure we can read
CertificateManager.writeCert(cert, certFile);
certFile.setReadable(true, false);
}
}

View File

@@ -0,0 +1,150 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate.firefox;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.certificate.CertificateChainBuilder;
import qz.installer.certificate.firefox.locator.AppInfo;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import java.io.*;
import java.nio.file.Path;
import java.security.cert.CertificateEncodingException;
import java.util.*;
/**
* Legacy Firefox Certificate installer
*
* For old Firefox-compatible browsers still in the wild such as Firefox 52 ESR, SeaMonkey, WaterFox, etc.
*/
public class LegacyFirefoxCertificateInstaller {
private static final Logger log = LogManager.getLogger(CertificateChainBuilder.class);
private static final String CFG_TEMPLATE = "assets/firefox-autoconfig.js.in";
private static final String CFG_FILE = Constants.PROPS_FILE + ".cfg";
private static final String PREFS_FILE = Constants.PROPS_FILE + ".js";
private static final String PREFS_DIR = "defaults/pref";
private static final String MAC_PREFIX = "Contents/Resources";
public static boolean installAutoConfigScript(AppInfo appInfo, String certData, String ... hostNames) {
try {
if(appInfo.getPath().toString().equals("/usr/bin")) {
throw new Exception("Preventing install to root location");
}
writePrefsFile(appInfo);
writeParsedConfig(appInfo, certData, false, hostNames);
return true;
} catch(Exception e) {
log.warn("Error installing auto-config support for {}", appInfo, e);
}
return false;
}
public static boolean uninstallAutoConfigScript(AppInfo appInfo) {
try {
writeParsedConfig(appInfo, "", true);
return true;
} catch(Exception e) {
log.warn("Error uninstalling auto-config support for {}", appInfo, e);
}
return false;
}
public static File tryWrite(AppInfo appInfo, boolean mkdirs, String ... paths) throws IOException {
Path dir = appInfo.getPath();
if (SystemUtilities.isMac()) {
dir = dir.resolve(MAC_PREFIX);
}
for (String path : paths) {
dir = dir.resolve(path);
}
File file = dir.toFile();
if(mkdirs) file.mkdirs();
if(file.exists() && file.isDirectory() && file.canWrite()) {
return file;
}
throw new IOException(String.format("Directory does not exist or is not writable: %s", file));
}
public static void deleteFile(File parent, String ... paths) {
if(parent != null) {
String toDelete = parent.getPath();
for (String path : paths) {
toDelete += File.separator + path;
}
File deleteFile = new File(toDelete);
if (!deleteFile.exists()) {
} else if (new File(toDelete).delete()) {
log.info("Deleted old file: {}", toDelete);
} else {
log.warn("Could not delete old file: {}", toDelete);
}
}
}
public static void writePrefsFile(AppInfo app) throws Exception {
File prefsDir = tryWrite(app, true, PREFS_DIR);
deleteFile(prefsDir, "firefox-prefs.js"); // cleanup old version
// first check that there aren't other prefs files
String pref = "general.config.filename";
for (File file : prefsDir.listFiles()) {
try {
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while((line = reader.readLine()) != null) {
if(line.contains(pref) && !line.contains(CFG_FILE)) {
throw new Exception(String.format("Browser already has %s defined in %s:\n %s", pref, file, line));
}
}
} catch(IOException ignore) {}
}
// write out the new prefs file
File prefsFile = new File(prefsDir, PREFS_FILE);
BufferedWriter writer = new BufferedWriter(new FileWriter(prefsFile));
String[] data = {
String.format("pref('%s', '%s');", pref, CFG_FILE),
"pref('general.config.obscure_value', 0);"
};
for (String line : data) {
writer.write(line + "\n");
}
writer.close();
prefsFile.setReadable(true, false);
}
private static void writeParsedConfig(AppInfo appInfo, String certData, boolean uninstall, String ... hostNames) throws IOException, CertificateEncodingException{
if (hostNames.length == 0) hostNames = CertificateChainBuilder.DEFAULT_HOSTNAMES;
File cfgDir = tryWrite(appInfo, false);
deleteFile(cfgDir, "firefox-config.cfg"); // cleanup old version
File dest = new File(cfgDir.getPath(), CFG_FILE);
HashMap<String, String> fieldMap = new HashMap<>();
// Dynamic fields
fieldMap.put("%CERT_DATA%", certData);
fieldMap.put("%COMMON_NAME%", hostNames[0]);
fieldMap.put("%TIMESTAMP%", uninstall ? "-1" : "" + new Date().getTime());
fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.getAppPath() != null ? SystemUtilities.getAppPath().toString() : "" : "");
fieldMap.put("%UNINSTALL%", "" + uninstall);
FileUtilities.configureAssetFile(CFG_TEMPLATE, dest, fieldMap, LegacyFirefoxCertificateInstaller.class);
dest.setReadable(true, false);
}
}

View File

@@ -0,0 +1,117 @@
//
// Firefox AutoConfig Certificate Installer for Legacy Firefox versions
// This is part of the QZ Tray application
//
var serviceObserver = {
observe: function observe(aSubject, aTopic, aData) {
// Get NSS certdb object
var certdb = getCertDB();
if (needsUninstall()) {
deleteCertificate();
unregisterProtocol();
} else if (needsCert()) {
deleteCertificate();
installCertificate();
registerProtocol();
}
// Compares the timestamp embedded in this script against that stored in the browser's about:config
function needsCert() {
try {
return getPref("%PROPS_FILE%.installer.timestamp") != "%TIMESTAMP%";
} catch(notfound) {}
return true;
}
// Installs the embedded base64 certificate into the browser
function installCertificate() {
certdb.addCertFromBase64(getCertData(), "C,C,C", "%COMMON_NAME% - %ABOUT_COMPANY%");
pref("%PROPS_FILE%.installer.timestamp", "%TIMESTAMP%");
}
// Deletes the certificate, if it exists
function deleteCertificate() {
var certs = certdb.getCerts();
var enumerator = certs.getEnumerator();
while (enumerator.hasMoreElements()) {
var cert = enumerator.getNext().QueryInterface(Components.interfaces.nsIX509Cert);
if (cert.containsEmailAddress("%ABOUT_EMAIL%")) {
try {
certdb.deleteCertificate(cert);
} catch (ignore) {}
}
}
pref("%PROPS_FILE%.installer.timestamp", "-1");
}
// Register the specified protocol to open with the specified application
function registerProtocol() {
// Only register if platform needs it (e.g. macOS)
var trayApp = "%APP_PATH%";
if (!trayApp) { return; }
try {
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);
file.initWithPath(trayApp);
var lhandler = Components.classes["@mozilla.org/uriloader/local-handler-app;1"].createInstance(Components.interfaces.nsILocalHandlerApp);
lhandler.executable = file;
lhandler.name = "%PROPS_FILE%";
var protocol = pservice.getProtocolHandlerInfo("%DATA_DIR%");
protocol.preferredApplicationHandler = lhandler;
protocol.preferredAction = 2; // useHelperApp
protocol.alwaysAskBeforeHandling = false;
hservice.store(protocol);
} catch(ignore) {}
}
// De-register the specified protocol from opening with the specified application
function unregisterProtocol() {
// Only register if platform needs it (e.g. macOS)
var trayApp = "%APP_PATH%";
if (!trayApp) { return; }
try {
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
hservice.remove(pservice.getProtocolHandlerInfo("%DATA_DIR%"));
} catch(ignore) {}
}
// Get certdb object
function getCertDB() {
// Import certificate using NSS certdb API (http://tinyurl.com/x509certdb)
var id = "@mozilla.org/security/x509certdb;1";
var db1 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB);
var db2 = db1;
try {
db2 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB2);
} catch(ignore) {}
return db2;
}
// The certificate to import (automatically generated by desktop installer)
function getCertData() {
return "%CERT_DATA%";
}
// Whether or not an uninstall should occur, flagged by the installer/uninstaller
function needsUninstall() {
try {
if (getPref("%PROPS_FILE%.installer.timestamp") == "-1") {
return false;
}
}
catch(notfound) {
return false;
}
return %UNINSTALL%;
}
}
};
Components.utils.import("resource://gre/modules/Services.jsm");
Services.obs.addObserver(serviceObserver, "profile-after-change", false);

View File

@@ -0,0 +1,91 @@
package qz.installer.certificate.firefox.locator;
import java.util.Locale;
public enum AppAlias {
// Tor Browser intentionally excluded; Tor's proxy blocks localhost connections
FIREFOX(
new Alias("Mozilla", "Mozilla Firefox", "org.mozilla.firefox", true),
new Alias("Mozilla", "Firefox Developer Edition", "org.mozilla.firefoxdeveloperedition", true),
new Alias("Mozilla", "Firefox Nightly", "org.mozilla.nightly", true),
new Alias("Mozilla", "SeaMonkey", "org.mozilla.seamonkey", false),
new Alias("Waterfox", "Waterfox", "net.waterfox.waterfoxcurrent", true),
new Alias("Waterfox", "Waterfox Classic", "org.waterfoxproject.waterfox classic", false),
new Alias("Mozilla", "Pale Moon", "org.mozilla.palemoon", false),
// IceCat is technically enterprise ready, but not officially distributed for macOS, Windows
new Alias("Mozilla", "IceCat", "org.gnu.icecat", false)
);
Alias[] aliases;
AppAlias(Alias... aliases) {
this.aliases = aliases;
}
public Alias[] getAliases() {
return aliases;
}
public static Alias findAlias(AppAlias appAlias, String appName, boolean stripVendor) {
if (appName != null) {
for (Alias alias : appAlias.aliases) {
if (appName.toLowerCase(Locale.ENGLISH).matches(alias.getName(stripVendor).toLowerCase(Locale.ENGLISH))) {
return alias;
}
}
}
return null;
}
public static class Alias {
private String vendor;
private String name;
private String bundleId;
private boolean enterpriseReady;
private String posix;
public Alias(String vendor, String name, String bundleId, boolean enterpriseReady) {
this.name = name;
this.vendor = vendor;
this.bundleId = bundleId;
this.enterpriseReady = enterpriseReady;
this.posix = getName(true).replaceAll(" ", "").toLowerCase(Locale.ENGLISH);
}
public String getVendor() {
return vendor;
}
public String getName() {
return name;
}
/**
* Remove vendor prefix if exists
*/
public String getName(boolean stripVendor) {
if(stripVendor && "Mozilla".equals(vendor) && name.startsWith(vendor)) {
return name.substring(vendor.length()).trim();
}
return name;
}
public String getBundleId() {
return bundleId;
}
public String getPosix() {
return posix;
}
/**
* Returns whether or not the app is known to recognizes enterprise policies, such as GPO
*/
public boolean isEnterpriseReady() {
return enterpriseReady;
}
@Override
public String toString() {
return name;
}
}
}

View File

@@ -0,0 +1,100 @@
package qz.installer.certificate.firefox.locator;
import com.github.zafarkhaja.semver.Version;
import java.nio.file.Path;
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
/**
* Container class for installed app information
*/
public class AppInfo {
private AppAlias.Alias alias;
private Path path;
private Path exePath;
private Version version;
public AppInfo(Alias alias, Path exePath, String version) {
this.alias = alias;
this.path = exePath.getParent();
this.exePath = exePath;
this.version = parseVersion(version);
}
public AppInfo(Alias alias, Path path, Path exePath, String version) {
this.alias = alias;
this.path = path;
this.exePath = exePath;
this.version = parseVersion(version);
}
public AppInfo(Alias alias, Path exePath) {
this.alias = alias;
this.path = exePath.getParent();
this.exePath = exePath;
}
public Alias getAlias() {
return alias;
}
public String getName(boolean stripVendor) {
return alias.getName(stripVendor);
}
public Path getExePath() {
return exePath;
}
public Path getPath() {
return path;
}
public void setPath(Path path) {
this.path = path;
}
public Version getVersion() {
return version;
}
public void setVersion(String version) {
this.version = parseVersion(version);
}
public void setVersion(Version version) {
this.version = version;
}
private static Version parseVersion(String version) {
try {
// Ensure < 3 octets (e.g. "56.0") doesn't failing
while(version.split("\\.").length < 3) {
version = version + ".0";
}
return Version.valueOf(version);
} catch(Exception ignore1) {
// Catch poor formatting (e.g. "97.0a1"), try to use major version only
if(version.split("\\.").length > 0) {
try {
String[] tryFix = version.split("\\.");
return Version.valueOf(tryFix[0] + ".0.0-unknown");
} catch(Exception ignore2) {}
}
}
return null;
}
@Override
public boolean equals(Object o) {
if(o instanceof AppInfo && o != null && path != null) {
return path.equals(((AppInfo)o).getPath());
}
return false;
}
@Override
public String toString() {
return alias + " " + path;
}
}

View File

@@ -0,0 +1,87 @@
package qz.installer.certificate.firefox.locator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
public abstract class AppLocator {
protected static final Logger log = LogManager.getLogger(AppLocator.class);
private static AppLocator INSTANCE = getPlatformSpecificAppLocator();
public abstract ArrayList<AppInfo> locate(AppAlias appAlias);
public abstract ArrayList<Path> getPidPaths(ArrayList<String> pids);
@SuppressWarnings("unused")
public ArrayList<String> getPids(String ... processNames) {
return getPids(new ArrayList<>(Arrays.asList(processNames)));
}
/**
* Linux, Mac
*/
public ArrayList<String> getPids(ArrayList<String> processNames) {
String[] response;
ArrayList<String> pidList = new ArrayList<>();
if(processNames.contains("firefox") && !(SystemUtilities.isWindows() || SystemUtilities.isMac())) {
processNames.add("MainThread"); // Workaround Firefox 79 https://github.com/qzind/tray/issues/701
processNames.add("GeckoMain"); // Workaround Firefox 94 https://bugzilla.mozilla.org/show_bug.cgi?id=1742606
}
if (processNames.size() == 0) return pidList;
// Quoting handled by the command processor (e.g. pgrep -x "myapp|my app" is perfectly valid)
String data = ShellUtilities.executeRaw("pgrep", "-x", String.join("|", processNames));
//Splitting an empty string results in a 1 element array, this is not what we want
if (!data.isEmpty()) {
response = data.split("\\s*\\r?\\n");
Collections.addAll(pidList, response);
}
return pidList;
}
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList) {
return getRunningPaths(appList, null);
}
/**
* Gets the path to the running executables matching on <code>AppInfo.getExePath</code>
* This is resource intensive; if a non-null <code>cache</code> is provided, it will return that instead
*/
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList, ArrayList<Path> cache) {
if(cache == null) {
ArrayList<String> appNames = new ArrayList<>();
for(AppInfo app : appList) {
String exeName = app.getExePath().getFileName().toString();
if (!appNames.contains(exeName)) appNames.add(exeName);
}
cache = INSTANCE.getPidPaths(INSTANCE.getPids(appNames));
}
return cache;
}
public static AppLocator getInstance() {
return INSTANCE;
}
private static AppLocator getPlatformSpecificAppLocator() {
switch(SystemUtilities.getOs()) {
case WINDOWS:
return new WindowsAppLocator();
case MAC:
return new MacAppLocator();
default:
return new LinuxAppLocator();
}
}
}

View File

@@ -0,0 +1,159 @@
package qz.installer.certificate.firefox.locator;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.UnixUtilities;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
public class LinuxAppLocator extends AppLocator {
private static final Logger log = LogManager.getLogger(LinuxAppLocator.class);
public ArrayList<AppInfo> locate(AppAlias appAlias) {
ArrayList<AppInfo> appList = new ArrayList<>();
// Workaround for calling "firefox --version" as sudo
String[] env = appendPaths("HOME=/tmp");
// Search for matching executable in all path values
aliasLoop:
for(AppAlias.Alias alias : appAlias.aliases) {
// Add non-standard app search locations (e.g. Fedora)
for (String dirname : appendPaths(alias.getPosix(), "/usr/lib/$/bin", "/usr/lib64/$/bin", "/usr/lib/$", "/usr/lib64/$")) {
Path path = Paths.get(dirname, alias.getPosix());
if (Files.isRegularFile(path) && Files.isExecutable(path)) {
log.info("Found {} {}: {}, investigating...", alias.getVendor(), alias.getName(true), path);
try {
File file = path.toFile().getCanonicalFile(); // fix symlinks
if(file.getPath().endsWith("/snap")) {
// Ubuntu 22.04+ ships Firefox as a snap
// Snaps are read-only and are symlinks back to /usr/bin/snap
// Reset the executable back to /snap/bin/firefox to get proper version information
file = path.toFile();
}
if(file.getPath().endsWith(".sh")) {
// Legacy Ubuntu likes to use .../firefox/firefox.sh, return .../firefox/firefox instead
log.info("Found an '.sh' file: {}, removing file extension: {}", file, file = new File(FilenameUtils.removeExtension(file.getPath())));
}
String contentType = Files.probeContentType(file.toPath());
if(contentType == null) {
// Fallback to commandline per https://bugs.openjdk.org/browse/JDK-8188228
contentType = ShellUtilities.executeRaw("file", "--mime-type", "--brief", file.getPath()).trim();
}
if(contentType != null && contentType.endsWith("/x-shellscript")) {
if(UnixUtilities.isFedora()) {
// Firefox's script is full of variables and not parsable, fallback to /usr/lib64/$, etc
log.info("Found shell script at {}, but we're on Fedora, so we'll look in some known locations instead.", file.getPath());
continue;
}
// Debian and Arch like to place a stub script directly in /usr/bin/
// TODO: Split into a function; possibly recurse on search paths
log.info("{} bin was expected but script found... Reading...", appAlias.name());
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while((line = reader.readLine()) != null) {
if(line.startsWith("exec") && line.contains(alias.getPosix())) {
String[] parts = line.split(" ");
// Get the app name after "exec"
if (parts.length > 1) {
log.info("Found a familiar line '{}', using '{}'", line, parts[1]);
Path p = Paths.get(parts[1]);
String exec = parts[1];
// Handle edge-case for esr release
if(!p.isAbsolute()) {
// Script doesn't contain the full path, go deeper
exec = Paths.get(dirname, exec).toFile().getCanonicalPath();
log.info("Calculated full bin path {}", exec);
}
// Make sure it actually exists
if(!(file = new File(exec)).exists()) {
log.warn("Sorry, we couldn't detect the real path of {}. Skipping...", appAlias.name());
continue aliasLoop;
}
break;
}
}
}
reader.close();
} else {
log.info("Assuming {} {} is installed: {}", alias.getVendor(), alias.getName(true), file);
}
AppInfo appInfo = new AppInfo(alias, file.toPath());
if(file.getPath().startsWith("/snap/")) {
// Ubuntu 22.04+ uses snaps, fallback to a sane "path" value
String snapPath = file.getPath(); // e.g. /snap/bin/firefox
snapPath = snapPath.replaceFirst("/bin/", "/");
snapPath += "/current";
appInfo.setPath(Paths.get(snapPath));
}
appList.add(appInfo);
// Call "--version" on executable to obtain version information
Process p = Runtime.getRuntime().exec(new String[] {file.getPath(), "--version" }, env);
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String version = reader.readLine();
reader.close();
if (version != null) {
log.info("We obtained version info: {}, but we'll need to parse it", version);
if(version.contains(" ")) {
String[] split = version.split(" ");
String parsed = split[split.length - 1];
String stripped = parsed.replaceAll("[^\\d.]", "");
appInfo.setVersion(stripped);
if(!parsed.equals(stripped)) {
// Add the meta data back (e.g. "esr")
appInfo.getVersion().setBuildMetadata(parsed.replaceAll("[\\d.]", ""));
}
} else {
appInfo.setVersion(version.trim());
}
}
break;
} catch(Exception e) {
log.warn("Something went wrong getting app info for {} {}", alias.getVendor(), alias.getName(true), e);
}
}
}
}
return appList;
}
@Override
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
ArrayList<Path> pathList = new ArrayList<>();
for(String pid : pids) {
try {
pathList.add(Paths.get("/proc/", pid, !SystemUtilities.isSolaris() ? "/exe" : "/path/a.out").toRealPath());
} catch(IOException e) {
log.warn("Process {} vanished", pid);
}
}
return pathList;
}
/**
* Returns a PATH value with provided paths appended, replacing "$" with POSIX app name
* Useful for strange Firefox install locations (e.g. Fedora)
*
* Usage: appendPaths("firefox", "/usr/lib64");
*
*/
private static String[] appendPaths(String posix, String ... prefixes) {
String newPath = System.getenv("PATH");
for (String prefix : prefixes) {
newPath = newPath + File.pathSeparator + prefix.replaceAll("\\$", posix);
}
return newPath.split(File.pathSeparator);
}
}

View File

@@ -0,0 +1,168 @@
package qz.installer.certificate.firefox.locator;
import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import qz.utils.ShellUtilities;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
public class MacAppLocator extends AppLocator{
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
private static String[] BLACKLISTED_PATHS = new String[]{"/Volumes/", "/.Trash/", "/Applications (Parallels)/" };
/**
* Helper class for finding key/value siblings from the DDM
*/
private enum SiblingNode {
NAME("_name"),
PATH("path"),
VERSION("version");
private String key;
private boolean wants;
SiblingNode(String key) {
this.key = key;
this.wants = false;
}
private boolean isKey(Node node) {
if (node.getNodeName().equals("key") && node.getTextContent().equals(key)) {
return true;
}
return false;
}
}
@Override
public ArrayList<AppInfo> locate(AppAlias appAlias) {
ArrayList<AppInfo> appList = new ArrayList<>();
Document doc;
try {
// system_profile benchmarks about 30% better than lsregister
Process p = Runtime.getRuntime().exec(new String[] {"system_profiler", "SPApplicationsDataType", "-xml"}, ShellUtilities.envp);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// don't let the <!DOCTYPE> fail parsing per https://github.com/qzind/tray/issues/809
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
doc = dbf.newDocumentBuilder().parse(p.getInputStream());
} catch(IOException | ParserConfigurationException | SAXException e) {
log.warn("Could not retrieve app listing for {}", appAlias.name(), e);
return appList;
}
doc.normalizeDocument();
NodeList nodeList = doc.getElementsByTagName("dict");
for (int i = 0; i < nodeList.getLength(); i++) {
NodeList dict = nodeList.item(i).getChildNodes();
HashMap<SiblingNode, String> foundApp = new HashMap<>();
for (int j = 0; j < dict.getLength(); j++) {
Node node = dict.item(j);
if (node.getNodeType() == Node.ELEMENT_NODE) {
for (SiblingNode sibling : SiblingNode.values()) {
if (sibling.wants) {
foundApp.put(sibling, node.getTextContent());
sibling.wants = false;
break;
} else if(sibling.isKey(node)) {
sibling.wants = true;
break;
}
}
}
}
AppAlias.Alias alias;
if((alias = AppAlias.findAlias(appAlias, foundApp.get(SiblingNode.NAME), true)) != null) {
appList.add(new AppInfo(alias, Paths.get(foundApp.get(SiblingNode.PATH)),
getExePath(foundApp.get(SiblingNode.PATH)), foundApp.get(SiblingNode.VERSION)
));
}
}
// Remove blacklisted paths
Iterator<AppInfo> appInfoIterator = appList.iterator();
while(appInfoIterator.hasNext()) {
AppInfo appInfo = appInfoIterator.next();
for(String listEntry : BLACKLISTED_PATHS) {
if (appInfo.getPath() != null && appInfo.getPath().toString().contains(listEntry)) {
appInfoIterator.remove();
}
}
}
return appList;
}
@Override
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
ArrayList<Path> processPaths = new ArrayList();
for (String pid : pids) {
Pointer buf = new Memory(SystemB.PROC_PIDPATHINFO_MAXSIZE);
SystemB.INSTANCE.proc_pidpath(Integer.parseInt(pid), buf, SystemB.PROC_PIDPATHINFO_MAXSIZE);
processPaths.add(Paths.get(buf.getString(0).trim()));
}
return processPaths;
}
/**
* Calculate executable path by parsing Contents/Info.plist
*/
private static Path getExePath(String appPath) {
Path path = Paths.get(appPath).toAbsolutePath().normalize();
Path plist = path.resolve("Contents/Info.plist");
Document doc;
try {
if(!plist.toFile().exists()) {
log.warn("Could not locate plist file for {}: {}", appPath, plist);
return null;
}
// Convert potentially binary plist files to XML
Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toString(), "-o", "-"}, ShellUtilities.envp);
doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream());
} catch(IOException | ParserConfigurationException | SAXException e) {
log.warn("Could not parse plist file for {}: {}", appPath, appPath, e);
return null;
}
doc.normalizeDocument();
boolean upNext = false;
NodeList nodeList = doc.getElementsByTagName("dict");
for (int i = 0; i < nodeList.getLength(); i++) {
NodeList dict = nodeList.item(i).getChildNodes();
for(int j = 0; j < dict.getLength(); j++) {
Node node = dict.item(j);
if ("key".equals(node.getNodeName()) && node.getTextContent().equals("CFBundleExecutable")) {
upNext = true;
} else if (upNext && "string".equals(node.getNodeName())) {
return path.resolve("Contents/MacOS/" + node.getTextContent());
}
}
}
return null;
}
private interface SystemB extends Library {
SystemB INSTANCE = Native.load("System", SystemB.class);
int PROC_ALL_PIDS = 1;
int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
int sysctlbyname(String name, Pointer oldp, IntByReference oldlenp, Pointer newp, int newlen);
int proc_listpids(int type, int typeinfo, int[] buffer, int buffersize);
int proc_pidpath(int pid, Pointer buffer, int buffersize);
}
}

View File

@@ -0,0 +1,142 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate.firefox.locator;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.Psapi;
import com.sun.jna.platform.win32.Tlhelp32;
import com.sun.jna.platform.win32.WinNT;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
import qz.utils.WindowsUtilities;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Locale;
import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE;
public class WindowsAppLocator extends AppLocator{
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
private static String REG_TEMPLATE = "Software\\%s%s\\%s%s";
@Override
public ArrayList<AppInfo> locate(AppAlias appAlias) {
ArrayList<AppInfo> appList = new ArrayList<>();
for (Alias alias : appAlias.aliases) {
if (alias.getVendor() != null) {
String[] suffixes = new String[]{ "", " ESR"};
String[] prefixes = new String[]{ "", "WOW6432Node\\"};
for (String suffix : suffixes) {
for (String prefix : prefixes) {
String key = String.format(REG_TEMPLATE, prefix, alias.getVendor(), alias.getName(), suffix);
AppInfo appInfo = getAppInfo(alias, key, suffix);
if (appInfo != null && !appList.contains(appInfo)) {
appList.add(appInfo);
}
}
}
}
}
return appList;
}
@Override
public ArrayList<String> getPids(ArrayList<String> processNames) {
ArrayList<String> pidList = new ArrayList<>();
if (processNames.isEmpty()) return pidList;
Tlhelp32.PROCESSENTRY32 pe32 = new Tlhelp32.PROCESSENTRY32();
pe32.dwSize = new WinNT.DWORD(pe32.size());
// Fetch a snapshot of all processes
WinNT.HANDLE hSnapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinNT.DWORD(0));
if (hSnapshot.equals(WinNT.INVALID_HANDLE_VALUE)) {
log.warn("Process snapshot has invalid handle");
return pidList;
}
if (Kernel32.INSTANCE.Process32First(hSnapshot, pe32)) {
do {
String processName = Native.toString(pe32.szExeFile);
if(processNames.contains(processName.toLowerCase(Locale.ENGLISH))) {
pidList.add(pe32.th32ProcessID.toString());
}
} while (Kernel32.INSTANCE.Process32Next(hSnapshot, pe32));
}
Kernel32.INSTANCE.CloseHandle(hSnapshot);
return pidList;
}
@Override
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
ArrayList<Path> pathList = new ArrayList<>();
for(String pid : pids) {
WinNT.HANDLE hProcess = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION | WinNT.PROCESS_VM_READ, false, Integer.parseInt(pid));
if (hProcess == null) {
log.warn("Handle for PID {} is missing, skipping.", pid);
continue;
}
int bufferSize = WinNT.MAX_PATH;
Pointer buffer = new Memory(bufferSize * Native.WCHAR_SIZE);
if (Psapi.INSTANCE.GetModuleFileNameEx(hProcess, null, buffer, bufferSize) == 0) {
log.warn("Full path to PID {} is empty, skipping.", pid);
Kernel32.INSTANCE.CloseHandle(hProcess);
continue;
}
Kernel32.INSTANCE.CloseHandle(hProcess);
pathList.add(Paths.get(Native.WCHAR_SIZE == 1 ?
buffer.getString(0) :
buffer.getWideString(0)));
}
return pathList;
}
/**
* Use a proprietary Firefox-only technique for getting "PathToExe" registry value
*/
private static AppInfo getAppInfo(Alias alias, String key, String suffix) {
String version = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "CurrentVersion");
if (version != null) {
version = version.split(" ")[0]; // chop off (x86 ...)
if (!suffix.isEmpty()) {
if (key.endsWith(suffix)) {
key = key.substring(0, key.length() - suffix.length());
}
version = version + suffix;
}
String exePath = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe");
if (exePath != null) {
// SemVer: Replace spaces in suffixes with dashes
version = version.replaceAll(" ", "-");
return new AppInfo(alias, Paths.get(exePath), version);
} else {
log.warn("Couldn't locate \"PathToExe\" for \"{}\" in \"{}\", skipping", alias.getName(), key);
}
}
return null;
}
}

View File

@@ -0,0 +1,161 @@
package qz.installer.provision;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.Phase;
import qz.build.provision.params.types.Script;
import qz.build.provision.params.types.Software;
import qz.common.Constants;
import qz.installer.provision.invoker.*;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import static qz.common.Constants.*;
import static qz.utils.FileUtilities.*;
public class ProvisionInstaller {
protected static final Logger log = LogManager.getLogger(ProvisionInstaller.class);
private ArrayList<Step> steps;
static {
// Populate variables for scripting environment
ShellUtilities.addEnvp("APP_TITLE", ABOUT_TITLE,
"APP_VERSION", VERSION,
"APP_ABBREV", PROPS_FILE,
"APP_VENDOR", ABOUT_COMPANY,
"APP_VENDOR_ABBREV", DATA_DIR,
"APP_ARCH", SystemUtilities.getArch(),
"APP_OS", SystemUtilities.getOs(),
"APP_DIR", SystemUtilities.getAppPath(),
"APP_USER_DIR", USER_DIR,
"APP_SHARED_DIR", SHARED_DIR);
}
public ProvisionInstaller(Path relativePath) throws IOException, JSONException {
this(relativePath, relativePath.resolve(Constants.PROVISION_FILE).toFile());
}
public ProvisionInstaller(Path relativePath, File jsonFile) throws IOException, JSONException {
if(!jsonFile.exists()) {
log.info("Provision file not found '{}', skipping", jsonFile);
this.steps = new ArrayList<>();
return;
}
this.steps = parse(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8), relativePath);
}
/**
* Package private for internal testing only
* Assumes files located in ./resources/ subdirectory
*/
ProvisionInstaller(Class relativeClass, InputStream in) throws IOException, JSONException {
this(relativeClass, IOUtils.toString(in, StandardCharsets.UTF_8));
}
/**
* Package private for internal testing only
* Assumes files located in ./resources/ subdirectory
*/
ProvisionInstaller(Class relativeClass, String jsonData) throws JSONException {
this.steps = parse(jsonData, relativeClass);
}
public void invoke(Phase phase) {
for(Step step : this.steps) {
if(phase == null || step.getPhase() == phase) {
try {
invokeStep(step);
}
catch(Exception e) {
log.error("[PROVISION] Provisioning step failed '{}'", step, e);
}
}
}
}
public void invoke() {
invoke(null);
}
private static ArrayList<Step> parse(String jsonData, Object relativeObject) throws JSONException {
return parse(new JSONArray(jsonData), relativeObject);
}
private boolean invokeStep(Step step) throws Exception {
if(Os.matchesHost(step.getOs())) {
log.info("[PROVISION] Invoking step '{}'", step.toString());
} else {
log.info("[PROVISION] Skipping step for different OS '{}'", step.toString());
return false;
}
Invokable invoker;
switch(step.getType()) {
case CA:
invoker = new CaInvoker(step, PropertyInvoker.getProperties(step));
break;
case CERT:
invoker = new CertInvoker(step);
break;
case CONF:
invoker = new ConfInvoker(step);
break;
case SCRIPT:
invoker = new ScriptInvoker(step);
break;
case SOFTWARE:
invoker = new SoftwareInvoker(step);
break;
case REMOVER:
invoker = new RemoverInvoker(step);
break;
case RESOURCE:
invoker = new ResourceInvoker(step);
break;
case PREFERENCE:
invoker = new PropertyInvoker(step, PropertyInvoker.getPreferences(step));
break;
case PROPERTY:
invoker = new PropertyInvoker(step, PropertyInvoker.getProperties(step));
break;
default:
throw new UnsupportedOperationException("Type " + step.getType() + " is not yet supported.");
}
return invoker.invoke();
}
public ArrayList<Step> getSteps() {
return steps;
}
private static ArrayList<Step> parse(JSONArray jsonArray, Object relativeObject) throws JSONException {
ArrayList<Step> steps = new ArrayList<>();
for(int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonStep = jsonArray.getJSONObject(i);
try {
steps.add(Step.parse(jsonStep, relativeObject));
} catch(Exception e) {
log.warn("[PROVISION] Unable to add step '{}'", jsonStep, e);
}
}
return steps;
}
public static boolean shouldBeExecutable(Path path) {
return Script.parse(path) != null || Software.parse(path) != Software.UNKNOWN;
}
}

View File

@@ -0,0 +1,49 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.PropertyHelper;
import qz.utils.ArgValue;
import qz.utils.FileUtilities;
import java.io.File;
import java.io.IOException;
/**
* Combines ResourceInvoker and PropertyInvoker to deploy a file and set a property to its deployed path
*/
public class CaInvoker extends InvokableResource {
Step step;
PropertyHelper properties;
public CaInvoker(Step step, PropertyHelper properties) {
this.step = step;
this.properties = properties;
}
@Override
public boolean invoke() throws IOException {
// First, write our cert file
File caCert = dataToFile(step);
if(caCert == null) {
return false;
}
// Next, handle our property step
Step propsStep = step.clone();
// If the property already exists, snag it
String key = ArgValue.AUTHCERT_OVERRIDE.getMatch();
String value = caCert.getPath();
if (properties.containsKey(key)) {
value = properties.getProperty(key) + FileUtilities.FILE_SEPARATOR + value;
}
propsStep.setData(String.format("%s=%s", key, value));
if (new PropertyInvoker(propsStep, properties).invoke()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.Constants;
import qz.utils.FileUtilities;
import java.io.File;
import static qz.utils.ArgParser.ExitStatus.*;
public class CertInvoker extends InvokableResource {
private Step step;
public CertInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File cert = dataToFile(step);
if(cert == null) {
return false;
}
return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS;
}
}

View File

@@ -0,0 +1,46 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.PropertyHelper;
import qz.utils.SystemUtilities;
import java.util.AbstractMap;
public class ConfInvoker extends PropertyInvoker {
public ConfInvoker(Step step) {
super(step, new PropertyHelper(calculateConfPath(step)));
}
public static String calculateConfPath(Step step) {
String relativePath = step.getArgs().get(0);
if(SystemUtilities.isMac()) {
return SystemUtilities.getJarParentPath().
resolve("../PlugIns/Java.runtime/Contents/Home/conf").
resolve(relativePath).
normalize()
.toString();
} else {
return SystemUtilities.getJarParentPath()
.resolve("runtime/conf")
.resolve(relativePath)
.normalize()
.toString();
}
}
@Override
public boolean invoke() {
Step step = getStep();
// Java uses the same "|" delimiter as we do, only parse one property at a time
AbstractMap.SimpleEntry<String, String> pair = parsePropertyPair(step, step.getData());
if (!pair.getValue().isEmpty()) {
properties.setProperty(pair);
if (properties.save()) {
log.info("Successfully provisioned '1' '{}'", step.getType());
return true;
}
log.error("An error occurred saving properties '{}' to file", step.getData());
}
return false;
}
}

View File

@@ -0,0 +1,10 @@
package qz.installer.provision.invoker;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public interface Invokable {
Logger log = LogManager.getLogger(Invokable.class);
boolean invoke() throws Exception;
}

View File

@@ -0,0 +1,63 @@
package qz.installer.provision.invoker;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.build.provision.Step;
import qz.build.provision.params.Type;
import qz.common.Constants;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
public abstract class InvokableResource implements Invokable {
static final Logger log = LogManager.getLogger(InvokableResource.class);
public static File dataToFile(Step step) throws IOException {
Path resourcePath = Paths.get(step.getData());
if(resourcePath.isAbsolute() || step.usingPath()) {
return pathResourceToFile(step);
}
if(step.usingClass()) {
return classResourceToFile(step);
}
return null;
}
/**
* Resolves the resource directly from file
*/
private static File pathResourceToFile(Step step) {
String resourcePath = step.getData();
Path dataPath = Paths.get(resourcePath);
return dataPath.isAbsolute() ? dataPath.toFile() : step.getRelativePath().resolve(resourcePath).toFile();
}
/**
* Copies resource from JAR to a temp file for use in installation
*/
private static File classResourceToFile(Step step) throws IOException {
// Resource may be inside the jar
InputStream in = step.getRelativeClass().getResourceAsStream("resources/" + step.getData());
if(in == null) {
log.warn("Resource '{}' is missing, skipping step", step.getData());
return null;
}
String suffix = "_" + Paths.get(step.getData()).getFileName().toString();
File destination = File.createTempFile(Constants.DATA_DIR + "_provision_", suffix);
Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
IOUtils.closeQuietly(in);
// Set scripts executable
if(step.getType() == Type.SCRIPT && !SystemUtilities.isWindows()) {
destination.setExecutable(true, false);
}
return destination;
}
}

View File

@@ -0,0 +1,99 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.Constants;
import qz.common.PropertyHelper;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
public class PropertyInvoker implements Invokable {
private Step step;
PropertyHelper properties;
public PropertyInvoker(Step step, PropertyHelper properties) {
this.step = step;
this.properties = properties;
}
public boolean invoke() {
HashMap<String, String> pairs = parsePropertyPairs(step);
if (!pairs.isEmpty()) {
for(Map.Entry<String, String> pair : pairs.entrySet()) {
properties.setProperty(pair);
}
if (properties.save()) {
log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType());
return true;
}
log.error("An error occurred saving properties '{}' to file", step.getData());
}
return false;
}
public static PropertyHelper getProperties(Step step) {
File propertiesFile;
if(step.getRelativePath() != null) {
// Assume qz-tray.properties is one directory up from provision folder
// required to prevent installing to payload
propertiesFile = step.getRelativePath().getParent().resolve(Constants.PROPS_FILE + ".properties").toFile();
} else {
// If relative path isn't set, fallback to the jar's parent path
propertiesFile = SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile();
}
log.info("Provisioning '{}' to properties file: '{}'", step.getData(), propertiesFile);
return new PropertyHelper(propertiesFile);
}
public static PropertyHelper getPreferences(Step step) {
return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
}
public static HashMap<String, String> parsePropertyPairs(Step step) {
HashMap<String, String> pairs = new HashMap<>();
if(step.getData() != null && !step.getData().trim().isEmpty()) {
String[] props = step.getData().split("\\|");
for(String prop : props) {
AbstractMap.SimpleEntry<String,String> pair = parsePropertyPair(step, prop);
if (pair != null) {
if(pairs.get(pair.getKey()) != null) {
log.warn("Property {} already exists, replacing [before: {}, after: {}] ",
pair.getKey(), pairs.get(pair.getKey()), pair.getValue());
}
pairs.put(pair.getKey(), pair.getValue());
}
}
} else {
log.error("Skipping Step '{}', Data is null or empty", step.getType());
}
return pairs;
}
public static AbstractMap.SimpleEntry<String, String> parsePropertyPair(Step step, String prop) {
if(prop.contains("=")) {
String[] pair = prop.split("=", 2);
if (!pair[0].trim().isEmpty()) {
if (!pair[1].trim().isEmpty()) {
return new AbstractMap.SimpleEntry<>(pair[0], pair[1]);
} else {
log.warn("Skipping '{}' '{}', property value is malformed", step.getType(), prop);
}
} else {
log.warn("Skipping '{}' '{}', property name is malformed", step.getType(), prop);
}
} else {
log.warn("Skipping '{}' '{}', property is malformed", step.getType(), prop);
}
return null;
}
public Step getStep() {
return step;
}
}

View File

@@ -0,0 +1,100 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Remover;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
public class RemoverInvoker extends InvokableResource {
private Step step;
private String aboutTitle; // e.g. "QZ Tray"
private String propsFile; // e.g. "qz-tray"
private String dataDir; // e.g. "qz"
public RemoverInvoker(Step step) {
this.step = step;
Remover remover = Remover.parse(step.getData());
if(remover == Remover.CUSTOM) {
// Fields are comma delimited in the data field
parseCustomFromData(step.getData());
} else {
aboutTitle = remover.getAboutTitle();
propsFile = remover.getPropsFile();
dataDir = remover.getDataDir();
}
}
@Override
public boolean invoke() throws Exception {
ArrayList<String> command = getRemoveCommand();
if(command.size() == 0) {
log.info("An existing installation of '{}' was not found. Skipping.", aboutTitle);
return true;
}
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
public void parseCustomFromData(String data) {
String[] parts = data.split(",");
aboutTitle = parts[0].trim();
propsFile = parts[1].trim();
dataDir = parts[2].trim();
}
/**
* Returns the installer command (including the installer itself and if needed, arguments) to
* invoke the installer file
*/
public ArrayList<String> getRemoveCommand() {
ArrayList<String> removeCmd = new ArrayList<>();
Os os = SystemUtilities.getOs();
switch(os) {
case WINDOWS:
Path win = Paths.get(System.getenv("PROGRAMFILES"))
.resolve(aboutTitle)
.resolve("uninstall.exe");
if(win.toFile().exists()) {
removeCmd.add(win.toString());
removeCmd.add("/S");
break;
}
case MAC:
Path legacy = Paths.get("/Applications")
.resolve(aboutTitle + ".app")
.resolve("Contents")
.resolve("uninstall");
Path mac = Paths.get("/Applications")
.resolve(aboutTitle + ".app")
.resolve("Contents")
.resolve("Resources")
.resolve("uninstall");
if(legacy.toFile().exists()) {
removeCmd.add(legacy.toString());
} else if(mac.toFile().exists()) {
removeCmd.add(mac.toString());
}
break;
default:
Path linux = Paths.get("/opt")
.resolve(propsFile)
.resolve("uninstall");
if(linux.toFile().exists()) {
removeCmd.add(linux.toString());
}
}
return removeCmd;
}
}

View File

@@ -0,0 +1,19 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
/**
* Stub class for deploying an otherwise "action-less" resource, only to be used by other tasks
*/
public class ResourceInvoker extends InvokableResource {
private Step step;
public ResourceInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
return dataToFile(step) != null;
}
}

View File

@@ -0,0 +1,77 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Script;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.ArrayList;
public class ScriptInvoker extends InvokableResource {
private Step step;
public ScriptInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File script = dataToFile(step);
if(script == null) {
return false;
}
Script engine = Script.parse(step.getData());
ArrayList<String> command = getInterpreter(engine);
if(command.isEmpty() && SystemUtilities.isWindows()) {
log.warn("No interpreter found for {}, skipping", step.getData());
return false;
}
command.add(script.toString());
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
/**
* Returns the interpreter command (and if needed, arguments) to invoke the script file
*
* An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3
* which will allow the OS to select the correct interpreter for the given file
*
* No special attention is given to "shebang", behavior may differ between OSs
*/
private static ArrayList<String> getInterpreter(Script engine) {
ArrayList<String> interpreter = new ArrayList<>();
Os osType = SystemUtilities.getOs();
switch(engine) {
case PS1:
if(osType == Os.WINDOWS) {
interpreter.add("powershell.exe");
} else if(osType == Os.MAC) {
interpreter.add("/usr/local/bin/pwsh");
} else {
interpreter.add("pwsh");
}
interpreter.add("-File");
break;
case PY:
interpreter.add(osType == Os.WINDOWS ? "python3.exe" : "python3");
break;
case BAT:
interpreter.add(osType == Os.WINDOWS ? "cmd.exe" : "wineconsole");
break;
case RB:
interpreter.add(osType == Os.WINDOWS ? "ruby.exe" : "ruby");
break;
case SH:
default:
// Allow the environment to parse it from the shebang at invocation time
}
return interpreter;
}
}

View File

@@ -0,0 +1,87 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Software;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class SoftwareInvoker extends InvokableResource {
private Step step;
public SoftwareInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File payload = dataToFile(step);
if(payload == null) {
return false;
}
Software installer = Software.parse(step.getData());
ArrayList<String> command = getInstallCommand(installer, step.getArgs(), payload);
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]), payload.getParentFile());
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
/**
* Returns the installer command (including the installer itself and if needed, arguments) to
* invoke the installer file
*/
public ArrayList<String> getInstallCommand(Software installer, List<String> args, File payload) {
ArrayList<String> interpreter = new ArrayList<>();
Os os = SystemUtilities.getOs();
switch(installer) {
case EXE:
if(!SystemUtilities.isWindows()) {
interpreter.add("wine");
}
// Executable on its own
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume exe args come after payload
break;
case MSI:
interpreter.add(os == Os.WINDOWS ? "msiexec.exe" : "msiexec");
interpreter.add("/i"); // Assume standard install
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume msiexec args come after payload
break;
case PKG:
if(os == Os.MAC) {
interpreter.add("installer");
interpreter.addAll(args); // Assume installer args come before payload
interpreter.add("-package");
interpreter.add(payload.toString());
interpreter.add("-target");
interpreter.add("/"); // Assume we don't want this on a removable volume
} else {
throw new UnsupportedOperationException("PKG is not yet supported on this platform");
}
break;
case DMG:
// DMG requires "hdiutil attach", but the mount point is unknown
throw new UnsupportedOperationException("DMG is not yet supported");
case RUN:
if(SystemUtilities.isWindows()) {
interpreter.add("bash");
interpreter.add("-c");
}
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume run args come after payload
// Executable on its own
break;
default:
// We'll try to parse it from the shebang just before invocation time
}
return interpreter;
}
}

View File

@@ -0,0 +1,44 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.shortcut;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.installer.LinuxInstaller;
/**
* @author Tres Finocchiaro
*/
class LinuxShortcutCreator extends ShortcutCreator {
private static final Logger log = LogManager.getLogger(LinuxShortcutCreator.class);
private static String DESKTOP = System.getProperty("user.home") + "/Desktop/";
public boolean canAutoStart() {
return Files.exists(Paths.get(LinuxInstaller.STARTUP_DIR, LinuxInstaller.SHORTCUT_NAME));
}
public void createDesktopShortcut() {
copyShortcut(LinuxInstaller.APP_LAUNCHER, DESKTOP);
}
private static void copyShortcut(String source, String target) {
try {
Files.copy(Paths.get(source), Paths.get(target));
} catch(IOException e) {
log.warn("Error creating shortcut {}", target, e);
}
}
}

View File

@@ -0,0 +1,100 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.shortcut;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import qz.common.Constants;
import qz.utils.MacUtilities;
import qz.utils.SystemUtilities;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @author Tres Finocchiaro
*/
class MacShortcutCreator extends ShortcutCreator {
private static final Logger log = LogManager.getLogger(MacShortcutCreator.class);
private static String SHORTCUT_PATH = System.getProperty("user.home") + "/Desktop/" + Constants.ABOUT_TITLE;
/**
* Verify LaunchAgents plist file exists and parse it to verify it's enabled
*/
@Override
public boolean canAutoStart() {
// plist is stored as io.qz.plist
Path plistPath = Paths.get("/Library/LaunchAgents", MacUtilities.getBundleId() + ".plist");
if (Files.exists(plistPath)) {
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(plistPath.toFile());
doc.getDocumentElement().normalize();
NodeList dictList = doc.getElementsByTagName("dict");
// Loop to find "RunAtLoad" key, then the adjacent key
boolean foundItem = false;
if (dictList.getLength() > 0) {
NodeList children = dictList.item(0).getChildNodes();
for(int n = 0; n < children.getLength(); n++) {
Node item = children.item(n);
// Apple stores booleans as adjacent tags to their owner
if (foundItem) {
String nodeName = children.item(n).getNodeName();
log.debug("Found RunAtLoad value {}", nodeName);
return "true".equals(nodeName);
}
if (item.getNodeName().equals("key") && item.getTextContent().equals("RunAtLoad")) {
log.debug("Found RunAtLoad key in {}", plistPath);
foundItem = true;
}
}
}
log.warn("RunAtLoad was not in plist {}, autostart will not work.", plistPath);
}
catch(SAXException | IOException | ParserConfigurationException e) {
log.warn("Error reading plist {}, autostart will not work.", plistPath, e);
}
} else {
log.warn("No plist {} found, autostart will not work", plistPath);
}
return false;
}
public void createDesktopShortcut() {
try {
new File(SHORTCUT_PATH).delete();
if(SystemUtilities.getJarParentPath().endsWith("Contents")) {
// We're probably running from an .app bundle
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getAppPath());
} else {
// We're running from a mystery location, use the jar instead
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getJarPath());
}
} catch(IOException e) {
log.warn("Could not create desktop shortcut {}", SHORTCUT_PATH, e);
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.shortcut;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.SystemUtilities;
/**
* Utility class for creating, querying and removing startup shortcuts and
* desktop shortcuts.
*
* @author Tres Finocchiaro
*/
public abstract class ShortcutCreator {
private static ShortcutCreator instance;
protected static final Logger log = LogManager.getLogger(ShortcutCreator.class);
public abstract boolean canAutoStart();
public abstract void createDesktopShortcut();
public static ShortcutCreator getInstance() {
if (instance == null) {
if (SystemUtilities.isWindows()) {
instance = new WindowsShortcutCreator();
} else if (SystemUtilities.isMac()) {
instance = new MacShortcutCreator();
} else {
instance = new LinuxShortcutCreator();
}
}
return instance;
}
}

View File

@@ -0,0 +1,60 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*
*/
package qz.installer.shortcut;
import com.sun.jna.platform.win32.Win32Exception;
import mslinks.ShellLinkException;
import mslinks.ShellLinkHelper;
import qz.common.Constants;
import qz.installer.WindowsSpecialFolders;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
/**
* @author Tres Finocchiaro
*/
public class WindowsShortcutCreator extends ShortcutCreator {
private static String SHORTCUT_NAME = Constants.ABOUT_TITLE + ".lnk";
public void createDesktopShortcut() {
createShortcut(WindowsSpecialFolders.DESKTOP.toString());
}
public boolean canAutoStart() {
try {
return Files.exists(Paths.get(WindowsSpecialFolders.COMMON_STARTUP.toString(), SHORTCUT_NAME));
} catch(Win32Exception e) {
log.warn("An exception occurred locating the startup folder; autostart cannot be determined.", e);
}
return false;
}
private void createShortcut(String folderPath) {
try {
ShellLinkHelper.createLink(getAppPath(), folderPath + File.separator + SHORTCUT_NAME);
}
catch(ShellLinkException | IOException ex) {
log.warn("Error creating desktop shortcut", ex);
}
}
/**
* Calculates .exe path from .jar
* fixme: overlaps SystemUtilities.getAppPath
*/
private static String getAppPath() {
return SystemUtilities.getJarPath().toString().replaceAll(".jar$", ".exe");
}
}