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
This commit is contained in:
2025-10-02 02:27:45 +03:00
parent 755400a269
commit c7266c32ee
444 changed files with 63195 additions and 1 deletions

133
tray/src/qz/App.java Normal file
View File

@@ -0,0 +1,133 @@
package qz;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy;
import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy;
import org.apache.logging.log4j.core.filter.ThresholdFilter;
import org.apache.logging.log4j.core.layout.PatternLayout;
import qz.build.provision.params.Phase;
import qz.common.Constants;
import qz.installer.Installer;
import qz.installer.certificate.CertificateManager;
import qz.installer.certificate.ExpiryTask;
import qz.installer.certificate.KeyPairWrapper;
import qz.installer.certificate.NativeCertificateInstaller;
import qz.installer.provision.ProvisionInstaller;
import qz.ui.PairingConfigDialog;
import qz.utils.*;
import qz.ws.PrintSocketServer;
import qz.ws.SingleInstanceChecker;
import qz.ws.substitutions.Substitutions;
import java.io.File;
import java.security.cert.X509Certificate;
import java.util.Properties;
public class App {
private static final Logger log = LogManager.getLogger(App.class);
private static Properties trayProperties = null;
public static void main(String ... args) {
ArgParser parser = new ArgParser(args);
LibUtilities.getInstance().bind();
if(parser.intercept()) {
FileUtilities.cleanup();
System.exit(parser.getExitCode());
}
SingleInstanceChecker.stealWebsocket = parser.hasFlag(ArgValue.STEAL);
setupFileLogging();
log.info(Constants.ABOUT_TITLE + " version: {}", Constants.VERSION);
log.info(Constants.ABOUT_TITLE + " vendor: {}", Constants.ABOUT_COMPANY);
log.info("Java version: {}", Constants.JAVA_VERSION.toString());
log.info("Java vendor: {}", Constants.JAVA_VENDOR);
Substitutions.setEnabled(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_ENABLE));
Substitutions.setStrict(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_STRICT));
CertificateManager certManager = null;
try {
// Gets and sets the SSL info, properties file
certManager = Installer.getInstance().certGen(false);
trayProperties = certManager.getProperties();
// Reoccurring (e.g. hourly) cert expiration check
new ExpiryTask(certManager).schedule();
} catch(Exception e) {
log.error("Something went critically wrong loading HTTPS", e);
}
Installer.getInstance().addUserSettings();
// Load overridable preferences set in qz-tray.properties file
NetworkUtilities.setPreferences(certManager.getProperties());
SingleInstanceChecker.setPreferences(certManager.getProperties());
// Linux needs the cert installed in user-space on every launch for Chrome SSL to work
if(!SystemUtilities.isWindows() && !SystemUtilities.isMac()) {
X509Certificate caCert = certManager.getKeyPair(KeyPairWrapper.Type.CA).getCert();
// Only install if a CA cert exists (e.g. one we generated)
if(caCert != null) {
NativeCertificateInstaller.getInstance().install(certManager.getKeyPair(KeyPairWrapper.Type.CA).getCert());
}
}
// Invoke any provisioning steps that are phase=startup
try {
ProvisionInstaller provisionInstaller = new ProvisionInstaller(SystemUtilities.getJarParentPath().resolve(Constants.PROVISION_DIR));
provisionInstaller.invoke(Phase.STARTUP);
} catch(Exception e) {
log.warn("An error occurred provisioning \"phase\": \"startup\" entries", e);
}
try {
log.info("Starting {} {}", Constants.ABOUT_TITLE, Constants.VERSION);
// Start the WebSocket
PrintSocketServer.runServer(certManager, parser.isHeadless());
}
catch(Exception e) {
log.error("Could not start tray manager", e);
}
FileUtilities.cleanup();
log.warn("The web socket server is no longer running");
// Show pairing config dialog if needed
PairingConfigDialog.showIfNeeded(null); // null for no parent frame, or pass your main frame if available
}
public static Properties getTrayProperties() {
return trayProperties;
}
private static void setupFileLogging() {
//disable jetty logging
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog");
System.setProperty("org.eclipse.jetty.LEVEL", "OFF");
if(PrefsSearch.getBoolean(ArgValue.LOG_DISABLE)) {
return;
}
int logSize = PrefsSearch.getInt(ArgValue.LOG_SIZE);
int logRotate = PrefsSearch.getInt(ArgValue.LOG_ROTATE);
Installer.getInstance().cleanupLegacyLogs(Math.max(logRotate, 5));
RollingFileAppender fileAppender = RollingFileAppender.newBuilder()
.setName("log-file")
.withAppend(true)
.setLayout(PatternLayout.newBuilder().withPattern("%d{ISO8601} [%p] %m%n").build())
.setFilter(ThresholdFilter.createFilter(Level.DEBUG, Filter.Result.ACCEPT, Filter.Result.DENY))
.withFileName(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log")
.withFilePattern(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".%i.log")
.withStrategy(DefaultRolloverStrategy.newBuilder()
.withMax(String.valueOf(logRotate))
.withFileIndex("min")
.build())
.withPolicy(SizeBasedTriggeringPolicy.createPolicy(String.valueOf(logSize)))
.withImmediateFlush(true)
.build();
fileAppender.start();
LoggerUtilities.getRootLogger().addAppender(fileAppender);
}
}

70
tray/src/qz/auth/CRL.java Normal file
View File

@@ -0,0 +1,70 @@
package qz.auth;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ConnectionUtilities;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
/**
* Wrapper class for the Certificate Revocation List
* Created by Steven on 2/4/2015. Package: qz.auth Project: qz-print
*/
public class CRL {
private static final Logger log = LogManager.getLogger(CRL.class);
/** The URL to the QZ CRL. Should not be changed except for dev tests */
public static final String CRL_URL = "https://crl.qz.io";
private static CRL instance = null;
private ArrayList<String> revokedHashes = new ArrayList<String>();
private boolean loaded = false;
private CRL() {}
public static CRL getInstance() {
if (instance == null) {
instance = new CRL();
new Thread() {
@Override
public void run() {
log.info("Loading CRL from {}", CRL_URL);
try(BufferedReader br = new BufferedReader(new InputStreamReader(ConnectionUtilities.getInputStream(CRL_URL, false)))) {
String line;
while((line = br.readLine()) != null) {
//Ignore empty and commented lines
if (!line.isEmpty() && line.charAt(0) != '#') {
instance.revokedHashes.add(line);
}
}
instance.loaded = true;
log.info("Successfully loaded {} CRL entries from {}", instance.revokedHashes.size(), CRL_URL);
}
catch(IOException e) {
log.warn("Unable to access CRL from {}, {}", CRL_URL, e.toString());
}
}
}.start();
}
return instance;
}
public boolean isRevoked(String fingerprint) {
return revokedHashes.contains(fingerprint);
}
public boolean isLoaded() {
return loaded;
}
}

View File

@@ -0,0 +1,536 @@
package qz.auth;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.Charsets;
import org.apache.commons.ssl.Base64;
import org.apache.commons.ssl.X509CertificateChainBuilder;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.App;
import qz.common.Constants;
import qz.utils.*;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.*;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* Created by Steven on 1/27/2015. Package: qz.auth Project: qz-print
* Wrapper to store certificate objects from
*/
public class Certificate {
private static final Logger log = LogManager.getLogger(Certificate.class);
private static final String QUIETLY_FAIL = "quiet";
public enum Algorithm {
SHA1("SHA1withRSA"),
SHA256("SHA256withRSA"),
SHA512("SHA512withRSA");
String name;
Algorithm(String name) {
this.name = name;
}
}
public static ArrayList<Certificate> rootCAs = new ArrayList<>();
public static Certificate builtIn;
private static CertPathValidator validator;
private static CertificateFactory factory;
private static boolean trustBuiltIn = false;
// id-at-description used for storing renewal information
private static ASN1ObjectIdentifier RENEWAL_OF = new ASN1ObjectIdentifier("2.5.4.13");
public static final String[] saveFields = new String[] {"fingerprint", "commonName", "organization", "validFrom", "validTo", "valid"};
public static final String SPONSORED_CN_PREFIX = "Sponsored:";
// Valid date range allows UI to only show "Expired" text for valid certificates
private static final Instant UNKNOWN_MIN = LocalDateTime.MIN.toInstant(ZoneOffset.UTC);
private static final Instant UNKNOWN_MAX = LocalDateTime.MAX.toInstant(ZoneOffset.UTC);
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final DateTimeFormatter DATE_PARSE = DateTimeFormatter.ofPattern("uuuu-MM-dd['T'][ ]HH:mm:ss[.n]['Z']"); //allow parsing of both ISO and custom formatted dates
private X509Certificate theCertificate;
private boolean sponsored;
private String fingerprint;
private String commonName;
private String organization;
private Instant validFrom;
private Instant validTo;
//used by review sites UI only
private boolean expired = false;
private boolean valid = false;
private boolean rootCA = false; // TODO: Move to constructor?
//Pre-set certificate for use when missing
public static final Certificate UNKNOWN;
static {
HashMap<String,String> map = new HashMap<>();
map.put("fingerprint", "UNKNOWN REQUEST");
map.put("commonName", "An anonymous request");
map.put("organization", "Unknown");
map.put("validFrom", UNKNOWN_MIN.toString());
map.put("validTo", UNKNOWN_MAX.toString());
map.put("valid", "false");
UNKNOWN = Certificate.loadCertificate(map);
}
static {
try {
Security.addProvider(new BouncyCastleProvider());
validator = CertPathValidator.getInstance("PKIX");
factory = CertificateFactory.getInstance("X.509");
builtIn = new Certificate("-----BEGIN CERTIFICATE-----\n" +
"MIIELzCCAxegAwIBAgIJALm151zCHDxiMA0GCSqGSIb3DQEBCwUAMIGsMQswCQYD\n" +
"VQQGEwJVUzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNhbmFzdG90YTEbMBkGA1UE\n" +
"CgwSUVogSW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBM\n" +
"TEMxGTAXBgNVBAMMEHF6aW5kdXN0cmllcy5jb20xJzAlBgkqhkiG9w0BCQEWGHN1\n" +
"cHBvcnRAcXppbmR1c3RyaWVzLmNvbTAgFw0xNTAzMDEyMzM4MjlaGA8yMTE1MDMw\n" +
"MjIzMzgyOVowgawxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTESMBAGA1UEBwwJ\n" +
"Q2FuYXN0b3RhMRswGQYDVQQKDBJRWiBJbmR1c3RyaWVzLCBMTEMxGzAZBgNVBAsM\n" +
"ElFaIEluZHVzdHJpZXMsIExMQzEZMBcGA1UEAwwQcXppbmR1c3RyaWVzLmNvbTEn\n" +
"MCUGCSqGSIb3DQEJARYYc3VwcG9ydEBxemluZHVzdHJpZXMuY29tMIIBIjANBgkq\n" +
"hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuWsBa6uk+RM4OKBZTRfIIyqaaFD71FAS\n" +
"7kojAQ+ySMpYuqLjIVZuCh92o1FGBvyBKUFc6knAHw5749yhLCYLXhzWwiNW2ri1\n" +
"Jwx/d83Wnaw6qA3lt++u3tmiA8tsFtss0QZW0YBpFsIqhamvB3ypwu0bdUV/oH7g\n" +
"/s8TFR5LrDfnfxlLFYhTUVWuWzMqEFAGnFG3uw/QMWZnQgkGbx0LMcYzdqFb7/vz\n" +
"rTSHfjJsisUTWPjo7SBnAtNYCYaGj0YH5RFUdabnvoTdV2XpA5IPYa9Q597g/M0z\n" +
"icAjuaK614nKXDaAUCbjki8RL3OK9KY920zNFboq/jKG6rKW2t51ZQIDAQABo1Aw\n" +
"TjAdBgNVHQ4EFgQUA0XGTcD6jqkL2oMPQaVtEgZDqV4wHwYDVR0jBBgwFoAUA0XG\n" +
"TcD6jqkL2oMPQaVtEgZDqV4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\n" +
"AQEAijcT5QMVqrWWqpNEe1DidzQfSnKo17ZogHW+BfUbxv65JbDIntnk1XgtLTKB\n" +
"VAdIWUtGZbXxrp16NEsh96V2hjDIoiAaEpW+Cp6AHhIVgVh7Q9Knq9xZ1t6H8PL5\n" +
"QiYQKQgJ0HapdCxlPKBfUm/Mj1ppNl9mPFJwgHmzORexbxrzU/M5i2jlies+CXNq\n" +
"cvmF2l33QNHnLwpFGwYKs08pyHwUPp6+bfci6lRvavztgvnKroWWIRq9ZPlC0yVK\n" +
"FFemhbCd7ZVbrTo0NcWZM1PTAbvlOikV9eh3i1Vot+3dJ8F27KwUTtnV0B9Jrxum\n" +
"W9P3C48mvwTxYZJFOu0N9UBLLg==\n" +
"-----END CERTIFICATE-----");
builtIn.valid = true;
setTrustBuiltIn(true);
scanAdditionalCAs();
}
catch(NoSuchAlgorithmException | CertificateException e) {
e.printStackTrace();
}
}
public static void scanAdditionalCAs() {
ArrayList<Map.Entry<Path, String>> certPaths = new ArrayList<>();
// First, look for "authcert.override", "-DtrustedRootCert"
certPaths.addAll(FileUtilities.parseDelimitedPaths(PrefsSearch.getString(ArgValue.AUTHCERT_OVERRIDE, App.getTrayProperties())));
// Second, look for "override.crt" within App directory
certPaths.add(new AbstractMap.SimpleEntry<>(SystemUtilities.getJarParentPath().resolve(Constants.OVERRIDE_CERT), QUIETLY_FAIL));
for(Map.Entry<Path, String> certPath : certPaths) {
if(certPath.getKey() != null) {
if (certPath.getKey().toFile().exists()) {
try {
Certificate caCert = new Certificate(FileUtilities.readLocalFile(certPath.getKey()));
caCert.rootCA = true;
caCert.valid = true;
if(!rootCAs.contains(caCert)) {
log.debug("Adding CA certificate: CN={}, O={} ({})",
caCert.getCommonName(), caCert.getOrganization(), caCert.getFingerprint());
rootCAs.add(caCert);
} else {
log.warn("CA cert exists, skipping: {}", certPath.getKey());
}
}
catch(Exception e) {
log.error("Error loading CA cert: {}", certPath.getKey(), e);
}
} else if(!certPath.getValue().equals(QUIETLY_FAIL)) {
log.warn("CA cert \"{}\" was provided, but could not be found, skipping.", certPath.getKey());
}
}
}
}
public Certificate(Path path) throws IOException, CertificateException {
this(new String(Files.readAllBytes(path), Charsets.UTF_8));
}
/** Decodes a certificate and intermediate certificate from the given string */
public Certificate(String in) throws CertificateException {
try {
//Strip beginning and end
String[] split = in.split("--START INTERMEDIATE CERT--");
byte[] serverCertificate = Base64.decodeBase64(split[0].replaceAll(X509Constants.BEGIN_CERT, "").replaceAll(X509Constants.END_CERT, ""));
X509Certificate theIntermediateCertificate;
if (split.length == 2) {
byte[] intermediateCertificate = Base64.decodeBase64(split[1].replaceAll(X509Constants.BEGIN_CERT, "").replaceAll(X509Constants.END_CERT, ""));
theIntermediateCertificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(intermediateCertificate));
} else {
theIntermediateCertificate = null; //Self-signed
}
//Generate cert
theCertificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(serverCertificate));
commonName = getSubjectX509Principal(theCertificate, BCStyle.CN);
if(commonName.isEmpty()) {
throw new CertificateException("Common Name cannot be blank.");
}
// Remove "Sponsored: " from CN, we'll swap the trusted icon instead <3
if(commonName.startsWith(SPONSORED_CN_PREFIX)) {
commonName = commonName.split(SPONSORED_CN_PREFIX)[1].trim();
sponsored = true;
} else {
sponsored = false;
}
fingerprint = makeThumbPrint(theCertificate);
organization = getSubjectX509Principal(theCertificate, BCStyle.O);
validFrom = theCertificate.getNotBefore().toInstant();
validTo = theCertificate.getNotAfter().toInstant();
// Check trust anchor against all root certs
Certificate foundRoot = null;
if(!this.rootCA) {
for(Certificate rootCA : rootCAs) {
HashSet<X509Certificate> chain = new HashSet<>();
try {
chain.add(rootCA.theCertificate);
if (theIntermediateCertificate != null) { chain.add(theIntermediateCertificate); }
X509Certificate[] x509Certificates = X509CertificateChainBuilder.buildPath(theCertificate, chain);
Set<TrustAnchor> anchor = new HashSet<>();
anchor.add(new TrustAnchor(rootCA.theCertificate, null));
PKIXParameters params = new PKIXParameters(anchor);
params.setRevocationEnabled(false); // TODO: Re-enable, remove proprietary CRL
validator.validate(factory.generateCertPath(Arrays.asList(x509Certificates)), params);
foundRoot = rootCA;
valid = true;
log.debug("Successfully chained certificate: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
break; // if successful, don't attempt another chain
}
catch(Exception e) {
log.warn("Problem building certificate chain (normal if multiple CAs are in use)");
}
}
}
// Check for expiration
Instant now = Instant.now();
if (expired = (validFrom.isAfter(now) || validTo.isBefore(now))) {
log.warn("Certificate is expired: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
valid = false;
}
// If cert matches a rootCA trust it blindly
// If cert is chained to a 3rd party rootCA, trust it blindly as well
Iterator<Certificate> allCerts = rootCAs.iterator();
while(allCerts.hasNext()) {
Certificate cert = allCerts.next();
if(cert.equals(this) || (cert.equals(foundRoot) && !cert.equals(builtIn))) {
log.debug("Adding {} to {} list", cert.toString(), Constants.ALLOW_FILE);
if(!isSaved()) {
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data());
}
valid = true;
break;
}
}
readRenewalInfo();
CRL qzCrl = CRL.getInstance();
if (qzCrl.isLoaded()) {
if (qzCrl.isRevoked(getFingerprint()) || (theIntermediateCertificate != null && qzCrl.isRevoked(makeThumbPrint(theIntermediateCertificate)))) {
log.error("Certificate has been revoked and can no longer be used: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
valid = false;
}
} else {
//Assume nothing is revoked, because we can't get the CRL
log.warn("Failed to retrieve QZ CRL, skipping CRL check");
}
}
catch(Exception e) {
CertificateException certificateException = new CertificateException();
certificateException.initCause(e);
throw certificateException;
}
}
private void readRenewalInfo() throws Exception {
Vector values = PrincipalUtil.getSubjectX509Principal(theCertificate).getValues(RENEWAL_OF);
Iterator renewals = values.iterator();
while(renewals.hasNext()) {
String renewalInfo = String.valueOf(renewals.next());
String renewalPrefix = "renewal-of-";
if (!renewalInfo.startsWith(renewalPrefix)) {
log.warn("Malformed renewal info: {}", renewalInfo);
continue;
}
String previousFingerprint = renewalInfo.substring(renewalPrefix.length());
if (previousFingerprint.length() != 40) {
log.warn("Malformed renewal fingerprint: {}", previousFingerprint);
continue;
}
// Add this certificate to the whitelist if the previous certificate was whitelisted
// First, handle shared directory
File sharedFile = FileUtilities.getFile(Constants.ALLOW_FILE, false);
if (existsInAnyFile(previousFingerprint, sharedFile) && !isSaved(false)) {
if(!FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), false)) {
// Fallback to local directory if shared is not writable
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), /* fallback */ true);
}
}
// Second, handle local directory
File localFile = FileUtilities.getFile(Constants.ALLOW_FILE, true);
if (existsInAnyFile(previousFingerprint, localFile) && !isSaved(true)) {
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), true);
}
}
}
private Certificate() {}
/**
* Used to rebuild a certificate for the 'Saved Sites' screen without having to decrypt the certificates again
*/
public static Certificate loadCertificate(HashMap<String,String> data) {
Certificate cert = new Certificate();
cert.fingerprint = data.get("fingerprint");
cert.commonName = data.get("commonName");
cert.organization = data.get("organization");
try {
cert.validFrom = Instant.from(LocalDateTime.from(DATE_PARSE.parse(data.get("validFrom"))).atZone(ZoneOffset.UTC));
cert.validTo = Instant.from(LocalDateTime.from(DATE_PARSE.parse(data.get("validTo"))).atZone(ZoneOffset.UTC));
}
catch(DateTimeException e) {
cert.validFrom = UNKNOWN_MIN;
cert.validTo = UNKNOWN_MAX;
log.warn("Unable to parse certificate date: {}", e.getMessage());
}
cert.valid = Boolean.parseBoolean(data.get("valid"));
return cert;
}
/**
* Checks given signature for given data against this certificate,
* ensuring it is properly signed
*
* @param signature the signature appended to the data, base64 encoded
* @param data the data to check
* @return true if signature valid, false if not
*/
public boolean isSignatureValid(Algorithm algorithm, String signature, String data) {
if (!signature.isEmpty()) {
//On errors, assume failure.
try {
Signature verifier = Signature.getInstance(algorithm.name);
verifier.initVerify(theCertificate.getPublicKey());
verifier.update(StringUtils.getBytesUtf8(DigestUtils.sha256Hex(data)));
return verifier.verify(Base64.decodeBase64(signature));
}
catch(GeneralSecurityException e) {
log.error("Unable to verify signature", e);
}
}
return false;
}
/** Checks if the certificate has been added to the specified allow file */
public boolean isSaved(boolean local) {
File allowed = FileUtilities.getFile(Constants.ALLOW_FILE, local);
return existsInAnyFile(getFingerprint(), allowed);
}
/** Checks if the certificate has been added to any allow file */
public boolean isSaved() {
return isSaved(false) || isSaved(true);
}
/** Checks if the certificate has been added to the local block file */
public boolean isBlocked() {
File blocks = FileUtilities.getFile(Constants.BLOCK_FILE, true);
File blocksShared = FileUtilities.getFile(Constants.BLOCK_FILE, false);
return existsInAnyFile(getFingerprint(), blocksShared, blocks);
}
private static boolean existsInAnyFile(String fingerprint, File... files) {
for(File file : files) {
if (file == null) { continue; }
try(BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while((line = br.readLine()) != null) {
if (line.contains("\t")) {
String print = line.substring(0, line.indexOf("\t"));
if (print.equals(fingerprint)) {
return true;
}
}
}
}
catch(IOException e) {
e.printStackTrace();
}
}
return false;
}
public String getFingerprint() {
return fingerprint;
}
public String getCommonName() {
return commonName;
}
public String getOrganization() {
return organization;
}
public String getValidFrom() {
if (validFrom.isAfter(UNKNOWN_MIN)) {
return DATE_FORMAT.format(validFrom.atZone(ZoneOffset.UTC));
} else {
return "Not Provided";
}
}
public String getValidTo() {
if (validTo.isBefore(UNKNOWN_MAX)) {
return DATE_FORMAT.format(validTo.atZone(ZoneOffset.UTC));
} else {
return "Not Provided";
}
}
public Instant getValidFromDate() {
return validFrom;
}
public Instant getValidToDate() {
return validTo;
}
/**
* Validates certificate against embedded cert.
*/
public boolean isTrusted() {
return isValid() && !isExpired();
}
public boolean isSponsored() {
return sponsored;
}
public boolean isValid() {
return valid;
}
public boolean isExpired() {
return expired;
}
public static String makeThumbPrint(X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(cert.getEncoded());
return ByteUtilities.bytesToHex(md.digest(), false);
}
private String data(boolean assumeTrusted) {
return getFingerprint() + "\t" +
getCommonName() + "\t" +
getOrganization() + "\t" +
getValidFrom() + "\t" +
getValidTo() + "\t" +
// Used by equals(), may fail if it hasn't been trusted yet
(assumeTrusted ? true : isTrusted());
}
public String data() {
return data(false);
}
@Override
public String toString() {
return getOrganization() + " (" + getCommonName() + ")";
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Certificate) {
return ((Certificate)obj).data(true).equals(data(true));
}
return super.equals(obj);
}
public static void setTrustBuiltIn(boolean trustBuiltIn) {
if(trustBuiltIn) {
if (!rootCAs.contains(builtIn)) {
log.debug("Adding internal CA certificate: CN={}, O={} ({})",
builtIn.getCommonName(), builtIn.getOrganization(), builtIn.getFingerprint());
builtIn.rootCA = true;
builtIn.valid = true;
rootCAs.add(0, builtIn);
}
} else {
if (rootCAs.contains(builtIn)) {
log.debug("Removing internal CA certificate: CN={}, O={} ({})",
builtIn.getCommonName(), builtIn.getOrganization(), builtIn.getFingerprint());
rootCAs.remove(builtIn);
}
}
Certificate.trustBuiltIn = trustBuiltIn;
}
public static boolean isTrustBuiltIn() {
return trustBuiltIn;
}
public static boolean hasAdditionalCAs() {
return rootCAs.size() > (isTrustBuiltIn() ? 1 : 0);
}
private static String getSubjectX509Principal(X509Certificate cert, ASN1ObjectIdentifier key) {
try {
Vector v = PrincipalUtil.getSubjectX509Principal(cert).getValues(key);
if(v.size() > 0) {
return String.valueOf(v.get(0));
}
} catch(CertificateEncodingException e) {
log.warn("Certificate encoding exception occurred", e);
}
return "";
}
}

View File

@@ -0,0 +1,87 @@
package qz.auth;
import org.json.JSONObject;
import qz.utils.FileUtilities;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
public class PairingAuth {
private static String site = null;
private static String pairingKey = null;
private static final String HMAC_ALGO = "HmacSHA256";
private static File configFile = null;
static {
loadConfig();
}
private static void loadConfig() {
try {
// Use QZ Tray's user directory - same location as PairingConfigDialog
configFile = new File(FileUtilities.USER_DIR.toFile(), "pairing-config.json");
if (!configFile.exists()) {
System.out.println("Pairing config file not found at: " + configFile.getAbsolutePath());
site = null;
pairingKey = null;
return;
}
String content = new String(Files.readAllBytes(configFile.toPath()));
JSONObject config = new JSONObject(content);
site = config.optString("site", null);
pairingKey = config.optString("pairing_key", null);
System.out.println("Pairing config loaded from: " + configFile.getAbsolutePath());
System.out.println("Site configured: " + site);
} catch (Exception e) {
System.err.println("Error loading pairing config: " + e.getMessage());
e.printStackTrace();
site = null;
pairingKey = null;
}
}
public static String getSite() {
return site;
}
public static String getPairingKey() {
return pairingKey;
}
public static boolean isConfigured() {
return site != null && !site.isEmpty() && pairingKey != null && !pairingKey.isEmpty();
}
public static String getConfigFilePath() {
return configFile != null ? configFile.getAbsolutePath() : "unknown";
}
public static void reload() {
loadConfig();
}
public static String hmacSignature(String message) {
if (pairingKey == null || pairingKey.isEmpty()) {
System.err.println("Cannot compute HMAC: pairing key is not configured");
return null;
}
try {
Mac mac = Mac.getInstance(HMAC_ALGO);
SecretKeySpec keySpec = new SecretKeySpec(pairingKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGO);
mac.init(keySpec);
byte[] hmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hmac);
} catch (Exception e) {
System.err.println("HMAC computation failed: " + e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,118 @@
package qz.auth;
import org.codehaus.jettison.json.JSONObject;
import qz.common.Constants;
import java.time.Instant;
import java.util.Arrays;
public class RequestState {
public enum Validity {
TRUSTED("Valid"),
EXPIRED("Expired Signature"),
UNSIGNED("Invalid Signature"),
EXPIRED_CERT("Expired Certificate"),
FUTURE_CERT("Future Certificate"),
INVALID_CERT("Invalid Certificate"),
UNKNOWN("Invalid");
private String formatted;
Validity(String formatted) {
this.formatted = formatted;
}
public String getFormatted() {
return formatted;
}
}
Certificate certUsed;
JSONObject requestData;
boolean initialConnect;
Validity status;
public RequestState(Certificate cert, JSONObject data) {
certUsed = cert;
requestData = data;
status = Validity.UNKNOWN;
}
public Certificate getCertUsed() {
return certUsed;
}
public JSONObject getRequestData() {
return requestData;
}
public boolean isInitialConnect() {
return initialConnect;
}
public void markNewConnection(Certificate cert) {
certUsed = cert;
initialConnect = true;
checkCertificateState(cert);
}
public void checkCertificateState(Certificate cert) {
if (cert.isTrusted()) {
status = Validity.TRUSTED;
} else if (cert.getValidToDate().isBefore(Instant.now())) {
status = Validity.EXPIRED_CERT;
} else if (cert.getValidFromDate().isAfter(Instant.now())) {
status = Validity.FUTURE_CERT;
} else if (!cert.isValid()) {
status = Validity.INVALID_CERT;
} else {
status = Validity.UNKNOWN;
}
}
public Validity getStatus() {
return status;
}
public void setStatus(Validity state) {
status = state;
}
public boolean hasCertificate() {
return certUsed != null && certUsed != Certificate.UNKNOWN;
}
public boolean hasSavedCert() {
return isVerified() && certUsed.isSaved();
}
public boolean hasBlockedCert() {
return certUsed == null || certUsed.isBlocked();
}
public String getCertName() {
return certUsed.getCommonName();
}
public boolean isVerified() {
return certUsed.isTrusted() && status == Validity.TRUSTED;
}
public boolean isSponsored() {
return certUsed.isSponsored();
}
public String getValidityInfo() {
if (status == Validity.TRUSTED) {
return Constants.TRUSTED_CERT;
} else if (Arrays.asList(Validity.UNSIGNED, Validity.EXPIRED, Validity.EXPIRED_CERT, Validity.FUTURE_CERT).contains(status)) {
return Constants.NO_TRUST + " - " + status.getFormatted();
} else {
return Constants.UNTRUSTED_CERT;
}
}
}

View File

@@ -0,0 +1,12 @@
package qz.auth;
/**
* Created by Tres on 3/3/2015.
*/
public class X509Constants {
public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
public static final String END_CERT = "-----END CERTIFICATE-----";
public static final String INTERMEDIATE_CERT = "--START INTERMEDIATE CERT--";
public static final String BEGIN_CRL = "-----BEGIN X509 CRL-----";
public static final String END_CRL = "-----END X509 CRL-----";
}

View File

@@ -0,0 +1,129 @@
package qz.build;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Fetches a zip or tarball from URL and decompresses it
*/
public class Fetcher {
public enum Format {
ZIP(".zip"),
TARBALL(".tar.gz"),
UNKNOWN(null);
String suffix;
Format(String suffix) {
this.suffix = suffix;
}
public String getSuffix() {
return suffix;
}
public static Format parse(String url) {
for(Format format : Format.values()) {
if (url.endsWith(format.getSuffix())) {
return format;
}
}
return UNKNOWN;
}
}
private static final Logger log = LogManager.getLogger(Fetcher.class);
public static void main(String ... args) throws IOException {
new Fetcher("jlink/qz-tray-src_x.x.x", "https://github.com/qzind/tray/archive/master.tar.gz").fetch().uncompress();
}
String resourceName;
String url;
Format format;
Path rootDir;
File tempArchive;
File tempExtracted;
File extracted;
public Fetcher(String resourceName, String url) {
this.url = url;
this.resourceName = resourceName;
this.format = Format.parse(url);
// Try to calculate out/
this.rootDir = SystemUtilities.getJarParentPath().getParent();
}
@SuppressWarnings("unused")
public Fetcher(String resourceName, String url, Format format, String rootDir) {
this.resourceName = resourceName;
this.url = url;
this.format = format;
this.rootDir = Paths.get(rootDir);
}
public Fetcher fetch() throws IOException {
extracted = new File(rootDir.toString(), resourceName);
if(extracted.isDirectory() && extracted.exists()) {
log.info("Resource '{}' from [{}] has already been downloaded and extracted. Using: [{}]", resourceName, url, extracted);
} else {
tempExtracted = new File(rootDir.toString(), resourceName + "~tmp");
if(tempExtracted.exists()) {
FileUtils.deleteDirectory(tempExtracted);
}
// temp directory to thwart partial extraction
tempExtracted.mkdirs();
tempArchive = File.createTempFile(resourceName, ".zip");
log.info("Fetching '{}' from [{}] and saving to [{}]", resourceName, url, tempArchive);
FileUtils.copyURLToFile(new URL(url), tempArchive);
}
return this;
}
public String uncompress() throws IOException {
if(tempArchive != null) {
log.info("Unzipping '{}' from [{}] to [{}]", resourceName, tempArchive, tempExtracted);
if(format == Format.ZIP) {
unzip(tempArchive.getAbsolutePath(), tempExtracted);
} else {
untar(tempArchive.getAbsolutePath(), tempExtracted);
}
log.info("Moving [{}] to [{}]", tempExtracted, extracted);
tempExtracted.renameTo(extracted);
}
return extracted.toString();
}
public static void untar(String sourceFile, File targetDir) throws IOException {
// TODO: Switch to TarArchiveInputStream from Apache Commons Compress
if (!ShellUtilities.execute("tar", "-xzf", sourceFile, "-C", targetDir.getPath())) {
throw new IOException("Something went wrong extracting " + sourceFile +", check logs for details");
}
}
public static void unzip(String sourceFile, File targetDir) throws IOException {
try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(sourceFile))) {
for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) {
Path resolvedPath = targetDir.toPath().resolve(ze.getName());
if (ze.isDirectory()) {
Files.createDirectories(resolvedPath);
} else {
Files.createDirectories(resolvedPath.getParent());
Files.copy(zipIn, resolvedPath);
}
}
}
}
}

View File

@@ -0,0 +1,341 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2020 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.build;
import com.github.zafarkhaja.semver.Version;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.build.jlink.Platform;
import qz.build.jlink.Vendor;
import qz.build.jlink.Url;
import qz.build.provision.params.Arch;
import qz.common.Constants;
import qz.utils.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
public class JLink {
private static final Logger log = LogManager.getLogger(JLink.class);
public static final Vendor JAVA_DEFAULT_VENDOR = Vendor.BELLSOFT;
private static final String JAVA_DEFAULT_VERSION = "11.0.17+7";
private static final String JAVA_DEFAULT_GC_ENGINE = "hotspot"; // or "openj9"
private static final String JAVA_DEFAULT_GC_VERSION = "0.35.0"; // openj9 gc only
private Path jarPath;
private Path jdepsPath;
private Path jlinkPath;
private Path jmodsPath;
private Path outPath;
private Version jdepsVersion;
private Platform hostPlatform;
private Platform targetPlatform;
private Arch hostArch;
private Arch targetArch;
private Vendor javaVendor;
private String gcEngine;
private String javaVersion;
private String gcVersion;
private Path targetJdk;
private Version javaSemver;
private LinkedHashSet<String> depList;
public JLink(String targetPlatform, String targetArch, String javaVendor, String javaVersion, String gcEngine, String gcVersion, String targetJdk) throws IOException {
this.hostPlatform = Platform.getCurrentPlatform();
this.hostArch = SystemUtilities.getArch();
this.targetPlatform = Platform.parse(targetPlatform, this.hostPlatform);
this.targetArch = Arch.parse(targetArch, this.hostArch);
this.javaVendor = Vendor.parse(javaVendor, JAVA_DEFAULT_VENDOR);
this.gcEngine = getParam("gcEngine", gcEngine, JAVA_DEFAULT_GC_ENGINE);
this.javaVersion = getParam("javaVersion", javaVersion, JAVA_DEFAULT_VERSION);
this.gcVersion = getParam("gcVersion", gcVersion, JAVA_DEFAULT_GC_VERSION);
this.javaSemver = SystemUtilities.getJavaVersion(this.javaVersion);
// Optional: Provide the location of a custom JDK on the local filesystem
if(!StringUtils.isEmpty(targetJdk)) {
Path jdkPath = Paths.get(targetJdk);
Properties jdkProps = new Properties();
jdkProps.load(new FileInputStream(jdkPath.resolve("release").toFile()));
String customVersion = jdkProps.getProperty("JAVA_VERSION");
if(customVersion.contains("\"")) {
customVersion = customVersion.split("\"")[1];
}
Version customSemver = SystemUtilities.getJavaVersion(customVersion);
if(needsDownload(javaSemver, customSemver)) {
// The "release" file doesn't have build info, so we can't auto-download :(
if(javaSemver.getMajorVersion() != customSemver.getMajorVersion()) {
log.error("Error: jlink version {}.0 does not match target java.base version {}.0", javaSemver.getMajorVersion(), customSemver.getMajorVersion());
} else {
// Handle edge-cases (e.g. JDK-8240734)
log.error("Error: jlink version {} is incompatible with target java.base version {}", javaSemver.getMajorVersion(), customSemver.getMajorVersion());
}
System.exit(2);
}
this.targetJdk = Paths.get(targetJdk);
calculateToolPaths(Paths.get(targetJdk));
} else {
// Determine if the version we're building with is compatible with the target version
if (needsDownload(javaSemver, Constants.JAVA_VERSION)) {
log.warn("Java versions are incompatible, locating a suitable runtime for Java " + javaSemver.getMajorVersion() + "...");
String hostJdk = downloadJdk(this.hostArch, this.hostPlatform);
calculateToolPaths(Paths.get(hostJdk));
} else {
calculateToolPaths(null);
}
}
if(this.targetJdk == null) {
targetJdk = downloadJdk(this.targetArch, this.targetPlatform);
jmodsPath = Paths.get(targetJdk, "jmods");
} else {
log.info("\"targetjdk\" was provided {}, skipping download", targetJdk);
jmodsPath = this.targetJdk.resolve("jmods");
}
log.info("Selecting jmods folder: {}", jmodsPath);
calculateJarPath()
.calculateOutPath()
.calculateDepList()
.deployJre();
}
public static void main(String ... args) throws IOException {
new JLink(null, null, null, null, null, null, null).calculateJarPath();
}
/**
* Handle incompatibilities between JDKs, download a fresh one if needed
*/
private static boolean needsDownload(Version want, Version installed) {
// jdeps and jlink historically require matching major JDK versions. Download if needed.
boolean downloadJdk = installed.getMajorVersion() != want.getMajorVersion();
// Per JDK-8240734: Major versions checks aren't enough starting with 11.0.16+8
// see also https://github.com/adoptium/adoptium-support/issues/557
Version bad = SystemUtilities.getJavaVersion("11.0.16+8");
if(want.greaterThanOrEqualTo(bad) && installed.lessThan(bad) ||
installed.greaterThanOrEqualTo(bad) && want.lessThan(bad)) {
// Force download
// Fixes "Hash of java.rmi differs from expected hash"
downloadJdk = true;
}
return downloadJdk;
}
/**
* Download the JDK and return the path it was extracted to
*/
private String downloadJdk(Arch arch, Platform platform) throws IOException {
String url = new Url(this.javaVendor).format(arch, platform, this.gcEngine, this.javaSemver, this.javaVersion, this.gcVersion);
// Saves to out e.g. "out/jlink/jdk-AdoptOpenjdk-amd64-platform-11_0_7"
String extractedJdk = new Fetcher(String.format("jlink/jdk-%s-%s-%s-%s", javaVendor.value(), arch, platform.value(), javaSemver.toString().replaceAll("\\+", "_")), url)
.fetch()
.uncompress();
// Get first subfolder, e.g. jdk-11.0.7+10
for(File subfolder : new File(extractedJdk).listFiles(pathname -> pathname.isDirectory())) {
extractedJdk = subfolder.getPath();
if(platform == Platform.MAC && Paths.get(extractedJdk, "/Contents/Home").toFile().isDirectory()) {
extractedJdk += "/Contents/Home";
}
log.info("Selecting JDK home: {}", extractedJdk);
break;
}
return extractedJdk;
}
private JLink calculateJarPath() {
if(SystemUtilities.isJar()) {
jarPath = SystemUtilities.getJarPath();
} else {
// Detect out/dist/qz-tray.jar for IDE usage
jarPath = SystemUtilities.getJarParentPath()
.resolve("dist")
.resolve(Constants.PROPS_FILE + ".jar");
}
log.info("Assuming jar path: {}", jarPath);
return this;
}
private JLink calculateOutPath() {
switch(targetPlatform) {
case MAC:
outPath = jarPath.resolve("../Java.runtime/Contents/Home").normalize();
break;
default:
outPath = jarPath.resolve("../runtime").normalize();
}
log.info("Assuming output path: {}", outPath);
return this;
}
private JLink calculateToolPaths(Path javaHome) throws IOException {
if(javaHome == null) {
javaHome = Paths.get(System.getProperty("java.home"));
}
log.info("Using JAVA_HOME: {}", javaHome);
jdepsPath = javaHome.resolve("bin").resolve(SystemUtilities.isWindows() ? "jdeps.exe" : "jdeps").normalize();
jlinkPath = javaHome.resolve("bin").resolve(SystemUtilities.isWindows() ? "jlink.exe" : "jlink").normalize();
log.info("Assuming jdeps path: {}", jdepsPath);
log.info("Assuming jlink path: {}", jlinkPath);
jdepsPath.toFile().setExecutable(true, false);
jlinkPath.toFile().setExecutable(true, false);
jdepsVersion = SystemUtilities.getJavaVersion(jdepsPath);
return this;
}
private JLink calculateDepList() throws IOException {
log.info("Calling jdeps to determine runtime dependencies");
depList = new LinkedHashSet<>();
// JDK11.0.11+requires suppressing of missing deps
String raw = jdepsVersion.compareTo(Version.valueOf("11.0.10")) > 0 ?
ShellUtilities.executeRaw(jdepsPath.toString(), "--multi-release", "9", "--list-deps", "--ignore-missing-deps", jarPath.toString()) :
ShellUtilities.executeRaw(jdepsPath.toString(), "--multi-release", "9", "--list-deps", jarPath.toString());
if (raw == null || raw.trim().isEmpty() || raw.trim().startsWith("Warning") ) {
throw new IOException("An unexpected error occurred calling jdeps. Please check the logs for details.\n" + raw);
}
for(String item : raw.split("\\r?\\n")) {
item = item.trim();
if(!item.isEmpty()) {
if(item.startsWith("JDK") || item.startsWith("jdk8internals")) {
// Remove e.g. "JDK removed internal API/sun.reflect"
log.trace("Removing dependency: '{}'", item);
continue;
}
if(item.contains("/")) {
// Isolate base name e.g. "java.base/com.sun.net.ssl"
item = item.split("/")[0];
}
depList.add(item);
}
}
switch(targetPlatform) {
case WINDOWS:
// Java accessibility bridge dependency, see https://github.com/qzind/tray/issues/1234
depList.add("jdk.accessibility");
default:
// Adds "bin/jcmd"
depList.add("jdk.jcmd");
// "jar:" URLs create transient zipfs dependency, see https://stackoverflow.com/a/57846672/3196753
depList.add("jdk.zipfs");
// fix for https://github.com/qzind/tray/issues/894 solution from https://github.com/adoptium/adoptium-support/issues/397
depList.add("jdk.crypto.ec");
}
return this;
}
private JLink deployJre() throws IOException {
if(targetPlatform == Platform.MAC) {
// Deploy Contents/MacOS/libjli.dylib
Path macOS = Files.createDirectories(outPath.resolve("../MacOS").normalize());
Path jliLib = macOS.resolve("libjli.dylib");
log.info("Deploying {}", macOS);
try {
// Not all jdks use a bundle format, but try this first
Files.copy(jmodsPath.resolve("../../MacOS/libjli.dylib").normalize(), jliLib, StandardCopyOption.REPLACE_EXISTING);
} catch(IOException ignore) {
// Fallback to flat format
String libjli = "../lib/jli/libjli.dylib";
if(javaSemver.getMajorVersion() >= 21) {
libjli = "../lib/libjli.dylib";
}
Files.copy(jmodsPath.resolve(libjli).normalize(), jliLib, StandardCopyOption.REPLACE_EXISTING);
}
// Deploy Contents/Info.plist
HashMap<String, String> fieldMap = new HashMap<>();
fieldMap.put("%BUNDLE_ID%", MacUtilities.getBundleId() + ".jre"); // e.g. io.qz.qz-tray.jre
fieldMap.put("%BUNDLE_VERSION%", String.format("%s.%s.%s", javaSemver.getMajorVersion(), javaSemver.getMinorVersion(), javaSemver.getPatchVersion()));
fieldMap.put("%BUNDLE_VERSION_FULL%", javaSemver.toString());
fieldMap.put("%BUNDLE_VENDOR%", javaVendor.getVendorName());
fieldMap.put("%BUNDLE_PRODUCT%", javaVendor.getProductName());
log.info("Deploying {}/Info.plist", macOS.getParent());
FileUtilities.configureAssetFile("assets/mac-runtime.plist.in", macOS.getParent().resolve("Info.plist"), fieldMap, JLink.class);
}
FileUtils.deleteQuietly(outPath.toFile());
if(ShellUtilities.execute(jlinkPath.toString(),
"--strip-debug",
"--compress=2",
"--no-header-files",
"--no-man-pages",
"--exclude-files=glob:**/legal/**",
"--module-path", jmodsPath.toString(),
"--add-modules", String.join(",", depList),
"--output", outPath.toString())) {
log.info("Successfully deployed a jre to {}", outPath);
// Remove all but java/javaw
List<String> keepFiles = new ArrayList<>();
//String[] keepFiles;
String keepExt;
switch(targetPlatform) {
case WINDOWS:
keepFiles.add("java.exe");
keepFiles.add("javaw.exe");
keepFiles.add("jcmd.exe");
if(depList.contains("jdk.accessibility")) {
// Java accessibility bridge switching tool
keepFiles.add("jabswitch.exe");
}
// Windows stores ".dll" files in bin
keepExt = ".dll";
break;
default:
keepFiles.add("java");
keepFiles.add("jcmd");
keepExt = null;
}
Files.list(outPath.resolve("bin")).forEach(binFile -> {
if(Files.isDirectory(binFile) || (keepExt != null && binFile.toString().endsWith(keepExt))) {
log.info("Keeping {}", binFile);
return; // iterate forEach
}
for(String name : keepFiles) {
if (binFile.endsWith(name)) {
log.info("Keeping {}", binFile);
return; // iterate forEach
}
}
log.info("Deleting {}", binFile);
binFile.toFile().delete();
});
return this;
}
throw new IOException("An error occurred deploying the jre. Please check the logs for details.");
}
public static String getParam(String paramName, String value, String fallback) {
if(value != null && !value.isEmpty() && !value.trim().isEmpty()) {
return value;
}
log.info("No {} specified, assuming '{}'", paramName, fallback);
return fallback;
}
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>libjli.dylib</string>
<key>CFBundleGetInfoString</key>
<string>%BUNDLE_VENDOR% %BUNDLE_PRODUCT% %BUNDLE_VERSION_FULL%</string>
<key>CFBundleIdentifier</key>
<string>%BUNDLE_ID%</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>7.0</string>
<key>CFBundleName</key>
<string>Java Runtime Image</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>%BUNDLE_VERSION%</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>%BUNDLE_VERSION%</string>
<key>JavaVM</key>
<dict>
<key>JVMCapabilities</key>
<array>
<string>CommandLine</string>
<string>JNI</string>
<string>BundledApp</string>
</array>
<key>JVMMinimumFrameworkVersion</key>
<string>17.0.0</string>
<key>JVMMinimumSystemVersion</key>
<string>10.6.0</string>
<key>JVMPlatformVersion</key>
<string>%BUNDLE_VERSION_FULL%</string>
<key>JVMVendor</key>
<string>%BUNDLE_VENDOR%</string>
<key>JVMVersion</key>
<string>%BUNDLE_VERSION%</string>
</dict>
</dict>
</plist>

View File

View File

@@ -0,0 +1,68 @@
package qz.build.jlink;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.reflect.Field;
import java.util.Locale;
/**
* A special template class for handling enums with varargs needing string matches.
*
* Parsable enums must declare <code>public static void String[] matches;</code>
* in the constructor, which <code>parse(Class enumType, </T>String value) will
* call using reflection.
*
* Enums are inherently static in Java and cannot extend superclasses. The
* workaround to avoid code duplication is to leverage reflection and generics in
* static utility functions.
*
* The downsides of this are:
* - Reflection is slow
* - Static helpers must be explicitly class-type-aware*
*
* *Non-static methods may be implicit, but create anti-patterns for static helpers
* such as <code>parse(String value)</code> as they would exist at
* <code>ENUM_ENTRY.parse(...)</code> rather than <code>EnumClass.parse(...)</code>.
*/
public interface Parsable<T extends Enum> {
Logger log = LogManager.getLogger(Parsable.class);
static <T extends Enum<T>> T parse(Class<T> enumType, String value) {
if(value != null && !value.trim().isEmpty()) {
for(T parsable : enumType.getEnumConstants()) {
try {
Field matchesField = parsable.getClass().getDeclaredField("matches");
String[] matches = (String[])matchesField.get(parsable);
for(String match : matches) {
if (match.equalsIgnoreCase(value)) {
return parsable;
}
}
} catch(NoSuchFieldException | IllegalAccessException | ClassCastException e) {
log.warn("Parsable enums must have a 'public String[] matches' field", e);
}
}
}
log.warn("Could not parse {} as a valid {} value", value, enumType.getSimpleName());
return null;
}
static <T extends Enum<T>> T parse(Class<T> enumType, String value, T fallback, boolean silent) {
if(value != null && !value.trim().isEmpty()) {
return parse(enumType, value);
}
if(!silent) {
log.warn("No {} specified, assuming '{}'", enumType.getSimpleName(), ((Parsable)fallback).value());
}
return fallback;
}
static <T extends Enum<T>> T parse(Class<T> enumType, String value, T fallback) {
return parse(enumType, value, fallback, false);
}
default String value() {
return ((T)this).toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,37 @@
package qz.build.jlink;
import qz.utils.SystemUtilities;
/**
* Handling of platform names as they would appear in a URL
* Values added must also be added to <code>ArgValue.JLINK --platform</code> values
*/
public enum Platform implements Parsable {
MAC("mac"),
WINDOWS("windows"),
LINUX("linux");
public final String[] matches;
Platform(String ... matches) { this.matches = matches; }
public static Platform parse(String value, Platform fallback) {
return Parsable.parse(Platform.class, value, fallback);
}
public static Platform parse(String value) {
return Parsable.parse(Platform.class, value);
}
public static Platform getCurrentPlatform() {
switch(SystemUtilities.getOs()) {
case MAC:
return Platform.MAC;
case WINDOWS:
return Platform.WINDOWS;
case LINUX:
default:
return Platform.LINUX;
}
}
}

View File

@@ -0,0 +1,72 @@
package qz.build.jlink;
import com.github.zafarkhaja.semver.Version;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.build.provision.params.Arch;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import static qz.build.jlink.Vendor.*;
/**
* Each JDK provider uses their own url format
*/
public class Url {
static HashMap<Vendor, String> VENDOR_URL_MAP = new HashMap<>();
static {
VENDOR_URL_MAP.put(BELLSOFT, "https://download.bell-sw.com/java/%s/bellsoft-jdk%s-%s-%s.%s");
VENDOR_URL_MAP.put(ECLIPSE, "https://github.com/adoptium/temurin%s-binaries/releases/download/jdk-%s/OpenJDK%sU-jdk_%s_%s_%s_%s.%s");
VENDOR_URL_MAP.put(IBM, "https://github.com/ibmruntimes/semeru%s-binaries/releases/download/jdk-%s_%s-%s/ibm-semeru-open-jdk_%s_%s_%s_%s-%s.%s");
VENDOR_URL_MAP.put(MICROSOFT, "https://aka.ms/download-jdk/microsoft-jdk-%s-%s-%s.%s");
VENDOR_URL_MAP.put(AMAZON, "https://corretto.aws/downloads/resources/%s/amazon-corretto-%s-%s-%s.%s");
VENDOR_URL_MAP.put(AZUL, "https://cdn.azul.com/zulu%s/bin/zulu%s-ca-jdk%s-%s_%s.%s");
}
private static final Logger log = LogManager.getLogger(Url.class);
Vendor vendor;
String pattern;
public Url(Vendor vendor) {
this.vendor = vendor;
if(!VENDOR_URL_MAP.containsKey(vendor)) {
throw new UnsupportedOperationException(String.format("Vendor provided '%s' couldn't be matched to a URL pattern, aborting.", vendor));
}
pattern = VENDOR_URL_MAP.get(vendor);
}
public String format(Arch arch, Platform platform, String gcEngine, Version javaSemver, String javaVersion, String gcVer) throws UnsupportedEncodingException {
Url pattern = new Url(vendor);
String urlArch = vendor.getUrlArch(arch);
String fileExt = vendor.getUrlExtension(platform);
String urlPlatform = vendor.getUrlPlatform(platform);
String urlJavaVersion = vendor.getUrlJavaVersion(javaSemver);
// Convert "+" to "%2B"
String urlJavaVersionEncode = URLEncoder.encode(javaVersion, "UTF-8");
int javaMajor = javaSemver.getMajorVersion();
switch(vendor) {
case BELLSOFT:
return String.format(pattern.pattern, urlJavaVersionEncode, urlJavaVersionEncode, urlPlatform, urlArch, fileExt);
case ECLIPSE:
return String.format(pattern.pattern, javaMajor, urlJavaVersionEncode, javaMajor, urlArch, urlPlatform, gcEngine, urlJavaVersion, fileExt);
case IBM:
return String.format(pattern.pattern, javaMajor, urlJavaVersionEncode, gcEngine, gcVer, urlArch, urlPlatform, urlJavaVersion, gcEngine, gcVer, fileExt);
case MICROSOFT:
return String.format(pattern.pattern, urlJavaVersion, urlPlatform, urlArch, fileExt);
case AMAZON:
return String.format(pattern.pattern, urlJavaVersion, urlJavaVersion, urlPlatform, urlArch, fileExt);
case AZUL:
// Special handling of Linux aarch64
String embedded = platform == Platform.LINUX ? "-embedded" : "";
return String.format(pattern.pattern, embedded, gcVer, urlJavaVersion, urlPlatform, urlArch, fileExt);
default:
throw new UnsupportedOperationException(String.format("URL pattern for '%s' (%s) is missing a format implementation.", vendor, pattern));
}
}
}

View File

@@ -0,0 +1,155 @@
package qz.build.jlink;
import com.github.zafarkhaja.semver.Version;
import qz.build.provision.params.Arch;
/**
* Handling of java vendors
*/
public enum Vendor implements Parsable {
ECLIPSE("Eclipse", "Adoptium", "adoptium", "temurin", "adoptopenjdk"),
BELLSOFT("BellSoft", "Liberica", "bellsoft", "liberica"),
IBM("IBM", "Semeru", "ibm", "semeru"),
MICROSOFT("Microsoft", "OpenJDK", "microsoft"),
AMAZON("Amazon", "Corretto", "amazon", "corretto"),
AZUL("Azul", "Zulu", "azul", "zulu");
public String vendorName;
public String productName;
public final String[] matches;
Vendor(String vendorName, String productName, String ... matches) {
this.matches = matches;
this.vendorName = vendorName;
this.productName = productName;
}
public static Vendor parse(String value, Vendor fallback) {
return Parsable.parse(Vendor.class, value, fallback, true);
}
public static Vendor parse(String value) {
return Parsable.parse(Vendor.class, value);
}
public String getVendorName() {
return vendorName;
}
public String getProductName() {
return productName;
}
/**
* Map Vendor to Arch value
*/
public String getUrlArch(Arch arch) {
switch(arch) {
case AARCH64:
// All vendors seem to use "aarch64" universally
return "aarch64";
case ARM32:
switch(this) {
case BELLSOFT:
return "arm32-vfp-hflt";
case AZUL:
return "aarch32hf";
case MICROSOFT:
case IBM:
throw new UnsupportedOperationException("Vendor does not provide builds for this architecture");
case AMAZON:
case ECLIPSE:
default:
return "arm";
}
case RISCV64:
return "riscv64";
case X86:
switch(this) {
case AZUL:
return "i686";
case BELLSOFT:
return "i586";
case ECLIPSE:
case IBM:
return "x86-32";
case AMAZON:
default:
return "x86";
}
case X86_64:
default:
switch(this) {
// BellSoft uses "amd64"
case BELLSOFT:
return "amd64";
}
return "x64";
}
}
/**
* Map Vendor to Platform name
*/
public String getUrlPlatform(Platform platform) {
switch(platform) {
case MAC:
switch(this) {
case BELLSOFT:
return "macos";
case MICROSOFT:
return "macOS";
case AMAZON:
case AZUL:
return "macosx";
}
default:
return platform.value();
}
}
/**
* Map Vendor and Platform to file extension
*/
public String getUrlExtension(Platform platform) {
switch(this) {
case BELLSOFT:
switch(platform) {
case LINUX:
return "tar.gz";
default:
// BellSoft uses "zip" for mac and windows platforms
return "zip";
}
default:
switch(platform) {
case WINDOWS:
return "zip";
default:
return "tar.gz";
}
}
}
public String getUrlJavaVersion(Version javaSemver) {
switch(this) {
case MICROSOFT:
case AZUL:
// Return shorted version (Microsoft, Azul suppresses the build information from URLs)
return javaSemver.toString().split("\\+")[0];
case AMAZON:
// Return lengthened version (Corretto formats major.minor.patch.build.number, e.g. 11.0.17.8.1)
String[] parts = javaSemver.toString().split("\\+");
String javaVersion = parts[0];
//
String buildAndNumber = parts[1];
if(!buildAndNumber.contains(".")) {
// Append ".1" if ".number" is missing
buildAndNumber += ".1";
}
return String.format("%s.%s", javaVersion, buildAndNumber);
}
// All others seem to prefer "+" replaced with "_"
return javaSemver.toString().replaceAll("\\+", "_");
}
}

View File

@@ -0,0 +1,328 @@
package qz.build.provision;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.build.provision.params.Arch;
import qz.build.provision.params.Os;
import qz.build.provision.params.Phase;
import qz.build.provision.params.Type;
import qz.common.Constants;
import qz.installer.provision.invoker.PropertyInvoker;
import qz.utils.ArgValue;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
public class ProvisionBuilder {
protected static final Logger log = LogManager.getLogger(ProvisionBuilder.class);
public static final Path BUILD_PROVISION_FOLDER = SystemUtilities.getJarParentPath().resolve(Constants.PROVISION_DIR);
public static final File BUILD_PROVISION_FILE = BUILD_PROVISION_FOLDER.resolve(Constants.PROVISION_FILE).toFile();
private File ingestFile;
private JSONArray jsonSteps;
private Arch targetArch;
private Os targetOs;
/**
* Parses command line input to create a "provision" folder in the dist directory for customizing the installation or startup
*/
public ProvisionBuilder(String type, String phase, String os, String arch, String data, String args, String description, String ... varArgs) throws IOException, JSONException {
createProvisionDirectory(false);
targetOs = Os.ALL;
targetArch = Arch.ALL;
jsonSteps = new JSONArray();
// Wrap into JSON so that we can save it
JSONObject jsonStep = new JSONObject();
putPattern(jsonStep, "description", description);
putPattern(jsonStep, "type", type);
putPattern(jsonStep, "phase", phase);
putPattern(jsonStep, "os", os);
putPattern(jsonStep, "arch", arch);
putPattern(jsonStep, "data", data);
putPattern(jsonStep, "args", args);
putPattern(jsonStep, "arg%d", varArgs);
// Command line invocation, use the working directory
Path relativePath = Paths.get(System.getProperty("user.dir"));
ingestStep(jsonStep, relativePath);
}
/**
* To be called by ant's <code>provision</code> target
*/
public ProvisionBuilder(File antJsonFile, String antTargetOs, String antTargetArch) throws IOException, JSONException {
createProvisionDirectory(true);
// Calculate the target os, architecture
this.targetArch = Arch.parseStrict(antTargetArch);
this.targetOs = Os.parseStrict(antTargetOs);
this.jsonSteps = new JSONArray();
this.ingestFile = antJsonFile;
String jsonData = FileUtils.readFileToString(antJsonFile, StandardCharsets.UTF_8);
JSONArray pendingSteps = new JSONArray(jsonData);
// Cycle through so that each Step can be individually processed
Path relativePath = antJsonFile.toPath().getParent();
for(int i = 0; i < pendingSteps.length(); i++) {
JSONObject jsonStep = pendingSteps.getJSONObject(i);
System.out.println();
try {
ingestStep(jsonStep, relativePath);
} catch(Exception e) {
log.warn("[SKIPPED] Step '{}'", jsonStep, e);
}
}
}
public JSONArray getJson() {
return jsonSteps;
}
/**
* Construct as a Step to perform basic parsing/sanity checks
* Copy resources (if needed) to provisioning directory
*/
private void ingestStep(JSONObject jsonStep, Path relativePath) throws JSONException, IOException {
Step step = Step.parse(jsonStep, relativePath);
if(!targetOs.matches(step.os)) {
log.info("[SKIPPED] Os '{}' does not match target Os '{}' '{}'", Os.serialize(step.os), targetOs, step);
return;
}
if(!targetArch.matches(step.arch)) {
log.info("[SKIPPED] Arch '{}' does not match target Os '{}' '{}'", Arch.serialize(step.arch), targetArch, step);
return;
}
// Inject any special inferences (such as inferring resources from args)
inferAdditionalSteps(step);
if(copyResource(step)) {
log.info("[SUCCESS] Step successfully processed '{}'", step);
jsonSteps.put(step.toJSON());
// Special case for custom websocket ports
if(step.getType() == Type.PROPERTY && step.getPhase() == Phase.CERTGEN) {
HashMap<String, String> pairs = PropertyInvoker.parsePropertyPairs(step);
if(pairs.get(ArgValue.WEBSOCKET_SECURE_PORTS.getMatch()) != null ||
pairs.get(ArgValue.WEBSOCKET_INSECURE_PORTS.getMatch()) != null) {
// Clone to install step
jsonSteps.put(step.cloneTo(Phase.INSTALL).toJSON());
}
}
} else {
log.error("[SKIPPED] Resources could not be saved '{}'", step);
}
}
/**
* Save any resources files required for INSTALL and SCRIPT steps to provision folder
*/
public boolean copyResource(Step step) throws IOException {
switch(step.getType()) {
case CA:
case CERT:
case SCRIPT:
case RESOURCE:
case SOFTWARE:
boolean isRelative = !Paths.get(step.getData()).isAbsolute();
File src;
if(isRelative) {
if(ingestFile != null) {
Path parentDir = ingestFile.getParentFile().toPath();
src = parentDir.resolve(step.getData()).toFile();
} else {
throw formatted("Unable to resolve path: '%s' '%s'", step.getData(), step);
}
} else {
src = new File(step.getData());
}
String fileName = src.getName();
if(fileName.equals(BUILD_PROVISION_FILE.getName())) {
throw formatted("Resource name conflicts with provision file '%s' '%s'", fileName, step);
}
File dest = BUILD_PROVISION_FOLDER.resolve(fileName).toFile();
int i = 0;
// Avoid conflicting file names
String name = dest.getName();
// Avoid resource clobbering when being invoked by command line or providing certificates.
// Otherwise, assume the intent is to re-use the same resource (e.g. "my_script.sh", etc)
if(ingestFile == null || step.getType() == Type.CERT) {
while(dest.exists()) {
// Append "filename-1.txt" until there's no longer a conflict
if (name.contains(".")) {
dest = BUILD_PROVISION_FOLDER.resolve(String.format("%s-%s.%s", FilenameUtils.removeExtension(name), ++i,
FilenameUtils.getExtension(name))).toFile();
} else {
dest = BUILD_PROVISION_FOLDER.resolve(String.format("%-%", name, ++i)).toFile();
}
}
}
FileUtils.copyFile(src, dest);
if(dest.exists()) {
step.setData(BUILD_PROVISION_FOLDER.relativize(dest.toPath()).toString());
} else {
return false;
}
break;
default:
}
return true;
}
/**
* Appends the JSONObject to the end of the provisionFile
*/
public boolean saveJson(boolean overwrite) throws IOException, JSONException {
// Read existing JSON file if exists
JSONArray mergeSteps;
if(!overwrite && BUILD_PROVISION_FILE.exists()) {
String jsonData = FileUtils.readFileToString(BUILD_PROVISION_FILE, StandardCharsets.UTF_8);
mergeSteps = new JSONArray(jsonData);
} else {
mergeSteps = new JSONArray();
}
// Merge in new steps
for(int i = 0; i < jsonSteps.length(); i++) {
mergeSteps.put(jsonSteps.getJSONObject(i));
}
FileUtils.writeStringToFile(BUILD_PROVISION_FILE, mergeSteps.toString(3), StandardCharsets.UTF_8);
return true;
}
/**
* Convenience method for adding a name/value pair into the JSONObject
*/
private static void putPattern(JSONObject jsonStep, String name, String val) throws JSONException {
if(val != null && !val.isEmpty()) {
jsonStep.put(name, val);
}
}
/**
* Convenience method for adding consecutive patterned value pairs into the JSONObject
* e.g. --arg1 "foo" --arg2 "bar"
*/
private static void putPattern(JSONObject jsonStep, String pattern, String ... varArgs) throws JSONException {
int argCounter = 0;
for(String arg : varArgs) {
jsonStep.put(String.format(pattern, ++argCounter), arg);
}
}
private static void createProvisionDirectory(boolean cleanDirectory) throws IOException {
if(cleanDirectory) {
FileUtils.deleteDirectory(BUILD_PROVISION_FOLDER.toFile());
}
if(BUILD_PROVISION_FOLDER.toFile().isDirectory()) {
return;
}
if(BUILD_PROVISION_FOLDER.toFile().mkdirs()) {
return;
}
throw formatted("Could not create provision destination: '%'", BUILD_PROVISION_FOLDER);
}
private static IOException formatted(String message, Object ... args) {
String formatted = String.format(message, args);
return new IOException(formatted);
}
/**
* Returns the first index of the specified arg prefix pattern(s)
*
* e.g. if pattern is "/f1", it will return 1 from args { "/s", "/f1C:\foo" }
*/
private int argPrefixIndex(Step step, String ... prefixes) {
for(int i = 0; i < step.args.size() ; i++){
for(String prefix : prefixes) {
if (step.args.get(i).toLowerCase().startsWith(prefix.toLowerCase())) {
return i;
}
}
}
return -1;
}
/**
* Returns the "value" of the specified arg prefix pattern(s)
*
* e.g. if pattern is "/f1", it will return "C:\foo" from args { "/s", "/f1C:\foo" }
*
*/
private String argPrefixValue(Step step, int index, String ... prefixes) {
String arg = step.args.get(index);
String value = null;
for(String prefix : prefixes) {
if (arg.toLowerCase().startsWith(prefix.toLowerCase())) {
value = arg.substring(prefix.length());
if((value.startsWith("\"") && value.endsWith("\"")) ||
(value.startsWith("'") && value.endsWith("'"))) {
// Remove surrounding quotes
value = value.substring(1, value.length() - 1);
}
}
}
return value;
}
/**
* Clones the provided step into a new step that performs a prerequisite task.
*
* This is "magic" in the sense that it's highly specific to <code>Type</code>
* <code>Os</code> and <code>Step.args</code>.
*
* For example:
*
* Older InstallShield installers supported the <code>/f1</code> parameter which
* implies an answer file of which we need to bundle for a successful deployment.
*/
private void inferAdditionalSteps(Step orig) throws JSONException, IOException {
// Infer resource step for InstallShield .iss answer files
if(orig.getType() == Type.SOFTWARE && Os.WINDOWS.matches(orig.getOs())) {
String[] patterns = { "/f1", "-f1" };
int index = argPrefixIndex(orig, patterns);
if(index > 0) {
String resource = argPrefixValue(orig, index, patterns);
if(resource != null) {
// Clone to copy the Phase, Os and Description
Step step = orig.clone();
// Swap Type, clear args and update the data
step.setType(Type.RESOURCE);
step.setArgs(new ArrayList<>());
step.setData(resource);
if(copyResource(step)) {
File resourceFile = new File(resource);
jsonSteps.put(step.toJSON());
orig.getArgs().set(index, String.format("/f1\"%s\"", resourceFile.getName()));
log.info("[SUCCESS] Step successfully inferred and appended '{}'", step);
} else {
log.error("[SKIPPED] Resources could not be saved '{}'", step);
}
}
}
}
}
}

View File

@@ -0,0 +1,337 @@
package qz.build.provision;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.build.provision.params.Arch;
import qz.build.provision.params.Os;
import qz.build.provision.params.Phase;
import qz.build.provision.params.Type;
import qz.build.provision.params.types.Remover;
import qz.build.provision.params.types.Software;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
public class Step {
protected static final Logger log = LogManager.getLogger(Step.class);
String description;
Type type;
List<String> args; // Type.SCRIPT or Type.INSTALLER or Type.CONF only
HashSet<Os> os;
HashSet<Arch> arch;
Phase phase;
String data;
Path relativePath;
Class relativeClass;
public Step(Path relativePath, String description, Type type, HashSet<Os> os, HashSet<Arch> arch, Phase phase, String data, List<String> args) {
this.relativePath = relativePath;
this.description = description;
this.type = type;
this.os = os;
this.arch = arch;
this.phase = phase;
this.data = data;
this.args = args;
}
/**
* Only should be used by unit tests
*/
Step(Class relativeClass, String description, Type type, HashSet<Os> os, HashSet<Arch> arch, Phase phase, String data, List<String> args) {
this.relativeClass = relativeClass;
this.description = description;
this.type = type;
this.os = os;
this.arch = arch;
this.phase = phase;
this.data = data;
this.args = args;
}
@Override
public String toString() {
return "Step { " +
"description=\"" + description + "\", " +
"type=\"" + type + "\", " +
"os=\"" + Os.serialize(os) + "\", " +
"arch=\"" + Arch.serialize(arch) + "\", " +
"phase=\"" + phase + "\", " +
"data=\"" + data + "\", " +
"args=\"" + StringUtils.join(args, ",") + "\" " +
"}";
}
public JSONObject toJSON() throws JSONException {
JSONObject json = new JSONObject();
json.put("description", description)
.put("type", type)
.put("os", Os.serialize(os))
.put("arch", Arch.serialize(arch))
.put("phase", phase)
.put("data", data);
for(int i = 0; i < args.size(); i++) {
json.put(String.format("arg%s", i + 1), args.get(i));
}
return json;
}
public String getDescription() {
return description;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public List<String> getArgs() {
return args;
}
public void setArgs(List<String> args) {
this.args = args;
}
public HashSet<Os> getOs() {
return os;
}
public void setOs(HashSet<Os> os) {
this.os = os;
}
public HashSet<Arch> getArch() {
return arch;
}
public void setArch(HashSet<Arch> arch) {
this.arch = arch;
}
public Phase getPhase() {
return phase;
}
public void setPhase(Phase phase) {
this.phase = phase;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public Class getRelativeClass() {
return relativeClass;
}
public Path getRelativePath() {
return relativePath;
}
public boolean usingClass() {
return relativeClass != null;
}
public boolean usingPath() {
return relativePath != null;
}
public static Step parse(JSONObject jsonStep, Object relativeObject) {
String description = jsonStep.optString("description", "");
Type type = Type.parse(jsonStep.optString("type", null));
String data = jsonStep.optString("data", null);
// Handle installer args
List<String> args = new LinkedList<>();
if(type == Type.SOFTWARE || type == Type.CONF) {
// Handle space-delimited args
args = Software.parseArgs(jsonStep.optString("args", ""));
// Handle standalone single args (won't break on whitespace)
// e.g. "arg1": "C:\Program Files\Foo"
int argCounter = 0;
while(true) {
String singleArg = jsonStep.optString(String.format("arg%d", ++argCounter), "");
if(!singleArg.trim().isEmpty()) {
args.add(singleArg.trim());
} else {
// stop searching if the next incremental arg (e.g. "arg2") isn't found
break;
}
}
}
// Mandate "args" as the CONF path
if(type == Type.CONF) {
// Honor "path" first, if provided
String path = jsonStep.optString("path", "");
if(!path.isEmpty()) {
args.add(0, path);
}
// Keep only the first value
if(args.size() > 0) {
args = args.subList(0, 1);
} else {
throw formatted("Conf path value cannot be blank.");
}
}
HashSet<Os> os = new HashSet<>();
if(jsonStep.has("os")) {
// Do not tolerate bad os values
String osString = jsonStep.optString("os");
os = Os.parse(osString);
if(os.size() == 0) {
throw formatted("Os provided '%s' could not be parsed", osString);
}
}
HashSet<Arch> arch = new HashSet<>();
if(jsonStep.has("arch")) {
// Do not tolerate bad arch values
String archString = jsonStep.optString("arch");
arch = Arch.parse(archString);
if(arch.size() == 0) {
throw formatted("Arch provided \"%s\" could not be parsed", archString);
}
}
Phase phase = null;
if(jsonStep.has("phase")) {
String phaseString = jsonStep.optString("phase", null);
phase = Phase.parse(phaseString);
if(phase == null) {
log.warn("Phase provided \"{}\" could not be parsed", phaseString);
}
}
Step step;
if(relativeObject instanceof Path) {
step = new Step((Path)relativeObject, description, type, os, arch, phase, data, args);
} else if(relativeObject instanceof Class) {
step = new Step((Class)relativeObject, description, type, os, arch, phase, data, args);
} else {
throw formatted("Parameter relativeObject must be of type 'Path' or 'Class' but '%s' was provided", relativeObject.getClass());
}
return step.sanitize();
}
private Step sanitize() {
return throwIfNull("Type", type)
.throwIfNull("Data", data)
.validateOs()
.validateArch()
.enforcePhase(Type.PREFERENCE, Phase.STARTUP)
.enforcePhase(Type.CA, Phase.CERTGEN)
.enforcePhase(Type.CERT, Phase.STARTUP)
.enforcePhase(Type.CONF, Phase.CERTGEN)
.enforcePhase(Type.SOFTWARE, Phase.INSTALL)
.enforcePhase(Type.REMOVER, Phase.INSTALL)
.enforcePhase(Type.PROPERTY, Phase.CERTGEN, Phase.INSTALL)
.validateRemover();
}
private Step validateRemover() {
if(type != Type.REMOVER) {
return this;
}
Remover remover = Remover.parse(data);
switch(remover) {
case CUSTOM:
break;
default:
if(remover.matchesCurrentSystem()) {
throw formatted("Remover '%s' would conflict with this installer, skipping. ", remover);
}
return this;
}
// Custom removers must have three elements
if(data == null || data.split(",").length != 3) {
throw formatted("Remover data '%s' is invalid. Data must match a known type [%s] or contain exactly 3 elements.", data, Remover.valuesDelimited(","));
}
return this;
}
private Step throwIfNull(String name, Object value) {
if(value == null) {
throw formatted("%s cannot be null", name);
}
return this;
}
private Step validateOs() {
if(os == null) {
if(type == Type.SOFTWARE) {
// Software must default to a sane operating system
os = Software.parse(data).defaultOs();
} else {
os = new HashSet<>();
}
}
if(os.size() == 0) {
os.add(Os.ALL);
log.debug("Os list is null, assuming '{}'", Os.ALL);
}
return this;
}
private Step validateArch() {
if(arch == null) {
arch = new HashSet<>();
}
if(arch.size() == 0) {
arch.add(Arch.ALL);
log.debug("Arch list is null, assuming '{}'", Arch.ALL);
}
return this;
}
private Step enforcePhase(Type matchType, Phase ... requiredPhases) {
if(requiredPhases.length == 0) {
throw new UnsupportedOperationException("At least one Phase must be specified");
}
if(type == matchType) {
for(Phase requiredPhase : requiredPhases) {
if (phase == null) {
phase = requiredPhase;
log.debug("Phase is null, defaulting to '{}' based on Type '{}'", phase, type);
return this;
} else if (phase == requiredPhase) {
return this;
}
}
log.debug("Phase '{}' is unsupported for Type '{}', defaulting to '{}'", phase, type, phase = requiredPhases[0]);
}
return this;
}
private static UnsupportedOperationException formatted(String message, Object ... args) {
String formatted = String.format(message, args);
return new UnsupportedOperationException(formatted);
}
Step cloneTo(Phase phase) {
return relativePath != null ?
new Step(relativePath, description, type, os, arch, phase, data, args) :
new Step(relativeClass, description, type, os, arch, phase, data, args);
}
public Step clone() {
return cloneTo(this.phase);
}
}

View File

@@ -0,0 +1,70 @@
package qz.build.provision.params;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
/**
* Basic architecture parser
*
* Note: All aliases must be lowercase
*/
public enum Arch {
X86("x32", "i386", "i486", "i586", "i686"),
X86_64("amd64"),
ARM32("arm", "armv1", "armv2", "armv3", "armv4", "armv5", "armv6", "armv7"),
AARCH64("arm64", "armv8", "armv9"),
RISCV32("rv32"),
RISCV64("rv64"),
PPC64("powerpc", "powerpc64"),
ALL(), // special handling
UNKNOWN();
private HashSet<String> aliases = new HashSet<>();
Arch(String ... aliases) {
this.aliases.add(name().toLowerCase(Locale.ENGLISH));
this.aliases.addAll(Arrays.asList(aliases));
}
public static Arch parseStrict(String input) throws UnsupportedOperationException {
return EnumParser.parseStrict(Arch.class, input, ALL, UNKNOWN);
}
public static HashSet<Arch> parse(String input) {
return EnumParser.parseSet(Arch.class, Arch.ALL, input);
}
public static Arch parse(String input, Arch fallback) {
Arch found = bestMatch(input);
return found == UNKNOWN ? fallback : found;
}
public static Arch bestMatch(String input) {
if(input != null) {
for(Arch arch : values()) {
if (arch.aliases.contains(input.toLowerCase())) {
return arch;
}
}
}
return Arch.UNKNOWN;
}
public boolean matches(HashSet<Arch> archList) {
return this == ALL || archList.contains(ALL) || (this != UNKNOWN && archList.contains(this));
}
public static String serialize(HashSet<Arch> archList) {
if(archList.contains(ALL)) {
return "*";
}
return StringUtils.join(archList, "|");
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,64 @@
package qz.build.provision.params;
import java.util.EnumSet;
import java.util.HashSet;
public interface EnumParser {
/**
* Basic enum parser
*/
static <T extends Enum<T>> T parse(Class<T> clazz, String s) {
return parse(clazz, s, null);
}
static <T extends Enum<T>> T parse(Class<T> clazz, String s, T fallbackValue) {
if(s != null) {
for(T en : EnumSet.allOf(clazz)) {
if (en.name().equalsIgnoreCase(s)) {
return en;
}
}
}
return fallbackValue;
}
static <T extends Enum<T>> T parseStrict(Class<T> clazz, String s, T ... blocklist) throws UnsupportedOperationException {
if(s != null) {
HashSet<T> matched = parseSet(clazz, null, s);
if (matched.size() == 1) {
T returnVal = matched.iterator().next();
boolean blocked = false;
for(T block : blocklist) {
if(returnVal == block) {
blocked = true;
break;
}
}
if(!blocked) {
return returnVal;
}
}
}
throw new UnsupportedOperationException(String.format("%s value '%s' failed to match one and only one item", clazz.getSimpleName(), s));
}
static <T extends Enum<T>> HashSet<T> parseSet(Class<T> clazz, T all, String s) {
HashSet<T> matched = new HashSet<>();
if(s != null) {
// Handle ALL="*"
if (all != null && s.equals("*")) {
matched.add(all);
}
String[] parts = s.split("\\|");
for(String part : parts) {
T parsed = parse(clazz, part);
if (parsed != null) {
matched.add(parsed);
}
}
}
return matched;
}
}

View File

@@ -0,0 +1,67 @@
package qz.build.provision.params;
import org.apache.commons.lang3.StringUtils;
import qz.utils.SystemUtilities;
import java.util.*;
/**
* Basic OS parser
*/
public enum Os {
WINDOWS,
MAC,
LINUX,
SOLARIS, // unsupported
ALL, // special handling
UNKNOWN;
public boolean matches(HashSet<Os> osList) {
return this == ALL || osList.contains(ALL) || (this != UNKNOWN && osList.contains(this));
}
public static boolean matchesHost(HashSet<Os> osList) {
for(Os os : osList) {
if(os == SystemUtilities.getOs() || os == ALL) {
return true;
}
}
return false;
}
public static Os parseStrict(String input) throws UnsupportedOperationException {
return EnumParser.parseStrict(Os.class, input, ALL, UNKNOWN);
}
public static Os bestMatch(String input) {
if(input != null) {
String name = input.toLowerCase(Locale.ENGLISH);
if (name.contains("win")) {
return Os.WINDOWS;
} else if (name.contains("mac")) {
return Os.MAC;
} else if (name.contains("linux")) {
return Os.LINUX;
} else if (name.contains("sunos")) {
return Os.SOLARIS;
}
}
return Os.UNKNOWN;
}
public static HashSet<Os> parse(String input) {
return EnumParser.parseSet(Os.class, Os.ALL, input);
}
public static String serialize(HashSet<Os> osList) {
if(osList.contains(ALL)) {
return "*";
}
return StringUtils.join(osList, "|");
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,19 @@
package qz.build.provision.params;
import java.util.Locale;
public enum Phase {
INSTALL,
CERTGEN,
STARTUP,
UNINSTALL;
public static Phase parse(String input) {
return EnumParser.parse(Phase.class, input);
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,24 @@
package qz.build.provision.params;
import java.util.Locale;
public enum Type {
SCRIPT,
SOFTWARE,
RESOURCE,
REMOVER, // QZ Tray remover
CA,
CERT,
CONF,
PROPERTY,
PREFERENCE;
public static Type parse(String input) {
return EnumParser.parse(Type.class, input);
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,67 @@
package qz.build.provision.params.types;
import org.apache.commons.lang3.StringUtils;
import qz.build.provision.params.EnumParser;
import qz.common.Constants;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
public enum Remover {
QZ("QZ Tray", "qz-tray", "qz"),
CUSTOM(null, null, null); // reserved
private String aboutTitle;
private String propsFile;
private String dataDir;
Remover(String aboutTitle, String propsFile, String dataDir) {
this.aboutTitle = aboutTitle;
this.propsFile = propsFile;
this.dataDir = dataDir;
}
public String getAboutTitle() {
return aboutTitle;
}
public String getPropsFile() {
return propsFile;
}
public String getDataDir() {
return dataDir;
}
public static String valuesDelimited(String delimiter) {
ArrayList<Remover> listing = new ArrayList<>(Arrays.asList(values()));
listing.remove(CUSTOM);
return StringUtils.join(listing, delimiter).toLowerCase(Locale.ENGLISH);
}
/**
* Defaults to custom if not found
*/
public static Remover parse(String input) {
Remover remover = EnumParser.parse(Remover.class, input);
if(remover == CUSTOM) {
throw new UnsupportedOperationException("Remover 'custom' is reserved for internal purposes");
}
if(remover == null) {
remover = CUSTOM;
}
return remover;
}
public boolean matchesCurrentSystem() {
return Constants.ABOUT_TITLE.equals(aboutTitle) ||
Constants.PROPS_FILE.equals(propsFile) ||
Constants.DATA_DIR.equals(dataDir);
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,31 @@
package qz.build.provision.params.types;
import org.apache.commons.io.FilenameUtils;
import qz.build.provision.params.EnumParser;
import java.nio.file.Path;
import java.util.Locale;
public enum Script {
PS1,
BAT,
SH,
PY,
RB;
public static Script parse(String input) {
if(input != null && !input.isEmpty()) {
return EnumParser.parse(Script.class, FilenameUtils.getExtension(input), SH);
}
return null;
}
public static Script parse(Path path) {
return parse(path.toAbsolutePath().toString());
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,63 @@
package qz.build.provision.params.types;
import org.apache.commons.io.FilenameUtils;
import qz.build.provision.params.EnumParser;
import qz.build.provision.params.Os;
import java.nio.file.Path;
import java.util.*;
public enum Software {
EXE,
MSI,
PKG,
DMG,
RUN,
UNKNOWN;
public static Software parse(String input) {
return EnumParser.parse(Software.class, FilenameUtils.getExtension(input), UNKNOWN);
}
public static Software parse(Path path) {
return parse(path.toString());
}
public static List<String> parseArgs(String input) {
List<String> args = new LinkedList<>();
if(input != null) {
String[] parts = input.split(" ");
for(String part : parts) {
if(!part.trim().isEmpty()) {
args.add(part.trim());
}
}
}
return args;
}
public HashSet<Os> defaultOs() {
HashSet<Os> list = new HashSet<>();
switch(this) {
case EXE:
case MSI:
list.add(Os.WINDOWS);
break;
case PKG:
case DMG:
list.add(Os.MAC);
break;
case RUN:
list.add(Os.LINUX);
break;
default:
list.add(Os.ALL);
}
return list;
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,205 @@
package qz.common;
import com.github.zafarkhaja.semver.Version;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.apache.commons.ssl.Base64;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.x509.extension.X509ExtensionUtil;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.installer.certificate.KeyPairWrapper;
import qz.installer.certificate.CertificateManager;
import qz.utils.MacUtilities;
import qz.utils.StringUtilities;
import qz.utils.SystemUtilities;
import qz.ws.PrintSocketServer;
import qz.ws.WebsocketPorts;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.*;
public class AboutInfo {
private static final Logger log = LogManager.getLogger(AboutInfo.class);
private static String preferredHostname = "localhost";
public static JSONObject gatherAbout(String domain, CertificateManager certificateManager) {
JSONObject about = new JSONObject();
try {
about.put("product", product());
about.put("socket", socket(certificateManager, domain));
about.put("environment", environment());
about.put("ssl", ssl(certificateManager));
about.put("libraries", libraries());
about.put("charsets", charsets());
}
catch(JSONException | GeneralSecurityException e) {
log.error("Failed to write JSON data", e);
}
return about;
}
private static JSONObject product() throws JSONException {
JSONObject product = new JSONObject();
product
.put("title", Constants.ABOUT_TITLE)
.put("version", Constants.VERSION)
.put("vendor", Constants.ABOUT_COMPANY)
.put("url", Constants.ABOUT_URL);
return product;
}
private static JSONObject socket(CertificateManager certificateManager, String domain) throws JSONException {
JSONObject socket = new JSONObject();
String sanitizeDomain = StringUtilities.escapeHtmlEntities(domain);
WebsocketPorts websocketPorts = PrintSocketServer.getWebsocketPorts();
// Gracefully handle XSS per https://github.com/qzind/tray/issues/1099
if(sanitizeDomain.contains("&lt;") || sanitizeDomain.contains("&gt;")) {
log.warn("Something smells fishy about this domain: \"{}\", skipping", domain);
sanitizeDomain = "unknown";
}
socket
.put("domain", sanitizeDomain)
.put("secureProtocol", "wss")
.put("securePort", certificateManager.isSslActive() ? websocketPorts.getSecurePort() : "none")
.put("insecureProtocol", "ws")
.put("insecurePort", websocketPorts.getInsecurePort());
return socket;
}
private static JSONObject environment() throws JSONException {
JSONObject environment = new JSONObject();
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
environment
.put("os", SystemUtilities.getOsDisplayName())
.put("os version", SystemUtilities.getOsDisplayVersion())
.put("java", String.format("%s (%s)", Constants.JAVA_VERSION, SystemUtilities.getArch().toString().toLowerCase()))
.put("java (location)", System.getProperty("java.home"))
.put("java (vendor)", Constants.JAVA_VENDOR)
.put("uptime", DurationFormatUtils.formatDurationWords(uptime, true, false))
.put("uptimeMillis", uptime)
.put("sandbox", SystemUtilities.isMac() && MacUtilities.isSandboxed());
return environment;
}
private static JSONObject ssl(CertificateManager certificateManager) throws JSONException, CertificateEncodingException {
JSONObject ssl = new JSONObject();
JSONArray certs = new JSONArray();
for (KeyPairWrapper keyPair : new KeyPairWrapper[]{certificateManager.getCaKeyPair(), certificateManager.getSslKeyPair() }) {
X509Certificate x509 = keyPair.getCert();
if (x509 != null) {
JSONObject cert = new JSONObject();
cert.put("alias", keyPair.getAlias());
try {
ASN1Primitive ext = X509ExtensionUtil.fromExtensionValue(x509.getExtensionValue(Extension.basicConstraints.getId()));
cert.put("rootca", BasicConstraints.getInstance(ext).isCA());
}
catch(IOException | NullPointerException e) {
cert.put("rootca", false);
}
cert.put("subject", x509.getSubjectX500Principal().getName());
cert.put("expires", SystemUtilities.toISO(x509.getNotAfter()));
cert.put("data", formatCert(x509.getEncoded()));
certs.put(cert);
}
}
ssl.put("certificates", certs);
return ssl;
}
public static String formatCert(byte[] encoding) {
return "-----BEGIN CERTIFICATE-----\r\n" +
new String(Base64.encodeBase64(encoding, true), StandardCharsets.UTF_8) +
"-----END CERTIFICATE-----\r\n";
}
private static JSONObject libraries() throws JSONException {
JSONObject libraries = new JSONObject();
SortedMap<String,String> libs = SecurityInfo.getLibVersions();
for(Map.Entry<String,String> entry : libs.entrySet()) {
String version = entry.getValue();
if (version == null) { version = "unknown"; }
libraries.put(entry.getKey(), version);
}
return libraries;
}
private static JSONObject charsets() throws JSONException {
JSONObject charsets = new JSONObject();
SortedMap<String,Charset> avail = Charset.availableCharsets();
ArrayList<String> names = new ArrayList<>();
for(Map.Entry<String,Charset> entry : avail.entrySet()) {
names.add(entry.getValue().name());
}
charsets.put("charsets", Arrays.toString(names.toArray()));
return charsets;
}
public static String getPreferredHostname() {
return preferredHostname;
}
public static Version findLatestVersion() {
log.trace("Looking for newer versions of {} online", Constants.ABOUT_TITLE);
try {
URL api = new URL(Constants.VERSION_CHECK_URL);
BufferedReader br = new BufferedReader(new InputStreamReader(api.openStream()));
StringBuilder rawJson = new StringBuilder();
String line;
while((line = br.readLine()) != null) {
rawJson.append(line);
}
JSONArray versions = new JSONArray(rawJson.toString());
for(int i = 0; i < versions.length(); i++) {
JSONObject versionData = versions.getJSONObject(i);
if(versionData.getString("target_commitish").equals("master")) {
Version latestVersion = Version.valueOf(versionData.getString("name"));
log.trace("Found latest version of {} online: {}", Constants.ABOUT_TITLE, latestVersion);
return latestVersion;
}
}
throw new Exception("Could not find valid json version information online.");
}
catch(Exception e) {
log.error("Failed to get latest version of {} online", Constants.ABOUT_TITLE, e);
}
return Constants.VERSION;
}
}

View File

@@ -0,0 +1,156 @@
/**
* @author Antoni Ten Monro's
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
* Copyright (C) 2013 Antoni Ten Monro's
*
* 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.common;
import org.apache.commons.lang3.ArrayUtils;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* Provides a simple and efficient way for concatenating byte arrays, similar
* in purpose to <code>StringBuilder</code>. Objects of this class are not
* thread safe and include no synchronization
*
* @author Antoni Ten Monro's
*/
@SuppressWarnings("UnusedDeclaration") //Library class
public final class ByteArrayBuilder {
private List<Byte> buffer;
/**
* Creates a new <code>ByteArrayBuilder</code> and sets initial capacity to 10
*/
public ByteArrayBuilder() {
this(null);
}
/**
* Creates a new <code>ByteArrayBuilder</code> and sets initial capacity to
* <code>initialCapacity</code>
*
* @param initialCapacity the initial capacity of the <code>ByteArrayBuilder</code>
*/
public ByteArrayBuilder(int initialCapacity) {
this(null, initialCapacity);
}
/**
* Creates a new <code>ByteArrayBuilder</code>, sets initial capacity to 10
* and appends <code>initialContents</code>
*
* @param initialContents the initial contents of the ByteArrayBuilder
*/
public ByteArrayBuilder(byte[] initialContents) {
this(initialContents, 16);
}
/**
* Creates a new <code>ByteArrayBuilder</code>, sets initial capacity to
* <code>initialContents</code> and appends <code>initialContents</code>
*
* @param initialContents the initial contents of the <code>ByteArrayBuilder</code>
* @param initialCapacity the initial capacity of the <code>ByteArrayBuilder</code>
*/
public ByteArrayBuilder(byte[] initialContents, int initialCapacity) {
buffer = new ArrayList<>(initialCapacity);
if (initialContents != null) {
append(initialContents);
}
}
/**
* Empties the <code>ByteArrayBuilder</code>
*/
public void clear() {
buffer.clear();
}
/**
* Clear a portion of the <code>ByteArrayBuilder</code>
*
* @param startIndex Starting index, inclusive
* @param endIndex Ending index, exclusive
*/
public final void clearRange(int startIndex, int endIndex) {
buffer.subList(startIndex, endIndex).clear();
}
/**
* Gives the number of bytes currently stored in this <code>ByteArrayBuilder</code>
*
* @return the number of bytes in the <code>ByteArrayBuilder</code>
*/
public int getLength() {
return buffer.size();
}
/**
* Appends a new byte array to this <code>ByteArrayBuilder</code>.
* Returns this same object to allow chaining calls
*
* @param bytes the byte array to append
* @return this <code>ByteArrayBuilder</code>
*/
public final ByteArrayBuilder append(byte[] bytes) {
for(byte b : bytes) {
buffer.add(b);
}
return this;
}
public final ByteArrayBuilder append(List<Byte> bytes) {
for(byte b : bytes) {
buffer.add(b);
}
return this;
}
/**
* Convenience method for append(byte[]) combined with a StringBuffer of specified
* charset
*
* @param string the String to append
* @param charset the Charset of the String
* @return this <code>ByteArrayBuilder</code>
*/
public final ByteArrayBuilder append(String string, Charset charset) throws UnsupportedEncodingException {
return append(string.getBytes(charset.name()));
}
/**
* Convenience method for append(byte[]) combined with a String of specified
* charset
*
* @param stringBuilder the StringBuilder to append
* @param charset the Charset of the StringBuilder
* @return this <code>ByteArrayBuilder</code>
*/
public final ByteArrayBuilder append(StringBuilder stringBuilder, Charset charset) throws UnsupportedEncodingException {
return append(stringBuilder.toString(), charset);
}
/**
* Returns the full contents of this <code>ByteArrayBuilder</code> as
* a single <code>byte</code> array.
*
* @return The contents of this <code>ByteArrayBuilder</code> as a single <code>byte</code> array
*/
public byte[] getByteArray() {
return ArrayUtils.toPrimitive(buffer.toArray(new Byte[buffer.size()]));
}
}

View File

@@ -0,0 +1,97 @@
package qz.common;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
/**
* A generic class that encapsulates an object for caching. The cached object
* will be refreshed automatically when accessed after its lifespan has expired.
*
* @param <T> The type of object to be cached.
*/
public class CachedObject<T> {
public static final long DEFAULT_LIFESPAN = 5000; // in milliseconds
T lastObject;
Supplier<T> supplier;
private long timestamp;
private long lifespan;
/**
* Creates a new CachedObject with a default lifespan of 5000 milliseconds
*
* @param supplier The function to pull new values from
*/
public CachedObject(Supplier<T> supplier) {
this(supplier, DEFAULT_LIFESPAN);
}
/**
* Creates a new CachedObject
*
* @param supplier The function to pull new values from
* @param lifespan The lifespan of the cached object in milliseconds
*/
public CachedObject(Supplier<T> supplier, long lifespan) {
this.supplier = supplier;
setLifespan(lifespan);
timestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, MIN_VALUE guarantees a first-run.
}
/**
* Registers a new supplier for the CachedObject
*
* @param supplier The function to pull new values from
*/
@SuppressWarnings("unused")
public void registerSupplier(Supplier<T> supplier) {
this.supplier = supplier;
}
/**
* Sets the lifespan of the cached object
*
* @param milliseconds The lifespan of the cached object in milliseconds
*/
public void setLifespan(long milliseconds) {
lifespan = Math.max(0, milliseconds); // prevent overflow
}
/**
* Retrieves the cached object.
* If the cached object's lifespan has expired, it gets refreshed before being returned.
*
* @return The cached object
*/
public T get() {
return get(false);
}
/**
* Retrieves the cached object.
* If the cached object's lifespan is expired or forceRefresh is true, it gets refreshed before being returned.
*
* @param forceRefresh If true, the cached object will be refreshed before being returned regardless of its lifespan
* @return The cached object
*/
public T get(boolean forceRefresh) {
long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
// check lifespan
if (forceRefresh || (timestamp + lifespan <= now)) {
timestamp = now;
lastObject = supplier.get();
}
return lastObject;
}
// Test
public static void main(String ... args) throws InterruptedException {
final AtomicInteger testInt = new AtomicInteger(0);
CachedObject<Integer> cachedString = new CachedObject<>(testInt::incrementAndGet);
for(int i = 0; i < 100; i++) {
Thread.sleep(1500);
System.out.println(cachedString.get());
}
}
}

View File

@@ -0,0 +1,103 @@
package qz.common;
import com.github.zafarkhaja.semver.Version;
import qz.utils.SystemUtilities;
import java.awt.*;
import static qz.ws.SingleInstanceChecker.STEAL_WEBSOCKET_PROPERTY;
/**
* Created by robert on 7/9/2014.
*/
public class Constants {
public static final String HEXES = "0123456789ABCDEF";
public static final char[] HEXES_ARRAY = HEXES.toCharArray();
public static final int BYTE_BUFFER_SIZE = 8192;
public static final Version VERSION = Version.valueOf("2.2.6-SNAPSHOT");
public static final Version JAVA_VERSION = SystemUtilities.getJavaVersion();
public static final String JAVA_VENDOR = System.getProperty("java.vendor");
/* QZ-Tray Constants */
public static final String BLOCK_FILE = "blocked";
public static final String ALLOW_FILE = "allowed";
public static final String TEMP_FILE = "temp";
public static final String LOG_FILE = "debug";
public static final String PROPS_FILE = "qz-tray"; // .properties extension is assumed
public static final String PREFS_FILE = "prefs"; // .properties extension is assumed
public static final String[] PERSIST_PROPS = {"file.whitelist", "file.allow", "networking.hostname", "networking.port", STEAL_WEBSOCKET_PROPERTY };
public static final String AUTOSTART_FILE = ".autostart";
public static final String DATA_DIR = "qz";
public static final int BORDER_PADDING = 10;
public static final String ABOUT_TITLE = "QZ Tray";
public static final String ABOUT_EMAIL = "support@qz.io";
public static final String ABOUT_URL = "https://qz.io";
public static final String ABOUT_COMPANY = "QZ Industries, LLC";
public static final String ABOUT_CITY = "Canastota";
public static final String ABOUT_STATE = "NY";
public static final String ABOUT_COUNTRY = "US";
public static final String ABOUT_LICENSING_URL = Constants.ABOUT_URL + "/licensing";
public static final String ABOUT_SUPPORT_URL = Constants.ABOUT_URL + "/support";
public static final String ABOUT_PRIVACY_URL = Constants.ABOUT_URL + "/privacy";
public static final String ABOUT_DOWNLOAD_URL = Constants.ABOUT_URL + "/download";
public static final String VERSION_CHECK_URL = "https://api.github.com/repos/qzind/tray/releases";
public static final String VERSION_DOWNLOAD_URL = "https://github.com/qzind/tray/releases";
public static final boolean ENABLE_DIAGNOSTICS = true; // Diagnostics menu (logs, etc)
public static final String TRUSTED_CERT = String.format("Verified by %s", Constants.ABOUT_COMPANY);
public static final String SPONSORED_CERT = String.format("Sponsored by %s", Constants.ABOUT_COMPANY);
public static final String SPONSORED_TOOLTIP = "Sponsored organization";
public static final String UNTRUSTED_CERT = "Untrusted website";
public static final String NO_TRUST = "Cannot verify trust";
public static final String PROBE_REQUEST = "getProgramName";
public static final String PROBE_RESPONSE = ABOUT_TITLE;
public static final String ALLOW_SITES_TEXT = "Permanently allowed \"%s\" to access local resources";
public static final String BLOCK_SITES_TEXT = "Permanently blocked \"%s\" from accessing local resources";
public static final String REMEMBER_THIS_DECISION = "Remember this decision";
public static final String STRICT_MODE_LABEL = "Use strict certificate mode";
public static final String STRICT_MODE_TOOLTIP = String.format("Prevents the ability to select \"%s\" for most websites", REMEMBER_THIS_DECISION);
public static final String STRICT_MODE_CONFIRM = String.format("Set strict certificate mode? Most websites will stop working with %s.", ABOUT_TITLE);
public static final String ALLOW_SITES_LABEL = "Sites permanently allowed access";
public static final String BLOCK_SITES_LABEL = "Sites permanently blocked from access";
public static final String ALLOWED = "Allowed";
public static final String BLOCKED = "Blocked";
public static final String OVERRIDE_CERT = "override.crt";
public static final String WHITELIST_CERT_DIR = "whitelist";
public static final String PROVISION_DIR = "provision";
public static final String PROVISION_FILE = "provision.json";
public static final String SIGNING_PRIVATE_KEY = "private-key.pem";
public static final String SIGNING_CERTIFICATE = "digital-certificate.txt";
public static final long VALID_SIGNING_PERIOD = 15 * 60 * 1000; //millis
public static final int EXPIRY_WARN = 30; // days
public static final Color WARNING_COLOR_LITE = Color.RED;
public static final Color TRUSTED_COLOR_LITE = Color.BLUE;
public static final Color WARNING_COLOR_DARK = Color.decode("#EB6261");
public static final Color TRUSTED_COLOR_DARK = Color.decode("#589DF6");
public static Color WARNING_COLOR = WARNING_COLOR_LITE;
public static Color TRUSTED_COLOR = TRUSTED_COLOR_LITE;
public static boolean MASK_TRAY_SUPPORTED = true;
public static final long MEMORY_PER_PRINT = 512; //MB
public static final String RAW_PRINT = ABOUT_TITLE + " Raw Print";
public static final String IMAGE_PRINT = ABOUT_TITLE + " Pixel Print";
public static final String PDF_PRINT = ABOUT_TITLE + " PDF Print";
public static final String HTML_PRINT = ABOUT_TITLE + " HTML Print";
public static final Integer[] DEFAULT_WSS_PORTS = {8181, 8282, 8383, 8484};
public static final Integer[] DEFAULT_WS_PORTS = {8182, 8283, 8384, 8485};
public static final Integer[] CUPS_RSS_PORTS = {8586, 8687, 8788, 8889};
}

View File

@@ -0,0 +1,100 @@
package qz.common;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ArgValue;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
/**
* Created by Tres on 12/16/2015.
*/
public class PropertyHelper extends Properties {
private static final Logger log = LogManager.getLogger(PropertyHelper.class);
private String file;
/**
* Default constructor
*/
public PropertyHelper() {
super();
}
/**
* Default constructor
* @param p Initial Properties
*/
public PropertyHelper(Properties p) {
super(p);
}
/**
* Custom constructor, attempts to load from file
* @param file File to load properties from
*/
public PropertyHelper(String file) {
super();
this.file = file;
load(file);
}
public PropertyHelper(File file) {
this(file == null ? null : file.getAbsolutePath());
}
public boolean getBoolean(String key, boolean defaultVal) {
String prop = getProperty(key);
if (prop != null) {
return Boolean.parseBoolean(prop);
} else {
return defaultVal;
}
}
public void setProperty(ArgValue arg, boolean value) {
setProperty(arg.getMatch(), "" + value);
}
public void load(File file) {
load(file == null ? null : file.getAbsolutePath());
}
public void load(String file) {
FileInputStream f = null;
try {
f = new FileInputStream(file);
load(f);
} catch (IOException e) {
log.warn("Could not load file: {}, reason: {}", file, e.getLocalizedMessage());
} finally {
if (f != null) {
try { f.close(); } catch(Throwable ignore) {};
}
}
}
public boolean save() {
boolean success = false;
FileOutputStream f = null;
try {
f = new FileOutputStream(file);
this.store(f, null);
success = true;
} catch (IOException e) {
log.error("Error saving file: {}", file, e);
} finally {
if (f != null) {
try { f.close(); } catch(Throwable ignore) {};
}
}
return success;
}
public synchronized Object setProperty(Map.Entry<String, String> pair) {
return super.setProperty(pair.getKey(), pair.getValue());
}
}

View File

@@ -0,0 +1,169 @@
package qz.common;
import com.sun.jna.Native;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.util.Jetty;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.usb4java.LibUsb;
import purejavahidapi.PureJavaHidApi;
import qz.utils.SystemUtilities;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.*;
/**
* Created by Kyle B. on 10/27/2017.
*/
public class SecurityInfo {
/**
* Wrap throwable operations into a try/catch
*/
private static class CheckedTreeMap<K, V> extends TreeMap<K, V> {
private static final Logger log = LogManager.getLogger(CheckedTreeMap.class);
interface CheckedValue {
Object check() throws Throwable;
}
@SuppressWarnings("unchecked")
public V put(K key, CheckedValue value) {
try {
return put(key, (V)value.check());
} catch(Throwable t) {
log.warn("A checked exception was suppressed adding key \"{}\"", key, t);
return put(key, (V)"missing");
}
}
}
private static final Logger log = LogManager.getLogger(SecurityInfo.class);
public static KeyStore getKeyStore(Properties props) {
if (props != null) {
String store = props.getProperty("wss.keystore", "");
char[] pass = props.getProperty("wss.storepass", "").toCharArray();
try {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(new FileInputStream(store), pass);
return keystore;
}
catch(GeneralSecurityException | IOException e) {
log.warn("Unable to create keystore from properties file: {}", e.getMessage());
}
}
return null;
}
public static SortedMap<String,String> getLibVersions() {
CheckedTreeMap<String,String> libVersions = new CheckedTreeMap<>();
// Use API-provided mechanism if available
libVersions.put("jna (native)", () -> Native.VERSION_NATIVE);
libVersions.put("jna (location)", () -> {
@SuppressWarnings("unused")
int ignore = Native.BOOL_SIZE;
return System.getProperty("jnidispatch.path");
});
libVersions.put("jna", Native.VERSION);
libVersions.put("jssc", () -> jssc.SerialNativeInterface.getLibraryVersion());
libVersions.put("jssc (native)", () -> jssc.SerialNativeInterface.getNativeLibraryVersion());
libVersions.put("jetty", Jetty.VERSION);
libVersions.put("pdfbox", org.apache.pdfbox.util.Version.getVersion());
libVersions.put("purejavahidapi", () -> PureJavaHidApi.getVersion());
libVersions.put("usb-api", javax.usb.Version.getApiVersion());
libVersions.put("not-yet-commons-ssl", org.apache.commons.ssl.Version.VERSION);
libVersions.put("mslinks", mslinks.ShellLink.VERSION);
libVersions.put("bouncycastle", "" + new BouncyCastleProvider().getVersion());
libVersions.put("usb4java (native)", () -> LibUsb.getVersion().toString());
libVersions.put("jre", Constants.JAVA_VERSION.toString());
libVersions.put("jre (vendor)", Constants.JAVA_VENDOR);
//JFX info, if it exists
try {
// "DO NOT LINK JAVAFX EVER" - JavaFX may not exist, use reflection to avoid compilation errors
Class<?> VersionInfo = Class.forName("com.sun.javafx.runtime.VersionInfo");
Path fxPath = Paths.get(VersionInfo.getProtectionDomain().getCodeSource().getLocation().toURI());
Method method = VersionInfo.getMethod("getVersion");
Object version = method.invoke(null);
libVersions.put("javafx", (String)version);
libVersions.put("javafx (location)", fxPath.toString());
} catch(Throwable e) {
libVersions.put("javafx", "missing");
libVersions.put("javafx (location)", "missing");
}
// Fallback to maven manifest information
HashMap<String,String> mavenVersions = getMavenVersions();
String[] mavenLibs = {"jetty-servlet", "jetty-io", "websocket-common",
"usb4java-javax", "java-semver", "commons-pool2",
"websocket-server", "jettison", "commons-codec", "log4j-api", "log4j-core",
"websocket-servlet", "jetty-http", "commons-lang3", "javax-websocket-server-impl",
"javax.servlet-api", "hid4java", "usb4java", "websocket-api", "jetty-util", "websocket-client",
"javax.websocket-api", "commons-io", "jetty-security"};
for(String lib : mavenLibs) {
libVersions.put(lib, mavenVersions.get(lib));
}
return libVersions;
}
public static void printLibInfo() {
String format = "%-40s%s%n";
System.out.printf(format, "LIBRARY NAME:", "VERSION:");
SortedMap<String,String> libVersions = SecurityInfo.getLibVersions();
for(Map.Entry<String,String> entry : libVersions.entrySet()) {
if (entry.getValue() == null) {
System.out.printf(format, entry.getKey(), "(unknown)");
} else {
System.out.printf(format, entry.getKey(), entry.getValue());
}
}
}
/**
* Fetches embedded version information based on maven properties
*
* @return HashMap of library name, version
*/
private static HashMap<String,String> getMavenVersions() {
final HashMap<String,String> mavenVersions = new HashMap<>();
String jar = "jar:" + SecurityInfo.class.getProtectionDomain().getCodeSource().getLocation().toString();
try(FileSystem fs = FileSystems.newFileSystem(new URI(jar), new HashMap<String,String>())) {
Files.walkFileTree(fs.getPath("/META-INF/maven"), new HashSet<>(), 3, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".properties")) {
try {
Properties props = new Properties();
props.load(Files.newInputStream(file, StandardOpenOption.READ));
mavenVersions.put(props.getProperty("artifactId"), props.getProperty("version"));
}
catch(Exception e) {
log.warn("Error reading properties from {}", file, e);
}
}
return FileVisitResult.CONTINUE;
}
});
}
catch(Exception ignore) {
log.warn("Could not open {} for version information. Most libraries will list as (unknown)", jar);
}
return mavenVersions;
}
}

View File

@@ -0,0 +1,693 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.common;
import com.github.zafarkhaja.semver.Version;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.server.Server;
import qz.App;
import qz.auth.Certificate;
import qz.auth.RequestState;
import qz.installer.shortcut.ShortcutCreator;
import qz.printer.PrintServiceMatcher;
import qz.printer.action.html.WebApp;
import qz.ui.*;
import qz.ui.component.IconCache;
import qz.ui.tray.TrayType;
import qz.utils.*;
import qz.ws.PrintSocketServer;
import qz.ws.SingleInstanceChecker;
import qz.ws.WebsocketPorts;
import qz.ws.substitutions.Substitutions;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import static qz.ui.component.IconCache.Icon.*;
import static qz.utils.ArgValue.*;
/**
* Manages the icons and actions associated with the TrayIcon
*
* @author Tres Finocchiaro
*/
public class TrayManager {
private static final Logger log = LogManager.getLogger(TrayManager.class);
private boolean headless;
// The cached icons
private final IconCache iconCache;
// Custom swing pop-up menu
private TrayType tray;
private ConfirmDialog confirmDialog;
private GatewayDialog gatewayDialog;
private AboutDialog aboutDialog;
private LogDialog logDialog;
private SiteManagerDialog sitesDialog;
private ArrayList<Component> componentList;
private IconCache.Icon shownIcon;
// Need a class reference to this so we can set it from the request dialog window
private JCheckBoxMenuItem anonymousItem;
// The name this UI component will use, i.e "QZ Print 1.9.0"
private final String name;
// The shortcut and startup helper
private final ShortcutCreator shortcutCreator;
private final PropertyHelper prefs;
// Action to run when reload is triggered
private Thread reloadThread;
// Actions to run if idle after startup
private java.util.Timer idleTimer = new java.util.Timer();
public TrayManager() {
this(false);
}
/**
* Create a AutoHideJSystemTray with the specified name/text
*/
public TrayManager(boolean isHeadless) {
name = Constants.ABOUT_TITLE + " " + Constants.VERSION;
prefs = new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
prefs.remove(SECURITY_FILE_STRICT.getMatch()); // per https://github.com/qzind/tray/issues/1337
// Set strict certificate mode preference
Certificate.setTrustBuiltIn(!getPref(TRAY_STRICTMODE));
// Configures JSON websocket messages
Substitutions.getInstance();
// Set FileIO security
FileUtilities.setFileIoEnabled(getPref(SECURITY_FILE_ENABLED));
FileUtilities.setFileIoStrict(getPref(SECURITY_FILE_STRICT));
// Headless if turned on by user or unsupported by environment
headless = isHeadless || getPref(HEADLESS) || GraphicsEnvironment.isHeadless();
if (headless) {
log.info("Running in headless mode");
}
// Set up the shortcut name so that the UI components can use it
shortcutCreator = ShortcutCreator.getInstance();
SystemUtilities.setSystemLookAndFeel(headless);
iconCache = new IconCache();
if (SystemUtilities.isSystemTraySupported(headless)) { // UI mode with tray
switch(SystemUtilities.getOs()) {
case WINDOWS:
tray = TrayType.JX.init(iconCache);
// Undocumented HiDPI behavior
tray.setImageAutoSize(true);
break;
case MAC:
tray = TrayType.CLASSIC.init(iconCache);
break;
default:
tray = TrayType.MODERN.init(iconCache);
}
// OS-specific tray icon handling
if (SystemTray.isSupported()) {
iconCache.fixTrayIcons(SystemUtilities.isDarkTaskbar());
}
// Iterates over all images denoted by IconCache.getTypes() and caches them
tray.setIcon(DANGER_ICON);
tray.setToolTip(name);
try {
SystemTray.getSystemTray().add(tray.tray());
}
catch(AWTException awt) {
log.error("Could not attach tray, forcing headless mode", awt);
headless = true;
}
} else if (!headless) { // UI mode without tray
tray = TrayType.TASKBAR.init(exitListener, iconCache);
tray.setIcon(DANGER_ICON);
tray.setToolTip(name);
tray.showTaskbar();
}
// TODO: Remove when fixed upstream. See issue #393
if (SystemUtilities.isUnix() && !isHeadless) {
// Update printer list in CUPS immediately (normally 2min)
System.setProperty("sun.java2d.print.polling", "false");
}
if (!headless) {
componentList = new ArrayList<>();
// The allow/block dialog
gatewayDialog = new GatewayDialog(null, "Action Required", iconCache);
componentList.add(gatewayDialog);
// The ok/cancel dialog
confirmDialog = new ConfirmDialog(null, "Please Confirm", iconCache);
componentList.add(confirmDialog);
// Detect theme changes
new Thread(() -> {
boolean darkDesktopMode = SystemUtilities.isDarkDesktop();
boolean darkTaskbarMode = SystemUtilities.isDarkTaskbar();
while(true) {
try {
Thread.sleep(1000);
if (darkDesktopMode != SystemUtilities.isDarkDesktop(true) ||
darkTaskbarMode != SystemUtilities.isDarkTaskbar(true)) {
darkDesktopMode = SystemUtilities.isDarkDesktop();
darkTaskbarMode = SystemUtilities.isDarkTaskbar();
iconCache.fixTrayIcons(darkTaskbarMode);
refreshIcon(null);
SwingUtilities.invokeLater(() -> {
SystemUtilities.setSystemLookAndFeel(headless);
for(Component c : componentList) {
SwingUtilities.updateComponentTreeUI(c);
if (c instanceof Themeable) {
((Themeable)c).refresh();
}
if (c instanceof JDialog) {
((JDialog)c).pack();
} else if (c instanceof JPopupMenu) {
((JPopupMenu)c).pack();
}
}
});
}
}
catch(InterruptedException ignore) {}
}
}).start();
}
if (tray != null) {
addMenuItems();
}
// Initialize idle actions
// Slow to start JavaFX the first time
if (getPref(TRAY_IDLE_JAVAFX)) {
performIfIdle((int)TimeUnit.SECONDS.toMillis(60), evt -> {
log.debug("IDLE: Starting up JFX for HTML printing");
try {
WebApp.initialize();
}
catch(IOException e) {
log.error("Idle runner failed to preemptively start JavaFX service");
}
});
}
// Slow to find printers the first time if a lot of printers are installed
// Must run after JavaFX per https://github.com/qzind/tray/issues/924
if (getPref(TRAY_IDLE_PRINTERS)) {
performIfIdle((int)TimeUnit.SECONDS.toMillis(120), evt -> {
log.debug("IDLE: Performing first run of find printers");
PrintServiceMatcher.getNativePrinterList(false, true);
});
}
}
/**
* Stand-alone invocation of TrayManager
*
* @param args arguments to pass to main
*/
public static void main(String args[]) {
SwingUtilities.invokeLater(TrayManager::new);
}
/**
* Builds the swing pop-up menu with the specified items
*/
private void addMenuItems() {
JPopupMenu popup = new JPopupMenu();
componentList.add(popup);
JMenu advancedMenu = new JMenu("Advanced");
advancedMenu.setMnemonic(KeyEvent.VK_A);
advancedMenu.setIcon(iconCache.getIcon(SETTINGS_ICON));
JMenuItem sitesItem = new JMenuItem("Site Manager...", iconCache.getIcon(SAVED_ICON));
sitesItem.setMnemonic(KeyEvent.VK_M);
sitesItem.addActionListener(savedListener);
sitesDialog = new SiteManagerDialog(sitesItem, iconCache, prefs);
componentList.add(sitesDialog);
JMenuItem pairingConfigItem = new JMenuItem("Pairing Configuration...", iconCache.getIcon(SETTINGS_ICON));
pairingConfigItem.setMnemonic(KeyEvent.VK_P);
pairingConfigItem.addActionListener(e -> new qz.ui.PairingConfigDialog(null).setVisible(true));
advancedMenu.add(pairingConfigItem);
JMenuItem diagnosticMenu = new JMenu("Diagnostic");
JMenuItem browseApp = new JMenuItem("Browse App folder...", iconCache.getIcon(FOLDER_ICON));
browseApp.setToolTipText(SystemUtilities.getJarParentPath().toString());
browseApp.setMnemonic(KeyEvent.VK_O);
browseApp.addActionListener(e -> ShellUtilities.browseAppDirectory());
diagnosticMenu.add(browseApp);
JMenuItem browseUser = new JMenuItem("Browse User folder...", iconCache.getIcon(FOLDER_ICON));
browseUser.setToolTipText(FileUtilities.USER_DIR.toString());
browseUser.setMnemonic(KeyEvent.VK_U);
browseUser.addActionListener(e -> ShellUtilities.browseDirectory(FileUtilities.USER_DIR));
diagnosticMenu.add(browseUser);
JMenuItem browseShared = new JMenuItem("Browse Shared folder...", iconCache.getIcon(FOLDER_ICON));
browseShared.setToolTipText(FileUtilities.SHARED_DIR.toString());
browseShared.setMnemonic(KeyEvent.VK_S);
browseShared.addActionListener(e -> ShellUtilities.browseDirectory(FileUtilities.SHARED_DIR));
diagnosticMenu.add(browseShared);
diagnosticMenu.add(new JSeparator());
JCheckBoxMenuItem notificationsItem = new JCheckBoxMenuItem("Show all notifications");
notificationsItem.setToolTipText("Shows all connect/disconnect messages, useful for debugging purposes");
notificationsItem.setMnemonic(KeyEvent.VK_S);
notificationsItem.setState(getPref(TRAY_NOTIFICATIONS));
notificationsItem.addActionListener(notificationsListener);
diagnosticMenu.add(notificationsItem);
JCheckBoxMenuItem monocleItem = new JCheckBoxMenuItem("Use Monocle for HTML");
monocleItem.setToolTipText("Use monocle platform for HTML printing (restart required)");
monocleItem.setMnemonic(KeyEvent.VK_U);
monocleItem.setState(getPref(TRAY_MONOCLE));
if(!SystemUtilities.hasMonocle()) {
log.warn("Monocle engine was not detected");
monocleItem.setEnabled(false);
monocleItem.setToolTipText("Monocle HTML engine was not detected");
}
monocleItem.addActionListener(monocleListener);
if (Constants.JAVA_VERSION.greaterThanOrEqualTo(Version.valueOf("11.0.0"))) { //only include if it can be used
diagnosticMenu.add(monocleItem);
}
diagnosticMenu.add(new JSeparator());
JMenuItem logItem = new JMenuItem("View logs (live feed)...", iconCache.getIcon(LOG_ICON));
logItem.setMnemonic(KeyEvent.VK_L);
logItem.addActionListener(logListener);
diagnosticMenu.add(logItem);
logDialog = new LogDialog(logItem, iconCache);
componentList.add(logDialog);
JMenuItem zipLogs = new JMenuItem("Zip logs (to Desktop)");
zipLogs.setToolTipText("Zip diagnostic logs, place on Desktop");
zipLogs.setMnemonic(KeyEvent.VK_Z);
zipLogs.addActionListener(e -> FileUtilities.zipLogs());
diagnosticMenu.add(zipLogs);
JMenuItem desktopItem = new JMenuItem("Create Desktop shortcut", iconCache.getIcon(DESKTOP_ICON));
desktopItem.setMnemonic(KeyEvent.VK_D);
desktopItem.addActionListener(desktopListener());
anonymousItem = new JCheckBoxMenuItem("Block anonymous requests");
anonymousItem.setToolTipText("Blocks all requests that do not contain a valid certificate/signature");
anonymousItem.setMnemonic(KeyEvent.VK_K);
anonymousItem.setState(Certificate.UNKNOWN.isBlocked());
anonymousItem.addActionListener(anonymousListener);
if(Constants.ENABLE_DIAGNOSTICS) {
advancedMenu.add(diagnosticMenu);
advancedMenu.add(new JSeparator());
}
advancedMenu.add(sitesItem);
advancedMenu.add(desktopItem);
advancedMenu.add(new JSeparator());
advancedMenu.add(anonymousItem);
JMenuItem reloadItem = new JMenuItem("Reload", iconCache.getIcon(RELOAD_ICON));
reloadItem.setMnemonic(KeyEvent.VK_R);
reloadItem.addActionListener(reloadListener);
JMenuItem aboutItem = new JMenuItem("About...", iconCache.getIcon(ABOUT_ICON));
aboutItem.setMnemonic(KeyEvent.VK_B);
aboutItem.addActionListener(aboutListener);
aboutDialog = new AboutDialog(aboutItem, iconCache);
componentList.add(aboutDialog);
if (SystemUtilities.isMac()) {
MacUtilities.registerAboutDialog(aboutDialog);
MacUtilities.registerQuitHandler(this);
}
JSeparator separator = new JSeparator();
JCheckBoxMenuItem startupItem = new JCheckBoxMenuItem("Automatically start");
startupItem.setMnemonic(KeyEvent.VK_S);
startupItem.setState(FileUtilities.isAutostart());
startupItem.addActionListener(startupListener());
if (!shortcutCreator.canAutoStart()) {
startupItem.setEnabled(false);
startupItem.setState(false);
startupItem.setToolTipText("Autostart has been disabled by the administrator");
}
JMenuItem exitItem = new JMenuItem("Exit", iconCache.getIcon(EXIT_ICON));
exitItem.addActionListener(exitListener);
popup.add(advancedMenu);
popup.add(reloadItem);
popup.add(aboutItem);
popup.add(startupItem);
popup.add(separator);
popup.add(exitItem);
if (tray != null) {
tray.setJPopupMenu(popup);
}
}
private final ActionListener notificationsListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
prefs.setProperty(TRAY_NOTIFICATIONS, ((JCheckBoxMenuItem)e.getSource()).getState());
}
};
private final ActionListener monocleListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JCheckBoxMenuItem j = (JCheckBoxMenuItem)e.getSource();
prefs.setProperty(TRAY_MONOCLE, j.getState());
displayWarningMessage(String.format("A restart of %s is required to ensure this feature is %sabled.",
Constants.ABOUT_TITLE, j.getState()? "en":"dis"));
}
};
private final ActionListener desktopListener() {
return e -> {
shortcutCreator.createDesktopShortcut();
};
}
private final ActionListener savedListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
sitesDialog.setVisible(true);
}
};
private final ActionListener anonymousListener = e -> {
boolean checkBoxState = true;
if (e.getSource() instanceof JCheckBoxMenuItem) {
checkBoxState = ((JCheckBoxMenuItem)e.getSource()).getState();
}
log.debug("Block unsigned: {}", checkBoxState);
if (checkBoxState) {
blackList(Certificate.UNKNOWN);
} else {
FileUtilities.deleteFromFile(Constants.BLOCK_FILE, Certificate.UNKNOWN.data(), true);
FileUtilities.deleteFromFile(Constants.BLOCK_FILE, Certificate.UNKNOWN.data(), false);
}
};
private final ActionListener logListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
logDialog.setVisible(true);
}
};
private ActionListener startupListener() {
return e -> {
JCheckBoxMenuItem source = (JCheckBoxMenuItem)e.getSource();
if (!source.getState() && !confirmDialog.prompt("Remove " + name + " from startup?")) {
source.setState(true);
return;
}
if (FileUtilities.setAutostart(source.getState())) {
displayInfoMessage("Successfully " + (source.getState() ? "enabled" : "disabled") + " autostart");
} else {
displayErrorMessage("Error " + (source.getState() ? "enabling" : "disabling") + " autostart");
}
source.setState(FileUtilities.isAutostart());
};
}
/**
* Sets the default reload action (in this case, <code>Thread.start()</code>) to be fired
*
* @param reloadThread The Thread to call when reload is clicked
*/
public void setReloadThread(Thread reloadThread) {
this.reloadThread = reloadThread;
}
private ActionListener reloadListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (reloadThread == null) {
showErrorDialog("Sorry, Reload has not yet been implemented.");
} else {
reloadThread.start();
}
}
};
private final ActionListener aboutListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
aboutDialog.setVisible(true);
}
};
private final ActionListener exitListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
boolean showAllNotifications = getPref(TRAY_NOTIFICATIONS);
if (!showAllNotifications || confirmDialog.prompt("Exit " + name + "?")) { exit(0); }
}
};
public void exit(int returnCode) {
prefs.save();
FileUtilities.cleanup();
System.exit(returnCode);
}
/**
* Displays a basic error dialog.
*/
private void showErrorDialog(String message) {
JOptionPane.showMessageDialog(null, message, name, JOptionPane.ERROR_MESSAGE);
}
public boolean showGatewayDialog(final RequestState request, final String prompt, final Point position) {
if (!headless) {
try {
SwingUtilities.invokeAndWait(() -> gatewayDialog.prompt("%s wants to " + prompt, request, position));
}
catch(Exception ignore) {}
if (gatewayDialog.isApproved()) {
log.info("Allowed {} to {}", request.getCertName(), prompt);
if (gatewayDialog.isPersistent()) {
whiteList(request.getCertUsed());
}
} else {
log.info("Denied {} to {}", request.getCertName(), prompt);
if (gatewayDialog.isPersistent()) {
if (!request.hasCertificate()) {
anonymousItem.doClick(); // if always block anonymous requests -> flag menu item
} else {
blackList(request.getCertUsed());
}
}
}
return gatewayDialog.isApproved();
} else {
return request.hasSavedCert();
}
}
private void whiteList(Certificate cert) {
if (FileUtilities.printLineToFile(Constants.ALLOW_FILE, cert.data())) {
displayInfoMessage(String.format(Constants.ALLOW_SITES_TEXT, cert.getOrganization()));
} else {
displayErrorMessage("Failed to write to file (Insufficient user privileges)");
}
}
private void blackList(Certificate cert) {
if (FileUtilities.printLineToFile(Constants.BLOCK_FILE, cert.data())) {
displayInfoMessage(String.format(Constants.BLOCK_SITES_TEXT, cert.getOrganization()));
} else {
displayErrorMessage("Failed to write to file (Insufficient user privileges)");
}
}
public void setServer(Server server, WebsocketPorts websocketPorts) {
if (server != null && server.getConnectors().length > 0) {
singleInstanceCheck(websocketPorts);
displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server));
if (!headless) {
aboutDialog.setServer(server);
setDefaultIcon();
}
} else {
displayErrorMessage("Invalid server");
}
}
/**
* Thread safe method for setting a fine status message. Messages are suppressed unless "Show all
* notifications" is checked.
*/
public void displayInfoMessage(String text) {
displayMessage(name, text, TrayIcon.MessageType.INFO);
}
/**
* Thread safe method for setting the default icon
*/
public void setDefaultIcon() {
// Workaround for JDK-8252015
if(SystemUtilities.isMac() && Constants.MASK_TRAY_SUPPORTED && !MacUtilities.jdkSupportsTemplateIcon()) {
setIcon(DEFAULT_ICON, () -> MacUtilities.toggleTemplateIcon(tray.tray()));
} else {
setIcon(DEFAULT_ICON);
}
}
/** Thread safe method for setting the error status message */
public void displayErrorMessage(String text) {
displayMessage(name, text, TrayIcon.MessageType.ERROR);
}
/** Thread safe method for setting the danger icon */
public void setDangerIcon() {
setIcon(DANGER_ICON);
}
/** Thread safe method for setting the warning status message */
public void displayWarningMessage(String text) {
displayMessage(name, text, TrayIcon.MessageType.WARNING);
}
/** Thread safe method for setting the warning icon */
public void setWarningIcon() {
setIcon(WARNING_ICON);
}
/** Thread safe method for setting the specified icon */
private void setIcon(final IconCache.Icon i, Runnable whenDone) {
if (tray != null && i != shownIcon) {
shownIcon = i;
refreshIcon(whenDone);
}
}
private void setIcon(final IconCache.Icon i) {
setIcon(i, null);
}
public void refreshIcon(final Runnable whenDone) {
SwingUtilities.invokeLater(() -> {
tray.setIcon(shownIcon);
if(whenDone != null) {
whenDone.run();
}
});
}
/**
* Thread safe method for setting the specified status message
*
* @param caption The title of the tray message
* @param text The text body of the tray message
* @param level The message type: Level.INFO, .WARN, .SEVERE
*/
private void displayMessage(final String caption, final String text, final TrayIcon.MessageType level) {
if (!headless) {
if (tray != null) {
SwingUtilities.invokeLater(() -> {
boolean showAllNotifications = getPref(TRAY_NOTIFICATIONS);
if (showAllNotifications || level != TrayIcon.MessageType.INFO) {
tray.displayMessage(caption, text, level);
}
});
}
} else {
log.info("{}: [{}] {}", caption, level, text);
}
}
public void singleInstanceCheck(WebsocketPorts websocketPorts) {
// Secure
for(int port : websocketPorts.getUnusedSecurePorts()) {
new SingleInstanceChecker(this, port, true);
}
// Insecure
for(int port : websocketPorts.getUnusedInsecurePorts()) {
new SingleInstanceChecker(this, port, false);
}
}
public boolean isMonoclePreferred() {
return getPref(TRAY_MONOCLE);
}
public boolean isHeadless() {
return headless;
}
/**
* Get boolean user pref: Searching "user", "app" and <code>System.getProperty(...)</code>.
*/
private boolean getPref(ArgValue argValue) {
return PrefsSearch.getBoolean(argValue, prefs, App.getTrayProperties());
}
private void performIfIdle(int idleQualifier, ActionListener performer) {
if (idleTimer != null) {
idleTimer.schedule(new TimerTask() {
@Override
public void run() {
performer.actionPerformed(null);
}
}, idleQualifier);
} else {
log.warn("Idle actions have already been cleared due to activity, task not scheduled.");
}
}
public void voidIdleActions() {
if (idleTimer != null) {
log.trace("Not idle, stopping any actions that haven't ran yet");
idleTimer.cancel();
idleTimer = null;
}
}
}

View File

@@ -0,0 +1,13 @@
package qz.communication;
public class DeviceException extends Exception {
public DeviceException(String message) {
super(message);
}
public DeviceException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,29 @@
package qz.communication;
public interface DeviceIO extends DeviceListener {
String getVendorId();
String getProductId();
void open() throws DeviceException;
boolean isOpen();
void close();
void setStreaming(boolean streaming);
boolean isStreaming();
byte[] readData(int responseSize, Byte exchangeConfig) throws DeviceException;
void sendData(byte[] data, Byte exchangeConfig) throws DeviceException;
byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException;
void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException;
}

View File

@@ -0,0 +1,9 @@
package qz.communication;
public interface DeviceListener {
/**
* Cleanup task for when a socket closes while a device is still streaming
*/
void close();
}

View File

@@ -0,0 +1,130 @@
package qz.communication;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.codehaus.jettison.json.JSONObject;
import qz.utils.UsbUtilities;
public class DeviceOptions {
public enum DeviceMode {
HID,
USB,
UNKNOWN;
public static DeviceMode parse(String callName) {
if (callName != null) {
if (callName.startsWith("usb")) {
return USB;
} else if (callName.startsWith("hid")) {
return HID;
}
}
return UNKNOWN;
}
}
private DeviceMode deviceMode;
private Integer vendorId;
private Integer productId;
//usb specific
private Byte interfaceId;
private Byte endpoint;
private int interval;
private int responseSize;
//hid specific
private Integer usagePage;
private String serial;
public DeviceOptions(JSONObject parameters, DeviceMode deviceMode) {
this.deviceMode = deviceMode;
vendorId = UsbUtilities.hexToInt(parameters.optString("vendorId"));
productId = UsbUtilities.hexToInt(parameters.optString("productId"));
if (!parameters.isNull("interface")) {
interfaceId = UsbUtilities.hexToByte(parameters.optString("interface"));
}
if (!parameters.isNull("endpoint")) {
endpoint = UsbUtilities.hexToByte(parameters.optString("endpoint"));
} else if (!parameters.isNull("reportId")) {
endpoint = UsbUtilities.hexToByte(parameters.optString("reportId"));
}
interval = parameters.optInt("interval", 100);
responseSize = parameters.optInt("responseSize");
if (!parameters.isNull("usagePage")) {
usagePage = UsbUtilities.hexToInt(parameters.optString("usagePage"));
}
if (!parameters.isNull("serial")) {
serial = parameters.optString("serial", "");
serial = serial.isEmpty() ? null : serial;
}
}
public Integer getVendorId() {
return vendorId;
}
public Integer getProductId() {
return productId;
}
public Byte getInterfaceId() {
return interfaceId;
}
public Byte getEndpoint() {
return endpoint;
}
public int getInterval() {
return interval;
}
public int getResponseSize() {
return responseSize;
}
public Integer getUsagePage() {
return usagePage;
}
public String getSerial() {
return serial;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof DeviceOptions)) { return false; }
DeviceOptions that = (DeviceOptions)obj;
if (this.getVendorId().equals(that.getVendorId()) && this.getProductId().equals(that.getProductId())) {
if (deviceMode == DeviceMode.USB
&& (this.getInterfaceId() == null || that.getInterfaceId() == null || this.getInterfaceId().equals(that.getInterfaceId()))
&& (this.getEndpoint() == null || that.getEndpoint() == null || this.getEndpoint().equals(that.getEndpoint()))) {
return true;
}
if (deviceMode == DeviceMode.HID
&& (this.getUsagePage() == null || that.getUsagePage() == null || this.getUsagePage().equals(that.getUsagePage()))
&& (this.getSerial() == null || that.getSerial() == null || this.getSerial().equals(that.getSerial()))) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
return new HashCodeBuilder()
.append(deviceMode)
.append(vendorId)
.append(productId)
.toHashCode();
}
}

View File

@@ -0,0 +1,173 @@
package qz.communication;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOCase;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.eclipse.jetty.websocket.api.Session;
import qz.ws.PrintSocketClient;
import qz.ws.StreamEvent;
import java.nio.channels.ClosedChannelException;
import java.nio.file.Path;
import java.nio.file.WatchKey;
import java.util.ArrayList;
public class FileIO implements DeviceListener {
public static final String SANDBOX_DATA_SUFFIX = "sandbox";
public static final String GLOBAL_DATA_SUFFIX = "shared";
// Pesky breadcrumb files that only the OS cares about
public static final String[] DEFAULT_EXCLUSIONS = { ".DS_Store", "Thumbs.db" };
public static final int FILE_LISTENER_DEFAULT_LINES = 10;
public enum ReadType {
BYTES, LINES
}
private Session session;
private Path originalPath;
private Path absolutePath;
private WatchKey wk;
private ReadType readType;
private boolean reversed;
private long bytes;
private int lines;
private IOCase caseSensitivity;
private ArrayList<String> inclusions;
private ArrayList<String> exclusions;
public FileIO(Session session, JSONObject params, Path originalPath, Path absolutePath) throws JSONException {
this.session = session;
this.originalPath = originalPath;
this.absolutePath = absolutePath;
inclusions = new ArrayList<>();
exclusions = new ArrayList<>();
JSONArray inc = params.optJSONArray("include");
JSONArray exc = params.optJSONArray("exclude");
caseSensitivity = params.optBoolean("ignoreCase", true) ? IOCase.INSENSITIVE : IOCase.SENSITIVE;
if (inc != null) {
for (int i = 0; i < inc.length(); i++) {
inclusions.add(inc.getString(i));
}
}
if (exc != null) {
for(int i = 0; i < exc.length(); i++) {
exclusions.add(exc.getString(i));
}
}
JSONObject options = params.optJSONObject("listener");
if (options != null) {
// Setup defaults
bytes = options.optLong("bytes", -1);
if (bytes > 0) {
readType = ReadType.BYTES;
} else {
readType = ReadType.LINES;
}
lines = options.optInt("lines", readType == ReadType.LINES? FILE_LISTENER_DEFAULT_LINES:-1);
reversed = options.optBoolean("reverse", readType == ReadType.LINES);
}
}
public boolean isMatch(String fileName) {
boolean match = inclusions.isEmpty();
for (String inclusion : inclusions) {
if(FilenameUtils.wildcardMatch(fileName, inclusion, caseSensitivity)) {
match = true;
break;
}
}
if(match) {
// Never match on DEFAULT_EXCLUSIONS
for(String exclusion : DEFAULT_EXCLUSIONS) {
if (FilenameUtils.wildcardMatch(fileName, exclusion, IOCase.INSENSITIVE)) {
return false;
}
}
for(String exclusion : exclusions) {
if (FilenameUtils.wildcardMatch(fileName, exclusion, caseSensitivity)) {
match = false;
break;
}
}
}
return match;
}
public boolean returnsContents() {
return bytes > 0 || lines > 0;
}
public ReadType getReadType() {
return readType;
}
public boolean isReversed() {
return reversed;
}
public long getBytes() {
return bytes;
}
public int getLines() {
return lines;
}
public Path getOriginalPath() {
return originalPath;
}
public Path getAbsolutePath() {
return absolutePath;
}
public boolean isWatching() {
return wk != null && wk.isValid();
}
public void setWk(WatchKey wk) {
this.wk = wk;
}
public void fileChanged(String fileName, String type, String fileData) throws ClosedChannelException {
StreamEvent evt = new StreamEvent(StreamEvent.Stream.FILE, StreamEvent.Type.ACTION)
.withData("file", getOriginalPath().resolve(fileName))
.withData("eventType", type);
if (fileData != null) {
evt.withData("fileData", fileData);
}
PrintSocketClient.sendStream(session, evt);
}
public void sendError(String message) throws ClosedChannelException {
StreamEvent eventErr = new StreamEvent(StreamEvent.Stream.FILE, StreamEvent.Type.ERROR)
.withData("message", message);
PrintSocketClient.sendStream(session, eventErr);
}
@Override
public void close() {
if (wk != null) {
wk.cancel();
}
}
}

View File

@@ -0,0 +1,67 @@
package qz.communication;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.utils.ByteUtilities;
import qz.utils.PrintingUtilities.Flavor;
import java.io.IOException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* Created by Kyle on 2/28/2018.
*/
public class FileParams {
private Path path;
private String data;
private Flavor flavor;
private boolean shared;
private boolean sandbox;
private OpenOption appendMode;
public FileParams(JSONObject params) throws JSONException {
path = Paths.get(params.getString("path"));
data = params.optString("data", "");
flavor = Flavor.parse(params, Flavor.PLAIN);
shared = params.optBoolean("shared", true);
sandbox = params.optBoolean("sandbox", true);
appendMode = params.optBoolean("append")? StandardOpenOption.APPEND:StandardOpenOption.TRUNCATE_EXISTING;
}
public Path getPath() {
return path;
}
public String toString(byte[] bytes) {
return ByteUtilities.toString(flavor, bytes);
}
public byte[] getData() throws IOException {
return flavor.read(data);
}
public Flavor getFlavor() {
return flavor;
}
public boolean isShared() {
return shared;
}
public boolean isSandbox() {
return sandbox;
}
public OpenOption getAppendMode() {
return appendMode;
}
}

View File

@@ -0,0 +1,110 @@
package qz.communication;
import org.hid4java.HidDevice;
import qz.ws.SocketConnection;
import javax.usb.util.UsbUtil;
public class H4J_HidIO implements DeviceIO, DeviceListener {
private HidDevice device;
private boolean streaming;
private DeviceOptions dOpts;
private SocketConnection websocket;
public H4J_HidIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
this(H4J_HidUtilities.findDevice(dOpts), dOpts, websocket);
}
private H4J_HidIO(HidDevice device, DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
this.dOpts = dOpts;
this.websocket = websocket;
if (device == null) {
throw new DeviceException("HID device could not be found");
}
this.device = device;
}
public void open() {
if (!isOpen()) {
device.open();
}
}
public boolean isOpen() {
return !device.isClosed();
}
public void setStreaming(boolean active) {
streaming = active;
}
public boolean isStreaming() {
return streaming;
}
public String getVendorId() {
return UsbUtil.toHexString(device.getVendorId());
}
public String getProductId() {
return UsbUtil.toHexString(device.getProductId());
}
public byte[] readData(int responseSize, Byte unused) throws DeviceException {
byte[] response = new byte[responseSize];
int read = device.read(response);
if (read == -1) {
throw new DeviceException("Failed to read from device");
}
return response;
}
public void sendData(byte[] data, Byte reportId) throws DeviceException {
if (reportId == null) { reportId = (byte)0x00; }
int wrote = device.write(data, data.length, reportId);
if (wrote == -1) {
throw new DeviceException("Failed to write to device");
}
}
public byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException {
if (reportId == null) { reportId = (byte)0x00; }
byte[] response = new byte[responseSize];
int read = device.getFeatureReport(response, reportId);
if (read == -1) {
throw new DeviceException("Failed to read from device");
}
return response;
}
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
if (reportId == null) { reportId = (byte)0x00; }
int wrote = device.sendFeatureReport(data, reportId);
if (wrote == -1) {
throw new DeviceException("Failed to write to device");
}
}
@Override
public void close() {
setStreaming(false);
// Remove orphaned reference
websocket.removeDevice(dOpts);
if (isOpen()) {
device.close();
}
}
}

View File

@@ -0,0 +1,83 @@
package qz.communication;
import org.codehaus.jettison.json.JSONArray;
import org.eclipse.jetty.websocket.api.Session;
import org.hid4java.HidDevice;
import org.hid4java.HidManager;
import org.hid4java.HidServicesListener;
import org.hid4java.event.HidServicesEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.ws.PrintSocketClient;
import qz.ws.StreamEvent;
import javax.usb.util.UsbUtil;
public class H4J_HidListener implements DeviceListener, HidServicesListener {
private static final Logger log = LogManager.getLogger(H4J_HidListener.class);
private Session session;
public H4J_HidListener(Session session) {
HidManager.getHidServices().addHidServicesListener(this);
this.session = session;
}
@Override
public void hidFailure(HidServicesEvent hidServicesEvent) {
log.debug("Device failure: {}", hidServicesEvent.getHidDevice().getProduct());
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Failure"), this);
}
@Override
public void hidDataReceived(HidServicesEvent hidServicesEvent) {
log.debug("Data received: {}", hidServicesEvent.getDataReceived().length + " bytes");
JSONArray hex = new JSONArray();
for(byte b : hidServicesEvent.getDataReceived()) {
hex.put(UsbUtil.toHexString(b));
}
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Data Received", hex), this);
}
@Override
public void hidDeviceDetached(HidServicesEvent hidServicesEvent) {
log.debug("Device detached: {}", hidServicesEvent.getHidDevice().getProduct());
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Detached"), this);
}
@Override
public void hidDeviceAttached(HidServicesEvent hidServicesEvent) {
log.debug("Device attached: {}", hidServicesEvent.getHidDevice().getProduct());
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Attached"), this);
}
private StreamEvent createStreamAction(HidDevice device, String action) {
return createStreamAction(device, action, null);
}
private StreamEvent createStreamAction(HidDevice device, String action, JSONArray dataArr) {
StreamEvent event = new StreamEvent(StreamEvent.Stream.HID, StreamEvent.Type.ACTION)
.withData("vendorId", UsbUtil.toHexString(device.getVendorId()))
.withData("productId", UsbUtil.toHexString(device.getProductId()))
.withData("actionType", action);
if (dataArr != null) {
event.withData("data", dataArr);
}
return event;
}
@Override
public void close() {
HidManager.getHidServices().removeHidServicesListener(this);
}
}

View File

@@ -0,0 +1,65 @@
package qz.communication;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.hid4java.HidDevice;
import org.hid4java.HidManager;
import org.hid4java.HidServices;
import javax.usb.util.UsbUtil;
import java.util.HashSet;
import java.util.List;
public class H4J_HidUtilities {
private static final HidServices service = HidManager.getHidServices();
public static List<HidDevice> getHidDevices() {
return service.getAttachedHidDevices();
}
public static JSONArray getHidDevicesJSON() throws JSONException {
List<HidDevice> devices = getHidDevices();
JSONArray devicesJSON = new JSONArray();
HashSet<String> unique = new HashSet<>();
for(HidDevice device : devices) {
JSONObject deviceJSON = new JSONObject();
deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId()))
.put("productId", UsbUtil.toHexString(device.getProductId()))
.put("usagePage", UsbUtil.toHexString((short)device.getUsagePage()))
.put("serial", device.getSerialNumber())
.put("manufacturer", device.getManufacturer())
.put("product", device.getProduct());
String uid = String.format("v%sp%su%ss%s", deviceJSON.optString("vendorId"), deviceJSON.optString("productId"), deviceJSON.optString("usagePage"), deviceJSON.optString("serial"));
if (!unique.contains(uid)) {
devicesJSON.put(deviceJSON);
unique.add(uid);
}
}
return devicesJSON;
}
public static HidDevice findDevice(DeviceOptions dOpts) {
if (dOpts.getVendorId() == null) {
throw new IllegalArgumentException("Vendor ID cannot be null");
}
if (dOpts.getProductId() == null) {
throw new IllegalArgumentException("Product ID cannot be null");
}
List<HidDevice> devices = getHidDevices();
for(HidDevice device : devices) {
if (device.isVidPidSerial(dOpts.getVendorId(), dOpts.getProductId(), dOpts.getSerial())
&& (dOpts.getUsagePage() == null || dOpts.getUsagePage() == device.getUsagePage())) {
return device;
}
}
return null;
}
}

View File

@@ -0,0 +1,154 @@
package qz.communication;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import purejavahidapi.HidDevice;
import purejavahidapi.HidDeviceInfo;
import purejavahidapi.InputReportListener;
import purejavahidapi.PureJavaHidApi;
import qz.utils.SystemUtilities;
import qz.ws.SocketConnection;
import javax.usb.util.UsbUtil;
import java.io.IOException;
import java.util.Vector;
public class PJHA_HidIO implements DeviceIO {
private static final Logger log = LogManager.getLogger(PJHA_HidIO.class);
private HidDeviceInfo deviceInfo;
private HidDevice device;
private static final int BUFFER_SIZE = 32;
private Vector<byte[]> dataBuffer;
private boolean streaming;
private DeviceOptions dOpts;
private SocketConnection websocket;
public PJHA_HidIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
this(PJHA_HidUtilities.findDevice(dOpts), dOpts, websocket);
}
private PJHA_HidIO(HidDeviceInfo deviceInfo, DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
this.dOpts = dOpts;
this.websocket = websocket;
if (deviceInfo == null) {
throw new DeviceException("HID device could not be found");
}
this.deviceInfo = deviceInfo;
dataBuffer = new Vector<byte[]>() {
@Override
public synchronized boolean add(byte[] e) {
while(this.size() >= BUFFER_SIZE) {
this.remove(0);
}
return super.add(e);
}
};
}
public void open() throws DeviceException {
if (!isOpen()) {
try {
device = PureJavaHidApi.openDevice(deviceInfo);
device.setInputReportListener(new InputReportListener() {
@Override
public void onInputReport(HidDevice source, byte id, byte[] data, int len) {
byte[] dataCopy = new byte[len];
System.arraycopy(data, 0, dataCopy, 0, len);
dataBuffer.add(dataCopy);
}
});
}
catch(IOException ex) {
throw new DeviceException(ex);
}
}
}
public boolean isOpen() {
return device != null;
}
public void setStreaming(boolean active) {
streaming = active;
}
public boolean isStreaming() {
return streaming;
}
public String getVendorId() {
return UsbUtil.toHexString(deviceInfo.getVendorId());
}
public String getProductId() {
return UsbUtil.toHexString(deviceInfo.getProductId());
}
public byte[] readData(int responseSize, Byte unused) throws DeviceException {
byte[] response = new byte[responseSize];
if (dataBuffer.isEmpty()) {
return new byte[0]; //no data received yet
}
byte[] latestData = dataBuffer.remove(0);
if (SystemUtilities.isWindows()) {
//windows missing the leading byte
System.arraycopy(latestData, 0, response, 1, Math.min(responseSize - 1, latestData.length));
} else {
System.arraycopy(latestData, 0, response, 0, Math.min(responseSize - 1, latestData.length));
}
return response;
}
public void sendData(byte[] data, Byte reportId) throws DeviceException {
if (reportId == null) { reportId = (byte)0x00; }
int wrote = device.setOutputReport(reportId, data, data.length);
if (wrote == -1) {
throw new DeviceException("Failed to write to device");
}
}
public byte[] getFeatureReport(int responseSize, Byte unused) throws DeviceException {
byte[] response = new byte[responseSize];
int read = device.getFeatureReport(response, responseSize);
if (read == -1) {
throw new DeviceException("Failed to read from device");
}
return response;
}
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
if (reportId == null) { reportId = (byte)0x00; }
int wrote = device.setFeatureReport(reportId, data, data.length);
if (wrote == -1) {
throw new DeviceException("Failed to write to device");
}
}
@Override
public void close() {
setStreaming(false);
// Remove orphaned reference
websocket.removeDevice(dOpts);
if (isOpen()) {
try {
device.setInputReportListener(null);
device.close();
}
catch(IllegalStateException e) {
log.warn("Device already closed");
}
}
device = null;
}
}

View File

@@ -0,0 +1,50 @@
package qz.communication;
import org.eclipse.jetty.websocket.api.Session;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import purejavahidapi.DeviceRemovalListener;
import purejavahidapi.HidDevice;
import qz.ws.PrintSocketClient;
import qz.ws.StreamEvent;
import javax.usb.util.UsbUtil;
public class PJHA_HidListener implements DeviceListener, DeviceRemovalListener {
private static final Logger log = LogManager.getLogger(PJHA_HidListener.class);
private Session session;
private HidDevice device;
public PJHA_HidListener(Session session) {
this.session = session;
}
public void setDevice(HidDevice device) {
this.device = device;
device.setDeviceRemovalListener(this);
}
private StreamEvent createStreamAction(HidDevice device, String action) {
return new StreamEvent(StreamEvent.Stream.HID, StreamEvent.Type.ACTION)
.withData("vendorId", UsbUtil.toHexString(device.getHidDeviceInfo().getVendorId()))
.withData("productId", UsbUtil.toHexString(device.getHidDeviceInfo().getProductId()))
.withData("actionType", action);
}
@Override
public void close() {
if (device != null) {
device.setDeviceRemovalListener(null);
}
}
@Override
public void onDeviceRemoval(HidDevice device) {
log.debug("Device detached: {}", device.getHidDeviceInfo().getProductString());
PrintSocketClient.sendStream(session, createStreamAction(device, "Device Detached"), this);
}
}

View File

@@ -0,0 +1,56 @@
package qz.communication;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import purejavahidapi.HidDeviceInfo;
import purejavahidapi.PureJavaHidApi;
import javax.usb.util.UsbUtil;
import java.util.List;
public class PJHA_HidUtilities {
public static JSONArray getHidDevicesJSON() throws JSONException {
List<HidDeviceInfo> devices = PureJavaHidApi.enumerateDevices();
JSONArray devicesJSON = new JSONArray();
for(HidDeviceInfo device : devices) {
JSONObject deviceJSON = new JSONObject();
deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId()))
.put("productId", UsbUtil.toHexString(device.getProductId()))
.put("usagePage", UsbUtil.toHexString(device.getUsagePage()))
.put("serial", device.getSerialNumberString())
.put("manufacturer", device.getManufacturerString())
.put("product", device.getProductString());
devicesJSON.put(deviceJSON);
}
return devicesJSON;
}
public static HidDeviceInfo findDevice(DeviceOptions dOpts) {
if (dOpts.getVendorId() == null) {
throw new IllegalArgumentException("Vendor ID cannot be null");
}
if (dOpts.getProductId() == null) {
throw new IllegalArgumentException("Product ID cannot be null");
}
List<HidDeviceInfo> devList = PureJavaHidApi.enumerateDevices();
for(HidDeviceInfo device : devList) {
if (device.getVendorId() == dOpts.getVendorId().shortValue() && device.getProductId() == dOpts.getProductId().shortValue()
&& (dOpts.getUsagePage() == null || dOpts.getUsagePage().shortValue() == device.getUsagePage())
&& (dOpts.getSerial() == null || dOpts.getSerial().equals(device.getSerialNumberString()))) {
return device;
}
}
return null;
}
}

View File

@@ -0,0 +1,297 @@
package qz.communication;
import jssc.*;
import org.apache.commons.codec.binary.StringUtils;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.ByteArrayBuilder;
import qz.utils.ByteUtilities;
import qz.utils.DeviceUtilities;
import qz.ws.SocketConnection;
import java.io.IOException;
/**
* @author Tres
*/
public class SerialIO implements DeviceListener {
private static final Logger log = LogManager.getLogger(SerialIO.class);
// Timeout to wait before giving up on reading the specified amount of bytes
private static final int TIMEOUT = 1200;
private String portName;
private SerialPort port;
private SerialOptions serialOpts;
private ByteArrayBuilder data = new ByteArrayBuilder();
private SocketConnection websocket;
/**
* Controller for serial communications
*
* @param portName Port name to open, such as "COM1" or "/dev/tty0/"
*/
public SerialIO(String portName, SocketConnection websocket) {
this.portName = portName;
this.websocket = websocket;
}
/**
* Open the specified port name.
*
* @param opts Parsed serial options
* @return Boolean indicating success.
* @throws SerialPortException If the port fails to open.
*/
public boolean open(SerialOptions opts) throws SerialPortException {
if (isOpen()) {
log.warn("Serial port [{}] is already open", portName);
return false;
}
port = new SerialPort(portName);
port.openPort();
serialOpts = new SerialOptions();
setOptions(opts);
return port.isOpened();
}
public void applyPortListener(SerialPortEventListener listener) throws SerialPortException {
port.addEventListener(listener);
}
/**
* @return Boolean indicating if port is currently open.
*/
public boolean isOpen() {
return port != null && port.isOpened();
}
public String processSerialEvent(SerialPortEvent event) {
SerialOptions.ResponseFormat format = serialOpts.getResponseFormat();
try {
// Receive data
if (event.isRXCHAR()) {
data.append(port.readBytes(event.getEventValue(), TIMEOUT));
String response = null;
if (format.isBoundNewline()) {
//process as line delimited
// check for CR AND NL
Integer endIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r', '\n'});
int delimSize = 2;
// check for CR OR NL
if(endIdx == null) {
endIdx = min(
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r'}),
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\n'}));
delimSize = 1;
}
if (endIdx != null) {
log.trace("Reading newline-delimited response");
byte[] output = new byte[endIdx];
System.arraycopy(data.getByteArray(), 0, output, 0, endIdx);
String buffer = new String(output, format.getEncoding());
if (!buffer.isEmpty()) {
//send non-empty string
response = buffer;
}
data.clearRange(0, endIdx + delimSize);
}
} else if (format.getBoundStart() != null && format.getBoundStart().length > 0) {
//process as formatted response
Integer startIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundStart());
if (startIdx != null) {
int startOffset = startIdx + format.getBoundStart().length;
int copyLength = 0;
int endIdx = 0;
if (format.getBoundEnd() != null && format.getBoundEnd().length > 0) {
//process as bounded response
Integer boundEnd = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundEnd(), startIdx);
if (boundEnd != null) {
log.trace("Reading bounded response");
copyLength = boundEnd - startOffset;
endIdx = boundEnd + 1;
if (format.isIncludeStart()) {
//also include the ending bytes
copyLength += format.getBoundEnd().length;
}
}
} else if (format.getFixedWidth() > 0) {
//process as fixed length prefixed response
log.trace("Reading fixed length prefixed response");
copyLength = format.getFixedWidth();
endIdx = startOffset + format.getFixedWidth();
} else if (format.getLength() != null) {
//process as dynamic formatted response
SerialOptions.ByteParam lengthParam = format.getLength();
if (data.getLength() > startOffset + lengthParam.getIndex() + lengthParam.getLength()) { //ensure there's length bytes to read
log.trace("Reading dynamic formatted response");
int expectedLength = ByteUtilities.parseBytes(data.getByteArray(), startOffset + lengthParam.getIndex(), lengthParam.getLength(), lengthParam.getEndian());
log.trace("Found length byte, expecting {} bytes", expectedLength);
startOffset += lengthParam.getIndex() + lengthParam.getLength(); // don't include the length byte(s) in the response
copyLength = expectedLength;
endIdx = startOffset + copyLength;
if (format.getCrc() != null) {
SerialOptions.ByteParam crcParam = format.getCrc();
log.trace("Expecting {} crc bytes", crcParam.getLength());
int expand = crcParam.getIndex() + crcParam.getLength();
//include crc in copy
copyLength += expand;
endIdx += expand;
}
}
} else {
//process as header formatted raw response - high risk of lost data, likely unintended settings
log.warn("Reading header formatted raw response, are you missing an rx option?");
copyLength = data.getLength() - startOffset;
endIdx = data.getLength();
}
if (copyLength > 0 && data.getLength() >= endIdx) {
log.debug("Response format readable, starting copy");
if (format.isIncludeStart()) {
//increase length to account for header bytes and bump offset back to include in copy
copyLength += (startOffset - startIdx);
startOffset = startIdx;
}
byte[] responseData = new byte[copyLength];
System.arraycopy(data.getByteArray(), startOffset, responseData, 0, copyLength);
response = new String(responseData, format.getEncoding());
data.clearRange(startIdx, endIdx);
}
}
} else if (format.getFixedWidth() > 0) {
if (data.getLength() >= format.getFixedWidth()) {
//process as fixed width response
log.trace("Reading fixed length response");
byte[] output = new byte[format.getFixedWidth()];
System.arraycopy(data.getByteArray(), 0, output, 0, format.getFixedWidth());
response = StringUtils.newStringUtf8(output);
data.clearRange(0, format.getFixedWidth());
}
} else {
//no processing, return raw
log.trace("Reading raw response");
response = new String(data.getByteArray(), format.getEncoding());
data.clear();
}
return response;
}
}
catch(SerialPortException e) {
log.error("Exception occurred while reading data from port.", e);
}
catch(SerialPortTimeoutException e) {
log.error("Timeout occurred waiting for port to respond.", e);
}
return null;
}
/**
* Sets and caches the properties as to not set them every data call
*
* @throws SerialPortException If the properties fail to set
*/
private void setOptions(SerialOptions opts) throws SerialPortException {
if (opts == null) { return; }
SerialOptions.PortSettings ps = opts.getPortSettings();
if (ps != null && !ps.equals(serialOpts.getPortSettings())) {
log.debug("Applying new port settings");
port.setParams(ps.getBaudRate(), ps.getDataBits(), ps.getStopBits(), ps.getParity());
port.setFlowControlMode(ps.getFlowControl());
serialOpts.setPortSettings(ps);
}
SerialOptions.ResponseFormat rf = opts.getResponseFormat();
if (rf != null) {
log.debug("Applying new response formatting");
serialOpts.setResponseFormat(rf);
}
}
/**
* Applies the port parameters and writes the buffered data to the serial port.
*/
public void sendData(JSONObject params, SerialOptions opts) throws JSONException, IOException, SerialPortException {
if (opts != null) {
setOptions(opts);
}
log.debug("Sending data over [{}]", portName);
port.writeBytes(DeviceUtilities.getDataBytes(params, serialOpts.getPortSettings().getEncoding()));
}
/**
* Closes the serial port, if open.
*
* @throws SerialPortException If the port fails to close.
*/
@Override
public void close() {
// Remove orphaned reference
websocket.removeSerialPort(portName);
if (!isOpen()) {
log.warn("Serial port [{}] is not open.", portName);
}
try {
boolean closed = port.closePort();
if (closed) {
log.info("Serial port [{}] closed successfully.", portName);
} else {
// Handle ambiguity in JSSCs API
throw new SerialPortException(portName, "closePort", "Port not closed");
}
} catch(SerialPortException e) {
log.warn("Serial port [{}] was not closed properly.", portName);
}
port = null;
portName = null;
}
private Integer min(Integer a, Integer b) {
if (a == null) { return b; }
if (b == null) { return a; }
return Math.min(a, b);
}
}

View File

@@ -0,0 +1,350 @@
package qz.communication;
import jssc.SerialPort;
import org.apache.commons.lang3.ArrayUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ByteUtilities;
import qz.utils.DeviceUtilities;
import qz.utils.LoggerUtilities;
import qz.utils.SerialUtilities;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Locale;
public class SerialOptions {
private static final Logger log = LogManager.getLogger(SerialOptions.class);
private static final String DEFAULT_BEGIN = "0x0002";
private static final String DEFAULT_END = "0x000D";
private PortSettings portSettings = null;
private ResponseFormat responseFormat = null;
/**
* Creates an empty/default options object
*/
public SerialOptions() {
portSettings = new PortSettings();
responseFormat = new ResponseFormat();
}
/**
* Parses the provided JSON object into relevant SerialPort constants
*/
public SerialOptions(JSONObject serialOpts, boolean isOpening) {
if (serialOpts == null) { return; }
//only apply port settings if opening or explicitly set in a send data call
if (isOpening || serialOpts.has("baudRate") || serialOpts.has("dataBits") || serialOpts.has("stopBits") || serialOpts.has("parity") || serialOpts.has("flowControl")) {
portSettings = new PortSettings();
if (!serialOpts.isNull("baudRate")) {
try { portSettings.baudRate = SerialUtilities.parseBaudRate(serialOpts.getString("baudRate")); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "baudRate", serialOpts.opt("baudRate")); }
}
if (!serialOpts.isNull("dataBits")) {
try { portSettings.dataBits = SerialUtilities.parseDataBits(serialOpts.getString("dataBits")); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "dataBits", serialOpts.opt("dataBits")); }
}
if (!serialOpts.isNull("stopBits")) {
try { portSettings.stopBits = SerialUtilities.parseStopBits(serialOpts.getString("stopBits")); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "stopBits", serialOpts.opt("stopBits")); }
}
if (!serialOpts.isNull("parity")) {
try { portSettings.parity = SerialUtilities.parseParity(serialOpts.getString("parity")); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "parity", serialOpts.opt("parity")); }
}
if (!serialOpts.isNull("flowControl")) {
try { portSettings.flowControl = SerialUtilities.parseFlowControl(serialOpts.getString("flowControl")); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "flowControl", serialOpts.opt("flowControl")); }
}
if (!serialOpts.isNull("encoding") && !serialOpts.optString("encoding").isEmpty()) {
try { portSettings.encoding = Charset.forName(serialOpts.getString("encoding")); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "encoding", serialOpts.opt("encoding")); }
}
}
if (!serialOpts.isNull("rx")) {
responseFormat = new ResponseFormat();
//Make the response encoding default to the port encoding. If this is removed it will default to UTF-8
responseFormat.encoding = portSettings.encoding;
JSONObject respOpts = serialOpts.optJSONObject("rx");
if (respOpts != null) {
if (!respOpts.isNull("start")) {
try {
JSONArray startBits = respOpts.getJSONArray("start");
ArrayList<Byte> bytes = new ArrayList<>();
for(int i = 0; i < startBits.length(); i++) {
byte[] charByte = DeviceUtilities.characterBytes(startBits.getString(i), responseFormat.encoding);
for(byte b : charByte) { bytes.add(b); }
}
responseFormat.boundStart = ArrayUtils.toPrimitive(bytes.toArray(new Byte[0]));
}
catch(JSONException e) {
try { responseFormat.boundStart = DeviceUtilities.characterBytes(respOpts.getString("start"), responseFormat.encoding); }
catch(JSONException e2) { LoggerUtilities.optionWarn(log, "string", "start", respOpts.opt("start")); }
}
}
if (!respOpts.isNull("includeHeader")) {
try { responseFormat.includeStart = respOpts.getBoolean("includeHeader"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "includeHeader", respOpts.opt("includeHeader")); }
}
if (!respOpts.isNull("end")) {
try { responseFormat.boundEnd = DeviceUtilities.characterBytes(respOpts.getString("end"), responseFormat.encoding); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "end", respOpts.opt("end")); }
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
log.warn("End bound set without start bound defined");
}
}
if (!respOpts.isNull("untilNewline")) {
try { responseFormat.boundNewline = respOpts.getBoolean("untilNewline"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "untilNewline", respOpts.opt("untilNewline")); }
}
if (!respOpts.isNull("width")) {
try { responseFormat.fixedWidth = respOpts.getInt("width"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", respOpts.opt("width")); }
}
if (!respOpts.isNull("lengthBytes")) {
try {
JSONObject lengthOpts = respOpts.optJSONObject("lengthBytes");
responseFormat.length = new ByteParam();
if (lengthOpts != null) {
if (!lengthOpts.isNull("index")) {
try { responseFormat.length.index = lengthOpts.getInt("index"); }
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.index", lengthOpts.opt("index")); }
}
if (!lengthOpts.isNull("length")) {
try { responseFormat.length.length = lengthOpts.getInt("length"); }
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.length", lengthOpts.opt("length")); }
}
if (!lengthOpts.isNull("endian")) {
try { responseFormat.length.endian = ByteUtilities.Endian.valueOf(lengthOpts.getString("endian").toUpperCase(Locale.ENGLISH)); }
catch(JSONException se) { LoggerUtilities.optionWarn(log, "string", "lengthBytes.endian", lengthOpts.opt("endian")); }
}
} else {
responseFormat.length.index = respOpts.getInt("lengthBytes");
}
}
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes", respOpts.opt("lengthBytes")); }
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
log.warn("Length byte(s) defined without start bound defined");
}
}
if (!respOpts.isNull("crcBytes")) {
try {
JSONObject crcOpts = respOpts.optJSONObject("crcBytes");
responseFormat.crc = new ByteParam();
if (crcOpts != null) {
if (!crcOpts.isNull("index")) {
try { responseFormat.crc.index = crcOpts.getInt("index"); }
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.index", crcOpts.opt("index")); }
}
if (!crcOpts.isNull("length")) {
try { responseFormat.crc.length = crcOpts.getInt("length"); }
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.length", crcOpts.opt("length")); }
}
} else {
responseFormat.crc.length = respOpts.getInt("crcBytes");
}
}
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "crcBytes", respOpts.opt("crcBytes")); }
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
log.warn("CRC byte(s) defined without start bound defined");
}
}
if (!respOpts.isNull("encoding") && !respOpts.optString("encoding").isEmpty()) {
try { responseFormat.encoding = Charset.forName(respOpts.getString("encoding")); }
catch(JSONException | IllegalArgumentException e) { LoggerUtilities.optionWarn(log, "charset", "encoding", respOpts.opt("encoding")); }
}
} else {
LoggerUtilities.optionWarn(log, "JSONObject", "rx", serialOpts.opt("rx"));
}
} else if (isOpening) {
// legacy support - only applies on port open
responseFormat = new ResponseFormat();
// legacy start only supports string, not an array
if (!serialOpts.isNull("start")) {
responseFormat.boundStart = DeviceUtilities.characterBytes(serialOpts.optString("start", DEFAULT_BEGIN), responseFormat.encoding);
} else {
responseFormat.boundStart = DeviceUtilities.characterBytes(DEFAULT_BEGIN, responseFormat.encoding);
}
if (!serialOpts.isNull("end")) {
responseFormat.boundEnd = DeviceUtilities.characterBytes(serialOpts.optString("end", DEFAULT_END), responseFormat.encoding);
} else {
responseFormat.boundEnd = DeviceUtilities.characterBytes(DEFAULT_END, responseFormat.encoding);
}
if (!serialOpts.isNull("width")) {
try {
responseFormat.fixedWidth = serialOpts.getInt("width");
if (responseFormat.boundEnd.length > 0) {
log.warn("Combining 'width' property with 'end' property has undefined behavior and should not be used");
}
}
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", serialOpts.opt("width")); }
}
}
}
public PortSettings getPortSettings() {
return portSettings;
}
public ResponseFormat getResponseFormat() {
return responseFormat;
}
public void setPortSettings(PortSettings portSettings) {
this.portSettings = portSettings;
}
public void setResponseFormat(ResponseFormat responseFormat) {
this.responseFormat = responseFormat;
}
public class PortSettings {
private Charset encoding = Charset.forName("UTF-8");
private int baudRate = SerialPort.BAUDRATE_9600;
private int dataBits = SerialPort.DATABITS_8;
private int stopBits = SerialPort.STOPBITS_1;
private int parity = SerialPort.PARITY_NONE;
private int flowControl = SerialPort.FLOWCONTROL_NONE;
public Charset getEncoding() {
return encoding;
}
public int getBaudRate() {
return baudRate;
}
public int getDataBits() {
return dataBits;
}
public int getStopBits() {
return stopBits;
}
public int getParity() {
return parity;
}
public int getFlowControl() {
return flowControl;
}
@Override
public boolean equals(Object o) {
if (o instanceof PortSettings) {
PortSettings that = (PortSettings)o;
return getEncoding().equals(that.getEncoding()) &&
getBaudRate() == that.getBaudRate() &&
getDataBits() == that.getDataBits() &&
getStopBits() == that.getStopBits() &&
getParity() == that.getParity() &&
getFlowControl() == that.getFlowControl();
} else {
return false;
}
}
}
public class ResponseFormat {
private Charset encoding = Charset.forName("UTF-8"); //Response charset
private byte[] boundStart; //Character(s) denoting start of new response
private byte[] boundEnd; //Character denoting end of a response
private boolean boundNewline; //If the response should be split on \r?\n
private int fixedWidth; //Fixed length response bounds
private ByteParam length; //Info about the data length byte(s)
private ByteParam crc; //Info about the data crc byte(s)
private boolean includeStart; //If the response headers should be sent as well
public Charset getEncoding() {
return encoding;
}
public byte[] getBoundStart() {
return boundStart;
}
public byte[] getBoundEnd() {
return boundEnd;
}
public int getFixedWidth() {
return fixedWidth;
}
public ByteParam getLength() {
return length;
}
public ByteParam getCrc() {
return crc;
}
public boolean isIncludeStart() {
return includeStart;
}
public boolean isBoundNewline() {
return boundNewline;
}
}
public class ByteParam {
private int index = 0;
private int length = 1;
private ByteUtilities.Endian endian = ByteUtilities.Endian.BIG;
public int getIndex() {
return index;
}
public int getLength() {
return length;
}
public ByteUtilities.Endian getEndian() {
return endian;
}
}
}

View File

@@ -0,0 +1,100 @@
package qz.communication;
import org.apache.commons.lang3.ArrayUtils;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.DeviceUtilities;
import qz.utils.NetworkUtilities;
import qz.ws.SocketConnection;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.ArrayList;
public class SocketIO implements DeviceListener {
private static final Logger log = LogManager.getLogger(SocketIO.class);
private String host;
private int port;
private Charset encoding;
private Socket socket;
private DataOutputStream dataOut;
private DataInputStream dataIn;
private SocketConnection websocket;
public SocketIO(String host, int port, Charset encoding, SocketConnection websocket) {
this.host = host;
this.port = port;
this.encoding = encoding;
this.websocket = websocket;
}
public boolean open() throws IOException {
socket = new Socket(host, port);
socket.setSoTimeout(NetworkUtilities.SOCKET_TIMEOUT);
dataOut = new DataOutputStream(socket.getOutputStream());
dataIn = new DataInputStream(socket.getInputStream());
return socket.isConnected();
}
public boolean isOpen() {
return socket.isConnected();
}
public void sendData(JSONObject params) throws JSONException, IOException {
log.debug("Sending data over [{}:{}]", host, port);
dataOut.write(DeviceUtilities.getDataBytes(params, encoding));
dataOut.flush();
}
public String processSocketResponse() throws IOException {
byte[] response = new byte[1024];
ArrayList<Byte> fullResponse = new ArrayList<>();
do {
int size = dataIn.read(response);
for(int i = 0; i < size; i++) {
fullResponse.add(response[i]);
}
}
while(dataIn.available() > 0);
if(fullResponse.size() > 0) {
return new String(ArrayUtils.toPrimitive(fullResponse.toArray(new Byte[0])), encoding);
}
return null;
}
@Override
public void close() {
// Remove orphaned reference
websocket.removeNetworkSocket(String.format("%s:%s", host, port));
try {
dataOut.close();
} catch(IOException e) {
log.warn("Could not close socket output stream", e);
}
try {
socket.close();
} catch(IOException e) {
log.warn("Could not close socket", e);
}
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}

View File

@@ -0,0 +1,153 @@
package qz.communication;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.UsbUtilities;
import qz.ws.SocketConnection;
import javax.usb.*;
import javax.usb.util.UsbUtil;
public class UsbIO implements DeviceIO {
private static final Logger log = LogManager.getLogger(UsbIO.class);
private UsbDevice device;
private UsbInterface iface;
private boolean streaming;
private DeviceOptions dOpts;
private SocketConnection websocket;
public UsbIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
this.dOpts = dOpts;
this.websocket = websocket;
UsbDevice device = UsbUtilities.findDevice(dOpts.getVendorId().shortValue(), dOpts.getProductId().shortValue());
if (device == null) {
throw new DeviceException("USB device could not be found");
}
if (dOpts.getInterfaceId() == null) {
throw new IllegalArgumentException("Device interface cannot be null");
}
this.iface = device.getActiveUsbConfiguration().getUsbInterface(dOpts.getInterfaceId());
if (iface == null) {
throw new DeviceException(String.format("Could not find USB interface matching [ vendorId: '%s', productId: '%s', interface: '%s' ]",
"0x" + UsbUtil.toHexString(dOpts.getVendorId()),
"0x" + UsbUtil.toHexString(dOpts.getProductId()),
"0x" + UsbUtil.toHexString(dOpts.getInterfaceId())));
}
this.device = device;
}
public void open() throws DeviceException {
try {
iface.claim(new UsbInterfacePolicy() {
@Override
public boolean forceClaim(UsbInterface usbInterface) {
// Releases kernel driver for systems that auto-claim usb devices
return true;
}
});
}
catch(UsbException e) {
throw new DeviceException(e);
}
}
public boolean isOpen() {
return iface.isClaimed();
}
public void setStreaming(boolean active) {
streaming = active;
}
public boolean isStreaming() {
return streaming;
}
public String getVendorId() {
return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idVendor());
}
public String getProductId() {
return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idProduct());
}
public String getInterface() {
return UsbUtil.toHexString(iface.getUsbInterfaceDescriptor().iInterface());
}
public byte[] readData(int responseSize, Byte endpoint) throws DeviceException {
try {
byte[] response = new byte[responseSize];
exchangeData(endpoint, response);
return response;
}
catch(UsbException e) {
throw new DeviceException(e);
}
}
public void sendData(byte[] data, Byte endpoint) throws DeviceException {
try {
exchangeData(endpoint, data);
}
catch(UsbException e) {
throw new DeviceException(e);
}
}
public byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException {
throw new DeviceException("USB feature reports are not supported");
}
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
throw new DeviceException("USB feature reports are not supported");
}
/**
* Data will be sent to or received from the open usb device, depending on the {@code endpoint} used.
*
* @param endpoint Endpoint on the usb device interface to pass data across
* @param data Byte array of data to send, or to be written from a receive
*/
private synchronized void exchangeData(Byte endpoint, byte[] data) throws UsbException, DeviceException {
if (endpoint == null) {
throw new IllegalArgumentException("Interface endpoint cannot be null");
}
UsbEndpoint usbEndpoint = iface.getUsbEndpoint(endpoint);
if(usbEndpoint == null) {
throw new DeviceException(String.format("Could not find USB endpoint matching [ endpoint: '%s' ]",
"0x" + UsbUtil.toHexString(endpoint)));
}
UsbPipe pipe = usbEndpoint.getUsbPipe();
if (!pipe.isOpen()) { pipe.open(); }
try {
pipe.syncSubmit(data);
}
finally {
if(pipe != null) {
pipe.close();
}
}
}
@Override
public void close() {
setStreaming(false);
// Remove orphaned reference
websocket.removeDevice(dOpts);
if (iface.isClaimed()) {
try {
iface.release();
}
catch(UsbException e) {
log.error("Unable to close USB device", e);
}
}
}
}

View File

@@ -0,0 +1,28 @@
package qz.communication;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.Winspool;
import com.sun.jna.win32.W32APIOptions;
/**
* TODO: Remove when JNA 5.14.0+ is bundled
*/
@SuppressWarnings("unused")
public interface WinspoolEx extends Winspool {
WinspoolEx INSTANCE = Native.load("Winspool.drv", WinspoolEx.class, W32APIOptions.DEFAULT_OPTIONS);
int JOB_CONTROL_NONE = 0x00000000; // Perform no additional action.
int JOB_CONTROL_PAUSE = 0x00000001; // Pause the print job.
int JOB_CONTROL_RESUME = 0x00000002; // Resume a paused print job.
int JOB_CONTROL_CANCEL = 0x00000003; // Delete a print job.
int JOB_CONTROL_RESTART = 0x00000004; // Restart a print job.
int JOB_CONTROL_DELETE = 0x00000005; // Delete a print job.
int JOB_CONTROL_SENT_TO_PRINTER = 0x00000006; // Used by port monitors to signal that a print job has been sent to the printer. This value SHOULD NOT be used remotely.
int JOB_CONTROL_LAST_PAGE_EJECTED = 0x00000007; // Used by language monitors to signal that the last page of a print job has been ejected from the printer. This value SHOULD NOT be used remotely.
int JOB_CONTROL_RETAIN = 0x00000008; // Keep the print job in the print queue after it prints.
int JOB_CONTROL_RELEASE = 0x00000009; // Release the print job, undoing the effect of a JOB_CONTROL_RETAIN action.
boolean SetJob(WinNT.HANDLE hPrinter, int JobId, int Level, Pointer pJob, int Command);
}

View File

@@ -0,0 +1,11 @@
package qz.exception;
public class InvalidRawImageException extends Exception {
public InvalidRawImageException(String msg) {
super(msg);
}
public InvalidRawImageException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,3 @@
package qz.exception;
public class MissingArgException extends Exception {}

View File

@@ -0,0 +1,10 @@
package qz.exception;
public class NullCommandException extends javax.print.PrintException {
public NullCommandException() {
super();
}
public NullCommandException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,7 @@
package qz.exception;
public class NullPrintServiceException extends javax.print.PrintException {
public NullPrintServiceException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,413 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.auth.Certificate;
import qz.build.provision.params.Phase;
import qz.installer.certificate.*;
import qz.installer.certificate.firefox.FirefoxCertificateInstaller;
import qz.installer.provision.ProvisionInstaller;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import qz.ws.WebsocketPorts;
import java.io.*;
import java.nio.file.*;
import java.security.cert.X509Certificate;
import java.util.*;
import static qz.common.Constants.*;
import static qz.installer.certificate.KeyPairWrapper.Type.CA;
import static qz.utils.FileUtilities.*;
/**
* Cross-platform wrapper for install steps
* - Used by CommandParser via command line
* - Used by PrintSocketServer at startup to ensure SSL is functioning
*/
public abstract class Installer {
protected static final Logger log = LogManager.getLogger(Installer.class);
// Silence prompts within our control
public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent"));
public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime";
WebsocketPorts websocketPorts;
public enum PrivilegeLevel {
USER,
SYSTEM
}
public abstract Installer removeLegacyStartup();
public abstract Installer addAppLauncher();
public abstract Installer addStartupEntry();
public abstract Installer addSystemSettings();
public abstract Installer removeSystemSettings();
public abstract void spawn(List<String> args) throws Exception;
public abstract void setDestination(String destination);
public abstract String getDestination();
private static Installer instance;
public static Installer getInstance() {
if(instance == null) {
switch(SystemUtilities.getOs()) {
case WINDOWS:
instance = new WindowsInstaller();
break;
case MAC:
instance = new MacInstaller();
break;
default:
instance = new LinuxInstaller();
}
}
return instance;
}
public static void install(String destination, boolean silent) throws Exception {
IS_SILENT |= silent; // preserve environmental variable if possible
getInstance();
if (destination != null) {
instance.setDestination(destination);
}
install();
}
public static boolean preinstall() {
getInstance();
log.info("Fixing runtime permissions...");
instance.setJrePermissions(SystemUtilities.getAppPath().toString());
log.info("Stopping running instances...");
return TaskKiller.killAll();
}
public static void install() throws Exception {
getInstance();
log.info("Installing to {}", instance.getDestination());
instance.removeLibs()
.removeProvisioning()
.deployApp()
.removeLegacyStartup()
.removeLegacyFiles()
.addSharedDirectory()
.addAppLauncher()
.addStartupEntry()
.invokeProvisioning(Phase.INSTALL)
.addSystemSettings();
}
public static void uninstall() {
log.info("Stopping running instances...");
TaskKiller.killAll();
getInstance();
log.info("Uninstalling from {}", instance.getDestination());
instance.removeSharedDirectory()
.removeSystemSettings()
.removeCerts()
.invokeProvisioning(Phase.UNINSTALL);
}
public Installer deployApp() throws IOException {
Path src = SystemUtilities.getAppPath();
Path dest = Paths.get(getDestination());
if(!Files.exists(dest)) {
Files.createDirectories(dest);
}
// Delete the JDK blindly
FileUtils.deleteDirectory(dest.resolve(JRE_LOCATION).toFile());
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
FileUtils.copyDirectory(src.toFile(), dest.toFile(), false);
FileUtilities.setPermissionsRecursively(dest, false);
// Fix permissions for provisioned files
FileUtilities.setExecutableRecursively(SystemUtilities.isMac() ?
dest.resolve("Contents/Resources").resolve(PROVISION_DIR) :
dest.resolve(PROVISION_DIR), false);
if(!SystemUtilities.isWindows()) {
setExecutable(SystemUtilities.isMac() ? "Contents/Resources/uninstall" : "uninstall");
setExecutable(SystemUtilities.isMac() ? "Contents/MacOS/" + ABOUT_TITLE : PROPS_FILE);
return setJrePermissions(getDestination());
}
return this;
}
private Installer setJrePermissions(String dest) {
File jreLocation = new File(dest, JRE_LOCATION);
File jreBin = new File(jreLocation, "bin");
File jreLib = new File(jreLocation, "lib");
// Set jre/bin/java and friends executable
File[] files = jreBin.listFiles(pathname -> !pathname.isDirectory());
if(files != null) {
for(File file : files) {
file.setExecutable(true, false);
}
}
// Set jspawnhelper executable
new File(jreLib, "jspawnhelper" + (SystemUtilities.isWindows() ? ".exe" : "")).setExecutable(true, false);
return this;
}
private void setExecutable(String relativePath) {
new File(getDestination(), relativePath).setExecutable(true, false);
}
/**
* Explicitly purge libs to notify system cache per https://github.com/qzind/tray/issues/662
*/
public Installer removeLibs() {
String[] dirs = { "libs" };
for (String dir : dirs) {
try {
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
} catch(IOException ignore) {}
}
return this;
}
public Installer cleanupLegacyLogs(int rolloverCount) {
// Convert old < 2.2.3 log file format
Path logLocation = USER_DIR;
int oldIndex = 0;
int newIndex = 0;
File oldFile;
do {
// Old: debug.log.1
oldFile = logLocation.resolve("debug.log." + ++oldIndex).toFile();
if(oldFile.exists()) {
// New: debug.1.log
File newFile;
do {
newFile = logLocation.resolve("debug." + ++newIndex + ".log").toFile();
} while(newFile.exists());
oldFile.renameTo(newFile);
log.info("Migrated log file {} to new location {}", oldFile, newFile);
}
} while(oldFile.exists() || oldIndex <= rolloverCount);
return this;
}
public Installer removeLegacyFiles() {
ArrayList<String> dirs = new ArrayList<>();
ArrayList<String> files = new ArrayList<>();
HashMap<String, String> move = new HashMap<>();
// QZ Tray 2.0 files
dirs.add("demo/js/3rdparty");
dirs.add("utils");
dirs.add("auth");
files.add("demo/js/qz-websocket.js");
files.add("windows-icon.ico");
// QZ Tray 2.2.3-SNAPSHOT accidentally wrote certs in the wrong place
dirs.add("ssl");
// QZ Tray 2.1 files
if(SystemUtilities.isMac()) {
// Moved to macOS Application Bundle standard https://developer.apple.com/go/?id=bundle-structure
dirs.add("demo");
dirs.add("libs");
files.add(PROPS_FILE + ".jar");
files.add("LICENSE.txt");
files.add("uninstall");
move.put(PROPS_FILE + ".properties", "Contents/Resources/" + PROPS_FILE + ".properties");
}
dirs.forEach(dir -> {
try {
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
} catch(IOException ignore) {}
});
files.forEach(file -> {
new File(instance.getDestination() + File.separator + file).delete();
});
move.forEach((src, dest) -> {
try {
FileUtils.moveFile(new File(instance.getDestination() + File.separator + src),
new File(instance.getDestination() + File.separator + dest));
} catch(IOException ignore) {}
});
return this;
}
public Installer addSharedDirectory() {
try {
Files.createDirectories(SHARED_DIR);
FileUtilities.setPermissionsRecursively(SHARED_DIR, true);
Path ssl = Paths.get(SHARED_DIR.toString(), "ssl");
Files.createDirectories(ssl);
FileUtilities.setPermissionsRecursively(ssl, true);
log.info("Created shared directory: {}", SHARED_DIR);
} catch(IOException e) {
log.warn("Could not create shared directory: {}", SHARED_DIR);
}
return this;
}
public Installer removeSharedDirectory() {
try {
FileUtils.deleteDirectory(SHARED_DIR.toFile());
log.info("Deleted shared directory: {}", SHARED_DIR);
} catch(IOException e) {
log.warn("Could not delete shared directory: {}", SHARED_DIR);
}
return this;
}
/**
* Checks, and if needed generates an SSL for the system
*/
public CertificateManager certGen(boolean forceNew, String... hostNames) throws Exception {
CertificateManager certificateManager = new CertificateManager(forceNew, hostNames);
boolean needsInstall = certificateManager.needsInstall();
try {
// Check that the CA cert is installed
X509Certificate caCert = certificateManager.getKeyPair(CA).getCert();
NativeCertificateInstaller installer = NativeCertificateInstaller.getInstance();
if (forceNew || needsInstall) {
// Remove installed certs per request (usually the desktop installer, or failure to write properties)
// Skip if running from IDE, this may accidentally remove sandboxed certs
if(SystemUtilities.isJar()) {
List<String> matchingCerts = installer.find();
installer.remove(matchingCerts);
}
installer.install(caCert);
FirefoxCertificateInstaller.install(caCert, hostNames);
} else {
// Make sure the certificate is recognized by the system
if(caCert == null) {
log.info("CA cert is empty, skipping installation checks. This is normal for trusted/3rd-party SSL certificates.");
} else {
File tempCert = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
CertificateManager.writeCert(caCert, tempCert); // temp cert
if (!installer.verify(tempCert)) {
installer.install(caCert);
FirefoxCertificateInstaller.install(caCert, hostNames);
}
if(!tempCert.delete()) {
tempCert.deleteOnExit();
}
}
}
}
catch(Exception e) {
log.error("Something went wrong obtaining the certificate. HTTPS will fail.", e);
}
// Add provisioning steps that come after certgen
if(SystemUtilities.isAdmin()) {
invokeProvisioning(Phase.CERTGEN);
}
return certificateManager;
}
/**
* Remove matching certs from user|system, then Firefox
*/
public Installer removeCerts() {
// System certs
NativeCertificateInstaller instance = NativeCertificateInstaller.getInstance();
instance.remove(instance.find());
// Firefox certs
FirefoxCertificateInstaller.uninstall();
return this;
}
/**
* Add user-specific settings
* Note: See override usage for platform-specific tasks
*/
public Installer addUserSettings() {
// Check for whitelisted certificates in <install>/whitelist/
Path whiteList = SystemUtilities.getJarParentPath().resolve(WHITELIST_CERT_DIR);
if(Files.exists(whiteList) && Files.isDirectory(whiteList)) {
for(File file : whiteList.toFile().listFiles()) {
try {
Certificate cert = new Certificate(FileUtilities.readLocalFile(file.getPath()));
if (!cert.isSaved()) {
FileUtilities.addToCertList(ALLOW_FILE, file);
}
} catch(Exception e) {
log.warn("Could not add {} to {}", file, ALLOW_FILE, e);
}
}
}
return instance;
}
public Installer invokeProvisioning(Phase phase) {
try {
Path provisionPath = SystemUtilities.isMac() ?
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
Paths.get(getDestination()).resolve(PROVISION_DIR);
ProvisionInstaller provisionInstaller = new ProvisionInstaller(provisionPath);
provisionInstaller.invoke(phase);
// Special case for custom websocket ports
if(phase == Phase.INSTALL) {
websocketPorts = WebsocketPorts.parseFromSteps(provisionInstaller.getSteps());
}
} catch(Exception e) {
log.warn("An error occurred invoking provision \"phase\": \"{}\"", phase, e);
}
return this;
}
public Installer removeProvisioning() {
try {
Path provisionPath = SystemUtilities.isMac() ?
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
Paths.get(getDestination()).resolve(PROVISION_DIR);
FileUtils.deleteDirectory(provisionPath.toFile());
} catch(Exception e) {
log.warn("An error occurred removing provision directory", e);
}
return this;
}
public static Properties persistProperties(File oldFile, Properties newProps) {
if(oldFile.exists()) {
Properties oldProps = new Properties();
try(Reader reader = new FileReader(oldFile)) {
oldProps.load(reader);
for(String key : PERSIST_PROPS) {
if (oldProps.containsKey(key)) {
String value = oldProps.getProperty(key);
log.info("Preserving {}={} for install", key, value);
newProps.put(key, value);
}
}
} catch(IOException e) {
log.warn("Warning, an error occurred reading the old properties file {}", oldFile, e);
}
}
return newProps;
}
public void spawn(String ... args) throws Exception {
spawn(new ArrayList(Arrays.asList(args)));
}
}

View File

@@ -0,0 +1,371 @@
package qz.installer;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.FileUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.UnixUtilities;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import static qz.common.Constants.*;
public class LinuxInstaller extends Installer {
protected static final Logger log = LogManager.getLogger(LinuxInstaller.class);
public static final String SHORTCUT_NAME = PROPS_FILE + ".desktop";
public static final String STARTUP_DIR = "/etc/xdg/autostart/";
public static final String STARTUP_LAUNCHER = STARTUP_DIR + SHORTCUT_NAME;
public static final String APP_DIR = "/usr/share/applications/";
public static final String APP_LAUNCHER = APP_DIR + SHORTCUT_NAME;
public static final String UDEV_RULES = "/lib/udev/rules.d/99-udev-override.rules";
public static final String[] CHROME_POLICY_DIRS = {"/etc/chromium/policies/managed", "/etc/opt/chrome/policies/managed" };
public static final String CHROME_POLICY = "{ \"URLAllowlist\": [\"" + DATA_DIR + "://*\"] }";
private String destination = "/opt/" + PROPS_FILE;
private String sudoer;
public LinuxInstaller() {
super();
sudoer = getSudoer();
}
public void setDestination(String destination) {
this.destination = destination;
}
public String getDestination() {
return destination;
}
public Installer addAppLauncher() {
addLauncher(APP_LAUNCHER, false);
return this;
}
public Installer addStartupEntry() {
addLauncher(STARTUP_LAUNCHER, true);
return this;
}
private void addLauncher(String location, boolean isStartup) {
HashMap<String, String> fieldMap = new HashMap<>();
// Dynamic fields
fieldMap.put("%DESTINATION%", destination);
fieldMap.put("%LINUX_ICON%", String.format("%s.svg", PROPS_FILE));
fieldMap.put("%COMMAND%", String.format("%s/%s", destination, PROPS_FILE));
fieldMap.put("%PARAM%", isStartup ? "--honorautostart" : "%u");
File launcher = new File(location);
try {
FileUtilities.configureAssetFile("assets/linux-shortcut.desktop.in", launcher, fieldMap, LinuxInstaller.class);
launcher.setReadable(true, false);
launcher.setExecutable(true, false);
} catch(IOException e) {
log.warn("Unable to write {} file: {}", isStartup ? "startup":"launcher", location, e);
}
}
public Installer removeLegacyStartup() {
log.info("Removing legacy autostart entries for all users matching {} or {}", ABOUT_TITLE, PROPS_FILE);
// assume users are in /home
String[] shortcutNames = {ABOUT_TITLE, PROPS_FILE};
for(File file : new File("/home").listFiles()) {
if (file.isDirectory()) {
File userStart = new File(file.getPath() + "/.config/autostart");
if (userStart.exists() && userStart.isDirectory()) {
for (String shortcutName : shortcutNames) {
File legacyStartup = new File(userStart.getPath() + File.separator + shortcutName + ".desktop");
if(legacyStartup.exists()) {
legacyStartup.delete();
}
}
}
}
}
return this;
}
public Installer addSystemSettings() {
// Legacy Ubuntu versions only: Patch Unity to show the System Tray
if(UnixUtilities.isUbuntu()) {
ShellUtilities.execute("gsettings", "set", "com.canonical.Unity.Panel", "systray", "-whitelist", "\"['all']\"");
if(ShellUtilities.execute("killall", "-w", "unity", "-panel")) {
ShellUtilities.execute("nohup", "unity", "-panel");
}
if(ShellUtilities.execute("killall", "-w", "unity", "-2d")) {
ShellUtilities.execute("nohup", "unity", "-2d");
}
}
// Chrome protocol handler
for (String policyDir : CHROME_POLICY_DIRS) {
log.info("Installing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
try {
FileUtilities.setPermissionsParentally(Files.createDirectories(Paths.get(policyDir)), false);
} catch(IOException e) {
log.warn("An error occurred creating {}", policyDir);
}
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
try (BufferedWriter writer = new BufferedWriter(new FileWriter(policy.toFile()))){
writer.write(CHROME_POLICY);
policy.toFile().setReadable(true, false);
}
catch(IOException e) {
log.warn("Unable to write chrome policy: {} ({}:launch will fail)", policy, DATA_DIR);
}
}
// USB permissions
try {
File udev = new File(UDEV_RULES);
if (udev.exists()) {
udev.delete();
}
FileUtilities.configureAssetFile("assets/linux-udev.rules.in", new File(UDEV_RULES), new HashMap<>(), LinuxInstaller.class);
// udev rules should be -rw-r--r--
udev.setReadable(true, false);
ShellUtilities.execute("udevadm", "control", "--reload-rules");
} catch(IOException e) {
log.warn("Could not install udev rules, usb support may fail {}", UDEV_RULES, e);
}
// Cleanup incorrectly placed files
File badFirefoxJs = new File("/usr/bin/defaults/pref/" + PROPS_FILE + ".js");
File badFirefoxCfg = new File("/usr/bin/" + PROPS_FILE + ".cfg");
if(badFirefoxCfg.exists()) {
log.info("Removing incorrectly placed Firefox configuration {}, {}...", badFirefoxJs, badFirefoxCfg);
badFirefoxCfg.delete();
new File("/usr/bin/defaults").delete();
}
// Cleanup incorrectly placed files
File badFirefoxPolicy = new File("/usr/bin/distribution/policies.json");
if(badFirefoxPolicy.exists()) {
log.info("Removing incorrectly placed Firefox policy {}", badFirefoxPolicy);
badFirefoxPolicy.delete();
// Delete the distribution folder too, as long as it's empty
File badPolicyFolder = badFirefoxPolicy.getParentFile();
if(badPolicyFolder.isDirectory() && badPolicyFolder.listFiles().length == 0) {
badPolicyFolder.delete();
}
}
// Cleanup
log.info("Cleaning up any remaining files...");
new File(destination + File.separator + "install").delete();
return this;
}
public Installer removeSystemSettings() {
// Chrome protocol handler
for (String policyDir : CHROME_POLICY_DIRS) {
log.info("Removing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
policy.toFile().delete();
}
// USB permissions
File udev = new File(UDEV_RULES);
if (udev.exists()) {
udev.delete();
}
return this;
}
// Environmental variables for spawning a task using sudo. Order is important.
static String[] SUDO_EXPORTS = {"USER", "HOME", "UPSTART_SESSION", "DISPLAY", "DBUS_SESSION_BUS_ADDRESS", "XDG_CURRENT_DESKTOP", "GNOME_DESKTOP_SESSION_ID" };
/**
* Spawns the process as the underlying regular user account, preserving the environment
*/
public void spawn(List<String> args) throws Exception {
if(!SystemUtilities.isAdmin()) {
// Not admin, just run as the existing user
ShellUtilities.execute(args.toArray(new String[args.size()]));
return;
}
// Get user's environment from dbus, etc
HashMap<String, String> env = getUserEnv(sudoer);
if(env.size() == 0) {
throw new Exception("Unable to get dbus info; can't spawn instance");
}
// Prepare the environment
String[] envp = new String[env.size() + ShellUtilities.envp.length];
int i = 0;
// Keep existing env
for(String keep : ShellUtilities.envp) {
envp[i++] = keep;
}
for(String key :env.keySet()) {
envp[i++] = String.format("%s=%s", key, env.get(key));
}
// Concat "sudo|su", sudoer, "nohup", args
ArrayList<String> argsList = sudoCommand(sudoer, true, args);
// Spawn
log.info("Executing: {}", Arrays.toString(argsList.toArray()));
Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
}
/**
* Constructs a command to help running as another user using "sudo" or "su"
*/
public static ArrayList<String> sudoCommand(String sudoer, boolean async, List<String> cmds) {
ArrayList<String> sudo = new ArrayList<>();
if(StringUtils.isEmpty(sudoer) || !userExists(sudoer)) {
throw new UnsupportedOperationException(String.format("Parameter [sudoer: %s] is empty or the provided user was not found", sudoer));
}
if(ShellUtilities.execute("which", "sudo") // check if sudo exists
|| ShellUtilities.execute("sudo", "-u", sudoer, "-v")) { // check if user can login
// Pass directly into "sudo"
log.info("Guessing that this system prefers \"sudo\" over \"su\".");
sudo.add("sudo");
// Add calling user
sudo.add("-E"); // preserve environment
sudo.add("-u");
sudo.add(sudoer);
// Add "background" task support
if(async) {
sudo.add("nohup");
}
if(cmds != null && cmds.size() > 0) {
// Add additional commands
sudo.addAll(cmds);
}
} else {
// Build and escape for "su"
log.info("Guessing that this system prefers \"su\" over \"sudo\".");
sudo.add("su");
// Add calling user
sudo.add(sudoer);
sudo.add("-c");
// Add "background" task support
if(async) {
sudo.add("nohup");
}
if(cmds != null && cmds.size() > 0) {
// Add additional commands
sudo.addAll(Arrays.asList(StringUtils.join(cmds, "\" \"") + "\""));
}
}
return sudo;
}
/**
* Gets the most likely non-root user account that the installer is running from
*/
private static String getSudoer() {
String sudoer = ShellUtilities.executeRaw("logname").trim();
if(sudoer.isEmpty() || SystemUtilities.isSolaris()) {
sudoer = System.getenv("SUDO_USER");
}
return sudoer;
}
/**
* Uses two common POSIX techniques for testing if the provided user account exists
*/
private static boolean userExists(String user) {
return ShellUtilities.execute("id", "-u", user) ||
ShellUtilities.execute("getent", "passwd", user);
}
/**
* Attempts to extract user environment variables from the dbus process to
* allow starting a graphical application as the current user.
*
* If this fails, items such as the user's desktop theme may not be known to Java
* at runtime resulting in the Swing L&F instead of the Gtk L&F.
*/
private static HashMap<String, String> getUserEnv(String matchingUser) {
if(!SystemUtilities.isAdmin()) {
throw new UnsupportedOperationException("Administrative access is required");
}
String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="};
ArrayList<String> pids = new ArrayList<>();
for(String dbusMatch : dbusMatches) {
pids.addAll(Arrays.asList(ShellUtilities.executeRaw("pgrep", "-f", dbusMatch).split("\\r?\\n")));
}
HashMap<String, String> env = new HashMap<>();
HashMap<String, String> tempEnv = new HashMap<>();
ArrayList<String> toExport = new ArrayList<>(Arrays.asList(SUDO_EXPORTS));
for(String pid : pids) {
if(pid.isEmpty()) {
continue;
}
try {
String[] vars;
if(SystemUtilities.isSolaris()) {
// Use pargs -e $$ to get environment
log.info("Reading environment info from [pargs, -e, {}]", pid);
String pargs = ShellUtilities.executeRaw("pargs", "-e", pid);
vars = pargs.split("\\r?\\n");
String delim = "]: ";
for(int i = 0; i < vars.length; i++) {
if(vars[i].contains(delim)) {
vars[i] = vars[i].substring(vars[i].indexOf(delim) + delim.length()).trim();
}
}
} else {
// Assume /proc/$$/environ
String environ = String.format("/proc/%s/environ", pid);
String delim = Pattern.compile("\0").pattern();
log.info("Reading environment info from {}", environ);
vars = new String(Files.readAllBytes(Paths.get(environ))).split(delim);
}
for(String var : vars) {
String[] parts = var.split("=", 2);
if(parts.length == 2) {
String key = parts[0].trim();
String val = parts[1].trim();
if(toExport.contains(key)) {
tempEnv.put(key, val);
}
}
}
} catch(Exception e) {
log.warn("An unexpected error occurred obtaining dbus info", e);
}
// Only add vars for the current user
if(matchingUser.trim().equals(tempEnv.get("USER"))) {
env.putAll(tempEnv);
} else {
log.debug("Expected USER={} but got USER={}, skipping results for {}", matchingUser, tempEnv.get("USER"), pid);
}
// Use gtk theme
if(env.containsKey("XDG_CURRENT_DESKTOP") && !env.containsKey("GNOME_DESKTOP_SESSION_ID")) {
if(env.get("XDG_CURRENT_DESKTOP").toLowerCase(Locale.ENGLISH).contains("gnome")) {
env.put("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated");
}
}
}
return env;
}
}

View File

@@ -0,0 +1,125 @@
package qz.installer;
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.FileUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import static qz.common.Constants.*;
public class MacInstaller extends Installer {
protected static final Logger log = LogManager.getLogger(MacInstaller.class);
private static final String PACKAGE_NAME = getPackageName();
public static final String LAUNCH_AGENT_PATH = String.format("/Library/LaunchAgents/%s.plist", MacInstaller.PACKAGE_NAME);
private String destination = "/Applications/" + ABOUT_TITLE + ".app";
public Installer addAppLauncher() {
// not needed; registered when "QZ Tray.app" is copied
return this;
}
public Installer addStartupEntry() {
File dest = new File(LAUNCH_AGENT_PATH);
HashMap<String, String> fieldMap = new HashMap<>();
// Dynamic fields
fieldMap.put("%PACKAGE_NAME%", PACKAGE_NAME);
fieldMap.put("%COMMAND%", String.format("%s/Contents/MacOS/%s", destination, ABOUT_TITLE));
fieldMap.put("%PARAM%", "--honorautostart");
try {
FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class);
// Disable service until reboot
if(SystemUtilities.isMac()) {
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
}
} catch(IOException e) {
log.warn("Unable to write startup file: {}", dest, e);
}
return this;
}
public void setDestination(String destination) {
this.destination = destination;
}
public String getDestination() {
return destination;
}
public Installer addSystemSettings() {
// Chrome protocol handler
String plist = "/Library/Preferences/com.google.Chrome.plist";
if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] {DATA_DIR + "://*" }).isEmpty()) {
ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLAllowlist", "-array-add", DATA_DIR +"://*");
}
return this;
}
public Installer removeSystemSettings() {
// Remove startup entry
File dest = new File(LAUNCH_AGENT_PATH);
dest.delete();
return this;
}
/**
* Removes legacy (<= 2.0) startup entries
*/
public Installer removeLegacyStartup() {
log.info("Removing startup entries for all users matching " + ABOUT_TITLE);
String script = "tell application \"System Events\" to delete "
+ "every login item where name is \"" + ABOUT_TITLE + "\""
+ " or name is \"" + PROPS_FILE + ".jar\"";
// Run on background thread in case System Events is hung or slow to respond
final String finalScript = script;
new Thread(() -> {
ShellUtilities.executeAppleScript(finalScript);
}).run();
return this;
}
public static String getPackageName() {
String packageName;
String[] parts = ABOUT_URL.split("\\W");
if (parts.length >= 2) {
// Parse io.qz.qz-print from Constants
packageName = String.format("%s.%s.%s", parts[parts.length - 1], parts[parts.length - 2], PROPS_FILE);
} else {
// Fallback on something sane
packageName = "local." + PROPS_FILE;
}
return packageName;
}
public void spawn(List<String> args) throws Exception {
if(SystemUtilities.isAdmin()) {
// macOS unconventionally uses "$USER" during its install process
String sudoer = System.getenv("USER");
if(sudoer == null || sudoer.isEmpty() || sudoer.equals("root")) {
// Fallback, should only fire via Terminal + sudo
sudoer = ShellUtilities.executeRaw("logname").trim();
}
// Start directly without waitFor(...), avoids deadlocking
Runtime.getRuntime().exec(new String[] { "su", sudoer, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""});
} else {
Runtime.getRuntime().exec(args.toArray(new String[args.size()]));
}
}
}

View File

@@ -0,0 +1,227 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashSet;
import static qz.common.Constants.PROPS_FILE;
public class TaskKiller {
protected static final Logger log = LogManager.getLogger(TaskKiller.class);
private static final String[] JAR_NAMES = {
PROPS_FILE + ".jar",
"qz.App", // v2.2.0...
"qz.ws.PrintSocketServer" // v2.0.0...v2.1.6
};
private static final String[] KILL_PID_CMD_POSIX = { "kill", "-9" };
private static final String[] KILL_PID_CMD_WIN32 = { "taskkill.exe", "/F", "/PID" };
private static final String[] KILL_PID_CMD = SystemUtilities.isWindows() ? KILL_PID_CMD_WIN32 : KILL_PID_CMD_POSIX;
/**
* Kills all QZ Tray processes, being careful not to kill itself
*/
public static boolean killAll() {
boolean success = true;
// Disable service until reboot
if(SystemUtilities.isMac()) {
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
}
// Use jcmd to get all java processes
HashSet<Integer> pids = findPidsJcmd();
if(!SystemUtilities.isWindows()) {
// Fallback to pgrep, needed for macOS (See JDK-8319589, JDK-8197387)
pids.addAll(findPidsPgrep());
} else if(WindowsUtilities.isSystemAccount()) {
// Fallback to powershell, needed for Windows
pids.addAll(findPidsPwsh());
}
// Careful not to kill ourselves ;)
pids.remove(SystemUtilities.getProcessId());
// Kill each PID
String[] killPid = new String[KILL_PID_CMD.length + 1];
System.arraycopy(KILL_PID_CMD, 0, killPid, 0, KILL_PID_CMD.length);
for (Integer pid : pids) {
killPid[killPid.length - 1] = pid.toString();
success = success && ShellUtilities.execute(killPid);
}
return success;
}
private static Path getJcmdPath() throws IOException {
Path jcmd;
if(SystemUtilities.isWindows()) {
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd.exe");
} else if (SystemUtilities.isMac()) {
jcmd = SystemUtilities.getJarParentPath().resolve("../PlugIns/Java.runtime/Contents/Home/bin/jcmd");
} else {
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd");
}
if(!jcmd.toFile().exists()) {
log.error("Could not find {}", jcmd);
throw new IOException("Could not find jcmd, we can't use it for detecting running instances");
}
return jcmd;
}
static final String[] PWSH_QUERY = { "powershell.exe", "-Command", "\"(Get-CimInstance Win32_Process -Filter \\\"Name = 'java.exe' OR Name = 'javaw.exe'\\\").Where({$_.CommandLine -like '*%s*'}).ProcessId\"" };
/**
* Leverage powershell.exe when run as SYSTEM to workaround https://github.com/qzind/tray/issues/1360
* TODO: Remove when jcmd is patched to work as SYSTEM account
*/
private static HashSet<Integer> findPidsPwsh() {
HashSet<Integer> foundPids = new HashSet<>();
for(String jarName : JAR_NAMES) {
String[] pwshQuery = PWSH_QUERY.clone();
int lastIndex = pwshQuery.length - 1;
// Format the last element to contain the jarName
pwshQuery[lastIndex] = String.format(pwshQuery[lastIndex], jarName);
String stdout = ShellUtilities.executeRaw(pwshQuery);
String[] lines = stdout.split("\\s*\\r?\\n");
for(String line : lines) {
if(line.trim().isEmpty()) {
// Don't try to process blank lines
continue;
}
int pid = parsePid(line);
if (pid >= 0) {
foundPids.add(pid);
} else {
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
}
}
}
return foundPids;
}
/**
* Use pgrep to fetch all PIDs to workaround https://github.com/openjdk/jdk/pull/25824
* TODO: Remove when jcmd is patched to work properly on macOS
*/
private static HashSet<Integer> findPidsPgrep() {
HashSet<Integer> foundPids = new HashSet<>();
for(String jarName : JAR_NAMES) {
String stdout = ShellUtilities.executeRaw("pgrep", "-f", jarName);
String[] lines = stdout.split("\\s*\\r?\\n");
for(String line : lines) {
if(line.trim().isEmpty()) {
// Don't try to process blank lines
continue;
}
int pid = parsePid(line);
if (pid >= 0) {
foundPids.add(pid);
} else {
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
}
}
}
return foundPids;
}
/**
* Uses jcmd to fetch all PIDs that match this product
*/
private static HashSet<Integer> findPidsJcmd() {
HashSet<Integer> foundPids = new HashSet<>();
String stdout;
String[] lines;
try {
stdout = ShellUtilities.executeRaw(getJcmdPath().toString(), "-l");
if(stdout == null) {
log.error("Error calling '{}' {}", getJcmdPath(), "-l");
return foundPids;
}
lines = stdout.split("\\r?\\n");
} catch(Exception e) {
log.error(e);
return foundPids;
}
for(String line : lines) {
if (line.trim().isEmpty()) {
// Don't try to process blank lines
continue;
}
// e.g. "35446 C:\Program Files\QZ Tray\qz-tray.jar"
String[] parts = line.split(" ", 2);
int pid = parsePid(parts);
if (pid >= 0) {
String args = parseArgs(parts);
if (args == null) {
log.warn("Found PID value '{}' but no args to match. Full line: '{}', Full output: '{}'", pid, line, stdout);
continue;
}
for(String jarName : JAR_NAMES) {
if (args.contains(jarName)) {
foundPids.add(pid);
break; // continue parent loop
}
}
} else {
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
}
}
return foundPids;
}
// Returns the second index of a String[], trimmed
private static String parseArgs(String[] input) {
if(input != null) {
if(input.length == 2) {
return input[1].trim();
}
}
return null;
}
// Parses an int value form the first index of a String[], returning -1 if something went wrong
private static int parsePid(String[] input) {
if(input != null) {
if(input.length == 2) {
return parsePid(input[0]);
}
}
return -1;
}
// Parses an int value form the provided string, returning -1 if something went wrong
private static int parsePid(String input) {
String pidString = input.trim();
if(StringUtils.isNumeric(pidString)) {
return Integer.parseInt(pidString);
}
return -1;
}
}

View File

@@ -0,0 +1,208 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer;
import com.sun.jna.platform.win32.*;
import mslinks.ShellLink;
import mslinks.ShellLinkException;
import mslinks.ShellLinkHelper;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import qz.ws.PrintSocketServer;
import javax.swing.*;
import static qz.common.Constants.*;
import static qz.installer.WindowsSpecialFolders.*;
import static com.sun.jna.platform.win32.WinReg.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class WindowsInstaller extends Installer {
protected static final Logger log = LogManager.getLogger(WindowsInstaller.class);
private String destination = getDefaultDestination();
private String destinationExe = getDefaultDestination() + File.separator + PROPS_FILE + ".exe";
public void setDestination(String destination) {
this.destination = destination;
this.destinationExe = destination + File.separator + PROPS_FILE + ".exe";
}
/**
* Cycles through registry keys removing legacy (<= 2.0) startup entries
*/
public Installer removeLegacyStartup() {
log.info("Removing legacy startup entries for all users matching " + ABOUT_TITLE);
for (String user : Advapi32Util.registryGetKeys(HKEY_USERS)) {
WindowsUtilities.deleteRegValue(HKEY_USERS, user.trim() + "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", ABOUT_TITLE);
}
try {
FileUtils.deleteQuietly(new File(STARTUP + File.separator + ABOUT_TITLE + ".lnk"));
} catch(Win32Exception ignore) {}
return this;
}
public Installer addAppLauncher() {
try {
// Delete old 2.0 launcher
FileUtils.deleteQuietly(new File(COMMON_START_MENU + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
Path loc = Paths.get(COMMON_START_MENU.toString(), "Programs", ABOUT_TITLE);
loc.toFile().mkdirs();
String lnk = loc + File.separator + ABOUT_TITLE + ".lnk";
String exe = destination + File.separator + PROPS_FILE+ ".exe";
log.info("Creating launcher \"{}\" -> \"{}\"", lnk, exe);
ShellLinkHelper.createLink(exe, lnk);
} catch(ShellLinkException | IOException | Win32Exception e) {
log.warn("Could not create launcher", e);
}
return this;
}
public Installer addStartupEntry() {
try {
String lnk = WindowsSpecialFolders.COMMON_STARTUP + File.separator + ABOUT_TITLE + ".lnk";
String exe = destination + File.separator + PROPS_FILE+ ".exe";
log.info("Creating startup entry \"{}\" -> \"{}\"", lnk, exe);
ShellLink link = ShellLinkHelper.createLink(exe, lnk).getLink();
link.setCMDArgs("--honorautostart"); // honors auto-start preferences
} catch(ShellLinkException | IOException | Win32Exception e) {
log.warn("Could not create startup launcher", e);
}
return this;
}
public Installer removeSystemSettings() {
// Cleanup registry
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + ABOUT_TITLE);
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\" + ABOUT_TITLE);
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, DATA_DIR);
// Chrome protocol handler
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
// Deprecated Chrome protocol handler
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLWhitelist", String.format("%s://*", DATA_DIR));
// Cleanup launchers
for(WindowsSpecialFolders folder : new WindowsSpecialFolders[] { START_MENU, COMMON_START_MENU, DESKTOP, PUBLIC_DESKTOP, COMMON_STARTUP, RECENT }) {
try {
new File(folder + File.separator + ABOUT_TITLE + ".lnk").delete();
// Since 2.1, start menus use subfolder
if (folder.equals(COMMON_START_MENU) || folder.equals(START_MENU)) {
FileUtils.deleteQuietly(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
FileUtils.deleteDirectory(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE));
}
} catch(InvalidPathException | IOException | Win32Exception ignore) {}
}
// Cleanup firewall rules
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
return this;
}
public Installer addSystemSettings() {
/**
* TODO: Upgrade JNA!
* 64-bit registry view is currently invoked by nsis (windows-installer.nsi.in) using SetRegView 64
* However, newer version of JNA offer direct WinNT.KEY_WOW64_64KEY registry support, safeguarding
* against direct calls to "java -jar qz-tray.jar install|keygen|etc", which will be needed moving forward
* for support and troubleshooting.
*/
// Mime-type support e.g. qz:launch
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "", String.format("URL:%s Protocol", ABOUT_TITLE));
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "URL Protocol", "");
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\DefaultIcon", DATA_DIR), "", String.format("\"%s\",1", destinationExe));
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\shell\\open\\command", DATA_DIR), "", String.format("\"%s\" \"%%1\"", destinationExe));
/// Uninstall info
String uninstallKey = String.format("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", ABOUT_TITLE);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, String.format("Software\\%s", ABOUT_TITLE), "", destination);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayName", String.format("%s %s", ABOUT_TITLE, VERSION));
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "Publisher", ABOUT_COMPANY);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "UninstallString", destination + File.separator + "uninstall.exe");
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayIcon", destinationExe);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "HelpLink", ABOUT_SUPPORT_URL );
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLUpdateInfo", ABOUT_DOWNLOAD_URL);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLInfoAbout", ABOUT_SUPPORT_URL);
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayVersion", VERSION.toString());
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "EstimatedSize", FileUtils.sizeOfDirectoryAsBigInteger(new File(destination)).intValue() / 1024);
// Chrome protocol handler
WindowsUtilities.addNumberedRegValue(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
// Firewall rules
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "add", "rule", String.format("name=%s", ABOUT_TITLE),
"dir=in", "action=allow", "profile=any", String.format("localport=%s", websocketPorts.allPortsAsString()), "localip=any", "protocol=tcp");
return this;
}
@Override
public Installer addUserSettings() {
// Whitelist loopback for IE/Edge
if(ShellUtilities.execute("CheckNetIsolation.exe", "LoopbackExempt", "-a", "-n=Microsoft.MicrosoftEdge_8wekyb3d8bbwe")) {
log.warn("Could not whitelist loopback connections for IE, Edge");
}
try {
// Intranet settings; uncheck "include sites not listed in other zones"
String key = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones\\1";
String value = "Flags";
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
// remove value using bitwise XOR
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data ^ 16);
}
// Legacy Edge loopback support
key = "Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge\\ExperimentalFeatures";
value = "AllowLocalhostLoopback";
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
// remove value using bitwise OR
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data | 1);
}
} catch(Exception e) {
log.warn("An error occurred configuring the \"Local Intranet Zone\"; connections to \"localhost\" may fail", e);
}
return super.addUserSettings();
}
public static String getDefaultDestination() {
String path = System.getenv("ProgramW6432");
if (path == null || path.trim().isEmpty()) {
path = System.getenv("ProgramFiles");
}
return path + File.separator + ABOUT_TITLE;
}
public String getDestination() {
return destination;
}
public void spawn(List<String> args) throws Exception {
if(SystemUtilities.isAdmin()) {
log.warn("Spawning as user isn't implemented; starting process with elevation instead");
}
ShellUtilities.execute(args.toArray(new String[args.size()]));
}
}

View File

@@ -0,0 +1,97 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer;
import com.sun.jna.platform.win32.*;
import qz.utils.WindowsUtilities;
/**
* Windows XP-compatible special folder's wrapper for JNA
*
*/
public enum WindowsSpecialFolders {
ADMIN_TOOLS(ShlObj.CSIDL_ADMINTOOLS, KnownFolders.FOLDERID_AdminTools),
STARTUP_ALT(ShlObj.CSIDL_ALTSTARTUP, KnownFolders.FOLDERID_Startup),
ROAMING_APPDATA(ShlObj.CSIDL_APPDATA, KnownFolders.FOLDERID_RoamingAppData),
RECYCLING_BIN(ShlObj.CSIDL_BITBUCKET, KnownFolders.FOLDERID_RecycleBinFolder),
CD_BURNING(ShlObj.CSIDL_CDBURN_AREA, KnownFolders.FOLDERID_CDBurning),
COMMON_ADMIN_TOOLS(ShlObj.CSIDL_COMMON_ADMINTOOLS, KnownFolders.FOLDERID_CommonAdminTools),
COMMON_STARTUP_ALT(ShlObj.CSIDL_COMMON_ALTSTARTUP, KnownFolders.FOLDERID_CommonStartup),
PROGRAM_DATA(ShlObj.CSIDL_COMMON_APPDATA, KnownFolders.FOLDERID_ProgramData),
PUBLIC_DESKTOP(ShlObj.CSIDL_COMMON_DESKTOPDIRECTORY, KnownFolders.FOLDERID_PublicDesktop),
PUBLIC_DOCUMENTS(ShlObj.CSIDL_COMMON_DOCUMENTS, KnownFolders.FOLDERID_PublicDocuments),
COMMON_FAVORITES(ShlObj.CSIDL_COMMON_FAVORITES, KnownFolders.FOLDERID_Favorites),
COMMON_MUSIC(ShlObj.CSIDL_COMMON_MUSIC, KnownFolders.FOLDERID_PublicMusic),
COMMON_OEM_LINKS(ShlObj.CSIDL_COMMON_OEM_LINKS, KnownFolders.FOLDERID_CommonOEMLinks),
COMMON_PICTURES(ShlObj.CSIDL_COMMON_PICTURES, KnownFolders.FOLDERID_PublicPictures),
COMMON_PROGRAMS(ShlObj.CSIDL_COMMON_PROGRAMS, KnownFolders.FOLDERID_CommonPrograms),
COMMON_START_MENU(ShlObj.CSIDL_COMMON_STARTMENU, KnownFolders.FOLDERID_CommonStartMenu),
COMMON_STARTUP(ShlObj.CSIDL_COMMON_STARTUP, KnownFolders.FOLDERID_CommonStartup),
COMMON_TEMPLATES(ShlObj.CSIDL_COMMON_TEMPLATES, KnownFolders.FOLDERID_CommonTemplates),
COMMON_VIDEO(ShlObj.CSIDL_COMMON_VIDEO, KnownFolders.FOLDERID_PublicVideos),
COMPUTERS_NEAR_ME(ShlObj.CSIDL_COMPUTERSNEARME, KnownFolders.FOLDERID_NetworkFolder),
CONNECTIONS_FOLDER(ShlObj.CSIDL_CONNECTIONS, KnownFolders.FOLDERID_ConnectionsFolder),
CONTROL_PANEL(ShlObj.CSIDL_CONTROLS, KnownFolders.FOLDERID_ControlPanelFolder),
COOKIES(ShlObj.CSIDL_COOKIES, KnownFolders.FOLDERID_Cookies),
DESKTOP_VIRTUAL(ShlObj.CSIDL_DESKTOP, KnownFolders.FOLDERID_Desktop),
DESKTOP(ShlObj.CSIDL_DESKTOPDIRECTORY, KnownFolders.FOLDERID_Desktop),
COMPUTER_FOLDER(ShlObj.CSIDL_DRIVES, KnownFolders.FOLDERID_ComputerFolder),
FAVORITES(ShlObj.CSIDL_FAVORITES, KnownFolders.FOLDERID_Favorites),
FONTS(ShlObj.CSIDL_FONTS, KnownFolders.FOLDERID_Fonts),
HISTORY(ShlObj.CSIDL_HISTORY, KnownFolders.FOLDERID_History),
INTERNET_FOLDER(ShlObj.CSIDL_INTERNET, KnownFolders.FOLDERID_InternetFolder),
INTERNET_CACHE(ShlObj.CSIDL_INTERNET_CACHE, KnownFolders.FOLDERID_InternetCache),
LOCAL_APPDATA(ShlObj.CSIDL_LOCAL_APPDATA, KnownFolders.FOLDERID_LocalAppData),
MY_DOCUMENTS(ShlObj.CSIDL_MYDOCUMENTS, KnownFolders.FOLDERID_Documents),
MY_MUSIC(ShlObj.CSIDL_MYMUSIC, KnownFolders.FOLDERID_Music),
MY_PICTURES(ShlObj.CSIDL_MYPICTURES, KnownFolders.FOLDERID_Pictures),
MY_VIDEOS(ShlObj.CSIDL_MYVIDEO, KnownFolders.FOLDERID_Videos),
NETWORK_NEIGHBORHOOD(ShlObj.CSIDL_NETHOOD, KnownFolders.FOLDERID_NetHood),
NETWORK_FOLDER(ShlObj.CSIDL_NETWORK, KnownFolders.FOLDERID_NetworkFolder),
PERSONAL_FOLDDER(ShlObj.CSIDL_PERSONAL, KnownFolders.FOLDERID_Documents),
PRINTERS(ShlObj.CSIDL_PRINTERS, KnownFolders.FOLDERID_PrintersFolder),
PRINTING_NEIGHBORHOODD(ShlObj.CSIDL_PRINTHOOD, KnownFolders.FOLDERID_PrintHood),
PROFILE_FOLDER(ShlObj.CSIDL_PROFILE, KnownFolders.FOLDERID_Profile),
PROGRAM_FILES(ShlObj.CSIDL_PROGRAM_FILES, KnownFolders.FOLDERID_ProgramFiles),
PROGRAM_FILESX86(ShlObj.CSIDL_PROGRAM_FILESX86, KnownFolders.FOLDERID_ProgramFilesX86),
PROGRAM_FILES_COMMON(ShlObj.CSIDL_PROGRAM_FILES_COMMON, KnownFolders.FOLDERID_ProgramFilesCommon),
PROGRAM_FILES_COMMONX86(ShlObj.CSIDL_PROGRAM_FILES_COMMONX86, KnownFolders.FOLDERID_ProgramFilesCommonX86),
PROGRAMS(ShlObj.CSIDL_PROGRAMS, KnownFolders.FOLDERID_Programs),
RECENT(ShlObj.CSIDL_RECENT, KnownFolders.FOLDERID_Recent),
RESOURCES(ShlObj.CSIDL_RESOURCES, KnownFolders.FOLDERID_ResourceDir),
RESOURCES_LOCALIZED(ShlObj.CSIDL_RESOURCES_LOCALIZED, KnownFolders.FOLDERID_LocalizedResourcesDir),
SEND_TO(ShlObj.CSIDL_SENDTO, KnownFolders.FOLDERID_SendTo),
START_MENU(ShlObj.CSIDL_STARTMENU, KnownFolders.FOLDERID_StartMenu),
STARTUP(ShlObj.CSIDL_STARTUP, KnownFolders.FOLDERID_Startup),
SYSTEM(ShlObj.CSIDL_SYSTEM, KnownFolders.FOLDERID_System),
SYSTEMX86(ShlObj.CSIDL_SYSTEMX86, KnownFolders.FOLDERID_SystemX86),
TEMPLATES(ShlObj.CSIDL_TEMPLATES, KnownFolders.FOLDERID_Templates),
WINDOWS(ShlObj.CSIDL_WINDOWS, KnownFolders.FOLDERID_Windows);
private int csidl;
private Guid.GUID guid;
WindowsSpecialFolders(int csidl, Guid.GUID guid) {
this.csidl = csidl;
this.guid = guid;
}
public String getPath() {
if(WindowsUtilities.isWindowsXP()) {
return Shell32Util.getSpecialFolderPath(csidl, false);
}
return Shell32Util.getKnownFolderPath(guid);
}
@Override
public String toString() {
return getPath();
}
}

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=%ABOUT_TITLE%
Exec="%COMMAND%" %PARAM%
Path=%DESTINATION%
Icon=%DESTINATION%/%LINUX_ICON%
MimeType=application/x-qz;x-scheme-handler/qz;
Terminal=false

View File

@@ -0,0 +1,2 @@
# %ABOUT_TITLE% usb override settings
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>%PACKAGE_NAME%</string>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key><false/>
<key>AfterInitialDemand</key><false/>
</dict>
<key>RunAtLoad</key><true/>
<key>ProgramArguments</key>
<array>
<string>%COMMAND%</string>
<string>%PARAM%</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,147 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.util.Calendar;
import java.util.Locale;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import qz.common.Constants;
import qz.utils.SystemUtilities;
import static qz.installer.certificate.KeyPairWrapper.Type.*;
public class CertificateChainBuilder {
public static final String[] DEFAULT_HOSTNAMES = {"localhost", "localhost.qz.io" };
private static int KEY_SIZE = 2048;
public static int CA_CERT_AGE = 7305; // 20 years
public static int SSL_CERT_AGE = 825; // Per https://support.apple.com/HT210176
private String[] hostNames;
public CertificateChainBuilder(String ... hostNames) {
Security.addProvider(new BouncyCastleProvider());
if(hostNames.length > 0) {
this.hostNames = hostNames;
} else {
this.hostNames = DEFAULT_HOSTNAMES;
}
}
public KeyPairWrapper createCaCert() throws IOException, GeneralSecurityException, OperatorException {
KeyPair keyPair = createRsaKey();
X509v3CertificateBuilder builder = createX509Cert(keyPair, CA_CERT_AGE, hostNames);
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(1))
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign + KeyUsage.cRLSign))
.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
// Signing
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate());
X509CertificateHolder certHolder = builder.build(sign);
// Convert to java-friendly format
return new KeyPairWrapper(CA, keyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
}
public KeyPairWrapper createSslCert(KeyPairWrapper caKeyPairWrapper) throws IOException, GeneralSecurityException, OperatorException {
KeyPair sslKeyPair = createRsaKey();
X509v3CertificateBuilder builder = createX509Cert(sslKeyPair, SSL_CERT_AGE, hostNames);
JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils();
builder.addExtension(Extension.authorityKeyIdentifier, false, utils.createAuthorityKeyIdentifier(caKeyPairWrapper.getCert()))
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature + KeyUsage.keyEncipherment))
.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}))
.addExtension(Extension.subjectAlternativeName, false, buildSan(hostNames))
.addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(sslKeyPair.getPublic()));
// Signing
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caKeyPairWrapper.getKey());
X509CertificateHolder certHolder = builder.build(sign);
// Convert to java-friendly format
return new KeyPairWrapper(SSL, sslKeyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
}
private static KeyPair createRsaKey() throws GeneralSecurityException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
private static X509v3CertificateBuilder createX509Cert(KeyPair keyPair, int age, String ... hostNames) {
String cn = hostNames.length > 0? hostNames[0]:DEFAULT_HOSTNAMES[0];
X500Name name = new X500NameBuilder()
.addRDN(BCStyle.C, Constants.ABOUT_COUNTRY)
.addRDN(BCStyle.ST, Constants.ABOUT_STATE)
.addRDN(BCStyle.L, Constants.ABOUT_CITY)
.addRDN(BCStyle.O, Constants.ABOUT_COMPANY)
.addRDN(BCStyle.OU, Constants.ABOUT_COMPANY)
.addRDN(BCStyle.EmailAddress, Constants.ABOUT_EMAIL)
.addRDN(BCStyle.CN, cn)
.build();
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Calendar notBefore = Calendar.getInstance(Locale.ENGLISH);
Calendar notAfter = Calendar.getInstance(Locale.ENGLISH);
notBefore.add(Calendar.DAY_OF_YEAR, -1);
notAfter.add(Calendar.DAY_OF_YEAR, age - 1);
SystemUtilities.swapLocale();
X509v3CertificateBuilder x509builder = new JcaX509v3CertificateBuilder(name, serial, notBefore.getTime(), notAfter.getTime(), name, keyPair.getPublic());
SystemUtilities.restoreLocale();
return x509builder;
}
/**
* Builds subjectAlternativeName extension; iterates and detects IPv4 or hostname
*/
private static GeneralNames buildSan(String ... hostNames) {
GeneralName[] gn = new GeneralName[hostNames.length];
for (int i = 0; i < hostNames.length; i++) {
int gnType = isIp(hostNames[i]) ? GeneralName.iPAddress : GeneralName.dNSName;
gn[i] = new GeneralName(gnType, hostNames[i]);
}
return GeneralNames.getInstance(new DERSequence(gn));
}
private static boolean isIp(String ip) {
try {
String[] split = ip.split("\\.");
if (split.length != 4) return false;
for (int i = 0; i < 4; ++i) {
int p = Integer.parseInt(split[i]);
if (p > 255 || p < 0) return false;
}
return true;
} catch (Exception ignore) {}
return false;
}
}

View File

@@ -0,0 +1,478 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.OperatorException;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ArgValue;
import qz.utils.FileUtilities;
import qz.utils.MacUtilities;
import qz.utils.SystemUtilities;
import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.*;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import static qz.utils.FileUtilities.*;
import static qz.installer.certificate.KeyPairWrapper.Type.*;
/**
* Stores and maintains reading and writing of certificate related files
*/
public class CertificateManager {
static List<Path> SAVE_LOCATIONS = new ArrayList<>();
static {
// Workaround for JDK-8266929
// See also https://github.com/qzind/tray/issues/814
SystemUtilities.clearAlgorithms();
// Skip shared location if running from IDE or build directory
// Prevents corrupting the version installed per https://github.com/qzind/tray/issues/1200
if(SystemUtilities.isJar() && SystemUtilities.isInstalled()) {
// Skip install location if running from sandbox (must remain sealed)
if(!SystemUtilities.isMac() || !MacUtilities.isSandboxed()) {
SAVE_LOCATIONS.add(SystemUtilities.getJarParentPath());
}
SAVE_LOCATIONS.add(SHARED_DIR);
}
SAVE_LOCATIONS.add(USER_DIR);
}
private static final Logger log = LogManager.getLogger(CertificateManager.class);
public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12";
public static String DEFAULT_KEYSTORE_EXTENSION = ".p12";
public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt";
private static int DEFAULT_PASSWORD_BITS = 100;
private boolean needsInstall;
private SslContextFactory.Server sslContextFactory;
private KeyPairWrapper sslKeyPair;
private KeyPairWrapper caKeyPair;
private Properties properties;
private char[] password;
/**
* For internal certs
*/
public CertificateManager(boolean forceNew, String ... hostNames) throws IOException, GeneralSecurityException, OperatorException {
Security.addProvider(new BouncyCastleProvider());
sslKeyPair = new KeyPairWrapper(SSL);
caKeyPair = new KeyPairWrapper(CA);
if (!forceNew) {
// order is important: ssl, ca
properties = loadProperties(sslKeyPair, caKeyPair);
}
if(properties == null) {
log.warn("Warning, SSL properties won't be loaded from disk... we'll try to create them...");
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
caKeyPair = cb.createCaCert();
sslKeyPair = cb.createSslCert(caKeyPair);
// Create CA
properties = createKeyStore(CA)
.writeCert(CA)
.writeKeystore(null, CA);
// Create SSL
properties = createKeyStore(SSL)
.writeCert(SSL)
.writeKeystore(properties, SSL);
// Save properties
saveProperties();
}
}
/**
* For trusted PEM-formatted certs
*/
public CertificateManager(File trustedPemKey, File trustedPemCert) throws Exception {
Security.addProvider(new BouncyCastleProvider());
needsInstall = false;
sslKeyPair = new KeyPairWrapper(SSL);
// Assumes ssl/privkey.pem, ssl/fullchain.pem
properties = createTrustedKeystore(trustedPemKey, trustedPemCert)
.writeKeystore(properties, SSL);
// Save properties
saveProperties();
}
/**
* For trusted PKCS12-formatted certs
*/
public CertificateManager(File pkcs12File, char[] password) throws Exception {
Security.addProvider(new BouncyCastleProvider());
needsInstall = false;
sslKeyPair = new KeyPairWrapper(SSL);
// Assumes direct pkcs12 import
this.password = password;
sslKeyPair.init(pkcs12File, password);
// Save it back, but to a location we can find
properties = writeKeystore(null, SSL);
// Save properties
saveProperties();
}
public void renewCertChain(String ... hostNames) throws Exception {
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
sslKeyPair = cb.createSslCert(caKeyPair);
createKeyStore(SSL).writeKeystore(properties, SSL);
reloadSslContextFactory();
}
public KeyPairWrapper getSslKeyPair() {
return sslKeyPair;
}
public KeyPairWrapper getCaKeyPair() {
return caKeyPair;
}
public KeyPairWrapper getKeyPair(KeyPairWrapper.Type type) {
switch(type) {
case SSL:
return sslKeyPair;
case CA:
default:
return caKeyPair;
}
}
public KeyPairWrapper getKeyPair(String alias) {
for(KeyPairWrapper.Type type : KeyPairWrapper.Type.values()) {
if (KeyPairWrapper.getAlias(type).equalsIgnoreCase(alias)) {
return getKeyPair(type);
}
}
return getKeyPair(KeyPairWrapper.Type.CA);
}
public Properties getProperties() {
return properties;
}
private char[] getPassword() {
if (password == null) {
if(caKeyPair != null && caKeyPair.getPassword() != null) {
// Reuse existing
password = caKeyPair.getPassword();
} else {
// Create new
BigInteger bi = new BigInteger(DEFAULT_PASSWORD_BITS, new SecureRandom());
password = bi.toString(16).toCharArray();
log.info("Created a random {} bit password: {}", DEFAULT_PASSWORD_BITS, new String(password));
}
}
return password;
}
public SslContextFactory.Server configureSslContextFactory() {
sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
return sslContextFactory;
}
public void reloadSslContextFactory() throws Exception {
if(isSslActive()) {
sslContextFactory.reload(sslContextFactory -> {
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
});
} else {
log.warn("SSL isn't active, can't reload");
}
}
public boolean isSslActive() {
return sslContextFactory != null;
}
public boolean needsInstall() {
return needsInstall;
}
public CertificateManager createKeyStore(KeyPairWrapper.Type type) throws IOException, GeneralSecurityException {
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
KeyStore keyStore = KeyStore.getInstance(DEFAULT_KEYSTORE_FORMAT);
keyStore.load(null, password);
List<X509Certificate> chain = new ArrayList<>();
chain.add(keyPair.getCert());
// Add ca to ssl cert chain
if (keyPair.getType() == SSL) {
chain.add(caKeyPair.getCert());
}
keyStore.setEntry(caKeyPair.getAlias(), new KeyStore.TrustedCertificateEntry(caKeyPair.getCert()), null);
keyStore.setKeyEntry(keyPair.getAlias(), keyPair.getKey(), getPassword(), chain.toArray(new X509Certificate[chain.size()]));
keyPair.init(keyStore, getPassword());
return this;
}
public CertificateManager createTrustedKeystore(File p12Store, String password) throws Exception {
sslKeyPair = new KeyPairWrapper(SSL);
sslKeyPair.init(p12Store, password.toCharArray());
return this;
}
public CertificateManager createTrustedKeystore(File pemKey, File pemCert) throws Exception {
sslKeyPair = new KeyPairWrapper(SSL);
// Private Key
PEMParser pem = new PEMParser(new FileReader(pemKey));
Object parsedObject = pem.readObject();
PrivateKeyInfo privateKeyInfo = parsedObject instanceof PEMKeyPair ? ((PEMKeyPair)parsedObject).getPrivateKeyInfo() : (PrivateKeyInfo)parsedObject;
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
KeyFactory factory = KeyFactory.getInstance("RSA");
PrivateKey key = factory.generatePrivate(privateKeySpec);
List<X509Certificate> certs = new ArrayList<>();
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
if(certHolder != null) {
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
}
// Certificate
pem = new PEMParser(new FileReader(pemCert));
while((certHolder = (X509CertificateHolder)pem.readObject()) != null) {
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
}
// Keystore
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null);
for (int i = 0; i < certs.size(); i++) {
ks.setCertificateEntry(sslKeyPair.getAlias() + "_" + i, certs.get(i));
}
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null);
keyStore.setKeyEntry(sslKeyPair.getAlias(), key, getPassword(), certs.toArray(new X509Certificate[certs.size()]));
sslKeyPair.init(keyStore, getPassword());
return this;
}
public static void writeCert(X509Certificate data, File dest) throws IOException {
// PEMWriter doesn't always clear the file, explicitly delete it, see issue #796
if(dest.exists()) {
dest.delete();
}
JcaMiscPEMGenerator cert = new JcaMiscPEMGenerator(data);
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(dest.toPath(), StandardOpenOption.CREATE)));
writer.writeObject(cert.generate());
writer.close();
FileUtilities.inheritParentPermissions(dest.toPath());
log.info("Wrote Cert: \"{}\"", dest);
}
public CertificateManager writeCert(KeyPairWrapper.Type type) throws IOException {
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
File certFile = new File(getWritableLocation("ssl"), keyPair.getAlias() + DEFAULT_CERTIFICATE_EXTENSION);
writeCert(keyPair.getCert(), certFile);
FileUtilities.inheritParentPermissions(certFile.toPath());
if(keyPair.getType() == CA) {
needsInstall = true;
}
return this;
}
public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) throws GeneralSecurityException, IOException {
File sslDir = getWritableLocation("ssl");
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
File keyFile = new File(sslDir, keyPair.getAlias() + DEFAULT_KEYSTORE_EXTENSION);
keyPair.getKeyStore().store(Files.newOutputStream(keyFile.toPath(), StandardOpenOption.CREATE), getPassword());
FileUtilities.inheritParentPermissions(keyFile.toPath());
log.info("Wrote {} Key: \"{}\"", DEFAULT_KEYSTORE_FORMAT, keyFile);
if (props == null) {
props = new Properties();
}
props.putIfAbsent(String.format("%s.keystore", keyPair.propsPrefix()), keyFile.toString());
props.putIfAbsent(String.format("%s.storepass", keyPair.propsPrefix()), new String(getPassword()));
props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias());
if (keyPair.getType() == SSL) {
props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), ArgValue.SECURITY_WSS_HOST.getDefaultVal());
}
return props;
}
public static File getWritableLocation(String ... suffixes) throws IOException {
// Get an array of preferred directories
ArrayList<Path> locs = new ArrayList<>();
if (suffixes.length == 0) {
locs.addAll(SAVE_LOCATIONS);
// Last, fallback on a directory we won't ever see again :/
locs.add(TEMP_DIR);
} else {
// Same as above, but with suffixes added (usually "ssl"), skipping the install location
for(Path saveLocation : SAVE_LOCATIONS) {
if(!saveLocation.equals(SystemUtilities.getJarParentPath())) {
locs.add(Paths.get(saveLocation.toString(), suffixes));
}
}
// Last, fallback on a directory we won't ever see again :/
locs.add(Paths.get(TEMP_DIR.toString(), suffixes));
}
// Find a suitable write location
File path;
for(Path loc : locs) {
if (loc == null) continue;
boolean isPreferred = locs.indexOf(loc) == 0;
path = loc.toFile();
path.mkdirs();
if (path.canWrite()) {
log.debug("Writing to {}", loc);
if(!isPreferred) {
log.warn("Warning, {} isn't the preferred write location, but we'll use it anyway", loc);
}
return path;
} else {
log.debug("Can't write to {}, trying the next...", loc);
}
}
throw new IOException("Can't find a suitable write location. SSL will fail.");
}
public static Properties loadProperties(KeyPairWrapper... keyPairs) {
log.info("Try to find SSL properties file...");
Properties props = null;
for(Path loc : SAVE_LOCATIONS) {
if (loc == null) continue;
try {
for(KeyPairWrapper keyPair : keyPairs) {
props = loadKeyPair(keyPair, loc, props);
}
// We've loaded without Exception, return
log.info("Found {}/{}.properties", loc, Constants.PROPS_FILE);
return props;
} catch(Exception ignore) {
log.warn("Properties couldn't be loaded at {}, trying fallback...", loc, ignore);
}
}
log.info("Could not get SSL properties from file.");
return null;
}
public static Properties loadKeyPair(KeyPairWrapper keyPair, Path parent, Properties existing) throws Exception {
Properties props;
if (existing == null) {
FileInputStream fis = null;
try {
props = new Properties();
props.load(fis = new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties")));
} finally {
if(fis != null) fis.close();
}
} else {
props = existing;
}
String ks = props.getProperty(String.format("%s.keystore", keyPair.propsPrefix()));
String pw = props.getProperty(String.format("%s.storepass", keyPair.propsPrefix()), "");
if(ks == null || ks.trim().isEmpty()) {
if(keyPair.getType() == SSL) {
throw new IOException("Missing wss.keystore entry");
} else {
// CA is only needed for internal certs, return
return props;
}
}
File ksFile = Paths.get(ks).isAbsolute()? new File(ks):new File(parent.toFile(), ks);
if (ksFile.exists()) {
keyPair.init(ksFile, pw.toCharArray());
return props;
}
return null;
}
private void saveProperties() throws IOException {
File propsFile = new File(getWritableLocation(), Constants.PROPS_FILE + ".properties");
Installer.persistProperties(propsFile, properties); // checks for props from previous install
properties.store(new FileOutputStream(propsFile), null);
FileUtilities.inheritParentPermissions(propsFile.toPath());
log.info("Successfully created SSL properties file: {}", propsFile);
}
public static boolean emailMatches(X509Certificate cert) {
return emailMatches(cert, false);
}
public static boolean emailMatches(X509Certificate cert, boolean quiet) {
try {
X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
RDN[] emailNames = x500name.getRDNs(BCStyle.E);
for(RDN emailName : emailNames) {
AttributeTypeAndValue first = emailName.getFirst();
if (first != null && first.getValue() != null && Constants.ABOUT_EMAIL.equals(first.getValue().toString())) {
if(!quiet) {
log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, ExpiryTask.CertProvider.INTERNAL);
}
return true;
}
}
}
catch(Exception ignore) {}
if(!quiet) {
log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL);
}
return false;
}
}

View File

@@ -0,0 +1,295 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import static qz.utils.FileUtilities.*;
public class ExpiryTask extends TimerTask {
private static final Logger log = LogManager.getLogger(CertificateManager.class);
public static final int DEFAULT_INITIAL_DELAY = 60 * 1000; // 1 minute
public static final int DEFAULT_CHECK_FREQUENCY = 3600 * 1000; // 1 hour
private static final int DEFAULT_GRACE_PERIOD_DAYS = 5;
private enum ExpiryState {VALID, EXPIRING, EXPIRED, MANAGED}
public enum CertProvider {
INTERNAL(Constants.ABOUT_COMPANY + ".*"),
LETS_ENCRYPT("Let's Encrypt.*"),
CA_CERT_ORG("CA Cert Signing.*"),
UNKNOWN;
String[] patterns;
CertProvider(String ... regexPattern) {
this.patterns = regexPattern;
}
}
private Timer timer;
private CertificateManager certificateManager;
private String[] hostNames;
private CertProvider certProvider;
public ExpiryTask(CertificateManager certificateManager) {
super();
this.certificateManager = certificateManager;
this.hostNames = parseHostNames();
this.certProvider = findCertProvider();
}
@Override
public void run() {
// Check for expiration
ExpiryState state = getExpiry(certificateManager.getSslKeyPair().getCert());
switch(state) {
case EXPIRING:
case EXPIRED:
log.info("Certificate ExpiryState {}, renewing/reloading...", state);
switch(certProvider) {
case INTERNAL:
if(renewInternalCert()) {
getExpiry();
}
break;
case CA_CERT_ORG:
case LETS_ENCRYPT:
if(renewExternalCert(certProvider)) {
getExpiry();
}
break;
case UNKNOWN:
default:
log.warn("Certificate can't be renewed/reloaded; ExpiryState: {}, CertProvider: {}", state, certProvider);
}
case VALID:
default:
}
}
public boolean renewInternalCert() {
try {
log.info("Requesting a new SSL certificate from {} ...", certificateManager.getCaKeyPair().getAlias());
certificateManager.renewCertChain(hostNames);
log.info("New SSL certificate created. Reloading SslContextFactory...");
certificateManager.reloadSslContextFactory();
log.info("Reloaded SSL successfully.");
return true;
}
catch(Exception e) {
log.error("Could not reload SSL certificate", e);
}
return false;
}
public ExpiryState getExpiry() {
return getExpiry(certificateManager.getSslKeyPair().getCert());
}
/**
* Returns true if the SSL certificate is generated by QZ Tray and expires inside the GRACE_PERIOD.
* GRACE_PERIOD is preferred for scheduling the renewals in advance, such as non-peak hours
*/
public static ExpiryState getExpiry(X509Certificate cert) {
// Invalid
if (cert == null) {
log.error("Can't check for expiration, certificate is missing.");
return ExpiryState.EXPIRED;
}
Date expireDate = cert.getNotAfter();
Calendar now = Calendar.getInstance(Locale.ENGLISH);
Calendar expires = Calendar.getInstance(Locale.ENGLISH);
expires.setTime(expireDate);
// Expired
if (now.after(expires)) {
log.info("SSL certificate has expired {}. It must be renewed immediately.", SystemUtilities.toISO(expireDate));
return ExpiryState.EXPIRED;
}
// Expiring
expires.add(Calendar.DAY_OF_YEAR, -DEFAULT_GRACE_PERIOD_DAYS);
if (now.after(expires)) {
log.info("SSL certificate will expire in less than {} days: {}", DEFAULT_GRACE_PERIOD_DAYS, SystemUtilities.toISO(expireDate));
return ExpiryState.EXPIRING;
}
// Valid
int days = (int)Math.round((expireDate.getTime() - new Date().getTime()) / (double)86400000);
log.info("SSL certificate is still valid for {} more days: {}. We'll make a new one automatically when needed.", days, SystemUtilities.toISO(expireDate));
return ExpiryState.VALID;
}
public void schedule() {
schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY);
}
public void schedule(int delayMillis, int freqMillis) {
if(timer != null) {
timer.cancel();
timer.purge();
}
timer = new Timer();
timer.scheduleAtFixedRate(this, delayMillis, freqMillis);
}
public String[] parseHostNames() {
return parseHostNames(certificateManager.getSslKeyPair().getCert());
}
public CertProvider findCertProvider() {
return findCertProvider(certificateManager.getSslKeyPair().getCert());
}
public static CertProvider findCertProvider(X509Certificate cert) {
// Internal certs use CN=localhost, trust email instead
if (CertificateManager.emailMatches(cert)) {
return CertProvider.INTERNAL;
}
String providerDN;
// check registered patterns to classify certificate
if(cert.getIssuerDN() != null && (providerDN = cert.getIssuerDN().getName()) != null) {
String cn = null;
try {
// parse issuer's DN
LdapName ldapName = new LdapName(providerDN);
for(Rdn rdn : ldapName.getRdns()) {
if(rdn.getType().equalsIgnoreCase("CN")) {
cn = (String)rdn.getValue();
break;
}
}
// compare cn to our pattern
if(cn != null) {
for(CertProvider provider : CertProvider.values()) {
for(String pattern : provider.patterns) {
if (cn.matches(pattern)) {
log.warn("Cert issuer detected as {}", provider.name());
return provider;
}
}
}
}
} catch(InvalidNameException ignore) {}
}
log.warn("A valid issuer couldn't be found, we won't know how to renew this cert when it expires");
return CertProvider.UNKNOWN;
}
public static String[] parseHostNames(X509Certificate cert) {
// Cache the SAN hosts for recreation
List<String> hostNameList = new ArrayList<>();
try {
Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
if (altNames != null) {
for(List<?> altName : altNames) {
if(altName.size()< 1) continue;
switch((Integer)altName.get(0)) {
case GeneralName.dNSName:
case GeneralName.iPAddress:
Object data = altName.get(1);
if (data instanceof String) {
hostNameList.add(((String)data));
}
break;
default:
}
}
} else {
log.error("getSubjectAlternativeNames is null?");
}
log.debug("Parsed hostNames: {}", String.join(", ", hostNameList));
} catch(CertificateException e) {
log.warn("Can't parse hostNames from this cert. Cert renewals will contain default values instead");
}
return hostNameList.toArray(new String[hostNameList.size()]);
}
public boolean renewExternalCert(CertProvider externalProvider) {
switch(externalProvider) {
case LETS_ENCRYPT:
return renewLetsEncryptCert(externalProvider);
case CA_CERT_ORG:
default:
log.error("Cert renewal for {} is not implemented", externalProvider);
}
return false;
}
private boolean renewLetsEncryptCert(CertProvider externalProvider) {
try {
File storagePath = CertificateManager.getWritableLocation("ssl");
// cerbot is much simpler than acme, let's use it
Path root = Paths.get(SHARED_DIR.toString(), "letsencrypt", "config");
log.info("Attempting to renew {}. Assuming certs are installed in {}...", externalProvider, root);
List<String> cmds = new ArrayList(Arrays.asList("certbot", "--force-renewal", "certonly"));
cmds.add("--standalone");
cmds.add("--config-dir");
String config = Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "config").toString();
cmds.add(config);
cmds.add("--logs-dir");
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "logs").toString());
cmds.add("--work-dir");
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt").toString());
// append dns names
for(String hostName : hostNames) {
cmds.add("-d");
cmds.add(hostName);
}
if (ShellUtilities.execute(cmds.toArray(new String[cmds.size()]))) {
// Assume the cert is stored in a folder called "letsencrypt/config/live/<domain>"
Path keyPath = Paths.get(config, "live", hostNames[0], "privkey.pem");
Path certPath = Paths.get(config, "live", hostNames[0], "fullchain.pem"); // fullchain required
certificateManager.createTrustedKeystore(keyPath.toFile(), certPath.toFile());
log.info("Files imported, converted and saved. Reloading SslContextFactory...");
certificateManager.reloadSslContextFactory();
log.info("Reloaded SSL successfully.");
return true;
} else {
log.warn("Something went wrong renewing the LetsEncrypt certificate. Please run the certbot command manually to learn more.");
}
} catch(Exception e) {
log.error("Error renewing/reloading LetsEncrypt cert", e);
}
return false;
}
}

View File

@@ -0,0 +1,130 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import qz.common.Constants;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
/**
* Wrap handling of X509Certificate, PrivateKey and KeyStore conversion
*/
public class KeyPairWrapper {
public enum Type {CA, SSL}
private Type type;
private PrivateKey key;
private char[] password;
private X509Certificate cert;
private KeyStore keyStore; // for SSL
public KeyPairWrapper(Type type) {
this.type = type;
}
public KeyPairWrapper(Type type, KeyPair keyPair, X509Certificate cert) {
this.type = type;
this.key = keyPair.getPrivate();
this.cert = cert;
}
/**
* Load from disk
*/
public void init(File keyFile, char[] password) throws IOException, GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance(keyFile.getName().endsWith(".jks") ? "JKS" : "PKCS12");
keyStore.load(new FileInputStream(keyFile), password);
init(keyStore, password);
}
/**
* Load from memory
*/
public void init(KeyStore keyStore, char[] password) throws GeneralSecurityException {
this.keyStore = keyStore;
KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(password);
KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(getAlias(), param);
// the entry we assume is always wrong for pkcs12 imports, search for it instead
if(entry == null) {
Enumeration<String> enumerator = keyStore.aliases();
while(enumerator.hasMoreElements()) {
String alias = enumerator.nextElement();
if(keyStore.isKeyEntry(alias)) {
this.password = password;
this.key = ((KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, param)).getPrivateKey();
this.cert = (X509Certificate)keyStore.getCertificate(alias);
return;
}
}
throw new GeneralSecurityException("Could not initialize the KeyStore for internal use");
}
this.password = password;
this.key = entry.getPrivateKey();
this.cert = (X509Certificate)keyStore.getCertificate(getAlias());
}
public X509Certificate getCert() {
return cert;
}
public PrivateKey getKey() {
return key;
}
public String getPasswordString() {
return new String(password);
}
public char[] getPassword() {
return password;
}
public static String getAlias(Type type) {
switch(type) {
case SSL:
return Constants.PROPS_FILE; // "qz-tray"
case CA:
default:
return "root-ca";
}
}
public String getAlias() {
return getAlias(getType());
}
public String propsPrefix() {
switch(type) {
case SSL:
return "wss";
case CA:
default:
return "ca";
}
}
public Type getType() {
return type;
}
public KeyStore getKeyStore() {
return keyStore;
}
}

View File

@@ -0,0 +1,365 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.util.encoders.Base64;
import qz.auth.X509Constants;
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ByteUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.UnixUtilities;
import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import static qz.installer.Installer.PrivilegeLevel.*;
/**
* @author Tres Finocchiaro
*/
public class LinuxCertificateInstaller extends NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(LinuxCertificateInstaller.class);
private static final String CA_CERTIFICATES = "/usr/local/share/ca-certificates/";
private static final String CA_CERTIFICATE_NAME = Constants.PROPS_FILE + "-root.crt"; // e.g. qz-tray-root.crt
private static final String PK11_KIT_ID = "pkcs11:id=";
private static String[] NSSDB_URLS = {
// Conventional cert store
"sql:" + System.getenv("HOME") + "/.pki/nssdb/",
// Snap-specific cert stores
"sql:" + System.getenv("HOME") + "/snap/chromium/current/.pki/nssdb/",
"sql:" + System.getenv("HOME") + "/snap/brave/current/.pki/nssdb/",
"sql:" + System.getenv("HOME") + "/snap/opera/current/.pki/nssdb/",
"sql:" + System.getenv("HOME") + "/snap/opera-beta/current/.pki/nssdb/"
};
private Installer.PrivilegeLevel certType;
public LinuxCertificateInstaller(Installer.PrivilegeLevel certType) {
setInstallType(certType);
findCertutil();
}
public Installer.PrivilegeLevel getInstallType() {
return certType;
}
public void setInstallType(Installer.PrivilegeLevel certType) {
this.certType = certType;
if (this.certType == SYSTEM) {
log.warn("Command \"certutil\" (required for certain browsers) needs to run as USER. We'll try again on launch.");
}
}
public boolean remove(List<String> idList) {
boolean success = true;
if(certType == SYSTEM) {
boolean first = distrustUsingUpdateCaCertificates(idList);
boolean second = distrustUsingTrustAnchor(idList);
success = first || second;
} else {
for(String nickname : idList) {
for(String nssdb : NSSDB_URLS) {
success = success && ShellUtilities.execute("certutil", "-d", nssdb, "-D", "-n", nickname);
}
}
}
return success;
}
public List<String> find() {
ArrayList<String> nicknames = new ArrayList<>();
if(certType == SYSTEM) {
nicknames = findUsingTrustAnchor();
nicknames.addAll(findUsingUsingUpdateCaCert());
} else {
try {
for(String nssdb : NSSDB_URLS) {
Process p = Runtime.getRuntime().exec(new String[] {"certutil", "-d", nssdb, "-L"});
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while((line = in.readLine()) != null) {
if (line.startsWith(Constants.ABOUT_COMPANY + " ")) {
nicknames.add(Constants.ABOUT_COMPANY);
break; // Stop reading input; nicknames can't appear more than once
}
}
in.close();
}
}
catch(IOException e) {
log.warn("Could not get certificate nicknames", e);
}
}
return nicknames;
}
public boolean verify(File ignore) { return true; } // no easy way to validate a cert, assume it's installed
public boolean add(File certFile) {
boolean success = true;
if(certType == SYSTEM) {
// Attempt two common methods for installing the SSL certificate
File systemCertFile;
boolean first = (systemCertFile = trustUsingUpdateCaCertificates(certFile)) != null;
boolean second = trustUsingTrustAnchor(systemCertFile, certFile);
success = first || second;
} else if(certType == USER) {
// Install certificate to local profile using "certutil"
for(String nssdb : NSSDB_URLS) {
String[] parts = nssdb.split(":", 2);
if (parts.length > 1) {
File folder = new File(parts[1]);
// If .pki/nssdb doesn't exist yet, don't create it! Per https://github.com/qzind/tray/issues/1003
if(folder.exists() && folder.isDirectory()) {
if (!ShellUtilities.execute("certutil", "-d", nssdb, "-A", "-t", "TC", "-n", Constants.ABOUT_COMPANY, "-i", certFile.getPath())) {
log.warn("Something went wrong creating {}. HTTPS will fail on certain browsers which depend on it.", nssdb);
success = false;
}
}
}
}
}
return success;
}
private boolean findCertutil() {
boolean installed = ShellUtilities.execute("which", "certutil");
if (!installed) {
if (certType == SYSTEM && promptCertutil()) {
if(UnixUtilities.isUbuntu() || UnixUtilities.isDebian()) {
installed = ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools");
} else if(UnixUtilities.isFedora()) {
installed = ShellUtilities.execute("dnf", "install", "-y", "nss-tools");
}
}
}
if(!installed) {
log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on certain browsers which depend on it.");
}
return installed;
}
private boolean promptCertutil() {
// Assume silent or headless installs want certutil
if(Installer.IS_SILENT || GraphicsEnvironment.isHeadless()) {
return true;
}
try {
SystemUtilities.setSystemLookAndFeel(true);
return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(null, "A critical component, \"certutil\" wasn't found. Attempt to fetch it now?");
} catch(Throwable ignore) {}
return true;
}
/**
* Common technique for installing system-wide certificates on Debian-based systems (Ubuntu, etc.)
*
* This technique is only known to work for select browsers, such as Epiphany. Browsers such as
* Firefox and Chromium require different techniques.
*
* @return Full path to the destination file if successful, otherwise <code>null</code>
*/
private File trustUsingUpdateCaCertificates(File certFile) {
if(hasUpdateCaCertificatesCommand()) {
File destFile = new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME);
log.debug("Copying SYSTEM SSL certificate {} to {}", certFile.getPath(), destFile.getPath());
try {
if (new File(CA_CERTIFICATES).isDirectory()) {
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
FileUtils.copyFile(certFile, destFile, false);
if (destFile.isFile()) {
// Attempt "update-ca-certificates" (Debian)
if (!ShellUtilities.execute("update-ca-certificates")) {
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
} else {
return destFile;
}
}
} else {
log.warn("{} is not a valid directory, skipping", CA_CERTIFICATES);
}
}
catch(IOException e) {
log.warn("Error copying SYSTEM SSL certificate file", e);
}
} else {
log.warn("Skipping SYSTEM SSL certificate install using \"update-ca-certificates\", command missing or invalid");
}
return null;
}
/**
* Common technique for installing system-wide certificates on Fedora-based systems
*
* Uses first existing non-null file provided
*/
private boolean trustUsingTrustAnchor(File ... certFiles) {
if (hasTrustAnchorCommand()) {
for(File certFile : certFiles) {
if (certFile == null || !certFile.exists()) {
continue;
}
// Install certificate to system using "trust anchor" (Fedora)
if (ShellUtilities.execute("trust", "anchor", "--store", certFile.getPath())) {
return true;
} else {
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
}
}
} else {
log.warn("Skipping SYSTEM SSL certificate install using \"trust anchor\", command missing or invalid");
}
return false;
}
private boolean distrustUsingUpdateCaCertificates(List<String> paths) {
if(hasUpdateCaCertificatesCommand()) {
boolean deleted = false;
for(String path : paths) {
// Process files only; not "trust anchor" URIs
if(!path.startsWith(PK11_KIT_ID)) {
File certFile = new File(path);
if (certFile.isFile() && certFile.delete()) {
deleted = true;
} else {
log.warn("SYSTEM SSL certificate {} does not exist, skipping", certFile.getPath());
}
}
}
// Attempt "update-ca-certificates" (Debian)
if(deleted) {
if (ShellUtilities.execute("update-ca-certificates")) {
return true;
} else {
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
}
}
} else {
log.warn("Skipping SYSTEM SSL certificate removal using \"update-ca-certificates\", command missing or invalid");
}
return false;
}
private boolean distrustUsingTrustAnchor(List<String> idList) {
if(hasTrustAnchorCommand()) {
for(String id : idList) {
// only remove by id
if (id.startsWith(PK11_KIT_ID) && !ShellUtilities.execute("trust", "anchor", "--remove", id)) {
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
}
}
} else {
log.warn("Skipping SYSTEM SSL certificate removal using \"trust anchor\", command missing or invalid");
}
return false;
}
/**
* Check for the presence of a QZ certificate in known locations (e.g. /usr/local/share/ca-certificates/
* and return the path if found
*/
private ArrayList<String> findUsingUsingUpdateCaCert() {
ArrayList<String> found = new ArrayList<>();
File[] systemCertFiles = { new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME) };
for(File file : systemCertFiles) {
if(file.isFile()) {
found.add(file.getPath());
}
}
return found;
}
/**
* Find QZ installed certificates in the "trust anchor" by searching by email.
*
* The "trust" utility identifies certificates as URIs:
* Example:
* pkcs11:id=%7C%5D%02%84%13%D4%CC%8A%9B%81%CE%17%1C%2E%29%1E%9C%48%63%42;type=cert
* ... which is an encoded version of the cert's SubjectKeyIdentifier field
* To identify a match:
* 1. Extract all trusted certificates and look for a familiar email address
* 2. If found, construct and store a "trust" compatible URI as the nickname
*/
private ArrayList<String> findUsingTrustAnchor() {
ArrayList<String> uris = new ArrayList<>();
File tempFile = null;
try {
// Temporary location for system certificates
tempFile = File.createTempFile("trust-extract-for-qz-", ".pem");
// Delete before use: "trust extract" requires an empty file
tempFile.delete();
if(ShellUtilities.execute("trust", "extract", "--format", "pem-bundle", tempFile.getPath())) {
BufferedReader reader = new BufferedReader(new FileReader(tempFile));
String line;
StringBuilder base64 = new StringBuilder();
while ((line = reader.readLine()) != null) {
if(line.startsWith(X509Constants.BEGIN_CERT)) {
// Beginning of a new certificate
base64.setLength(0);
} else if(line.startsWith(X509Constants.END_CERT)) {
// End of the existing certificate
byte[] certBytes = Base64.decode(base64.toString());
CertificateFactory factory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes));
if(CertificateManager.emailMatches(cert, true)) {
byte[] extensionValue = cert.getExtensionValue(Extension.subjectKeyIdentifier.getId());
byte[] octets = DEROctetString.getInstance(extensionValue).getOctets();
SubjectKeyIdentifier subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets);
byte[] keyIdentifier = subjectKeyIdentifier.getKeyIdentifier();
String hex = ByteUtilities.bytesToHex(keyIdentifier, true);
String uri = PK11_KIT_ID + hex.replaceAll("(.{2})", "%$1") + ";type=cert";
log.info("Found matching cert: {}", uri);
uris.add(uri);
}
} else {
base64.append(line);
}
}
reader.close();
}
} catch(IOException | CertificateException e) {
log.warn("An error occurred finding preexisting \"trust anchor\" certificates", e);
} finally {
if(tempFile != null && !tempFile.delete()) {
tempFile.deleteOnExit();
}
}
return uris;
}
private boolean hasUpdateCaCertificatesCommand() {
return ShellUtilities.execute("which", "update-ca-certificates");
}
private boolean hasTrustAnchorCommand() {
return ShellUtilities.execute("trust", "anchor", "--help");
}
}

View File

@@ -0,0 +1,91 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ShellUtilities;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class MacCertificateInstaller extends NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(MacCertificateInstaller.class);
public static final String USER_STORE = System.getProperty("user.home") + "/Library/Keychains/login.keychain"; // aka login.keychain-db
public static final String SYSTEM_STORE = "/Library/Keychains/System.keychain";
private String certStore;
public MacCertificateInstaller(Installer.PrivilegeLevel certType) {
setInstallType(certType);
}
public boolean add(File certFile) {
if (certStore.equals(USER_STORE)) {
// This will prompt the user
return ShellUtilities.execute("security", "add-trusted-cert", "-r", "trustRoot", "-k", certStore, certFile.getPath());
} else {
return ShellUtilities.execute("security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", certStore, certFile.getPath());
}
}
public boolean remove(List<String> idList) {
boolean success = true;
for (String certId : idList) {
success = success && ShellUtilities.execute("security", "delete-certificate", "-Z", certId, certStore);
}
return success;
}
public List<String> find() {
ArrayList<String> hashList = new ArrayList<>();
try {
Process p = Runtime.getRuntime().exec(new String[] {"security", "find-certificate", "-a", "-e", Constants.ABOUT_EMAIL, "-Z", certStore});
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
if (line.contains("SHA-1") && line.contains(":")) {
hashList.add(line.split(":", 2)[1].trim());
}
}
in.close();
} catch(IOException e) {
log.warn("Could not get certificate list", e);
}
return hashList;
}
public boolean verify(File certFile) {
return ShellUtilities.execute( "security", "verify-cert", "-c", certFile.getPath());
}
public void setInstallType(Installer.PrivilegeLevel type) {
if (type == Installer.PrivilegeLevel.USER) {
certStore = USER_STORE;
} else {
certStore = SYSTEM_STORE;
}
}
public Installer.PrivilegeLevel getInstallType() {
if (certStore == USER_STORE) {
return Installer.PrivilegeLevel.USER;
} else {
return Installer.PrivilegeLevel.SYSTEM;
}
}
}

View File

@@ -0,0 +1,105 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.installer.Installer;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.security.cert.X509Certificate;
import java.util.List;
public abstract class NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(NativeCertificateInstaller.class);
protected static NativeCertificateInstaller instance;
public static NativeCertificateInstaller getInstance() {
return getInstance(SystemUtilities.isAdmin() ? Installer.PrivilegeLevel.SYSTEM : Installer.PrivilegeLevel.USER);
}
public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) {
if (instance == null) {
switch(SystemUtilities.getOs()) {
case WINDOWS:
instance = new WindowsCertificateInstaller(type);
break;
case MAC:
instance = new MacCertificateInstaller(type);
break;
case LINUX:
default:
instance = new LinuxCertificateInstaller(type);
}
}
return instance;
}
/**
* Install a certificate from memory
*/
public boolean install(X509Certificate cert) {
File certFile = null;
try {
certFile = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
JcaMiscPEMGenerator generator = new JcaMiscPEMGenerator(cert);
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(certFile.toPath(), StandardOpenOption.CREATE)));
writer.writeObject(generator.generate());
writer.close();
return install(certFile);
} catch(IOException e) {
log.warn("Could not install cert from temp file", e);
} finally {
if(certFile != null && !certFile.delete()) {
certFile.deleteOnExit();
}
}
return false;
}
/**
* Install a certificate from disk
*/
public boolean install(File certFile) {
String helper = instance.getClass().getSimpleName();
String store = instance.getInstallType().name();
if(SystemUtilities.isJar()) {
if (remove(find())) {
log.info("Certificate removed from {} store using {}", store, helper);
} else {
log.warn("Could not remove certificate from {} store using {}", store, helper);
}
} else {
log.info("Skipping {} store certificate removal, IDE detected.", store, helper);
}
if (add(certFile)) {
log.info("Certificate added to {} store using {}", store, helper);
return true;
} else {
log.warn("Could not install certificate to {} store using {}", store, helper);
}
return false;
}
public abstract boolean add(File certFile);
public abstract boolean remove(List<String> idList);
public abstract List<String> find();
public abstract boolean verify(File certFile);
public abstract void setInstallType(Installer.PrivilegeLevel certType);
public abstract Installer.PrivilegeLevel getInstallType();
}

View File

@@ -0,0 +1,236 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.Kernel32Util;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.openssl.PEMParser;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
public class WindowsCertificateInstaller extends NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(WindowsCertificateInstaller.class);
private WinCrypt.HCERTSTORE store;
private byte[] certBytes;
private Installer.PrivilegeLevel certType;
public WindowsCertificateInstaller(Installer.PrivilegeLevel certType) {
setInstallType(certType);
}
public boolean add(File certFile) {
log.info("Writing certificate {} to {} store using Crypt32...", certFile, certType);
try {
byte[] bytes = getCertBytes(certFile);
Pointer pointer = new Memory(bytes.length);
pointer.write(0, bytes, 0, bytes.length);
boolean success = Crypt32.INSTANCE.CertAddEncodedCertificateToStore(
openStore(),
WinCrypt.X509_ASN_ENCODING,
pointer,
bytes.length,
Crypt32.CERT_STORE_ADD_REPLACE_EXISTING,
null
);
if(!success) {
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
}
closeStore();
return success;
} catch(IOException e) {
log.warn("An error occurred installing the certificate", e);
} finally {
certBytes = null;
}
return false;
}
private byte[] getCertBytes(File certFile) throws IOException {
if(certBytes == null) {
PEMParser pem = new PEMParser(new FileReader(certFile));
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
certBytes = certHolder.getEncoded();
}
return certBytes;
}
private WinCrypt.HCERTSTORE openStore() {
if(store == null) {
store = openStore(certType);
}
return store;
}
private void closeStore() {
if(store != null && closeStore(store)) {
store = null;
} else {
log.warn("Unable to close {} cert store", certType);
}
}
private static WinCrypt.HCERTSTORE openStore(Installer.PrivilegeLevel certType) {
log.info("Opening {} store using Crypt32...", certType);
WinCrypt.HCERTSTORE store = Crypt32.INSTANCE.CertOpenStore(
Crypt32.CERT_STORE_PROV_SYSTEM,
0,
null,
certType == Installer.PrivilegeLevel.USER ? Crypt32.CERT_SYSTEM_STORE_CURRENT_USER : Crypt32.CERT_SYSTEM_STORE_LOCAL_MACHINE,
"ROOT"
);
if(store == null) {
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
}
return store;
}
private static boolean closeStore(WinCrypt.HCERTSTORE certStore) {
boolean isClosed = Crypt32.INSTANCE.CertCloseStore(
certStore, 0
);
if(!isClosed) {
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
}
return isClosed;
}
public boolean remove(List<String> ignore) {
boolean success = true;
WinCrypt.CERT_CONTEXT hCertContext;
WinCrypt.CERT_CONTEXT pPrevCertContext = null;
while(true) {
hCertContext = Crypt32.INSTANCE.CertFindCertificateInStore(
openStore(),
WinCrypt.X509_ASN_ENCODING,
0,
Crypt32.CERT_FIND_SUBJECT_STR,
Constants.ABOUT_EMAIL,
pPrevCertContext);
if(hCertContext == null) {
break;
}
pPrevCertContext = Crypt32.INSTANCE.CertDuplicateCertificateContext(hCertContext);
if(success = (success && Crypt32.INSTANCE.CertDeleteCertificateFromStore(hCertContext))) {
log.info("Successfully deleted certificate matching {}", Constants.ABOUT_EMAIL);
} else {
log.info("Could not delete certificate: {}", Kernel32Util.formatMessage(Native.getLastError()));
}
}
closeStore();
return success;
}
public List<String> find() {
return null;
}
public void setInstallType(Installer.PrivilegeLevel type) {
this.certType = type;
}
public Installer.PrivilegeLevel getInstallType() {
return certType;
}
public boolean verify(File certFile) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(getCertBytes(certFile));
WinCrypt.DATA_BLOB thumbPrint = new WinCrypt.DATA_BLOB(md.digest());
WinNT.HANDLE cert = Crypt32.INSTANCE.CertFindCertificateInStore(
openStore(),
WinCrypt.X509_ASN_ENCODING,
0,
Crypt32.CERT_FIND_SHA1_HASH,
thumbPrint,
null);
return cert != null;
} catch(IOException | NoSuchAlgorithmException e) {
log.warn("An error occurred verifying the cert is installed: {}", certFile, e);
}
return false;
}
/**
* The JNA's Crypt32 instance oversimplifies store handling, preventing user stores from being used
*/
interface Crypt32 extends StdCallLibrary {
int CERT_SYSTEM_STORE_CURRENT_USER = 65536;
int CERT_SYSTEM_STORE_LOCAL_MACHINE = 131072;
int CERT_STORE_PROV_SYSTEM = 10;
int CERT_STORE_ADD_REPLACE_EXISTING = 3;
int CERT_FIND_SUBJECT_STR = 524295;
int CERT_FIND_SHA1_HASH = 65536;
Crypt32 INSTANCE = Native.load("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS);
WinCrypt.HCERTSTORE CertOpenStore(int lpszStoreProvider, int dwMsgAndCertEncodingType, Pointer hCryptProv, int dwFlags, String pvPara);
boolean CertCloseStore(WinCrypt.HCERTSTORE hCertStore, int dwFlags);
boolean CertAddEncodedCertificateToStore(WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, Pointer pbCertEncoded, int cbCertEncoded, int dwAddDisposition, Pointer ppCertContext);
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, String pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, Structure pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
boolean CertDeleteCertificateFromStore(WinCrypt.CERT_CONTEXT pCertContext);
boolean CertFreeCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
WinCrypt.CERT_CONTEXT CertDuplicateCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
}
// Polyfill from JNA5+
@SuppressWarnings("UnusedDeclaration") //Library class
public static class WinCrypt {
public static int X509_ASN_ENCODING = 0x00000001;
public static class HCERTSTORE extends WinNT.HANDLE {
public HCERTSTORE() {}
public HCERTSTORE(Pointer p) {
super(p);
}
}
public static class CERT_CONTEXT extends WinNT.HANDLE {
public CERT_CONTEXT() {}
public CERT_CONTEXT(Pointer p) {
super(p);
}
}
public static class DATA_BLOB extends com.sun.jna.platform.win32.WinCrypt.DATA_BLOB {
// Wrap the constructor for code readability
public DATA_BLOB() {
super();
}
public DATA_BLOB(byte[] data) {
super(data);
}
}
}
}

View File

@@ -0,0 +1,136 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import qz.utils.ShellUtilities;
import qz.utils.WindowsUtilities;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* Command Line technique for installing certificates on Windows
* Fallback class for when JNA is not available (e.g. Windows on ARM)
*/
@SuppressWarnings("UnusedDeclaration") //Library class
public class WindowsCertificateInstallerCli extends NativeCertificateInstaller {
private static final Logger log = LogManager.getLogger(WindowsCertificateInstallerCli.class);
private Installer.PrivilegeLevel certType;
public WindowsCertificateInstallerCli(Installer.PrivilegeLevel certType) {
setInstallType(certType);
}
public boolean add(File certFile) {
if (WindowsUtilities.isWindowsXP()) return false;
if (certType == Installer.PrivilegeLevel.USER) {
// This will prompt the user
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "-user", "Root", certFile.getPath());
} else {
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "Root", certFile.getPath());
}
}
public boolean remove(List<String> idList) {
if (WindowsUtilities.isWindowsXP()) return false;
boolean success = true;
for (String certId : idList) {
if (certType == Installer.PrivilegeLevel.USER) {
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "-user", "Root", certId);
} else {
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "Root", certId);
}
}
return success;
}
/**
* Returns a list of serials, if found
*/
public List<String> find() {
ArrayList<String> serialList = new ArrayList<>();
try {
Process p;
if (certType == Installer.PrivilegeLevel.USER) {
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "-user", "Root"});
} else {
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "Root"});
}
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
if (line.contains("================")) {
// First line is serial
String serial = parseNextLine(in);
if (serial != null) {
// Second line is issuer
String issuer = parseNextLine(in);
if (issuer.contains("OU=" + Constants.ABOUT_COMPANY)) {
serialList.add(serial);
}
}
}
}
in.close();
} catch(Exception e) {
log.info("Unable to find a Trusted Root Certificate matching \"OU={}\"", Constants.ABOUT_COMPANY);
}
return serialList;
}
public boolean verify(File certFile) {
return verifyCert(certFile);
}
public static boolean verifyCert(File certFile) {
// -user also will check the root store
String dwErrorStatus = ShellUtilities.execute( new String[] {"certutil", "-user", "-verify", certFile.getPath() }, new String[] { "dwErrorStatus=" }, false, false);
if(!dwErrorStatus.isEmpty()) {
String[] parts = dwErrorStatus.split("[\r\n\\s]+");
for(String part : parts) {
if(part.startsWith("dwErrorStatus=")) {
log.info("Certificate validity says {}", part);
String[] status = part.split("=", 2);
if (status.length == 2) {
return status[1].trim().equals("0");
}
}
}
}
log.warn("Unable to determine certificate validity, you'll be prompted on startup");
return false;
}
public void setInstallType(Installer.PrivilegeLevel type) {
this.certType = type;
}
public Installer.PrivilegeLevel getInstallType() {
return certType;
}
private static String parseNextLine(BufferedReader reader) throws IOException {
String data = reader.readLine();
if (data != null) {
String[] split = data.split(":", 2);
if (split.length == 2) {
return split[1].trim();
}
}
return null;
}
}

View File

@@ -0,0 +1,7 @@
package qz.installer.certificate.firefox;
class ConflictingPolicyException extends Exception {
ConflictingPolicyException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,282 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate.firefox;
import com.github.zafarkhaja.semver.Version;
import com.sun.jna.platform.win32.WinReg;
import org.codehaus.jettison.json.JSONException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
import qz.installer.certificate.CertificateManager;
import qz.installer.certificate.firefox.locator.AppAlias;
import qz.installer.certificate.firefox.locator.AppInfo;
import qz.installer.certificate.firefox.locator.AppLocator;
import qz.utils.JsonWriter;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
/**
* Installs the Firefox Policy file via Enterprise Policy, Distribution Policy file or AutoConfig, depending on OS & version
*/
public class FirefoxCertificateInstaller {
protected static final Logger log = LogManager.getLogger(FirefoxCertificateInstaller.class);
/**
* Versions are for Mozilla's official Firefox release.
* 3rd-party/clones may adopt Enterprise Policy support under
* different version numbers, adapt as needed.
*/
private static final Version WINDOWS_POLICY_VERSION = Version.valueOf("62.0.0");
private static final Version MAC_POLICY_VERSION = Version.valueOf("63.0.0");
private static final Version LINUX_POLICY_VERSION = Version.valueOf("65.0.0");
public static final Version FIREFOX_RESTART_VERSION = Version.valueOf("60.0.0");
public static final String LINUX_GLOBAL_POLICY_LOCATION = "/etc/firefox/policies/policies.json";
public static final String LINUX_SNAP_CERT_LOCATION = "/etc/firefox/policies/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION; // See https://github.com/mozilla/policy-templates/issues/936
public static final String LINUX_GLOBAL_CERT_LOCATION = "/usr/lib/mozilla/certificates/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION;
private static String DISTRIBUTION_ENTERPRISE_ROOT_POLICY = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }";
private static String DISTRIBUTION_INSTALL_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION + "\", \"" + LINUX_SNAP_CERT_LOCATION + "\" ] } } }";
private static String DISTRIBUTION_REMOVE_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"/opt/" + Constants.PROPS_FILE + "/auth/root-ca.crt\"] } } }";
public static final String DISTRIBUTION_POLICY_LOCATION = "distribution/policies.json";
public static final String DISTRIBUTION_MAC_POLICY_LOCATION = "Contents/Resources/" + DISTRIBUTION_POLICY_LOCATION;
public static final String POLICY_AUDIT_MESSAGE = "Enterprise policy installed by " + Constants.ABOUT_TITLE + " on " + SystemUtilities.timeStamp();
public static void install(X509Certificate cert, String ... hostNames) {
// Blindly install Firefox enterprise policies to the system (macOS, Windows)
ArrayList<AppAlias.Alias> enterpriseFailed = new ArrayList<>();
for(AppAlias.Alias alias : AppAlias.FIREFOX.getAliases()) {
boolean success = false;
try {
if(alias.isEnterpriseReady() && !hasEnterprisePolicy(alias, false)) {
log.info("Installing Firefox enterprise certificate policy for {}", alias);
success = installEnterprisePolicy(alias, false);
}
} catch(ConflictingPolicyException e) {
log.warn("Conflict found installing {} enterprise cert support. We'll fallback on the distribution policy instead", alias.getName(), e);
}
if(!success) {
enterpriseFailed.add(alias);
}
}
// Search for installed instances
ArrayList<AppInfo> foundApps = AppLocator.getInstance().locate(AppAlias.FIREFOX);
ArrayList<Path> processPaths = null;
for(AppInfo appInfo : foundApps) {
boolean success = false;
if (honorsPolicy(appInfo)) {
if((SystemUtilities.isWindows()|| SystemUtilities.isMac()) && !enterpriseFailed.contains(appInfo.getAlias())) {
// Enterprise policy was already installed
success = true;
} else {
log.info("Installing Firefox distribution policy for {}", appInfo);
success = installDistributionPolicy(appInfo, cert);
}
} else {
log.info("Installing Firefox auto-config script for {}", appInfo);
try {
String certData = Base64.getEncoder().encodeToString(cert.getEncoded());
success = LegacyFirefoxCertificateInstaller.installAutoConfigScript(appInfo, certData, hostNames);
}
catch(CertificateEncodingException e) {
log.warn("Unable to install auto-config script for {}", appInfo, e);
}
}
if(success) {
issueRestartWarning(processPaths = AppLocator.getRunningPaths(foundApps, processPaths), appInfo);
}
}
}
public static void uninstall() {
ArrayList<AppInfo> appList = AppLocator.getInstance().locate(AppAlias.FIREFOX);
for(AppInfo appInfo : appList) {
if(honorsPolicy(appInfo)) {
if(SystemUtilities.isWindows() || SystemUtilities.isMac()) {
log.info("Skipping uninstall of Firefox enterprise root certificate policy for {}", appInfo);
} else {
try {
File policy = appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toFile();
if(policy.exists()) {
JsonWriter.write(appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toString(), DISTRIBUTION_INSTALL_CERT_POLICY, false, true);
}
} catch(IOException | JSONException e) {
log.warn("Unable to remove Firefox policy for {}", appInfo, e);
}
}
} else {
log.info("Uninstalling Firefox auto-config script for {}", appInfo);
LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(appInfo);
}
}
}
public static boolean honorsPolicy(AppInfo appInfo) {
if (appInfo.getVersion() == null) {
log.warn("Firefox-compatible browser found {}, but no version information is available", appInfo);
return false;
}
if(SystemUtilities.isWindows()) {
return appInfo.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION);
} else if (SystemUtilities.isMac()) {
return appInfo.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION);
} else {
return appInfo.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION);
}
}
/**
* Returns true if an alternative Firefox policy (e.g. registry, plist user or system) is installed
*/
private static boolean hasEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) throws ConflictingPolicyException {
if(SystemUtilities.isWindows()) {
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));
Integer foundPolicy = WindowsUtilities.getRegInt(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots");
if(foundPolicy != null) {
return foundPolicy == 1;
}
} else if(SystemUtilities.isMac()) {
String policyLocation = "/Library/Preferences/";
if(userOnly) {
policyLocation = System.getProperty("user.home") + policyLocation;
}
String policesEnabled = ShellUtilities.executeRaw(new String[] { "defaults", "read", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled"}, true);
String foundPolicy = ShellUtilities.executeRaw(new String[] {"defaults", "read", policyLocation + alias.getBundleId(), "Certificates"}, true);
if(!policesEnabled.isEmpty() && !foundPolicy.isEmpty()) {
// Policies exist, decide how to proceed
if(policesEnabled.trim().equals("1") && foundPolicy.contains("ImportEnterpriseRoots = 1;")) {
return true;
}
throw new ConflictingPolicyException(String.format("%s enterprise policy conflict at %s: %s", alias.getName(), policyLocation + alias.getBundleId(), foundPolicy));
}
} else {
// Linux alternate policy not yet supported
}
return false;
}
/**
* Install policy to distribution/policies.json
*/
public static boolean installDistributionPolicy(AppInfo app, X509Certificate cert) {
Path jsonPath = app.getPath().resolve(SystemUtilities.isMac() ? DISTRIBUTION_MAC_POLICY_LOCATION:DISTRIBUTION_POLICY_LOCATION);
String jsonPolicy = SystemUtilities.isWindows() || SystemUtilities.isMac() ? DISTRIBUTION_ENTERPRISE_ROOT_POLICY:DISTRIBUTION_INSTALL_CERT_POLICY;
// Special handling for snaps
if(app.getPath().toString().startsWith("/snap")) {
log.info("Snap detected, installing policy file to global location instead: {}", LINUX_GLOBAL_POLICY_LOCATION);
jsonPath = Paths.get(LINUX_GLOBAL_POLICY_LOCATION);
}
try {
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
// Linux lacks the concept of "enterprise roots", we'll write it to a known location instead
writeCertFile(cert, LINUX_SNAP_CERT_LOCATION); // so that the snap can read from it
writeCertFile(cert, LINUX_GLOBAL_CERT_LOCATION); // default location for non-snaps
}
File jsonFile = jsonPath.toFile();
// Make sure we can traverse and read
File distribution = jsonFile.getParentFile();
distribution.mkdirs();
distribution.setReadable(true, false);
distribution.setExecutable(true, false);
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
// Delete previous policy
JsonWriter.write(jsonPath.toString(), DISTRIBUTION_REMOVE_CERT_POLICY, false, true);
}
JsonWriter.write(jsonPath.toString(), jsonPolicy, false, false);
// Make sure ew can read
jsonFile.setReadable(true, false);
return true;
} catch(JSONException | IOException e) {
log.warn("Could not install distribution policy {} to {}", jsonPolicy, jsonPath.toString(), e);
}
return false;
}
public static boolean installEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) {
if(SystemUtilities.isWindows()) {
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));;
WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "Comment", POLICY_AUDIT_MESSAGE);
return WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots", 1);
} else if(SystemUtilities.isMac()) {
String policyLocation = "/Library/Preferences/";
if(userOnly) {
policyLocation = System.getProperty("user.home") + policyLocation;
}
return ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled", "-bool", "TRUE"}, true) &&
ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "Certificates", "-dict", "ImportEnterpriseRoots", "-bool", "TRUE",
"Comment", "-string", POLICY_AUDIT_MESSAGE}, true);
}
return false;
}
public static boolean issueRestartWarning(ArrayList<Path> runningPaths, AppInfo appInfo) {
boolean firefoxIsRunning = runningPaths.contains(appInfo.getExePath());
// Edge case for detecting if snap is running, since we can't compare the exact path easily
for(Path runningPath : runningPaths) {
if(runningPath.startsWith("/snap/")) {
firefoxIsRunning = true;
}
}
if (firefoxIsRunning) {
if (appInfo.getVersion().greaterThanOrEqualTo(FirefoxCertificateInstaller.FIREFOX_RESTART_VERSION)) {
try {
Installer.getInstance().spawn(appInfo.getExePath().toString(), "-private", "about:restartrequired");
return true;
}
catch(Exception ignore) {}
} else {
log.warn("{} must be restarted manually for changes to take effect", appInfo);
}
}
return false;
}
private static void writeCertFile(X509Certificate cert, String location) throws IOException {
File certFile = new File(location);
// Make sure we can traverse and read
File certs = new File(location).getParentFile();
certs.mkdirs();
certs.setReadable(true, false);
certs.setExecutable(true, false);
File mozilla = certs.getParentFile();
mozilla.setReadable(true, false);
mozilla.setExecutable(true, false);
// Make sure we can read
CertificateManager.writeCert(cert, certFile);
certFile.setReadable(true, false);
}
}

View File

@@ -0,0 +1,150 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate.firefox;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.certificate.CertificateChainBuilder;
import qz.installer.certificate.firefox.locator.AppInfo;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import java.io.*;
import java.nio.file.Path;
import java.security.cert.CertificateEncodingException;
import java.util.*;
/**
* Legacy Firefox Certificate installer
*
* For old Firefox-compatible browsers still in the wild such as Firefox 52 ESR, SeaMonkey, WaterFox, etc.
*/
public class LegacyFirefoxCertificateInstaller {
private static final Logger log = LogManager.getLogger(CertificateChainBuilder.class);
private static final String CFG_TEMPLATE = "assets/firefox-autoconfig.js.in";
private static final String CFG_FILE = Constants.PROPS_FILE + ".cfg";
private static final String PREFS_FILE = Constants.PROPS_FILE + ".js";
private static final String PREFS_DIR = "defaults/pref";
private static final String MAC_PREFIX = "Contents/Resources";
public static boolean installAutoConfigScript(AppInfo appInfo, String certData, String ... hostNames) {
try {
if(appInfo.getPath().toString().equals("/usr/bin")) {
throw new Exception("Preventing install to root location");
}
writePrefsFile(appInfo);
writeParsedConfig(appInfo, certData, false, hostNames);
return true;
} catch(Exception e) {
log.warn("Error installing auto-config support for {}", appInfo, e);
}
return false;
}
public static boolean uninstallAutoConfigScript(AppInfo appInfo) {
try {
writeParsedConfig(appInfo, "", true);
return true;
} catch(Exception e) {
log.warn("Error uninstalling auto-config support for {}", appInfo, e);
}
return false;
}
public static File tryWrite(AppInfo appInfo, boolean mkdirs, String ... paths) throws IOException {
Path dir = appInfo.getPath();
if (SystemUtilities.isMac()) {
dir = dir.resolve(MAC_PREFIX);
}
for (String path : paths) {
dir = dir.resolve(path);
}
File file = dir.toFile();
if(mkdirs) file.mkdirs();
if(file.exists() && file.isDirectory() && file.canWrite()) {
return file;
}
throw new IOException(String.format("Directory does not exist or is not writable: %s", file));
}
public static void deleteFile(File parent, String ... paths) {
if(parent != null) {
String toDelete = parent.getPath();
for (String path : paths) {
toDelete += File.separator + path;
}
File deleteFile = new File(toDelete);
if (!deleteFile.exists()) {
} else if (new File(toDelete).delete()) {
log.info("Deleted old file: {}", toDelete);
} else {
log.warn("Could not delete old file: {}", toDelete);
}
}
}
public static void writePrefsFile(AppInfo app) throws Exception {
File prefsDir = tryWrite(app, true, PREFS_DIR);
deleteFile(prefsDir, "firefox-prefs.js"); // cleanup old version
// first check that there aren't other prefs files
String pref = "general.config.filename";
for (File file : prefsDir.listFiles()) {
try {
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while((line = reader.readLine()) != null) {
if(line.contains(pref) && !line.contains(CFG_FILE)) {
throw new Exception(String.format("Browser already has %s defined in %s:\n %s", pref, file, line));
}
}
} catch(IOException ignore) {}
}
// write out the new prefs file
File prefsFile = new File(prefsDir, PREFS_FILE);
BufferedWriter writer = new BufferedWriter(new FileWriter(prefsFile));
String[] data = {
String.format("pref('%s', '%s');", pref, CFG_FILE),
"pref('general.config.obscure_value', 0);"
};
for (String line : data) {
writer.write(line + "\n");
}
writer.close();
prefsFile.setReadable(true, false);
}
private static void writeParsedConfig(AppInfo appInfo, String certData, boolean uninstall, String ... hostNames) throws IOException, CertificateEncodingException{
if (hostNames.length == 0) hostNames = CertificateChainBuilder.DEFAULT_HOSTNAMES;
File cfgDir = tryWrite(appInfo, false);
deleteFile(cfgDir, "firefox-config.cfg"); // cleanup old version
File dest = new File(cfgDir.getPath(), CFG_FILE);
HashMap<String, String> fieldMap = new HashMap<>();
// Dynamic fields
fieldMap.put("%CERT_DATA%", certData);
fieldMap.put("%COMMON_NAME%", hostNames[0]);
fieldMap.put("%TIMESTAMP%", uninstall ? "-1" : "" + new Date().getTime());
fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.getAppPath() != null ? SystemUtilities.getAppPath().toString() : "" : "");
fieldMap.put("%UNINSTALL%", "" + uninstall);
FileUtilities.configureAssetFile(CFG_TEMPLATE, dest, fieldMap, LegacyFirefoxCertificateInstaller.class);
dest.setReadable(true, false);
}
}

View File

@@ -0,0 +1,117 @@
//
// Firefox AutoConfig Certificate Installer for Legacy Firefox versions
// This is part of the QZ Tray application
//
var serviceObserver = {
observe: function observe(aSubject, aTopic, aData) {
// Get NSS certdb object
var certdb = getCertDB();
if (needsUninstall()) {
deleteCertificate();
unregisterProtocol();
} else if (needsCert()) {
deleteCertificate();
installCertificate();
registerProtocol();
}
// Compares the timestamp embedded in this script against that stored in the browser's about:config
function needsCert() {
try {
return getPref("%PROPS_FILE%.installer.timestamp") != "%TIMESTAMP%";
} catch(notfound) {}
return true;
}
// Installs the embedded base64 certificate into the browser
function installCertificate() {
certdb.addCertFromBase64(getCertData(), "C,C,C", "%COMMON_NAME% - %ABOUT_COMPANY%");
pref("%PROPS_FILE%.installer.timestamp", "%TIMESTAMP%");
}
// Deletes the certificate, if it exists
function deleteCertificate() {
var certs = certdb.getCerts();
var enumerator = certs.getEnumerator();
while (enumerator.hasMoreElements()) {
var cert = enumerator.getNext().QueryInterface(Components.interfaces.nsIX509Cert);
if (cert.containsEmailAddress("%ABOUT_EMAIL%")) {
try {
certdb.deleteCertificate(cert);
} catch (ignore) {}
}
}
pref("%PROPS_FILE%.installer.timestamp", "-1");
}
// Register the specified protocol to open with the specified application
function registerProtocol() {
// Only register if platform needs it (e.g. macOS)
var trayApp = "%APP_PATH%";
if (!trayApp) { return; }
try {
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);
file.initWithPath(trayApp);
var lhandler = Components.classes["@mozilla.org/uriloader/local-handler-app;1"].createInstance(Components.interfaces.nsILocalHandlerApp);
lhandler.executable = file;
lhandler.name = "%PROPS_FILE%";
var protocol = pservice.getProtocolHandlerInfo("%DATA_DIR%");
protocol.preferredApplicationHandler = lhandler;
protocol.preferredAction = 2; // useHelperApp
protocol.alwaysAskBeforeHandling = false;
hservice.store(protocol);
} catch(ignore) {}
}
// De-register the specified protocol from opening with the specified application
function unregisterProtocol() {
// Only register if platform needs it (e.g. macOS)
var trayApp = "%APP_PATH%";
if (!trayApp) { return; }
try {
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
hservice.remove(pservice.getProtocolHandlerInfo("%DATA_DIR%"));
} catch(ignore) {}
}
// Get certdb object
function getCertDB() {
// Import certificate using NSS certdb API (http://tinyurl.com/x509certdb)
var id = "@mozilla.org/security/x509certdb;1";
var db1 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB);
var db2 = db1;
try {
db2 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB2);
} catch(ignore) {}
return db2;
}
// The certificate to import (automatically generated by desktop installer)
function getCertData() {
return "%CERT_DATA%";
}
// Whether or not an uninstall should occur, flagged by the installer/uninstaller
function needsUninstall() {
try {
if (getPref("%PROPS_FILE%.installer.timestamp") == "-1") {
return false;
}
}
catch(notfound) {
return false;
}
return %UNINSTALL%;
}
}
};
Components.utils.import("resource://gre/modules/Services.jsm");
Services.obs.addObserver(serviceObserver, "profile-after-change", false);

View File

@@ -0,0 +1,91 @@
package qz.installer.certificate.firefox.locator;
import java.util.Locale;
public enum AppAlias {
// Tor Browser intentionally excluded; Tor's proxy blocks localhost connections
FIREFOX(
new Alias("Mozilla", "Mozilla Firefox", "org.mozilla.firefox", true),
new Alias("Mozilla", "Firefox Developer Edition", "org.mozilla.firefoxdeveloperedition", true),
new Alias("Mozilla", "Firefox Nightly", "org.mozilla.nightly", true),
new Alias("Mozilla", "SeaMonkey", "org.mozilla.seamonkey", false),
new Alias("Waterfox", "Waterfox", "net.waterfox.waterfoxcurrent", true),
new Alias("Waterfox", "Waterfox Classic", "org.waterfoxproject.waterfox classic", false),
new Alias("Mozilla", "Pale Moon", "org.mozilla.palemoon", false),
// IceCat is technically enterprise ready, but not officially distributed for macOS, Windows
new Alias("Mozilla", "IceCat", "org.gnu.icecat", false)
);
Alias[] aliases;
AppAlias(Alias... aliases) {
this.aliases = aliases;
}
public Alias[] getAliases() {
return aliases;
}
public static Alias findAlias(AppAlias appAlias, String appName, boolean stripVendor) {
if (appName != null) {
for (Alias alias : appAlias.aliases) {
if (appName.toLowerCase(Locale.ENGLISH).matches(alias.getName(stripVendor).toLowerCase(Locale.ENGLISH))) {
return alias;
}
}
}
return null;
}
public static class Alias {
private String vendor;
private String name;
private String bundleId;
private boolean enterpriseReady;
private String posix;
public Alias(String vendor, String name, String bundleId, boolean enterpriseReady) {
this.name = name;
this.vendor = vendor;
this.bundleId = bundleId;
this.enterpriseReady = enterpriseReady;
this.posix = getName(true).replaceAll(" ", "").toLowerCase(Locale.ENGLISH);
}
public String getVendor() {
return vendor;
}
public String getName() {
return name;
}
/**
* Remove vendor prefix if exists
*/
public String getName(boolean stripVendor) {
if(stripVendor && "Mozilla".equals(vendor) && name.startsWith(vendor)) {
return name.substring(vendor.length()).trim();
}
return name;
}
public String getBundleId() {
return bundleId;
}
public String getPosix() {
return posix;
}
/**
* Returns whether or not the app is known to recognizes enterprise policies, such as GPO
*/
public boolean isEnterpriseReady() {
return enterpriseReady;
}
@Override
public String toString() {
return name;
}
}
}

View File

@@ -0,0 +1,100 @@
package qz.installer.certificate.firefox.locator;
import com.github.zafarkhaja.semver.Version;
import java.nio.file.Path;
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
/**
* Container class for installed app information
*/
public class AppInfo {
private AppAlias.Alias alias;
private Path path;
private Path exePath;
private Version version;
public AppInfo(Alias alias, Path exePath, String version) {
this.alias = alias;
this.path = exePath.getParent();
this.exePath = exePath;
this.version = parseVersion(version);
}
public AppInfo(Alias alias, Path path, Path exePath, String version) {
this.alias = alias;
this.path = path;
this.exePath = exePath;
this.version = parseVersion(version);
}
public AppInfo(Alias alias, Path exePath) {
this.alias = alias;
this.path = exePath.getParent();
this.exePath = exePath;
}
public Alias getAlias() {
return alias;
}
public String getName(boolean stripVendor) {
return alias.getName(stripVendor);
}
public Path getExePath() {
return exePath;
}
public Path getPath() {
return path;
}
public void setPath(Path path) {
this.path = path;
}
public Version getVersion() {
return version;
}
public void setVersion(String version) {
this.version = parseVersion(version);
}
public void setVersion(Version version) {
this.version = version;
}
private static Version parseVersion(String version) {
try {
// Ensure < 3 octets (e.g. "56.0") doesn't failing
while(version.split("\\.").length < 3) {
version = version + ".0";
}
return Version.valueOf(version);
} catch(Exception ignore1) {
// Catch poor formatting (e.g. "97.0a1"), try to use major version only
if(version.split("\\.").length > 0) {
try {
String[] tryFix = version.split("\\.");
return Version.valueOf(tryFix[0] + ".0.0-unknown");
} catch(Exception ignore2) {}
}
}
return null;
}
@Override
public boolean equals(Object o) {
if(o instanceof AppInfo && o != null && path != null) {
return path.equals(((AppInfo)o).getPath());
}
return false;
}
@Override
public String toString() {
return alias + " " + path;
}
}

View File

@@ -0,0 +1,87 @@
package qz.installer.certificate.firefox.locator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
public abstract class AppLocator {
protected static final Logger log = LogManager.getLogger(AppLocator.class);
private static AppLocator INSTANCE = getPlatformSpecificAppLocator();
public abstract ArrayList<AppInfo> locate(AppAlias appAlias);
public abstract ArrayList<Path> getPidPaths(ArrayList<String> pids);
@SuppressWarnings("unused")
public ArrayList<String> getPids(String ... processNames) {
return getPids(new ArrayList<>(Arrays.asList(processNames)));
}
/**
* Linux, Mac
*/
public ArrayList<String> getPids(ArrayList<String> processNames) {
String[] response;
ArrayList<String> pidList = new ArrayList<>();
if(processNames.contains("firefox") && !(SystemUtilities.isWindows() || SystemUtilities.isMac())) {
processNames.add("MainThread"); // Workaround Firefox 79 https://github.com/qzind/tray/issues/701
processNames.add("GeckoMain"); // Workaround Firefox 94 https://bugzilla.mozilla.org/show_bug.cgi?id=1742606
}
if (processNames.size() == 0) return pidList;
// Quoting handled by the command processor (e.g. pgrep -x "myapp|my app" is perfectly valid)
String data = ShellUtilities.executeRaw("pgrep", "-x", String.join("|", processNames));
//Splitting an empty string results in a 1 element array, this is not what we want
if (!data.isEmpty()) {
response = data.split("\\s*\\r?\\n");
Collections.addAll(pidList, response);
}
return pidList;
}
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList) {
return getRunningPaths(appList, null);
}
/**
* Gets the path to the running executables matching on <code>AppInfo.getExePath</code>
* This is resource intensive; if a non-null <code>cache</code> is provided, it will return that instead
*/
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList, ArrayList<Path> cache) {
if(cache == null) {
ArrayList<String> appNames = new ArrayList<>();
for(AppInfo app : appList) {
String exeName = app.getExePath().getFileName().toString();
if (!appNames.contains(exeName)) appNames.add(exeName);
}
cache = INSTANCE.getPidPaths(INSTANCE.getPids(appNames));
}
return cache;
}
public static AppLocator getInstance() {
return INSTANCE;
}
private static AppLocator getPlatformSpecificAppLocator() {
switch(SystemUtilities.getOs()) {
case WINDOWS:
return new WindowsAppLocator();
case MAC:
return new MacAppLocator();
default:
return new LinuxAppLocator();
}
}
}

View File

@@ -0,0 +1,159 @@
package qz.installer.certificate.firefox.locator;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import qz.utils.UnixUtilities;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
public class LinuxAppLocator extends AppLocator {
private static final Logger log = LogManager.getLogger(LinuxAppLocator.class);
public ArrayList<AppInfo> locate(AppAlias appAlias) {
ArrayList<AppInfo> appList = new ArrayList<>();
// Workaround for calling "firefox --version" as sudo
String[] env = appendPaths("HOME=/tmp");
// Search for matching executable in all path values
aliasLoop:
for(AppAlias.Alias alias : appAlias.aliases) {
// Add non-standard app search locations (e.g. Fedora)
for (String dirname : appendPaths(alias.getPosix(), "/usr/lib/$/bin", "/usr/lib64/$/bin", "/usr/lib/$", "/usr/lib64/$")) {
Path path = Paths.get(dirname, alias.getPosix());
if (Files.isRegularFile(path) && Files.isExecutable(path)) {
log.info("Found {} {}: {}, investigating...", alias.getVendor(), alias.getName(true), path);
try {
File file = path.toFile().getCanonicalFile(); // fix symlinks
if(file.getPath().endsWith("/snap")) {
// Ubuntu 22.04+ ships Firefox as a snap
// Snaps are read-only and are symlinks back to /usr/bin/snap
// Reset the executable back to /snap/bin/firefox to get proper version information
file = path.toFile();
}
if(file.getPath().endsWith(".sh")) {
// Legacy Ubuntu likes to use .../firefox/firefox.sh, return .../firefox/firefox instead
log.info("Found an '.sh' file: {}, removing file extension: {}", file, file = new File(FilenameUtils.removeExtension(file.getPath())));
}
String contentType = Files.probeContentType(file.toPath());
if(contentType == null) {
// Fallback to commandline per https://bugs.openjdk.org/browse/JDK-8188228
contentType = ShellUtilities.executeRaw("file", "--mime-type", "--brief", file.getPath()).trim();
}
if(contentType != null && contentType.endsWith("/x-shellscript")) {
if(UnixUtilities.isFedora()) {
// Firefox's script is full of variables and not parsable, fallback to /usr/lib64/$, etc
log.info("Found shell script at {}, but we're on Fedora, so we'll look in some known locations instead.", file.getPath());
continue;
}
// Debian and Arch like to place a stub script directly in /usr/bin/
// TODO: Split into a function; possibly recurse on search paths
log.info("{} bin was expected but script found... Reading...", appAlias.name());
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while((line = reader.readLine()) != null) {
if(line.startsWith("exec") && line.contains(alias.getPosix())) {
String[] parts = line.split(" ");
// Get the app name after "exec"
if (parts.length > 1) {
log.info("Found a familiar line '{}', using '{}'", line, parts[1]);
Path p = Paths.get(parts[1]);
String exec = parts[1];
// Handle edge-case for esr release
if(!p.isAbsolute()) {
// Script doesn't contain the full path, go deeper
exec = Paths.get(dirname, exec).toFile().getCanonicalPath();
log.info("Calculated full bin path {}", exec);
}
// Make sure it actually exists
if(!(file = new File(exec)).exists()) {
log.warn("Sorry, we couldn't detect the real path of {}. Skipping...", appAlias.name());
continue aliasLoop;
}
break;
}
}
}
reader.close();
} else {
log.info("Assuming {} {} is installed: {}", alias.getVendor(), alias.getName(true), file);
}
AppInfo appInfo = new AppInfo(alias, file.toPath());
if(file.getPath().startsWith("/snap/")) {
// Ubuntu 22.04+ uses snaps, fallback to a sane "path" value
String snapPath = file.getPath(); // e.g. /snap/bin/firefox
snapPath = snapPath.replaceFirst("/bin/", "/");
snapPath += "/current";
appInfo.setPath(Paths.get(snapPath));
}
appList.add(appInfo);
// Call "--version" on executable to obtain version information
Process p = Runtime.getRuntime().exec(new String[] {file.getPath(), "--version" }, env);
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String version = reader.readLine();
reader.close();
if (version != null) {
log.info("We obtained version info: {}, but we'll need to parse it", version);
if(version.contains(" ")) {
String[] split = version.split(" ");
String parsed = split[split.length - 1];
String stripped = parsed.replaceAll("[^\\d.]", "");
appInfo.setVersion(stripped);
if(!parsed.equals(stripped)) {
// Add the meta data back (e.g. "esr")
appInfo.getVersion().setBuildMetadata(parsed.replaceAll("[\\d.]", ""));
}
} else {
appInfo.setVersion(version.trim());
}
}
break;
} catch(Exception e) {
log.warn("Something went wrong getting app info for {} {}", alias.getVendor(), alias.getName(true), e);
}
}
}
}
return appList;
}
@Override
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
ArrayList<Path> pathList = new ArrayList<>();
for(String pid : pids) {
try {
pathList.add(Paths.get("/proc/", pid, !SystemUtilities.isSolaris() ? "/exe" : "/path/a.out").toRealPath());
} catch(IOException e) {
log.warn("Process {} vanished", pid);
}
}
return pathList;
}
/**
* Returns a PATH value with provided paths appended, replacing "$" with POSIX app name
* Useful for strange Firefox install locations (e.g. Fedora)
*
* Usage: appendPaths("firefox", "/usr/lib64");
*
*/
private static String[] appendPaths(String posix, String ... prefixes) {
String newPath = System.getenv("PATH");
for (String prefix : prefixes) {
newPath = newPath + File.pathSeparator + prefix.replaceAll("\\$", posix);
}
return newPath.split(File.pathSeparator);
}
}

View File

@@ -0,0 +1,168 @@
package qz.installer.certificate.firefox.locator;
import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import qz.utils.ShellUtilities;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
public class MacAppLocator extends AppLocator{
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
private static String[] BLACKLISTED_PATHS = new String[]{"/Volumes/", "/.Trash/", "/Applications (Parallels)/" };
/**
* Helper class for finding key/value siblings from the DDM
*/
private enum SiblingNode {
NAME("_name"),
PATH("path"),
VERSION("version");
private String key;
private boolean wants;
SiblingNode(String key) {
this.key = key;
this.wants = false;
}
private boolean isKey(Node node) {
if (node.getNodeName().equals("key") && node.getTextContent().equals(key)) {
return true;
}
return false;
}
}
@Override
public ArrayList<AppInfo> locate(AppAlias appAlias) {
ArrayList<AppInfo> appList = new ArrayList<>();
Document doc;
try {
// system_profile benchmarks about 30% better than lsregister
Process p = Runtime.getRuntime().exec(new String[] {"system_profiler", "SPApplicationsDataType", "-xml"}, ShellUtilities.envp);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// don't let the <!DOCTYPE> fail parsing per https://github.com/qzind/tray/issues/809
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
doc = dbf.newDocumentBuilder().parse(p.getInputStream());
} catch(IOException | ParserConfigurationException | SAXException e) {
log.warn("Could not retrieve app listing for {}", appAlias.name(), e);
return appList;
}
doc.normalizeDocument();
NodeList nodeList = doc.getElementsByTagName("dict");
for (int i = 0; i < nodeList.getLength(); i++) {
NodeList dict = nodeList.item(i).getChildNodes();
HashMap<SiblingNode, String> foundApp = new HashMap<>();
for (int j = 0; j < dict.getLength(); j++) {
Node node = dict.item(j);
if (node.getNodeType() == Node.ELEMENT_NODE) {
for (SiblingNode sibling : SiblingNode.values()) {
if (sibling.wants) {
foundApp.put(sibling, node.getTextContent());
sibling.wants = false;
break;
} else if(sibling.isKey(node)) {
sibling.wants = true;
break;
}
}
}
}
AppAlias.Alias alias;
if((alias = AppAlias.findAlias(appAlias, foundApp.get(SiblingNode.NAME), true)) != null) {
appList.add(new AppInfo(alias, Paths.get(foundApp.get(SiblingNode.PATH)),
getExePath(foundApp.get(SiblingNode.PATH)), foundApp.get(SiblingNode.VERSION)
));
}
}
// Remove blacklisted paths
Iterator<AppInfo> appInfoIterator = appList.iterator();
while(appInfoIterator.hasNext()) {
AppInfo appInfo = appInfoIterator.next();
for(String listEntry : BLACKLISTED_PATHS) {
if (appInfo.getPath() != null && appInfo.getPath().toString().contains(listEntry)) {
appInfoIterator.remove();
}
}
}
return appList;
}
@Override
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
ArrayList<Path> processPaths = new ArrayList();
for (String pid : pids) {
Pointer buf = new Memory(SystemB.PROC_PIDPATHINFO_MAXSIZE);
SystemB.INSTANCE.proc_pidpath(Integer.parseInt(pid), buf, SystemB.PROC_PIDPATHINFO_MAXSIZE);
processPaths.add(Paths.get(buf.getString(0).trim()));
}
return processPaths;
}
/**
* Calculate executable path by parsing Contents/Info.plist
*/
private static Path getExePath(String appPath) {
Path path = Paths.get(appPath).toAbsolutePath().normalize();
Path plist = path.resolve("Contents/Info.plist");
Document doc;
try {
if(!plist.toFile().exists()) {
log.warn("Could not locate plist file for {}: {}", appPath, plist);
return null;
}
// Convert potentially binary plist files to XML
Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toString(), "-o", "-"}, ShellUtilities.envp);
doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream());
} catch(IOException | ParserConfigurationException | SAXException e) {
log.warn("Could not parse plist file for {}: {}", appPath, appPath, e);
return null;
}
doc.normalizeDocument();
boolean upNext = false;
NodeList nodeList = doc.getElementsByTagName("dict");
for (int i = 0; i < nodeList.getLength(); i++) {
NodeList dict = nodeList.item(i).getChildNodes();
for(int j = 0; j < dict.getLength(); j++) {
Node node = dict.item(j);
if ("key".equals(node.getNodeName()) && node.getTextContent().equals("CFBundleExecutable")) {
upNext = true;
} else if (upNext && "string".equals(node.getNodeName())) {
return path.resolve("Contents/MacOS/" + node.getTextContent());
}
}
}
return null;
}
private interface SystemB extends Library {
SystemB INSTANCE = Native.load("System", SystemB.class);
int PROC_ALL_PIDS = 1;
int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
int sysctlbyname(String name, Pointer oldp, IntByReference oldlenp, Pointer newp, int newlen);
int proc_listpids(int type, int typeinfo, int[] buffer, int buffersize);
int proc_pidpath(int pid, Pointer buffer, int buffersize);
}
}

View File

@@ -0,0 +1,142 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.certificate.firefox.locator;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.Psapi;
import com.sun.jna.platform.win32.Tlhelp32;
import com.sun.jna.platform.win32.WinNT;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
import qz.utils.WindowsUtilities;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Locale;
import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE;
public class WindowsAppLocator extends AppLocator{
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
private static String REG_TEMPLATE = "Software\\%s%s\\%s%s";
@Override
public ArrayList<AppInfo> locate(AppAlias appAlias) {
ArrayList<AppInfo> appList = new ArrayList<>();
for (Alias alias : appAlias.aliases) {
if (alias.getVendor() != null) {
String[] suffixes = new String[]{ "", " ESR"};
String[] prefixes = new String[]{ "", "WOW6432Node\\"};
for (String suffix : suffixes) {
for (String prefix : prefixes) {
String key = String.format(REG_TEMPLATE, prefix, alias.getVendor(), alias.getName(), suffix);
AppInfo appInfo = getAppInfo(alias, key, suffix);
if (appInfo != null && !appList.contains(appInfo)) {
appList.add(appInfo);
}
}
}
}
}
return appList;
}
@Override
public ArrayList<String> getPids(ArrayList<String> processNames) {
ArrayList<String> pidList = new ArrayList<>();
if (processNames.isEmpty()) return pidList;
Tlhelp32.PROCESSENTRY32 pe32 = new Tlhelp32.PROCESSENTRY32();
pe32.dwSize = new WinNT.DWORD(pe32.size());
// Fetch a snapshot of all processes
WinNT.HANDLE hSnapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinNT.DWORD(0));
if (hSnapshot.equals(WinNT.INVALID_HANDLE_VALUE)) {
log.warn("Process snapshot has invalid handle");
return pidList;
}
if (Kernel32.INSTANCE.Process32First(hSnapshot, pe32)) {
do {
String processName = Native.toString(pe32.szExeFile);
if(processNames.contains(processName.toLowerCase(Locale.ENGLISH))) {
pidList.add(pe32.th32ProcessID.toString());
}
} while (Kernel32.INSTANCE.Process32Next(hSnapshot, pe32));
}
Kernel32.INSTANCE.CloseHandle(hSnapshot);
return pidList;
}
@Override
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
ArrayList<Path> pathList = new ArrayList<>();
for(String pid : pids) {
WinNT.HANDLE hProcess = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION | WinNT.PROCESS_VM_READ, false, Integer.parseInt(pid));
if (hProcess == null) {
log.warn("Handle for PID {} is missing, skipping.", pid);
continue;
}
int bufferSize = WinNT.MAX_PATH;
Pointer buffer = new Memory(bufferSize * Native.WCHAR_SIZE);
if (Psapi.INSTANCE.GetModuleFileNameEx(hProcess, null, buffer, bufferSize) == 0) {
log.warn("Full path to PID {} is empty, skipping.", pid);
Kernel32.INSTANCE.CloseHandle(hProcess);
continue;
}
Kernel32.INSTANCE.CloseHandle(hProcess);
pathList.add(Paths.get(Native.WCHAR_SIZE == 1 ?
buffer.getString(0) :
buffer.getWideString(0)));
}
return pathList;
}
/**
* Use a proprietary Firefox-only technique for getting "PathToExe" registry value
*/
private static AppInfo getAppInfo(Alias alias, String key, String suffix) {
String version = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "CurrentVersion");
if (version != null) {
version = version.split(" ")[0]; // chop off (x86 ...)
if (!suffix.isEmpty()) {
if (key.endsWith(suffix)) {
key = key.substring(0, key.length() - suffix.length());
}
version = version + suffix;
}
String exePath = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe");
if (exePath != null) {
// SemVer: Replace spaces in suffixes with dashes
version = version.replaceAll(" ", "-");
return new AppInfo(alias, Paths.get(exePath), version);
} else {
log.warn("Couldn't locate \"PathToExe\" for \"{}\" in \"{}\", skipping", alias.getName(), key);
}
}
return null;
}
}

View File

@@ -0,0 +1,161 @@
package qz.installer.provision;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.Phase;
import qz.build.provision.params.types.Script;
import qz.build.provision.params.types.Software;
import qz.common.Constants;
import qz.installer.provision.invoker.*;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import static qz.common.Constants.*;
import static qz.utils.FileUtilities.*;
public class ProvisionInstaller {
protected static final Logger log = LogManager.getLogger(ProvisionInstaller.class);
private ArrayList<Step> steps;
static {
// Populate variables for scripting environment
ShellUtilities.addEnvp("APP_TITLE", ABOUT_TITLE,
"APP_VERSION", VERSION,
"APP_ABBREV", PROPS_FILE,
"APP_VENDOR", ABOUT_COMPANY,
"APP_VENDOR_ABBREV", DATA_DIR,
"APP_ARCH", SystemUtilities.getArch(),
"APP_OS", SystemUtilities.getOs(),
"APP_DIR", SystemUtilities.getAppPath(),
"APP_USER_DIR", USER_DIR,
"APP_SHARED_DIR", SHARED_DIR);
}
public ProvisionInstaller(Path relativePath) throws IOException, JSONException {
this(relativePath, relativePath.resolve(Constants.PROVISION_FILE).toFile());
}
public ProvisionInstaller(Path relativePath, File jsonFile) throws IOException, JSONException {
if(!jsonFile.exists()) {
log.info("Provision file not found '{}', skipping", jsonFile);
this.steps = new ArrayList<>();
return;
}
this.steps = parse(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8), relativePath);
}
/**
* Package private for internal testing only
* Assumes files located in ./resources/ subdirectory
*/
ProvisionInstaller(Class relativeClass, InputStream in) throws IOException, JSONException {
this(relativeClass, IOUtils.toString(in, StandardCharsets.UTF_8));
}
/**
* Package private for internal testing only
* Assumes files located in ./resources/ subdirectory
*/
ProvisionInstaller(Class relativeClass, String jsonData) throws JSONException {
this.steps = parse(jsonData, relativeClass);
}
public void invoke(Phase phase) {
for(Step step : this.steps) {
if(phase == null || step.getPhase() == phase) {
try {
invokeStep(step);
}
catch(Exception e) {
log.error("[PROVISION] Provisioning step failed '{}'", step, e);
}
}
}
}
public void invoke() {
invoke(null);
}
private static ArrayList<Step> parse(String jsonData, Object relativeObject) throws JSONException {
return parse(new JSONArray(jsonData), relativeObject);
}
private boolean invokeStep(Step step) throws Exception {
if(Os.matchesHost(step.getOs())) {
log.info("[PROVISION] Invoking step '{}'", step.toString());
} else {
log.info("[PROVISION] Skipping step for different OS '{}'", step.toString());
return false;
}
Invokable invoker;
switch(step.getType()) {
case CA:
invoker = new CaInvoker(step, PropertyInvoker.getProperties(step));
break;
case CERT:
invoker = new CertInvoker(step);
break;
case CONF:
invoker = new ConfInvoker(step);
break;
case SCRIPT:
invoker = new ScriptInvoker(step);
break;
case SOFTWARE:
invoker = new SoftwareInvoker(step);
break;
case REMOVER:
invoker = new RemoverInvoker(step);
break;
case RESOURCE:
invoker = new ResourceInvoker(step);
break;
case PREFERENCE:
invoker = new PropertyInvoker(step, PropertyInvoker.getPreferences(step));
break;
case PROPERTY:
invoker = new PropertyInvoker(step, PropertyInvoker.getProperties(step));
break;
default:
throw new UnsupportedOperationException("Type " + step.getType() + " is not yet supported.");
}
return invoker.invoke();
}
public ArrayList<Step> getSteps() {
return steps;
}
private static ArrayList<Step> parse(JSONArray jsonArray, Object relativeObject) throws JSONException {
ArrayList<Step> steps = new ArrayList<>();
for(int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonStep = jsonArray.getJSONObject(i);
try {
steps.add(Step.parse(jsonStep, relativeObject));
} catch(Exception e) {
log.warn("[PROVISION] Unable to add step '{}'", jsonStep, e);
}
}
return steps;
}
public static boolean shouldBeExecutable(Path path) {
return Script.parse(path) != null || Software.parse(path) != Software.UNKNOWN;
}
}

View File

@@ -0,0 +1,49 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.PropertyHelper;
import qz.utils.ArgValue;
import qz.utils.FileUtilities;
import java.io.File;
import java.io.IOException;
/**
* Combines ResourceInvoker and PropertyInvoker to deploy a file and set a property to its deployed path
*/
public class CaInvoker extends InvokableResource {
Step step;
PropertyHelper properties;
public CaInvoker(Step step, PropertyHelper properties) {
this.step = step;
this.properties = properties;
}
@Override
public boolean invoke() throws IOException {
// First, write our cert file
File caCert = dataToFile(step);
if(caCert == null) {
return false;
}
// Next, handle our property step
Step propsStep = step.clone();
// If the property already exists, snag it
String key = ArgValue.AUTHCERT_OVERRIDE.getMatch();
String value = caCert.getPath();
if (properties.containsKey(key)) {
value = properties.getProperty(key) + FileUtilities.FILE_SEPARATOR + value;
}
propsStep.setData(String.format("%s=%s", key, value));
if (new PropertyInvoker(propsStep, properties).invoke()) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.Constants;
import qz.utils.FileUtilities;
import java.io.File;
import static qz.utils.ArgParser.ExitStatus.*;
public class CertInvoker extends InvokableResource {
private Step step;
public CertInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File cert = dataToFile(step);
if(cert == null) {
return false;
}
return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS;
}
}

View File

@@ -0,0 +1,46 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.PropertyHelper;
import qz.utils.SystemUtilities;
import java.util.AbstractMap;
public class ConfInvoker extends PropertyInvoker {
public ConfInvoker(Step step) {
super(step, new PropertyHelper(calculateConfPath(step)));
}
public static String calculateConfPath(Step step) {
String relativePath = step.getArgs().get(0);
if(SystemUtilities.isMac()) {
return SystemUtilities.getJarParentPath().
resolve("../PlugIns/Java.runtime/Contents/Home/conf").
resolve(relativePath).
normalize()
.toString();
} else {
return SystemUtilities.getJarParentPath()
.resolve("runtime/conf")
.resolve(relativePath)
.normalize()
.toString();
}
}
@Override
public boolean invoke() {
Step step = getStep();
// Java uses the same "|" delimiter as we do, only parse one property at a time
AbstractMap.SimpleEntry<String, String> pair = parsePropertyPair(step, step.getData());
if (!pair.getValue().isEmpty()) {
properties.setProperty(pair);
if (properties.save()) {
log.info("Successfully provisioned '1' '{}'", step.getType());
return true;
}
log.error("An error occurred saving properties '{}' to file", step.getData());
}
return false;
}
}

View File

@@ -0,0 +1,10 @@
package qz.installer.provision.invoker;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public interface Invokable {
Logger log = LogManager.getLogger(Invokable.class);
boolean invoke() throws Exception;
}

View File

@@ -0,0 +1,63 @@
package qz.installer.provision.invoker;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.build.provision.Step;
import qz.build.provision.params.Type;
import qz.common.Constants;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
public abstract class InvokableResource implements Invokable {
static final Logger log = LogManager.getLogger(InvokableResource.class);
public static File dataToFile(Step step) throws IOException {
Path resourcePath = Paths.get(step.getData());
if(resourcePath.isAbsolute() || step.usingPath()) {
return pathResourceToFile(step);
}
if(step.usingClass()) {
return classResourceToFile(step);
}
return null;
}
/**
* Resolves the resource directly from file
*/
private static File pathResourceToFile(Step step) {
String resourcePath = step.getData();
Path dataPath = Paths.get(resourcePath);
return dataPath.isAbsolute() ? dataPath.toFile() : step.getRelativePath().resolve(resourcePath).toFile();
}
/**
* Copies resource from JAR to a temp file for use in installation
*/
private static File classResourceToFile(Step step) throws IOException {
// Resource may be inside the jar
InputStream in = step.getRelativeClass().getResourceAsStream("resources/" + step.getData());
if(in == null) {
log.warn("Resource '{}' is missing, skipping step", step.getData());
return null;
}
String suffix = "_" + Paths.get(step.getData()).getFileName().toString();
File destination = File.createTempFile(Constants.DATA_DIR + "_provision_", suffix);
Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
IOUtils.closeQuietly(in);
// Set scripts executable
if(step.getType() == Type.SCRIPT && !SystemUtilities.isWindows()) {
destination.setExecutable(true, false);
}
return destination;
}
}

View File

@@ -0,0 +1,99 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.common.Constants;
import qz.common.PropertyHelper;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
public class PropertyInvoker implements Invokable {
private Step step;
PropertyHelper properties;
public PropertyInvoker(Step step, PropertyHelper properties) {
this.step = step;
this.properties = properties;
}
public boolean invoke() {
HashMap<String, String> pairs = parsePropertyPairs(step);
if (!pairs.isEmpty()) {
for(Map.Entry<String, String> pair : pairs.entrySet()) {
properties.setProperty(pair);
}
if (properties.save()) {
log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType());
return true;
}
log.error("An error occurred saving properties '{}' to file", step.getData());
}
return false;
}
public static PropertyHelper getProperties(Step step) {
File propertiesFile;
if(step.getRelativePath() != null) {
// Assume qz-tray.properties is one directory up from provision folder
// required to prevent installing to payload
propertiesFile = step.getRelativePath().getParent().resolve(Constants.PROPS_FILE + ".properties").toFile();
} else {
// If relative path isn't set, fallback to the jar's parent path
propertiesFile = SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile();
}
log.info("Provisioning '{}' to properties file: '{}'", step.getData(), propertiesFile);
return new PropertyHelper(propertiesFile);
}
public static PropertyHelper getPreferences(Step step) {
return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
}
public static HashMap<String, String> parsePropertyPairs(Step step) {
HashMap<String, String> pairs = new HashMap<>();
if(step.getData() != null && !step.getData().trim().isEmpty()) {
String[] props = step.getData().split("\\|");
for(String prop : props) {
AbstractMap.SimpleEntry<String,String> pair = parsePropertyPair(step, prop);
if (pair != null) {
if(pairs.get(pair.getKey()) != null) {
log.warn("Property {} already exists, replacing [before: {}, after: {}] ",
pair.getKey(), pairs.get(pair.getKey()), pair.getValue());
}
pairs.put(pair.getKey(), pair.getValue());
}
}
} else {
log.error("Skipping Step '{}', Data is null or empty", step.getType());
}
return pairs;
}
public static AbstractMap.SimpleEntry<String, String> parsePropertyPair(Step step, String prop) {
if(prop.contains("=")) {
String[] pair = prop.split("=", 2);
if (!pair[0].trim().isEmpty()) {
if (!pair[1].trim().isEmpty()) {
return new AbstractMap.SimpleEntry<>(pair[0], pair[1]);
} else {
log.warn("Skipping '{}' '{}', property value is malformed", step.getType(), prop);
}
} else {
log.warn("Skipping '{}' '{}', property name is malformed", step.getType(), prop);
}
} else {
log.warn("Skipping '{}' '{}', property is malformed", step.getType(), prop);
}
return null;
}
public Step getStep() {
return step;
}
}

View File

@@ -0,0 +1,100 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Remover;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
public class RemoverInvoker extends InvokableResource {
private Step step;
private String aboutTitle; // e.g. "QZ Tray"
private String propsFile; // e.g. "qz-tray"
private String dataDir; // e.g. "qz"
public RemoverInvoker(Step step) {
this.step = step;
Remover remover = Remover.parse(step.getData());
if(remover == Remover.CUSTOM) {
// Fields are comma delimited in the data field
parseCustomFromData(step.getData());
} else {
aboutTitle = remover.getAboutTitle();
propsFile = remover.getPropsFile();
dataDir = remover.getDataDir();
}
}
@Override
public boolean invoke() throws Exception {
ArrayList<String> command = getRemoveCommand();
if(command.size() == 0) {
log.info("An existing installation of '{}' was not found. Skipping.", aboutTitle);
return true;
}
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
public void parseCustomFromData(String data) {
String[] parts = data.split(",");
aboutTitle = parts[0].trim();
propsFile = parts[1].trim();
dataDir = parts[2].trim();
}
/**
* Returns the installer command (including the installer itself and if needed, arguments) to
* invoke the installer file
*/
public ArrayList<String> getRemoveCommand() {
ArrayList<String> removeCmd = new ArrayList<>();
Os os = SystemUtilities.getOs();
switch(os) {
case WINDOWS:
Path win = Paths.get(System.getenv("PROGRAMFILES"))
.resolve(aboutTitle)
.resolve("uninstall.exe");
if(win.toFile().exists()) {
removeCmd.add(win.toString());
removeCmd.add("/S");
break;
}
case MAC:
Path legacy = Paths.get("/Applications")
.resolve(aboutTitle + ".app")
.resolve("Contents")
.resolve("uninstall");
Path mac = Paths.get("/Applications")
.resolve(aboutTitle + ".app")
.resolve("Contents")
.resolve("Resources")
.resolve("uninstall");
if(legacy.toFile().exists()) {
removeCmd.add(legacy.toString());
} else if(mac.toFile().exists()) {
removeCmd.add(mac.toString());
}
break;
default:
Path linux = Paths.get("/opt")
.resolve(propsFile)
.resolve("uninstall");
if(linux.toFile().exists()) {
removeCmd.add(linux.toString());
}
}
return removeCmd;
}
}

View File

@@ -0,0 +1,19 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
/**
* Stub class for deploying an otherwise "action-less" resource, only to be used by other tasks
*/
public class ResourceInvoker extends InvokableResource {
private Step step;
public ResourceInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
return dataToFile(step) != null;
}
}

View File

@@ -0,0 +1,77 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Script;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.ArrayList;
public class ScriptInvoker extends InvokableResource {
private Step step;
public ScriptInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File script = dataToFile(step);
if(script == null) {
return false;
}
Script engine = Script.parse(step.getData());
ArrayList<String> command = getInterpreter(engine);
if(command.isEmpty() && SystemUtilities.isWindows()) {
log.warn("No interpreter found for {}, skipping", step.getData());
return false;
}
command.add(script.toString());
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
/**
* Returns the interpreter command (and if needed, arguments) to invoke the script file
*
* An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3
* which will allow the OS to select the correct interpreter for the given file
*
* No special attention is given to "shebang", behavior may differ between OSs
*/
private static ArrayList<String> getInterpreter(Script engine) {
ArrayList<String> interpreter = new ArrayList<>();
Os osType = SystemUtilities.getOs();
switch(engine) {
case PS1:
if(osType == Os.WINDOWS) {
interpreter.add("powershell.exe");
} else if(osType == Os.MAC) {
interpreter.add("/usr/local/bin/pwsh");
} else {
interpreter.add("pwsh");
}
interpreter.add("-File");
break;
case PY:
interpreter.add(osType == Os.WINDOWS ? "python3.exe" : "python3");
break;
case BAT:
interpreter.add(osType == Os.WINDOWS ? "cmd.exe" : "wineconsole");
break;
case RB:
interpreter.add(osType == Os.WINDOWS ? "ruby.exe" : "ruby");
break;
case SH:
default:
// Allow the environment to parse it from the shebang at invocation time
}
return interpreter;
}
}

View File

@@ -0,0 +1,87 @@
package qz.installer.provision.invoker;
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.types.Software;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class SoftwareInvoker extends InvokableResource {
private Step step;
public SoftwareInvoker(Step step) {
this.step = step;
}
@Override
public boolean invoke() throws Exception {
File payload = dataToFile(step);
if(payload == null) {
return false;
}
Software installer = Software.parse(step.getData());
ArrayList<String> command = getInstallCommand(installer, step.getArgs(), payload);
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]), payload.getParentFile());
if(!success) {
log.error("An error occurred invoking [{}]", step.getData());
}
return success;
}
/**
* Returns the installer command (including the installer itself and if needed, arguments) to
* invoke the installer file
*/
public ArrayList<String> getInstallCommand(Software installer, List<String> args, File payload) {
ArrayList<String> interpreter = new ArrayList<>();
Os os = SystemUtilities.getOs();
switch(installer) {
case EXE:
if(!SystemUtilities.isWindows()) {
interpreter.add("wine");
}
// Executable on its own
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume exe args come after payload
break;
case MSI:
interpreter.add(os == Os.WINDOWS ? "msiexec.exe" : "msiexec");
interpreter.add("/i"); // Assume standard install
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume msiexec args come after payload
break;
case PKG:
if(os == Os.MAC) {
interpreter.add("installer");
interpreter.addAll(args); // Assume installer args come before payload
interpreter.add("-package");
interpreter.add(payload.toString());
interpreter.add("-target");
interpreter.add("/"); // Assume we don't want this on a removable volume
} else {
throw new UnsupportedOperationException("PKG is not yet supported on this platform");
}
break;
case DMG:
// DMG requires "hdiutil attach", but the mount point is unknown
throw new UnsupportedOperationException("DMG is not yet supported");
case RUN:
if(SystemUtilities.isWindows()) {
interpreter.add("bash");
interpreter.add("-c");
}
interpreter.add(payload.toString());
interpreter.addAll(args); // Assume run args come after payload
// Executable on its own
break;
default:
// We'll try to parse it from the shebang just before invocation time
}
return interpreter;
}
}

View File

@@ -0,0 +1,44 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.shortcut;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.installer.LinuxInstaller;
/**
* @author Tres Finocchiaro
*/
class LinuxShortcutCreator extends ShortcutCreator {
private static final Logger log = LogManager.getLogger(LinuxShortcutCreator.class);
private static String DESKTOP = System.getProperty("user.home") + "/Desktop/";
public boolean canAutoStart() {
return Files.exists(Paths.get(LinuxInstaller.STARTUP_DIR, LinuxInstaller.SHORTCUT_NAME));
}
public void createDesktopShortcut() {
copyShortcut(LinuxInstaller.APP_LAUNCHER, DESKTOP);
}
private static void copyShortcut(String source, String target) {
try {
Files.copy(Paths.get(source), Paths.get(target));
} catch(IOException e) {
log.warn("Error creating shortcut {}", target, e);
}
}
}

View File

@@ -0,0 +1,100 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.shortcut;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import qz.common.Constants;
import qz.utils.MacUtilities;
import qz.utils.SystemUtilities;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @author Tres Finocchiaro
*/
class MacShortcutCreator extends ShortcutCreator {
private static final Logger log = LogManager.getLogger(MacShortcutCreator.class);
private static String SHORTCUT_PATH = System.getProperty("user.home") + "/Desktop/" + Constants.ABOUT_TITLE;
/**
* Verify LaunchAgents plist file exists and parse it to verify it's enabled
*/
@Override
public boolean canAutoStart() {
// plist is stored as io.qz.plist
Path plistPath = Paths.get("/Library/LaunchAgents", MacUtilities.getBundleId() + ".plist");
if (Files.exists(plistPath)) {
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(plistPath.toFile());
doc.getDocumentElement().normalize();
NodeList dictList = doc.getElementsByTagName("dict");
// Loop to find "RunAtLoad" key, then the adjacent key
boolean foundItem = false;
if (dictList.getLength() > 0) {
NodeList children = dictList.item(0).getChildNodes();
for(int n = 0; n < children.getLength(); n++) {
Node item = children.item(n);
// Apple stores booleans as adjacent tags to their owner
if (foundItem) {
String nodeName = children.item(n).getNodeName();
log.debug("Found RunAtLoad value {}", nodeName);
return "true".equals(nodeName);
}
if (item.getNodeName().equals("key") && item.getTextContent().equals("RunAtLoad")) {
log.debug("Found RunAtLoad key in {}", plistPath);
foundItem = true;
}
}
}
log.warn("RunAtLoad was not in plist {}, autostart will not work.", plistPath);
}
catch(SAXException | IOException | ParserConfigurationException e) {
log.warn("Error reading plist {}, autostart will not work.", plistPath, e);
}
} else {
log.warn("No plist {} found, autostart will not work", plistPath);
}
return false;
}
public void createDesktopShortcut() {
try {
new File(SHORTCUT_PATH).delete();
if(SystemUtilities.getJarParentPath().endsWith("Contents")) {
// We're probably running from an .app bundle
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getAppPath());
} else {
// We're running from a mystery location, use the jar instead
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getJarPath());
}
} catch(IOException e) {
log.warn("Could not create desktop shortcut {}", SHORTCUT_PATH, e);
}
}
}

View File

@@ -0,0 +1,41 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.installer.shortcut;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.SystemUtilities;
/**
* Utility class for creating, querying and removing startup shortcuts and
* desktop shortcuts.
*
* @author Tres Finocchiaro
*/
public abstract class ShortcutCreator {
private static ShortcutCreator instance;
protected static final Logger log = LogManager.getLogger(ShortcutCreator.class);
public abstract boolean canAutoStart();
public abstract void createDesktopShortcut();
public static ShortcutCreator getInstance() {
if (instance == null) {
if (SystemUtilities.isWindows()) {
instance = new WindowsShortcutCreator();
} else if (SystemUtilities.isMac()) {
instance = new MacShortcutCreator();
} else {
instance = new LinuxShortcutCreator();
}
}
return instance;
}
}

View File

@@ -0,0 +1,60 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*
*/
package qz.installer.shortcut;
import com.sun.jna.platform.win32.Win32Exception;
import mslinks.ShellLinkException;
import mslinks.ShellLinkHelper;
import qz.common.Constants;
import qz.installer.WindowsSpecialFolders;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
/**
* @author Tres Finocchiaro
*/
public class WindowsShortcutCreator extends ShortcutCreator {
private static String SHORTCUT_NAME = Constants.ABOUT_TITLE + ".lnk";
public void createDesktopShortcut() {
createShortcut(WindowsSpecialFolders.DESKTOP.toString());
}
public boolean canAutoStart() {
try {
return Files.exists(Paths.get(WindowsSpecialFolders.COMMON_STARTUP.toString(), SHORTCUT_NAME));
} catch(Win32Exception e) {
log.warn("An exception occurred locating the startup folder; autostart cannot be determined.", e);
}
return false;
}
private void createShortcut(String folderPath) {
try {
ShellLinkHelper.createLink(getAppPath(), folderPath + File.separator + SHORTCUT_NAME);
}
catch(ShellLinkException | IOException ex) {
log.warn("Error creating desktop shortcut", ex);
}
}
/**
* Calculates .exe path from .jar
* fixme: overlaps SystemUtilities.getAppPath
*/
private static String getAppPath() {
return SystemUtilities.getJarPath().toString().replaceAll(".jar$", ".exe");
}
}

View File

@@ -0,0 +1,752 @@
package qz.printer;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.LoggerUtilities;
import qz.utils.PrintingUtilities;
import qz.utils.SystemUtilities;
import javax.print.attribute.ResolutionSyntax;
import javax.print.attribute.Size2DSyntax;
import javax.print.attribute.standard.Chromaticity;
import javax.print.attribute.standard.OrientationRequested;
import javax.print.attribute.standard.PrinterResolution;
import javax.print.attribute.standard.Sides;
import java.awt.*;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class PrintOptions {
private static final Logger log = LogManager.getLogger(PrintOptions.class);
private Pixel psOptions = new Pixel();
private Raw rawOptions = new Raw();
private Default defOptions = new Default();
/**
* Parses the provided JSON Object into relevant Pixel and Raw options
*/
public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities.Format format) {
if (configOpts == null) { return; }
//check for raw options
if (!configOpts.isNull("forceRaw")) {
rawOptions.forceRaw = configOpts.optBoolean("forceRaw", false);
} else if (!configOpts.isNull("altPrinting")) {
log.warn("Raw option \"altPrinting\" is deprecated. Please use \"forceRaw\" instead.");
rawOptions.forceRaw = configOpts.optBoolean("altPrinting", false);
}
if (rawOptions.forceRaw && SystemUtilities.isWindows()) {
log.warn("Forced raw printing is not supported on Windows");
rawOptions.forceRaw = false;
}
if (!configOpts.isNull("encoding")) {
JSONObject encodings = configOpts.optJSONObject("encoding");
if (encodings != null) {
rawOptions.srcEncoding = encodings.optString("from", null);
rawOptions.destEncoding = encodings.optString("to", null);
} else {
rawOptions.destEncoding = configOpts.optString("encoding", null);
}
}
if (!configOpts.isNull("spool")) {
JSONObject spool = configOpts.optJSONObject("spool");
if (spool != null) {
if (!spool.isNull("size")) {
try { rawOptions.spoolSize = spool.getInt("size"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); }
}
// TODO: Implement spool.start
if (!spool.isNull("end")) {
rawOptions.spoolEnd = spool.optString("end");
}
} else {
LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool"));
}
} else {
// Deprecated
if (!configOpts.isNull("perSpool")) {
try { rawOptions.spoolSize = configOpts.getInt("perSpool"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "perSpool", configOpts.opt("perSpool")); }
}
if (!configOpts.isNull("endOfDoc")) {
rawOptions.spoolEnd = configOpts.optString("endOfDoc", null);
}
}
if (!configOpts.isNull("copies")) {
try { rawOptions.copies = configOpts.getInt("copies"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); }
}
if (!configOpts.isNull("jobName")) {
rawOptions.jobName = configOpts.optString("jobName", null);
}
if (!configOpts.isNull("retainTemp")) {
rawOptions.retainTemp = configOpts.optBoolean("retainTemp", false);
}
//check for pixel options
if (!configOpts.isNull("units")) {
switch(configOpts.optString("units")) {
case "mm":
psOptions.units = Unit.MM; break;
case "cm":
psOptions.units = Unit.CM; break;
case "in":
psOptions.units = Unit.INCH; break;
default:
LoggerUtilities.optionWarn(log, "valid value", "units", configOpts.opt("units")); break;
}
}
if (!configOpts.isNull("bounds")) {
try {
JSONObject bounds = configOpts.getJSONObject("bounds");
psOptions.bounds = new Bounds(bounds.optDouble("x", 0), bounds.optDouble("y", 0), bounds.optDouble("width", 0), bounds.optDouble("height", 0));
}
catch(JSONException e) {
LoggerUtilities.optionWarn(log, "JSONObject", "bounds", configOpts.opt("bounds"));
}
}
if (!configOpts.isNull("colorType")) {
try {
psOptions.colorType = ColorType.valueOf(configOpts.optString("colorType").toUpperCase(Locale.ENGLISH));
}
catch(IllegalArgumentException e) {
LoggerUtilities.optionWarn(log, "valid value", "colorType", configOpts.opt("colorType"));
}
}
if (!configOpts.isNull("copies")) {
try { psOptions.copies = configOpts.getInt("copies"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); }
if (psOptions.copies < 1) {
log.warn("Cannot have less than one copy");
psOptions.copies = 1;
}
}
if (!configOpts.isNull("density")) {
JSONObject asymmDPI = configOpts.optJSONObject("density");
if (asymmDPI != null) {
psOptions.density = asymmDPI.optInt("feed");
psOptions.crossDensity = asymmDPI.optInt("cross");
} else {
List<PrinterResolution> rSupport = output.isSetService()?
output.getNativePrinter().getResolutions():new ArrayList<>();
JSONArray possibleDPIs = configOpts.optJSONArray("density");
if (possibleDPIs != null && possibleDPIs.length() > 0) {
PrinterResolution usableRes = null;
if (!rSupport.isEmpty()) {
for(int i = 0; i < possibleDPIs.length(); i++) {
PrinterResolution compareRes;
asymmDPI = possibleDPIs.optJSONObject(i);
if (asymmDPI != null) {
compareRes = new PrinterResolution(asymmDPI.optInt("cross"), asymmDPI.optInt("feed"), psOptions.units.resSyntax);
} else {
compareRes = new PrinterResolution(possibleDPIs.optInt(i), possibleDPIs.optInt(i), psOptions.units.resSyntax);
}
if (rSupport.contains(compareRes)) {
usableRes = compareRes;
break;
}
}
}
if (usableRes == null) {
log.warn("Supported printer densities not found, using first value provided");
asymmDPI = possibleDPIs.optJSONObject(0);
if (asymmDPI != null) {
psOptions.density = asymmDPI.optInt("feed");
psOptions.crossDensity = asymmDPI.optInt("cross");
} else {
psOptions.density = possibleDPIs.optInt(0);
}
} else {
psOptions.density = usableRes.getFeedResolution(psOptions.units.resSyntax);
psOptions.crossDensity = usableRes.getCrossFeedResolution(psOptions.units.resSyntax);
}
} else {
String relDPI = configOpts.optString("density", "").toLowerCase(Locale.ENGLISH);
if ("best".equals(relDPI)) {
PrinterResolution bestRes = null;
for(PrinterResolution pr : rSupport) {
if (bestRes == null || !pr.lessThanOrEquals(bestRes)) {
bestRes = pr;
}
}
if (bestRes != null) {
psOptions.density = bestRes.getFeedResolution(psOptions.units.resSyntax);
psOptions.crossDensity = bestRes.getCrossFeedResolution(psOptions.units.resSyntax);
} else {
log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI);
}
} else if ("draft".equals(relDPI)) {
PrinterResolution lowestRes = null;
for(PrinterResolution pr : rSupport) {
if (lowestRes == null || pr.lessThanOrEquals(lowestRes)) {
lowestRes = pr;
}
}
if (lowestRes != null) {
psOptions.density = lowestRes.getFeedResolution(psOptions.units.resSyntax);
psOptions.crossDensity = lowestRes.getCrossFeedResolution(psOptions.units.resSyntax);
} else {
log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI);
}
} else {
try { psOptions.density = configOpts.getDouble("density"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "density", configOpts.opt("density")); }
}
}
}
}
if (!configOpts.isNull("dithering")) {
try {
if (configOpts.getBoolean("dithering")) {
psOptions.dithering = RenderingHints.VALUE_DITHER_ENABLE;
} else {
psOptions.dithering = RenderingHints.VALUE_DITHER_DISABLE;
}
}
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "dithering", configOpts.opt("dithering")); }
}
if (!configOpts.isNull("duplex")) {
try {
if (configOpts.getBoolean("duplex")) {
psOptions.duplex = Sides.DUPLEX;
}
}
catch(JSONException e) {
//not a boolean, try as a string
try {
String duplex = configOpts.getString("duplex").toLowerCase(Locale.ENGLISH);
if (duplex.matches("^(duplex|(two.sided.)?long(.edge)?)$")) {
psOptions.duplex = Sides.DUPLEX;
} else if (duplex.matches("^(tumble|(two.sided.)?short(.edge)?)$")) {
psOptions.duplex = Sides.TUMBLE;
}
//else - one sided (default)
}
catch(JSONException e2) { LoggerUtilities.optionWarn(log, "valid value", "duplex", configOpts.opt("duplex")); }
}
}
if (!configOpts.isNull("interpolation")) {
switch(configOpts.optString("interpolation")) {
case "bicubic":
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; break;
case "bilinear":
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR; break;
case "nearest-neighbor":
case "nearest":
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; break;
default:
LoggerUtilities.optionWarn(log, "valid value", "interpolation", configOpts.opt("interpolation")); break;
}
}
if (!configOpts.isNull("jobName")) {
psOptions.jobName = configOpts.optString("jobName", null);
}
if (!configOpts.isNull("legacy")) {
psOptions.legacy = configOpts.optBoolean("legacy", false);
}
if (!configOpts.isNull("margins")) {
Margins m = new Margins();
JSONObject subMargins = configOpts.optJSONObject("margins");
if (subMargins != null) {
//each individually
if (!subMargins.isNull("top")) {
try { m.top = subMargins.getDouble("top"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.top", subMargins.opt("top")); }
}
if (!subMargins.isNull("right")) {
try { m.right = subMargins.getDouble("right"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.right", subMargins.opt("right")); }
}
if (!subMargins.isNull("bottom")) {
try { m.bottom = subMargins.getDouble("bottom"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.bottom", subMargins.opt("bottom")); }
}
if (!subMargins.isNull("left")) {
try { m.left = subMargins.getDouble("left"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.left", subMargins.opt("left")); }
}
} else {
try { m.setAll(configOpts.getDouble("margins")); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins", configOpts.opt("margins")); }
}
psOptions.margins = m;
}
if (!configOpts.isNull("orientation")) {
try {
psOptions.orientation = Orientation.valueOf(configOpts.optString("orientation").replaceAll("-", "_").toUpperCase(Locale.ENGLISH));
}
catch(IllegalArgumentException e) {
LoggerUtilities.optionWarn(log, "valid value", "orientation", configOpts.opt("orientation"));
}
}
if (!configOpts.isNull("paperThickness")) {
try { psOptions.paperThickness = configOpts.getDouble("paperThickness"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "paperThickness", configOpts.opt("paperThickness")); }
}
if (!configOpts.isNull("spool")) {
JSONObject spool = configOpts.optJSONObject("spool");
if (spool != null) {
if (!spool.isNull("size")) {
try { psOptions.spoolSize = spool.getInt("size"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); }
}
} else {
LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool"));
}
}
if (!configOpts.isNull("printerTray")) {
psOptions.printerTray = configOpts.optString("printerTray", null);
// Guard empty string value; will break pattern matching
if(psOptions.printerTray != null && psOptions.printerTray.trim().equals("")) {
psOptions.printerTray = null;
}
}
if (!configOpts.isNull("rasterize")) {
try { psOptions.rasterize = configOpts.getBoolean("rasterize"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "rasterize", configOpts.opt("rasterize")); }
}
if (!configOpts.isNull("rotation")) {
try { psOptions.rotation = configOpts.getDouble("rotation"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "rotation", configOpts.opt("rotation")); }
}
if (!configOpts.isNull("scaleContent")) {
try { psOptions.scaleContent = configOpts.getBoolean("scaleContent"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "scaleContent", configOpts.opt("scaleContent")); }
}
if (!configOpts.isNull("size")) {
Size s = new Size();
JSONObject subSize = configOpts.optJSONObject("size");
if (subSize != null) {
if (!subSize.isNull("width")) {
try { s.width = subSize.getDouble("width"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.width", subSize.opt("width")); }
}
if (!subSize.isNull("height")) {
try { s.height = subSize.getDouble("height"); }
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.height", subSize.opt("height")); }
}
if (s.height <= 0 && s.width <= 0) {
log.warn("Page size has been set without dimensions, using default");
} else {
psOptions.size = s;
}
} else {
LoggerUtilities.optionWarn(log, "JSONObject", "size", configOpts.opt("size"));
}
}
//grab any useful service defaults
PrinterResolution defaultRes = null;
if (output.isSetService()) {
defaultRes = output.getNativePrinter().getResolution().value();
if (defaultRes == null) {
//printer has no default resolution set, see if it is possible to pull anything
List<PrinterResolution> rSupport = output.getNativePrinter().getResolutions();
if (rSupport.size() > 0) {
defaultRes = rSupport.get(0);
log.warn("Default resolution for {} is missing, using fallback: {}", output.getNativePrinter().getName(), defaultRes);
} else {
log.warn("Default resolution for {} is missing, no fallback available.", output.getNativePrinter().getName());
}
}
}
if (defaultRes != null) {
//convert dphi to unit-dependant density ourselves (to keep as double type)
defOptions.density = (double)defaultRes.getFeedResolution(1) / psOptions.getUnits().getDPIUnits();
} else {
try { defOptions.density = configOpts.getDouble("fallbackDensity"); }
catch(JSONException e) {
LoggerUtilities.optionWarn(log, "double", "fallbackDensity", configOpts.opt("fallbackDensity"));
//manually convert default dphi to a density value based on units
defOptions.density = 60000d / psOptions.getUnits().getDPIUnits();
}
}
if ((psOptions.isRasterize() || format == PrintingUtilities.Format.IMAGE) && psOptions.getDensity() <= 1) {
psOptions.density = defOptions.density;
psOptions.crossDensity = defOptions.density;
}
if (output.isSetService()) {
try {
PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(output.getPrintService());
PageFormat page = job.getPageFormat(null);
defOptions.pageSize = new Size(page.getWidth(), page.getHeight());
}
catch(PrinterException e) {
log.warn("Unable to find the default paper size");
}
}
}
public Raw getRawOptions() {
return rawOptions;
}
public Pixel getPixelOptions() {
return psOptions;
}
public Default getDefaultOptions() { return defOptions; }
// Option groups //
/** Raw printing options */
public class Raw {
private boolean forceRaw = false; //Alternate printing for linux systems
private String destEncoding = null; //Text encoding / charset
private String srcEncoding = null; //Conversion text encoding
private String spoolEnd = null; //End of document character(s)
private int spoolSize = 1; //Pages per spool
private int copies = 1; //Job copies
private String jobName = null; //Job name
private boolean retainTemp = false; //Retain any temporary files
public boolean isForceRaw() {
return forceRaw;
}
public String getDestEncoding() {
return destEncoding;
}
public String getSrcEncoding() {
return srcEncoding;
}
public String getSpoolEnd() {
return spoolEnd;
}
public int getSpoolSize() {
return spoolSize;
}
public int getCopies() {
return copies;
}
public boolean isRetainTemp() { return retainTemp; }
public String getJobName(String defaultVal) {
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
}
}
/** Pixel printing options */
public class Pixel {
private Bounds bounds = null; //Bounding box rectangle
private ColorType colorType = ColorType.COLOR; //Color / black&white
private int copies = 1; //Job copies
private double crossDensity = 0; //Cross feed density
private double density = 0; //Pixel density (DPI or DPMM), feed density if crossDensity is defined
private Object dithering = RenderingHints.VALUE_DITHER_DEFAULT; //Image dithering
private Sides duplex = Sides.ONE_SIDED; //Multi-siding
private Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; //Image interpolation
private String jobName = null; //Job name
private boolean legacy = false; //Legacy printing
private Margins margins = new Margins(); //Page margins
private Orientation orientation = null; //Page orientation
private double paperThickness = -1; //Paper thickness
private int spoolSize = 0; //Pages before sending to printer
private String printerTray = null; //Printer tray to use
private boolean rasterize = true; //Whether documents are rasterized before printing
private double rotation = 0; //Image rotation
private boolean scaleContent = true; //Adjust paper size for best image fit
private Size size = null; //Paper size
private Unit units = Unit.INCH; //Units for density, margins, size
public Bounds getBounds() {
return bounds;
}
public ColorType getColorType() {
return colorType;
}
public int getCopies() {
return copies;
}
public double getCrossDensity() {
return crossDensity;
}
public double getDensity() {
return density;
}
public Object getDithering() {
return dithering;
}
public Sides getDuplex() {
return duplex;
}
public Object getInterpolation() {
return interpolation;
}
public String getJobName(String defaultVal) {
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
}
public boolean isLegacy() {
return legacy;
}
public Margins getMargins() {
return margins;
}
public Orientation getOrientation() {
return orientation;
}
public double getPaperThickness() {
return paperThickness;
}
public int getSpoolSize() {
return spoolSize;
}
public String getPrinterTray() {
return printerTray;
}
public boolean isRasterize() {
return rasterize;
}
public double getRotation() {
return rotation;
}
public boolean isScaleContent() {
return scaleContent;
}
public Size getSize() {
return size;
}
public Unit getUnits() {
return units;
}
}
/** PrintService Defaults **/
public class Default {
private double density;
private Size pageSize;
public double getDensity() {
return density;
}
public Size getPageSize() {
return pageSize;
}
}
// Sub options //
/** Pixel page size options */
public class Size {
private double width = -1; //Page width
private double height = -1; //Page height
public Size() {}
public Size(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
}
/** Pixel page margins options */
public class Margins {
private double top = 0; //Top page margin
private double right = 0; //Right page margin
private double bottom = 0; //Bottom page margin
private double left = 0; //Left page margin
private void setAll(double margin) {
top = margin;
right = margin;
bottom = margin;
left = margin;
}
public double top() {
return top;
}
public double right() {
return right;
}
public double bottom() {
return bottom;
}
public double left() {
return left;
}
}
/* Bounding box generic rectangle */
public class Bounds {
private double x;
private double y;
private double width;
private double height;
public Bounds(double x, double y, double width, double height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
}
/** Pixel dimension values */
public enum Unit {
INCH(ResolutionSyntax.DPI, 1.0f, 1.0f, Size2DSyntax.INCH), //1in = 1in
CM(ResolutionSyntax.DPCM, .3937f, 2.54f, 10000), //1cm = .3937in ; 1in = 2.54cm
MM(ResolutionSyntax.DPCM * 10, .03937f, 25.4f, Size2DSyntax.MM); //1mm = .03937in ; 1in = 25.4mm
private final float fromInch;
private final float toInch; //multiplicand to convert to inches
private final int resSyntax;
private final int µm;
Unit(int resSyntax, float toIN, float fromIN, int µm) {
toInch = toIN;
fromInch = fromIN;
this.resSyntax = resSyntax;
this.µm = µm;
}
public float toInches() {
return toInch;
}
public float as1Inch() {
return fromInch;
}
public int getDPIUnits() {
return resSyntax;
}
public int getMediaSizeUnits() {
return µm;
}
}
/** Pixel page orientation option */
public enum Orientation {
PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 0),
REVERSE_PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 180),
LANDSCAPE(OrientationRequested.LANDSCAPE, PageFormat.LANDSCAPE, 270),
REVERSE_LANDSCAPE(OrientationRequested.REVERSE_LANDSCAPE, PageFormat.REVERSE_LANDSCAPE, 90);
private final OrientationRequested orientationRequested;
private final int orientationFormat;
private final int degreesRot;
Orientation(OrientationRequested orientationRequested, int orientationFormat, int degreesRot) {
this.orientationRequested = orientationRequested;
this.orientationFormat = orientationFormat;
this.degreesRot = degreesRot;
}
public OrientationRequested getAsOrientRequested() {
return orientationRequested;
}
public int getAsOrientFormat() {
return orientationFormat;
}
public int getDegreesRot() {
return degreesRot;
}
}
/** Pixel page color option */
public enum ColorType {
COLOR(Chromaticity.COLOR),
GREYSCALE(Chromaticity.MONOCHROME),
GRAYSCALE(Chromaticity.MONOCHROME),
BLACKWHITE(Chromaticity.MONOCHROME),
DEFAULT(null);
private final Chromaticity chromatic;
ColorType(Chromaticity chromatic) {
this.chromatic = chromatic;
}
public Chromaticity getAsChromaticity() {
return chromatic;
}
}
}

View File

@@ -0,0 +1,92 @@
package qz.printer;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.printer.info.NativePrinter;
import qz.utils.FileUtilities;
import javax.print.PrintService;
import javax.print.attribute.standard.Media;
import java.io.File;
import java.nio.file.Paths;
public class PrintOutput {
private NativePrinter printer = null;
private File file = null;
private String host = null;
private int port = -1;
public PrintOutput(JSONObject configPrinter) throws JSONException, IllegalArgumentException {
if (configPrinter == null) { return; }
if (configPrinter.has("name")) {
printer = PrintServiceMatcher.matchPrinter(configPrinter.getString("name"));
if (printer == null) {
throw new IllegalArgumentException("Cannot find printer with name \"" + configPrinter.getString("name") + "\"");
}
}
if (configPrinter.has("file")) {
String filename = configPrinter.getString("file");
if (!FileUtilities.isGoodExtension(Paths.get(filename))) {
throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited file extension)");
} else if (FileUtilities.isBadPath(filename)) {
throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited directory name)");
} else {
file = new File(filename);
}
}
if (configPrinter.has("host")) {
host = configPrinter.getString("host");
port = configPrinter.optInt("port", 9100); // default to port 9100 (HP/JetDirect standard) if not provided
}
//at least one method must be set for printing
if (!isSetService() && !isSetFile() && !isSetHost()) {
throw new IllegalArgumentException("No printer output has been specified");
}
}
public boolean isSetService() {
return printer != null && printer.getPrintService() != null && !printer.getPrintService().isNull();
}
public PrintService getPrintService() {
return printer.getPrintService().value();
}
public NativePrinter getNativePrinter() {
return printer;
}
public boolean isSetFile() {
return file != null;
}
public File getFile() {
return file;
}
public boolean isSetHost() {
return host != null;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public Media[] getSupportedMedia() {
return (Media[])getPrintService().getSupportedAttributeValues(Media.class, null, null);
}
}

View File

@@ -0,0 +1,242 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.printer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.printer.info.CachedPrintServiceLookup;
import qz.printer.info.NativePrinter;
import qz.printer.info.NativePrinterMap;
import qz.utils.SystemUtilities;
import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.print.attribute.ResolutionSyntax;
import javax.print.attribute.standard.*;
import java.util.*;
public class PrintServiceMatcher {
private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class);
// PrintService is slow in CUPS, use a cache instead per JDK-7001133
// TODO: Include JDK version test for caching when JDK-7001133 is fixed upstream
private static final boolean useCache = SystemUtilities.isUnix();
public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) {
NativePrinterMap printers = NativePrinterMap.getInstance();
printers.putAll(true, lookupPrintServices());
if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); }
if (!silent) { log.debug("Found {} printers", printers.size()); }
return printers;
}
private static PrintService[] lookupPrintServices() {
return useCache ? CachedPrintServiceLookup.lookupPrintServices() :
PrintServiceLookup.lookupPrintServices(null, null);
}
private static PrintService lookupDefaultPrintService() {
return useCache ? CachedPrintServiceLookup.lookupDefaultPrintService() :
PrintServiceLookup.lookupDefaultPrintService();
}
public static NativePrinterMap getNativePrinterList(boolean silent) {
return getNativePrinterList(silent, false);
}
public static NativePrinterMap getNativePrinterList() {
return getNativePrinterList(false);
}
public static NativePrinter getDefaultPrinter() {
PrintService defaultService = lookupDefaultPrintService();
if(defaultService == null) {
return null;
}
NativePrinterMap printers = NativePrinterMap.getInstance();
if (!printers.contains(defaultService)) {
printers.putAll(false, defaultService);
}
return printers.get(defaultService);
}
public static String findPrinterName(String query) throws JSONException {
NativePrinter printer = PrintServiceMatcher.matchPrinter(query);
if (printer != null) {
return printer.getPrintService().value().getName();
} else {
return null;
}
}
/**
* Finds {@code PrintService} by looking at any matches to {@code printerSearch}.
*
* @param printerSearch Search query to compare against service names.
*/
public static NativePrinter matchPrinter(String printerSearch, boolean silent) {
NativePrinter exact = null;
NativePrinter begins = null;
NativePrinter partial = null;
if (!silent) { log.debug("Searching for PrintService matching {}", printerSearch); }
// Fix for https://github.com/qzind/tray/issues/931
// This is more than an optimization, removal will lead to a regression
NativePrinter defaultPrinter = getDefaultPrinter();
if (defaultPrinter != null && printerSearch.equals(defaultPrinter.getName())) {
if (!silent) { log.debug("Matched default printer, skipping further search"); }
return defaultPrinter;
}
printerSearch = printerSearch.toLowerCase(Locale.ENGLISH);
// Search services for matches
for(NativePrinter printer : getNativePrinterList(silent).values()) {
if (printer.getName() == null) {
continue;
}
String printerName = printer.getName().toLowerCase(Locale.ENGLISH);
if (printerName.equals(printerSearch)) {
exact = printer;
break;
}
if (printerName.startsWith(printerSearch)) {
begins = printer;
continue;
}
if (printerName.contains(printerSearch)) {
partial = printer;
continue;
}
if (SystemUtilities.isMac()) {
// 1.9 compat: fallback for old style names
PrinterName name = printer.getLegacyName();
if (name == null || name.getValue() == null) { continue; }
printerName = name.getValue().toLowerCase(Locale.ENGLISH);
if (printerName.equals(printerSearch)) {
exact = printer;
continue;
}
if (printerName.startsWith(printerSearch)) {
begins = printer;
continue;
}
if (printerName.contains(printerSearch)) {
partial = printer;
}
}
}
// Return closest match
NativePrinter use = null;
if (exact != null) {
use = exact;
} else if (begins != null) {
use = begins;
} else if (partial != null) {
use = partial;
}
if (use != null) {
if(!silent) log.debug("Found match: {}", use.getPrintService().value().getName());
} else {
log.warn("Printer not found: {}", printerSearch);
}
return use;
}
public static NativePrinter matchPrinter(String printerSearch) {
return matchPrinter(printerSearch, false);
}
public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException {
JSONArray list = new JSONArray();
PrintService defaultService = lookupDefaultPrintService();
boolean mediaTrayCrawled = false;
for(NativePrinter printer : getNativePrinterList().values()) {
PrintService ps = printer.getPrintService().value();
JSONObject jsonService = new JSONObject();
jsonService.put("name", ps.getName());
if (includeDetails) {
jsonService.put("driver", printer.getDriver().value());
jsonService.put("connection", printer.getConnection());
jsonService.put("default", ps == defaultService);
if (!mediaTrayCrawled) {
log.info("Gathering printer MediaTray information...");
mediaTrayCrawled = true;
}
HashSet<String> uniqueSizes = new HashSet<>(); // prevents duplicates
JSONArray trays = new JSONArray();
JSONArray sizes = new JSONArray();
for(Media m : (Media[])ps.getSupportedAttributeValues(Media.class, null, null)) {
if (m instanceof MediaTray) { trays.put(m.toString()); }
if (m instanceof MediaSizeName) {
if(uniqueSizes.add(m.toString())) {
MediaSize mediaSize = MediaSize.getMediaSizeForName((MediaSizeName)m);
if(mediaSize == null) {
continue;
}
JSONObject size = new JSONObject();
size.put("name", m.toString());
JSONObject in = new JSONObject();
in.put("width", mediaSize.getX(MediaPrintableArea.INCH));
in.put("height", mediaSize.getY(MediaPrintableArea.INCH));
size.put("in", in);
JSONObject mm = new JSONObject();
mm.put("width", mediaSize.getX(MediaPrintableArea.MM));
mm.put("height", mediaSize.getY(MediaPrintableArea.MM));
size.put("mm", mm);
sizes.put(size);
}
}
}
if(trays.length() > 0) {
jsonService.put("trays", trays);
}
if(sizes.length() > 0) {
jsonService.put("sizes", sizes);
}
PrinterResolution res = printer.getResolution().value();
int density = -1; if (res != null) { density = res.getFeedResolution(ResolutionSyntax.DPI); }
jsonService.put("density", density);
}
list.put(jsonService);
}
return list;
}
}

View File

@@ -0,0 +1,97 @@
package qz.printer.action;
import org.apache.commons.codec.binary.Base64InputStream;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.printer.PrintOptions;
import qz.printer.PrintOutput;
import qz.utils.PrintingUtilities;
import javax.print.DocFlavor;
import javax.print.DocPrintJob;
import javax.print.PrintException;
import javax.print.SimpleDoc;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.standard.JobName;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Locale;
public class PrintDirect extends PrintRaw {
private static final Logger log = LogManager.getLogger(PrintDirect.class);
private ArrayList<String> prints = new ArrayList<>();
private ArrayList<PrintingUtilities.Flavor> flavors = new ArrayList<>();
@Override
public PrintingUtilities.Format getFormat() {
return PrintingUtilities.Format.DIRECT;
}
@Override
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
for(int i = 0; i < printData.length(); i++) {
JSONObject data = printData.optJSONObject(i);
if (data == null) { continue; }
prints.add(data.getString("data"));
flavors.add(PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN));
}
}
@Override
public void print(PrintOutput output, PrintOptions options) throws PrintException {
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
attributes.add(new JobName(options.getRawOptions().getJobName(Constants.RAW_PRINT), Locale.getDefault()));
for(int i = 0; i < prints.size(); i++) {
DocPrintJob printJob = output.getPrintService().createPrintJob();
InputStream stream = null;
try {
switch(flavors.get(i)) {
case BASE64:
stream = new Base64InputStream(new ByteArrayInputStream(prints.get(i).getBytes("UTF-8")));
break;
case FILE:
stream = new DataInputStream(new URL(prints.get(i)).openStream());
break;
case PLAIN:
default:
stream = new ByteArrayInputStream(prints.get(i).getBytes("UTF-8"));
break;
}
SimpleDoc doc = new SimpleDoc(stream, DocFlavor.INPUT_STREAM.AUTOSENSE, null);
waitForPrint(printJob, doc, attributes);
}
catch(IOException e) {
throw new PrintException(e);
}
finally {
if (stream != null) {
try { stream.close(); } catch(Exception ignore) {}
}
}
}
}
@Override
public void cleanup() {
prints.clear();
flavors.clear();
}
}

View File

@@ -0,0 +1,414 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.printer.action;
import com.sun.javafx.print.PrintHelper;
import com.sun.javafx.print.Units;
import javafx.print.*;
import org.apache.commons.io.IOUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.printer.PrintOptions;
import qz.printer.PrintOutput;
import qz.printer.action.html.WebApp;
import qz.printer.action.html.WebAppModel;
import qz.utils.PrintingUtilities;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.standard.Copies;
import javax.print.attribute.standard.CopiesSupported;
import javax.print.attribute.standard.Sides;
import javax.swing.*;
import java.awt.*;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class PrintHTML extends PrintImage implements PrintProcessor {
private static final Logger log = LogManager.getLogger(PrintHTML.class);
private List<WebAppModel> models;
private JLabel legacyLabel = null;
public PrintHTML() {
super();
models = new ArrayList<>();
}
@Override
public PrintingUtilities.Format getFormat() {
return PrintingUtilities.Format.HTML;
}
@Override
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
try {
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
if (!pxlOpts.isLegacy()) {
WebApp.initialize();
}
for(int i = 0; i < printData.length(); i++) {
JSONObject data = printData.getJSONObject(i);
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
String source;
switch(flavor) {
case FILE:
case PLAIN:
// We'll toggle between 'plain' and 'file' when we construct WebAppModel
source = data.getString("data");
break;
default:
source = new String(flavor.read(data.getString("data")), StandardCharsets.UTF_8);
}
double pageZoom = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch()) / 72.0;
if (pageZoom <= 1) { pageZoom = 1; }
double pageWidth = 0;
double pageHeight = 0;
double convertFactor = (72.0 / pxlOpts.getUnits().as1Inch());
boolean renderFromHeight = Arrays.asList(PrintOptions.Orientation.LANDSCAPE,
PrintOptions.Orientation.REVERSE_LANDSCAPE).contains(pxlOpts.getOrientation());
if (pxlOpts.getSize() != null) {
if (!renderFromHeight) {
pageWidth = pxlOpts.getSize().getWidth() * convertFactor;
} else {
pageWidth = pxlOpts.getSize().getHeight() * convertFactor;
}
} else if (options.getDefaultOptions().getPageSize() != null) {
if (!renderFromHeight) {
pageWidth = options.getDefaultOptions().getPageSize().getWidth();
} else {
pageWidth = options.getDefaultOptions().getPageSize().getHeight();
}
}
if (pxlOpts.getMargins() != null) {
PrintOptions.Margins margins = pxlOpts.getMargins();
if (!renderFromHeight || pxlOpts.isRasterize()) {
pageWidth -= (margins.left() + margins.right()) * convertFactor;
} else {
pageWidth -= (margins.top() + margins.bottom()) * convertFactor; //due to vector margin matching
}
}
if (!data.isNull("options")) {
JSONObject dataOpt = data.getJSONObject("options");
if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) {
pageWidth = dataOpt.optDouble("pageWidth") * convertFactor;
}
if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) {
pageHeight = dataOpt.optDouble("pageHeight") * convertFactor;
}
}
models.add(new WebAppModel(source, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, pxlOpts.isScaleContent(), pageZoom));
}
log.debug("Parsed {} html records", models.size());
}
catch(IOException e) {
throw new UnsupportedOperationException("Unable to start JavaFX service", e);
}
catch(NoClassDefFoundError e) {
throw new UnsupportedOperationException("JavaFX libraries not found", e);
}
}
@Override
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
if (options.getPixelOptions().isLegacy()) {
printLegacy(output, options);
} else if (options.getPixelOptions().isRasterize()) {
//grab a snapshot of the pages for PrintImage instead of printing directly
for(WebAppModel model : models) {
try { images.add(WebApp.raster(model)); }
catch(Throwable t) {
if (model.getZoom() > 1 && t instanceof IllegalArgumentException) {
//probably a unrecognized image loader error, try at default zoom
try {
log.warn("Capture failed with increased zoom, attempting with default value");
model.setZoom(1);
images.add(WebApp.raster(model));
}
catch(Throwable tt) {
throw new PrinterException(tt.getMessage());
}
} else {
throw new PrinterException(t.getMessage());
}
}
}
super.print(output, options);
} else {
Printer fxPrinter = null;
for(Printer p : Printer.getAllPrinters()) {
if (p.getName().equals(output.getPrintService().getName())) {
fxPrinter = p;
break;
}
}
if (fxPrinter == null) {
throw new PrinterException("Cannot find printer under the JavaFX libraries");
}
PrinterJob job = PrinterJob.createPrinterJob(fxPrinter);
// apply option settings
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
JobSettings settings = job.getJobSettings();
settings.setJobName(pxlOpts.getJobName(Constants.HTML_PRINT));
settings.setPrintQuality(PrintQuality.HIGH);
// If colortype is default, leave printColor blank. The system's printer settings will be used instead.
if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) {
settings.setPrintColor(getColor(pxlOpts));
}
if (pxlOpts.getDuplex() == Sides.DUPLEX || pxlOpts.getDuplex() == Sides.TWO_SIDED_LONG_EDGE) {
settings.setPrintSides(PrintSides.DUPLEX);
}
if (pxlOpts.getDuplex() == Sides.TUMBLE || pxlOpts.getDuplex() == Sides.TWO_SIDED_SHORT_EDGE) {
settings.setPrintSides(PrintSides.TUMBLE);
}
if (pxlOpts.getPrinterTray() != null) {
PaperSource tray = findFXTray(fxPrinter.getPrinterAttributes().getSupportedPaperSources(), pxlOpts.getPrinterTray());
if (tray != null) {
settings.setPaperSource(tray);
}
}
if (pxlOpts.getDensity() > 0) {
settings.setPrintResolution(PrintHelper.createPrintResolution((int)pxlOpts.getDensity(), (int)pxlOpts.getDensity()));
}
Paper paper;
if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) {
double convert = 1;
Units units = getUnits(pxlOpts);
if (units == null) {
convert = 10; //need to adjust from cm to mm only for DPCM sizes
units = Units.MM;
}
paper = PrintHelper.createPaper("Custom", pxlOpts.getSize().getWidth() * convert, pxlOpts.getSize().getHeight() * convert, units);
} else {
PrintOptions.Size paperSize = options.getDefaultOptions().getPageSize();
paper = PrintHelper.createPaper("Default", paperSize.getWidth(), paperSize.getHeight(), Units.POINT);
}
PageOrientation orient = fxPrinter.getPrinterAttributes().getDefaultPageOrientation();
if (pxlOpts.getOrientation() != null) {
orient = getOrientation(pxlOpts);
}
try {
PageLayout layout;
PrintOptions.Margins m = pxlOpts.getMargins();
if (m != null) {
//force access to the page layout constructor as the adjusted margins on small sizes are wildly inaccurate
Constructor<PageLayout> plCon = PageLayout.class.getDeclaredConstructor(Paper.class, PageOrientation.class, double.class, double.class, double.class, double.class);
plCon.setAccessible(true);
//margins defined as pnt (1/72nds)
double asPnt = pxlOpts.getUnits().toInches() * 72;
if (orient == PageOrientation.PORTRAIT || orient == PageOrientation.REVERSE_PORTRAIT) {
layout = plCon.newInstance(paper, orient, m.left() * asPnt, m.right() * asPnt, m.top() * asPnt, m.bottom() * asPnt);
} else {
//rotate margins to match raster prints
layout = plCon.newInstance(paper, orient, m.top() * asPnt, m.bottom() * asPnt, m.right() * asPnt, m.left() * asPnt);
}
} else {
//if margins are not provided, use default paper margins
PageLayout valid = fxPrinter.getDefaultPageLayout();
layout = fxPrinter.createPageLayout(paper, orient, valid.getLeftMargin(), valid.getRightMargin(), valid.getTopMargin(), valid.getBottomMargin());
}
//force our layout as the default to avoid default-margin exceptions on small paper sizes
Field field = fxPrinter.getClass().getDeclaredField("defPageLayout");
field.setAccessible(true);
field.set(fxPrinter, layout);
settings.setPageLayout(layout);
}
catch(Exception e) {
log.error("Failed to set custom layout", e);
}
settings.setCopies(pxlOpts.getCopies());
log.trace("{}", settings.toString());
//javaFX lies about this value, so pull from original print service
CopiesSupported cSupport = (CopiesSupported)output.getPrintService()
.getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], null);
try {
if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) {
for(WebAppModel model : models) {
WebApp.print(job, model);
}
} else {
settings.setCopies(1); //manually handle copies if they are not supported
for(int i = 0; i < pxlOpts.getCopies(); i++) {
for(WebAppModel model : models) {
WebApp.print(job, model);
}
}
}
}
catch(Throwable t) {
job.cancelJob();
throw new PrinterException(t.getMessage());
}
//send pending prints
job.endJob();
}
}
private void printLegacy(PrintOutput output, PrintOptions options) throws PrinterException {
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
java.awt.print.PrinterJob job = java.awt.print.PrinterJob.getPrinterJob();
job.setPrintService(output.getPrintService());
PageFormat page = job.getPageFormat(null);
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
//setup swing ui
JFrame legacyFrame = new JFrame(pxlOpts.getJobName(Constants.HTML_PRINT));
legacyFrame.setUndecorated(true);
legacyFrame.setLayout(new FlowLayout());
legacyFrame.setExtendedState(Frame.ICONIFIED);
legacyLabel = new JLabel();
legacyLabel.setOpaque(true);
legacyLabel.setBackground(Color.WHITE);
legacyLabel.setBorder(null);
legacyLabel.setDoubleBuffered(false);
legacyFrame.add(legacyLabel);
try {
for(WebAppModel model : models) {
if (model.isPlainText()) {
legacyLabel.setText(cleanHtmlContent(model.getSource()));
} else {
try(InputStream fis = new URL(model.getSource()).openStream()) {
String webPage = cleanHtmlContent(IOUtils.toString(fis, "UTF-8"));
legacyLabel.setText(webPage);
}
}
legacyFrame.pack();
legacyFrame.setVisible(true);
job.setPrintable(this);
printCopies(output, pxlOpts, job, attributes);
}
}
catch(Exception e) {
throw new PrinterException(e.getMessage());
}
finally {
legacyFrame.dispose();
}
}
private String cleanHtmlContent(String html) {
return html.replaceAll("^[\\s\\S]*<(HTML|html)\\b.*?>", "<html>");
}
@Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
if (legacyLabel == null) {
return super.print(graphics, pageFormat, pageIndex);
} else {
if (graphics == null) { throw new PrinterException("No graphics specified"); }
if (pageFormat == null) { throw new PrinterException("No page format specified"); }
if (pageIndex + 1 > models.size()) {
return NO_SUCH_PAGE;
}
log.trace("Requested page {} for printing", pageIndex);
Graphics2D graphics2D = (Graphics2D)graphics;
graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation));
graphics2D.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
graphics2D.scale(pageFormat.getImageableWidth() / pageFormat.getWidth(), pageFormat.getImageableHeight() / pageFormat.getHeight());
legacyLabel.paint(graphics2D);
return PAGE_EXISTS;
}
}
@Override
public void cleanup() {
super.cleanup();
models.clear();
legacyLabel = null;
}
public static Units getUnits(PrintOptions.Pixel opts) {
switch(opts.getUnits()) {
case INCH:
return Units.INCH;
case MM:
return Units.MM;
default:
return null;
}
}
public static PageOrientation getOrientation(PrintOptions.Pixel opts) {
switch(opts.getOrientation()) {
case LANDSCAPE:
return PageOrientation.LANDSCAPE;
case REVERSE_LANDSCAPE:
return PageOrientation.REVERSE_LANDSCAPE;
case REVERSE_PORTRAIT:
return PageOrientation.REVERSE_PORTRAIT;
default:
return PageOrientation.PORTRAIT;
}
}
public static PrintColor getColor(PrintOptions.Pixel opts) {
switch(opts.getColorType()) {
case COLOR:
return PrintColor.COLOR;
default:
return PrintColor.MONOCHROME;
}
}
}

Some files were not shown because too many files have changed in this diff Show More