diff --git a/src/qz/installer/Installer.java b/src/qz/installer/Installer.java index 2a68fea3b..d1c4b41bf 100644 --- a/src/qz/installer/Installer.java +++ b/src/qz/installer/Installer.java @@ -27,9 +27,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Locale; -import java.util.Properties; +import java.util.*; import static qz.common.Constants.*; import static qz.installer.certificate.KeyPairWrapper.Type.CA; @@ -280,4 +278,8 @@ public static Properties persistProperties(File oldFile, Properties newProps) { } return newProps; } + + public void spawn(String ... args) throws Exception { + spawn(new ArrayList(Arrays.asList(args))); + } } diff --git a/src/qz/installer/LinuxInstaller.java b/src/qz/installer/LinuxInstaller.java index 433f66e7d..1780ac643 100644 --- a/src/qz/installer/LinuxInstaller.java +++ b/src/qz/installer/LinuxInstaller.java @@ -1,5 +1,6 @@ package qz.installer; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qz.utils.FileUtilities; @@ -159,9 +160,12 @@ public Installer removeSystemSettings() { * Spawns the process as the underlying regular user account, preserving the environment */ public void spawn(List args) throws Exception { - args.remove(0); // the first arg is "spawn", remove it + if(!SystemUtilities.isAdmin()) { + ShellUtilities.execute(args.toArray(new String[args.size()])); + return; + } String whoami = ShellUtilities.executeRaw("logname").trim(); - if(whoami.isEmpty()) { + if(whoami.isEmpty() || SystemUtilities.isSolaris()) { whoami = System.getenv("SUDO_USER"); } @@ -183,8 +187,25 @@ public void spawn(List args) throws Exception { ArrayList toExport = new ArrayList<>(Arrays.asList(SUDO_EXPORTS)); for(String pid : pids) { try { - String delim = Pattern.compile("\0").pattern(); - String[] vars = new String(Files.readAllBytes(Paths.get(String.format("/proc/%s/environ", pid)))).split(delim); + 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) { @@ -195,16 +216,20 @@ public void spawn(List args) throws Exception { } } } - } catch(Exception ignore) {} + } catch(Exception e) { + log.warn("An unexpected error occurred obtaining dbus info", e); + } // Only add vars for the current user if(whoami.trim().equals(tempEnv.get("USER"))) { env.putAll(tempEnv); + } else { + log.debug("Expected USER={} but got USER={}, skipping results for {}", whoami, tempEnv.get("USER"), pid); } } if(env.size() == 0) { - throw new Exception("Unable to get dbus info from /proc, can't spawn instance"); + throw new Exception("Unable to get dbus info; can't spawn instance"); } // Prepare the environment @@ -221,20 +246,20 @@ public void spawn(List args) throws Exception { // Determine if this environment likes sudo String[] sudoCmd = { "sudo", "-E", "-u", whoami, "nohup" }; String[] suCmd = { "su", whoami, "-c", "nohup" }; - String[] asUser = ShellUtilities.execute("which", "sudo") ? sudoCmd : suCmd; - // Build and escape our command - List argsList = new ArrayList<>(); - argsList.addAll(Arrays.asList(asUser)); - String command = ""; - Pattern quote = Pattern.compile("\""); - for(String arg : args) { - command += String.format(" %s", arg); + ArrayList argsList = new ArrayList<>(); + if(ShellUtilities.execute("which", "sudo")) { + // Pass directly into sudo + argsList.addAll(Arrays.asList(sudoCmd)); + argsList.addAll(args); + } else { + // Build and escape for su + argsList.addAll(Arrays.asList(suCmd)); + argsList.addAll(Arrays.asList(StringUtils.join(args, "\" \"") + "\"")); } - argsList.add(command.trim()); // Spawn - System.out.println(String.join(" ", argsList)); + log.info("Executing: {}", Arrays.toString(argsList.toArray())); Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp); } diff --git a/src/qz/installer/MacInstaller.java b/src/qz/installer/MacInstaller.java index a27d9f309..b6eb94eec 100644 --- a/src/qz/installer/MacInstaller.java +++ b/src/qz/installer/MacInstaller.java @@ -9,13 +9,13 @@ * this software. http://www.gnu.org/licenses/lgpl-2.1.html */ +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qz.utils.FileUtilities; import qz.utils.ShellUtilities; import qz.utils.SystemUtilities; -import javax.swing.*; import java.io.File; import java.io.IOException; import java.util.HashMap; @@ -120,6 +120,16 @@ public static String getPackageName() { } public void spawn(List args) throws Exception { - throw new UnsupportedOperationException("Spawn is not yet support on Mac"); + if(SystemUtilities.isAdmin()) { + // macOS unconventionally uses "$USER" during its install process + String whoami = System.getenv("USER"); + if(whoami == null || whoami.isEmpty() || whoami.equals("root")) { + // Fallback, should only fire via Terminal + sudo + whoami = ShellUtilities.executeRaw("logname").trim(); + } + ShellUtilities.execute("su", whoami, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""); + } else { + ShellUtilities.execute(args.toArray(new String[args.size()])); + } } } diff --git a/src/qz/installer/TaskKiller.java b/src/qz/installer/TaskKiller.java index 5ef45ddf0..7f4de4b88 100644 --- a/src/qz/installer/TaskKiller.java +++ b/src/qz/installer/TaskKiller.java @@ -2,6 +2,9 @@ import com.sun.jna.platform.win32.Kernel32; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qz.installer.certificate.firefox.locator.AppLocator; import qz.utils.MacUtilities; import qz.utils.ShellUtilities; import qz.utils.SystemUtilities; @@ -12,16 +15,13 @@ import java.util.Arrays; import java.util.List; -import static qz.common.Constants.ABOUT_TITLE; import static qz.common.Constants.PROPS_FILE; -import static qz.installer.Installer.InstallType.PREINSTALL; public class TaskKiller { - private static final String[] JAVA_PID_QUERY_POSIX = {"pgrep", "java" }; + protected static final Logger log = LoggerFactory.getLogger(TaskKiller.class); private static final String[] TRAY_PID_QUERY_POSIX = {"pgrep", "-f", PROPS_FILE + ".jar" }; private static final String[] KILL_PID_CMD_POSIX = {"kill", "-9", ""/*pid placeholder*/}; - private static final String[] JAVA_PID_QUERY_WIN32 = {"wmic.exe", "process", "where", "Name like '%java%'", "get", "processid" }; private static final String[] TRAY_PID_QUERY_WIN32 = {"wmic.exe", "process", "where", "CommandLine like '%" + PROPS_FILE + ".jar" + "%'", "get", "processid" }; private static final String[] KILL_PID_CMD_WIN32 = {"taskkill.exe", "/F", "/PID", "" /*pid placeholder*/ }; @@ -31,25 +31,26 @@ public class TaskKiller { public static boolean killAll() { boolean success = true; - String[] javaProcs; + ArrayList javaProcs; String[] trayProcs; int selfProc; String[] killCmd; if(SystemUtilities.isWindows()) { - javaProcs = ShellUtilities.executeRaw(JAVA_PID_QUERY_WIN32).split("\\s*\\r?\\n"); + // Windows may be running under javaw.exe (normal) or java.exe (terminal) + javaProcs = AppLocator.getInstance().getPids("java.exe", "javaw.exe"); trayProcs = ShellUtilities.executeRaw(TRAY_PID_QUERY_WIN32).split("\\s*\\r?\\n"); selfProc = Kernel32.INSTANCE.GetCurrentProcessId(); killCmd = KILL_PID_CMD_WIN32; } else { - javaProcs = ShellUtilities.executeRaw(JAVA_PID_QUERY_POSIX).split("\\s*\\r?\\n"); + javaProcs = AppLocator.getInstance().getPids( "java"); trayProcs = ShellUtilities.executeRaw(TRAY_PID_QUERY_POSIX).split("\\s*\\r?\\n"); selfProc = MacUtilities.getProcessID(); // Works for Linux too killCmd = KILL_PID_CMD_POSIX; } - if (javaProcs.length > 0) { + if (!javaProcs.isEmpty()) { // Find intersections of java and qz-tray.jar List intersections = new ArrayList<>(Arrays.asList(trayProcs)); - intersections.retainAll(Arrays.asList(javaProcs)); + intersections.retainAll(javaProcs); // Remove any instances created by this installer intersections.remove("" + selfProc); diff --git a/src/qz/installer/WindowsInstaller.java b/src/qz/installer/WindowsInstaller.java index f8854e3e1..1b74dde7c 100644 --- a/src/qz/installer/WindowsInstaller.java +++ b/src/qz/installer/WindowsInstaller.java @@ -17,6 +17,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; import qz.utils.WindowsUtilities; import qz.ws.PrintSocketServer; @@ -193,6 +194,9 @@ public String getDestination() { } public void spawn(List args) throws Exception { - throw new UnsupportedOperationException("Spawn is not yet support on Windows"); + if(SystemUtilities.isAdmin()) { + log.warn("Spawning as user isn't implemented; starting process with elevation instead"); + } + ShellUtilities.execute(args.toArray(new String[args.size()])); } } diff --git a/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java b/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java index ecb9bccef..31f4b0bbb 100644 --- a/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java +++ b/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java @@ -15,8 +15,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.SystemUtilities; @@ -24,7 +26,6 @@ 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; @@ -44,6 +45,7 @@ public class FirefoxCertificateInstaller { 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"); private static String ENTERPRISE_ROOT_POLICY = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }"; private static String INSTALL_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION + "\"] } } }"; @@ -53,63 +55,74 @@ public class FirefoxCertificateInstaller { public static final String MAC_POLICY_LOCATION = "Contents/Resources/" + POLICY_LOCATION; public static void install(X509Certificate cert, String ... hostNames) { - ArrayList appList = AppLocator.locate(AppAlias.FIREFOX); - for(AppLocator app : appList) { - if(honorsPolicy(app)) { - log.info("Installing Firefox ({}) enterprise root certificate policy {}", app.getName(), app.getPath()); - installPolicy(app, cert); + ArrayList appList = AppLocator.getInstance().locate(AppAlias.FIREFOX); + ArrayList processPaths = AppLocator.getRunningPaths(appList); + for(AppInfo appInfo : appList) { + if (honorsPolicy(appInfo)) { + log.info("Installing Firefox ({}) enterprise root certificate policy {}", appInfo.getName(), appInfo.getPath()); + installPolicy(appInfo, cert); } else { - log.info("Installing Firefox ({}) auto-config script {}", app.getName(), app.getPath()); + log.info("Installing Firefox ({}) auto-config script {}", appInfo.getName(), appInfo.getPath()); try { String certData = Base64.getEncoder().encodeToString(cert.getEncoded()); - LegacyFirefoxCertificateInstaller.installAutoConfigScript(app, certData, hostNames); - } catch(CertificateEncodingException e) { - log.warn("Unable to install auto-config script to {}", app.getPath(), e); + LegacyFirefoxCertificateInstaller.installAutoConfigScript(appInfo, certData, hostNames); } + catch(CertificateEncodingException e) { + log.warn("Unable to install auto-config script to {}", appInfo.getPath(), e); + } + } + + if(processPaths.contains(appInfo.getExePath())) { + if (appInfo.getVersion().greaterThanOrEqualTo(FIREFOX_RESTART_VERSION)) { + try { + Installer.getInstance().spawn(appInfo.getExePath().toString(), "-private", "about:restartrequired"); + continue; + } catch(Exception ignore) {} + } + log.warn("{} must be restarted for changes to take effect", appInfo.getName()); } } } public static void uninstall() { - ArrayList appList = AppLocator.locate(AppAlias.FIREFOX); - for(AppLocator app : appList) { - if(honorsPolicy(app)) { + ArrayList 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 {}", app.getPath()); + log.info("Skipping uninstall of Firefox enterprise root certificate policy {}", appInfo.getPath()); } else { try { - File policy = Paths.get(app.getPath(), POLICY_LOCATION).toFile(); + File policy = appInfo.getPath().resolve(POLICY_LOCATION).toFile(); if(policy.exists()) { - JsonWriter.write(Paths.get(app.getPath(), POLICY_LOCATION).toString(), INSTALL_CERT_POLICY, false, true); + JsonWriter.write(appInfo.getPath().resolve(POLICY_LOCATION).toString(), INSTALL_CERT_POLICY, false, true); } } catch(IOException | JSONException e) { - log.warn("Unable to remove Firefox ({}) policy {}", app.getName(), e); + log.warn("Unable to remove Firefox ({}) policy {}", appInfo.getName(), e); } } - } else { - log.info("Uninstalling Firefox auto-config script {}", app.getPath()); - LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(app); + log.info("Uninstalling Firefox auto-config script {}", appInfo.getPath()); + LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(appInfo); } } } - public static boolean honorsPolicy(AppLocator app) { - if (app.getVersion() == null) { - log.warn("Firefox-compatible browser was found {}, but no version information is available", app.getPath()); + public static boolean honorsPolicy(AppInfo appInfo) { + if (appInfo.getVersion() == null) { + log.warn("Firefox-compatible browser was found {}, but no version information is available", appInfo.getPath()); return false; } if(SystemUtilities.isWindows()) { - return app.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION); + return appInfo.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION); } else if (SystemUtilities.isMac()) { - return app.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION); + return appInfo.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION); } else { - return app.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION); + return appInfo.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION); } } - public static void installPolicy(AppLocator app, X509Certificate cert) { - Path jsonPath = Paths.get(app.getPath(), SystemUtilities.isMac() ? MAC_POLICY_LOCATION : POLICY_LOCATION); + public static void installPolicy(AppInfo app, X509Certificate cert) { + Path jsonPath = app.getPath().resolve(SystemUtilities.isMac() ? MAC_POLICY_LOCATION : POLICY_LOCATION); String jsonPolicy = SystemUtilities.isWindows() || SystemUtilities.isMac() ? ENTERPRISE_ROOT_POLICY : INSTALL_CERT_POLICY; try { if(jsonPolicy.equals(INSTALL_CERT_POLICY)) { @@ -151,8 +164,4 @@ public static void installPolicy(AppLocator app, X509Certificate cert) { log.warn("Could not install enterprise policy {} to {}", jsonPolicy, jsonPath.toString(), e); } } - - public static boolean checkRunning(AppLocator app, boolean isSilent) { - throw new UnsupportedOperationException(); - } } diff --git a/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java b/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java index c998d0686..8139c0086 100644 --- a/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java +++ b/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java @@ -14,11 +14,12 @@ import org.slf4j.LoggerFactory; import qz.common.Constants; import qz.installer.certificate.CertificateChainBuilder; -import qz.installer.certificate.firefox.locator.AppLocator; +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.*; @@ -36,7 +37,7 @@ public class LegacyFirefoxCertificateInstaller { private static final String PREFS_DIR = "defaults/pref"; private static final String MAC_PREFIX = "Contents/Resources"; - public static void installAutoConfigScript(AppLocator app, String certData, String ... hostNames) { + public static void installAutoConfigScript(AppInfo app, String certData, String ... hostNames) { try { writePrefsFile(app); writeParsedConfig(app, certData, false, hostNames); @@ -45,23 +46,23 @@ public static void installAutoConfigScript(AppLocator app, String certData, Stri } } - public static void uninstallAutoConfigScript(AppLocator app) { + public static void uninstallAutoConfigScript(AppInfo appInfo) { try { - writeParsedConfig(app, "", true); + writeParsedConfig(appInfo, "", true); } catch(Exception e) { - log.warn("Error uninstalling auto-config support for {}", app.getName(), e); + log.warn("Error uninstalling auto-config support for {}", appInfo.getName(), e); } } - public static File tryWrite(AppLocator app, boolean mkdirs, String ... paths) throws IOException { - String dir = app.getPath(); + public static File tryWrite(AppInfo appInfo, boolean mkdirs, String ... paths) throws IOException { + Path dir = appInfo.getPath(); if (SystemUtilities.isMac()) { - dir += File.separator + MAC_PREFIX; + dir = dir.resolve(MAC_PREFIX); } for (String path : paths) { - dir += File.separator + path; + dir = dir.resolve(path); } - File file = new File(dir); + File file = dir.toFile(); if(mkdirs) file.mkdirs(); if(file.exists() && file.isDirectory() && file.canWrite()) { @@ -87,7 +88,7 @@ public static void deleteFile(File parent, String ... paths) { } } - public static void writePrefsFile(AppLocator app) throws Exception { + public static void writePrefsFile(AppInfo app) throws Exception { File prefsDir = tryWrite(app, true, PREFS_DIR); deleteFile(prefsDir, "firefox-prefs.js"); // cleanup old version @@ -119,10 +120,10 @@ public static void writePrefsFile(AppLocator app) throws Exception { prefsFile.setReadable(true, false); } - private static void writeParsedConfig(AppLocator app, String certData, boolean uninstall, String ... hostNames) throws IOException, CertificateEncodingException{ + 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(app, false); + File cfgDir = tryWrite(appInfo, false); deleteFile(cfgDir, "firefox-config.cfg"); // cleanup old version File dest = new File(cfgDir.getPath(), CFG_FILE); diff --git a/src/qz/installer/certificate/firefox/locator/AppAlias.java b/src/qz/installer/certificate/firefox/locator/AppAlias.java index 88b72d2f4..dbebcd825 100644 --- a/src/qz/installer/certificate/firefox/locator/AppAlias.java +++ b/src/qz/installer/certificate/firefox/locator/AppAlias.java @@ -22,10 +22,10 @@ public Alias[] getAliases() { return aliases; } - public boolean matches(AppLocator info) { - if (info.getName() != null && !info.isBlacklisted()) { + public boolean matches(AppInfo appInfo) { + if (appInfo.getName() != null && !appInfo.isBlacklisted()) { for (Alias alias : aliases) { - if (info.getName().toLowerCase(Locale.ENGLISH).matches(alias.name.toLowerCase(Locale.ENGLISH))) { + if (appInfo.getName().toLowerCase(Locale.ENGLISH).matches(alias.name.toLowerCase(Locale.ENGLISH))) { return true; } } diff --git a/src/qz/installer/certificate/firefox/locator/AppInfo.java b/src/qz/installer/certificate/firefox/locator/AppInfo.java new file mode 100644 index 000000000..977d5579d --- /dev/null +++ b/src/qz/installer/certificate/firefox/locator/AppInfo.java @@ -0,0 +1,96 @@ +package qz.installer.certificate.firefox.locator; + +import com.github.zafarkhaja.semver.Version; + +import java.nio.file.Path; + +/** + * Container class for installed app information + */ +public class AppInfo { + String name; + Path path; + Path exePath; + Version version; + boolean isBlacklisted = false; + + public AppInfo() {} + + public AppInfo(String name, Path exePath, String version) { + this.name = name; + this.path = exePath.getParent(); + this.exePath = exePath; + this.version = parseVersion(version); + } + + public AppInfo(String name, Path exePath) { + this.name = name; + this.path = exePath.getParent(); + this.exePath = exePath; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Path getExePath() { + return exePath; + } + + public void setExePath(Path exePath) { + this.exePath = 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; + } + + public boolean isBlacklisted() { + return isBlacklisted; + } + + public void setBlacklisted(boolean blacklisted) { + isBlacklisted = blacklisted; + } + + 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"; + } + if (version != null) { + return Version.valueOf(version); + } + } catch(Exception ignore) {} + return null; + } + + @Override + public boolean equals(Object o) { + if(o instanceof AppLocator && o != null && path != null) { + return path.equals(((AppInfo)o).getPath()); + } + return false; + } +} diff --git a/src/qz/installer/certificate/firefox/locator/AppLocator.java b/src/qz/installer/certificate/firefox/locator/AppLocator.java index 891b1ddf1..b6934f1ff 100644 --- a/src/qz/installer/certificate/firefox/locator/AppLocator.java +++ b/src/qz/installer/certificate/firefox/locator/AppLocator.java @@ -1,66 +1,67 @@ package qz.installer.certificate.firefox.locator; -import com.github.zafarkhaja.semver.Version; +import org.slf4j.LoggerFactory; +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 { - String name; - String path; - Version version; - void setName(String name) { - this.name = name; - } + protected static final org.slf4j.Logger log = LoggerFactory.getLogger(AppLocator.class); - void setPath(String path) { - this.path = path; - } + private static AppLocator INSTANCE = getPlatformSpecificAppLocator(); - void setVersion(String version) { - try { - // Less than three octets (e.g. "56.0") will fail parsing - while(version.split("\\.").length < 3) { - version = version + ".0"; - } - if (version != null) { - this.version = Version.valueOf(version); - } - } catch(Exception ignore) {} - } + public abstract ArrayList locate(AppAlias appAlias); + public abstract ArrayList getPidPaths(ArrayList pids); - public String getName() { - return name; + public ArrayList getPids(String ... processNames) { + return getPids(new ArrayList<>(Arrays.asList(processNames))); } - public String getPath() { - return path; + /** + * Linux, Mac + */ + public ArrayList getPids(ArrayList processNames) { + String[] response; + ArrayList pidList = new ArrayList<>(); + + 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 Version getVersion() { - return version; + public static ArrayList getRunningPaths(ArrayList appList) { + ArrayList appNames = new ArrayList<>(); + for (AppInfo app : appList) { + String exeName = app.getExePath().getFileName().toString(); + if (!appNames.contains(exeName)) appNames.add(exeName); + } + + return INSTANCE.getPidPaths(INSTANCE.getPids(appNames)); } - abstract boolean isBlacklisted(); + public static AppLocator getInstance() { + return INSTANCE; + } - public static ArrayList locate(AppAlias appAlias) { + private static AppLocator getPlatformSpecificAppLocator() { if (SystemUtilities.isWindows()) { - return WindowsAppLocator.findApp(appAlias); + return new WindowsAppLocator(); } else if (SystemUtilities.isMac()) { - return MacAppLocator.findApp(appAlias); - } - return LinuxAppLocator.findApp(appAlias) ; - } - - @Override - public boolean equals(Object o) { - if(o instanceof AppLocator && o != null && path != null) { - if (SystemUtilities.isWindows()) { - return path.equalsIgnoreCase(((AppLocator)o).getPath()); - } else { - return path.equals(((AppLocator)o).getPath()); - } + return new MacAppLocator(); } - return false; + return new LinuxAppLocator(); } -} +} \ No newline at end of file diff --git a/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java b/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java index 6d047cc5d..35396629c 100644 --- a/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java +++ b/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java @@ -1,38 +1,74 @@ package qz.installer.certificate.firefox.locator; +import org.apache.commons.io.FilenameUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import qz.utils.SystemUtilities; -import java.io.BufferedReader; -import java.io.File; -import java.io.InputStreamReader; +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 = LoggerFactory.getLogger(LinuxAppLocator.class); - public LinuxAppLocator(String name, String path) { - setName(name); - setPath(path); - } - - public static ArrayList findApp(AppAlias appAlias) { - ArrayList appList = new ArrayList<>(); + public ArrayList locate(AppAlias appAlias) { + ArrayList 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.posix, "/usr/lib/$/bin", "/usr/lib64/$/bin")) { - File file = new File(dirname, alias.posix); - if (file.isFile() && file.canExecute()) { + Path path = Paths.get(dirname, alias.posix); + if (Files.isRegularFile(path) && Files.isExecutable(path)) { + log.info("Found {} {}: {}, investigating...", alias.vendor, alias.name, path); try { - file = file.getCanonicalFile(); // fix symlinks - AppLocator info = new LinuxAppLocator(alias.name, file.getParentFile().getCanonicalPath()); - appList.add(info); + File file = path.toFile().getCanonicalFile(); // fix symlinks + String contentType = Files.probeContentType(file.toPath()); + if(file.getPath().endsWith(".sh")) { + // 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()))); + } else if(contentType != null && contentType.equals("application/x-shellscript")) { + // 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.posix)) { + 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; + } + } + } + } else { + log.info("Assuming {} {} is installed: {}", alias.vendor, alias.name, file); + } + AppInfo appInfo = new AppInfo(alias.name, file.toPath()); + appList.add(appInfo); // Call "--version" on executable to obtain version information Process p = Runtime.getRuntime().exec(new String[] {file.getCanonicalPath(), "--version" }, env); @@ -40,16 +76,23 @@ public static ArrayList findApp(AppAlias appAlias) { 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(" "); - info.setVersion(split[split.length - 1]); + 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 { - info.setVersion(version.trim()); + appInfo.setVersion(version.trim()); } } break; } catch(Exception e) { - e.printStackTrace(); + log.warn("Something went wrong getting app info for {} {}", alias.vendor, alias.name, e); } } } @@ -58,6 +101,20 @@ public static ArrayList findApp(AppAlias appAlias) { return appList; } + @Override + public ArrayList getPidPaths(ArrayList pids) { + ArrayList 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 @@ -66,16 +123,11 @@ public static ArrayList findApp(AppAlias appAlias) { * Usage: appendPaths("firefox", "/usr/lib64"); * */ - public static String[] appendPaths(String posix, String ... prefixes) { + 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); } - - @Override - boolean isBlacklisted() { - return false; - } } diff --git a/src/qz/installer/certificate/firefox/locator/MacAppLocator.java b/src/qz/installer/certificate/firefox/locator/MacAppLocator.java index 5d078195a..bede81bf4 100644 --- a/src/qz/installer/certificate/firefox/locator/MacAppLocator.java +++ b/src/qz/installer/certificate/firefox/locator/MacAppLocator.java @@ -1,5 +1,10 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; @@ -10,11 +15,15 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.regex.Pattern; -public class MacAppLocator extends AppLocator { +public class MacAppLocator extends AppLocator{ protected static final Logger log = LoggerFactory.getLogger(MacAppLocator.class); private static String[] BLACKLIST = new String[]{ "/Volumes/", "/.Trash/", "/Applications (Parallels)/" }; @@ -43,10 +52,10 @@ private boolean isKey(Node node) { return false; } - private void set(Node node, AppLocator info) { + private void set(Node node, AppInfo info) { switch(this) { case NAME: info.setName(node.getTextContent()); break; - case PATH: info.setPath(node.getTextContent()); break; + case PATH: info.setPath(Paths.get(node.getTextContent())); break; case VERSION: info.setVersion(node.getTextContent()); break; default: throw new UnsupportedOperationException(this.name() + " not supported"); } @@ -54,17 +63,9 @@ private void set(Node node, AppLocator info) { } } - public boolean isBlacklisted() { - for (String item : BLACKLIST) { - if (path != null && path.matches(Pattern.quote(item))) { - return true; - } - } - return false; - } - - public static ArrayList findApp(AppAlias appAlias) { - ArrayList appList = new ArrayList<>(); + @Override + public ArrayList locate(AppAlias appAlias) { + ArrayList appList = new ArrayList<>(); Document doc; try { @@ -80,13 +81,13 @@ public static ArrayList findApp(AppAlias appAlias) { NodeList nodeList = doc.getElementsByTagName("dict"); for (int i = 0; i < nodeList.getLength(); i++) { NodeList dict = nodeList.item(i).getChildNodes(); - MacAppLocator info = new MacAppLocator(); + AppInfo appInfo = new AppInfo(); 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) { - sibling.set(node, info); + sibling.set(node, appInfo); break; } else if(sibling.isKey(node)) { break; @@ -94,10 +95,77 @@ public static ArrayList findApp(AppAlias appAlias) { } } } - if (appAlias.matches(info)) { - appList.add(info); + if (appAlias.matches(appInfo)) { + appList.add(appInfo); + } + } + + for(AppInfo appInfo : appList) { + // Mark blacklisted locations + for(String listEntry : BLACKLIST) { + if (appInfo.getPath() != null && appInfo.getPath().toString().matches(Pattern.quote(listEntry))) { + appInfo.setBlacklisted(true); + } } + // Calculate exePath + appInfo.setExePath(getExePath(appInfo)); } return appList; } + + @Override + public ArrayList getPidPaths(ArrayList pids) { + ArrayList 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(AppInfo appInfo) { + Path plist = appInfo.getPath().resolve("Contents/Info.plist"); + Document doc; + try { + if(!plist.toFile().exists()) { + log.warn("Could not locate plist file for {}: {}", appInfo.getName(), plist); + return null; + } + // Convert potentially binary plist files to XML + Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toAbsolutePath().toString(), "-o", "-"}, ShellUtilities.envp); + doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream()); + } catch(IOException | ParserConfigurationException | SAXException e) { + log.warn("Could not parse plist file for {}: {}", appInfo.getName(), plist, 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 appInfo.getPath().resolve("Contents/MacOS/" + node.getTextContent()); + } + } + } + return null; + } + + private interface SystemB extends Library { + SystemB INSTANCE = Native.loadLibrary("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); + } } diff --git a/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java b/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java index 1b8302928..46b6d26b4 100644 --- a/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java +++ b/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java @@ -10,27 +10,36 @@ package qz.installer.certificate.firefox.locator; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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.util.ArrayList; import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE; -public class WindowsAppLocator extends AppLocator { +public class WindowsAppLocator extends AppLocator{ protected static final Logger log = LoggerFactory.getLogger(MacAppLocator.class); + + private static final String[] WIN32_PID_QUERY = {"wmic.exe", "process", "where", null, "get", "processid"}; + private static final int WIN32_PID_QUERY_INPUT_INDEX = 3; + + private static final String[] WIN32_PATH_QUERY = {"wmic.exe", "process", "where", null, "get", "ExecutablePath"}; + private static final int WIN32_PATH_QUERY_INPUT_INDEX = 3; + private static String REG_TEMPLATE = "Software\\%s%s\\%s%s"; - public WindowsAppLocator(String name, String path, String version) { - setName(name); - setPath(path); - setVersion(version); - } - public static ArrayList findApp(AppAlias appAlias) { - ArrayList appList = new ArrayList<>(); + @Override + public ArrayList locate(AppAlias appAlias) { + ArrayList appList = new ArrayList<>(); for (AppAlias.Alias alias : appAlias.aliases) { if (alias.vendor != null) { String[] suffixes = new String[]{ "", " ESR"}; @@ -38,9 +47,9 @@ public static ArrayList findApp(AppAlias appAlias) { for (String suffix : suffixes) { for (String prefix : prefixes) { String key = String.format(REG_TEMPLATE, prefix, alias.vendor, alias.name, suffix); - AppLocator appLocator = getAppInfo(alias.name, key, suffix); - if (appLocator != null && !appList.contains(appLocator)) { - appList.add(appLocator); + AppInfo appInfo = getAppInfo(alias.name, key, suffix); + if (appInfo != null && !appList.contains(appInfo)) { + appList.add(appInfo); } } } @@ -49,7 +58,56 @@ public static ArrayList findApp(AppAlias appAlias) { return appList; } - public static AppLocator getAppInfo(String name, String key, String suffix) { + @Override + public ArrayList getPids(ArrayList processNames) { + ArrayList pidList = new ArrayList<>(); + + if (processNames.isEmpty()) return pidList; + + WIN32_PID_QUERY[WIN32_PID_QUERY_INPUT_INDEX] = "(Name='" + String.join("' OR Name='", processNames) + "')"; + String[] response = ShellUtilities.executeRaw(WIN32_PID_QUERY).split("[\\r\\n]+"); + + // Add all found pids + for(String line : response) { + String pid = line.trim(); + if(StringUtils.isNumeric(pid.trim())) { + pidList.add(pid); + } + } + + if(SystemUtilities.isWindowsXP()) { + // Cleanup XP crumbs per https://stackoverflow.com/q/12391655/3196753 + File f = new File("TempWmicBatchFile.bat"); + if(f.exists()) { + f.deleteOnExit(); + } + } + + return pidList; + } + + @Override + public ArrayList getPidPaths(ArrayList pids) { + ArrayList pathList = new ArrayList<>(); + + for(String pid : pids) { + WIN32_PATH_QUERY[WIN32_PATH_QUERY_INPUT_INDEX] = "ProcessId=" + pid; + String[] response = ShellUtilities.executeRaw(WIN32_PATH_QUERY).split("\\s*\\r?\\n"); + if (response.length > 1) { + try { + pathList.add(Paths.get(response[1]).toRealPath()); + } catch(IOException e) { + log.warn("Could not locate process " + pid); + } + } + } + return pathList; + } + + /** + * Use a proprietary Firefox-only technique for getting "PathToExe" registry value + */ + private static AppInfo getAppInfo(String name, String key, String suffix) { String version = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "CurrentVersion"); if (version != null) { version = version.split(" ")[0]; // chop off (x86 ...) @@ -59,19 +117,14 @@ public static AppLocator getAppInfo(String name, String key, String suffix) { } version = version + suffix; } - String path = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe"); - if (path != null) { + Path exePath = Paths.get(WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe")); + + if (exePath != null) { // SemVer: Replace spaces in suffixes with dashes - path = new File(path).getParent(); version = version.replaceAll(" ", "-"); - return new WindowsAppLocator(name, path, version); + return new AppInfo(name, exePath, version); } } return null; } - - @Override - boolean isBlacklisted() { - return false; - } } diff --git a/src/qz/utils/ArgParser.java b/src/qz/utils/ArgParser.java index 3c96670f2..8ca60a488 100644 --- a/src/qz/utils/ArgParser.java +++ b/src/qz/utils/ArgParser.java @@ -21,6 +21,7 @@ import java.io.File; import java.util.*; +import java.util.List; import static qz.common.Constants.*; import static qz.utils.ArgParser.ExitStatus.*; @@ -146,6 +147,7 @@ public ExitStatus processInstallerArgs(Installer.InstallType type, List Installer.uninstall(); return SUCCESS; case SPAWN: + args.remove(0); // first argument is "spawn", remove it Installer.getInstance().spawn(args); return SUCCESS; default: diff --git a/test/qz/installer/browser/AppFinderTests.java b/test/qz/installer/browser/AppFinderTests.java index 929e956e3..70471afef 100644 --- a/test/qz/installer/browser/AppFinderTests.java +++ b/test/qz/installer/browser/AppFinderTests.java @@ -2,9 +2,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import qz.installer.Installer; +import qz.installer.certificate.firefox.FirefoxCertificateInstaller; import qz.installer.certificate.firefox.locator.AppAlias; +import qz.installer.certificate.firefox.locator.AppInfo; import qz.installer.certificate.firefox.locator.AppLocator; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Date; @@ -17,19 +21,31 @@ public static void main(String ... args) throws Exception { private static void runTest(AppAlias app) { Date begin = new Date(); - ArrayList appList = AppLocator.locate(app); + ArrayList appList = AppLocator.getInstance().locate(app); + ArrayList processPaths = AppLocator.getRunningPaths(appList); StringBuilder output = new StringBuilder("Found apps:\n"); - for (AppLocator info : appList) { - output.append(String.format(" name: '%s', path: '%s', version: '%s'\n", - info.getName(), - info.getPath(), - info.getVersion() + for (AppInfo appInfo : appList) { + output.append(String.format(" name: '%s', path: '%s', exePath: '%s', version: '%s'\n", + appInfo.getName(), + appInfo.getPath(), + appInfo.getExePath(), + appInfo.getVersion() )); + + if(processPaths.contains(appInfo.getExePath())) { + if (appInfo.getVersion().greaterThanOrEqualTo(FirefoxCertificateInstaller.FIREFOX_RESTART_VERSION)) { + try { + Installer.getInstance().spawn(appInfo.getExePath().toString(), "-private", "about:restartrequired"); + continue; + } catch(Exception ignore) {} + } + log.warn("{} must be restarted for changes to take effect", appInfo.getName()); + } } Date end = new Date(); log.debug(output.toString()); - log.debug("Time to find find {}: {}s", app.name(), (end.getTime() - begin.getTime())/1000.0f); + log.debug("Time to find and execute {}: {}s", app.name(), (end.getTime() - begin.getTime())/1000.0f); } } \ No newline at end of file