@@ -2982,13 +2960,6 @@
Options
}).catch(displayError);
}
- $("#askFileModal").on("shown.bs.modal", function() {
- $("#askFile").focus().select();
- });
- $("#askHostModal").on("shown.bs.modal", function() {
- $("#askHost").focus().select();
- });
-
//make dirty when changed
$("input").add("select").on('change', function() {
$(this).addClass("dirty");
@@ -3375,11 +3346,6 @@ Options
return options;
}
- function setPrintFile() {
- setPrinter({ file: $("#askFile").val() });
- $("#askFileModal").modal('hide');
- }
-
function setPrintHost() {
setPrinter({ host: $("#askHost").val(), port: $("#askPort").val() });
$("#askHostModal").modal('hide');
diff --git a/src/qz/auth/CRL.java b/src/qz/auth/CRL.java
index 11c945123..861235ed7 100644
--- a/src/qz/auth/CRL.java
+++ b/src/qz/auth/CRL.java
@@ -38,7 +38,7 @@ public static CRL getInstance() {
public void run() {
log.info("Loading CRL from {}", CRL_URL);
- try(BufferedReader br = new BufferedReader(new InputStreamReader(ConnectionUtilities.getInputStream(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
diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java
index c2c5917a6..12b773ac0 100644
--- a/src/qz/common/TrayManager.java
+++ b/src/qz/common/TrayManager.java
@@ -534,9 +534,10 @@ private void blackList(Certificate cert) {
}
}
- public void setServer(Server server, int insecurePortIndex) {
+ public void setServer(Server server, int insecurePortInUse, int securePortInUse) {
if (server != null && server.getConnectors().length > 0) {
- singleInstanceCheck(PrintSocketServer.INSECURE_PORTS, insecurePortIndex);
+ singleInstanceCheck(PrintSocketServer.INSECURE_PORTS, insecurePortInUse, false);
+ singleInstanceCheck(PrintSocketServer.SECURE_PORTS, securePortInUse, true);
displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server));
@@ -632,10 +633,10 @@ private void displayMessage(final String caption, final String text, final TrayI
}
}
- public void singleInstanceCheck(java.util.List insecurePorts, Integer insecurePortIndex) {
- for(int port : insecurePorts) {
- if (port != insecurePorts.get(insecurePortIndex)) {
- new SingleInstanceChecker(this, port);
+ public void singleInstanceCheck(java.util.List ports, Integer portInUse, boolean usingSecure) {
+ for(int port : ports) {
+ if (portInUse == -1 || port != ports.get(portInUse)) {
+ new SingleInstanceChecker(this, port, usingSecure);
}
}
}
diff --git a/src/qz/installer/certificate/CertificateManager.java b/src/qz/installer/certificate/CertificateManager.java
index 8b6972f25..c67674c91 100644
--- a/src/qz/installer/certificate/CertificateManager.java
+++ b/src/qz/installer/certificate/CertificateManager.java
@@ -29,6 +29,7 @@
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.installer.Installer;
+import qz.utils.ArgValue;
import qz.utils.FileUtilities;
import qz.utils.MacUtilities;
import qz.utils.SystemUtilities;
@@ -60,10 +61,7 @@ public class CertificateManager {
public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12";
public static String DEFAULT_KEYSTORE_EXTENSION = ".p12";
-
public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt";
-
- private static String DEFAULT_HOST_SCOPE = "0.0.0.0";
private static int DEFAULT_PASSWORD_BITS = 100;
private boolean needsInstall;
@@ -331,7 +329,7 @@ public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) thro
props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias());
if (keyPair.getType() == SSL) {
- props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), DEFAULT_HOST_SCOPE);
+ props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), ArgValue.SECURITY_WSS_HOST.getDefaultVal());
}
diff --git a/src/qz/printer/action/PrintImage.java b/src/qz/printer/action/PrintImage.java
index aec9b489a..fe8bc1bf1 100644
--- a/src/qz/printer/action/PrintImage.java
+++ b/src/qz/printer/action/PrintImage.java
@@ -82,7 +82,7 @@ public void parseData(JSONArray printData, PrintOptions options) throws JSONExce
case PLAIN:
// There's really no such thing as a 'PLAIN' image, assume it's a URL
case FILE:
- bi = ImageIO.read(ConnectionUtilities.getInputStream(data.getString("data")));
+ bi = ImageIO.read(ConnectionUtilities.getInputStream(data.getString("data"), true));
break;
default:
bi = ImageIO.read(new ByteArrayInputStream(flavor.read(data.getString("data"))));
diff --git a/src/qz/printer/action/PrintPDF.java b/src/qz/printer/action/PrintPDF.java
index a4772c45d..8e3e68cf2 100644
--- a/src/qz/printer/action/PrintPDF.java
+++ b/src/qz/printer/action/PrintPDF.java
@@ -118,7 +118,7 @@ public void parseData(JSONArray printData, PrintOptions options) throws JSONExce
case PLAIN:
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
case FILE:
- doc = PDDocument.load(ConnectionUtilities.getInputStream(data.getString("data")));
+ doc = PDDocument.load(ConnectionUtilities.getInputStream(data.getString("data"), true));
break;
default:
doc = PDDocument.load(new ByteArrayInputStream(flavor.read(data.getString("data"))));
diff --git a/src/qz/printer/action/PrintRaw.java b/src/qz/printer/action/PrintRaw.java
index 89093d477..d5e728768 100644
--- a/src/qz/printer/action/PrintRaw.java
+++ b/src/qz/printer/action/PrintRaw.java
@@ -11,7 +11,6 @@
import com.ibm.icu.text.ArabicShapingException;
import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.ssl.Base64;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.rendering.PDFRenderer;
@@ -175,7 +174,7 @@ private ImageWrapper getImageWrapper(String data, JSONObject opt, PrintingUtilit
case PLAIN:
// There's really no such thing as a 'PLAIN' image, assume it's a URL
case FILE:
- bi = ImageIO.read(ConnectionUtilities.getInputStream(data));
+ bi = ImageIO.read(ConnectionUtilities.getInputStream(data, true));
break;
default:
bi = ImageIO.read(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
@@ -191,7 +190,7 @@ private ImageWrapper getPdfWrapper(String data, JSONObject opt, PrintingUtilitie
case PLAIN:
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
case FILE:
- doc = PDDocument.load(ConnectionUtilities.getInputStream(data));
+ doc = PDDocument.load(ConnectionUtilities.getInputStream(data, true));
break;
default:
doc = PDDocument.load(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
@@ -329,7 +328,7 @@ public void print(PrintOutput output, PrintOptions options) throws PrintExceptio
if (output.isSetHost()) {
printToHost(output.getHost(), output.getPort(), bab.getByteArray());
} else if (output.isSetFile()) {
- printToFile(output.getFile(), bab.getByteArray());
+ printToFile(output.getFile(), bab.getByteArray(), true);
} else {
if (rawOpts.isForceRaw()) {
if(tempFiles == null) {
@@ -339,7 +338,7 @@ public void print(PrintOutput output, PrintOptions options) throws PrintExceptio
if(tempFiles.size() <= j) {
tempFile = File.createTempFile("qz_raw_", null);
tempFiles.add(j, tempFile);
- printToFile(tempFile, bab.getByteArray());
+ printToFile(tempFile, bab.getByteArray(), false);
} else {
tempFile = tempFiles.get(j);
}
@@ -401,7 +400,15 @@ private void printToHost(String host, int port, byte[] cmds) throws IOException
*
* @param file File to be written
*/
- private void printToFile(File file, byte[] cmds) throws IOException {
+ private void printToFile(File file, byte[] cmds, boolean locationRestricted) throws IOException {
+ if(file == null) throw new IOException("No file specified");
+
+ if(locationRestricted && !PrefsSearch.getBoolean(ArgValue.SECURITY_PRINT_TOFILE)) {
+ log.error("Printing to file '{}' is not permitted. Configure property '{}' to modify this behavior.",
+ file, ArgValue.SECURITY_PRINT_TOFILE.getMatch());
+ throw new IOException(String.format("Printing to file '%s' is not permitted", file));
+ }
+
log.debug("Printing to file: {}", file.getName());
//throws any exception and auto-closes stream
diff --git a/src/qz/utils/ArgValue.java b/src/qz/utils/ArgValue.java
index d16f5025c..f73b7eb3f 100644
--- a/src/qz/utils/ArgValue.java
+++ b/src/qz/utils/ArgValue.java
@@ -69,6 +69,16 @@ public enum ArgValue {
"security.file.enabled"),
SECURITY_FILE_STRICT(PREFERENCES, "Enable/disable signing requirements for File Communications features", null, true,
"security.file.strict"),
+ SECURITY_DATA_PROTOCOLS(PREFERENCES, "URL protocols allowed for print, serial, hid, etc", null, "http,https",
+ "security.data.protocols"),
+ SECURITY_PRINT_TOFILE(PREFERENCES, "Enable/disable printing directly to file paths", null, false,
+ "security.print.tofile"),
+ SECURITY_WSS_SNISTRICT(PREFERENCES, "Enables strict http/websocket SNI checks", null, false,
+ "security.wss.snistrict"),
+ SECURITY_WSS_HTTPSONLY(PREFERENCES, "Disables insecure http/websocket ports (e.g. '8182')", null, false,
+ "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"),
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,
diff --git a/src/qz/utils/ConnectionUtilities.java b/src/qz/utils/ConnectionUtilities.java
index f87cc893d..21221ce0c 100644
--- a/src/qz/utils/ConnectionUtilities.java
+++ b/src/qz/utils/ConnectionUtilities.java
@@ -12,8 +12,11 @@
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
+import java.nio.file.Paths;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.HashMap;
@@ -39,9 +42,17 @@ public final class ConnectionUtilities {
*
* @param urlString an absolute URL giving location of resource to read.
*/
- public static InputStream getInputStream(String urlString) throws IOException {
+ public static InputStream getInputStream(String urlString, boolean protocolRestricted) throws IOException {
try {
- URLConnection urlConn = new URL(urlString).openConnection();
+ URL url = new URL(urlString);
+ if(protocolRestricted) {
+ String allowed = PrefsSearch.getString(ArgValue.SECURITY_DATA_PROTOCOLS);
+ if(!isAllowed(allowed, url)) {
+ log.error("URL '{}' is not a valid http or https location. Configure property '{}' to modify this behavior.", url, ArgValue.SECURITY_DATA_PROTOCOLS.getMatch());
+ throw new IOException(String.format("URL '%s' is not a valid [%s] location", url, allowed));
+ }
+ }
+ URLConnection urlConn = url.openConnection();
for( String key : getRequestProperties().keySet()) {
urlConn.setRequestProperty(key, requestProps.get(key));
}
@@ -54,6 +65,34 @@ public static InputStream getInputStream(String urlString) throws IOException {
}
}
+ private static boolean isAllowed(String allowed, URL url) {
+ if(url == null) return false;
+ String urlProtocol = url.getProtocol();
+ if(urlProtocol == null || urlProtocol.trim().isEmpty()) return false;
+ allowed = ArgValue.SECURITY_DATA_PROTOCOLS.getDefaultVal() +
+ (allowed == null || allowed.trim().isEmpty() ? "" : "," + allowed);
+ String[] protocols = allowed.split(",");
+ // Loop over http, https, etc
+ for(String protocol : protocols) {
+ if(urlProtocol.trim().equalsIgnoreCase(protocol.trim())) {
+ return true;
+ }
+ }
+ // Allow exception for file: demo/assets
+ if(urlProtocol.trim().toLowerCase(Locale.ENGLISH).equals("file")) {
+ try {
+ // Sanitize manipulative URLs
+ url = Paths.get(url.toURI()).normalize().toUri().toURL();
+ if (url.getPath().matches(".*/demo/assets/.*|.*/tray/assets/.*")) {
+ log.warn("Allowing printing from restricted protocol '{}:' for demo asset '{}'", urlProtocol, url);
+ return true;
+ }
+ }
+ catch(URISyntaxException | MalformedURLException ignore) {}
+ }
+ return false;
+ }
+
/**
* A blind SSL trust manager, for debugging SSL issues
*/
diff --git a/src/qz/utils/FileUtilities.java b/src/qz/utils/FileUtilities.java
index 788fca336..51c54e7ad 100644
--- a/src/qz/utils/FileUtilities.java
+++ b/src/qz/utils/FileUtilities.java
@@ -592,7 +592,7 @@ public static String readLocalFile(Path path) throws IOException {
}
public static byte[] readRawFile(String url) throws IOException {
- return readFile(new DataInputStream(ConnectionUtilities.getInputStream(url)));
+ return readFile(new DataInputStream(ConnectionUtilities.getInputStream(url, true)));
}
private static byte[] readFile(DataInputStream in) throws IOException {
diff --git a/src/qz/ws/PrintSocketServer.java b/src/qz/ws/PrintSocketServer.java
index c76190d23..c45c76cd3 100644
--- a/src/qz/ws/PrintSocketServer.java
+++ b/src/qz/ws/PrintSocketServer.java
@@ -23,16 +23,15 @@
import qz.common.Constants;
import qz.common.TrayManager;
import qz.installer.certificate.CertificateManager;
+import qz.utils.ArgValue;
+import qz.utils.PrefsSearch;
import javax.servlet.DispatcherType;
import javax.swing.*;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
+import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -54,23 +53,37 @@ public class PrintSocketServer {
private static TrayManager trayManager;
private static Server server;
+ private static boolean httpsOnly;
+ private static boolean sniStrict;
+ private static String wssHost;
public static void runServer(CertificateManager certManager, boolean headless) throws InterruptedException, InvocationTargetException {
SwingUtilities.invokeAndWait(() -> {
PrintSocketServer.setTrayManager(new TrayManager(headless));
});
+ wssHost = PrefsSearch.getString(ArgValue.SECURITY_WSS_HOST, certManager.getProperties());
+ httpsOnly = PrefsSearch.getBoolean(ArgValue.SECURITY_WSS_HTTPSONLY, certManager.getProperties());
+ sniStrict = PrefsSearch.getBoolean(ArgValue.SECURITY_WSS_SNISTRICT, certManager.getProperties());
server = findAvailableSecurePort(certManager);
+
Connector secureConnector = null;
if (server.getConnectors().length > 0 && !server.getConnectors()[0].isFailed()) {
secureConnector = server.getConnectors()[0];
}
+ if (httpsOnly && secureConnector == null) {
+ log.error("Failed to start in https-only mode");
+ return;
+ }
+
while(!running.get() && insecurePortIndex.get() < INSECURE_PORTS.size()) {
try {
ServerConnector connector = new ServerConnector(server);
connector.setPort(getInsecurePortInUse());
- if (secureConnector != null) {
+ if(httpsOnly) {
+ server.setConnectors(new Connector[] {secureConnector});
+ } else if (secureConnector != null) {
//setup insecure connector before secure
server.setConnectors(new Connector[] {connector, secureConnector});
} else {
@@ -115,7 +128,9 @@ public static void runServer(CertificateManager certManager, boolean headless) t
running.set(true);
log.info("Server started on port(s) " + getPorts(server));
- trayManager.setServer(server, insecurePortIndex.get());
+ int insecurePort = httpsOnly ? -1 : insecurePortIndex.get();
+ int securePort = secureConnector == null ? -1 : securePortIndex.get();
+ trayManager.setServer(server, insecurePort, securePort);
server.join();
}
catch(IOException | MultiException e) {
@@ -148,13 +163,13 @@ private static Server findAvailableSecurePort(CertificateManager certManager) {
// Disable SNI checks for easier print-server testing (replicates Jetty 9.x behavior)
HttpConfiguration httpsConfig = new HttpConfiguration();
SecureRequestCustomizer customizer = new SecureRequestCustomizer();
- customizer.setSniHostCheck(false);
+ customizer.setSniHostCheck(sniStrict);
httpsConfig.addCustomizer(customizer);
HttpConnectionFactory httpConnection = new HttpConnectionFactory(httpsConfig);
ServerConnector secureConnector = new ServerConnector(server, sslConnection, httpConnection);
- secureConnector.setHost(certManager.getProperties().getProperty("wss.host"));
+ secureConnector.setHost(wssHost);
secureConnector.setPort(getSecurePortInUse());
server.setConnectors(new Connector[] {secureConnector});
diff --git a/src/qz/ws/SingleInstanceChecker.java b/src/qz/ws/SingleInstanceChecker.java
index 24398fe30..254b53f92 100644
--- a/src/qz/ws/SingleInstanceChecker.java
+++ b/src/qz/ws/SingleInstanceChecker.java
@@ -14,6 +14,10 @@
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
+import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.*;
@@ -53,17 +57,27 @@ public class SingleInstanceChecker {
private WebSocketClient client;
- public SingleInstanceChecker(TrayManager trayManager, int port) {
+ public SingleInstanceChecker(TrayManager trayManager, int port, boolean usingSecure) {
this.trayManager = trayManager;
log.debug("Checking for a running instance of {} on port {}", Constants.ABOUT_TITLE, port);
autoCloseClient(AUTO_CLOSE);
- connectTo("ws://localhost:" + port);
+ String uri = String.format("%s//localhost:%d", (usingSecure ? "wss:" : "ws:"), port);
+ connectTo(uri, usingSecure);
}
- private void connectTo(String uri) {
+ private void connectTo(String uri, boolean usingSecure) {
try {
if (client == null) {
- client = new WebSocketClient();
+ if(usingSecure) {
+ // Self-signed certs won't be trusted, create "trustAll" connector
+ SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(true);
+ ClientConnector clientConnector = new ClientConnector();
+ clientConnector.setSslContextFactory(sslContextFactory);
+ HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP(clientConnector));
+ client = new WebSocketClient(httpClient);
+ } else {
+ client = new WebSocketClient();
+ }
client.start();
client.setConnectTimeout(TIMEOUT);
client.setIdleTimeout(Duration.ofMillis(TIMEOUT));