diff --git a/src/qz/installer/LinuxInstaller.java b/src/qz/installer/LinuxInstaller.java index 4e29f2c8a..c529b3c13 100644 --- a/src/qz/installer/LinuxInstaller.java +++ b/src/qz/installer/LinuxInstaller.java @@ -1,6 +1,5 @@ package qz.installer; -import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -31,6 +30,12 @@ public class LinuxInstaller extends Installer { public static final String CHROME_POLICY = "{ \"URLWhitelist\": [\"" + DATA_DIR + "://*\"] }"; private String destination = "/opt/" + PROPS_FILE; + private String sudoer; + + public LinuxInstaller() { + super(); + sudoer = getSudoer(); + } public void setDestination(String destination) { this.destination = destination; @@ -188,18 +193,114 @@ public Installer removeSystemSettings() { */ public void spawn(List 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 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 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 sudoCommand(String sudoer, boolean async, List cmds) { + ArrayList 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; + } - if(sudoer != null && !sudoer.trim().isEmpty()) { - sudoer = sudoer.trim(); - } else { - throw new Exception("Unable to get current user, can't spawn instance"); + /** + * 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 getUserEnv(String matchingUser) { + if(!SystemUtilities.isAdmin()) { + throw new UnsupportedOperationException("Administrative access is required"); } String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="}; @@ -251,10 +352,10 @@ public void spawn(List args) throws Exception { } // Only add vars for the current user - if(sudoer.trim().equals(tempEnv.get("USER"))) { + if(matchingUser.trim().equals(tempEnv.get("USER"))) { env.putAll(tempEnv); } else { - log.debug("Expected USER={} but got USER={}, skipping results for {}", sudoer, tempEnv.get("USER"), pid); + log.debug("Expected USER={} but got USER={}, skipping results for {}", matchingUser, tempEnv.get("USER"), pid); } // Use gtk theme @@ -264,40 +365,7 @@ public void spawn(List args) throws Exception { } } } - - 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)); - } - - // Determine if this environment likes sudo - String[] sudoCmd = { "sudo", "-E", "-u", sudoer, "nohup" }; - String[] suCmd = { "su", sudoer, "-c", "nohup" }; - - 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, "\" \"") + "\"")); - } - - // Spawn - log.info("Executing: {}", Arrays.toString(argsList.toArray())); - Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp); + return env; } } diff --git a/src/qz/installer/certificate/CertificateManager.java b/src/qz/installer/certificate/CertificateManager.java index b9db982c4..8b6972f25 100644 --- a/src/qz/installer/certificate/CertificateManager.java +++ b/src/qz/installer/certificate/CertificateManager.java @@ -11,8 +11,13 @@ 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; @@ -444,4 +449,29 @@ private void saveProperties() throws IOException { 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; + } } diff --git a/src/qz/installer/certificate/ExpiryTask.java b/src/qz/installer/certificate/ExpiryTask.java index dd496108f..34c86eb03 100644 --- a/src/qz/installer/certificate/ExpiryTask.java +++ b/src/qz/installer/certificate/ExpiryTask.java @@ -146,20 +146,6 @@ public static ExpiryState getExpiry(X509Certificate cert) { return ExpiryState.VALID; } - private static boolean emailMatches(X509Certificate cert) { - try { - X500Name x500name = new JcaX509CertificateHolder(cert).getSubject(); - String email = x500name.getRDNs(BCStyle.E)[0].getFirst().getValue().toString(); - if (Constants.ABOUT_EMAIL.equals(email)) { - log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, CertProvider.INTERNAL); - return true; - } - } - catch(Exception ignore) {} - 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; - } - public void schedule() { schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY); } @@ -183,7 +169,7 @@ public CertProvider findCertProvider() { public static CertProvider findCertProvider(X509Certificate cert) { // Internal certs use CN=localhost, trust email instead - if (emailMatches(cert)) { + if (CertificateManager.emailMatches(cert)) { return CertProvider.INTERNAL; } diff --git a/src/qz/installer/certificate/LinuxCertificateInstaller.java b/src/qz/installer/certificate/LinuxCertificateInstaller.java index b252c5d2e..78b6662f6 100644 --- a/src/qz/installer/certificate/LinuxCertificateInstaller.java +++ b/src/qz/installer/certificate/LinuxCertificateInstaller.java @@ -10,20 +10,27 @@ 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.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; +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; @@ -34,13 +41,16 @@ */ 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", + "sql:" + System.getenv("HOME") + "/.pki/nssdb/", // Snap-specific cert stores - "sql:" + System.getenv("HOME") + "/snap/chromium/current/.pki/nssdb", + "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/" @@ -60,16 +70,21 @@ public Installer.PrivilegeLevel getInstallType() { public void setInstallType(Installer.PrivilegeLevel certType) { this.certType = certType; if (this.certType == SYSTEM) { - log.warn("Command \"certutil\" needs to run as USER. We'll try again on launch. Ignore warnings about SYSTEM store."); + log.warn("Command \"certutil\" (required for certain browsers) needs to run as USER. We'll try again on launch."); } } public boolean remove(List idList) { boolean success = true; - if(certType == SYSTEM) return false; - for(String nickname : idList) { - for(String nssdb : NSSDB_URLS) { - success = success && ShellUtilities.execute("certutil", "-d", nssdb, "-D", "-n", nickname); + 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; @@ -77,22 +92,27 @@ public boolean remove(List idList) { public List find() { ArrayList nicknames = new ArrayList<>(); - if(certType == SYSTEM) return nicknames; - 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 + 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(); } - in.close(); } - } catch(IOException e) { - log.warn("Could not get certificate nicknames", e); + catch(IOException e) { + log.warn("Could not get certificate nicknames", e); + } } return nicknames; } @@ -100,34 +120,49 @@ public List find() { public boolean verify(File ignore) { return true; } // no easy way to validate a cert, assume it's installed public boolean add(File certFile) { - if(certType == SYSTEM) return false; - // Create directories as needed boolean success = true; - 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 browsers which depend on it.", nssdb); - success = false; + + 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() { - if (!ShellUtilities.execute("which", "certutil")) { - if (UnixUtilities.isUbuntu() && certType == SYSTEM && promptCertutil()) { - return ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools"); - } else { - log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on browsers which depend on it."); + boolean installed = ShellUtilities.execute("which", "certutil"); + if (!installed) { + if (certType == SYSTEM && promptCertutil()) { + if(UnixUtilities.isUbuntu()) { + installed = ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools"); + } else if(UnixUtilities.isFedora()) { + installed = ShellUtilities.execute("dnf", "install", "-y", "nss-tools"); + } } } - return false; + 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() { @@ -141,4 +176,190 @@ private boolean promptCertutil() { } 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 null + */ + 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 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 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 findUsingUsingUpdateCaCert() { + ArrayList 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 findUsingTrustAnchor() { + ArrayList 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"); + } } diff --git a/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java b/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java index b04e154bd..24a212a77 100644 --- a/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java +++ b/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java @@ -5,6 +5,7 @@ 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; @@ -24,9 +25,8 @@ public ArrayList locate(AppAlias appAlias) { // 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")) { + 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); @@ -38,15 +38,21 @@ public ArrayList locate(AppAlias appAlias) { // 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(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()))); - } else if(contentType != null && contentType.endsWith("/x-shellscript")) { + 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()); @@ -75,6 +81,7 @@ public ArrayList locate(AppAlias appAlias) { } } } + reader.close(); } else { log.info("Assuming {} {} is installed: {}", alias.getVendor(), alias.getName(true), file); } diff --git a/src/qz/utils/UnixUtilities.java b/src/qz/utils/UnixUtilities.java index 7c82d943c..906e9b3d4 100644 --- a/src/qz/utils/UnixUtilities.java +++ b/src/qz/utils/UnixUtilities.java @@ -19,7 +19,9 @@ import qz.common.Constants; import java.awt.*; +import java.io.BufferedReader; import java.io.FileNotFoundException; +import java.io.FileReader; import java.io.IOException; import java.nio.file.*; import java.util.Arrays; @@ -96,11 +98,11 @@ public static String getOsDisplayName() { Map map = getReleaseMap(); for (String nameKey: OS_NAME_KEYS) { if (map.containsKey(nameKey)) { - unixRelease = UnixUtilities.getReleaseMap().get(nameKey); + unixRelease = map.get(nameKey); break; } } - } catch(FileNotFoundException e) { + } catch(IOException e) { log.warn("Could not find a suitable os-release file {}", Arrays.toString(OS_RELEASE_FILES)); } if(unixRelease == null) { @@ -120,12 +122,12 @@ public static String getOsDisplayVersion() { Map map = getReleaseMap(); for(String versionKey : OS_VERSION_KEYS) { if (map.containsKey(versionKey)) { - unixVersion = UnixUtilities.getReleaseMap().get(versionKey); + unixVersion = map.get(versionKey); break; } } } - catch(FileNotFoundException e) { + catch(IOException e) { log.warn("Could not find a suitable os-release file {}", Arrays.toString(OS_RELEASE_FILES)); } if(unixVersion == null) { @@ -142,18 +144,22 @@ public static String getOsDisplayVersion() { return unixVersion; } - private static Map getReleaseMap() throws FileNotFoundException { + private static Map getReleaseMap() throws IOException { HashMap map = new HashMap<>(); - Path release = findOsReleaseFile(); - String result = ShellUtilities.executeRaw( - new String[] {"cat", release.toString()} - ); - - String[] results = result.split("\n"); - for (String line: results) { - String[] tokens = line.split("=", 2); - if (tokens.length != 2) continue; - map.put(tokens[0], tokens[1].replaceAll("\"", "")); + BufferedReader reader = null; + try { + Path release = findOsReleaseFile(); + reader = new BufferedReader(new FileReader(release.toFile())); + String line; + while((line = reader.readLine()) != null) { + String[] tokens = line.split("=", 2); + if (tokens.length != 2) continue; + map.put(tokens[0], tokens[1].replaceAll("\"", "")); + } + } finally{ + if(reader != null) { + reader.close(); + } } return map; } @@ -245,6 +251,6 @@ public static boolean isUbuntu() { */ public static boolean isFedora() { if(!SystemUtilities.isLinux()) return false; - return unixRelease != null && getOsDisplayName().contains("Fedora"); + return getOsDisplayName() != null && getOsDisplayName().contains("Fedora"); } }