Skip to content

Commit

Permalink
Add restart warning for Firefox (qzind#621)
Browse files Browse the repository at this point in the history
Adds restart warning for Firefox 60.0+
- Fixes Firefox detection on Debian, Manjaro
- Fixes spawning after install on Debian, Manjaro, Solaris

Co-authored-by: Vzor- <[email protected]>
  • Loading branch information
tresf and Vzor- authored Apr 25, 2020
1 parent 49cc801 commit 28ea9fd
Show file tree
Hide file tree
Showing 15 changed files with 534 additions and 194 deletions.
8 changes: 5 additions & 3 deletions src/qz/installer/Installer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)));
}
}
57 changes: 41 additions & 16 deletions src/qz/installer/LinuxInstaller.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -159,9 +160,12 @@ public Installer removeSystemSettings() {
* Spawns the process as the underlying regular user account, preserving the environment
*/
public void spawn(List<String> 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");
}

Expand All @@ -183,8 +187,25 @@ public void spawn(List<String> args) throws Exception {
ArrayList<String> 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) {
Expand All @@ -195,16 +216,20 @@ public void spawn(List<String> 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
Expand All @@ -221,20 +246,20 @@ public void spawn(List<String> 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<String> argsList = new ArrayList<>();
argsList.addAll(Arrays.asList(asUser));
String command = "";
Pattern quote = Pattern.compile("\"");
for(String arg : args) {
command += String.format(" %s", arg);
ArrayList<String> 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);
}

Expand Down
14 changes: 12 additions & 2 deletions src/qz/installer/MacInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,6 +120,16 @@ public static String getPackageName() {
}

public void spawn(List<String> 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()]));
}
}
}
19 changes: 10 additions & 9 deletions src/qz/installer/TaskKiller.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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*/ };

Expand All @@ -31,25 +31,26 @@ public class TaskKiller {
public static boolean killAll() {
boolean success = true;

String[] javaProcs;
ArrayList<String> 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<String> intersections = new ArrayList<>(Arrays.asList(trayProcs));
intersections.retainAll(Arrays.asList(javaProcs));
intersections.retainAll(javaProcs);

// Remove any instances created by this installer
intersections.remove("" + selfProc);
Expand Down
6 changes: 5 additions & 1 deletion src/qz/installer/WindowsInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -193,6 +194,9 @@ public String getDestination() {
}

public void spawn(List<String> 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()]));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
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;

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;
Expand All @@ -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 + "\"] } } }";
Expand All @@ -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<AppLocator> 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<AppInfo> appList = AppLocator.getInstance().locate(AppAlias.FIREFOX);
ArrayList<Path> 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<AppLocator> appList = AppLocator.locate(AppAlias.FIREFOX);
for(AppLocator app : appList) {
if(honorsPolicy(app)) {
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 {}", 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)) {
Expand Down Expand Up @@ -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();
}
}
Loading

0 comments on commit 28ea9fd

Please sign in to comment.