cleaning structure

This commit is contained in:
Quality System Admin
2025-10-16 01:42:59 +03:00
parent e0ba349862
commit 50c791e242
469 changed files with 1016 additions and 29776 deletions

133
old code/tray/src/qz/App.java Executable 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);
}
}

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);
}
}