Files
quality_recticel/tray/src/qz/installer/Installer.java
Scheianu Ionut c7266c32ee Add custom QZ Tray fork with pairing key authentication
- Custom fork of QZ Tray 2.2.x with certificate validation bypassed
- Implemented pairing key (HMAC) authentication as replacement
- Modified files: PrintSocketClient.java (certificate check disabled)
- New files: PairingAuth.java, PairingConfigDialog.java
- Excluded build artifacts (out/, lib/javafx*) from repository
- Library JARs included for dependency management
2025-10-02 02:27:45 +03:00

414 lines
15 KiB
Java

/**
* @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)));
}
}