updated control access
This commit is contained in:
413
old code/tray/src/qz/installer/Installer.java
Executable file
413
old code/tray/src/qz/installer/Installer.java
Executable 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)));
|
||||
}
|
||||
}
|
||||
371
old code/tray/src/qz/installer/LinuxInstaller.java
Executable file
371
old code/tray/src/qz/installer/LinuxInstaller.java
Executable 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;
|
||||
}
|
||||
|
||||
}
|
||||
125
old code/tray/src/qz/installer/MacInstaller.java
Executable file
125
old code/tray/src/qz/installer/MacInstaller.java
Executable 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()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
227
old code/tray/src/qz/installer/TaskKiller.java
Executable file
227
old code/tray/src/qz/installer/TaskKiller.java
Executable 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;
|
||||
}
|
||||
}
|
||||
208
old code/tray/src/qz/installer/WindowsInstaller.java
Executable file
208
old code/tray/src/qz/installer/WindowsInstaller.java
Executable 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()]));
|
||||
}
|
||||
}
|
||||
97
old code/tray/src/qz/installer/WindowsSpecialFolders.java
Executable file
97
old code/tray/src/qz/installer/WindowsSpecialFolders.java
Executable 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();
|
||||
}
|
||||
}
|
||||
8
old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in
Executable file
8
old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in
Executable 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
|
||||
2
old code/tray/src/qz/installer/assets/linux-udev.rules.in
Executable file
2
old code/tray/src/qz/installer/assets/linux-udev.rules.in
Executable file
@@ -0,0 +1,2 @@
|
||||
# %ABOUT_TITLE% usb override settings
|
||||
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"
|
||||
18
old code/tray/src/qz/installer/assets/mac-launchagent.plist.in
Executable file
18
old code/tray/src/qz/installer/assets/mac-launchagent.plist.in
Executable 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>
|
||||
147
old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java
Executable file
147
old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java
Executable 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;
|
||||
}
|
||||
}
|
||||
478
old code/tray/src/qz/installer/certificate/CertificateManager.java
Executable file
478
old code/tray/src/qz/installer/certificate/CertificateManager.java
Executable 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;
|
||||
}
|
||||
}
|
||||
295
old code/tray/src/qz/installer/certificate/ExpiryTask.java
Executable file
295
old code/tray/src/qz/installer/certificate/ExpiryTask.java
Executable 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;
|
||||
}
|
||||
|
||||
}
|
||||
130
old code/tray/src/qz/installer/certificate/KeyPairWrapper.java
Executable file
130
old code/tray/src/qz/installer/certificate/KeyPairWrapper.java
Executable 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;
|
||||
}
|
||||
}
|
||||
365
old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java
Executable file
365
old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java
Executable 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");
|
||||
}
|
||||
}
|
||||
91
old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java
Executable file
91
old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java
Executable file
105
old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java
Executable 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();
|
||||
}
|
||||
236
old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java
Executable file
236
old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
Executable file
136
old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
Executable 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package qz.installer.certificate.firefox;
|
||||
|
||||
class ConflictingPolicyException extends Exception {
|
||||
ConflictingPolicyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
91
old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java
Executable file
91
old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java
Executable file
100
old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java
Executable 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;
|
||||
}
|
||||
}
|
||||
87
old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java
Executable file
87
old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
159
old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
Executable file
159
old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
Executable 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);
|
||||
}
|
||||
}
|
||||
168
old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
Executable file
168
old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
Executable 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
161
old code/tray/src/qz/installer/provision/ProvisionInstaller.java
Executable file
161
old code/tray/src/qz/installer/provision/ProvisionInstaller.java
Executable 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;
|
||||
}
|
||||
}
|
||||
49
old code/tray/src/qz/installer/provision/invoker/CaInvoker.java
Executable file
49
old code/tray/src/qz/installer/provision/invoker/CaInvoker.java
Executable 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;
|
||||
}
|
||||
}
|
||||
26
old code/tray/src/qz/installer/provision/invoker/CertInvoker.java
Executable file
26
old code/tray/src/qz/installer/provision/invoker/CertInvoker.java
Executable 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;
|
||||
}
|
||||
}
|
||||
46
old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java
Executable file
46
old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java
Executable 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;
|
||||
}
|
||||
}
|
||||
10
old code/tray/src/qz/installer/provision/invoker/Invokable.java
Executable file
10
old code/tray/src/qz/installer/provision/invoker/Invokable.java
Executable 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;
|
||||
}
|
||||
63
old code/tray/src/qz/installer/provision/invoker/InvokableResource.java
Executable file
63
old code/tray/src/qz/installer/provision/invoker/InvokableResource.java
Executable 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;
|
||||
}
|
||||
}
|
||||
99
old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java
Executable file
99
old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java
Executable 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;
|
||||
}
|
||||
}
|
||||
100
old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java
Executable file
100
old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java
Executable 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;
|
||||
}
|
||||
}
|
||||
19
old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java
Executable file
19
old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java
Executable 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;
|
||||
}
|
||||
}
|
||||
77
old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java
Executable file
77
old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java
Executable 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;
|
||||
}
|
||||
}
|
||||
87
old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java
Executable file
87
old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java
Executable 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;
|
||||
}
|
||||
|
||||
}
|
||||
44
old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java
Executable file
44
old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java
Executable file
100
old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
old code/tray/src/qz/installer/shortcut/ShortcutCreator.java
Executable file
41
old code/tray/src/qz/installer/shortcut/ShortcutCreator.java
Executable 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;
|
||||
}
|
||||
}
|
||||
60
old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java
Executable file
60
old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java
Executable 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user