Skip to content

Commit

Permalink
Add provisioning support for custom websocket ports (#1263)
Browse files Browse the repository at this point in the history
Add ability to set custom websocket ports via automatic provisioning or manually via qz-tray.properties.
  • Loading branch information
tresf committed May 6, 2024
1 parent 2ab63ba commit ac5c6c1
Show file tree
Hide file tree
Showing 14 changed files with 287 additions and 63 deletions.
6 changes: 4 additions & 2 deletions src/qz/common/AboutInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import qz.utils.StringUtilities;
import qz.utils.SystemUtilities;
import qz.ws.PrintSocketServer;
import qz.ws.WebsocketPorts;

import java.io.BufferedReader;
import java.io.IOException;
Expand Down Expand Up @@ -68,6 +69,7 @@ private static JSONObject product() throws JSONException {
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("<") || sanitizeDomain.contains(">")) {
Expand All @@ -78,9 +80,9 @@ private static JSONObject socket(CertificateManager certificateManager, String d
socket
.put("domain", sanitizeDomain)
.put("secureProtocol", "wss")
.put("securePort", certificateManager.isSslActive() ? PrintSocketServer.getSecurePortInUse() : "none")
.put("securePort", certificateManager.isSslActive() ? websocketPorts.getSecurePort() : "none")
.put("insecureProtocol", "ws")
.put("insecurePort", PrintSocketServer.getInsecurePortInUse());
.put("insecurePort", websocketPorts.getInsecurePort());

return socket;
}
Expand Down
5 changes: 2 additions & 3 deletions src/qz/common/Constants.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package qz.common;

import com.github.zafarkhaja.semver.Version;
import qz.utils.ArgValue;
import qz.utils.SystemUtilities;

import java.awt.*;
Expand Down Expand Up @@ -98,7 +97,7 @@ public class Constants {
public static final String PDF_PRINT = ABOUT_TITLE + " PDF Print";
public static final String HTML_PRINT = ABOUT_TITLE + " HTML Print";

public static final Integer[] WSS_PORTS = {8181, 8282, 8383, 8484};
public static final Integer[] WS_PORTS = {8182, 8283, 8384, 8485};
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};
}
5 changes: 5 additions & 0 deletions src/qz/common/PropertyHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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.
Expand Down Expand Up @@ -92,4 +93,8 @@ public boolean save() {
}
return success;
}

public synchronized Object setProperty(Map.Entry<String, String> pair) {
return super.setProperty(pair.getKey(), pair.getValue());
}
}
19 changes: 11 additions & 8 deletions src/qz/common/TrayManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import qz.utils.*;
import qz.ws.PrintSocketServer;
import qz.ws.SingleInstanceChecker;
import qz.ws.WebsocketPorts;
import qz.ws.substitutions.Substitutions;

import javax.swing.*;
Expand Down Expand Up @@ -538,10 +539,9 @@ private void blackList(Certificate cert) {
}
}

public void setServer(Server server, int insecurePortInUse, int securePortInUse) {
public void setServer(Server server, WebsocketPorts websocketPorts) {
if (server != null && server.getConnectors().length > 0) {
singleInstanceCheck(PrintSocketServer.INSECURE_PORTS, insecurePortInUse, false);
singleInstanceCheck(PrintSocketServer.SECURE_PORTS, securePortInUse, true);
singleInstanceCheck(websocketPorts);

displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server));

Expand Down Expand Up @@ -637,11 +637,14 @@ private void displayMessage(final String caption, final String text, final TrayI
}
}

public void singleInstanceCheck(java.util.List<Integer> ports, Integer portInUse, boolean usingSecure) {
for(int port : ports) {
if (portInUse == -1 || port != ports.get(portInUse)) {
new SingleInstanceChecker(this, port, usingSecure);
}
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);
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/qz/installer/Installer.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import qz.installer.provision.ProvisionInstaller;
import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import qz.ws.WebsocketPorts;

import java.io.*;
import java.nio.file.*;
Expand All @@ -42,6 +43,8 @@ public abstract class Installer {
public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent"));
public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime";

WebsocketPorts websocketPorts;

public enum PrivilegeLevel {
USER,
SYSTEM
Expand Down Expand Up @@ -103,8 +106,8 @@ public static void install() throws Exception {
.addSharedDirectory()
.addAppLauncher()
.addStartupEntry()
.addSystemSettings()
.invokeProvisioning(Phase.INSTALL);
.invokeProvisioning(Phase.INSTALL)
.addSystemSettings();
}

public static void uninstall() {
Expand Down Expand Up @@ -362,8 +365,13 @@ public Installer invokeProvisioning(Phase phase) {
Paths.get(getDestination()).resolve(PROVISION_DIR);
ProvisionInstaller provisionInstaller = new ProvisionInstaller(provisionPath);
provisionInstaller.invoke(phase);

// Special case for custom websocket ports
if(phase == Phase.INSTALL) {
websocketPorts = WebsocketPorts.parseFromSteps(provisionInstaller.getSteps());
}
} catch(Exception e) {
log.warn("An error occurred deleting provisioning directory \"phase\": \"{}\" entries", phase, e);
log.warn("An error occurred invoking provision \"phase\": \"{}\"", phase, e);
}
return this;
}
Expand Down
3 changes: 1 addition & 2 deletions src/qz/installer/WindowsInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,9 @@ public Installer addSystemSettings() {
WindowsUtilities.addNumberedRegValue(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLWhitelist", String.format("%s://*", DATA_DIR));

// Firewall rules
String ports = StringUtils.join(PrintSocketServer.SECURE_PORTS, ",") + "," + StringUtils.join(PrintSocketServer.INSECURE_PORTS, ",");
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "add", "rule", String.format("name=%s", ABOUT_TITLE),
"dir=in", "action=allow", "profile=any", String.format("localport=%s", ports), "localip=any", "protocol=tcp");
"dir=in", "action=allow", "profile=any", String.format("localport=%s", websocketPorts.allPortsAsString()), "localip=any", "protocol=tcp");
return this;
}

Expand Down
11 changes: 11 additions & 0 deletions src/qz/installer/provision/ProvisionInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
Expand All @@ -10,10 +11,14 @@
import qz.build.provision.Step;
import qz.build.provision.params.Os;
import qz.build.provision.params.Phase;
import qz.build.provision.params.Type;
import qz.build.provision.params.types.Script;
import qz.build.provision.params.types.Software;
import qz.common.Constants;
import qz.installer.Installer;
import qz.installer.provision.invoker.*;
import qz.utils.ArgValue;
import qz.utils.PrefsSearch;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;

Expand All @@ -23,6 +28,8 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import static qz.common.Constants.*;
import static qz.utils.FileUtilities.*;
Expand Down Expand Up @@ -129,6 +136,10 @@ private boolean invokeStep(Step step) throws Exception {
return invoker.invoke();
}

public ArrayList<Step> getSteps() {
return steps;
}

private static ArrayList<Step> parse(JSONArray jsonArray, Object relativeObject) throws JSONException {
ArrayList<Step> steps = new ArrayList<>();
for(int i = 0; i < jsonArray.length(); i++) {
Expand Down
50 changes: 30 additions & 20 deletions src/qz/installer/provision/invoker/PropertyInvoker.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

import java.io.File;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class PropertyInvoker implements Invokable {
private Step step;
Expand All @@ -20,27 +21,16 @@ public PropertyInvoker(Step step, PropertyHelper properties) {
}

public boolean invoke() {
if(step.getData() != null && !step.getData().trim().isEmpty()) {
String[] props = step.getData().split("\\|");
ArrayList<AbstractMap.SimpleEntry<String,String>> pairs = new ArrayList<>();
for(String prop : props) {
AbstractMap.SimpleEntry<String,String> pair = parsePropertyPair(step, prop);
if (pair != null) {
pairs.add(pair);
}
HashMap<String, String> pairs = parsePropertyPairs(step);
if (!pairs.isEmpty()) {
for(Map.Entry<String, String> pair : pairs.entrySet()) {
properties.setProperty(pair);
}
if (!pairs.isEmpty()) {
for(AbstractMap.SimpleEntry<String,String> pair : pairs) {
properties.setProperty(pair.getKey(), pair.getValue());
}
if (properties.save()) {
log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType());
return true;
}
log.error("An error occurred saving properties '{}' to file", step.getData());
if (properties.save()) {
log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType());
return true;
}
} else {
log.error("Skipping Step '{}', Data is null or empty", step.getType());
log.error("An error occurred saving properties '{}' to file", step.getData());
}
return false;
}
Expand All @@ -63,6 +53,26 @@ public static PropertyHelper getPreferences(Step step) {
return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
}

public static HashMap<String, String> parsePropertyPairs(Step step) {
HashMap<String, String> pairs = new HashMap<>();
if(step.getData() != null && !step.getData().trim().isEmpty()) {
String[] props = step.getData().split("\\|");
for(String prop : props) {
AbstractMap.SimpleEntry<String,String> pair = parsePropertyPair(step, prop);
if (pair != null) {
if(pairs.get(pair.getKey()) != null) {
log.warn("Property {} already exists, replacing [before: {}, after: {}] ",
pair.getKey(), pairs.get(pair.getKey()), pair.getValue());
}
pairs.put(pair.getKey(), pair.getValue());
}
}
} else {
log.error("Skipping Step '{}', Data is null or empty", step.getType());
}
return pairs;
}


private static AbstractMap.SimpleEntry<String, String> parsePropertyPair(Step step, String prop) {
if(prop.contains("=")) {
Expand Down
2 changes: 1 addition & 1 deletion src/qz/ui/AboutDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private LinkLabel getLinkLibrary() {
// Some OSs (e.g. FreeBSD) return null for server.getURI(), fallback to sane values
URI uri = server.getURI();
String scheme = uri == null ? "http" : uri.getScheme();
int port = uri == null ? PrintSocketServer.getInsecurePortInUse(): uri.getPort();
int port = uri == null ? PrintSocketServer.getWebsocketPorts().getInsecurePort(): uri.getPort();
linkLibrary.setLinkLocation(String.format("%s://%s:%s", scheme, AboutInfo.getPreferredHostname(), port));
}
return linkLibrary;
Expand Down
5 changes: 5 additions & 0 deletions src/qz/utils/ArgValue.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package qz.utils;

import org.apache.commons.lang3.StringUtils;
import qz.common.Constants;
import qz.ws.substitutions.Substitutions;

Expand Down Expand Up @@ -88,6 +89,10 @@ public enum ArgValue {
"security.wss.httpsonly"),
SECURITY_WSS_HOST(PREFERENCES, "Influences which physical adapter to bind to by setting the host parameter for http/websocket listening", null, "0.0.0.0",
"security.wss.host"),
WEBSOCKET_SECURE_PORTS(PREFERENCES, "Comma separated list of secure websocket (wss://) ports to use", null, StringUtils.join(Constants.DEFAULT_WSS_PORTS, ","),
"websocket.secure.ports"),
WEBSOCKET_INSECURE_PORTS(PREFERENCES, "Comma separated list of insecure websocket (ws://) ports to use", null, StringUtils.join(Constants.DEFAULT_WS_PORTS, ","),
"websocket.insecure.ports"),
LOG_DISABLE(PREFERENCES, "Disable/enable logging features", null, false,
"log.disable"),
LOG_ROTATE(PREFERENCES, "Number of log files to retain when the size fills up", null, 5,
Expand Down
21 changes: 21 additions & 0 deletions src/qz/utils/PrefsSearch.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import qz.installer.certificate.CertificateManager;
import qz.installer.certificate.KeyPairWrapper;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import static qz.installer.certificate.KeyPairWrapper.Type.CA;
Expand Down Expand Up @@ -90,6 +92,25 @@ public static int getInt(ArgValue argValue, Properties ... propsArray) {
return getInt(argValue, true, propsArray);
}

public static List<Integer> getIntegerArray(ArgValue argValue, Properties ... propsArray) {
return parseIntegerArray(getString(argValue, propsArray));
}

public static List<Integer> parseIntegerArray(String commaSeparated) {
List<Integer> parsed = new ArrayList<>();
try {
if (commaSeparated != null && !commaSeparated.isEmpty()) {
String[] split = commaSeparated.split(",");
for(String item : split) {
parsed.add(Integer.parseInt(item));
}
}
} catch(NumberFormatException nfe) {
log.warn("Failed parsing {} as a valid integer array", commaSeparated, nfe);
}
return parsed;
}

public static boolean getBoolean(ArgValue argValue, Properties ... propsArray) {
return getBoolean(argValue, true, propsArray);
}
Expand Down

0 comments on commit ac5c6c1

Please sign in to comment.