Skip to content

Commit

Permalink
Add qz:steal support (qzind#849)
Browse files Browse the repository at this point in the history
Add support for shutting down another instance's websocket
  • Loading branch information
tresf authored Aug 11, 2021
1 parent 9c0fa0e commit 3715f22
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 26 deletions.
7 changes: 4 additions & 3 deletions src/qz/common/AboutInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ public static String getPreferredHostname() {
}

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()));
Expand All @@ -158,14 +159,14 @@ public static Version findLatestVersion() {
JSONObject versionData = versions.getJSONObject(i);
if(versionData.getString("target_commitish").equals("master")) {
Version latestVersion = Version.valueOf(versionData.getString("name"));
log.trace("Found latest version: {}", latestVersion);
log.trace("Found latest version of {} online: {}", Constants.ABOUT_TITLE, latestVersion);
return latestVersion;
}
}
log.error("Failed to get latest version info");
throw new Exception("Could not find valid json version information online.");
}
catch(Exception e) {
log.error("Failed to get latest version info", e);
log.error("Failed to get latest version of {} online", Constants.ABOUT_TITLE, e);
}

return Constants.VERSION;
Expand Down
4 changes: 3 additions & 1 deletion src/qz/common/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import java.awt.*;

import static qz.ws.SingleInstanceChecker.STEAL_WEBSOCKET_PROPERTY;

/**
* Created by robert on 7/9/2014.
*/
Expand All @@ -23,7 +25,7 @@ public class Constants {
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" };
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 LOG_SIZE = 524288;
Expand Down
4 changes: 2 additions & 2 deletions src/qz/installer/MacInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ public String getDestination() {
public Installer addSystemSettings() {
// Chrome protocol handler
String plist = "/Library/Preferences/com.google.Chrome.plist";
if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] { "qz://*" }).isEmpty()) {
ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLWhitelist", "-array-add", "qz://*");
if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] {DATA_DIR + "://*" }).isEmpty()) {
ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLWhitelist", "-array-add", DATA_DIR +"://*");
}
return this;
}
Expand Down
4 changes: 1 addition & 3 deletions src/qz/installer/TaskKiller.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static boolean killAll() {

ArrayList<String> javaProcs;
String[] trayProcs;
int selfProc;
int selfProc = SystemUtilities.getProcessId();
String[] killCmd;
// Disable service until reboot
if(SystemUtilities.isMac()) {
Expand All @@ -43,12 +43,10 @@ public static boolean killAll() {
// Windows may be running under javaw.exe (normal) or java.exe (terminal)
javaProcs = AppLocator.getInstance().getPids("java.exe", "javaw.exe");
trayProcs = ShellUtilities.executeRaw(TRAY_PID_QUERY_WIN32).split("\\s*\\r?\\n");
selfProc = Kernel32.INSTANCE.GetCurrentProcessId();
killCmd = KILL_PID_CMD_WIN32;
} else {
javaProcs = AppLocator.getInstance().getPids( "java");
trayProcs = ShellUtilities.executeRaw(TRAY_PID_QUERY_POSIX).split("\\s*\\r?\\n");
selfProc = MacUtilities.getProcessID(); // Works for Linux too
killCmd = KILL_PID_CMD_POSIX;
}
if (!javaProcs.isEmpty()) {
Expand Down
2 changes: 2 additions & 0 deletions src/qz/installer/WindowsInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public Installer removeLegacyStartup() {

public Installer addAppLauncher() {
try {
// Delete old 2.0 launcher
FileUtils.deleteQuietly(new File(COMMON_START_MENU + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
Path loc = Paths.get(COMMON_START_MENU.toString(), "Programs", ABOUT_TITLE);
loc.toFile().mkdirs();
String lnk = loc + File.separator + ABOUT_TITLE + ".lnk";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ interface Crypt32 extends StdCallLibrary {
int CERT_FIND_SUBJECT_STR = 524295;
int CERT_FIND_SHA1_HASH = 65536;

Crypt32 INSTANCE = Native.loadLibrary("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS);
Crypt32 INSTANCE = Native.load("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS);

WinCrypt.HCERTSTORE CertOpenStore(int lpszStoreProvider, int dwMsgAndCertEncodingType, Pointer hCryptProv, int dwFlags, String pvPara);
boolean CertCloseStore(WinCrypt.HCERTSTORE hCertStore, int dwFlags);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ private static Path getExePath(String appPath) {
}

private interface SystemB extends Library {
SystemB INSTANCE = Native.loadLibrary("System", SystemB.class);
SystemB INSTANCE = Native.load("System", SystemB.class);
int PROC_ALL_PIDS = 1;
int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
int sysctlbyname(String name, Pointer oldp, IntByReference oldlenp, Pointer newp, int newlen);
Expand Down
2 changes: 1 addition & 1 deletion src/qz/printer/status/Cups.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
public interface Cups extends Library {

Cups INSTANCE = Native.loadLibrary("cups", Cups.class);
Cups INSTANCE = Native.load("cups", Cups.class);

/**
* Static class to facilitate readability of values
Expand Down
2 changes: 1 addition & 1 deletion src/qz/ui/BasicDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public void setVisible(boolean b) {
// fix window focus on macOS
if (SystemUtilities.isMac() && !GraphicsEnvironment.isHeadless()) {
ShellUtilities.executeAppleScript("tell application \"System Events\" \n" +
"set frontmost of every process whose unix id is " + MacUtilities.getProcessID() + " to true \n" +
"set frontmost of every process whose unix id is " + SystemUtilities.getProcessId() + " to true \n" +
"end tell");
}
super.setVisible(b);
Expand Down
2 changes: 2 additions & 0 deletions src/qz/utils/ArgValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public enum ArgValue {
// Options
AUTOSTART(OPTION,"Read and honor any autostart preferences before launching.", null,
"--honorautostart", "-A"),
STEAL(OPTION, "Ask other running instance to stop so that this instance can take precedence.", null,
"--steal", Constants.DATA_DIR + ":steal"),
HEADLESS(OPTION, "Force startup \"headless\" without graphical interface or interactive components.", null,
"--headless"),

Expand Down
4 changes: 2 additions & 2 deletions src/qz/utils/GtkUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private interface GTK extends Library {
}

private interface GTK3 extends GTK {
GTK3 INSTANCE = Native.loadLibrary("gtk-3", GTK3.class);
GTK3 INSTANCE = Native.load("gtk-3", GTK3.class);

// Gtk 3.0+
int gtk_get_minor_version ();
Expand All @@ -112,7 +112,7 @@ private interface GTK3 extends GTK {
}

private interface GTK2 extends GTK {
GTK2 INSTANCE = Native.loadLibrary("gtk-x11-2.0", GTK2.class);
GTK2 INSTANCE = Native.load("gtk-x11-2.0", GTK2.class);

// Gtk 2.1-3.0
double gdk_screen_get_resolution(Pointer screen);
Expand Down
4 changes: 2 additions & 2 deletions src/qz/utils/MacUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public static int getScaleFactor() {
return 1;
}

public static int getProcessID() {
static int getProcessId() {
if(pid == null) {
try {
pid = CLibrary.INSTANCE.getpid();
Expand All @@ -163,7 +163,7 @@ public static int getProcessID() {
}

private interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary) Native.loadLibrary("c", CLibrary.class);
CLibrary INSTANCE = Native.load("c", CLibrary.class);
int getpid ();
}

Expand Down
64 changes: 63 additions & 1 deletion src/qz/utils/SystemUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import com.github.zafarkhaja.semver.Version;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.ssl.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.common.Constants;
Expand All @@ -22,12 +23,15 @@
import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;

import static com.sun.jna.platform.win32.WinReg.*;
Expand Down Expand Up @@ -111,6 +115,13 @@ public static boolean isAdmin() {
}
}

public static int getProcessId() {
if(isWindows()) {
return WindowsUtilities.getProcessId();
}
return MacUtilities.getProcessId(); // works for Linux too
}

/**
* Handle Java versioning nuances
* To eventually be replaced with <code>java.lang.Runtime.Version</code> (JDK9+)
Expand Down Expand Up @@ -359,7 +370,7 @@ public static boolean prefersMaskTrayIcon() {
if (Constants.MASK_TRAY_SUPPORTED) {
if (SystemUtilities.isMac()) {
// Assume a pid of -1 is a broken JNA
return MacUtilities.getProcessID() != -1;
return MacUtilities.getProcessId() != -1;
} else if (SystemUtilities.isWindows() && SystemUtilities.getOSVersion().getMajorVersion() >= 10) {
return true;
}
Expand Down Expand Up @@ -584,4 +595,55 @@ public static void clearAlgorithms() {
log.warn("Unable to apply JDK-8266929 patch. Some algorithms may fail.", e);
}
}

/**
* A challenge which can only be calculated by an app installed and running on this machine
* Calculates two bytes:
* - First byte is a salted version of the timestamp of qz-tray.jar
* - Second byte is a throw-away byte
* - Bytes are converted to Base64 and returned as a String
*/
public static String calculateSaltedChallenge() {
int salt = new Random().nextInt(9);
long salted = (calculateChallenge() * 10) + salt;
long obfuscated = salted * new Random().nextInt(9);
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * 2);
buffer.putLong(obfuscated);
buffer.putLong(0, salted);
return new String(Base64.encodeBase64(buffer.array(), false), StandardCharsets.UTF_8);
}

private static long calculateChallenge() {
if(getJarPath() != null) {
File jarFile = new File(getJarPath());
if (jarFile.exists()) {
return jarFile.lastModified();
}
}
return -1L; // Fallback when running from IDE
}

/**
* Decodes challenge string to see if it originated from this application
* - Base64 string is decoded into two bytes
* - First byte is unsalted
* - Second byte is ignored
* - If unsalted value of first byte matches the timestamp of qz-tray.jar, return true
* - If unsalted value doesn't match or if any exceptions occurred, we assume the message is invalid
*/
public static boolean validateSaltedChallenge(String message) {
try {
log.info("Attempting to validating challenge: {}", message);
byte[] decoded = Base64.decodeBase64(message);
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * 2);
buffer.put(decoded);
buffer.flip();//need flip
long salted = buffer.getLong(0); // only first byte matters
long challenge = salted / 10L;
return challenge == calculateChallenge();
} catch(Exception ignore) {
log.warn("An exception occurred validating challenge: {}", message, ignore);
}
return false;
}
}
11 changes: 11 additions & 0 deletions src/qz/utils/WindowsUtilities.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package qz.utils;

import com.github.zafarkhaja.semver.Version;
import com.sun.jna.Native;
import com.sun.jna.platform.win32.*;
import com.sun.jna.win32.W32APIOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.common.Constants;
Expand Down Expand Up @@ -285,4 +287,13 @@ public static boolean elevatedFileCopy(Path source, Path destination) {
String[] command = {"Start-Process", "powershell.exe", "-ArgumentList", args, "-Wait", "-Verb", "RunAs"};
return ShellUtilities.execute("powershell.exe", "-command", String.join(" ", command));
}

static int getProcessId() {
try {
return Kernel32.INSTANCE.GetCurrentProcessId();
} catch(UnsatisfiedLinkError | NoClassDefFoundError e) {
log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1.");
}
return -1;
}
}
22 changes: 21 additions & 1 deletion src/qz/ws/PrintSocketClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.*;
Expand Down Expand Up @@ -46,6 +47,11 @@ public class PrintSocketClient {
//websocket port -> Connection
private static final HashMap<Integer,SocketConnection> openConnections = new HashMap<>();

private Server server;

public PrintSocketClient(Server server) {
this.server = server;
}

@OnWebSocketConnect
public void onConnect(Session session) {
Expand Down Expand Up @@ -588,7 +594,21 @@ private void processMessage(Session session, JSONObject json, SocketConnection c
case GET_VERSION:
sendResult(session, UID, Constants.VERSION);
break;

case WEBSOCKET_STOP:
log.info("Another instance of {} is asking this to close", Constants.ABOUT_TITLE);
String challenge = json.optString("challenge", "");
if(SystemUtilities.validateSaltedChallenge(challenge)) {
log.info("Challenge validated: {}, honoring shutdown request", challenge);

session.close(SingleInstanceChecker.REQUEST_INSTANCE_TAKEOVER);
try {
server.stop();
} catch(Exception ignore) {}
trayManager.exit(0);
} else {
log.warn("A valid challenge was not provided: {}, ignoring request to close", challenge);
}
break;
case INVALID:
default:
sendError(session, UID, "Invalid function call: " + json.optString("call", "NONE"));
Expand Down
9 changes: 7 additions & 2 deletions src/qz/ws/PrintSocketServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static qz.utils.ArgParser.ExitStatus.SUCCESS;
import static qz.utils.ArgValue.STEAL;

/**
* Created by robert on 9/9/2014.
*/
Expand All @@ -63,13 +66,14 @@ public class PrintSocketServer {

private static boolean forceHeadless;


public static void main(String[] args) {
ArgParser parser = new ArgParser(args);
if (parser.intercept()) {
System.exit(parser.getExitCode());
}
forceHeadless = parser.hasFlag(ArgValue.HEADLESS);
SingleInstanceChecker.stealWebsocket = parser.hasFlag(STEAL);

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());
Expand All @@ -89,6 +93,7 @@ public static void main(String[] args) {

// Load overridable preferences set in qz-tray.properties file
NetworkUtilities.setPreferences(certificateManager.getProperties());
SingleInstanceChecker.setPreferences(certificateManager.getProperties());

// Linux needs the cert installed in user-space on every launch for Chrome SSL to work
if (!SystemUtilities.isWindows() && !SystemUtilities.isMac()) {
Expand Down Expand Up @@ -151,7 +156,7 @@ public static void runServer() {

// Handle WebSocket connections
WebSocketUpgradeFilter filter = WebSocketUpgradeFilter.configure(context);
filter.addMapping(new ServletPathSpec("/"), (req, resp) -> new PrintSocketClient());
filter.addMapping(new ServletPathSpec("/"), (req, resp) -> new PrintSocketClient(server));
filter.getFactory().getPolicy().setMaxTextMessageSize(MAX_MESSAGE_SIZE);

// Handle HTTP landing page
Expand Down
Loading

0 comments on commit 3715f22

Please sign in to comment.