diff --git a/.idea/tray.iml b/.idea/tray.iml
index 5bc8ad17d..38596f954 100644
--- a/.idea/tray.iml
+++ b/.idea/tray.iml
@@ -1,11 +1,12 @@
-
+
+
@@ -34,4 +35,4 @@
-
+
\ No newline at end of file
diff --git a/ant/apple/apple-bundle.plist.in b/ant/apple/apple-bundle.plist.in
index 265f2d26b..a7ccaf93f 100644
--- a/ant/apple/apple-bundle.plist.in
+++ b/ant/apple/apple-bundle.plist.in
@@ -2,7 +2,7 @@
CFBundleDevelopmentRegionEnglish
- CFBundleIconFile${apple.icon}
+ CFBundleIconFile${project.filename}
CFBundlePackageTypeAPPL
CFBundleGetInfoString${project.name} ${build.version}
CFBundleSignature${project.name}
diff --git a/ant/apple/apple-keygen.sh.in b/ant/apple/apple-keygen.sh.in
deleted file mode 100644
index 169a2cf0f..000000000
--- a/ant/apple/apple-keygen.sh.in
+++ /dev/null
@@ -1,217 +0,0 @@
-#!/bin/bash
-##########################################################################################
-# ${project.name} MacOS KeyGen Utility #
-##########################################################################################
-# Description: #
-# 1. Creates a self-signed Java Keystore for jetty wss://localhost or [hostname] #
-# 2. Exports public certificate from Java Keystore #
-# 3. Imports into Apple OS X Trusted Root Certs #
-# #
-# Note: If [trustedcert] and [trustedkey] are specified, import to browser/OS is #
-# omitted. #
-# #
-# Depends: #
-# java #
-# #
-# Optional: #
-# openssl - Required if providing [trustedcert], [trustedkey] parameters #
-# #
-# Usage: #
-# $ ./${apple.keygen.name} "install" [hostname] [trustedcert] [trustedkey] #
-# $ ./${apple.keygen.name} "uninstall" #
-# #
-##########################################################################################
-
-# Handle CN=${ssl.cn} override
-cnoverride="$2"
-
-# Handle trusted ssl certificate
-if [[ -n $3 && -n $4 ]]; then
- trustedcertpath="$3"
- trustedkeypath="$4"
-fi
-
-# Random password hash
-password=$(cat /dev/urandom | env LC_CTYPE=C tr -dc 'a-z0-9' | fold -w ${ssl.passlength} | head -n 1)
-
-if ${apple.jvmver} > /dev/null 2>&1; then
- # Prefix with java_home --exec
- keytoolcmd="${apple.jvmcmd} keytool"
-else
- # Fallback on Internet Plug-Ins version if needed
- keytoolcmd="\"${apple.jvmfallback}/keytool\""
-fi
-
-# Check for IPv4 address
-function ip4 {
- if [[ $1 =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
- return 0
- fi
- return 1
-}
-
-# Replace all install-time variables
-function replace_vars {
- cmd=$(echo "$1" | sed -e "s|\"keytool\"|$keytoolcmd|g")
-
- # Handle CN=${ssl.cn} override
- if [ -n "$cnoverride" ]; then
- cmd=$(echo "$cmd" | sed -e "s|CN=${ssl.cn},|CN=$cnoverride,|g")
- if ip4 "$cnoverride"; then
- cmd=$(echo "$cmd" | sed -e "s|san=dns:${ssl.cn},|san=ip:$cnoverride,|g")
- else
- cmd=$(echo "$cmd" | sed -e "s|san=dns:${ssl.cn},|san=dns:$cnoverride,|g")
- fi
- # Remove dangling san
- cmd=$(echo "$cmd" | sed -e "s|,dns:${ssl.cnalt}||g")
- fi
-
- cmd=$(echo "$cmd" | sed -e "s|\!install|${apple.installdir}|g")
- cmd=$(echo "$cmd" | sed -e "s|\!storepass|$password|g")
- cmd=$(echo "$cmd" | sed -e "s|\!keypass|$password|g")
- cmd=$(echo "$cmd" | sed -e "s|\!sslcert|$trustedcertpath|g")
- cmd=$(echo "$cmd" | sed -e "s|\!sslkey|$trustedkeypath|g")
-
- echo "$cmd"
- return 0
-}
-
-# Handle "community" mode, custom signing auth cert
-if [ -n '${build.type}' ]; then
- authcertpath=$(echo "${authcert.install}" | sed -e "s|\!install|${apple.installdir}|g")
-fi
-
-# Write out the secure websocket properties file
-function write_properties {
- propspath=$(echo "$1" | sed -e "s|\!install|${apple.installdir}|g")
- keystorepath=$(echo "${ssl.jks}" | sed -e "s|\!install|${apple.installdir}|g")
- echo "wss.alias=${ssl.alias}" > "$propspath"
- echo "wss.keystore=$keystorepath" >> "$propspath"
- echo "wss.keypass=$password" >> "$propspath"
- echo "wss.storepass=$password" >> "$propspath"
- echo "wss.host=${ssl.host}" >> "$propspath"
- if [ -n "$authcertpath" ]; then
- echo "authcert.override=$authcertpath" >> "$propspath"
- fi
- echo "" >> "$propspath"
- check_exists "$propspath"
- return $?
-}
-
-# Delete a file if exists
-function delete_file {
- testfile=$(echo "$1" | sed -e "s|\!install|${apple.installdir}|g")
- rm -f "$testfile" > /dev/null 2>&1
- return 0
-}
-
-# Check to see if file exists with optional message
-function check_exists {
- testfile=$(echo "$1" | sed -e "s|\!install|${apple.installdir}|g")
- if [ -e "$testfile" ]; then
- if [ -n "$2" ]; then
- echo -e "${bash.success} $2 $testfile"
- else
- echo -e "${bash.success} $testfile"
- fi
- return 0
- fi
- echo -e "${bash.failure} $testfile"
- return 1
-}
-
-# Remove all matching system certificates
-function remove_certs {
- if security find-certificate -e "${vendor.email}" -Z > /dev/null 2>&1; then
- echo -e "${bash.success} Found certificate matching ${vendor.email}"
- hash=$(security find-certificate -e "${vendor.email}" -Z |grep ^SHA-1|rev|cut -d' ' -f1|rev)
- if [ -n "$hash" ]; then
- # Remove and recurse
- security delete-certificate -Z "$hash" > /dev/null 2>&1 && remove_certs
- fi
- else
- echo -e "${bash.success} No more matching certificates found"
- fi
-
- return 0
-}
-
-# Runs a steps, optionally checks for a file
-# e.g: run_step "Description" "ls -al *.txt > ./out" "./out"
-function run_step {
- if eval "$(replace_vars "$2") > /dev/null 2>&1"; then
- if [ -z "$3" ]; then
- echo -e "${bash.success} $1"
- return 0
- elif check_exists "$3" "$1"; then
- return 0
- else
- return 1
- fi
- fi
- echo -e "${bash.failure}\n"
- return 1
-}
-
-#
-# Uninstall mode
-#
-if [ "$1" == "uninstall" ]; then
- echo -e "\nRemoving installed certificates..."
- remove_certs
- echo -e "\n[Finished ${apple.keygen.name}]\n"
- exit 0
-fi
-
-#
-# Install mode
-#
-
-# Delete old files if exist
-delete_file "${ssl.jks}"
-delete_file "${ssl.crt}"
-
-# Handle trusted ssl certificate, if specified
-if [ -n "$trustedcertpath" ]; then
- echo -e "\nCreating keystore for secure websockets..."
- run_step "\nConverting to PKCS12 keypair" "${trusted.command}" "${trusted.keypair}"
- run_step "\nConverting to jks format" "${trusted.convert}" "${ssl.jks}"
- write_properties "${ssl.properties}" || exit 1
- echo -e "\n[Finished ${apple.keygen.name}]\n"
- exit 0
-fi
-
-# Handle self-signed certificate
-echo -e "\nCreating keystore for secure websockets..."
-# Delete old files if exist
-delete_file "${ca.jks}"
-delete_file "${ca.crt}"
-delete_file "${ssl.csr}"
-
-run_step "Creating a CA keypair" "${ca.jkscmd}" "${ca.jks}" || exit 1
-run_step "Exporting CA certificate" "${ca.crtcmd}" "${ca.crt}" || exit 1
-run_step "Creating an SSL keypair" "${ssl.jkscmd}" "${ssl.jks}" || exit 1
-run_step "Creating an SSL CSR" "${ssl.jkscsr}" "${ssl.csr}" || exit 1
-run_step "Issuing SSL certificate from CA" "${ssl.crtcmd}" "${ssl.crt}" || exit 1
-run_step "Importing CA certificate into SSL keypair" "${ssl.importca}" "" || exit 1
-run_step "Importing chained SSL certificate into SSL keypair" "${ssl.importssl}" "" || exit 1
-
-# Kill any running versions
-kill -9 $(ps -e |grep "${project.filename}.jar" |sed "/grep/d"|awk '{print $1}') > /dev/null 2>&1
-
-echo -e "\nWriting properties file..."
-write_properties "${ssl.properties}" || exit 1
-
-echo -e "\nRemoving installed certificates..."
-remove_certs
-
-# Install new certificate
-run_step "Installing certificate" "${apple.keygen.install}" "" || exit 1
-
-echo -e "\nCleaning up..."
-delete_file "${ca.jks}"
-delete_file "${ssl.csr}"
-delete_file "${ssl.crt}"
-
-echo -e "\n[Finished ${apple.keygen.name}]\n"
-exit 0
diff --git a/ant/apple/apple-launcher.sh.in b/ant/apple/apple-launcher.sh.in
deleted file mode 100644
index 66f032fa9..000000000
--- a/ant/apple/apple-launcher.sh.in
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/bin/bash
-##########################################################################################
-# ${project.name} MacOS Launcher #
-##########################################################################################
-# Description: #
-# 1. Searches for Java #
-# 2. Launches ${project.name} with the best Java version #
-# #
-# Depends: #
-# java #
-# #
-# Usage: #
-# $ ./${project.name} #
-##########################################################################################
-
-# Build absolute path to the jar file, based relative to the location of this script
-installpath=$(echo "$0" | rev | cut -d/ -f4- | rev)
-jarpath=$installpath/${project.filename}.jar
-iconpath=$installpath/${apple.resources}/${apple.icon}
-localautostart="${apple.datadir.local}/${autostart.name}"
-globalautostart="${apple.datadir.shared}/${autostart.name}"
-${apple.jvmver} > /dev/null 2>&1
-fallback=$?
-
-# If launched at startup, check override first
-if [ "$1" = "-A" ]; then
- echo "Autostart switch '-A' was provided"
- if [ -f "$localautostart" ] && [ "$(head -n 1 "$localautostart")" = "0" ]; then
- echo "Skipping autostart per '$localautostart'"
- exit 0
- elif [ -f "$globalautostart" ] && [ "$(head -n 1 "$globalautostart")" = "0" ]; then
- echo "Skipping autostart per '$globalautostart'"
- exit 0
- else
- echo "No autostart conflicts found in '$localautostart' or '$globalautostart'. Starting."
- fi
-fi
-
-# Fallback on Internet Plug-Ins version if needed
-if [ $fallback -eq 0 ]; then
- ${apple.jvmcmd} java ${launch.opts} -Xdock:name="${project.name}" -Xdock:icon="$iconpath" -jar -Dapple.awt.UIElement="true" "$jarpath" -NSRequiresAquaSystemAppearance False
-else
- "${apple.jvmfallback}/java" ${launch.opts} -Xdock:name="${project.name}" -Xdock:icon="$iconpath" -jar -Dapple.awt.UIElement="true" "$jarpath" -NSRequiresAquaSystemAppearance False
-fi
-
-exit $?
diff --git a/ant/apple/apple-packager.sh.in b/ant/apple/apple-packager.sh.in
deleted file mode 100644
index ca1e5168c..000000000
--- a/ant/apple/apple-packager.sh.in
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/bash
-##########################################################################################
-# ${project.name} MacOS Packager #
-##########################################################################################
-# Description: #
-# 1. Packages software into an Apple pkg installer #
-# #
-# Depends: #
-# pkgbuild, security #
-# #
-# Usage: #
-# $ chmod +x ${apple.packager.name} #
-# $ ./${apple.packager.name} #
-# #
-##########################################################################################
-echo
-echo "============================================"
-echo " Packaging ${project.name}"
-echo "============================================"
-echo
-chmod +x "${apple.keygen.out}"
-
-# Checks if we have an Apple code signing cert
-security find-identity -v |grep "(${apple.packager.signid})"
-
-if [ $? == 0 ]; then
- signing="--sign \"${apple.packager.signid}\""
- suffix=""
-else
- signing=""
- suffix="-unsigned"
-fi
-
-eval "pkgbuild --identifier \"${project.filename}\" \
- --root \"${dist.dir}\" \
- --install-location \"${apple.installdir}\" \
- --scripts \"${apple.scripts}\" \
- --version \"${build.version}\" \
- ${signing}\
- \"${out.dir}/${project.filename}${build.type}-${build.version}${suffix}.pkg\""
-code=$?
-
-echo
-echo "============================================"
-echo " Finished "
-echo "============================================"
-echo
-exit $code
diff --git a/ant/apple/apple-postinstall.sh.in b/ant/apple/apple-postinstall.sh.in
index 043a2e86d..c3ce1a1e0 100644
--- a/ant/apple/apple-postinstall.sh.in
+++ b/ant/apple/apple-postinstall.sh.in
@@ -1,62 +1,23 @@
#!/bin/bash
-##########################################################################################
-# ${project.name} MacOS Postinstall #
-##########################################################################################
-# Description: #
-# 1. Generates and installs certificate for secure websockets #
-# 2. Installs certificate into Firefox (if installed) #
-# 3. Performs any cleanup operations #
-# 4. Launches app as user (not root) #
-# #
-# Usage: #
-# $ ./postinstall #
-##########################################################################################
-# Install ${project.name} certificate
-"${apple.installdir}/auth/${apple.keygen.name}" "install"
+# Halt on first error
+set -e
-if [ $? -eq 0 ]; then
- # Install Firefox certificate
- "${apple.installdir}/auth/firefox/${firefoxcert.name}" "install"
-fi
-
-# Install startup
-site=$(echo "${vendor.website}"|rev|cut -d/ -f1|rev)
-package=$(echo "$site" |rev |cut -d. -f1|rev).$(echo "$site" |rev |cut -d. -f2|rev).${project.filename}
-cat > /Library/LaunchAgents/$package.plist << EOT
-
-
-
-
- Label$package
- KeepAlive
-
- SuccessfulExit
- AfterInitialDemand
-
- RunAtLoad
- ProgramArguments
-
- ${apple.installdir}/${apple.macos}/${project.name}
- -A
-
-
-
-EOT
+# Get working directory
+DIR=$(cd "$(dirname "$0")" && pwd)
+pushd "$DIR/payload/Contents/MacOS/"
-# Shared directory for FileIO operations
-mkdir -p "${apple.datadir.shared}" 2>&1
-chmod 777 "${apple.datadir.shared}"
+./"${project.name}" install
+popd
-# Cleanup resources from previous versions
-rm -rf "${apple.installdir}/demo/js/3rdparty"
-rm "${apple.installdir}/demo/js/qz-websocket.js"
+# Use install target from pkgbuild, an undocumented feature; fallback on sane location
+if [ -n "$2" ]; then
+ pushd "$2/Contents/MacOS/"
+else
+ pushd "/Applications/${project.name}.app/Contents/MacOS/"
+fi
-# Remove 2.1 startup entry
-site=$(echo "${vendor.website}"|rev|cut -d/ -f1|rev)
-package=$(echo "$site" |rev |cut -d. -f1|rev).$(echo "$site" |rev |cut -d. -f2|rev).${project.filename}
-rm "/Library/LaunchAgents/$package.plist"
+./"${project.name}" certgen
-# Start ${project.name}
-echo -e "\nStarting ${project.name} as $USER..."
-su $USER -c "open \"${apple.installdir}\""
+# Start qz by calling open on the .app as an ordinary user
+su "$USER" -c "open ../../"
\ No newline at end of file
diff --git a/ant/apple/apple-preinstall.sh.in b/ant/apple/apple-preinstall.sh.in
index d5c87ba27..9c760c19e 100644
--- a/ant/apple/apple-preinstall.sh.in
+++ b/ant/apple/apple-preinstall.sh.in
@@ -1,43 +1,10 @@
#!/bin/bash
-##########################################################################################
-# ${project.name} MacOS Preinstall #
-##########################################################################################
-# Description: #
-# 1. Checks for minimum Java version #
-# 2. Kills any running instances #
-# #
-# Usage: #
-# $ ./preinstall #
-##########################################################################################
-# Check minimum java version
-function check_java() {
- curver=$("${apple.jvmfallback}/java" -version 2>&1 | grep -i version | cut -d'"' -f2 | cut -d'.' -f1-2)
- minver="${javac.target}"
+# Halt on first error
+set -e
- if [ -z "$curver" ]; then
- curver="0.0"
- fi
+# Get working directory
+DIR=$(cd "$(dirname "$0")" && pwd)
+pushd "$DIR/payload/Contents/MacOS/"
- if [ $(echo "$curver>=$minver" | bc -l) -eq 0 ]; then
- osascript -e "tell app \"Installer\" to display dialog \"Java $minver is required for installation.\nDownload it now?\""
- if [ $? -eq 0 ]; then
- open "${java.download}"
- fi
- exit 1
- fi
-}
-
-# Use java_home command to check minimum Java version
-${apple.jvmver} > /dev/null 2>&1
-code=$?
-
-# Fallback on Internet Plug-Ins version if needed
-if [ $code -ne 0 ]; then
- check_java
- code=$?
-fi
-
-# Kill any running versions
-kill -9 $(ps -e |grep "${project.filename}.jar" |sed "/grep/d"|awk '{print $1}') > /dev/null 2>&1
-exit $code
+./"${project.name}" preinstall
\ No newline at end of file
diff --git a/ant/apple/apple-uninstall.sh.in b/ant/apple/apple-uninstall.sh.in
deleted file mode 100644
index 046d54e84..000000000
--- a/ant/apple/apple-uninstall.sh.in
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/bash
-##################################################################################################
-# ${project.name} MacOS Uninstall #
-##################################################################################################
-# Description: #
-# 1. Removes certificate for secure websockets #
-# 2. Removes certificate in Firefox #
-# 3. Removes application bundle #
-# #
-# Usage: #
-# $ sudo ./uninstall #
-##################################################################################################
-
-if [ "$(id -u)" != "0" ]; then
- echo -e "\nThis script must be run with root (sudo) privileges" 1>&2
- echo -e "${bash.failure}"
- exit 1
-fi
-
-# Kill any running versions
-echo -e "Killing any running versions..."
-kill -9 $(ps -e |grep "${project.filename}.jar" |sed "/grep/d"|awk '{print $1}') > /dev/null 2>&1
-if [ $? -eq 0 ]; then
- echo -e "${bash.success}"
-else
- echo -e "${bash.skipped}"
-fi
-
-# Remove startup entry
-site=$(echo "${vendor.website}"|rev|cut -d/ -f1|rev)
-package=$(echo "$site" |rev |cut -d. -f1|rev).$(echo "$site" |rev |cut -d. -f2|rev).${project.filename}
-rm -f /Library/LaunchAgents/$package.plist
-
-# Uninstall ${project.name} system certificates
-"${apple.installdir}/auth/${apple.keygen.name}" "uninstall"
-
-if [ $? -eq 0 ]; then
- # Uninstall Firefox certificate
- "${apple.installdir}/auth/firefox/${firefoxcert.name}" "uninstall"
-fi
-
-# Remove 1.9/2.0 style lingering startup shortcuts
-users=$(dscl . list /Users | sed "/^_/d")
-a=0
-while read -r line; do
- sudo -u "$line" osascript -e "tell application \"System Events\" to delete every login item where name is \"${project.name}\"" > /dev/null 2>&1
-done <<< "$users"
-
-echo -e "Cleanup is complete. Removing ${apple.installdir}..."
-rm -rf "${apple.installdir}"
-echo -e "${bash.success}"
-
-echo -e "\nUninstall of ${project.name} complete.\n"
diff --git a/ant/apple/apple.properties b/ant/apple/apple.properties
index 806802a16..4407bab1d 100644
--- a/ant/apple/apple.properties
+++ b/ant/apple/apple.properties
@@ -1,57 +1,2 @@
# Apple build properties
-apple.icon=apple-icon.icns
-
-apple.scripts=${build.dir}/scripts
-
-apple.packager.name=apple-packager.sh
-apple.packager.in=${basedir}/ant/apple/${apple.packager.name}.in
-apple.packager.out=${build.dir}/${apple.packager.name}
-apple.packager.cert=${basedir}/ant/apple/apple-packager.cer
-apple.codesign.cert=${basedir}/ant/apple/apple-codesign.cer
apple.packager.signid=P5DMU6659X
-apple.intermediate.cert=${basedir}/ant/apple/apple-intermediate.cer
-
-apple.keygen.store=trustRoot
-apple.keygen.name=apple-keygen.sh
-apple.keygen.in=${basedir}/ant/apple/${apple.keygen.name}.in
-apple.keygen.out=${dist.dir}/auth/${apple.keygen.name}
-apple.keygen.install=security add-trusted-cert -d -r \\"${apple.keygen.store}\\" -k \\"${apple.keychain}\\" \\"${ca.crt}\\"
-apple.jvmver=/usr/libexec/java_home -v ${javac.target}+
-apple.jvmcmd=${apple.jvmver} --exec
-apple.jvmfallback=/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin
-
-apple.postinstall.in=${basedir}/ant/apple/apple-postinstall.sh.in
-apple.postinstall.out=${apple.scripts}/postinstall
-
-apple.preinstall.in=${basedir}/ant/apple/apple-preinstall.sh.in
-apple.preinstall.out=${apple.scripts}/preinstall
-
-apple.uninstall.in=${basedir}/ant/apple/apple-uninstall.sh.in
-apple.uninstall.out=${dist.dir}/uninstall
-
-apple.plist.in=${basedir}/ant/apple/apple-bundle.plist.in
-apple.plist.out=${dist.dir}/Contents/Info.plist
-
-apple.resources=Contents/Resources
-apple.macos=Contents/MacOS
-
-apple.launcher.in=${basedir}/ant/apple/apple-launcher.sh.in
-apple.launcher.out=${dist.dir}/${apple.macos}/${project.name}
-
-apple.installdir=/Applications/${project.name}.app
-apple.datadir.local=$HOME/Library/Application Support/${project.datadir}
-apple.datadir.shared=/Library/Application Support/${project.datadir}
-
-apple.keychain=/Library/Keychains/System.keychain
-
-# Console colors
-bash.red=\\x1B[1;31m
-bash.green=\\x1B[1;32m
-bash.yellow=\\x1B[1;33m
-bash.plain=\\x1B[0m
-bash.colors=red=${bash.red};green=${bash.green};yellow=${bash.yellow};plain=${bash.plain};
-
-bash.success=\ \ \ [${bash.green}success${bash.plain}]
-bash.failure=\ \ \ [${bash.red}failure${bash.plain}]
-bash.skipped=\ \ \ [${bash.yellow}skipped${bash.plain}]
-bash.aborted=\ \ \ [${bash.red}aborted${bash.plain}]
diff --git a/ant/apple/apple-codesign.cer b/ant/apple/certs/apple-codesign.cer
similarity index 100%
rename from ant/apple/apple-codesign.cer
rename to ant/apple/certs/apple-codesign.cer
diff --git a/ant/apple/apple-intermediate.cer b/ant/apple/certs/apple-intermediate.cer
similarity index 100%
rename from ant/apple/apple-intermediate.cer
rename to ant/apple/certs/apple-intermediate.cer
diff --git a/ant/apple/apple-packager.cer b/ant/apple/certs/apple-packager.cer
similarity index 100%
rename from ant/apple/apple-packager.cer
rename to ant/apple/certs/apple-packager.cer
diff --git a/ant/firefox/firefox-cert.sh.in b/ant/firefox/firefox-cert.sh.in
deleted file mode 100755
index 5e16991a1..000000000
--- a/ant/firefox/firefox-cert.sh.in
+++ /dev/null
@@ -1,253 +0,0 @@
-#!/bin/bash
-###############################################################################
-# ${project.name} Linux / Unix Firefox Certificate Utility #
-###############################################################################
-# Description: #
-# INSTALL: #
-# * Searches for Firefox installation path #
-# * Automatically installs (or toggles on) the use of a custom SSL #
-# certificate based on Firefox version and OS using a combination of #
-# legacy AutoConfig or polcies.json #
-# #
-# UNINSTALL: #
-# * If necessary, automatically deletes the custom SSL certificate using #
-# a combination of legacy AutoConfig or policies.json #
-# #
-# Depends: #
-# - lsregister (Apple-only, provided by launch services) #
-# - perl (standard with most modern Linux/Unix systems) #
-# #
-# Usage: #
-# $ ./${firefoxcert.name} "install" [hostname] #
-# $ ./${firefoxcert.name} "uninstall" [hostname] #
-# #
-###############################################################################
-
-# Array of possible Firefox application names.
-appnames=("IceWeasel" "Firefox") # "Firefox" "IceWeasel", etc
-
-# Array of possible pref tag conflicts
-conflicts=("general.config.filename")
-
-mask=755
-
-#
-# Uses "which" and "readlink" to locate firefox on Linux, etc
-#
-function get_ffdir()
-{
- for i in "${appnames[@]}"; do
- ffdirtemp=$("$locationdir/${locator.name}" $i)
- if [ $? == 0 ]; then
- ffdir="$ffdirtemp"
- return 0
- fi
- done
- return 1
-}
-
-echo "Looking for python..."
-pythonpath="$(which python || which python3)"
-if [ -z "$pythonpath" ]; then
- echo -e "${bash.failure} python was not found"
-else
- echo -e "${bash.success} python found $pythonpath"
-fi
-
-echo "Searching for Firefox..."
-
-ffdir=""
-if [[ "$OSTYPE" == "darwin"* ]]; then
- # macOS
- locationdir=$(cd "$(dirname "$0")"; pwd)
- get_ffdir
-
- bindir="$ffdir/Contents/Resources/"
- prefdir="$ffdir/Contents/Resources/defaults/pref"
- installdir="${apple.installdir}"
- trayapp="$installdir" # use .app package
-
- # Use policies.json deployment (Firefox 63+)
- if [ -n "$ffdir" ]; then
- version=$(HOME=/tmp "$ffdir/Contents/MacOS/firefox" --version|rev|cut -d' ' -f1|rev|cut -d. -f1)
- if [ "$version" -ge "63" ]; then
- policy='{ "policies": { "Certificates": { "ImportEnterpriseRoots": true } } }'
- policypath="$ffdir/Contents/Resources/distribution/policies.json"
- fi
- fi
-else
- # Linux, etc
- location=$(readlink -f "$0")
- locationdir=$(dirname "$location")
- get_ffdir
-
- bindir="$ffdir"
- prefdir="$ffdir/defaults/pref"
- installdir="${linux.installdir}"
- dercertpath=$(echo "${ca.crt}" | sed -e "s|\!install|$installdir|g")
- trayapp="" # skip
-
- # Use policies.json deployment (Firefox 65+)
- if [ -n "$ffdir" ]; then
- version=$(HOME=/tmp XAUTHORITY=/tmp $ffdir/firefox --version|rev|cut -d' ' -f1|rev|cut -d. -f1)
- if [ "$version" -ge "65" ]; then
- policy='{ "policies": { "Certificates": { "Install": ["'$dercertpath'"] } } }'
- policypath="$ffdir/distribution/policies.json"
- fi
- fi
-fi
-
-# Firefox was not found, skip Firefox certificate installation
-if [ -z "$ffdir" ] || [ "$ffdir" = "/" ]; then
- echo -e "${bash.skipped} Firefox not found"
- exit 0
-else
- echo -e "${bash.success} Firefox $version found at $ffdir"
-fi
-
-# Handle CN=${ssl.cn} override
-cname="${ssl.cn}"
-if [ -n "$2" ]; then
- cname="$2"
-fi
-
-# Perform substitutions
-dercertpath=$(echo "${ca.crt}" | sed -e "s|\!install|$installdir|g")
-prefspath=$(echo "${firefoxprefs.install}" | sed -e "s|\!install|$installdir|g")
-configpath=$(echo "${firefoxconfig.install}" | sed -e "s|\!install|$installdir|g")
-
-#
-# Uninstall mode
-#
-if [ "$1" == "uninstall" ]; then
- # Newer Firefox versions don't use AutoConfig
- if [ -n "$policy" ]; then
- if [[ "$OSTYPE" == "darwin"* ]]; then
- # macOS
- echo -e "${bash.skipped} Configured via policies.json, no uninstall needed"
- exit 0
- else
- # Linux, etc
- echo -e "\nSearching for $policypath..."
- if [ -f "$policypath" ]; then
- # Delete cert entry
- "$pythonpath" "$locationdir/${jsonwriter.name}" "$policypath" "$policy" "" "true"
- echo -e "${bash.success} Deleted cert entry"
- exit 0
- else
- echo -e "${bash.skipped} $policypath not found"
- exit 0
- fi
- fi
- fi
- echo -e "\nSearching for ${project.name} AutoConfig..."
- if [ -f "$bindir/${firefoxconfig.name}" ]; then
- echo -e "${bash.success} Check Firefox config exists"
- cp "$configpath" "$bindir/${firefoxconfig.name}"
- chmod $mask "$bindir/${firefoxconfig.name}"
- # Replace ${certData} with the blank string
- perl -pi -e "s#\\\$\{certData\}##g" "$bindir/${firefoxconfig.name}"
- ret1=$?
- perl -pi -e "s#\\\$\{uninstall\}#true#g" "$bindir/${firefoxconfig.name}"
- ret2=$?
- perl -pi -e "s#\\\$\{timestamp\}#-1#g" "$bindir/${firefoxconfig.name}"
- ret3=$?
- perl -pi -e "s#\\\$\{commonName\}#$cname#g" "$bindir/${firefoxconfig.name}"
- ret4=$?
- perl -pi -e "s#\\\$\{trayApp\}##g" "$bindir/${firefoxconfig.name}"
- ret5=$?
- if [ $ret1 -eq 0 ] && [ $ret2 -eq 0 ] && [ $ret3 -eq 0 ] && [ $ret4 -eq 0 ] && [ $ret5 -eq 0 ]; then
- echo -e "${bash.success} Certificate removed successfully"
- else
- echo -e "${bash.failure} ${project.name} Certificate removal failed"
- exit 1
- fi
- else
- echo -e "${bash.skipped} ${project.name} AutoConfig not found"
- fi
- echo -e "\n[Finished ${firefoxcert.name}]\n"
- exit 0
-fi
-
-#
-# Install mode (default)
-#
-
-# Use policy file
-if [ -n "$policy" ]; then
- "$pythonpath" "$locationdir/${jsonwriter.name}" "$policypath" "$policy"
- chmod go+r "$policypath"
- chmod go+rx "$(dirname "$policypath")"
- echo -e "${bash.success} Installed $policypath"
- exit 0
-fi
-
-echo -e "\nSearching for Firefox AutoConfig conflicts..."
-
-# Iterate over each preference file looking for conflicts
-for i in $prefdir/*; do
- if [ "$i" == "$prefdir/${firefoxprefs.name}" ]; then
- # skip, ${project.name} preferences
- echo -e "${bash.skipped} Ignoring ${project.name} preference file \"${firefoxprefs.name}\""
- continue
- fi
- for j in "${conflicts[@]}"; do
- grep '"$j"' $i &>/dev/null
- ret1=$?
- grep "'$j'" $i &>/dev/null
- ret2=$?
- if [ $ret1 -eq 0 ] || [ $ret2 -eq 0 ]; then
- echo -e "${bash.failure} Conflict found while looking for \"$j\"\n\tin $i"
- exit 1
- fi
- done
-done
-
-echo -e "${bash.success} No conflicts found"
-
-
-
-echo -e "\nRegistering with Firefox..."
-cp "$prefspath" "$prefdir/${firefoxprefs.name}"
-cp "$configpath" "$bindir/${firefoxconfig.name}"
-chmod $mask "$prefdir/${firefoxprefs.name}"
-chmod $mask "$bindir/${firefoxconfig.name}"
-
-bcert="-----BEGIN CERTIFICATE-----"
-ecert="-----END CERTIFICATE-----"
-blank=""
-
-# Read certificate, stripping newlines
-certdata=$(cat "$dercertpath" |tr -d '\n'|tr -d '\r')
-
-# Strip all non-base64 data
-certdata=$(echo "$certdata" | sed -e "s|$bcert|$blank|g")
-certdata=$(echo "$certdata" | sed -e "s|$ecert|$blank|g")
-timestamp=$(date +%s)
-
-if [ -f "$bindir/${firefoxconfig.name}" ]; then
- echo -e "${bash.success} Check ${project.name} AutoConfig exists"
- # Replace ${certData} with the base64 string
- perl -pi -e "s#\\\$\{certData\}#$certdata#g" "$bindir/${firefoxconfig.name}"
- ret1=$?
- perl -pi -e "s#\\\$\{uninstall\}#false#g" "$bindir/${firefoxconfig.name}"
- ret2=$?
- perl -pi -e "s#\\\$\{timestamp\}#$timestamp#g" "$bindir/${firefoxconfig.name}"
- ret3=$?
- perl -pi -e "s#\\\$\{commonName\}#$cname#g" "$bindir/${firefoxconfig.name}"
- ret4=$?
- perl -pi -e "s#\\\$\{trayApp\}#$trayapp#g" "$bindir/${firefoxconfig.name}"
- ret5=$?
- if [ $ret1 -eq 0 ] && [ $ret2 -eq 0 ] && [ $ret3 -eq 0 ] && [ $ret4 -eq 0 ] && [ $ret5 -eq 0 ]; then
- echo -e "${bash.success} Certificate installed"
- else
- echo -e "${bash.failure} Certificate installation failed"
- fi
-else
- echo -e "${bash.failure} Cannot locate ${project.name} AutoConfig"
- exit 1
-fi
-
-echo -e "\n[Finished ${firefoxcert.name}]\n"
-exit 0
-
diff --git a/ant/firefox/firefox-json-writer.py b/ant/firefox/firefox-json-writer.py
deleted file mode 100755
index 8e9825921..000000000
--- a/ant/firefox/firefox-json-writer.py
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/env python
-
-import errno
-import json
-import os
-import sys
-
-DEFAULT_PATH = '/Applications/Firefox.app/Contents/Resources/distribution/policies.json'
-DEFAULT_DATA = '{ "policies": { "Certificates": { "ImportEnterpriseRoots": true } } }'
-DEFAULT_OVERWRITE = False
-DEFAULT_DELETE = False
-
-path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_PATH
-merge = json.loads(sys.argv[2]) if len(sys.argv) > 2 else json.loads(DEFAULT_DATA)
-overwrite = sys.argv[3].lower() == 'true' if len(sys.argv) > 3 else DEFAULT_OVERWRITE
-deletion = sys.argv[4].lower() == 'true' if len(sys.argv) > 4 else DEFAULT_DELETE
-
-
-def mkdir_p(path):
- try:
- os.makedirs(path)
- except OSError as e: # Python >2.5
- if e.errno == errno.EEXIST and os.path.isdir(path):
- pass
- else:
- raise
-
-
-def load_json(path):
- data = json.loads("{}")
- if os.path.isfile(path):
- try:
- stream = open(path, "r")
- data = json.load(stream)
- stream.close()
- except ValueError as e:
- print("Warning: Not a valid JSON file: " + path)
-
- return data
-
-
-def merge_json(base, append):
- """ Writes values from append to base, deep copying if necessary """
- for key, val in append.items():
- base_val = base.get(key)
-
- if base_val is None:
- base[key] = val
- elif type(base_val) is dict and type(val) is dict:
- merge_json(base_val, val)
- elif overwrite:
- # only forces overwrite if no deeper objects exist first
- base[key] = val
- elif type(base_val) is list and type(val) is list:
- # merge list if not overwritten
- for v in val:
- if v not in base_val:
- base_val.append(v)
-
-
-def delete_json(base, append):
- """ Removes values in append from base """
- for key, val in append.items():
- base_val = base.get(key)
-
- if type(base_val) is dict and type(val) is dict:
- delete_json(base_val, val)
- elif type(base_val) is list and type(val) is list:
- for v in val:
- if v in base_val:
- base_val.remove(v)
- elif base_val is not None:
- base.pop(key)
-
-
-policy = load_json(path)
-if deletion:
- delete_json(policy, merge)
-else:
- merge_json(policy, merge)
-
-mkdir_p(os.path.dirname(path))
-stream = open(path, "w+")
-stream.write(json.dumps(policy, sort_keys=True, indent=2))
-stream.close()
diff --git a/ant/firefox/firefox-prefs.js.in b/ant/firefox/firefox-prefs.js.in
deleted file mode 100644
index 1a8f76fd4..000000000
--- a/ant/firefox/firefox-prefs.js.in
+++ /dev/null
@@ -1,3 +0,0 @@
-pref('general.config.filename', '${firefoxconfig.name}');
-pref('general.config.sandbox_enabled', false);
-pref('general.config.obscure_value', 0);
diff --git a/ant/firefox/locator.sh.in b/ant/firefox/locator.sh.in
deleted file mode 100755
index 771a7bba4..000000000
--- a/ant/firefox/locator.sh.in
+++ /dev/null
@@ -1,117 +0,0 @@
-#!/bin/bash
-###############################################################################
-# ${project.name} Linux / Unix Program Locator #
-###############################################################################
-# Description: #
-# INSTALL: #
-# 1. Searches for specified program's installation path #
-# #
-# Depends: #
-# - lsregister (Apple-only, provided by launch services) #
-# - perl (standard with most modern Linux/Unix systems) #
-# #
-# Usage: #
-# $ ./${locator.name} "directoryName" [binName] #
-# #
-###############################################################################
-# Array of secondary search locations
-secondarylocations=("/usr/lib64" "/usr/lib")
-
-
-#
-# Calls lsregister -dump and parses the output for "/Firefox.app", etc. Returns the very first result found.
-#
-function get_osx_targetdir()
-{
- # OSX Array of possible lsregister command locations
- lsregs=("/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister")
- return_val=1
- for i in "${lsregs[@]}"; do
- if [ -f $i ]; then
- IFS_backup=$IFS
- #The filenames may have spaces, use the newline to seperate the array items
- IFS=$'\n'
- #"/$with_dir.app$", the first $ is the bash variable marker and the second is a wildcard meaning "is the end of the line"
- targetdir=$($i -dump |grep -E "/$with_dir.app$" |sed "/\/Volumes\//d" |sed "/\/.Trash\//d" |sed "/\/Applications (Parallels)\//d" |cut -d'/' -f2-)
-
- for r in $targetdir; do
- #If it matches our top priority location, return. We are done.
- if [[ "/$r" == "/Applications"* ]]; then
- dir_out="/$r"
- return_val=0
- break
- #If it matches our second priority, remember it and continue. (remembers last instance)
- elif [[ "/$r" == "$HOME/Applications"* ]]; then
- dir_out="/$r"
- return_val=0
- #If nothing else, remember it if we don't have anything else yet
- elif [ dir_out == "" ]; then
- dir_out="/$r"
- return_val=0
- fi
- done
- fi
- done
- IFS=$IFS_backup
- return $return_val
-}
-
-#
-# Uses "which" and "readlink" to locate firefox on Linux, etc
-#
-function get_targetdir()
-{
- # Convert "Firefox" to "firefox", etc
- lowerdir=$(echo "$with_dir" |tr '[:upper:]' '[:lower:]')
- lowerbin=$(echo "$with_bin" |tr '[:upper:]' '[:lower:]')
- location=$(readlink -f "$(which $lowerdir 2> /dev/null)")
- targetdir=$(dirname "$location")
- if [[ "$targetdir" != "/usr/bin" ]] && [ -f "$targetdir/$lowerbin" ] && file -b "$targetdir/$lowerbin" |grep -q ELF; then
- dir_out="$targetdir"
- return 0
- else
- for d in "${secondarylocations[@]}"; do
- targetdir=$(echo "$d")
- if [ -f "$targetdir/$lowerdir/$lowerbin" ]; then
- dir_out="$targetdir/$lowerdir"
- return 0
- fi
- done
- targetdir=""
- return 1
- fi
- return 0
-}
-
-targetdir=""
-with_dir=""
-with_bin=""
-dir_out=""
-
-if [ "$#" == 0 ]; then
- echo "No program specified, proper usage is locator "
- exit 0
-elif [ "$#" == 1 ]; then
- with_dir=$1
- with_bin=$1
-else
- with_dir=$1
- with_bin=$2
-fi
-
-if [[ "$OSTYPE" == "darwin"* ]]; then
- # Mac OSX
- get_osx_targetdir
-else
- # Linux, etc
- get_targetdir
-fi
-
-echo "$dir_out"
-
-# Firefox was not found, skip Firefox certificate installation
-if [ -z "$targetdir" ] || [ "$targetdir" = "/" ]; then
- echo "$1 not found"
- exit 1
-fi
-exit 0
diff --git a/ant/javafx.xml b/ant/javafx.xml
index 4e4d7face..0970f47b4 100644
--- a/ant/javafx.xml
+++ b/ant/javafx.xml
@@ -112,14 +112,14 @@
-
+
-
+
@@ -179,7 +179,7 @@
-
+
@@ -188,7 +188,7 @@
-
+
@@ -198,7 +198,7 @@
-
+
@@ -208,7 +208,7 @@
-
+
diff --git a/ant/linux/linux-installer.sh.in b/ant/linux/linux-installer.sh.in
index 22511cd9d..261dfaeeb 100644
--- a/ant/linux/linux-installer.sh.in
+++ b/ant/linux/linux-installer.sh.in
@@ -1,288 +1,62 @@
#!/bin/bash
-##########################################################################################
-# ${project.name} Linux Installer #
-##########################################################################################
-# Description: #
-# 1. Stops any existing instances #
-# 2. Patches Ubuntu Unity Desktop for tray icon support #
-# 3. Installs to /opt/${project.filename}/ #
-# 4. Installs certificate to OS using certutil #
-# 5. Installs certificate into Firefox (if installed) #
-# #
-# Note: If [trustedcert] and [trustedkey] are specified, import to browser/OS is #
-# omitted. #
-# #
-# Depends: #
-# java, certutil #
-# #
-# Optional: #
-# openssl - Required if providing [trustedcert], [trustedkey] parameters #
-# #
-# Usage: #
-# $ chmod +x ${linux.installer.name} #
-# $ sudo ./${linux.installer.name} [noprompt] [hostname] [trustedcert] [trustedkey] #
-# #
-##########################################################################################
+
+# Halt on first error
+set -e
if [ "$(id -u)" != "0" ]; then
- echo -e "\nThis script must be run with root (sudo) privileges" 1>&2
- echo -e "${bash.failure}"
+ echo "This script must be run with root (sudo) privileges" 1>&2
exit 1
fi
-noprompt="$1"
-cname="$2"
-trustedcert="$3"
-trustedkey="$4"
-mask=755
-destdir="${linux.installdir}"
-shortcut="/usr/share/applications/${project.filename}.desktop"
-jarfile="${destdir}/${project.filename}.jar"
-launcher="${destdir}/${linux.launcher.name}"
-
-# Confirmation dialog
-height=8; width=41
-function confirm_dialog() {
- # Allow bypassing via 2nd param ("y" or "Y")
- if [ "$2" == "-y" ]; then
- echo "Param \"$2\" was supplied to confirmation dialog, supressing..."
- return 0
- fi
- dialog --help > /dev/null 2>&1
- if [ $? -ne 0 ]; then
- # Legacy input fallback
- echo -e "\n"; read -p "$1 [y/N] " -r; echo
- if [[ $REPLY =~ ^[Yy]$ ]] ; then
- return 0
- else
- echo -e "${bash.aborted}\n"
- exit 1
- fi
- fi
- dialog --title "Install ${project.name}" --backtitle "Install ${project.name}" --yesno "$1" $height $width
- if [ $? -ne 0 ]; then
- echo -e "\n\n${bash.aborted}\n"
- exit 1
- fi
-}
-
-# Progress dialog
-function progress_dialog() {
- dialog --help > /dev/null 2>&1
- if [ $? -ne 0 ]; then
- # Fallback on old input prompt
- echo -e " - $2"; return 0
- fi
- echo "$1" | dialog --title "Install ${project.name}" \
- --backtitle "Install ${project.name}" \
- --gauge "$2" $height $width
-}
-
-# Check minimum java version
-function check_java() {
- curver=$(java -version 2>&1 | grep -i version | awk -F"\"" '{ print $2 }' | awk -F"." '{ print $1 "." $2 }')
- minver="${javac.target}"
-
- if [ -z "$curver" ]; then
- curver="0.0"
- fi
-
- desired=$(echo -e "$minver\n$curver")
- actual=$(echo "$desired" |sort -t '.' -k 1,1 -k 2,2 -n)
-
- if [ "$desired" != "$actual" ]; then
- echo -e "\n\n${bash.failure}\n"
- echo -e "Please install Java ${javac.target} or higher to continue\n"
- exit 1
- fi
-}
-
-confirm_dialog "Are you sure you want to install ${project.name}?" "$noprompt"
+# Console colors
+RED="\\x1B[1;31m";GREEN="\\x1B[1;32m";YELLOW="\\x1B[1;33m";PLAIN="\\x1B[0m"
-progress_dialog 5 "Checking for Java ${javac.target}+..."
-check_java
+# Statuses
+SUCCESS=" [${GREEN}success${PLAIN}]"
+FAILURE=" [${RED}failure${PLAIN}]"
+WARNING=" [${YELLOW}warning${PLAIN}]"
-progress_dialog 10 "Stopping any running versions..."
-pkill -f "java -jar ${jarfile}" > /dev/null 2>&1
-pkill -f "java -jar ${launch.opts} ${jarfile}" > /dev/null 2>&1
-pkill -f "java ${launch.opts} -jar ${jarfile}" > /dev/null 2>&1
-
-progress_dialog 20 "Deleting old files..."
-rm -rf "${destdir}" > /dev/null 2>&1
-
-# Remove 2.1 startup entry
-rm "/etc/xdg/autostart/${project.filename}.desktop"
-
-progress_dialog 25 "Creating directories..."
-mkdir -p "${destdir}" > /dev/null 2>&1
-
-# Shared directory for FileIO operations
-mkdir -p "${linux.datadir.shared}" 2>&1
-chmod 777 "${linux.datadir.shared}"
-
-progress_dialog 30 "Installing new version..."
-cp -R ./ "${destdir}"
-rm "${destdir}/`basename $0`"
-
-progress_dialog 40 "Creating desktop shortcut..."
-echo "[Desktop Entry]
-Type=Application
-Name=${project.name}
-Exec="${launcher}"
-StartupWMClass=${project.name}
-Path=${destdir}
-Icon=${destdir}/${linux.icon}
-MimeType=application/x-qz;x-scheme-handler/qz;
-Terminal=false
-Comment=${project.name}" > "${shortcut}"
-chmod $mask "${shortcut}"
-
-# Tell the desktop to look for new mimetypes in the background
-makeself_umask=`umask`
-umask 0002 # more permissive umask for mimetype registration
-update-desktop-database > /dev/null 2>&1 &
-umask $makeself_umask
-
-# Ubuntu process restarter
-function restart_it() {
- # Check for running process, kill it
- ps -e |grep -q $1
- if [ $? -eq 0 ]; then
- progress_dialog $2 "Killing $1..."
- killall -w $1 > /dev/null 2>&1
- fi
+mask=755
- # Make sure process isn't running, start it
- ps -e |grep -q $1
- if [ $? -ne 0 ]; then
- progress_dialog $(($2 + 5)) "Starting $1..."
- nohup $1 > /dev/null 2>&1 &
- fi
-}
+echo -e "Starting install...\n"
-# Provide user environmental variables to the sudo environment
-function sudo_env() {
- userid="$(logname 2>/dev/null || echo $SUDO_USER)"
- if [ -z "$1" ]; then
- daemon="dbus-daemon"
- lookfor="--config-file="
+run_task () {
+ echo -e "Running $1 task..."
+ if [ -n "$DEBUG" ]; then
+ "./${project.filename}" $@ && ret_val=$? || ret_val=$?
else
- daemon="$1"
- lookfor="$2"
+ "./${project.filename}" $@ &> /dev/null && ret_val=$? || ret_val=$?
fi
- pid=$(ps aux |grep "^$userid" |grep "$daemon" | grep -- "$lookfor" |awk '{print $2}')
- # Replace null delimiters with newline for grep
- envt=$(cat "/proc/$pid/environ" 2> /dev/null |tr '\0' '\n')
-
- # List of environmental variables to use; adjust as needed
- # UPSTART_SESSION must come before GNOME_DESKTOP_SESSION_ID
- exports=( "UPSTART_SESSION" "DISPLAY" "DBUS_SESSION_BUS_ADDRESS" "XDG_CURRENT_DESKTOP" "GNOME_DESKTOP_SESSION_ID" )
-
- for i in "${exports[@]}"; do
- # Re-set the variable within this session by name
- # Careful, this technique won't yet work with spaces
- if echo "$envt" | grep "^$i=" > /dev/null 2>&1; then
- eval "$(echo "$envt" | grep "^$i=")" > /dev/null 2>&1
- export $i > /dev/null 2>&1
- elif initctl --user get-env $i > /dev/null 2>&1; then
- eval "$i=$(initctl --user get-env $i)" > /dev/null 2>&1
- export $i > /dev/null 2>&1
- fi
- # echo -e " $i=${!i}"
- done
- # Handle Ubuntu Gnome
- if [ -z "$GNOME_DESKTOP_SESSION_ID" ]; then
- if [[ "$XDG_CURRENT_DESKTOP" == *"GNOME" ]]; then
- export GNOME_DESKTOP_SESSION_ID="this-is-deprecated"
- echo -e " GNOME_DESKTOP_SESSION_ID=$GNOME_DESKTOP_SESSION_ID (fixed)"
+ if [ $ret_val -eq 0 ]; then
+ echo -e " $SUCCESS Task $1 was successful"
+ else
+ if [ "$1" == "spawn" ]; then
+ echo -e " $WARNING Task $1 skipped. You'll have to start ${project.name} manually."
+ return
fi
+ echo -e " $FAILURE Task $1 failed.\n\nRe-run with DEBUG=true for more information."
+ false # throw error
fi
-
}
-# Check for Ubuntu to fix System Tray
-grep -q "Ubuntu" /etc/lsb-release > /dev/null 2>&1
-if [ $? -eq 0 ]; then
- gsettings set com.canonical.Unity.Panel systray-whitelist "['all']" > /dev/null 2>&1
- restart_it unity-panel-service 50
- restart_it unity-2d-panel 60
-fi
-
-progress_dialog 70 "Generating certificate..."
-chmod $mask "$destdir/auth/${linux.keygen.name}" > /dev/null 2>&1
-"$destdir/auth/${linux.keygen.name}" "$cname" "$trustedcert" "$trustedkey"
+# Make a temporary jar for preliminary installation steps
+run_task preinstall
-if [[ -n $trustedcert && -n $trustedkey ]]; then
- progress_dialog 75 "Skipping OS/browser cert import..."
-else
- progress_dialog 75 "Importing Firefox locator..."
- chmod $mask "$destdir/auth/firefox/${locator.name}" > /dev/null 2>&1
+run_task install --dest "/opt/${project.filename}"
- progress_dialog 80 "Importing Firefox certificate..."
- chmod $mask "$destdir/auth/firefox/${firefoxcert.name}" > /dev/null 2>&1
- "$destdir/auth/firefox/${firefoxcert.name}" "install" "$cname"
+# We should be installed now, generate the certificate
+pushd "/opt/${project.filename}" &> /dev/null
+run_task certgen
- progress_dialog 85 "Checking for certutil..."
- which certutil > /dev/null 2>&1
- if [ $? -ne 0 ]; then
- confirm_dialog "Certutil not found. Attempt to fetch now?" "$noprompt"
- apt-get install -y libnss3-tools > /dev/null 2>&1
- which certutil > /dev/null 2>&1
- if [ $? -ne 0 ]; then
- echo -e "\t- Success"
- else
- echo -e "\t- Failed"
- fi
- fi
-fi
-
-progress_dialog 90 "Setting permissions..."
-chmod -R $mask "${destdir}"
-
-progress_dialog 92 "Installing usb/udev rules..."
-rm -f "/lib/udev/rules.d/${linux.udev.name}" > /dev/null 2>&1
-mv "$destdir/${linux.udev.name}" "/lib/udev/rules.d/${linux.udev.name}" > /dev/null 2>&1
-udevadm control --reload-rules > /dev/null 2>&1
-
-progress_dialog 93 "Cleaning up old versions..."
-
-# Remove old startup entries
-for i in /home/* ; do
- if [ -n "${project.filename}" ]; then
- rm "$i/.config/autostart/${project.filename}.desktop" > /dev/null 2>&1
- fi
- if [ -n "${project.name}" ]; then
- rm "$i/.config/autostart/${project.name}.desktop" > /dev/null 2>&1
- fi
-done
-
-progress_dialog 94 "Adding startup entry..."
-echo "[Desktop Entry]
-Type=Application
-Name=${project.name}
-Exec="${launcher} -A"
-Path=${destdir}
-Icon=${destdir}/${linux.icon}
-MimeType=application/x-qz;x-scheme-handler/qz;
-Terminal=false
-Comment=${project.name}" > "/etc/xdg/autostart/${project.filename}.desktop"
+# Tell the desktop to look for new mimetypes in the background
+umask_bak="$(umask)"
+umask 0002 # more permissive umask for mimetype registration
+update-desktop-database &> /dev/null &
+umask "$umask_bak"
-# Allow oridinary users to write
-chmod 777 "/etc/xdg/autostart/${project.filename}.desktop"
+echo "Installation complete... Starting ${project.name}..."
+# spawn itself as a regular user, inheriting environment
+run_task spawn "/opt/${project.filename}/${project.filename}"
-cd "${destdir}"
-progress_dialog 95 "Installation complete... Starting ${project.name}..."
-which sudo > /dev/null 2>&1
-if [ $? -ne 0 ]; then
- progress_dialog 100 "Finished. Please launch ${project.name} manually."
-else
- # Start ${project.name} as the user that's logged in
- sudo_env
- sudo_env "ibus-daemon" "--panel"
- userid="$(logname 2>/dev/null || echo $SUDO_USER)"
- sudo -E -u $userid nohup "java" ${launch.opts} -jar "${jarfile}" > /dev/null 2>&1 &
- progress_dialog 100 "Finished. ${project.name} should start automatically."
-fi
-echo
-exit 0
+popd &> /dev/null
\ No newline at end of file
diff --git a/ant/linux/linux-keygen.sh.in b/ant/linux/linux-keygen.sh.in
deleted file mode 100644
index d94bcb12d..000000000
--- a/ant/linux/linux-keygen.sh.in
+++ /dev/null
@@ -1,175 +0,0 @@
-#!/bin/bash
-##########################################################################################
-# ${project.name} Linux KeyGen Utility #
-##########################################################################################
-# Description: #
-# 1. Creates a self-signed Java Keystore for jetty wss://localhost or [hostname] #
-# 2. Exports public certificate from Java Keystore #
-# #
-# Note: If [trustedcert] and [trustedkey] are specified, import to browser/OS is #
-# omitted. #
-# #
-# Depends: #
-# java #
-# #
-# Optional: #
-# openssl - Required if providing [trustedcert], [trustedkey] parameters #
-# #
-# Usage: #
-# $ ./${linux.keygen.name} [hostname] [trustedcert] [trustedkey] #
-# #
-##########################################################################################
-
-# Handle CN=${ssl.cn} override
-cnoverride="$1"
-
-# Handle trusted ssl certificate
-if [[ -n $2 && -n $3 ]]; then
- trustedcertpath="$2"
- trustedkeypath="$3"
-fi
-
-# Random password hash
-password=$(cat /dev/urandom | env LC_CTYPE=C tr -dc 'a-z0-9' | fold -w ${ssl.passlength} | head -n 1)
-
-# Check for IPv4 address
-function ip4 {
- if [[ $1 =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
- return 0
- fi
- return 1
-}
-
-# Replace all install-time variables
-function replace_vars {
- # TODO: OpenSUSE as well as some others don't have keytool in $PATH
- cmd=$(echo "$1" | sed -e "s|\"keytool\"|keytool|g")
-
- # Handle CN=${ssl.cn} override
- if [ -n "$cnoverride" ]; then
- cmd=$(echo "$cmd" | sed -e "s|CN=${ssl.cn},|CN=$cnoverride,|g")
- if ip4 "$cnoverride"; then
- cmd=$(echo "$cmd" | sed -e "s|san=dns:${ssl.cn},|san=ip:$cnoverride,|g")
- else
- cmd=$(echo "$cmd" | sed -e "s|san=dns:${ssl.cn},|san=dns:$cnoverride,|g")
- fi
- # Remove dangling san
- cmd=$(echo "$cmd" | sed -e "s|,dns:${ssl.cnalt}||g")
- fi
-
- cmd=$(echo "$cmd" | sed -e "s|\!install|${linux.installdir}|g")
- cmd=$(echo "$cmd" | sed -e "s|\!storepass|$password|g")
- cmd=$(echo "$cmd" | sed -e "s|\!keypass|$password|g")
- cmd=$(echo "$cmd" | sed -e "s|\!sslcert|$trustedcertpath|g")
- cmd=$(echo "$cmd" | sed -e "s|\!sslkey|$trustedkeypath|g")
-
- echo "$cmd"
- return 0
-}
-
-# Handle "community" mode, custom signing auth cert
-if [ -n "${build.type}" ]; then
- authcertpath=$(echo "${authcert.install}" | sed -e "s|\!install|${linux.installdir}|g")
-fi
-
-# Write out the secure websocket properties file
-function write_properties {
- propspath=$(echo "$1" | sed -e "s|\!install|${linux.installdir}|g")
- keystorepath=$(echo "${ssl.jks}" | sed -e "s|\!install|${linux.installdir}|g")
- echo "wss.alias=${ssl.alias}" > "$propspath"
- echo "wss.keystore=$keystorepath" >> "$propspath"
- echo "wss.keypass=$password" >> "$propspath"
- echo "wss.storepass=$password" >> "$propspath"
- echo "wss.host=${ssl.host}" >> "$propspath"
- if [ -n "$authcertpath" ]; then
- echo "authcert.override=$authcertpath" >> "$propspath"
- fi
- echo "" >> "$propspath"
- check_exists "$propspath"
- return $?
-}
-
-# Delete a file if exists
-function delete_file {
- testfile=$(echo "$1" | sed -e "s|\!install|${linux.installdir}|g")
- rm -f "$testfile" > /dev/null 2>&1
- return 0
-}
-
-# Check to see if file exists with optional message
-function check_exists {
- testfile=$(echo "$1" | sed -e "s|\!install|${linux.installdir}|g")
- if [ -e "$testfile" ]; then
- if [ -n "$2" ]; then
- echo -e "${bash.success} $2 $testfile"
- else
- echo -e "${bash.success} $testfile"
- fi
- return 0
- fi
- echo -e "${bash.failure} $testfile"
- return 1
-}
-
-# Runs a steps, optionally checks for a file
-# e.g: run_step "Description" "ls -al *.txt > ./out" "./out"
-function run_step {
- if eval "$(replace_vars "$2") > /dev/null 2>&1"; then
- if [ -z "$3" ]; then
- echo -e "${bash.success} $1"
- return 0
- elif check_exists "$3" "$1"; then
- return 0
- else
- return 1
- fi
- fi
- echo -e "${bash.failure}\n"
- return 1
-}
-
-# Delete old files if exist
-delete_file "${ssl.jks}"
-delete_file "${ssl.crt}"
-
-# Handle trusted ssl certificate, if specified
-if [ -n "$trustedcertpath" ]; then
- echo -e "\nCreating keystore for secure websockets..."
- run_step "\nConverting to PKCS12 keypair" "${trusted.command}" "${trusted.keypair}"
- run_step "\nConverting to jks format" "${trusted.convert}" "${ssl.jks}"
- write_properties "${ssl.properties}" || exit 1
- echo -e "\n[Finished ${linux.keygen.name}]\n"
- exit 0
-fi
-
-# Check for keytool command
-"${jks.keytool} -help" > /dev/null 2>&1
-if [ $? -ne 0 ]; then
- export PATH=$PATH:/usr/java/latest/bin/
-fi
-
-# Handle self-signed certificate
-echo -e "\nCreating keystore for secure websockets..."
-# Delete old files if exist
-delete_file "${ca.jks}"
-delete_file "${ca.crt}"
-delete_file "${ssl.csr}"
-
-run_step "Creating a CA keypair" "${ca.jkscmd}" "${ca.jks}" || exit 1
-run_step "Exporting CA certificate" "${ca.crtcmd}" "${ca.crt}" || exit 1
-run_step "Creating an SSL keypair" "${ssl.jkscmd}" "${ssl.jks}" || exit 1
-run_step "Creating an SSL CSR" "${ssl.jkscsr}" "${ssl.csr}" || exit 1
-run_step "Issuing SSL certificate from CA" "${ssl.crtcmd}" "${ssl.crt}" || exit 1
-run_step "Importing CA certificate into SSL keypair" "${ssl.importca}" "" || exit 1
-run_step "Importing chained SSL certificate into SSL keypair" "${ssl.importssl}" "" || exit 1
-
-echo -e "\nWriting properties file..."
-write_properties "${ssl.properties}" || exit 1
-
-echo -e "\nCleaning up..."
-delete_file "${ca.jks}"
-delete_file "${ssl.csr}"
-delete_file "${ssl.crt}"
-
-echo -e "\n[Finished ${linux.keygen.name}]\n"
-exit 0
diff --git a/ant/linux/linux-launcher.sh.in b/ant/linux/linux-launcher.sh.in
deleted file mode 100644
index a88ac5007..000000000
--- a/ant/linux/linux-launcher.sh.in
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-##########################################################################################
-# ${project.name} Linux Launcher #
-##########################################################################################
-# Description: #
-# 1. Determines if program should autostart (if applicable) #
-# 2. Launch application # #
-# #
-# Usage: #
-# $ ./launcher #
-# #
-# Options: #
-# -A Check autostart file to determine if ${project.name} should launch #
-# #
-##########################################################################################
-
-destdir="${linux.installdir}"
-jarfile="${destdir}/${project.filename}.jar"
-localautostart="${linux.datadir.local}/${autostart.name}"
-globalautostart="${linux.datadir.shared}/${autostart.name}"
-
-# If launched at startup, check override first
-if [ "$1" = "-A" ]; then
- echo "Autostart switch '-A' was provided"
- if [ -f "$localautostart" ] && [ "$(head -n 1 "$localautostart")" = "0" ]; then
- echo "Skipping autostart per '$localautostart'"
- exit 0
- elif [ -f "$globalautostart" ] && [ "$(head -n 1 "$globalautostart")" = "0" ]; then
- echo "Skipping autostart per '$globalautostart'"
- exit 0
- else
- echo "No autostart conflicts found in '$localautostart' or '$globalautostart'. Starting."
- fi
-fi
-
-eval java ${launch.opts} -jar \"${jarfile}\"
-exit 0
\ No newline at end of file
diff --git a/ant/linux/linux-packager.sh.in b/ant/linux/linux-packager.sh.in
deleted file mode 100644
index 4cb49578d..000000000
--- a/ant/linux/linux-packager.sh.in
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-##########################################################################################
-# ${project.name} Linux Packager #
-##########################################################################################
-# Description: #
-# 1. Packages software for Linux self extracting archive and installer #
-# #
-# Depends: #
-# makeself (sudo apt-get install makeself) #
-# #
-# Usage: #
-# $ chmod +x ${linux.packager.name} #
-# $ ./${linux.packager.name} #
-# #
-##########################################################################################
-echo
-echo "============================================"
-echo " Packaging ${project.name}"
-echo "============================================"
-echo
-chmod +x "${linux.installer.out}"
-makeself "${dist.dir}" "${out.dir}/${project.filename}${build.type}-${build.version}.run" "${project.name} Installer" "./${linux.installer.name}"
-code=$?
-echo
-echo "============================================"
-echo " Finished "
-echo "============================================"
-echo
-exit $code
diff --git a/ant/linux/linux-uninstall.sh.in b/ant/linux/linux-uninstall.sh.in
index e268c58b6..369181fc1 100644
--- a/ant/linux/linux-uninstall.sh.in
+++ b/ant/linux/linux-uninstall.sh.in
@@ -1,58 +1,8 @@
#!/bin/bash
-echo -e "Stopping any running versions..."
-pkill -f "java -jar ${linux.installdir}/${project.filename}.jar" > /dev/null 2>&1
-a=$?
-pkill -f "java ${launch.opts} -jar ${linux.installdir}/${project.filename}.jar" > /dev/null 2>&1
-if [ $a -eq 0 -o $? -eq 0 ]; then
- echo -e "${bash.success}"
-else
- echo -e "${bash.skipped}"
-fi
-
-# Remove Firefox certificate
-"${linux.installdir}/auth/firefox/${firefoxcert.name}" "uninstall"
-
-echo -e "Cleaning up shortcuts and certificates..."
-# Remove startup entries and certificates for all users
-a=0
-b=0
-for i in /home/* ; do
- # Remove 1.9/2.0 style startup shortcuts
- if [ -n "${project.filename}" ]; then
- rm "$i/.config/autostart/${project.filename}.desktop" > /dev/null 2>&1
- let "a++"
- fi
- if [ -n "${project.name}" ]; then
- rm "$i/.config/autostart/${project.name}.desktop" > /dev/null 2>&1
- let "a++"
- fi
-
- certutil -D -d "sql:$i/.pki/nssdb" -n "${vendor.company}" > /dev/null 2>&1
- if [ $? -eq 0 ]; then
- let "b++"
- fi
-done
-
-if [ $a -ne 0 ]; then
- echo -e "${bash.success} Removed $a startup entries"
-else
- echo -e "${bash.skipped} No startup entries found"
-fi
-
-if [ $b -ne 0 ]; then
- echo -e "${bash.success} Removed $b certificates"
-else
- echo -e "${bash.skipped} No certificates found"
-fi
+# Halt on first error
+set -e
-echo -e "Removing application shortuct..."
-rm -rf "/usr/share/applications/${project.filename}.desktop" > /dev/null 2>&1
-if [ $? -eq 0 ]; then
- echo -e "${bash.success}"
-else
- echo -e "${bash.skipped}"
-fi
echo -e "Cleanup is complete. Removing ${linux.installdir}..."
rm -rf "${linux.installdir}"
diff --git a/ant/linux/linux.properties b/ant/linux/linux.properties
deleted file mode 100644
index 060697c70..000000000
--- a/ant/linux/linux.properties
+++ /dev/null
@@ -1,41 +0,0 @@
-# Linux build properties
-linux.icon=linux-icon.svg
-
-linux.keygen.name=linux-keygen.sh
-linux.keygen.in=${basedir}/ant/linux/${linux.keygen.name}.in
-linux.keygen.out=${dist.dir}/auth/${linux.keygen.name}
-
-linux.udev.name=99-udev-override.rules
-linux.udev.in=${basedir}/ant/linux/linux-udev.rules.in
-linux.udev.out=${dist.dir}/${linux.udev.name}
-
-linux.installer.name=linux-installer.sh
-linux.installer.in=${basedir}/ant/linux/${linux.installer.name}.in
-linux.installer.out=${dist.dir}/${linux.installer.name}
-
-linux.launcher.name=linux-launcher.sh
-linux.launcher.in=${basedir}/ant/linux/${linux.launcher.name}.in
-linux.launcher.out=${dist.dir}/${linux.launcher.name}
-
-linux.uninstall.in=${basedir}/ant/linux/linux-uninstall.sh.in
-linux.uninstall.out=${dist.dir}/uninstall
-
-linux.installdir=/opt/${project.filename}
-linux.datadir.local=$HOME/.${project.datadir}
-linux.datadir.shared=/srv/${project.datadir}
-
-linux.packager.name=linux-packager.sh
-linux.packager.in=${basedir}/ant/linux/${linux.packager.name}.in
-linux.packager.out=${build.dir}/${linux.packager.name}
-
-# Console colors
-bash.red=\\e[1;31m
-bash.green=\\e[1;32m
-bash.yellow=\\e[1;33m
-bash.plain=\\033[0m
-bash.colors=red=${bash.red};green=${bash.green};yellow=${bash.yellow};plain=${bash.plain};
-
-bash.success=\ \ \ [${bash.green}success${bash.plain}]
-bash.failure=\ \ \ [${bash.red}failure${bash.plain}]
-bash.skipped=\ \ \ [${bash.yellow}skipped${bash.plain}]
-bash.aborted=\ \ \ [${bash.red}aborted${bash.plain}]
diff --git a/ant/project.properties b/ant/project.properties
index 6cdb34f8c..4ded930c0 100644
--- a/ant/project.properties
+++ b/ant/project.properties
@@ -9,38 +9,19 @@ project.datadir=qz
launch.opts=-Xms512m
-js.dir=js
-css.dir=css
-fonts.dir=fonts
-lib.dir=lib
-demo.dir=demo
-asset.dir=assets
src.dir=${basedir}/src
out.dir=${basedir}/out
build.dir=${out.dir}/build
-build.project.dir=${build.dir}/${project.filename}
-branding.dir=${asset.dir}/branding
-sign.lib.dir=${out.dir}/jar-signed
-
dist.dir=${out.dir}/dist
-dist.jar=${dist.dir}/${project.filename}.jar
-authcert.name=override.crt
-authcert.build=${dist.dir}/${authcert.name}
-authcert.install=!install/${authcert.name}
-autostart.name=.autostart
+sign.lib.dir=${out.dir}/jar-signed
jar.compress=true
jar.index=true
+# See also qz.common.Constants.java
javac.source=1.8
javac.target=1.8
javafx.version=11.0.2
javafx.mirror=https://gluonhq.com/download
java.download=https://adoptopenjdk.net/?variant=openjdk11
-
-manifest.application.name=${project.name}
-manifest.main.class=qz.ws.PrintSocketServer
-# Optional override of default Permissions manifest attribute (supported values: sandbox, all-permissions)
-manifest.permissions=all-permissions
-
diff --git a/ant/ssl.properties b/ant/ssl.properties
deleted file mode 100644
index 90d44bbcc..000000000
--- a/ant/ssl.properties
+++ /dev/null
@@ -1,59 +0,0 @@
-# Platform-independent info used at install time for wss:// signing
-# Values prefixed with an !exclamation-mark can't be determined until install time
-ssl.cn=localhost
-ssl.cnalt=localhost.qz.io
-ssl.city=Canastota
-ssl.state=NY
-ssl.country=US
-ssl.company=QZ Industries\\\\, LLC
-ssl.validity=7305
-
-ssl.dname=\\"CN=${ssl.cn}, EMAILADDRESS=${vendor.email}, OU=${ssl.company}, O=${ssl.company}, L=${ssl.city}, S=${ssl.state}, C=${ssl.country}\\"
-ssl.properties=!install/${project.filename}.properties
-ssl.host=0.0.0.0
-ssl.passlength=10
-
-ca.jks=!install/auth/root-ca.jks
-ca.crt=!install/auth/root-ca.crt
-ca.alias=root-ca
-ca.jkscmd=\\"keytool\\" -genkeypair -noprompt -alias ${ca.alias} -keyalg RSA -keysize 2048 -dname ${ssl.dname} -validity ${ssl.validity} -keystore \\"${ca.jks}\\" -keypass !keypass -storepass !storepass -ext ku:critical=cRLSign,keyCertSign -ext bc:critical=ca:true,pathlen:1
-ca.crtcmd=\\"keytool\\" -exportcert -alias root-ca -keystore \\"${ca.jks}\\" -keypass !keypass -storepass !storepass -file \\"${ca.crt}\\" -rfc -ext ku:critical=cRLSign,keyCertSign -ext bc:critical=ca:true,pathlen:1
-
-ssl.jks=!install/auth/${project.filename}.jks
-ssl.crt=!install/auth/${project.filename}.crt
-ssl.csr=!install/auth/${project.filename}.csr
-ssl.alias=${project.filename}
-ssl.jkscmd=\\"keytool\\" -genkeypair -noprompt -alias ${ssl.alias} -keyalg RSA -keysize 2048 -dname ${ssl.dname} -validity ${ssl.validity} -keystore \\"${ssl.jks}\\" -storepass !storepass -keypass !keypass -ext ku:critical=digitalSignature,keyEncipherment -ext eku=serverAuth,clientAuth -ext san=dns:${ssl.cn},dns:${ssl.cnalt} -ext bc:critical=ca:false
-ssl.jkscsr=\\"keytool\\" -certreq -keyalg RSA -alias ${ssl.alias} -file \\"${ssl.csr}\\" -keystore \\"${ssl.jks}\\" -keypass !keypass -storepass !storepass
-ssl.crtcmd=\\"keytool\\" -keypass !keypass -storepass !storepass -validity ${ssl.validity} -keystore \\"${ca.jks}\\" -gencert -alias ${ca.alias} -infile \\"${ssl.csr}\\" -ext ku:critical=digitalSignature,keyEncipherment -ext eku=serverAuth,clientAuth -ext san=dns:${ssl.cn},dns:${ssl.cnalt} -ext bc:critical=ca:false -rfc -outfile \\"${ssl.crt}\\"
-ssl.importca=\\"keytool\\" -noprompt -import -trustcacerts -alias ${ca.alias} -file \\"${ca.crt}\\" -keystore \\"${ssl.jks}\\" -keypass !keypass -storepass !storepass
-ssl.importssl=\\"keytool\\" -noprompt -import -trustcacerts -alias ${ssl.alias} -file \\"${ssl.crt}\\" -keystore \\"${ssl.jks}\\" -keypass !keypass -storepass !storepass
-
-firefoxconfig.name=firefox-config.cfg
-firefoxconfig.in=${basedir}/ant/firefox/${firefoxconfig.name}.in
-firefoxconfig.out=${dist.dir}/auth/firefox/${firefoxconfig.name}
-firefoxconfig.install=!install/auth/firefox/${firefoxconfig.name}
-
-firefoxprefs.name=firefox-prefs.js
-firefoxprefs.in=${basedir}/ant/firefox/${firefoxprefs.name}.in
-firefoxprefs.out=${dist.dir}/auth/firefox/${firefoxprefs.name}
-firefoxprefs.install=!install/auth/firefox/${firefoxprefs.name}
-
-firefoxcert.name=firefox-cert.sh
-firefoxcert.in=${basedir}/ant/firefox/${firefoxcert.name}.in
-firefoxcert.out=${dist.dir}/auth/firefox/${firefoxcert.name}
-
-locator.name=locator.sh
-locator.in=${basedir}/ant/firefox/${locator.name}.in
-locator.out=${dist.dir}/auth/firefox/${locator.name}
-
-jsonwriter.name=firefox-json-writer.py
-jsonwriter.in=${basedir}/ant/firefox/${jsonwriter.name}
-jsonwriter.out=${dist.dir}/auth/firefox/${jsonwriter.name}
-
-# Trusted SSL installs only
-trusted.crt=!sslcert
-trusted.jks=!sslkey
-trusted.keypair=!install/auth/${project.filename}.p12
-trusted.command=openssl pkcs12 -export -in \\"${trusted.crt}\\" -inkey \\"${trusted.jks}\\" -out \\"${trusted.keypair}\\" -name ${ssl.alias} -passout pass:!keypass
-trusted.convert=\\"keytool\\" -importkeystore -deststorepass !storepass -destkeypass !keypass -destkeystore \\"${ssl.jks}\\" -srckeystore \\"${trusted.keypair}\\" -srcstoretype PKCS12 -srcstorepass !storepass -alias ${ssl.alias}
diff --git a/ant/unix/unix-launcher.sh.in b/ant/unix/unix-launcher.sh.in
new file mode 100644
index 000000000..88bb47a6e
--- /dev/null
+++ b/ant/unix/unix-launcher.sh.in
@@ -0,0 +1,95 @@
+#!/usr/bin/env bash
+# Shared launcher for MacOS and Linux
+# Parameters -- if any -- are passed on to the app
+
+# Halt on first error
+set -e
+
+# Configured by ant at build time
+JAVA_MIN="${javac.target}"
+LAUNCH_OPTS="${launch.opts}"
+ABOUT_TITLE="${project.name}"
+PROPS_FILE="${project.filename}"
+
+# Get working directory
+DIR=$(cd "$(dirname "$0")" && pwd)
+pushd "$DIR" &> /dev/null
+
+# Console colors
+RED="\\x1B[1;31m";GREEN="\\x1B[1;32m";YELLOW="\\x1B[1;33m";PLAIN="\\x1B[0m"
+
+# Statuses
+SUCCESS=" [${GREEN}success${PLAIN}]"
+FAILURE=" [${RED}failure${PLAIN}]"
+WARNING=" [${YELLOW}warning${PLAIN}]"
+
+echo "Looking for Java..."
+
+# Honor JAVA_HOME
+if [ -n "$JAVA_HOME" ]; then
+ echo -e "$WARNING JAVA_HOME was detected, using $JAVA_HOME..."
+ PATH="$JAVA_HOME/bin:$PATH"
+fi
+
+# Check for bundled JRE
+if [ -d ./jre ]; then
+ echo -e "$SUCCESS A bundled runtime was found. Using..."
+ PATH="$(pwd)/jre/bin:$PATH"
+ export PATH
+fi
+
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ ICON_PATH="$DIR/Contents/Resources/apple-icon.icns"
+ MAC_PRIMARY="/usr/libexec/java_home"
+ MAC_FALLACK="/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin"
+ echo "Trying $MAC_PRIMARY..."
+ if "$MAC_PRIMARY" -v $JAVA_MIN+ &>/dev/null; then
+ echo -e "$SUCCESS Using \"$MAC_PRIMARY -v $JAVA_MIN+ --exec\" to launch $ABOUT_TITLE"
+ java() {
+ "$MAC_PRIMARY" -v $JAVA_MIN+ --exec java "$@"
+ }
+ elif [ -d "/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin" ]; then
+ echo -e "$WARNING No luck using $MAC_PRIMARY"
+ echo "Trying $MAC_FALLACK..."
+ java() {
+ "$MAC_FALLACK/java" "$@"
+ }
+ fi
+else
+ export PATH="$PATH:/usr/java/latest/bin/"
+fi
+
+# Make sure Java version is sufficient
+echo "Verifying the Java version is $JAVA_MIN+..."
+curver=$(java -version 2>&1 | grep -i version | awk -F"\"" '{ print $2 }' | awk -F"." '{ print $1 "." $2 }')
+minver="$JAVA_MIN"
+if [ -z "$curver" ]; then
+ curver="0.0"
+fi
+desired=$(echo -e "$minver\n$curver")
+actual=$(echo "$desired" |sort -t '.' -k 1,1 -k 2,2 -n)
+if [ "$desired" != "$actual" ]; then
+ echo -e "$FAILURE Please install Java $JAVA_MIN or higher to continue"
+ exit 1
+else
+ echo -e "$SUCCESS Java $curver was detected"
+fi
+
+
+if command -v java &>/dev/null; then
+ echo -e "$ABOUT_TITLE is starting..."
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ if [ -f "$PROPS_FILE.jar" ]; then
+ prefix="" # aside launcher, e.g. preinstall
+ else
+ prefix="../../" # back two directories, e.g. postinstall
+ fi
+ java $LAUNCH_OPTS -Xdock:name="$ABOUT_TITLE" -Xdock:icon="$ICON_PATH" -jar -Dapple.awt.UIElement="true" "${prefix}$PROPS_FILE.jar" -NSRequiresAquaSystemAppearance False "$@"
+ else
+ java $LAUNCH_OPTS -jar "$PROPS_FILE.jar" "$@"
+ fi
+else
+ echo -e "$FAILURE Java $JAVA_MIN+ was not found"
+fi
+
+popd &>/dev/null
\ No newline at end of file
diff --git a/ant/unix/unix-uninstall.sh.in b/ant/unix/unix-uninstall.sh.in
new file mode 100644
index 000000000..ae7d16cc6
--- /dev/null
+++ b/ant/unix/unix-uninstall.sh.in
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# Halt on first error
+set -e
+
+if [ "$(id -u)" != "0" ]; then
+ echo "This script must be run with root (sudo) privileges" 1>&2
+ exit 1
+fi
+
+# Get working directory
+DIR=$(cd "$(dirname "$0")" && pwd)
+pushd "$DIR"
+
+echo "Running uninstall tasks..."
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ "$DIR/Contents/MacOS/${project.name}" uninstall
+else
+ "$DIR/${project.filename}" uninstall
+fi
+
+echo "Deleting files..."
+rm -rf "$DIR"
+echo -e "\nUninstall of ${project.name} complete.\n"
+
+popd &>/dev/null
\ No newline at end of file
diff --git a/ant/windows/nsis/Include/FindJava.nsh b/ant/windows/nsis/Include/FindJava.nsh
new file mode 100644
index 000000000..92ebff090
--- /dev/null
+++ b/ant/windows/nsis/Include/FindJava.nsh
@@ -0,0 +1,88 @@
+!include FileFunc.nsh
+!include LogicLib.nsh
+!include x64.nsh
+
+!include StrRep.nsh
+!include IndexOf.nsh
+
+; Resulting variable
+Var /GLOBAL java
+Var /GLOBAL javaw
+
+; Constants
+!define EXE "java.exe"
+
+!define ADOPT "SOFTWARE\Classes\AdoptOpenJDK.jarfile\shell\open\command"
+
+!define JRE "Software\JavaSoft\Java Runtime Environment"
+!define JRE32 "Software\Wow6432Node\JavaSoft\Java Runtime Environment"
+!define JDK "Software\JavaSoft\JDK"
+!define JDK32 "Software\Wow6432Node\JavaSoft\JDK"
+
+; Macros
+!macro _ReadAdoptKey
+ ClearErrors
+ ReadRegStr $0 HKLM "${ADOPT}" ""
+ StrCpy $0 "$0" "" 1 ; Remove first double-quote
+ ${IndexOf} $1 $0 "$\"" ; Find the index of second double-quote
+ StrCpy $0 "$0" $1 ; Get the string section up to the index
+ IfFileExists "$0" Found
+!macroend
+
+!macro _ReadReg key
+ ClearErrors
+ ReadRegStr $0 HKLM "${key}" "CurrentVersion"
+ ReadRegStr $0 HKLM "${key}\$0" "JavaHome"
+ IfErrors +2 0
+ StrCpy $0 "$0\bin\${EXE}"
+ IfFileExists "$0" Found
+!macroend
+
+!macro _ReadWorking path
+ ClearErrors
+ StrCpy $0 "$EXEDIR\${path}\bin\${EXE}"
+ IfFileExists $0 Found
+!macroend
+
+!macro _ReadEnv var
+ ClearErrors
+ ReadEnvStr $0 "${var}"
+ StrCpy $0 "$0\bin\${EXE}"
+ IfFileExists "$0" Found
+!macroend
+
+; Create the shared function.
+!macro _FindJava un
+ Function ${un}FindJava
+ ${If} ${RunningX64}
+ SetRegView 64
+ ${EndIf}
+
+ ; Check relative directories
+ !insertmacro _ReadWorking "jre"
+ !insertmacro _ReadWorking "jdk"
+
+ ; Check common env vars
+ !insertmacro _ReadEnv "JAVA_HOME"
+
+ ; Check registry
+ !insertmacro _ReadAdoptKey
+ !insertmacro _ReadReg "${JRE}"
+ !insertmacro _ReadReg "${JRE32}"
+ !insertmacro _ReadReg "${JDK}"
+ !insertmacro _ReadReg "${JDK32}"
+
+ ; Give up. Use java.exe and hope it works
+ StrCpy $0 "${EXE}"
+
+ ; Set global var
+ Found:
+ StrCpy $java $0
+ ${StrRep} '$java' '$java' 'javaw.exe' '${EXE}' ; AdoptOpenJDK returns "javaw.exe"
+ ${StrRep} '$javaw' '$java' '${EXE}' 'javaw.exe'
+ FunctionEnd
+!macroend
+
+; Allows registering identical functions for install and uninstall
+!insertmacro _FindJava ""
+!insertmacro _FindJava "un."
\ No newline at end of file
diff --git a/ant/windows/nsis/Include/IndexOf.nsh b/ant/windows/nsis/Include/IndexOf.nsh
new file mode 100644
index 000000000..88f4d63c9
--- /dev/null
+++ b/ant/windows/nsis/Include/IndexOf.nsh
@@ -0,0 +1,28 @@
+!define IndexOf "!insertmacro IndexOf"
+
+!macro IndexOf Var Str Char
+ Push "${Char}"
+ Push "${Str}"
+
+ Exch $R0
+ Exch
+ Exch $R1
+ Push $R2
+ Push $R3
+
+ StrCpy $R3 $R0
+ StrCpy $R0 -1
+ IntOp $R0 $R0 + 1
+ StrCpy $R2 $R3 1 $R0
+ StrCmp $R2 "" +2
+ StrCmp $R2 $R1 +2 -3
+
+ StrCpy $R0 -1
+
+ Pop $R3
+ Pop $R2
+ Pop $R1
+ Exch $R0
+
+ Pop "${Var}"
+!macroend
\ No newline at end of file
diff --git a/ant/windows/nsis/Include/StdUtils.nsh b/ant/windows/nsis/Include/StdUtils.nsh
index 2a9d7bb14..537655da5 100644
--- a/ant/windows/nsis/Include/StdUtils.nsh
+++ b/ant/windows/nsis/Include/StdUtils.nsh
@@ -1,6 +1,6 @@
#################################################################################
# StdUtils plug-in for NSIS
-# Copyright (C) 2004-2014 LoRd_MuldeR
+# Copyright (C) 2004-2018 LoRd_MuldeR
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -19,111 +19,150 @@
# http://www.gnu.org/licenses/lgpl-2.1.txt
#################################################################################
+# DEVELOPER NOTES:
+# - Please see "https://github.com/lordmulder/stdutils/" for news and updates!
+# - Please see "Docs\StdUtils\StdUtils.html" for detailed function descriptions!
+# - Please see "Examples\StdUtils\StdUtilsTest.nsi" for usage examples!
#################################################################################
# FUNCTION DECLARTIONS
#################################################################################
-!define StdUtils.Time '!insertmacro _StdUtils_Time' #time(), as in C standard library
-!define StdUtils.GetMinutes '!insertmacro _StdUtils_GetMinutes' #GetSystemTimeAsFileTime(), returns the number of minutes
-!define StdUtils.GetHours '!insertmacro _StdUtils_GetHours' #GetSystemTimeAsFileTime(), returns the number of hours
-!define StdUtils.GetDays '!insertmacro _StdUtils_GetDays' #GetSystemTimeAsFileTime(), returns the number of days
-!define StdUtils.Rand '!insertmacro _StdUtils_Rand' #rand(), as in C standard library
-!define StdUtils.RandMax '!insertmacro _StdUtils_RandMax' #rand(), as in C standard library, with maximum value
-!define StdUtils.RandMinMax '!insertmacro _StdUtils_RandMinMax' #rand(), as in C standard library, with minimum/maximum value
-!define StdUtils.RandList '!insertmacro _StdUtils_RandList' #rand(), as in C standard library, with list support
-!define StdUtils.FormatStr '!insertmacro _StdUtils_FormatStr' #sprintf(), as in C standard library, one '%d' placeholder
-!define StdUtils.FormatStr2 '!insertmacro _StdUtils_FormatStr2' #sprintf(), as in C standard library, two '%d' placeholders
-!define StdUtils.FormatStr3 '!insertmacro _StdUtils_FormatStr3' #sprintf(), as in C standard library, three '%d' placeholders
-!define StdUtils.ScanStr '!insertmacro _StdUtils_ScanStr' #sscanf(), as in C standard library, one '%d' placeholder
-!define StdUtils.ScanStr2 '!insertmacro _StdUtils_ScanStr2' #sscanf(), as in C standard library, two '%d' placeholders
-!define StdUtils.ScanStr3 '!insertmacro _StdUtils_ScanStr3' #sscanf(), as in C standard library, three '%d' placeholders
-!define StdUtils.TrimStr '!insertmacro _StdUtils_TrimStr' #Remove whitspaces from string, left and right
-!define StdUtils.TrimStrLeft '!insertmacro _StdUtils_TrimStrLeft' #Remove whitspaces from string, left side only
-!define StdUtils.TrimStrRight '!insertmacro _StdUtils_TrimStrRight' #Remove whitspaces from string, right side only
-!define StdUtils.RevStr '!insertmacro _StdUtils_RevStr' #Reverse a string, e.g. "reverse me" <-> "em esrever"
-!define StdUtils.SHFileMove '!insertmacro _StdUtils_SHFileMove' #SHFileOperation(), using the FO_MOVE operation
-!define StdUtils.SHFileCopy '!insertmacro _StdUtils_SHFileCopy' #SHFileOperation(), using the FO_COPY operation
-!define StdUtils.ExecShellAsUser '!insertmacro _StdUtils_ExecShlUser' #ShellExecute() as NON-elevated user from elevated installer
-!define StdUtils.InvokeShellVerb '!insertmacro _StdUtils_InvkeShlVrb' #Invokes a "shell verb", e.g. for pinning items to the taskbar
-!define StdUtils.ExecShellWaitEx '!insertmacro _StdUtils_ExecShlWaitEx' #ShellExecuteEx(), returns the handle of the new process
-!define StdUtils.WaitForProcEx '!insertmacro _StdUtils_WaitForProcEx' #WaitForSingleObject(), e.g. to wait for a running process
-!define StdUtils.GetParameter '!insertmacro _StdUtils_GetParameter' #Get the value of a specific command-line option
-!define StdUtils.GetAllParameters '!insertmacro _StdUtils_GetAllParams' #Get complete command-line, but without executable name
-!define StdUtils.GetRealOSVersion '!insertmacro _StdUtils_GetRealOSVer' #Get the *real* Windows version number, even on Windows 8.1+
-!define StdUtils.GetRealOSBuildNo '!insertmacro _StdUtils_GetRealOSBld' #Get the *real* Windows build number, even on Windows 8.1+
-!define StdUtils.GetRealOSName '!insertmacro _StdUtils_GetRealOSStr' #Get the *real* Windows version, as a "friendly" name
-!define StdUtils.VerifyOSVersion '!insertmacro _StdUtils_VrfyRealOSVer' #Compare *real* operating system to an expected version number
-!define StdUtils.VerifyOSBuildNo '!insertmacro _StdUtils_VrfyRealOSBld' #Compare *real* operating system to an expected build number
-!define StdUtils.GetLibVersion '!insertmacro _StdUtils_GetLibVersion' #Get the current StdUtils library version (for debugging)
-!define StdUtils.SetVerbose '!insertmacro _StdUtils_SetVerbose' #Enable or disable "verbose" mode (for debugging)
+!ifndef ___STDUTILS__NSH___
+!define ___STDUTILS__NSH___
+
+!define StdUtils.Time '!insertmacro _StdU_Time' #time(), as in C standard library
+!define StdUtils.GetMinutes '!insertmacro _StdU_GetMinutes' #GetSystemTimeAsFileTime(), returns the number of minutes
+!define StdUtils.GetHours '!insertmacro _StdU_GetHours' #GetSystemTimeAsFileTime(), returns the number of hours
+!define StdUtils.GetDays '!insertmacro _StdU_GetDays' #GetSystemTimeAsFileTime(), returns the number of days
+!define StdUtils.Rand '!insertmacro _StdU_Rand' #rand(), as in C standard library
+!define StdUtils.RandMax '!insertmacro _StdU_RandMax' #rand(), as in C standard library, with maximum value
+!define StdUtils.RandMinMax '!insertmacro _StdU_RandMinMax' #rand(), as in C standard library, with minimum/maximum value
+!define StdUtils.RandList '!insertmacro _StdU_RandList' #rand(), as in C standard library, with list support
+!define StdUtils.RandBytes '!insertmacro _StdU_RandBytes' #Generates random bytes, returned as Base64-encoded string
+!define StdUtils.FormatStr '!insertmacro _StdU_FormatStr' #sprintf(), as in C standard library, one '%d' placeholder
+!define StdUtils.FormatStr2 '!insertmacro _StdU_FormatStr2' #sprintf(), as in C standard library, two '%d' placeholders
+!define StdUtils.FormatStr3 '!insertmacro _StdU_FormatStr3' #sprintf(), as in C standard library, three '%d' placeholders
+!define StdUtils.ScanStr '!insertmacro _StdU_ScanStr' #sscanf(), as in C standard library, one '%d' placeholder
+!define StdUtils.ScanStr2 '!insertmacro _StdU_ScanStr2' #sscanf(), as in C standard library, two '%d' placeholders
+!define StdUtils.ScanStr3 '!insertmacro _StdU_ScanStr3' #sscanf(), as in C standard library, three '%d' placeholders
+!define StdUtils.TrimStr '!insertmacro _StdU_TrimStr' #Remove whitspaces from string, left and right
+!define StdUtils.TrimStrLeft '!insertmacro _StdU_TrimStrLeft' #Remove whitspaces from string, left side only
+!define StdUtils.TrimStrRight '!insertmacro _StdU_TrimStrRight' #Remove whitspaces from string, right side only
+!define StdUtils.RevStr '!insertmacro _StdU_RevStr' #Reverse a string, e.g. "reverse me" <-> "em esrever"
+!define StdUtils.ValidFileName '!insertmacro _StdU_ValidFileName' #Test whether string is a valid file name - no paths allowed
+!define StdUtils.ValidPathSpec '!insertmacro _StdU_ValidPathSpec' #Test whether string is a valid full(!) path specification
+!define StdUtils.ValidDomainName '!insertmacro _StdU_ValidDomain' #Test whether string is a valid host name or domain name
+!define StdUtils.StrToUtf8 '!insertmacro _StdU_StrToUtf8' #Convert string from Unicode (UTF-16) or ANSI to UTF-8 bytes
+!define StdUtils.StrFromUtf8 '!insertmacro _StdU_StrFromUtf8' #Convert string from UTF-8 bytes to Unicode (UTF-16) or ANSI
+!define StdUtils.SHFileMove '!insertmacro _StdU_SHFileMove' #SHFileOperation(), using the FO_MOVE operation
+!define StdUtils.SHFileCopy '!insertmacro _StdU_SHFileCopy' #SHFileOperation(), using the FO_COPY operation
+!define StdUtils.AppendToFile '!insertmacro _StdU_AppendToFile' #Append contents of an existing file to another file
+!define StdUtils.ExecShellAsUser '!insertmacro _StdU_ExecShlUser' #ShellExecute() as NON-elevated user from elevated installer
+!define StdUtils.InvokeShellVerb '!insertmacro _StdU_InvkeShlVrb' #Invokes a "shell verb", e.g. for pinning items to the taskbar
+!define StdUtils.ExecShellWaitEx '!insertmacro _StdU_ExecShlWaitEx' #ShellExecuteEx(), returns the handle of the new process
+!define StdUtils.WaitForProcEx '!insertmacro _StdU_WaitForProcEx' #WaitForSingleObject(), e.g. to wait for a running process
+!define StdUtils.GetParameter '!insertmacro _StdU_GetParameter' #Get the value of a specific command-line option
+!define StdUtils.TestParameter '!insertmacro _StdU_TestParameter' #Test whether a specific command-line option has been set
+!define StdUtils.ParameterCnt '!insertmacro _StdU_ParameterCnt' #Get number of command-line tokens, similar to argc in main()
+!define StdUtils.ParameterStr '!insertmacro _StdU_ParameterStr' #Get the n-th command-line token, similar to argv[i] in main()
+!define StdUtils.GetAllParameters '!insertmacro _StdU_GetAllParams' #Get complete command-line, but without executable name
+!define StdUtils.GetRealOSVersion '!insertmacro _StdU_GetRealOSVer' #Get the *real* Windows version number, even on Windows 8.1+
+!define StdUtils.GetRealOSBuildNo '!insertmacro _StdU_GetRealOSBld' #Get the *real* Windows build number, even on Windows 8.1+
+!define StdUtils.GetRealOSName '!insertmacro _StdU_GetRealOSStr' #Get the *real* Windows version, as a "friendly" name
+!define StdUtils.GetOSEdition '!insertmacro _StdU_GetOSEdition' #Get the Windows edition, i.e. "workstation" or "server"
+!define StdUtils.GetOSReleaseId '!insertmacro _StdU_GetOSRelIdNo' #Get the Windows release identifier (on Windows 10)
+!define StdUtils.GetOSReleaseName '!insertmacro _StdU_GetOSRelIdStr' #Get the Windows release (on Windows 10), as a "friendly" name
+!define StdUtils.VerifyOSVersion '!insertmacro _StdU_VrfyRealOSVer' #Compare *real* operating system to an expected version number
+!define StdUtils.VerifyOSBuildNo '!insertmacro _StdU_VrfyRealOSBld' #Compare *real* operating system to an expected build number
+!define StdUtils.HashText '!insertmacro _StdU_HashText' #Compute hash from text string (CRC32, MD5, SHA1/2/3, BLAKE2)
+!define StdUtils.HashFile '!insertmacro _StdU_HashFile' #Compute hash from file (CRC32, MD5, SHA1/2/3, BLAKE2)
+!define StdUtils.NormalizePath '!insertmacro _StdU_NormalizePath' #Simplifies the path to produce a direct, well-formed path
+!define StdUtils.GetParentPath '!insertmacro _StdU_GetParentPath' #Get parent path by removing the last component from the path
+!define StdUtils.SplitPath '!insertmacro _StdU_SplitPath' #Split the components of the given path
+!define StdUtils.GetDrivePart '!insertmacro _StdU_GetDrivePart' #Get drive component of path
+!define StdUtils.GetDirectoryPart '!insertmacro _StdU_GetDirPart' #Get directory component of path
+!define StdUtils.GetFileNamePart '!insertmacro _StdU_GetFNamePart' #Get file name component of path
+!define StdUtils.GetExtensionPart '!insertmacro _StdU_GetExtnPart' #Get file extension component of path
+!define StdUtils.TimerCreate '!insertmacro _StdU_TimerCreate' #Create a new event-timer that will be triggered periodically
+!define StdUtils.TimerDestroy '!insertmacro _StdU_TimerDestroy' #Destroy a running timer created with TimerCreate()
+!define StdUtils.ProtectStr '!insertmacro _StdU_PrtctStr' #Protect a given String using Windows' DPAPI
+!define StdUtils.UnprotectStr '!insertmacro _StdU_UnprtctStr' #Unprotect a string that was protected via ProtectStr()
+!define StdUtils.GetLibVersion '!insertmacro _StdU_GetLibVersion' #Get the current StdUtils library version (for debugging)
+!define StdUtils.SetVerbose '!insertmacro _StdU_SetVerbose' #Enable or disable "verbose" mode (for debugging)
#################################################################################
# MACRO DEFINITIONS
#################################################################################
-!macro _StdUtils_Time out
+!macro _StdU_Time out
StdUtils::Time /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_GetMinutes out
+!macro _StdU_GetMinutes out
StdUtils::GetMinutes /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_GetHours out
+!macro _StdU_GetHours out
StdUtils::GetHours /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_GetDays out
+!macro _StdU_GetDays out
StdUtils::GetDays /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_Rand out
+!macro _StdU_Rand out
StdUtils::Rand /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_RandMax out max
+!macro _StdU_RandMax out max
push ${max}
StdUtils::RandMax /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_RandMinMax out min max
+!macro _StdU_RandMinMax out min max
push ${min}
push ${max}
StdUtils::RandMinMax /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_RandList count max
+!macro _StdU_RandList count max
push ${max}
push ${count}
StdUtils::RandList /NOUNLOAD
!macroend
-!macro _StdUtils_FormatStr out format val
- push '${format}'
+!macro _StdU_RandBytes out count
+ push ${count}
+ StdUtils::RandBytes /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_FormatStr out format val
+ push `${format}`
push ${val}
StdUtils::FormatStr /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_FormatStr2 out format val1 val2
- push '${format}'
+!macro _StdU_FormatStr2 out format val1 val2
+ push `${format}`
push ${val1}
push ${val2}
StdUtils::FormatStr2 /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_FormatStr3 out format val1 val2 val3
- push '${format}'
+!macro _StdU_FormatStr3 out format val1 val2 val3
+ push `${format}`
push ${val1}
push ${val2}
push ${val3}
@@ -131,17 +170,17 @@
pop ${out}
!macroend
-!macro _StdUtils_ScanStr out format input default
- push '${format}'
- push '${input}'
+!macro _StdU_ScanStr out format input default
+ push `${format}`
+ push `${input}`
push ${default}
StdUtils::ScanStr /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_ScanStr2 out1 out2 format input default1 default2
- push '${format}'
- push '${input}'
+!macro _StdU_ScanStr2 out1 out2 format input default1 default2
+ push `${format}`
+ push `${input}`
push ${default1}
push ${default2}
StdUtils::ScanStr2 /NOUNLOAD
@@ -149,9 +188,9 @@
pop ${out2}
!macroend
-!macro _StdUtils_ScanStr3 out1 out2 out3 format input default1 default2 default3
- push '${format}'
- push '${input}'
+!macro _StdU_ScanStr3 out1 out2 out3 format input default1 default2 default3
+ push `${format}`
+ push `${input}`
push ${default1}
push ${default2}
push ${default3}
@@ -161,55 +200,96 @@
pop ${out3}
!macroend
-!macro _StdUtils_TrimStr var
+!macro _StdU_TrimStr var
push ${var}
StdUtils::TrimStr /NOUNLOAD
pop ${var}
!macroend
-!macro _StdUtils_TrimStrLeft var
+!macro _StdU_TrimStrLeft var
push ${var}
StdUtils::TrimStrLeft /NOUNLOAD
pop ${var}
!macroend
-!macro _StdUtils_TrimStrRight var
+!macro _StdU_TrimStrRight var
push ${var}
StdUtils::TrimStrRight /NOUNLOAD
pop ${var}
!macroend
-!macro _StdUtils_RevStr var
+!macro _StdU_RevStr var
push ${var}
StdUtils::RevStr /NOUNLOAD
pop ${var}
!macroend
-!macro _StdUtils_SHFileMove out from to hwnd
- push '${from}'
- push '${to}'
+!macro _StdU_ValidFileName out test
+ push `${test}`
+ StdUtils::ValidFileName /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_ValidPathSpec out test
+ push `${test}`
+ StdUtils::ValidPathSpec /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_ValidDomain out test
+ push `${test}`
+ StdUtils::ValidDomainName /NOUNLOAD
+ pop ${out}
+!macroend
+
+
+!macro _StdU_StrToUtf8 out str
+ push `${str}`
+ StdUtils::StrToUtf8 /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_StrFromUtf8 out trnc str
+ push ${trnc}
+ push `${str}`
+ StdUtils::StrFromUtf8 /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_SHFileMove out from to hwnd
+ push `${from}`
+ push `${to}`
push ${hwnd}
StdUtils::SHFileMove /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_SHFileCopy out from to hwnd
- push '${from}'
- push '${to}'
+!macro _StdU_SHFileCopy out from to hwnd
+ push `${from}`
+ push `${to}`
push ${hwnd}
StdUtils::SHFileCopy /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_ExecShlUser out file verb args
- push '${file}'
- push '${verb}'
- push '${args}'
+!macro _StdU_AppendToFile out from dest offset maxlen
+ push `${from}`
+ push `${dest}`
+ push ${offset}
+ push ${maxlen}
+ StdUtils::AppendToFile /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_ExecShlUser out file verb args
+ push `${file}`
+ push `${verb}`
+ push `${args}`
StdUtils::ExecShellAsUser /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_InvkeShlVrb out path file verb_id
+!macro _StdU_InvkeShlVrb out path file verb_id
push "${path}"
push "${file}"
push ${verb_id}
@@ -217,77 +297,195 @@
pop ${out}
!macroend
-!macro _StdUtils_ExecShlWaitEx out_res out_val file verb args
- push '${file}'
- push '${verb}'
- push '${args}'
+!macro _StdU_ExecShlWaitEx out_res out_val file verb args
+ push `${file}`
+ push `${verb}`
+ push `${args}`
StdUtils::ExecShellWaitEx /NOUNLOAD
pop ${out_res}
pop ${out_val}
!macroend
-!macro _StdUtils_WaitForProcEx out handle
- push '${handle}'
+!macro _StdU_WaitForProcEx out handle
+ push `${handle}`
StdUtils::WaitForProcEx /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_GetParameter out name default
- push '${name}'
- push '${default}'
+!macro _StdU_GetParameter out name default
+ push `${name}`
+ push `${default}`
StdUtils::GetParameter /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_GetAllParams out truncate
- push '${truncate}'
+!macro _StdU_TestParameter out name
+ push `${name}`
+ StdUtils::TestParameter /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_ParameterCnt out
+ StdUtils::ParameterCnt /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_ParameterStr out index
+ push ${index}
+ StdUtils::ParameterStr /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_GetAllParams out truncate
+ push `${truncate}`
StdUtils::GetAllParameters /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_GetRealOSVer out_major out_minor out_spack
+!macro _StdU_GetRealOSVer out_major out_minor out_spack
StdUtils::GetRealOsVersion /NOUNLOAD
pop ${out_major}
pop ${out_minor}
pop ${out_spack}
!macroend
-!macro _StdUtils_GetRealOSBld out
+!macro _StdU_GetRealOSBld out
StdUtils::GetRealOsBuildNo /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_GetRealOSStr out
+!macro _StdU_GetRealOSStr out
StdUtils::GetRealOsName /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_VrfyRealOSVer out major minor spack
- push '${major}'
- push '${minor}'
- push '${spack}'
+!macro _StdU_VrfyRealOSVer out major minor spack
+ push `${major}`
+ push `${minor}`
+ push `${spack}`
StdUtils::VerifyRealOsVersion /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_VrfyRealOSBld out build
- push '${build}'
+!macro _StdU_VrfyRealOSBld out build
+ push `${build}`
StdUtils::VerifyRealOsBuildNo /NOUNLOAD
pop ${out}
!macroend
-!macro _StdUtils_GetLibVersion out_ver out_tst
+!macro _StdU_GetOSEdition out
+ StdUtils::GetOsEdition /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_GetOSRelIdNo out
+ StdUtils::GetOsReleaseId /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_GetOSRelIdStr out
+ StdUtils::GetOsReleaseName /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_HashText out type text
+ push `${type}`
+ push `${text}`
+ StdUtils::HashText /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_HashFile out type file
+ push `${type}`
+ push `${file}`
+ StdUtils::HashFile /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_NormalizePath out path
+ push `${path}`
+ StdUtils::NormalizePath /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_GetParentPath out path
+ push `${path}`
+ StdUtils::GetParentPath /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_SplitPath out_drive out_dir out_fname out_ext path
+ push `${path}`
+ StdUtils::SplitPath /NOUNLOAD
+ pop ${out_drive}
+ pop ${out_dir}
+ pop ${out_fname}
+ pop ${out_ext}
+!macroend
+
+!macro _StdU_GetDrivePart out path
+ push `${path}`
+ StdUtils::GetDrivePart /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_GetDirPart out path
+ push `${path}`
+ StdUtils::GetDirectoryPart /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_GetFNamePart out path
+ push `${path}`
+ StdUtils::GetFileNamePart /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_GetExtnPart out path
+ push `${path}`
+ StdUtils::GetExtensionPart /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_TimerCreate out callback interval
+ GetFunctionAddress ${out} ${callback}
+ push ${out}
+ push ${interval}
+ StdUtils::TimerCreate /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_TimerDestroy out timer_id
+ push ${timer_id}
+ StdUtils::TimerDestroy /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_PrtctStr out dpsc salt text
+ push `${dpsc}`
+ push `${salt}`
+ push `${text}`
+ StdUtils::ProtectStr /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_UnprtctStr out trnc salt data
+ push `${trnc}`
+ push `${salt}`
+ push `${data}`
+ StdUtils::UnprotectStr /NOUNLOAD
+ pop ${out}
+!macroend
+
+!macro _StdU_GetLibVersion out_ver out_tst
StdUtils::GetLibVersion /NOUNLOAD
pop ${out_ver}
pop ${out_tst}
!macroend
-!macro _StdUtils_SetVerbose on
- !if "${on}" != "0"
- StdUtils::EnableVerboseMode /NOUNLOAD
- !else
- StdUtils::DisableVerboseMode /NOUNLOAD
- !endif
+!macro _StdU_SetVerbose enable
+ Push ${enable}
+ StdUtils::SetVerboseMode /NOUNLOAD
!macroend
@@ -295,7 +493,9 @@
# MAGIC NUMBERS
#################################################################################
-!define StdUtils.Const.ISV_PinToTaskbar 5386
-!define StdUtils.Const.ISV_UnpinFromTaskbar 5387
-!define StdUtils.Const.ISV_PinToStartmenu 5381
-!define StdUtils.Const.ISV_UnpinFromStartmenu 5382
+!define StdUtils.Const.ShellVerb.PinToTaskbar 0
+!define StdUtils.Const.ShellVerb.UnpinFromTaskbar 1
+!define StdUtils.Const.ShellVerb.PinToStart 2
+!define StdUtils.Const.ShellVerb.UnpinFromStart 3
+
+!endif # !___STDUTILS__NSH___
diff --git a/ant/windows/nsis/Include/StrRep.nsh b/ant/windows/nsis/Include/StrRep.nsh
new file mode 100644
index 000000000..7286b7c45
--- /dev/null
+++ b/ant/windows/nsis/Include/StrRep.nsh
@@ -0,0 +1,66 @@
+!define StrRep "!insertmacro StrRep"
+!macro StrRep output string old new
+ Push `${string}`
+ Push `${old}`
+ Push `${new}`
+ !ifdef __UNINSTALL__
+ Call un.StrRep
+ !else
+ Call StrRep
+ !endif
+ Pop ${output}
+!macroend
+
+!macro Func_StrRep un
+ Function ${un}StrRep
+ Exch $R2 ;new
+ Exch 1
+ Exch $R1 ;old
+ Exch 2
+ Exch $R0 ;string
+ Push $R3
+ Push $R4
+ Push $R5
+ Push $R6
+ Push $R7
+ Push $R8
+ Push $R9
+
+ StrCpy $R3 0
+ StrLen $R4 $R1
+ StrLen $R6 $R0
+ StrLen $R9 $R2
+ loop:
+ StrCpy $R5 $R0 $R4 $R3
+ StrCmp $R5 $R1 found
+ StrCmp $R3 $R6 done
+ IntOp $R3 $R3 + 1 ;move offset by 1 to check the next character
+ Goto loop
+ found:
+ StrCpy $R5 $R0 $R3
+ IntOp $R8 $R3 + $R4
+ StrCpy $R7 $R0 "" $R8
+ StrCpy $R0 $R5$R2$R7
+ StrLen $R6 $R0
+ IntOp $R3 $R3 + $R9 ;move offset by length of the replacement string
+ Goto loop
+ done:
+
+ Pop $R9
+ Pop $R8
+ Pop $R7
+ Pop $R6
+ Pop $R5
+ Pop $R4
+ Pop $R3
+ Push $R0
+ Push $R1
+ Pop $R0
+ Pop $R1
+ Pop $R0
+ Pop $R2
+ Exch $R1
+ FunctionEnd
+!macroend
+!insertmacro Func_StrRep ""
+!insertmacro Func_StrRep "un."
\ No newline at end of file
diff --git a/ant/windows/nsis/Plugins/Release_ANSI/StdUtils.dll b/ant/windows/nsis/Plugins/Release_ANSI/StdUtils.dll
index aad41269c..5317b6d40 100644
Binary files a/ant/windows/nsis/Plugins/Release_ANSI/StdUtils.dll and b/ant/windows/nsis/Plugins/Release_ANSI/StdUtils.dll differ
diff --git a/ant/windows/nsis/Plugins/Release_Unicode/StdUtils.dll b/ant/windows/nsis/Plugins/Release_Unicode/StdUtils.dll
index 9c85c4035..6c852f4c4 100644
Binary files a/ant/windows/nsis/Plugins/Release_Unicode/StdUtils.dll and b/ant/windows/nsis/Plugins/Release_Unicode/StdUtils.dll differ
diff --git a/ant/windows/nsis/uninstall.ico b/ant/windows/nsis/uninstall.ico
new file mode 100644
index 000000000..dd5405cd0
Binary files /dev/null and b/ant/windows/nsis/uninstall.ico differ
diff --git a/ant/windows/nsis/welcome.bmp b/ant/windows/nsis/welcome.bmp
new file mode 100644
index 000000000..ab6523727
Binary files /dev/null and b/ant/windows/nsis/welcome.bmp differ
diff --git a/ant/windows/windows-cleanup.js b/ant/windows/windows-cleanup.js
deleted file mode 100644
index 7a32778d9..000000000
--- a/ant/windows/windows-cleanup.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/** Post-Install Cleanup **/
-
-var app = getArg(0);
-
-if (app) {
- removeStartupEntries(app);
-} else {
- WScript.Echo("No app name provided. Exiting.");
- WScript.Quit(1);
-}
-
-WScript.Quit(0);
-
-/**
- * Cycles through all users on system and removes matching startup entries.
- */
-function removeStartupEntries(app) {
- WScript.Echo('Removing startup entries for all users matching "' + app + '"');
- var shell = new ActiveXObject("WScript.shell");
- // get all users
- var proc = shell.Exec('reg.exe query HKU');
- var users = proc.StdOut.ReadAll().split(/[\r\n]+/);
- for (var i = 0; i < users.length; i++) {
- try {
- var key = trim(users[i]) + "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\" + app;
- shell.RegDelete(key);
- WScript.Echo(' - [success] Removed "' + app + '" startup in ' + users[i]);
- } catch(ignore) {}
- }
-}
-
-/*
- * Gets then nth argument passed into this script
- * Returns defaultVal if argument wasn't found
- */
-function getArg(index, defaultVal) {
- if (index >= WScript.Arguments.length || trim(WScript.Arguments(index)) == "") {
- return defaultVal;
- }
- return WScript.Arguments(index);
-}
-
-/*
- * Functional equivalent of foo.trim()
- */
-function trim(val) {
- return val.replace(/^\s+/,'').replace(/\s+$/,'');
-}
diff --git a/ant/windows/windows-installer.nsi.in b/ant/windows/windows-installer.nsi.in
new file mode 100644
index 000000000..97e309cd7
--- /dev/null
+++ b/ant/windows/windows-installer.nsi.in
@@ -0,0 +1,121 @@
+!include MUI2.nsh
+!include x64.nsh
+!include LogicLib.nsh
+
+!ifdef NSIS_UNICODE
+ !addplugindir "${basedir}/ant/windows/nsis/Plugins/Release_Unicode"
+!else
+ !addplugindir "${basedir}/ant/windows/nsis/Plugins/Release_ANSI"
+!endif
+!addincludedir "${basedir}/ant/windows/nsis/Include"
+!include FindJava.nsh
+!include StdUtils.nsh
+
+
+Name "${project.name}"
+OutFile "${out.dir}/${project.filename}${build.type}-${build.version}.exe"
+RequestExecutionLevel admin
+
+!define MUI_ICON "${basedir}/assets/branding/windows-icon.ico"
+
+; Branding for qz only
+!if "${project.filename}" == "qz-tray"
+ !define MUI_UNICON "${basedir}/ant/windows/nsis/uninstall.ico"
+ !define MUI_WELCOMEFINISHPAGE_BITMAP "${basedir}\ant\windows\nsis\welcome.bmp"
+ !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${basedir}\ant\windows\nsis\welcome.bmp"
+!endif
+
+!insertmacro MUI_PAGE_WELCOME
+!insertmacro MUI_PAGE_DIRECTORY
+!insertmacro MUI_PAGE_INSTFILES
+!insertmacro MUI_UNPAGE_CONFIRM
+!insertmacro MUI_UNPAGE_INSTFILES
+!insertmacro MUI_LANGUAGE "English"
+
+!macro QzInstaller step option value
+ SetDetailsPrint textonly
+ DetailPrint "Running ${step}..."
+ SetDetailsPrint listonly
+ DetailPrint 'Running ${step}: $java -jar "$OUTDIR\${project.filename}.jar" "${step}" "${option}" "${value}"'
+ SetDetailsPrint both
+ ClearErrors
+ nsExec::ExecToLog '$java -jar "$OUTDIR\${project.filename}.jar" "${step}" "${option}" "${value}"'
+ Pop $0
+ ${If} "$0" != "0"
+ Abort "Installation failed during ${step} step. Please check log for details."
+ ${EndIf}
+!macroend
+
+Section
+ ; Set environmental variable for silent install to be picked up by Java
+ ${If} ${Silent}
+ System::Call 'Kernel32::SetEnvironmentVariable(t, t)i ("${vendor.name}_silent", "1").r0'
+ ${EndIf}
+
+ ; Set the $java variable
+ Call FindJava
+
+ ; Create the uninstaller
+ SetOutPath $INSTDIR
+ WriteUninstaller "$INSTDIR\uninstall.exe" ; Backslash required
+
+ ; Run preinstall tasks
+ SetOutPath "$PluginsDir\tmp"
+ DetailPrint "Extracting..."
+ SetDetailsPrint none ; Temporarily suppress details
+ File /r "${dist.dir}\*"
+ SetDetailsPrint both
+ !insertmacro QzInstaller "preinstall" "" ""
+
+ ; Run install tasks
+ !insertmacro QzInstaller "install" "--dest" $INSTDIR
+
+ ; Run certgen tasks
+ SetOutPath $INSTDIR
+ !insertmacro QzInstaller "certgen" "" ""
+
+ ; Launch a non-elevated instance of ${project.name}
+ ${StdUtils.ExecShellAsUser} $0 "$INSTDIR\${project.filename}.exe" "open" ""
+SectionEnd
+
+Section "Uninstall"
+ DetailPrint "Uninstalling..."
+
+ ; Set the $java variable
+ Call un.FindJava
+
+ ; Run uninstall tasks
+ SetOutPath $INSTDIR
+ !insertmacro QzInstaller "uninstall" "" ""
+
+ ; Remove all files
+ DetailPrint "Removing all files..."
+ SetDetailsPrint none ; Temporarily suppress details
+ SetOutPath $TEMP
+ RMDir /r "$INSTDIR"
+ SetDetailsPrint both
+SectionEnd
+
+!macro Init
+ ${If} ${RunningX64}
+ SetRegView 64
+ ${DisableX64FSRedirection}
+ ${EndIf}
+ ${If} $INSTDIR == ""
+ ${If} ${RunningX64}
+ StrCpy $INSTDIR "$PROGRAMFILES64\${project.name}"
+ ${Else}
+ StrCpy $INSTDIR "$PROGRAMFILES\${project.name}"
+ ${EndIf}
+ ${EndIf}
+!macroend
+
+; Runs for installs
+Function .onInit
+ !insertmacro Init
+FunctionEnd
+
+; Runs for uninstall
+Function un.onInit
+ !insertmacro Init
+FunctionEnd
\ No newline at end of file
diff --git a/ant/windows/windows-json-parser.js b/ant/windows/windows-json-parser.js
deleted file mode 100644
index f6fada687..000000000
--- a/ant/windows/windows-json-parser.js
+++ /dev/null
@@ -1,530 +0,0 @@
-// json2.js
-// 2017-06-12
-// Public Domain.
-// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
-
-// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
-// NOT CONTROL.
-
-// This file creates a global JSON object containing two methods: stringify
-// and parse. This file provides the ES5 JSON capability to ES3 systems.
-// If a project might run on IE8 or earlier, then this file should be included.
-// This file does nothing on ES5 systems.
-
-// JSON.stringify(value, replacer, space)
-// value any JavaScript value, usually an object or array.
-// replacer an optional parameter that determines how object
-// values are stringified for objects. It can be a
-// function or an array of strings.
-// space an optional parameter that specifies the indentation
-// of nested structures. If it is omitted, the text will
-// be packed without extra whitespace. If it is a number,
-// it will specify the number of spaces to indent at each
-// level. If it is a string (such as "\t" or " "),
-// it contains the characters used to indent at each level.
-// This method produces a JSON text from a JavaScript value.
-// When an object value is found, if the object contains a toJSON
-// method, its toJSON method will be called and the result will be
-// stringified. A toJSON method does not serialize: it returns the
-// value represented by the name/value pair that should be serialized,
-// or undefined if nothing should be serialized. The toJSON method
-// will be passed the key associated with the value, and this will be
-// bound to the value.
-
-// For example, this would serialize Dates as ISO strings.
-
-// Date.prototype.toJSON = function (key) {
-// function f(n) {
-// // Format integers to have at least two digits.
-// return (n < 10)
-// ? "0" + n
-// : n;
-// }
-// return this.getUTCFullYear() + "-" +
-// f(this.getUTCMonth() + 1) + "-" +
-// f(this.getUTCDate()) + "T" +
-// f(this.getUTCHours()) + ":" +
-// f(this.getUTCMinutes()) + ":" +
-// f(this.getUTCSeconds()) + "Z";
-// };
-
-// You can provide an optional replacer method. It will be passed the
-// key and value of each member, with this bound to the containing
-// object. The value that is returned from your method will be
-// serialized. If your method returns undefined, then the member will
-// be excluded from the serialization.
-
-// If the replacer parameter is an array of strings, then it will be
-// used to select the members to be serialized. It filters the results
-// such that only members with keys listed in the replacer array are
-// stringified.
-
-// Values that do not have JSON representations, such as undefined or
-// functions, will not be serialized. Such values in objects will be
-// dropped; in arrays they will be replaced with null. You can use
-// a replacer function to replace those with JSON values.
-
-// JSON.stringify(undefined) returns undefined.
-
-// The optional space parameter produces a stringification of the
-// value that is filled with line breaks and indentation to make it
-// easier to read.
-
-// If the space parameter is a non-empty string, then that string will
-// be used for indentation. If the space parameter is a number, then
-// the indentation will be that many spaces.
-
-// Example:
-
-// text = JSON.stringify(["e", {pluribus: "unum"}]);
-// // text is '["e",{"pluribus":"unum"}]'
-
-// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t");
-// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
-
-// text = JSON.stringify([new Date()], function (key, value) {
-// return this[key] instanceof Date
-// ? "Date(" + this[key] + ")"
-// : value;
-// });
-// // text is '["Date(---current time---)"]'
-
-// JSON.parse(text, reviver)
-// This method parses a JSON text to produce an object or array.
-// It can throw a SyntaxError exception.
-
-// The optional reviver parameter is a function that can filter and
-// transform the results. It receives each of the keys and values,
-// and its return value is used instead of the original value.
-// If it returns what it received, then the structure is not modified.
-// If it returns undefined then the member is deleted.
-
-// Example:
-
-// // Parse the text. Values that look like ISO date strings will
-// // be converted to Date objects.
-
-// myData = JSON.parse(text, function (key, value) {
-// var a;
-// if (typeof value === "string") {
-// a =
-// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
-// if (a) {
-// return new Date(Date.UTC(
-// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]
-// ));
-// }
-// return value;
-// }
-// });
-
-// myData = JSON.parse(
-// "[\"Date(09/09/2001)\"]",
-// function (key, value) {
-// var d;
-// if (
-// typeof value === "string"
-// && value.slice(0, 5) === "Date("
-// && value.slice(-1) === ")"
-// ) {
-// d = new Date(value.slice(5, -1));
-// if (d) {
-// return d;
-// }
-// }
-// return value;
-// }
-// );
-
-// This is a reference implementation. You are free to copy, modify, or
-// redistribute.
-
-/*jslint
- eval, for, this
-*/
-
-/*property
- JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
- getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
- lastIndex, length, parse, prototype, push, replace, slice, stringify,
- test, toJSON, toString, valueOf
-*/
-
-
-// Create a JSON object only if one does not already exist. We create the
-// methods in a closure to avoid creating global variables.
-
-if (typeof JSON !== "object") {
- JSON = {};
-}
-
-(function () {
- "use strict";
-
- var rx_one = /^[\],:{}\s]*$/;
- var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
- var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
- var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
- var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
- var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
-
- function f(n) {
- // Format integers to have at least two digits.
- return (n < 10)
- ? "0" + n
- : n;
- }
-
- function this_value() {
- return this.valueOf();
- }
-
- if (typeof Date.prototype.toJSON !== "function") {
-
- Date.prototype.toJSON = function () {
-
- return isFinite(this.valueOf())
- ? (
- this.getUTCFullYear()
- + "-"
- + f(this.getUTCMonth() + 1)
- + "-"
- + f(this.getUTCDate())
- + "T"
- + f(this.getUTCHours())
- + ":"
- + f(this.getUTCMinutes())
- + ":"
- + f(this.getUTCSeconds())
- + "Z"
- )
- : null;
- };
-
- Boolean.prototype.toJSON = this_value;
- Number.prototype.toJSON = this_value;
- String.prototype.toJSON = this_value;
- }
-
- var gap;
- var indent;
- var meta;
- var rep;
-
-
- function quote(string) {
-
-// If the string contains no control characters, no quote characters, and no
-// backslash characters, then we can safely slap some quotes around it.
-// Otherwise we must also replace the offending characters with safe escape
-// sequences.
-
- rx_escapable.lastIndex = 0;
- return rx_escapable.test(string)
- ? "\"" + string.replace(rx_escapable, function (a) {
- var c = meta[a];
- return typeof c === "string"
- ? c
- : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
- }) + "\""
- : "\"" + string + "\"";
- }
-
-
- function str(key, holder) {
-
-// Produce a string from holder[key].
-
- var i; // The loop counter.
- var k; // The member key.
- var v; // The member value.
- var length;
- var mind = gap;
- var partial;
- var value = holder[key];
-
-// If the value has a toJSON method, call it to obtain a replacement value.
-
- if (
- value
- && typeof value === "object"
- && typeof value.toJSON === "function"
- ) {
- value = value.toJSON(key);
- }
-
-// If we were called with a replacer function, then call the replacer to
-// obtain a replacement value.
-
- if (typeof rep === "function") {
- value = rep.call(holder, key, value);
- }
-
-// What happens next depends on the value's type.
-
- switch (typeof value) {
- case "string":
- return quote(value);
-
- case "number":
-
-// JSON numbers must be finite. Encode non-finite numbers as null.
-
- return (isFinite(value))
- ? String(value)
- : "null";
-
- case "boolean":
- case "null":
-
-// If the value is a boolean or null, convert it to a string. Note:
-// typeof null does not produce "null". The case is included here in
-// the remote chance that this gets fixed someday.
-
- return String(value);
-
-// If the type is "object", we might be dealing with an object or an array or
-// null.
-
- case "object":
-
-// Due to a specification blunder in ECMAScript, typeof null is "object",
-// so watch out for that case.
-
- if (!value) {
- return "null";
- }
-
-// Make an array to hold the partial results of stringifying this object value.
-
- gap += indent;
- partial = [];
-
-// Is the value an array?
-
- if (Object.prototype.toString.apply(value) === "[object Array]") {
-
-// The value is an array. Stringify every element. Use null as a placeholder
-// for non-JSON values.
-
- length = value.length;
- for (i = 0; i < length; i += 1) {
- partial[i] = str(i, value) || "null";
- }
-
-// Join all of the elements together, separated with commas, and wrap them in
-// brackets.
-
- v = partial.length === 0
- ? "[]"
- : gap
- ? (
- "[\n"
- + gap
- + partial.join(",\n" + gap)
- + "\n"
- + mind
- + "]"
- )
- : "[" + partial.join(",") + "]";
- gap = mind;
- return v;
- }
-
-// If the replacer is an array, use it to select the members to be stringified.
-
- if (rep && typeof rep === "object") {
- length = rep.length;
- for (i = 0; i < length; i += 1) {
- if (typeof rep[i] === "string") {
- k = rep[i];
- v = str(k, value);
- if (v) {
- partial.push(quote(k) + (
- (gap)
- ? ": "
- : ":"
- ) + v);
- }
- }
- }
- } else {
-
-// Otherwise, iterate through all of the keys in the object.
-
- for (k in value) {
- if (Object.prototype.hasOwnProperty.call(value, k)) {
- v = str(k, value);
- if (v) {
- partial.push(quote(k) + (
- (gap)
- ? ": "
- : ":"
- ) + v);
- }
- }
- }
- }
-
-// Join all of the member texts together, separated with commas,
-// and wrap them in braces.
-
- v = partial.length === 0
- ? "{}"
- : gap
- ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}"
- : "{" + partial.join(",") + "}";
- gap = mind;
- return v;
- }
- }
-
-// If the JSON object does not yet have a stringify method, give it one.
-
- if (typeof JSON.stringify !== "function") {
- meta = { // table of character substitutions
- "\b": "\\b",
- "\t": "\\t",
- "\n": "\\n",
- "\f": "\\f",
- "\r": "\\r",
- "\"": "\\\"",
- "\\": "\\\\"
- };
- JSON.stringify = function (value, replacer, space) {
-
-// The stringify method takes a value and an optional replacer, and an optional
-// space parameter, and returns a JSON text. The replacer can be a function
-// that can replace values, or an array of strings that will select the keys.
-// A default replacer method can be provided. Use of the space parameter can
-// produce text that is more easily readable.
-
- var i;
- gap = "";
- indent = "";
-
-// If the space parameter is a number, make an indent string containing that
-// many spaces.
-
- if (typeof space === "number") {
- for (i = 0; i < space; i += 1) {
- indent += " ";
- }
-
-// If the space parameter is a string, it will be used as the indent string.
-
- } else if (typeof space === "string") {
- indent = space;
- }
-
-// If there is a replacer, it must be a function or an array.
-// Otherwise, throw an error.
-
- rep = replacer;
- if (replacer && typeof replacer !== "function" && (
- typeof replacer !== "object"
- || typeof replacer.length !== "number"
- )) {
- throw new Error("JSON.stringify");
- }
-
-// Make a fake root object containing our value under the key of "".
-// Return the result of stringifying the value.
-
- return str("", {"": value});
- };
- }
-
-
-// If the JSON object does not yet have a parse method, give it one.
-
- if (typeof JSON.parse !== "function") {
- JSON.parse = function (text, reviver) {
-
-// The parse method takes a text and an optional reviver function, and returns
-// a JavaScript value if the text is a valid JSON text.
-
- var j;
-
- function walk(holder, key) {
-
-// The walk method is used to recursively walk the resulting structure so
-// that modifications can be made.
-
- var k;
- var v;
- var value = holder[key];
- if (value && typeof value === "object") {
- for (k in value) {
- if (Object.prototype.hasOwnProperty.call(value, k)) {
- v = walk(value, k);
- if (v !== undefined) {
- value[k] = v;
- } else {
- delete value[k];
- }
- }
- }
- }
- return reviver.call(holder, key, value);
- }
-
-
-// Parsing happens in four stages. In the first stage, we replace certain
-// Unicode characters with escape sequences. JavaScript handles many characters
-// incorrectly, either silently deleting them, or treating them as line endings.
-
- text = String(text);
- rx_dangerous.lastIndex = 0;
- if (rx_dangerous.test(text)) {
- text = text.replace(rx_dangerous, function (a) {
- return (
- "\\u"
- + ("0000" + a.charCodeAt(0).toString(16)).slice(-4)
- );
- });
- }
-
-// In the second stage, we run the text against regular expressions that look
-// for non-JSON patterns. We are especially concerned with "()" and "new"
-// because they can cause invocation, and "=" because it can cause mutation.
-// But just to be safe, we want to reject all unexpected forms.
-
-// We split the second stage into 4 regexp operations in order to work around
-// crippling inefficiencies in IE's and Safari's regexp engines. First we
-// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
-// replace all simple value tokens with "]" characters. Third, we delete all
-// open brackets that follow a colon or comma or that begin the text. Finally,
-// we look to see that the remaining characters are only whitespace or "]" or
-// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.
-
- if (
- rx_one.test(
- text
- .replace(rx_two, "@")
- .replace(rx_three, "]")
- .replace(rx_four, "")
- )
- ) {
-
-// In the third stage we use the eval function to compile the text into a
-// JavaScript structure. The "{" operator is subject to a syntactic ambiguity
-// in JavaScript: it can begin a block or an object literal. We wrap the text
-// in parens to eliminate the ambiguity.
-
- j = eval("(" + text + ")");
-
-// In the optional fourth stage, we recursively walk the new structure, passing
-// each name/value pair to a reviver function for possible transformation.
-
- return (typeof reviver === "function")
- ? walk({"": j}, "")
- : j;
- }
-
-// If the text is not JSON parseable, then a SyntaxError is thrown.
-
- throw new SyntaxError("JSON.parse");
- };
- }
-}());
diff --git a/ant/windows/windows-keygen.js.in b/ant/windows/windows-keygen.js.in
deleted file mode 100644
index 3183b7513..000000000
--- a/ant/windows/windows-keygen.js.in
+++ /dev/null
@@ -1,845 +0,0 @@
-/**
- * @author Tres Finocchiaro
- *
- * Copyright (C) 2017 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
- */
-
-/**********************************************************************************************
- * Windows KeyGen Utility *
- **********************************************************************************************
- * Description: *
- * Utility to create a private key and install its respective public certificate to the *
- * system. When run in "uninstall" mode, the public certificate is removed based on *
- * matched publisher/vendor information. *
- * *
- * INSTALL: *
- * 1. Creates a self-signed Java Keystore for jetty wss://${ssl.cn} *
- * 2. Exports public certificate from Java Keystore *
- * 3. Imports into Windows trusted cert store *
- * 4. Imports into Firefox web browser (if installed) *
- * *
- * Note: If [ssl_cert] and [ssl_key] are specified, import to browser/OS is omitted. *
- * *
- * UNINSTALL *
- * 1. Deletes certificate from Windows trusted cert store *
- * 2. Deletes certificate from Firefox web browser (if installed) *
- * *
- * Depends: *
- * keytool.exe (distributed with jre: ${java.download}) *
- * *
- * Usage: *
- * cscript //NoLogo ${windows.keygen.name} *
- * "C:\Program Files\${project.name}" install [hostname] [portable_firefox] [ssl_cert] [ssl_key] *
- * *
- * cscript //NoLogo ${windows.keygen.name} *
- * "C:\Program Files\${project.name}" uninstall [hostname] [portable_firefox] *
- * *
- **********************************************************************************************/
-
-var shell = new ActiveXObject("WScript.shell");
-var fso = new ActiveXObject("Scripting.FileSystemObject");
-var newLine = "\r\n";
-
-// Uses passed-in parameter as install location. Will fallback to registry if not provided.
-var qzInstall = getArg(0, getRegValue("HKLM\\Software\\${project.name}\\"));
-var installMode = getArg(1, "install");
-var cnOverride = getArg(2, null);
-var firefoxPortable = getArg(3, null);
-var trusted = { cert: getArg(4, null), key: getArg(5, null) };
-
-var firefoxVer;
-var firefoxInstall;
-
-/**
- * Prototypes and polyfills
- */
-String.prototype.replaceAll = function(search, replacement) {
- var target = this;
- return target.replace(new RegExp(search, 'g'), replacement);
-};
-
-Array.isArray = function(obj) {
- return Object.prototype.toString.call(obj) === '[object Array]';
-};
-
-if (typeof console === 'undefined') {
- var console = {
- log: function(msg) { WScript.Echo(msg); },
- warn: function(msg) { WScript.Echo("WARN: " + msg); },
- error: function(object, status) {
- WScript.Echo("ERROR: " + (typeof object === 'object' ? object.message : object));
- WScript.Quit(status ? status : -1);
- }
- }
-}
-
-if (installMode == "install") {
- var password, javaEnv;
- if (trusted.cert && trusted.key) {
- createPKCS12();
- createJavaKeystore();
- } else if (createJavaKeystore()) {
- try { installWindowsCertificate(); }
- catch (err) { installWindowsXPCertificate(); }
- if (hasFirefoxConflict()) {
- alert("WARNING: ${project.name} installation would conflict with an existing Firefox AutoConfig rule.\n\n" +
- "Please notify your administrator of this warning.\n\n" +
- "The installer will continue, but ${project.name} will not function with Firefox until this conflict is resolved.",
- "Firefox AutoConfig Warning");
- } else {
- installFirefoxCertificate();
- }
- }
-} else {
- try { deleteWindowsCertificate(); }
- catch (err) { deleteWindowsXPCertificate(); }
- deleteFirefoxCertificate();
-}
-
-WScript.Quit(0);
-
-/**
- * Deletes a file
- */
-function deleteFile(filePath) {
- if (fso.FileExists(filePath)) {
- try {
- fso.DeleteFile(filePath);
- } catch (err) {
- console.error("Unable to delete " + filePath);
- }
- }
-}
-
-function java() {
- if (!javaEnv) {
- var regKey = "HKLM\\Software\\JavaSoft\\Java Runtime Environment\\";
- var jreHome = getRegValue(regKey + getRegValue(regKey + "CurrentVersion") + "\\JavaHome");
- // Look again, but for JDK
- if (!jreHome) {
- regKey = "HKLM\\Software\\JavaSoft\\JDK\\";
- jreHome = getRegValue(regKey + getRegValue(regKey + "CurrentVersion") + "\\JavaHome");
- }
- var keyTool = jreHome + "\\bin\\keytool.exe";
- if (!jreHome) {
- try {
- // AdoptOpenJDK adds java.exe to PATH
-
- // Re-read PATH vars in case they've changed
- var newPath = (shell.Environment("USER")("PATH") + ";" + shell.Environment("SYSTEM")("PATH")).replace(/;;/g, ";");
- // Expand any %STRINGS% inside path and set for the next running process
- shell.Environment("PROCESS")("PATH") = shell.ExpandEnvironmentStrings(newPath);
-
- shell.Exec('java.exe'); // throws exception if not found
- jreHome = "NOT_NEEDED";
- regkey = "NOT_NEEDED";
- keyTool = "keytool.exe";
- } catch(ignore) {}
- }
- javaEnv = {
- regKey: regKey,
- jreHome: jreHome,
- keyTool: keyTool
- };
- }
- return javaEnv;
-}
-
-/**
- * Generates a random string to be used as a password
- */
-function pw() {
- if (!password) {
- password = "";
- var chars = "abcdefghijklmnopqrstuvwxyz0123456789";
- for( var i=0; i < parseInt("${ssl.passlength}"); i++ ) {
- password += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- }
- return password;
-}
-
-/**
- * Reads a registry value, taking 32-bit/64-bit architecture into consideration
- */
-function getRegValue(path) {
- // If 64-bit OS, try 32-bit registry first
- if (shell.ExpandEnvironmentStrings("ProgramFiles(x86)")) {
- path = path.replace("\\Software\\", "\\Software\\Wow6432Node\\");
- }
-
- var regValue = "";
- try {
- regValue = shell.RegRead(path);
- } catch (err) {
- try {
- // Fall back to 64-bit registry
- path = path.replace("\\Software\\Wow6432Node\\", "\\Software\\");
- regValue = shell.RegRead(path);
- } catch (err) {}
- }
- return regValue;
-}
-
-/**
- * Displays a message regarding whether or not a file exists
- */
-function verifyExists(path, msg) {
- var success = fso.FileExists(path);
- console.log(" - " + (success ? "[success] " : "[failed] ") + msg);
- return success;
-}
-
-/**
- * Displays a message regarding whether or not a command succeeded
- */
-function verifyExec(cmd, msg) {
- try {
- var success = shell.Run(cmd, 0, true) == 0;
- console.log(" - " + (success ? "[success] " : "[failed] ") + msg);
- return success;
- } catch(err) {
- console.log(' - [failed] Error executing "' + cmd + '"');
- return false;
- }
-}
-
-/**
- * Replaces "!install" with proper location, usually "C:\Program Files\", fixes forward slashes
- */
-function fixPath(path) {
- var removeTrailing = qzInstall.replace(/\\$/, "").replace(/\/$/, "");
- return path.replace("!install", removeTrailing).replace(/\//g, "\\");
-}
-
-function replaceVars(cmd) {
- var c = cmd;
-
- // Handle CN=${ssl.cn} override
- if (cnOverride) {
- c = c.replace("CN=${ssl.cn},", "CN=" + cnOverride + ",")
- .replace("san=dns:${ssl.cn},", "san=" + (isIp4(cnOverride) ? "ip:" : "dns:" ) + cnOverride + ",")
- .replace(",dns:${ssl.cnalt}", "");
- }
-
- return c.replaceAll("${ca.jks}", fixPath("${ca.jks}"))
- .replaceAll("${ca.crt}", fixPath("${ca.crt}"))
- .replaceAll("${ssl.jks}", fixPath("${ssl.jks}"))
- .replaceAll("${ssl.crt}", fixPath("${ssl.crt}"))
- .replaceAll("${ssl.csr}", fixPath("${ssl.csr}"))
- .replaceAll("!storepass", pw())
- .replaceAll("!keypass", pw())
- .replaceAll("!sslcert", trusted.cert)
- .replaceAll("!sslkey", trusted.key)
- .replaceAll("keytool", java().keyTool)
- .replaceAll("${trusted.keypair}", fixPath("${trusted.keypair}"));
- }
-
-/*
- * Reads in a text file, expands the specified named variable replacements and writes it back out.
- */
-function writeParsedConfig(inPath, outPath, replacements) {
- var inFile = fso.OpenTextFile(inPath, 1, true); // 1 = ForReading
- var outFile = fso.OpenTextFile(outPath, 2, true); // 2 = ForWriting
-
- while(!inFile.AtEndOfStream) {
- line = inFile.ReadLine()
-
- // Process all variable replacements
- for (var key in replacements) {
- if (!replacements.hasOwnProperty(key)) continue;
- // Escape leading "$" prior to building regex
- var varName = (key.indexOf("$") == 0 ? "\\" + key : key);
- var re = new RegExp(varName, 'g')
- line = line.replace(re, replacements[key]);
- }
- outFile.WriteLine(line);
- }
- inFile.close();
- outFile.close();
-}
-
-/*
- * Reads in a X509 certificate, stripping BEGIN, END and NEWLINE string
- */
-function readPlainCert(certPath) {
- var certFile = fso.OpenTextFile(certPath, 1, true);
- var certData = "";
- while (!certFile.AtEndOfStream) { certData += strip(certFile.ReadLine()); }
- certFile.close();
- return certData;
-}
-
-/*
- * Strips non-base64 data (i.e RFC X509 --START, --END) from a string
- */
-function strip(line) {
- var X509 = ["-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----", "\r", "\n"];
- for (var i in X509) { line = line.replace(new RegExp(X509[i], 'g'), ''); }
- return line;
-}
-
-/*
- * Creates the Java Keystore
- */
-function createJavaKeystore() {
- var success = false;
-
- if (!java().jreHome) {
- console.error("Can't find JavaHome. Secure websockets will not work.", "${windows.err.java}");
- }
-
- if (!qzInstall) {
- console.error("Can't find ${project.name} installation path. Secure websockets will not work.", "${windows.err.install}");
- }
-
- deleteFile(fixPath("${ssl.jks}"));
- deleteFile(fixPath("${ssl.crt}"));
-
- console.log("Creating keystore for wss://" + (cnOverride || "${ssl.cn}") + " (this could take a minute)...");
-
- if (trusted.cert) {
- success = verifyExec(replaceVars("${trusted.convert}"), "Converting trusted keypair to Java format");
- } else {
- deleteFile(fixPath("${ca.jks}"));
- deleteFile(fixPath("${ca.crt}"));
- deleteFile(fixPath("${ssl.jks}"));
- deleteFile(fixPath("${ssl.csr}"));
-
- success = verifyExec(replaceVars("${ca.jkscmd}"), "Creating a CA keypair: ${ca.jks}") &&
- verifyExec(replaceVars("${ca.crtcmd}"), "Exporting CA certificate: ${ca.crt}") &&
- verifyExec(replaceVars("${ssl.jkscmd}"), "Creating an SSL keypair: ${ssl.jks}") &&
- verifyExec(replaceVars("${ssl.jkscsr}"), "Creating an SSL CSR: ${ssl.csr}") &&
- verifyExec(replaceVars("${ssl.crtcmd}"), "Issuing SSL certificate from CA: ${ssl.crt}") &&
- verifyExec(replaceVars("${ssl.importca}"), "Importing CA certificate into SSL keypair: ${ssl.crt}") &&
- verifyExec(replaceVars("${ssl.importssl}"), "Importing chained SSL certificate into SSL keypair: ${ca.crt}");
-
- deleteFile(fixPath("${ca.jks}"));
- deleteFile(fixPath("${ssl.csr}"));
- deleteFile(fixPath("${ssl.crt}"));
- }
-
- return success && writePropertiesFile();
-}
-
-/*
- * Writes ${ssl.properties}
- */
-function writePropertiesFile() {
- console.log("Writing ${ssl.properties}...");
-
- // Kills any running ${project.name} instances
- verifyExec("wmic.exe process where \"Name like '%java%' and CommandLine like '%${project.filename}.jar%'\" call terminate",
- "Shutting down any ${project.name} instances");
-
- // Handle "community" mode, custom signing auth cert
- var authCert = "${build.type}" ? fixPath("${authcert.install}").replace(/\\/g, "\\\\") : "";
-
- try {
- var file = fso.OpenTextFile(fixPath("${ssl.properties}"), 2, true);
- file.WriteLine("wss.alias=" + "${ssl.alias}");
- file.WriteLine("wss.keystore=" + fixPath("${ssl.jks}").replace(/\\/g, "\\\\"));
- file.WriteLine("wss.keypass=" + pw());
- file.WriteLine("wss.storepass=" + pw());
- file.WriteLine("wss.host=${ssl.host}");
- file.Write(authCert ? "authcert.override=" + authCert + newLine : "");
- file.Close();
- console.log(" - [success] Writing SSL properties file: ${ssl.properties}");
- return true;
- } catch(err) {
- console.log(" - [failed] Error writing: ${ssl.properties}");
- }
-}
-
-/*
- * Exports certificate to native format
- */
-function installWindowsCertificate() {
- console.log("Installing native certificate for secure websockets...");
- var success = verifyExec(replaceVars("${windows.keygen.install}"), "Installing native certificate") && findWindowsMatches("");
- if (success) {
- console.log(" - [success] Checking certificate installed");
- } else {
- throw "${windows.keygen.tool} failed";
- }
-}
-
-function installWindowsXPCertificate() {
- shell.Popup("Automatic certificate installation is not available for this platform.\n" +
- "For secure websockets to function properly:\n\n" +
- " 1. Navigate to \"" + fixPath("${ca.crt}") + "\"\n" +
- " 2. Click \"Install Certificate...\"\n" +
- " 3. Click \"Place all certificates in the following store\"\n" +
- " 4. Browse to \"Trusted Root Certificate Authorities\"\n" +
- " 5. Click \"Finish\"\n" +
- " 6. Click \"Yes\" on thumbprint Security Warning\n\n" +
- "Click OK to automatically launch the certificate import wizard now.\n", 0, "Warning - ${project.name}", 48);
-
- // Do not wrap quotes around ${ca.crt}, or this next line will fail
- shell.Run("rundll32.exe cryptext.dll,CryptExtAddCER " + fixPath("${ca.crt}"), 1, true);
-}
-
-/*
- * Gets the Firefox installation path, stores it a global variable "firefoxInstall"
- */
-function getFirefoxInstall() {
- console.log("Searching for Firefox...");
-
- // Use provided install directory, if supplied
- if (firefoxPortable) {
- firefoxInstall = firefoxPortable + "\\App\\Firefox\\firefox.exe";
- return firefoxInstall;
- }
-
- // Determine if Firefox is installed
- var firefoxKey = "HKLM\\Software\\Mozilla\\Mozilla Firefox";
- firefoxVer = getRegValue(firefoxKey + "\\");
- if (!firefoxVer) {
- // Look for Extended Support Release
- firefoxVer = getRegValue(firefoxKey + " ESR\\");
- if (firefoxVer) {
- firefoxVer += " ESR";
- console.log(" - [success] Found Firefox " + firefoxVer);
- }
- else {
- console.log(" - [skipped] Firefox was not detected");
- return false;
- }
- } else {
- console.log(" - [success] Found Firefox " + firefoxVer);
- }
-
- // Determine full path to firefox.exe, i.e. "C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
- firefoxInstall = getRegValue(firefoxKey + " " + firefoxVer + "\\bin\\PathToExe");
-
- return firefoxInstall;
-}
-
-/*
- * Iterates over the installed preferences file looking for a non-${project.name} AutoConfig rule
- */
-function hasFirefoxConflict() {
- if (!getFirefoxInstall()) { return false; }
-
- // Use policy files for versions >= 62
- try {
- var version = firefoxVer.split('.')[0];
- console.log(" - [success] Parsed Firefox major version: " + version);
- if (parseInt(version) >= 62) {
- installFirefoxPolicy();
- // Toggle-off cert install, favor policy instead
- firefoxInstall = false;
- return;
- }
- } catch(err) {
- console.warn("Unable to parse Firefox major version from \"" + version
- + "\". Falling back to AutConfig mode.");
- }
-
- console.log("Searching for Firefox AutoConfig conflicts...");
- // AutoConfig rule conflicts to search for
- var conflicts = ["general.config.filename"];
-
- // White-listed preference files, used for ${project.name} deployment
- var exceptions = ["${firefoxprefs.name}"];
- var folder = fso.GetFolder(firefoxInstall + "\\..\\defaults\\pref");
- var o = new Enumerator(folder.Files);
- for ( ; !o.atEnd(); o.moveNext()) {
- var whitelist = false;
- for (var i in exceptions) {
- if (exceptions[i] == o.item().Name) {
- console.log(" - [skipped] Writing ${project.name} config file: " + exceptions[i]);
- whitelist = true;
- }
- }
- if (!whitelist && parseFirefoxPref(o.item(), conflicts)) {
- return true;
- }
- }
- console.log(" - [success] No conflicts found");
- return false;
-}
-
-/*
- * Reads a Firefox preference file for already existing AutoConfig rule conflicts
- * Conflicts suggest an enterprise-type deployment environment.
- * Returns true if a conflict exists.
- */
-function parseFirefoxPref(file, conflicts) {
- var inFile = fso.OpenTextFile(file.Path, 1, true); // 1 = ForReading
- var counter = 0;
- while(!inFile.AtEndOfStream) {
- var line = inFile.ReadLine()
- counter++;
- for (var i in conflicts) {
- // Check for both quote styles, 'foo.bar.name' and "foo.bar.name"
- if (line.indexOf("'" + conflicts[i] + "'") >= 0 ||
- line.indexOf('"' + conflicts[i] + '"') >= 0) {
- console.log(" - [error] Conflict found in " + file.Name +
- "\n\t Conflict on line " + counter + ": \"" + line + "\"");
- inFile.close();
- return true;
- }
- }
- }
- inFile.close();
- return false;
-}
-
-
-/*
- * Delete certificate for Mozilla Firefox browser, which utilizes its own cert database
- */
-function deleteFirefoxCertificate() {
- if (!getFirefoxInstall()) { return; }
-
- console.log("Removing from Firefox...");
-
- // Use policy files for versions >= 62
- try {
- var version = firefoxVer.split('.')[0];
- console.log(" - [skipped] Configured via policies.json, no uninstall needed");
- if (parseInt(version) >= 62) {
- return;
- }
- } catch(err) {
- console.warn("Unable to parse Firefox major version from \"" + version
- + "\". Falling back to AutConfig mode.");
- }
- var firefoxCfg = firefoxInstall + "\\..\\${firefoxconfig.name}";
-
- // Variable replacements for Firefox config file
- var replacements = {
- "${certData}" : "",
- "${uninstall}" : "true",
- "${timestamp}" : "-1",
- "${commonName}" : (cnOverride || "${ssl.cn}"),
- "${trayApp}" : ""
- };
-
- // 1. readPlainCert() reads in certificate, stripping non-base64 content
- // 2. writeParsedConfig(...) reads, parses and writes config file in same folder as firefox.exe
- writeParsedConfig(fixPath("${firefoxconfig.install}"), firefoxCfg, replacements);
- verifyExists(firefoxCfg, "Firefox config exists");
-}
-
-/*
- * Install certificate for Mozilla Firefox browser, which utilizes its own cert database
- */
-function installFirefoxCertificate() {
- if (!firefoxInstall) {
- console.log("Skipping Firefox cert install...");
- return;
- }
- console.log("Registering with Firefox...");
- var firefoxCfg = firefoxInstall + "\\..\\${firefoxconfig.name}";
-
- // Variable replacements for Firefox config file
- var replacements = {
- "${certData}" : readPlainCert(fixPath("${ca.crt}")),
- "${uninstall}" : "false",
- "${timestamp}" : new Date().getTime(),
- "${commonName}" : (cnOverride || "${ssl.cn}"),
- "${trayApp}" : ""
- };
-
- // 1. readPlainCert() reads in certificate, stripping non-base64 content
- // 2. writeParsedConfig(...) reads, parses and writes config file in same folder as firefox.exe
- writeParsedConfig(fixPath("${firefoxconfig.install}"), firefoxCfg, replacements);
- verifyExists(firefoxCfg, "Checking Firefox config exists");
-
- // Install the preference file tells Firefox to launches ${firefoxconfig.name} each time it starts
- var firefoxPrefs = firefoxInstall + "\\..\\defaults\\pref\\${firefoxprefs.name}";
- fso.CopyFile(fixPath("${firefoxprefs.install}"), firefoxPrefs);
-}
-
-/*
- * Install a Firefox policy to import the system certificates
- */
-function installFirefoxPolicy() {
- include('${windows.jsonparser.name}');
-
- var policyPath = firefoxInstall + "\\..\\distribution\\policies.json";
- var policyJSON = read(policyPath);
- try {
- policy = JSON.parse(policyJSON || "{}");
- } catch(err) {
- policy = JSON.parse("{}");
- }
- var merge = JSON.parse('{ "policies": { "Certificates": { "ImportEnterpriseRoots": true } } }');
-
- var before = JSON.stringify(policy, null, 2);
- mergeJSON(policy, merge, false);
- var after = JSON.stringify(policy, null, 2);
-
- write(policyPath, after);
-
- console.log("\r\npolicy.json (before):\r\n" + before);
- console.log("\r\npolicy.json (after):\r\n" + after);
-}
-
-/*
- * Writes values from append to base, deep copying if necessary
- * See also firefox-json-writer.py: merge_json
- */
-function mergeJSON(base, append, overwrite) {
- if (!append) { return; }
-
- for(var key in append) {
- if (append.hasOwnProperty(key)) {
- var val = append[key];
- var baseVal = base[key];
-
- if (baseVal === undefined) {
- base[key] = val;
- } else if ((val && typeof val == 'object') && (baseVal && typeof baseVal == 'object')) {
- if (Array.isArray(baseVal) && Array.isArray(val)) {
- if (overwrite) {
- base[key] = val;
- } else {
- base[key] = base[key].concat(val);
- }
- } else {
- mergeJSON(baseVal, val, overwrite);
- }
- } else if (overwrite) {
- base[key] = val;
- }
- }
- }
-
- return base;
-}
-
-/*
- * Convert Trusted SSL certificate to Java Keystore for use with secure websockets
- */
-function createPKCS12() {
- var keyPair = fixPath("${trusted.keypair}");
-
- var generated = "${trusted.command}"
- .replace("${trusted.keypair}", keyPair)
- .replace("${trusted.crt}", trusted.cert)
- .replace("${trusted.jks}", trusted.key)
- .replace("!keypass", pw());
-
- console.log("Creating PKCS12 keypair (this requires openssl)...");
- if (!verifyExec(generated, "Creating PKCS12 keypair")) {
- console.error("Error creating PKCS12 keypair from: " + trusted.cert + ", " + trusted.key + " to " + keyPair);
- }
-}
-
-
-
-/*
- * Deletes windows certificates based on specific CN and OU values
- */
-function deleteWindowsCertificate() {
- console.log("Deleting old certificates...");
- var serialDelim = "||";
- var matches = findWindowsMatches(serialDelim);
-
- // If matches are found, delete them
- if (matches) {
- matches = matches.split(serialDelim);
- for (var i in matches) {
- if (matches[i]) {
- var success = verifyExec(replaceVars("${windows.keygen.uninstall}").replaceAll("!match", matches[i]), 'Remove "${vendor.company}" ' + matches[i] + ' certificate');
- if (!success) {
- throw "${windows.keygen.tool} failed";
- }
- }
- }
-
- // Verify removal
- matches = findWindowsMatches();
- if (matches) {
- console.log(" - [failed] Some certificates not deleted");
- return false;
- } else {
- console.log(" - [success] Certificate(s) removed");
- }
- } else {
- console.log(" - [skipped] No matches found");
- }
- return true;
-}
-
-/*
- * Certutil isn't available on Windows XP, show manual instructions instead
- */
-function deleteWindowsXPCertificate() {
- shell.Popup("Automatic certificate deletion is not available for this platform.\n" +
- "To completely remove unused certificates:\n\n" +
- " 1. Manage computer certificates\n" +
- " 2. Click \"Trusted Root Certificate Authorities...\"\n" +
- " 3. Click \"Certificates\"\n" +
- " 4. Browse to \"${ssl.cn}, ${vendor.company}\"\n" +
- " 5. Right Click, \"Delete\"\n" +
- "Click OK to automatically launch the certificate manager.\n", 0, "Warning - ${project.name}", 48);
-
- shell.Run("mmc.exe certmgr.msc", 1, true);
-}
-
-
-/*
- * Returns matching serial numbers delimited by two pipes, i.e "9876fedc||1234abcd"
- */
-function findWindowsMatches(serialDelim) {
- var matches = "";
- var proc = shell.Exec('${windows.keygen.tool} -store "${windows.keygen.store}"');
- var certBlock = "";
- while (!proc.StdOut.AtEndOfStream) {
- var line = proc.StdOut.ReadLine()
- // Read certBlock in block sections
- if (line.indexOf("================") === -1) {
- certBlock += line + newLine;
- } else {
- var serial = parseCertificateSerial(certBlock);
- if (serial && isVendorMatch(certBlock)) {
- matches += serial + serialDelim;
- }
- certBlock = "";
- }
- }
- return matches;
-}
-
-/*
- * Parses the supplied data for serialTag
- * If found, returns the serial number of the certificate, i.e. "89e301a9"
- */
-function parseCertificateSerial(certBlock) {
- // First line should have serial
- var lines = certBlock.split(newLine);
- if (lines.length > 0) {
- var serialParts = lines[0].split(":");
- if (serialParts.length > 0) {
- return trim(serialParts[1]);
- }
- }
- return false;
-}
-
-/*
- * Parses the supplied data for issuerTag
- * If found, parses the matched line for specific CN and OU values.
- * Returns true if found
- */
-function isVendorMatch(certBlock) {
- // Second line should have issuer
- var lines = certBlock.split(newLine);
- if (lines.length > 0) {
- var issuerLine = lines[1];
- var i = issuerLine.indexOf(":") + 1;
- if (i > 1) {
- var issuer = trim(issuerLine.substring(i, issuerLine.length));
- return issuer.indexOf("OU=${vendor.company}") !== -1 && issuer.indexOf("CN=" + (cnOverride || "${ssl.cn}")) !== -1;
- }
- }
- return false;
-}
-
-/*
- * Functional equivalent of foo.trim()
- */
-function trim(val) {
- return val ? val.replace(/^\s+/,'').replace(/\s+$/,'') : val;
-}
-
-/*
- * Parses a string to determine if it is an IPv4 address
- */
-function isIp4(host) {
- var parts = host.split(".");
- var counter = 0;
- for (var i = 0; i < parts.length; i++) {
- if (isNaN(parseInt(parts[i]))) {
- return false;
- }
- counter++
- }
- return counter == 4;
-}
-
-/*
- * Gets then nth argument passed into this script
- * Returns defaultVal if argument wasn't found
- */
-function getArg(index, defaultVal) {
- if (index >= WScript.Arguments.length || trim(WScript.Arguments(index)) == "") {
- return defaultVal;
- }
- return WScript.Arguments(index);
-}
-
-/*
- * Mimic an alert dialog, used only for OK_ONLY + WARNING (0 + 48), replaced with a console message if silent install
- */
-function alert(message, title) {
- if (shell.Environment("PROCESS").Item("${vendor.name}_silent")) {
- console.warn(message);
- } else {
- shell.Popup(message, 0, title == null ? "Warning" : title, 48);
- }
-}
-
-/*
- * Write a text file at path containing the specified data creating
- * parent directories as needed
- */
-function write(path, data) {
- var ForWriting = 2;
- var fso = new ActiveXObject("Scripting.FileSystemObject");
- if (!fso.FileExists(path)) {
- mkdirs(fso.GetParentFolderName(path), fso);
- fso.CreateTextFile(path);
- }
- var file = fso.GetFile(path);
- var stream = file.OpenAsTextStream(ForWriting);
- var data = stream.Write(data);
- stream.Close();
-}
-
-/*
- * Reads a text file at path and returns contents as a string
- */
-function read(path) {
- var data;
- var ForReading = 1;
- var fso = new ActiveXObject("Scripting.FileSystemObject");
- if (fso.FileExists(path)) {
- var file = fso.GetFile(path);
- if (file.Size > 0) {
- var stream = file.OpenAsTextStream(ForReading);
- data = stream.readAll();
- stream.Close();
- }
- }
- return data;
-}
-
-/*
- * Creates a directory and all parents using the provided FileSystemObject handle
- */
-function mkdirs(path, fso) {
- if(!fso.FolderExists(path) && path !== "") {
- if (!fso.FolderExists(fso.GetParentFolderName(path))) {
- mkdirs(fso.GetParentFolderName(path), fso);
- }
- fso.CreateFolder(path);
- }
-}
-
-/*
- * Imports a JavaScript library by calling eval on the source content
- */
-function include(path) {
- var fso = new ActiveXObject("Scripting.FileSystemObject");
- path = fso.GetParentFolderName(WScript.ScriptFullName) + "\\" + path;
- eval(read(path));
-}
diff --git a/ant/windows/windows-launcher.nsi.in b/ant/windows/windows-launcher.nsi.in
index 244c1c966..c1594c25a 100644
--- a/ant/windows/windows-launcher.nsi.in
+++ b/ant/windows/windows-launcher.nsi.in
@@ -1,13 +1,16 @@
-;------------------
-; ${project.name} Launcher
-;------------------
-; Creates a ${project.filename}.exe launcher which performs automatic JRE detection
-
!include x64.nsh
!include LogicLib.nsh
-!addincludedir "${windows.nsis.addons}/Include"
+
+!ifdef NSIS_UNICODE
+ !addplugindir "${basedir}/ant/windows/nsis/Plugins/Release_Unicode"
+!else
+ !addplugindir "${basedir}/ant/windows/nsis/Plugins/Release_ANSI"
+!endif
+!addincludedir "${basedir}/ant/windows/nsis/Include"
!include StdUtils.nsh
-!include FileFunc.nsh
+!include FindJava.nsh
+
+!insertmacro GetParameters
; Run this exe as non-admin
RequestExecutionLevel user
@@ -15,137 +18,38 @@ RequestExecutionLevel user
; Application information
Name "${project.name}"
Caption "${project.name}"
-Icon "${basedir}\${branding.dir}\${windows.icon}"
-OutFile "${dist.dir}\${project.filename}.exe"
+Icon "${basedir}/assets/branding/windows-icon.ico"
+OutFile "${dist.dir}/${project.filename}.exe"
SilentInstall silent
AutoCloseWindow true
ShowInstDetails nevershow
; Full path to jar
-!define JAR "$EXEDIR\${project.filename}.jar"
+!define JAR "$EXEDIR/${project.filename}.jar"
-; Autostart variable
-Var AUTOSTART
-
-Section ""
- Call DetermineAutostart
- ${If} $AUTOSTART != "1"
- Abort
- ${EndIf}
+Section
+ ${If} ${RunningX64}
+ ${DisableX64FSRedirection}
+ ${EndIf}
+ SetOutPath $EXEDIR
+ ; Get params to pass to jar
+ Var /GLOBAL params
+ ${GetParameters} $params
- ${If} ${RunningX64}
- ${DisableX64FSRedirection}
- ${EndIf}
- Call FindJRE
- Pop $R0
-
- ; change for your purpose (-jar etc.)
- StrCpy $0 '"$R0" ${launch.opts} -jar "${JAR}"'
-
- SetOutPath $EXEDIR
+ ; Sets the $java variable
+ Call FindJava
- Exec $0
- ${If} ${RunningX64}
- ${EnableX64FSRedirection}
- ${EndIf}
+ Exec '"$javaw" ${launch.opts} -jar "${JAR}" $params'
+ ${If} ${RunningX64}
+ ${EnableX64FSRedirection}
+ ${EndIf}
SectionEnd
-; FindJRE (find "javaw.exe")
-; 1 - Search in .\jre directory (e.g. JRE Installed with application)
-; 2 - Search in JAVA_HOME environment variable
-; 3 - Search in the native registry
-; 4 - Search in the 32-bit registry
-; 5 - Fall-back to "javaw.exe" (such as in current dir or PATH)
-Function FindJRE
- Push $R0
- Push $R1
-
- ClearErrors
- StrCpy $R0 "$EXEDIR\jre\bin\javaw.exe"
- IfFileExists $R0 JreFound
- StrCpy $R0 ""
-
- ClearErrors
- ReadEnvStr $R0 "JAVA_HOME"
- StrCpy $R0 "$R0\bin\javaw.exe"
- IfErrors 0 JreFound
-
- ClearErrors
- ReadRegStr $R1 HKLM "Software\JavaSoft\Java Runtime Environment" "CurrentVersion"
- ReadRegStr $R0 HKLM "Software\JavaSoft\Java Runtime Environment\$R1" "JavaHome"
- StrCpy $R0 "$R0\bin\javaw.exe"
- IfErrors 0 JreFound
-
- ; Fall-back to 32-bit registry
- ${If} ${RunningX64}
- ClearErrors
- ReadRegStr $R1 HKLM "Software\Wow6432Node\JavaSoft\Java Runtime Environment" "CurrentVersion"
- ReadRegStr $R0 HKLM "Software\Wow6432Node\JavaSoft\Java Runtime Environment\$R1" "JavaHome"
- StrCpy $R0 "$R0\bin\javaw.exe"
- IfErrors 0 JreFound
- ${EndIf}
-
- ; Look again, but for JDK
- ClearErrors
- ReadRegStr $R1 HKLM "Software\JavaSoft\JDK" "CurrentVersion"
- ReadRegStr $R0 HKLM "Software\JavaSoft\JDK\$R1" "JavaHome"
- StrCpy $R0 "$R0\bin\javaw.exe"
- IfErrors 0 JreFound
-
- ; Fall-back to 32-bit registry
- ${If} ${RunningX64}
- ClearErrors
- ReadRegStr $R1 HKLM "Software\Wow6432Node\JavaSoft\JDK" "CurrentVersion"
- ReadRegStr $R0 HKLM "Software\Wow6432Node\JavaSoft\JDK\$R1" "JavaHome"
- StrCpy $R0 "$R0\bin\javaw.exe"
- IfErrors 0 JreFound
- ${EndIf}
-
- ; Give up. Use javaw.exe and hope it works
- StrCpy $R0 "javaw.exe"
-
- JreFound:
- Pop $R1
- Exch $R0
-FunctionEnd
-
-Function DetermineAutostart
- ClearErrors
- ${GetParameters} $0
- ${GetOptions} "$0" "-A" $3
- IfErrors 0 LocalAutostart
- StrCpy $AUTOSTART "1"
- Return
-
- LocalAutostart:
- ClearErrors
- ReadEnvStr $R0 "APPDATA"
- FileOpen $1 "$R0\${project.datadir}\${autostart.name}" r
- FileSeek $1 0
- FileRead $1 $AUTOSTART
- FileClose $1
-
- IfErrors SharedAutostart 0
- Return
-
- SharedAutostart:
- ClearErrors
- ReadEnvStr $R0 "PROGRAMDATA"
- FileOpen $2 "$R0\${project.datadir}\${autostart.name}" r
- FileSeek $2 0
- FileRead $2 $AUTOSTART
- FileClose $2
-
- IfErrors 0 +2
- StrCpy $AUTOSTART "1"
-
-FunctionEnd
-
Function .onInit
${If} ${RunningX64}
- ; Force 64-bit registry view by default
SetRegView 64
+ ${DisableX64FSRedirection}
${EndIf}
-FunctionEnd
+FunctionEnd
\ No newline at end of file
diff --git a/ant/windows/windows-packager.nsi.in b/ant/windows/windows-packager.nsi.in
deleted file mode 100644
index 5c792a828..000000000
--- a/ant/windows/windows-packager.nsi.in
+++ /dev/null
@@ -1,234 +0,0 @@
-!include MUI2.nsh
-!include x64.nsh
-!include LogicLib.nsh
-
-!ifdef NSIS_UNICODE
- !addplugindir "${windows.nsis.addons}/Plugins/Release_Unicode"
-!else
- !addplugindir "${windows.nsis.addons}/Plugins/Release_ANSI"
-!endif
-
-!addincludedir "${windows.nsis.addons}/Include/"
-!include StdUtils.nsh
-
-Name "${project.name}"
-OutFile "${out.dir}\${project.filename}${build.type}-${build.version}.exe"
-RequestExecutionLevel admin
-
-;-------------------------------
-
-!define MUI_ICON "${basedir}\${branding.dir}\${windows.icon}"
-
-!insertmacro MUI_PAGE_WELCOME
-!insertmacro MUI_PAGE_DIRECTORY
-!insertmacro MUI_PAGE_INSTFILES
-
-!insertmacro MUI_UNPAGE_CONFIRM
-!insertmacro MUI_UNPAGE_INSTFILES
-
-!insertmacro MUI_LANGUAGE "English"
-
-;------------------------------
-
-Section
- ; Sets the context of shell folders to "All Users"
- SetShellVarContext all
-
- ; Kills any running ${project.name} processes
- nsExec::ExecToLog "wmic.exe process where $\"Name like '%java%' and CommandLine like '%${project.filename}.jar%'$\" call terminate"
-
- ; Set environmental variable for silent install to be picked up by cscript
- ${If} ${Silent}
- System::Call 'Kernel32::SetEnvironmentVariable(t, t)i ("${vendor.name}_silent", "1").r0'
- ${EndIf}
-
- ; Cleanup for wmic on Windows XP
- SetShellVarContext current
- Delete "$DESKTOP\TempWmicBatchFile.bat"
- SetShellVarContext all
-
- SetOutPath "$INSTDIR"
-
- ; Cleanup resources from previous versions
- DetailPrint "Cleaning up resources from previous versions..."
- RMDir /r "$INSTDIR\demo\js\3rdparty"
- Delete "$INSTDIR\demo\js\qz-websocket.js"
-
- ; Remove 2.1 startup entry
- Delete "$SMSTARTUP\${project.name}.lnk"
-
- File /r "${dist.dir}\*"
-
- WriteRegStr HKLM "Software\${project.name}" \
- "" "$INSTDIR"
- WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "DisplayName" "${project.name} ${build.version}"
- WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "Publisher" "${vendor.company}"
- WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
- WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "DisplayIcon" "$INSTDIR\${windows.icon}"
- WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "HelpLink" "${vendor.website}/support"
- WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "URLUpdateInfo" "${vendor.website}/download"
- WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "URLInfoAbout" "${vendor.website}/support"
- WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "DisplayVersion" "${build.version}"
- WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}" \
- "EstimatedSize" "${build.size}"
-
- ; Allow localhost connections for Microsoft Edge
- DetailPrint "Whitelisting loopback connections for Microsoft Edge..."
- nsExec::ExecToLog "CheckNetIsolation.exe LoopbackExempt -a -n=$\"Microsoft.MicrosoftEdge_8wekyb3d8bbwe$\""
-
- ; Mimetype support, e.g. ${vendor.name}:launch
- WriteRegStr HKCR "${vendor.name}" "" "URL:${project.name} Protocol"
- WriteRegStr HKCR "${vendor.name}" "URL Protocol" ""
- WriteRegStr HKCR "${vendor.name}\DefaultIcon" "" "$\"$INSTDIR\${windows.icon}$\",1"
- WriteRegStr HKCR "${vendor.name}\shell\open\command" "" "$\"$INSTDIR\${project.filename}.exe$\" $\"%1$\""
-
- WriteUninstaller "$INSTDIR\uninstall.exe"
-
- ; Prevent launching exe from SysWOW64
- ${If} ${RunningX64}
- ${DisableX64FSRedirection}
- ${EndIf}
-
- ; Handle edge-case where jscript support is unregistered
- nsExec::ExecToLog "regsvr32.exe /s $\"%systemroot%\system32\jscript.dll$\""
-
- ; Remove ${vendor.company} certificates
- nsExec::ExecToLog "cscript.exe //NoLogo //E:jscript $\"$INSTDIR\auth\${windows.keygen.name}$\" $\"$INSTDIR$\" uninstall"
-
- ; Perform cleanup operations from previous install
- nsExec::ExecToLog "cscript.exe //NoLogo //E:jscript $\"$INSTDIR\utils\${windows.cleanup.name}$\" $\"${project.name}$\""
-
- keygen:
- ; Exports a self-signed certificate and properties file
- DetailPrint "Generating a unique certificate for HTTPS support..."
- nsExec::ExecToLog "cscript.exe //NoLogo //E:jscript $\"$INSTDIR\auth\${windows.keygen.name}$\" $\"$INSTDIR$\" install"
- Pop $0
-
- ; Secure websockets is required, handle errors
- ${If} "$0" != "0"
- ${If} "$0" == "${windows.err.java}"
- MessageBox MB_YESNO "Java is required for installation. Download now?" IDYES true IDNO false
- true:
- ExecShell "open" "${java.download}"
- MessageBox MB_OK "Click OK after Java is installed to resume installation"
- Goto keygen
- false:
- SetErrorLevel $0
- Abort "Failed while checking for Java ${javac.source}"
- ${Else}
- Abort "Installation failed. Please check log for details."
- ${EndIf}
- ${EndIf}
-
- ${If} ${RunningX64}
- ${EnableX64FSRedirection}
- ${EndIf}
-
- CreateShortCut "$SMPROGRAMS\${project.name}.lnk" "$INSTDIR\${project.filename}.exe" "" "$INSTDIR\${windows.icon}" 0
- CreateShortCut "$SMSTARTUP\${project.name}.lnk" "$INSTDIR\${project.filename}.exe" "-A" "$INSTDIR\${windows.icon}" 0
-
- ; Shared directory
- ReadEnvStr $R0 "PROGRAMDATA"
- CreateDirectory "$R0\${vendor.name}"
-
- ; Grant R+W to Authenticated Users (S-1-5-11)
- AccessControl::GrantOnFile "$R0\${vendor.name}" "(S-1-5-11)" "GenericRead + GenericWrite"
-
- ; Delete matching firewall rules
- DetailPrint "Removing ${project.name} firewall rules..."
- nsExec::ExecToLog "netsh.exe advfirewall firewall delete rule name= $\"${project.name}$\""
-
- ; Install new Firewall rules
- DetailPrint "Installing ${project.name} inbound firewall rule..."
- nsExec::ExecToLog "netsh.exe advfirewall firewall add rule name=$\"${project.name}$\" dir=in action=allow profile=any localport=8181,8282,8383,8484,8182,8283,8384,8485 localip=any protocol=tcp"
-
- ; Launch a non-elevated instance of ${project.name}
- ${StdUtils.ExecShellAsUser} $0 "$SMPROGRAMS\${project.name}.lnk" "open" ""
-SectionEnd
-
-;-------------------------------
-
-Section "Uninstall"
- ; Sets the context of shell folders to "All Users"
- SetShellVarContext all
-
- ; Kills any running ${project.name} processes
- nsExec::ExecToLog "wmic.exe process where $\"Name like '%java%' and CommandLine like '%${project.filename}.jar%'$\" call terminate"
-
- ; Set environmental variable for silent install to be picked up by cscript
- ${If} ${Silent}
- System::Call 'Kernel32::SetEnvironmentVariable(t, t)i ("${vendor.name}_silent", "1").r0'
- ${EndIf}
-
- ; Cleanup for wmic on Windows XP
- Delete "$DESKTOP\TempWmicBatchFile.bat"
-
- ; Prevent launching exe from SysWOW64
- ${If} ${RunningX64}
- ${DisableX64FSRedirection}
- ${EndIf}
-
- ; Remove ${vendor.company} certificates
- nsExec::ExecToLog "cscript.exe //NoLogo //E:jscript $\"$INSTDIR\auth\${windows.keygen.name}$\" $\"$INSTDIR$\" uninstall"
-
- ${If} ${RunningX64}
- ${EnableX64FSRedirection}
- ${EndIf}
-
- ; Remove startup entries
- nsExec::ExecToLog "cscript.exe //NoLogo //E:jscript $\"$INSTDIR\utils\${windows.cleanup.name}$\" $\"${project.name}$\""
-
- ; Delete matching firewall rules
- DetailPrint "Removing ${project.name} firewall rules..."
- nsExec::ExecToLog "netsh.exe advfirewall firewall delete rule name= $\"${project.name}$\""
-
- Delete "$SMPROGRAMS\${project.name}.lnk"
- Delete "$INSTDIR\uninstall.exe"
- RMDir /r "$INSTDIR"
-
- DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${project.name}"
- DeleteRegKey HKLM "Software\${project.name}"
-
- ; Remove URI handler
- DeleteRegKey HKCR "${vendor.name}"
-
- Delete "$DESKTOP\${project.name}.url"
- Delete "$DESKTOP\${project.name}.lnk"
- Delete "$SMSTARTUP\${project.name}.lnk"
-
- ; Sets the context of shell folders to current user
- SetShellVarContext current
- Delete "$DESKTOP\${project.name}.url"
- Delete "$DESKTOP\${project.name}.lnk"
- Delete "$SMPROGRAMS\${project.name}.lnk"
-SectionEnd
-
-;-------------------------------
-
-Function .onInit
- ${If} ${RunningX64}
- SetRegView 64
- ${EndIf}
- ${If} $InstDir == ""
- ${If} ${RunningX64}
- StrCpy $INSTDIR "$PROGRAMFILES64\${project.name}"
- ${Else}
- StrCpy $INSTDIR "$PROGRAMFILES\${project.name}"
- ${EndIf}
- ${EndIf}
-FunctionEnd
-
-Function un.onInit
- ${If} ${RunningX64}
- SetRegView 64
- ${EndIf}
-FunctionEnd
-
diff --git a/ant/windows/windows.properties b/ant/windows/windows.properties
deleted file mode 100644
index 13e9d54c9..000000000
--- a/ant/windows/windows.properties
+++ /dev/null
@@ -1,28 +0,0 @@
-# Windows build properties
-windows.icon=windows-icon.ico
-
-windows.keygen.store=Root
-windows.keygen.name=windows-keygen.js
-windows.keygen.in=${basedir}/ant/windows/${windows.keygen.name}.in
-windows.keygen.out=${dist.dir}/auth/${windows.keygen.name}
-windows.jsonparser.name=windows-json-parser.js
-windows.jsonparser.in=${basedir}/ant/windows/${windows.jsonparser.name}
-windows.jsonparser.out=${dist.dir}/auth/${windows.jsonparser.name}
-
-windows.keygen.tool=certutil.exe
-windows.keygen.install=${windows.keygen.tool} -addstore -f \\"${windows.keygen.store}\\" \\"${ca.crt}\\"
-windows.keygen.uninstall=${windows.keygen.tool} -delstore \\"${windows.keygen.store}\\" \\"!match\\"
-
-windows.nsis.addons=${basedir}/ant/windows/nsis
-windows.packager.in=${basedir}/ant/windows/windows-packager.nsi.in
-windows.packager.out=${build.dir}/windows-packager.nsi
-windows.cleanup.name=windows-cleanup.js
-windows.cleanup.in=${basedir}/ant/windows/${windows.cleanup.name}
-windows.cleanup.out=${dist.dir}/utils/${windows.cleanup.name}
-windows.launcher.in=${basedir}/ant/windows/windows-launcher.nsi.in
-windows.launcher.out=${build.dir}/windows-launcher.nsi
-
-# jscript/nsis shared error codes
-windows.err.java=2
-windows.err.install=4
-
diff --git a/assets/qz-print.properties b/assets/qz-print.properties
deleted file mode 100644
index 4f713b12d..000000000
--- a/assets/qz-print.properties
+++ /dev/null
@@ -1,10 +0,0 @@
-#
-# These values are needed to build the project properly.
-#
-# This is a sample file. The version used by the build files should reference
-# a copy of this file on your local machine. Do not check in changes to this
-# file into GIT.
-#
-
-jdk.home={path to the JDK}
-build.version=1.6
\ No newline at end of file
diff --git a/build.xml b/build.xml
index 95f3ef8cb..2f319adc8 100644
--- a/build.xml
+++ b/build.xml
@@ -16,7 +16,7 @@
-
+
@@ -46,24 +46,24 @@
-
+
-
+
-
+
-
+
@@ -74,7 +74,7 @@
-
+
@@ -86,18 +86,18 @@
Building Jar for Socket use
-
-
+
+
-
+
-
-
-
+
+
+
@@ -112,8 +112,8 @@
Self-signing Socket jar
-
Signing Socket jar with timestamp
-
-
+
@@ -160,67 +160,50 @@
+
Bundling with manual cert for signing auth: ${authcert.use}
-
+
+
Copying resource files to output
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
- Processing self-signing variables
-
-
- Creating Firefox certificate config files
-
-
-
-
-
-
-
-
-
-
@@ -283,10 +251,11 @@
+
- Signing Windows Executable: No tsaurl was provided so this exe was not timestamped. Users will not be able to validate this exe after the signer certificate's expiration date or after any future revocation date.
+ Signing Windows installer: No tsaurl was provided so this exe was not timestamped. Users will not be able to validate this exe after the signer certificate's expiration date or after any future revocation date.
- Signing Windows Executable:
+ Signing Windows installer
-
+
Creating installer using pkgbuild
@@ -332,76 +301,97 @@
###################################
-->
-
-
-
+
+
-
-
-
+
+
-
-
-
+
-
+
-
-
-
-
-
-
+
-
+
-
-
+
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
+
-
-
-
+
+
+
@@ -436,7 +426,7 @@
-
+
@@ -481,59 +471,40 @@
-
+
Creating installer using makeself
-
-
-
-
-
-
+
-
+
+
-
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
diff --git a/lib/websocket/javax-websocket-server-impl-9.2.14.v20151106.jar b/lib/websocket/javax-websocket-server-impl-9.2.14.v20151106.jar
deleted file mode 100644
index fdb7a1d55..000000000
Binary files a/lib/websocket/javax-websocket-server-impl-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/javax-websocket-server-impl-9.4.21.v20190926.jar b/lib/websocket/javax-websocket-server-impl-9.4.21.v20190926.jar
new file mode 100644
index 000000000..9afa1ca91
Binary files /dev/null and b/lib/websocket/javax-websocket-server-impl-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/jettison-1.3.3-SNAPSHOT.jar b/lib/websocket/jettison-1.3.3-SNAPSHOT.jar
new file mode 100644
index 000000000..33f97ee0a
Binary files /dev/null and b/lib/websocket/jettison-1.3.3-SNAPSHOT.jar differ
diff --git a/lib/websocket/jettison-1.3.3.jar b/lib/websocket/jettison-1.3.3.jar
deleted file mode 100644
index 332a475cc..000000000
Binary files a/lib/websocket/jettison-1.3.3.jar and /dev/null differ
diff --git a/lib/websocket/jetty-client-9.4.21.v20190926.jar b/lib/websocket/jetty-client-9.4.21.v20190926.jar
new file mode 100644
index 000000000..3be7bb4e7
Binary files /dev/null and b/lib/websocket/jetty-client-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/jetty-http-9.2.14.v20151106.jar b/lib/websocket/jetty-http-9.2.14.v20151106.jar
deleted file mode 100644
index a5e3db77f..000000000
Binary files a/lib/websocket/jetty-http-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/jetty-http-9.4.21.v20190926.jar b/lib/websocket/jetty-http-9.4.21.v20190926.jar
new file mode 100644
index 000000000..ccb18c179
Binary files /dev/null and b/lib/websocket/jetty-http-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/jetty-io-9.2.14.v20151106.jar b/lib/websocket/jetty-io-9.2.14.v20151106.jar
deleted file mode 100644
index 8517a5fe5..000000000
Binary files a/lib/websocket/jetty-io-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/jetty-io-9.4.21.v20190926.jar b/lib/websocket/jetty-io-9.4.21.v20190926.jar
new file mode 100644
index 000000000..ed80e0d49
Binary files /dev/null and b/lib/websocket/jetty-io-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/jetty-security-9.2.14.v20151106.jar b/lib/websocket/jetty-security-9.2.14.v20151106.jar
deleted file mode 100644
index 6795140ec..000000000
Binary files a/lib/websocket/jetty-security-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/jetty-security-9.4.21.v20190926.jar b/lib/websocket/jetty-security-9.4.21.v20190926.jar
new file mode 100644
index 000000000..bb11aed05
Binary files /dev/null and b/lib/websocket/jetty-security-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/jetty-server-9.2.14.v20151106.jar b/lib/websocket/jetty-server-9.2.14.v20151106.jar
deleted file mode 100644
index 9eb9c6fb9..000000000
Binary files a/lib/websocket/jetty-server-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/jetty-server-9.4.21.v20190926.jar b/lib/websocket/jetty-server-9.4.21.v20190926.jar
new file mode 100644
index 000000000..de1d9bb1c
Binary files /dev/null and b/lib/websocket/jetty-server-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/jetty-servlet-9.2.14.v20151106.jar b/lib/websocket/jetty-servlet-9.2.14.v20151106.jar
deleted file mode 100644
index 137723292..000000000
Binary files a/lib/websocket/jetty-servlet-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/jetty-servlet-9.4.21.v20190926.jar b/lib/websocket/jetty-servlet-9.4.21.v20190926.jar
new file mode 100644
index 000000000..01e9b4617
Binary files /dev/null and b/lib/websocket/jetty-servlet-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/jetty-util-9.2.14.v20151106.jar b/lib/websocket/jetty-util-9.2.14.v20151106.jar
deleted file mode 100644
index 0a33a9a1d..000000000
Binary files a/lib/websocket/jetty-util-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/jetty-util-9.4.21.v20190926.jar b/lib/websocket/jetty-util-9.4.21.v20190926.jar
new file mode 100644
index 000000000..c6745d581
Binary files /dev/null and b/lib/websocket/jetty-util-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/not-going-to-be-commons-ssl-0.3.20.jar b/lib/websocket/not-going-to-be-commons-ssl-0.3.20.jar
new file mode 100644
index 000000000..54bf4aa84
Binary files /dev/null and b/lib/websocket/not-going-to-be-commons-ssl-0.3.20.jar differ
diff --git a/lib/websocket/not-yet-commons-ssl-0.3.16.jar b/lib/websocket/not-yet-commons-ssl-0.3.16.jar
deleted file mode 100644
index 71fd59e4e..000000000
Binary files a/lib/websocket/not-yet-commons-ssl-0.3.16.jar and /dev/null differ
diff --git a/lib/websocket/websocket-api-9.2.14.v20151106.jar b/lib/websocket/websocket-api-9.2.14.v20151106.jar
deleted file mode 100644
index 710cca016..000000000
Binary files a/lib/websocket/websocket-api-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/websocket-api-9.4.21.v20190926.jar b/lib/websocket/websocket-api-9.4.21.v20190926.jar
new file mode 100644
index 000000000..b3dd4cc9a
Binary files /dev/null and b/lib/websocket/websocket-api-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/websocket-client-9.2.14.v20151106.jar b/lib/websocket/websocket-client-9.2.14.v20151106.jar
deleted file mode 100644
index 347e5728d..000000000
Binary files a/lib/websocket/websocket-client-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/websocket-client-9.4.21.v20190926.jar b/lib/websocket/websocket-client-9.4.21.v20190926.jar
new file mode 100644
index 000000000..4443fdbc9
Binary files /dev/null and b/lib/websocket/websocket-client-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/websocket-common-9.2.14.v20151106.jar b/lib/websocket/websocket-common-9.2.14.v20151106.jar
deleted file mode 100644
index a12190153..000000000
Binary files a/lib/websocket/websocket-common-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/websocket-common-9.4.21.v20190926.jar b/lib/websocket/websocket-common-9.4.21.v20190926.jar
new file mode 100644
index 000000000..cb93191d9
Binary files /dev/null and b/lib/websocket/websocket-common-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/websocket-server-9.2.14.v20151106.jar b/lib/websocket/websocket-server-9.2.14.v20151106.jar
deleted file mode 100644
index ef2c89c6a..000000000
Binary files a/lib/websocket/websocket-server-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/websocket-server-9.4.21.v20190926.jar b/lib/websocket/websocket-server-9.4.21.v20190926.jar
new file mode 100644
index 000000000..27a40e7dc
Binary files /dev/null and b/lib/websocket/websocket-server-9.4.21.v20190926.jar differ
diff --git a/lib/websocket/websocket-servlet-9.2.14.v20151106.jar b/lib/websocket/websocket-servlet-9.2.14.v20151106.jar
deleted file mode 100644
index 9f48be20b..000000000
Binary files a/lib/websocket/websocket-servlet-9.2.14.v20151106.jar and /dev/null differ
diff --git a/lib/websocket/websocket-servlet-9.4.21.v20190926.jar b/lib/websocket/websocket-servlet-9.4.21.v20190926.jar
new file mode 100644
index 000000000..a3350260b
Binary files /dev/null and b/lib/websocket/websocket-servlet-9.4.21.v20190926.jar differ
diff --git a/src/qz/auth/Certificate.java b/src/qz/auth/Certificate.java
index e5a83270e..ce7fb506e 100644
--- a/src/qz/auth/Certificate.java
+++ b/src/qz/auth/Certificate.java
@@ -13,6 +13,7 @@
import qz.common.Constants;
import qz.utils.ByteUtilities;
import qz.utils.FileUtilities;
+import qz.utils.SystemUtilities;
import qz.ws.PrintSocketServer;
import javax.security.cert.CertificateParsingException;
@@ -92,23 +93,7 @@ public enum Algorithm {
static {
try {
Security.addProvider(new BouncyCastleProvider());
-
- String overridePath;
- Properties trayProperties = PrintSocketServer.getTrayProperties();
- if (trayProperties != null && trayProperties.containsKey("authcert.override")) {
- overridePath = trayProperties.getProperty("authcert.override");
- } else {
- overridePath = System.getProperty("trustedRootCert");
- }
- if (overridePath != null) {
- try {
- trustedRootCert = new Certificate(FileUtilities.readLocalFile(overridePath));
- overrideTrustedRootCert = true;
- }
- catch(IOException e) {
- e.printStackTrace();
- }
- }
+ checkOverrideCertPath();
if (trustedRootCert == null) {
trustedRootCert = new Certificate("-----BEGIN CERTIFICATE-----\n" +
@@ -150,6 +135,52 @@ public enum Algorithm {
}
+ private static void checkOverrideCertPath() {
+ // Priority: Check environmental variable
+ String override = System.getProperty("trustedRootCert");
+ String helpText = "System property \"trustedRootCert\"";
+ if(setOverrideCert(override, helpText, false)) {
+ return;
+ }
+
+ // Preferred: Look for file called "override.crt" in installation directory
+ override = FileUtilities.getParentDirectory(SystemUtilities.getJarPath()) + File.separator + Constants.OVERRIDE_CERT;
+ helpText = String.format("Override cert \"%s\"", Constants.OVERRIDE_CERT);
+ if(setOverrideCert(override, helpText, true)) {
+ return;
+ }
+
+ // Fallback (deprecated): Parse "authcert.override" from qz-tray.properties
+ // Entry was created by 2.0 build system, removed in newer versions in favor of the hard-coded filename
+ Properties props = PrintSocketServer.getTrayProperties();
+ helpText = "Properties file entry \"authcert.override\"";
+ if(props != null && setOverrideCert(props.getProperty("authcert.override"), helpText, false)) {
+ log.warn("Deprecation warning: \"authcert.override\" is no longer supported.\n" +
+ "{} will look for the system property \"trustedRootCert\", or look for" +
+ "a file called {} in the working path.", Constants.ABOUT_TITLE, Constants.OVERRIDE_CERT);
+ return;
+ }
+ }
+
+ private static boolean setOverrideCert(String path, String helpText, boolean quiet) {
+ if(path != null && !path.trim().isEmpty()) {
+ if (new File(path).exists()) {
+ try {
+ log.error("Using override cert: {}", path);
+ trustedRootCert = new Certificate(FileUtilities.readLocalFile(path));
+ overrideTrustedRootCert = true;
+ return true;
+ }
+ catch(Exception e) {
+ log.error("Error loading override cert: {}", path, e);
+ }
+ } else if(!quiet) {
+ log.warn("{} \"{}\" was provided, but could not be found, skipping.", helpText, path);
+ }
+ }
+ return false;
+ }
+
/** Decodes a certificate and intermediate certificate from the given string */
@SuppressWarnings("deprecation")
public Certificate(String in) throws CertificateParsingException {
diff --git a/src/qz/common/AboutInfo.java b/src/qz/common/AboutInfo.java
index f04556dd7..3389e3a63 100644
--- a/src/qz/common/AboutInfo.java
+++ b/src/qz/common/AboutInfo.java
@@ -12,6 +12,8 @@
import org.codehaus.jettison.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import qz.installer.certificate.KeyPairWrapper;
+import qz.installer.certificate.CertificateManager;
import qz.utils.SystemUtilities;
import qz.ws.PrintSocketServer;
@@ -22,8 +24,6 @@
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
@@ -36,16 +36,14 @@ public class AboutInfo {
private static String preferredHostname = "localhost";
- public static JSONObject gatherAbout(String domain) {
+ public static JSONObject gatherAbout(String domain, CertificateManager certificateManager) {
JSONObject about = new JSONObject();
- KeyStore keyStore = SecurityInfo.getKeyStore(PrintSocketServer.getTrayProperties());
-
try {
about.put("product", product());
- about.put("socket", socket(keyStore, domain));
+ about.put("socket", socket(certificateManager, domain));
about.put("environment", environment());
- about.put("ssl", ssl(keyStore));
+ about.put("ssl", ssl(certificateManager));
about.put("libraries", libraries());
}
catch(JSONException | GeneralSecurityException e) {
@@ -67,13 +65,13 @@ private static JSONObject product() throws JSONException {
return product;
}
- private static JSONObject socket(KeyStore keystore, String domain) throws JSONException {
+ private static JSONObject socket(CertificateManager certificateManager, String domain) throws JSONException {
JSONObject socket = new JSONObject();
socket
.put("domain", domain)
.put("secureProtocol", "wss")
- .put("securePort", keystore == null? "none":PrintSocketServer.getSecurePortInUse())
+ .put("securePort", certificateManager.isSslActive() ? PrintSocketServer.getSecurePortInUse() : "none")
.put("insecureProtocol", "ws")
.put("insecurePort", PrintSocketServer.getInsecurePortInUse());
@@ -94,34 +92,27 @@ private static JSONObject environment() throws JSONException {
return environment;
}
- private static JSONObject ssl(KeyStore keystore) throws JSONException, KeyStoreException, CertificateEncodingException {
+ private static JSONObject ssl(CertificateManager certificateManager) throws JSONException, CertificateEncodingException {
JSONObject ssl = new JSONObject();
JSONArray certs = new JSONArray();
- if (keystore != null) {
- Enumeration aliases = keystore.aliases();
- while(aliases.hasMoreElements()) {
- String alias = aliases.nextElement();
- if ("X.509".equals(keystore.getCertificate(alias).getType())) {
- JSONObject cert = new JSONObject();
- X509Certificate x509 = (X509Certificate)keystore.getCertificate(alias);
- cert.put("alias", alias);
- 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);
- }
- String cn = x509.getSubjectX500Principal().getName();
- if (!cert.getBoolean("rootca")) {
- preferredHostname = cn;
- }
- cert.put("subject", cn);
- cert.put("expires", toISO(x509.getNotAfter()));
- cert.put("data", formatCert(x509.getEncoded()));
- certs.put(cert);
+
+ 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", toISO(x509.getNotAfter()));
+ cert.put("data", formatCert(x509.getEncoded()));
+ certs.put(cert);
}
}
ssl.put("certificates", certs);
diff --git a/src/qz/common/Constants.java b/src/qz/common/Constants.java
index d427cec2c..280e0abbf 100644
--- a/src/qz/common/Constants.java
+++ b/src/qz/common/Constants.java
@@ -12,7 +12,7 @@ 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.1.0-RC7");
+ public static final Version VERSION = Version.valueOf("2.1.0-RC9");
public static final Version JAVA_VERSION = SystemUtilities.getJavaVersion();
public static final String JAVA_VENDOR = System.getProperty("java.vendor");
@@ -25,18 +25,27 @@ public class Constants {
public static final String PREFS_FILE = "prefs"; // .properties extension is assumed
public static final String AUTOSTART_FILE = ".autostart";
public static final String DATA_DIR = "qz";
- public static final String SHARED_DATA_DIR = "shared";
public static final int LOG_SIZE = 524288;
public static final int LOG_ROTATIONS = 5;
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 UNTRUSTED_CERT = "Untrusted website";
@@ -57,6 +66,8 @@ public class Constants {
public static final String ALLOWED = "Allowed";
public static final String BLOCKED = "Blocked";
+ public static final String OVERRIDE_CERT = "override.crt";
+
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;
@@ -78,8 +89,4 @@ public class Constants {
public static final Integer[] WSS_PORTS = {8181, 8282, 8383, 8484};
public static final Integer[] WS_PORTS = {8182, 8283, 8384, 8485};
public static final Integer[] CUPS_RSS_PORTS = {8586, 8687, 8788, 8889};
-
- public static final String SANDBOX_DIR = "/sandbox";
- public static final String NOT_SANDBOX_DIR = "/shared";
- public static final int FILE_LISTENER_DEFAULT_LINES = 10;
}
diff --git a/src/qz/common/SecurityInfo.java b/src/qz/common/SecurityInfo.java
index d395f59a3..660d1ff05 100644
--- a/src/qz/common/SecurityInfo.java
+++ b/src/qz/common/SecurityInfo.java
@@ -6,7 +6,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import purejavahidapi.PureJavaHidApi;
-import qz.deploy.DeployUtilities;
+import qz.utils.SystemUtilities;
import java.io.FileInputStream;
import java.io.IOException;
@@ -69,7 +69,7 @@ public static SortedMap getLibVersions() {
Method method = VersionInfo.getMethod("getVersion");
Object version = method.invoke(null);
libVersions.put("javafx", (String)version);
- if (fxPath.contains(DeployUtilities.detectJarPath()) || fxPath.contains("/tray/")) {
+ if (fxPath.contains(SystemUtilities.detectJarPath()) || fxPath.contains("/tray/")) {
libVersions.put("javafx (location)", "Bundled/" + Constants.ABOUT_TITLE);
} else {
libVersions.put("javafx (location)", "System/" + Constants.JAVA_VENDOR);
@@ -96,6 +96,19 @@ public static SortedMap getLibVersions() {
return libVersions;
}
+ public static void printLibInfo() {
+ String format = "%-40s%s%n";
+ System.out.printf(format, "LIBRARY NAME:", "VERSION:");
+ SortedMap libVersions = SecurityInfo.getLibVersions();
+ for(Map.Entry 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
*
diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java
index 593d726af..37ed278fe 100644
--- a/src/qz/common/TrayManager.java
+++ b/src/qz/common/TrayManager.java
@@ -17,9 +17,8 @@
import org.slf4j.LoggerFactory;
import qz.auth.Certificate;
import qz.auth.RequestState;
-import qz.deploy.DeployUtilities;
-import qz.deploy.LinuxCertificate;
-import qz.deploy.WindowsDeploy;
+import qz.installer.WindowsInstaller;
+import qz.installer.shortcut.ShortcutCreator;
import qz.ui.*;
import qz.ui.component.IconCache;
import qz.ui.tray.TrayType;
@@ -69,7 +68,7 @@ public class TrayManager {
private final String name;
// The shortcut and startup helper
- private final DeployUtilities shortcutCreator;
+ private final ShortcutCreator shortcutCreator;
private final PropertyHelper prefs;
@@ -86,7 +85,7 @@ public TrayManager() {
public TrayManager(boolean isHeadless) {
name = Constants.ABOUT_TITLE + " " + Constants.VERSION;
- prefs = new PropertyHelper(SystemUtilities.getDataDirectory() + File.separator + Constants.PREFS_FILE + ".properties");
+ prefs = new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
//headless if turned on by user or unsupported by environment
headless = isHeadless || prefs.getBoolean(Constants.PREFS_HEADLESS, false) || GraphicsEnvironment.isHeadless();
@@ -95,8 +94,7 @@ public TrayManager(boolean isHeadless) {
}
// Setup the shortcut name so that the UI components can use it
- shortcutCreator = DeployUtilities.getSystemShortcutCreator();
- shortcutCreator.setShortcutName(Constants.ABOUT_TITLE);
+ shortcutCreator = ShortcutCreator.getInstance();
SystemUtilities.setSystemLookAndFeel();
iconCache = new IconCache();
@@ -140,14 +138,6 @@ public TrayManager(boolean isHeadless) {
// Update printer list in CUPS immediately (normally 2min)
System.setProperty("sun.java2d.print.polling", "false");
}
- if (SystemUtilities.isLinux()) {
- // Install cert into user's nssdb for Chrome, etc
- LinuxCertificate.installCertificate();
- } else if (SystemUtilities.isWindows()) {
- // Configure IE intranet zone via registry to allow websockets
- WindowsDeploy.configureIntranetZone();
- WindowsDeploy.configureEdgeLoopback();
- }
if (!headless) {
componentList = new ArrayList<>();
@@ -221,40 +211,68 @@ private void addMenuItems() {
sitesDialog = new SiteManagerDialog(sitesItem, iconCache);
componentList.add(sitesDialog);
- anonymousItem = new JCheckBoxMenuItem("Block Anonymous Requests");
- anonymousItem.setToolTipText("Blocks all requests that do no contain a valid certificate/signature");
- anonymousItem.setMnemonic(KeyEvent.VK_K);
- anonymousItem.setState(Certificate.UNKNOWN.isBlocked());
- anonymousItem.addActionListener(anonymousListener);
+ JMenuItem diagnosticMenu = new JMenu("Diagnostic");
- JMenuItem logItem = new JMenuItem("View Logs...", iconCache.getIcon(IconCache.Icon.LOG_ICON));
- logItem.setMnemonic(KeyEvent.VK_L);
- logItem.addActionListener(logListener);
- logDialog = new LogDialog(logItem, iconCache);
- componentList.add(logDialog);
+ JMenuItem browseApp = new JMenuItem("Browse App folder...", iconCache.getIcon(IconCache.Icon.FOLDER_ICON));
+ browseApp.setToolTipText(FileUtilities.getParentDirectory(SystemUtilities.getJarPath()));
+ browseApp.setMnemonic(KeyEvent.VK_O);
+ browseApp.addActionListener(e -> ShellUtilities.browseAppDirectory());
+ diagnosticMenu.add(browseApp);
+
+ JMenuItem browseUser = new JMenuItem("Browse User folder...", iconCache.getIcon(IconCache.Icon.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(IconCache.Icon.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(prefs.getBoolean(Constants.PREFS_NOTIFICATIONS, false));
notificationsItem.addActionListener(notificationsListener);
+ diagnosticMenu.add(notificationsItem);
- JMenuItem openItem = new JMenuItem("Open file location", iconCache.getIcon(IconCache.Icon.FOLDER_ICON));
- openItem.setMnemonic(KeyEvent.VK_O);
- openItem.addActionListener(openListener);
+ diagnosticMenu.add(new JSeparator());
+
+ JMenuItem logItem = new JMenuItem("View logs (live feed)...", iconCache.getIcon(IconCache.Icon.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(IconCache.Icon.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(anonymousItem);
- advancedMenu.add(logItem);
- advancedMenu.add(notificationsItem);
- advancedMenu.add(new JSeparator());
- advancedMenu.add(openItem);
advancedMenu.add(desktopItem);
-
+ advancedMenu.add(new JSeparator());
+ advancedMenu.add(anonymousItem);
JMenuItem reloadItem = new JMenuItem("Reload", iconCache.getIcon(IconCache.Icon.RELOAD_ICON));
reloadItem.setMnemonic(KeyEvent.VK_R);
@@ -275,7 +293,7 @@ private void addMenuItems() {
JCheckBoxMenuItem startupItem = new JCheckBoxMenuItem("Automatically start");
startupItem.setMnemonic(KeyEvent.VK_S);
- startupItem.setState(shortcutCreator.isAutostart());
+ startupItem.setState(FileUtilities.isAutostart());
startupItem.addActionListener(startupListener());
if (!shortcutCreator.canAutoStart()) {
startupItem.setEnabled(false);
@@ -302,21 +320,7 @@ private void addMenuItems() {
private final ActionListener notificationsListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
- JCheckBoxMenuItem j = (JCheckBoxMenuItem)e.getSource();
- prefs.setProperty(Constants.PREFS_NOTIFICATIONS, j.getState());
- }
- };
-
- private final ActionListener openListener = new ActionListener() {
- public void actionPerformed(ActionEvent e) {
- try {
- ShellUtilities.browseDirectory(shortcutCreator.getParentDirectory());
- }
- catch(Exception ex) {
- if (!SystemUtilities.isLinux() || !ShellUtilities.execute(new String[] {"xdg-open", shortcutCreator.getParentDirectory()})) {
- showErrorDialog("Sorry, unable to open the file browser: " + ex.getLocalizedMessage());
- }
- }
+ prefs.setProperty(Constants.PREFS_NOTIFICATIONS, ((JCheckBoxMenuItem)e.getSource()).getState());
}
};
@@ -361,12 +365,12 @@ private ActionListener startupListener() {
source.setState(true);
return;
}
- if (shortcutCreator.setAutostart(source.getState())) {
+ if (FileUtilities.setAutostart(source.getState())) {
displayInfoMessage("Successfully " + (source.getState() ? "enabled" : "disabled") + " autostart");
} else {
displayErrorMessage("Error " + (source.getState() ? "enabling" : "disabling") + " autostart");
}
- source.setState(shortcutCreator.isAutostart());
+ source.setState(FileUtilities.isAutostart());
};
}
diff --git a/src/qz/communication/FileIO.java b/src/qz/communication/FileIO.java
index 4844b3ef1..82bb22a31 100644
--- a/src/qz/communication/FileIO.java
+++ b/src/qz/communication/FileIO.java
@@ -3,7 +3,6 @@
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.eclipse.jetty.websocket.api.Session;
-import qz.common.Constants;
import qz.ws.PrintSocketClient;
import qz.ws.StreamEvent;
@@ -11,6 +10,9 @@
import java.nio.file.WatchKey;
public class FileIO {
+ public static final String SANDBOX_DATA_SUFFIX = "sandbox";
+ public static final String GLOBAL_DATA_SUFFIX = "shared";
+ public static final int FILE_LISTENER_DEFAULT_LINES = 10;
public enum ReadType {
BYTES, LINES
@@ -43,7 +45,7 @@ public FileIO(Session session, JSONObject params, Path originalPath, Path absolu
readType = ReadType.LINES;
}
- lines = options.optInt("lines", readType == ReadType.LINES? Constants.FILE_LISTENER_DEFAULT_LINES:-1);
+ lines = options.optInt("lines", readType == ReadType.LINES? FILE_LISTENER_DEFAULT_LINES:-1);
reversed = options.optBoolean("reverse", readType == ReadType.LINES);
}
}
diff --git a/src/qz/deploy/DeployUtilities.java b/src/qz/deploy/DeployUtilities.java
deleted file mode 100644
index 76ec3b03c..000000000
--- a/src/qz/deploy/DeployUtilities.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/**
- * @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.deploy;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import qz.common.Constants;
-import qz.utils.SystemUtilities;
-
-import java.io.*;
-import java.net.URLDecoder;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardOpenOption;
-import java.util.List;
-import java.util.Properties;
-
-/**
- * Utility class for creating, querying and removing startup shortcuts and
- * desktop shortcuts.
- *
- * @author Tres Finocchiaro
- */
-public abstract class DeployUtilities {
-
- // System logger
- protected static final Logger log = LoggerFactory.getLogger(DeployUtilities.class);
-
- // Default shortcut name to create
- static private final String DEFAULT_SHORTCUT_NAME = "Java Shortcut";
-
- private String jarPath;
- private String shortcutName;
-
- public boolean setAutostart(boolean autostart) {
- try {
- return writeAutoStartFile(autostart ? "1": "0");
- }
- catch(IOException e) {
- return false;
- }
- }
-
- public boolean isAutostart() {
- try {
- return "1".equals(readAutoStartFile());
- }
- catch(IOException e) {
- return false;
- }
- }
-
- public abstract boolean canAutoStart();
-
- /**
- * Creates a startup for the current OS. Automatically detects the OS and
- * places the shortcut item on the user's Desktop.
- *
- * @return Returns true
if the startup item was created
- */
- public abstract boolean createDesktopShortcut();
-
- /**
- * Parses the parent directory from an absolute file URL. This will not work
- * with relative paths.
- * // Good:
- * getWorkingPath("C:\Folder\MyFile.jar");
- *
- * // Bad:
- * getWorkingPath("C:\Folder\SubFolder\..\MyFile.jar");
- *
- *
- * @param filePath Absolute path to a jar file
- * @return The calculated working path value, or an empty string if one
- * could not be determined
- */
- private static String getParentDirectory(String filePath) {
- // Working path should always default to the JARs parent folder
- int lastSlash = filePath.lastIndexOf(File.separator);
- return lastSlash < 0? "":filePath.substring(0, lastSlash);
- }
-
- public String getParentDirectory() {
- return getParentDirectory(getJarPath());
- }
-
- public void setShortcutName(String shortcutName) {
- if (shortcutName != null) {
- this.shortcutName = shortcutName;
- }
- }
-
- public String getShortcutName() {
- return shortcutName == null? DEFAULT_SHORTCUT_NAME:shortcutName;
- }
-
- /**
- * Detects the OS and creates the appropriate shortcut creator
- *
- * @return The appropriate shortcut creator for the currently running OS
- */
- public static DeployUtilities getSystemShortcutCreator() {
- if (SystemUtilities.isWindows()) {
- return new WindowsDeploy();
- } else if (SystemUtilities.isMac()) {
- return new MacDeploy();
- } else {
- return new LinuxDeploy();
- }
- }
-
- private static boolean writeAutoStartFile(String mode) throws IOException {
- Path autostartFile = Paths.get(SystemUtilities.getDataDirectory() , Constants.AUTOSTART_FILE);
- Files.write(autostartFile, mode.getBytes(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
- return readAutoStartFile().equals(mode);
- }
-
- /**
- *
- * @return First line of ".autostart" file in user or shared space or "0" if blank. If neither are found, returns "1".
- * @throws IOException
- */
- private static String readAutoStartFile() throws IOException {
- log.debug("Checking for {} file in user directory {}...", Constants.AUTOSTART_FILE, SystemUtilities.getDataDirectory());
- Path userAutoStart = Paths.get(SystemUtilities.getDataDirectory() ,Constants.AUTOSTART_FILE);
- List lines = null;
- if (Files.exists(userAutoStart)) {
- lines = Files.readAllLines(userAutoStart);
- } else {
- log.debug("Checking for {} file in shared directory {}...", Constants.AUTOSTART_FILE, SystemUtilities.getDataDirectory());
- Path sharedAutoStart = Paths.get(SystemUtilities.getSharedDataDirectory(), Constants.AUTOSTART_FILE);
- if (Files.exists(sharedAutoStart)) {
- lines = Files.readAllLines(sharedAutoStart);
- }
- }
- if (lines == null) {
- log.info("File {} was not found in user or shared directory", Constants.AUTOSTART_FILE);
- // Default behavior is to autostart if no preference has been set
- return "1";
- } else if (lines.isEmpty()) {
- log.warn("File {} is empty, this shouldn't happen.", Constants.AUTOSTART_FILE);
- return "0";
- } else {
- String val = lines.get(0).trim();
- log.debug("File {} contains {}", Constants.AUTOSTART_FILE, val);
- return val;
- }
- }
-
- /**
- * Sets the executable permission flag for a file. This only works on
- * Linux/Unix.
- *
- * @param filePath The full file path to set the execute flag on
- * @return true
if successful, false
otherwise
- */
- @SuppressWarnings("ResultOfMethodCallIgnored")
- private static boolean setExecutable(String filePath) {
- if (!SystemUtilities.isWindows()) {
- try {
- File f = new File(filePath);
- f.setExecutable(true);
- return true;
- }
- catch(SecurityException e) {
- log.error("Unable to set file as executable: {}", filePath, e);
- }
- } else {
- return true;
- }
- return false;
- }
-
- /**
- * Gets the path to qz-tray.properties
- */
- private static String detectPropertiesPath() {
- // Use supplied path from IDE or command line
- // i.e -DsslPropertiesFile=C:\qz-tray.properties
- String override = System.getProperty("sslPropertiesFile");
- if (override != null) {
- return override;
- }
-
- String jarPath = detectJarPath();
- String propFile = Constants.PROPS_FILE + ".properties";
- return getParentDirectory(jarPath) + File.separator + propFile;
- }
-
- /**
- * Returns a properties object containing the SSL properties infor
- */
- public static Properties loadTrayProperties() {
- Properties trayProps = new Properties();
- String trayPropsPath = DeployUtilities.detectPropertiesPath();
- log.info("Main properties file " + trayPropsPath);
-
- File propsFile = new File(trayPropsPath);
- try(FileInputStream inputStream = new FileInputStream(propsFile)) {
- trayProps.load(inputStream);
- return trayProps;
- }
- catch(IOException e) {
- e.printStackTrace();
- log.warn("Failed to load properties file!");
- return null;
- }
- }
-
- /**
- * Determines the currently running Jar's absolute path on the local filesystem
- *
- * @return A String value representing the absolute path to the currently running
- * jar
- */
- public static String detectJarPath() {
- try {
- String jarPath = new File(DeployUtilities.class.getProtectionDomain()
- .getCodeSource().getLocation().getPath()).getCanonicalPath();
- // Fix characters that get URL encoded when calling getPath()
- return URLDecoder.decode(jarPath, "UTF-8");
- }
- catch(IOException ex) {
- log.error("Unable to determine Jar path", ex);
- }
- return null;
- }
-
- /**
- * Returns the jar which we will create a shortcut for
- *
- * @return The path to the jar path which has been set
- */
- public String getJarPath() {
- if (jarPath == null) {
- jarPath = detectJarPath();
- }
- return jarPath;
- }
-
- /**
- * Small Enum for differentiating "desktop" and "startup"
- */
- public enum ToggleType {
- STARTUP, DESKTOP;
-
- /**
- * Returns the English description of this object
- *
- * @return The string "startup" or "desktop"
- */
- @Override
- public String toString() {
- return getName();
- }
-
- /**
- * Returns the English description of this object
- *
- * @return The string "startup" or "desktop"
- */
- public String getName() {
- return this.name() == null? null:this.name().toLowerCase();
- }
- }
-}
diff --git a/src/qz/deploy/LinuxCertificate.java b/src/qz/deploy/LinuxCertificate.java
deleted file mode 100644
index 615169d50..000000000
--- a/src/qz/deploy/LinuxCertificate.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * @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.deploy;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import qz.common.Constants;
-import qz.utils.ShellUtilities;
-
-import java.util.Properties;
-
-/**
- * @author Tres Finocchiaro
- */
-public class LinuxCertificate {
-
- private static final Logger log = LoggerFactory.getLogger(LinuxCertificate.class);
-
- private static String nssdb = "sql:" + System.getenv("HOME") + "/.pki/nssdb";
-
- private static String getCertificatePath() {
- // We assume that if the keystore is "qz-tray.jks", the cert must be "root-ca.crt"
- Properties sslProperties = DeployUtilities.loadTrayProperties();
- if (sslProperties != null) {
- return sslProperties.getProperty("wss.keystore").replace(Constants.PROPS_FILE + ".jks", "root-ca.crt");
- }
-
- return null;
- }
-
- public static void installCertificate() {
- String certPath = getCertificatePath();
- String errMsg = "";
- boolean success = false;
- if (certPath != null) {
- String certutil = "certutil";
- success = ShellUtilities.execute(new String[] {
- certutil, "-d", nssdb, "-A", "-t", "TC", "-n", Constants.ABOUT_COMPANY, "-i", certPath
- });
-
- if (!success) {
- errMsg += "Error executing " + certutil +
- ". Ensure it is installed properly with write access to " + nssdb + ".";
- }
- } else {
- errMsg += "Unable to determine path to certificate.";
- }
-
- if (!success) {
- log.warn("{} Secure websockets will not function on certain browsers.", errMsg);
- }
- }
-}
diff --git a/src/qz/deploy/LinuxDeploy.java b/src/qz/deploy/LinuxDeploy.java
deleted file mode 100644
index 40898fa7c..000000000
--- a/src/qz/deploy/LinuxDeploy.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * @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.deploy;
-
-import java.awt.Toolkit;
-import java.lang.reflect.Field;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import qz.common.Constants;
-import qz.utils.*;
-
-/**
- * @author Tres Finocchiaro
- */
-class LinuxDeploy extends DeployUtilities {
-
- private static final Logger log = LoggerFactory.getLogger(LinuxDeploy.class);
-
- private static String STARTUP = "/etc/xdg/autostart/";
- private static String DESKTOP = System.getProperty("user.home") + "/Desktop/";
-
- private String appLauncher = "/usr/share/applications/" + getShortcutName();
-
- @Override
- public boolean canAutoStart() {
- return Files.exists(Paths.get(STARTUP, getShortcutName()));
- }
-
- @Override
- public boolean createDesktopShortcut() {
- return copyShortcut(appLauncher, DESKTOP);
- }
-
- private static boolean copyShortcut(String source, String target) {
- return ShellUtilities.execute(new String[] {
- "cp", source, target
- });
- }
-
- @Override
- public void setShortcutName(String name) {
- super.setShortcutName(name);
- // Fix window titles on Gnome 3 per JDK-6528430
- try {
- Toolkit t = Toolkit.getDefaultToolkit();
- Field f = t.getClass().getDeclaredField("awtAppClassName");
- f.setAccessible(true);
- f.set(t, name);
- }
- catch (Exception ignore) {}
- }
-
- @Override
- public String getShortcutName() {
- return Constants.PROPS_FILE + ".desktop";
- }
-}
-
diff --git a/src/qz/deploy/WindowsDeploy.java b/src/qz/deploy/WindowsDeploy.java
deleted file mode 100644
index 90de3387d..000000000
--- a/src/qz/deploy/WindowsDeploy.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * @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.deploy;
-
-import mslinks.ShellLink;
-import qz.utils.ShellUtilities;
-
-import java.io.IOException;
-import java.nio.file.*;
-
-/**
- * @author Tres Finocchiaro
- */
-public class WindowsDeploy extends DeployUtilities {
-
- @Override
- public boolean createDesktopShortcut() {
- return createShortcut(System.getenv("userprofile") + "\\Desktop\\");
- }
-
- @Override
- public boolean canAutoStart() {
- return Files.exists(Paths.get(getStartupDirectory(), getShortcutName() + ".lnk"));
- }
-
- /**
- * Remove flag "Include all local (intranet) sites not listed in other zones". Requires CheckNetIsolation
- * to be effective; Has no effect on domain networks with "Automatically detect intranet network" checked.
- *
- * @return true if successful
- */
- public static boolean configureIntranetZone() {
- String path = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones\\1";
- String name = "Flags";
- int value = 16;
-
- // If the above value is set, remove it using bitwise XOR, thus disabling this setting
- int data = ShellUtilities.getRegistryDWORD(path, name);
- return data != -1 && ((data & value) != value || ShellUtilities.setRegistryDWORD(path, name, data ^ value));
- }
-
- /**
- * Set legacy Edge flag: about:flags > Developer Settings > Allow localhost loopback
- *
- * @return true if successful
- */
- public static boolean configureEdgeLoopback() {
- String path = "HKCU\\Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge\\ExperimentalFeatures";
- String name = "AllowLocalhostLoopback";
- int value = 1;
-
- // If the above value does not exist, add it using bitwise OR, thus enabling this setting
- int data = ShellUtilities.getRegistryDWORD(path, name);
- return data != -1 && ((data & value) == value || ShellUtilities.setRegistryDWORD(path, name, data | value));
- }
-
- /**
- * Creates a Windows ".lnk" shortcut
- *
- * @param folderPath Absolute path to a jar file
- * @return Whether or not the shortcut was created successfully
- */
- private boolean createShortcut(String folderPath) {
- try {
- ShellLink.createLink(getAppPath(), folderPath + getShortcutName() + ".lnk");
- }
- catch(IOException ex) {
- log.warn("Error creating desktop shortcut", ex);
- return false;
- }
- return true;
- }
-
- /**
- * Returns path to executable jar or windows executable
- */
- private String getAppPath() {
- return getJarPath().replaceAll(".jar$", ".exe");
- }
-
-
- private static String getStartupDirectory() {
- if (System.getenv("programdata") == null) {
- // XP
- return System.getenv("allusersprofile") + "\\Start Menu\\Programs\\Startup\\";
- }
- return System.getenv("programdata") + "\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\";
- }
-}
diff --git a/src/qz/exception/MissingArgException.java b/src/qz/exception/MissingArgException.java
new file mode 100644
index 000000000..8d946e132
--- /dev/null
+++ b/src/qz/exception/MissingArgException.java
@@ -0,0 +1,3 @@
+package qz.exception;
+
+public class MissingArgException extends Exception {}
diff --git a/src/qz/exception/NullCommandException.java b/src/qz/exception/NullCommandException.java
index adabb723e..46314fd30 100644
--- a/src/qz/exception/NullCommandException.java
+++ b/src/qz/exception/NullCommandException.java
@@ -1,6 +1,9 @@
package qz.exception;
public class NullCommandException extends javax.print.PrintException {
+ public NullCommandException() {
+ super();
+ }
public NullCommandException(String msg) {
super(msg);
}
diff --git a/src/qz/installer/Installer.java b/src/qz/installer/Installer.java
new file mode 100644
index 000000000..328700cf4
--- /dev/null
+++ b/src/qz/installer/Installer.java
@@ -0,0 +1,239 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer;
+
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.installer.certificate.*;
+import qz.installer.certificate.firefox.FirefoxCertificateInstaller;
+import qz.utils.FileUtilities;
+import qz.utils.SystemUtilities;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import static qz.common.Constants.*;
+import static qz.installer.certificate.KeyPairWrapper.Type.CA;
+import static qz.utils.FileUtilities.*;
+
+/**
+ * Cross-platform wrapper for install steps
+ * - Used by CommandParser via command line
+ * - Used by PrintSocketServer at startup to ensure SSL is functioning
+ */
+public abstract class Installer {
+ protected static final Logger log = LoggerFactory.getLogger(Installer.class);
+
+ // Silence prompts within our control
+ public static boolean IS_SILENT = "1".equals(System.getenv(PROPS_FILE + "_silent"));
+
+
+ public enum InstallType {
+ PREINSTALL(""),
+ INSTALL("install --dest /my/install/location [--silent]"),
+ CERTGEN("certgen [--key key.pem --cert cert.pem] [--pfx cert.pfx --pass 12345] [--host \"list;of;hosts\""),
+ UNINSTALL(""),
+ SPAWN("spawn [params]");
+ public String usage;
+ InstallType(String usage) {
+ this.usage = usage;
+ }
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+ }
+
+ public enum PrivilegeLevel {
+ USER,
+ SYSTEM
+ }
+
+ public abstract Installer removeLegacyStartup();
+ public abstract Installer addAppLauncher();
+ public abstract Installer addStartupEntry();
+ public abstract Installer addSystemSettings();
+ public abstract Installer removeSystemSettings();
+ public abstract void spawn(List args) throws Exception;
+
+ public abstract Installer addUserSettings();
+
+ public abstract void setDestination(String destination);
+ public abstract String getDestination();
+
+ private static Installer instance;
+
+ public static Installer getInstance() {
+ if(instance == null) {
+ if(SystemUtilities.isWindows()) {
+ instance = new WindowsInstaller();
+ } else if(SystemUtilities.isMac()) {
+ instance = new MacInstaller();
+ } else {
+ instance = new LinuxInstaller();
+ }
+ }
+ return instance;
+ }
+
+ public static void install(String destination, boolean silent) throws Exception {
+ IS_SILENT = silent;
+ getInstance();
+ if (destination != null) {
+ instance.setDestination(destination);
+ }
+ install();
+ }
+
+ public static boolean preinstall() {
+ log.info("Stopping running instances...");
+ return TaskKiller.killAll();
+ }
+
+ public static void install() throws Exception {
+ getInstance();
+ log.info("Installing to {}", instance.getDestination());
+ instance.deployApp()
+ .removeLegacyStartup()
+ .removeLegacyFiles()
+ .addSharedDirectory()
+ .addAppLauncher()
+ .addStartupEntry()
+ .addSystemSettings();
+ }
+
+ public static void uninstall() {
+ log.info("Stopping running instances...");
+ TaskKiller.killAll();
+ getInstance();
+ log.info("Uninstalling from {}", instance.getDestination());
+ instance.removeSharedDirectory()
+ .removeSystemSettings()
+ .removeCerts();
+ }
+
+ public Installer deployApp() throws IOException {
+ Path src = SystemUtilities.detectAppPath();
+ Path dest = Paths.get(getDestination());
+
+ if(!Files.exists(dest)) {
+ Files.createDirectories(dest);
+ }
+
+ FileUtils.copyDirectory(src.toFile(), dest.toFile());
+ FileUtilities.setPermissionsRecursively(dest, false);
+ if(SystemUtilities.isWindows()) {
+ // skip
+ } else if(SystemUtilities.isMac()) {
+ setExecutable("uninstall");
+ setExecutable("Contents/MacOS/" + ABOUT_TITLE);
+ } else {
+ setExecutable("uninstall");
+ setExecutable(PROPS_FILE);
+ }
+ return this;
+ }
+
+ private void setExecutable(String relativePath) {
+ new File(getDestination(), relativePath).setExecutable(true, false);
+ }
+
+
+ public Installer removeLegacyFiles() {
+ String[] dirs = { "demo/js/3rdparty", "utils", "auth" };
+ String[] files = { "demo/js/qz-websocket.js", "windows-icon.ico", "Contents/Resources/apple-icon.icns" };
+ for (String dir : dirs) {
+ try {
+ FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
+ } catch(IOException ignore) {}
+ }
+ for (String file : files) {
+ new File(instance.getDestination() + File.separator + file).delete();
+ }
+ return this;
+ }
+
+ public Installer addSharedDirectory() {
+ try {
+ Files.createDirectories(SHARED_DIR);
+ FileUtilities.setPermissionsRecursively(SHARED_DIR, true);
+ Path ssl = Paths.get(SHARED_DIR.toString(), "ssl");
+ Files.createDirectories(ssl);
+ FileUtilities.setPermissionsRecursively(ssl, true);
+
+ log.info("Created shared directory: {}", SHARED_DIR);
+ } catch(IOException e) {
+ log.warn("Could not create shared directory: {}", SHARED_DIR);
+ }
+ return this;
+ }
+
+ public Installer removeSharedDirectory() {
+ try {
+ FileUtils.deleteDirectory(SHARED_DIR.toFile());
+ log.info("Deleted shared directory: {}", SHARED_DIR);
+ } catch(IOException e) {
+ log.warn("Could not delete shared directory: {}", SHARED_DIR);
+ }
+ return this;
+ }
+
+ /**
+ * Checks, and if needed generates an SSL for the system
+ */
+ public CertificateManager certGen(boolean forceNew, String... hostNames) throws Exception {
+ CertificateManager certificateManager = new CertificateManager(forceNew, hostNames);
+ boolean needsInstall = certificateManager.needsInstall();
+ try {
+ // Check that the CA cert is installed
+ X509Certificate caCert = certificateManager.getKeyPair(CA).getCert();
+ NativeCertificateInstaller installer = NativeCertificateInstaller.getInstance();
+
+ if (forceNew || needsInstall) {
+ // Remove installed certs per request (usually the desktop installer, or failure to write properties)
+ List matchingCerts = installer.find();
+ installer.remove(matchingCerts);
+ installer.install(caCert);
+ FirefoxCertificateInstaller.install(caCert, hostNames);
+ } else {
+ // Make sure the certificate is recognized by the system
+ File tempCert = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
+ CertificateManager.writeCert(caCert, tempCert); // temp cert
+ if(!installer.verify(tempCert)) {
+ installer.install(caCert);
+ FirefoxCertificateInstaller.install(caCert, hostNames);
+ }
+ }
+ }
+ catch(Exception e) {
+ log.error("Something went wrong obtaining the certificate. HTTPS will fail.", e);
+ }
+
+ return certificateManager;
+ }
+
+ /**
+ * Remove matching certs from user|system, then Firefox
+ */
+ public void removeCerts() {
+ // System certs
+ NativeCertificateInstaller instance = NativeCertificateInstaller.getInstance();
+ instance.remove(instance.find());
+ // Firefox certs
+ FirefoxCertificateInstaller.uninstall();
+ }
+}
diff --git a/src/qz/installer/LinuxInstaller.java b/src/qz/installer/LinuxInstaller.java
new file mode 100644
index 000000000..e0e6def7e
--- /dev/null
+++ b/src/qz/installer/LinuxInstaller.java
@@ -0,0 +1,218 @@
+package qz.installer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.utils.FileUtilities;
+import qz.utils.ShellUtilities;
+import qz.utils.SystemUtilities;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import static qz.common.Constants.*;
+
+public class LinuxInstaller extends Installer {
+ protected static final Logger log = LoggerFactory.getLogger(LinuxInstaller.class);
+
+ public static final String SHORTCUT_NAME = PROPS_FILE + ".desktop";
+ public static final String STARTUP_DIR = "/etc/xdg/autostart/";
+ public static final String STARTUP_LAUNCHER = STARTUP_DIR + SHORTCUT_NAME;
+ public static final String APP_DIR = "/usr/share/applications/";
+ public static final String APP_LAUNCHER = APP_DIR + SHORTCUT_NAME;
+ public static final String UDEV_RULES = "/lib/udev/rules.d/99-udev-override.rules";
+
+ private String destination = "/opt/" + PROPS_FILE;
+
+ public void setDestination(String destination) {
+ this.destination = destination;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public Installer addAppLauncher() {
+ addLauncher(APP_LAUNCHER, false);
+ return this;
+ }
+
+ public Installer addStartupEntry() {
+ addLauncher(STARTUP_LAUNCHER, false);
+ return this;
+ }
+
+ private void addLauncher(String location, boolean isStartup) {
+ HashMap fieldMap = new HashMap<>();
+ // Dynamic fields
+ fieldMap.put("%DESTINATION%", destination);
+ fieldMap.put("%LINUX_ICON%", String.format("%s.svg", PROPS_FILE));
+ fieldMap.put("%COMMAND%", String.format("%s/%s", destination, PROPS_FILE));
+ fieldMap.put("%PARAM%", isStartup ? "--honorautostart" : "");
+
+ File launcher = new File(location);
+ try {
+ FileUtilities.configureAssetFile("assets/linux-shortcut.desktop.in", launcher, fieldMap, LinuxInstaller.class);
+ launcher.setReadable(true, false);
+ launcher.setExecutable(true, false);
+ } catch(IOException e) {
+ log.warn("Unable to write {} file: {}", isStartup ? "startup":"launcher", location, e);
+ }
+ }
+
+ public Installer addUserSettings() {
+ return this;
+ }
+
+ public Installer removeLegacyStartup() {
+ log.info("Removing legacy autostart entries for all users matching {} or {}", ABOUT_TITLE, PROPS_FILE);
+ // assume users are in /home
+ String[] shortcutNames = {ABOUT_TITLE, PROPS_FILE};
+ for(File file : new File("/home").listFiles()) {
+ if (file.isDirectory()) {
+ File userStart = new File(file.getPath() + "/.config/autostart");
+ if (userStart.exists() && userStart.isDirectory()) {
+ for (String shortcutName : shortcutNames) {
+ File legacyStartup = new File(userStart.getPath() + File.separator + shortcutName + ".desktop");
+ if(legacyStartup.exists()) {
+ legacyStartup.delete();
+ }
+ }
+ }
+ }
+ }
+ return this;
+ }
+
+ public Installer addSystemSettings() {
+ // Legacy Ubuntu versions only: Patch Unity to show the System Tray
+ if(SystemUtilities.isUbuntu()) {
+ ShellUtilities.execute("gsettings", "set", "com.canonical.Unity.Panel", "systray", "-whitelist", "\"['all']\"");
+
+ if(ShellUtilities.execute("killall", "-w", "unity", "-panel")) {
+ ShellUtilities.execute("nohup", "unity", "-panel");
+ }
+
+ if(ShellUtilities.execute("killall", "-w", "unity", "-2d")) {
+ ShellUtilities.execute("nohup", "unity", "-2d");
+ }
+ }
+
+ try {
+ File udev = new File(UDEV_RULES);
+ if (udev.exists()) {
+ udev.delete();
+ }
+ FileUtilities.configureAssetFile("assets/linux-udev.rules.in", new File(UDEV_RULES), new HashMap<>(), LinuxInstaller.class);
+ ShellUtilities.execute("udevadm", "control", "--reload-rules");
+ } catch(IOException e) {
+ log.warn("Could not install udev rules, usb support may fail {}", UDEV_RULES, e);
+ }
+
+ // Cleanup
+ log.info("Cleaning up any remaining files...");
+ new File(destination + File.separator + "install").delete();
+ return this;
+ }
+
+ public Installer removeSystemSettings() {
+ File udev = new File(UDEV_RULES);
+ if (udev.exists()) {
+ udev.delete();
+ }
+ return this;
+ }
+
+ // Environmental variables for spawning a task using sudo. Order is important.
+ static String[] SUDO_EXPORTS = {"USER", "HOME", "UPSTART_SESSION", "DISPLAY", "DBUS_SESSION_BUS_ADDRESS", "XDG_CURRENT_DESKTOP", "GNOME_DESKTOP_SESSION_ID" };
+
+ /**
+ * Spawns the process as the underlying regular user account, preserving the environment
+ */
+ public void spawn(List args) throws Exception {
+ args.remove(0); // the first arg is "spawn", remove it
+ String whoami = ShellUtilities.executeRaw("logname").trim();
+ if(whoami.isEmpty()) {
+ whoami = System.getenv("SUDO_USER");
+ }
+
+ if(whoami != null && !whoami.trim().isEmpty()) {
+ whoami = whoami.trim();
+ } else {
+ throw new Exception("Unable to get current user, can't spawn instance");
+ }
+
+ String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="};
+
+ ArrayList pids = new ArrayList<>();
+ for(String dbusMatch : dbusMatches) {
+ pids.addAll(Arrays.asList(ShellUtilities.executeRaw("pgrep", "-f", dbusMatch).split("\\r?\\n")));
+ }
+
+ HashMap env = new HashMap<>();
+ HashMap tempEnv = new HashMap<>();
+ ArrayList toExport = new ArrayList<>(Arrays.asList(SUDO_EXPORTS));
+ for(String pid : pids) {
+ try {
+ String delim = Pattern.compile("\0").pattern();
+ String[] vars = new String(Files.readAllBytes(Paths.get(String.format("/proc/%s/environ", pid)))).split(delim);
+ for(String var : vars) {
+ String[] parts = var.split("=", 2);
+ if(parts.length == 2) {
+ String key = parts[0].trim();
+ String val = parts[1].trim();
+ if(toExport.contains(key)) {
+ tempEnv.put(key, val);
+ }
+ }
+ }
+ } catch(Exception ignore) {}
+
+ // Only add vars for the current user
+ if(whoami.trim().equals(tempEnv.get("USER"))) {
+ env.putAll(tempEnv);
+ }
+ }
+
+ if(env.size() == 0) {
+ throw new Exception("Unable to get dbus info from /proc, can't spawn instance");
+ }
+
+ // Prepare the environment
+ String[] envp = new String[env.size() + ShellUtilities.envp.length];
+ int i = 0;
+ // Keep existing env
+ for(String keep : ShellUtilities.envp) {
+ envp[i++] = keep;
+ }
+ for(String key :env.keySet()) {
+ envp[i++] = String.format("%s=%s", key, env.get(key));
+ }
+
+ // Determine if this environment likes sudo
+ String[] sudoCmd = { "sudo", "-E", "-u", whoami, "nohup" };
+ String[] suCmd = { "su", whoami, "-c", "nohup" };
+ String[] asUser = ShellUtilities.execute("which", "sudo") ? sudoCmd : suCmd;
+
+ // Build and escape our command
+ List argsList = new ArrayList<>();
+ argsList.addAll(Arrays.asList(asUser));
+ String command = "";
+ Pattern quote = Pattern.compile("\"");
+ for(String arg : args) {
+ command += String.format(" %s", arg);
+ }
+ argsList.add(command.trim());
+
+ // Spawn
+ System.out.println(String.join(" ", argsList));
+ Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
+ }
+
+}
diff --git a/src/qz/installer/MacInstaller.java b/src/qz/installer/MacInstaller.java
new file mode 100644
index 000000000..2200107fa
--- /dev/null
+++ b/src/qz/installer/MacInstaller.java
@@ -0,0 +1,120 @@
+package qz.installer;
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.utils.FileUtilities;
+import qz.utils.ShellUtilities;
+import qz.utils.SystemUtilities;
+
+import javax.swing.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+
+import static qz.common.Constants.*;
+
+public class MacInstaller extends Installer {
+ protected static final Logger log = LoggerFactory.getLogger(MacInstaller.class);
+ private static final String PACKAGE_NAME = getPackageName();
+ private String destination = "/Applications/" + ABOUT_TITLE + ".app";
+
+ public Installer addAppLauncher() {
+ // not needed; registered when "QZ Tray.app" is copied
+ return this;
+ }
+
+ public Installer addStartupEntry() {
+ File dest = new File(String.format("/Library/LaunchAgents/%s.plist", PACKAGE_NAME));
+ HashMap fieldMap = new HashMap<>();
+ // Dynamic fields
+ fieldMap.put("%PACKAGE_NAME%", PACKAGE_NAME);
+ fieldMap.put("%COMMAND%", String.format("%s/Contents/MacOS/%s", destination, ABOUT_TITLE));
+ fieldMap.put("%PARAM%", "--honorautostart");
+
+ try {
+ FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class);
+ } catch(IOException e) {
+ log.warn("Unable to write startup file: {}", dest, e);
+ }
+
+ return this;
+ }
+
+ public void setDestination(String destination) {
+ this.destination = destination;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public Installer addUserSettings() { return this; }
+
+ public Installer addSystemSettings() { return this; }
+ public Installer removeSystemSettings() {
+ // Remove startup entry
+ File dest = new File(String.format("/Library/LaunchAgents/%s.plist", PACKAGE_NAME));
+ dest.delete();
+ return this;
+ }
+
+ /**
+ * Removes legacy (<= 2.0) startup entries
+ */
+ public Installer removeLegacyStartup() {
+ log.info("Removing startup entries for all users matching " + ABOUT_TITLE);
+ String script = "tell application \"System Events\" to delete "
+ + "every login item where name is \"" + ABOUT_TITLE + "\"";
+
+ // Handle edge-case for when running from IDE
+ File jar = new File(SystemUtilities.getJarPath());
+ if(jar.getName().endsWith(".jar")) {
+ script += " or name is \"" + jar.getName() + "\"";
+ }
+
+ // Run on background thread in case System Events is hung or slow to respond
+ final String finalScript = script;
+ new Thread(() -> {
+ ShellUtilities.executeAppleScript(finalScript);
+ }).run();
+ return this;
+ }
+
+ public static String getAppPath() {
+ // Return the Mac ".app" location
+ String target = SystemUtilities.getJarPath();
+ int appIndex = target.indexOf(".app/");
+ if (appIndex > 0) {
+ return target.substring(0, appIndex -1);
+ }
+ // Fallback on the ".jar" location
+ return target;
+ }
+
+ public static String getPackageName() {
+ String packageName;
+ String[] parts = ABOUT_URL.split("\\W");
+ if (parts.length >= 2) {
+ // Parse io.qz.qz-print from Constants
+ packageName = String.format("%s.%s.%s", parts[parts.length - 1], parts[parts.length - 2], PROPS_FILE);
+ } else {
+ // Fallback on something sane
+ packageName = "local." + PROPS_FILE;
+ }
+ return packageName;
+ }
+
+ public void spawn(List args) throws Exception {
+ throw new UnsupportedOperationException("Spawn is not yet support on Mac");
+ }
+}
diff --git a/src/qz/installer/TaskKiller.java b/src/qz/installer/TaskKiller.java
new file mode 100644
index 000000000..5ef45ddf0
--- /dev/null
+++ b/src/qz/installer/TaskKiller.java
@@ -0,0 +1,89 @@
+package qz.installer;
+
+import com.sun.jna.platform.win32.Kernel32;
+import org.apache.commons.lang3.StringUtils;
+import qz.utils.MacUtilities;
+import qz.utils.ShellUtilities;
+import qz.utils.SystemUtilities;
+import qz.ws.PrintSocketServer;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static qz.common.Constants.ABOUT_TITLE;
+import static qz.common.Constants.PROPS_FILE;
+import static qz.installer.Installer.InstallType.PREINSTALL;
+
+public class TaskKiller {
+ private static final String[] JAVA_PID_QUERY_POSIX = {"pgrep", "java" };
+ private static final String[] TRAY_PID_QUERY_POSIX = {"pgrep", "-f", PROPS_FILE + ".jar" };
+ private static final String[] KILL_PID_CMD_POSIX = {"kill", "-9", ""/*pid placeholder*/};
+
+ private static final String[] JAVA_PID_QUERY_WIN32 = {"wmic.exe", "process", "where", "Name like '%java%'", "get", "processid" };
+ private static final String[] TRAY_PID_QUERY_WIN32 = {"wmic.exe", "process", "where", "CommandLine like '%" + PROPS_FILE + ".jar" + "%'", "get", "processid" };
+ private static final String[] KILL_PID_CMD_WIN32 = {"taskkill.exe", "/F", "/PID", "" /*pid placeholder*/ };
+
+ /**
+ * Kills all QZ Tray processes, being careful not to kill itself
+ */
+ public static boolean killAll() {
+ boolean success = true;
+
+ String[] javaProcs;
+ String[] trayProcs;
+ int selfProc;
+ String[] killCmd;
+ if(SystemUtilities.isWindows()) {
+ javaProcs = ShellUtilities.executeRaw(JAVA_PID_QUERY_WIN32).split("\\s*\\r?\\n");
+ trayProcs = ShellUtilities.executeRaw(TRAY_PID_QUERY_WIN32).split("\\s*\\r?\\n");
+ selfProc = Kernel32.INSTANCE.GetCurrentProcessId();
+ killCmd = KILL_PID_CMD_WIN32;
+ } else {
+ javaProcs = ShellUtilities.executeRaw(JAVA_PID_QUERY_POSIX).split("\\s*\\r?\\n");
+ 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.length > 0) {
+ // Find intersections of java and qz-tray.jar
+ List intersections = new ArrayList<>(Arrays.asList(trayProcs));
+ intersections.retainAll(Arrays.asList(javaProcs));
+
+ // Remove any instances created by this installer
+ intersections.remove("" + selfProc);
+
+ // Kill whatever's left
+ for (String pid : intersections) {
+ // isNumeric() needed for Windows; filters whitespace, headers
+ if(StringUtils.isNumeric(pid)) {
+ // Set last command to the pid
+ killCmd[killCmd.length -1] = pid;
+ success = success && ShellUtilities.execute(killCmd);
+ }
+ }
+ }
+
+ // Use jcmd to kill class processes too, such as through the IDE
+ if(SystemUtilities.isJDK()) {
+ String[] procs = ShellUtilities.executeRaw("jcmd", "-l").split("\\r?\\n");
+ for(String proc : procs) {
+ String[] parts = proc.split(" ", 1);
+ if (parts.length >= 2 && parts[1].contains(PrintSocketServer.class.getCanonicalName())) {
+ killCmd[killCmd.length - 1] = parts[0].trim();
+ success = success && ShellUtilities.execute(killCmd);
+ }
+ }
+ }
+
+ if(SystemUtilities.isWindowsXP()) {
+ File f = new File("TempWmicBatchFile.bat");
+ if(f.exists()) {
+ f.deleteOnExit();
+ }
+ }
+
+ return success;
+ }
+}
diff --git a/src/qz/installer/WindowsInstaller.java b/src/qz/installer/WindowsInstaller.java
new file mode 100644
index 000000000..0fe40595b
--- /dev/null
+++ b/src/qz/installer/WindowsInstaller.java
@@ -0,0 +1,190 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer;
+
+import com.sun.jna.platform.win32.*;
+import mslinks.ShellLink;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.utils.ShellUtilities;
+import qz.utils.WindowsUtilities;
+import qz.ws.PrintSocketServer;
+
+import javax.swing.*;
+
+import static qz.common.Constants.*;
+import static qz.installer.WindowsSpecialFolders.*;
+import static com.sun.jna.platform.win32.WinReg.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+
+public class WindowsInstaller extends Installer {
+ protected static final Logger log = LoggerFactory.getLogger(WindowsInstaller.class);
+ private String destination = getDefaultDestination();
+ private String destinationExe;
+
+ public void setDestination(String destination) {
+ this.destination = destination;
+ this.destinationExe = destination + File.separator + PROPS_FILE+ ".exe";
+ }
+
+ /**
+ * Cycles through registry keys removing legacy (<= 2.0) startup entries
+ */
+ public Installer removeLegacyStartup() {
+ log.info("Removing legacy startup entries for all users matching " + ABOUT_TITLE);
+ // This can take a while, run in a background thread
+ SwingUtilities.invokeLater(() -> {
+ for (String user : Advapi32Util.registryGetKeys(HKEY_CURRENT_USER)) {
+ WindowsUtilities.deleteRegKey(HKEY_USERS, user.trim() + "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\" + ABOUT_TITLE);
+ }
+ });
+
+ return this;
+ }
+
+ public Installer addAppLauncher() {
+ try {
+ Path loc = Paths.get(COMMON_START_MENU.toString(), "Programs", ABOUT_TITLE);
+ loc.toFile().mkdirs();
+ String lnk = loc + File.separator + ABOUT_TITLE + ".lnk";
+ String exe = destination + File.separator + PROPS_FILE+ ".exe";
+ log.info("Creating launcher \"{}\" -> \"{}\"", lnk, exe);
+ ShellLink.createLink(exe, lnk);
+ } catch(IOException e) {
+ log.warn("Could not create launcher", e);
+ }
+ return this;
+ }
+
+ public Installer addStartupEntry() {
+ try {
+ String lnk = WindowsSpecialFolders.COMMON_STARTUP + File.separator + ABOUT_TITLE + ".lnk";
+ String exe = destination + File.separator + PROPS_FILE+ ".exe";
+ log.info("Creating startup entry \"{}\" -> \"{}\"", lnk, exe);
+ ShellLink link = ShellLink.createLink(exe, lnk);
+ link.setCMDArgs("--honorautostart"); // honors auto-start preferences
+ } catch(IOException e) {
+ log.warn("Could not create startup launcher", e);
+ }
+ return this;
+ }
+ public Installer removeSystemSettings() {
+ // Cleanup registry
+ WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + ABOUT_TITLE);
+ WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\" + ABOUT_TITLE);
+ WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, DATA_DIR);
+
+ // Cleanup launchers
+ for(WindowsSpecialFolders folder : new WindowsSpecialFolders[] { START_MENU, COMMON_START_MENU, DESKTOP, PUBLIC_DESKTOP }) {
+ try {
+ new File(folder + File.separator + ABOUT_TITLE + ".lnk").delete();
+ // Since 2.1, start menus use subfolder
+ if (folder.equals(COMMON_START_MENU) || folder.equals(START_MENU)) {
+ FileUtils.deleteDirectory(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE));
+ }
+ } catch(IOException ignore) {}
+ }
+
+ // Cleanup firewall rules
+ ShellUtilities.execute("netsh.exe", "advfirewall", "delete", "rule", String.format("name=\"%s\"", ABOUT_TITLE));
+ return this;
+ }
+
+ public Installer addSystemSettings() {
+ /**
+ * TODO: Upgrade JNA!
+ * 64-bit registry view is currently invoked by nsis (windows-installer.nsi.in) using SetRegView 64
+ * However, newer version of JNA offer direct WinNT.KEY_WOW64_64KEY registry support, safeguarding
+ * against direct calls to "java -jar qz-tray.jar install|keygen|etc", which will be needed moving forward
+ * for support and troubleshooting.
+ */
+
+ // Mime-type support e.g. qz:launch
+ WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "", String.format("URL:%s Protocol", ABOUT_TITLE));
+ WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "URL Protocol", "");
+ WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\DefaultIcon", DATA_DIR), "", String.format("\"%s\",1", destinationExe));
+ WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\shell\\open\\command", DATA_DIR), "", String.format("\"%s\" \"%%1\"", destinationExe));
+
+ /// Uninstall info
+ String uninstallKey = String.format("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", ABOUT_TITLE);
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, String.format("Software\\%s", ABOUT_TITLE), "", destination);
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayName", String.format("%s %s", ABOUT_TITLE, VERSION));
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "Publisher", ABOUT_COMPANY);
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "UninstallString", destination + File.separator + "uninstall.exe");
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayIcon", destinationExe);
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "HelpLink", ABOUT_SUPPORT_URL );
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLUpdateInfo", ABOUT_DOWNLOAD_URL);
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLInfoAbout", ABOUT_SUPPORT_URL);
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayVersion", VERSION.toString());
+ WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "EstimatedSize", FileUtils.sizeOfDirectoryAsBigInteger(new File(destination)).intValue() / 1024);
+
+ // Firewall rules
+ String ports = StringUtils.join(PrintSocketServer.SECURE_PORTS, ",") + "," + StringUtils.join(PrintSocketServer.INSECURE_PORTS, ",");
+ ShellUtilities.execute("netsh.exe", "advfirewall", "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");
+ return this;
+ }
+
+ public Installer addUserSettings() {
+ // Whitelist loopback for IE/Edge
+ if(ShellUtilities.execute("CheckNetIsolation.exe", "LoopbackExempt", "-a", "-n=Microsoft.MicrosoftEdge_8wekyb3d8bbwe")) {
+ log.warn("Could not whitelist loopback connections for IE, Edge");
+ }
+
+ try {
+ // Intranet settings; uncheck "include sites not listed in other zones"
+ String key = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones\\1";
+ String value = "Flags";
+ if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
+ int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
+ // remove value using bitwise XOR
+ Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data ^ 16);
+ }
+
+ // Legacy Edge loopback support
+ key = "Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge\\ExperimentalFeatures";
+ value = "AllowLocalhostLoopback";
+ if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
+ int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
+ // remove value using bitwise OR
+ Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data | 1);
+ }
+ } catch(Exception e) {
+ log.warn("An error occurred configuring the \"Local Intranet Zone\"; connections to \"localhost\" may fail", e);
+ }
+ return this;
+ }
+
+ public static String getDefaultDestination() {
+ String path = System.getenv("ProgramW6432");
+ if (path == null || path.trim().isEmpty()) {
+ path = System.getenv("ProgramFiles");
+ }
+ return path + File.separator + ABOUT_TITLE;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public void spawn(List args) throws Exception {
+ throw new UnsupportedOperationException("Spawn is not yet support on Windows");
+ }
+}
diff --git a/src/qz/installer/WindowsSpecialFolders.java b/src/qz/installer/WindowsSpecialFolders.java
new file mode 100644
index 000000000..dc042628e
--- /dev/null
+++ b/src/qz/installer/WindowsSpecialFolders.java
@@ -0,0 +1,97 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer;
+
+import com.sun.jna.platform.win32.*;
+import qz.utils.SystemUtilities;
+
+/**
+ * Windows XP-compatible special folder's wrapper for JNA
+ *
+ */
+public enum WindowsSpecialFolders {
+ ADMIN_TOOLS(ShlObj.CSIDL_ADMINTOOLS, KnownFolders.FOLDERID_AdminTools),
+ STARTUP_ALT(ShlObj.CSIDL_ALTSTARTUP, KnownFolders.FOLDERID_Startup),
+ ROAMING_APPDATA(ShlObj.CSIDL_APPDATA, KnownFolders.FOLDERID_RoamingAppData),
+ RECYCLING_BIN(ShlObj.CSIDL_BITBUCKET, KnownFolders.FOLDERID_RecycleBinFolder),
+ CD_BURNING(ShlObj.CSIDL_CDBURN_AREA, KnownFolders.FOLDERID_CDBurning),
+ COMMON_ADMIN_TOOLS(ShlObj.CSIDL_COMMON_ADMINTOOLS, KnownFolders.FOLDERID_CommonAdminTools),
+ COMMON_STARTUP_ALT(ShlObj.CSIDL_COMMON_ALTSTARTUP, KnownFolders.FOLDERID_CommonStartup),
+ PROGRAM_DATA(ShlObj.CSIDL_COMMON_APPDATA, KnownFolders.FOLDERID_ProgramData),
+ PUBLIC_DESKTOP(ShlObj.CSIDL_COMMON_DESKTOPDIRECTORY, KnownFolders.FOLDERID_PublicDesktop),
+ PUBLIC_DOCUMENTS(ShlObj.CSIDL_COMMON_DOCUMENTS, KnownFolders.FOLDERID_PublicDocuments),
+ COMMON_FAVORITES(ShlObj.CSIDL_COMMON_FAVORITES, KnownFolders.FOLDERID_Favorites),
+ COMMON_MUSIC(ShlObj.CSIDL_COMMON_MUSIC, KnownFolders.FOLDERID_PublicMusic),
+ COMMON_OEM_LINKS(ShlObj.CSIDL_COMMON_OEM_LINKS, KnownFolders.FOLDERID_CommonOEMLinks),
+ COMMON_PICTURES(ShlObj.CSIDL_COMMON_PICTURES, KnownFolders.FOLDERID_PublicPictures),
+ COMMON_PROGRAMS(ShlObj.CSIDL_COMMON_PROGRAMS, KnownFolders.FOLDERID_CommonPrograms),
+ COMMON_START_MENU(ShlObj.CSIDL_COMMON_STARTMENU, KnownFolders.FOLDERID_CommonStartMenu),
+ COMMON_STARTUP(ShlObj.CSIDL_COMMON_STARTUP, KnownFolders.FOLDERID_CommonStartup),
+ COMMON_TEMPLATES(ShlObj.CSIDL_COMMON_TEMPLATES, KnownFolders.FOLDERID_CommonTemplates),
+ COMMON_VIDEO(ShlObj.CSIDL_COMMON_VIDEO, KnownFolders.FOLDERID_PublicVideos),
+ COMPUTERS_NEAR_ME(ShlObj.CSIDL_COMPUTERSNEARME, KnownFolders.FOLDERID_NetworkFolder),
+ CONNECTIONS_FOLDER(ShlObj.CSIDL_CONNECTIONS, KnownFolders.FOLDERID_ConnectionsFolder),
+ CONTROL_PANEL(ShlObj.CSIDL_CONTROLS, KnownFolders.FOLDERID_ControlPanelFolder),
+ COOKIES(ShlObj.CSIDL_COOKIES, KnownFolders.FOLDERID_Cookies),
+ DESKTOP_VIRTUAL(ShlObj.CSIDL_DESKTOP, KnownFolders.FOLDERID_Desktop),
+ DESKTOP(ShlObj.CSIDL_DESKTOPDIRECTORY, KnownFolders.FOLDERID_Desktop),
+ COMPUTER_FOLDER(ShlObj.CSIDL_DRIVES, KnownFolders.FOLDERID_ComputerFolder),
+ FAVORITES(ShlObj.CSIDL_FAVORITES, KnownFolders.FOLDERID_Favorites),
+ FONTS(ShlObj.CSIDL_FONTS, KnownFolders.FOLDERID_Fonts),
+ HISTORY(ShlObj.CSIDL_HISTORY, KnownFolders.FOLDERID_History),
+ INTERNET_FOLDER(ShlObj.CSIDL_INTERNET, KnownFolders.FOLDERID_InternetFolder),
+ INTERNET_CACHE(ShlObj.CSIDL_INTERNET_CACHE, KnownFolders.FOLDERID_InternetCache),
+ LOCAL_APPDATA(ShlObj.CSIDL_LOCAL_APPDATA, KnownFolders.FOLDERID_LocalAppData),
+ MY_DOCUMENTS(ShlObj.CSIDL_MYDOCUMENTS, KnownFolders.FOLDERID_Documents),
+ MY_MUSIC(ShlObj.CSIDL_MYMUSIC, KnownFolders.FOLDERID_Music),
+ MY_PICTURES(ShlObj.CSIDL_MYPICTURES, KnownFolders.FOLDERID_Pictures),
+ MY_VIDEOS(ShlObj.CSIDL_MYVIDEO, KnownFolders.FOLDERID_Videos),
+ NETWORK_NEIGHBORHOOD(ShlObj.CSIDL_NETHOOD, KnownFolders.FOLDERID_NetHood),
+ NETWORK_FOLDER(ShlObj.CSIDL_NETWORK, KnownFolders.FOLDERID_NetworkFolder),
+ PERSONAL_FOLDDER(ShlObj.CSIDL_PERSONAL, KnownFolders.FOLDERID_Documents),
+ PRINTERS(ShlObj.CSIDL_PRINTERS, KnownFolders.FOLDERID_PrintersFolder),
+ PRINTING_NEIGHBORHOODD(ShlObj.CSIDL_PRINTHOOD, KnownFolders.FOLDERID_PrintHood),
+ PROFILE_FOLDER(ShlObj.CSIDL_PROFILE, KnownFolders.FOLDERID_Profile),
+ PROGRAM_FILES(ShlObj.CSIDL_PROGRAM_FILES, KnownFolders.FOLDERID_ProgramFiles),
+ PROGRAM_FILESX86(ShlObj.CSIDL_PROGRAM_FILESX86, KnownFolders.FOLDERID_ProgramFilesX86),
+ PROGRAM_FILES_COMMON(ShlObj.CSIDL_PROGRAM_FILES_COMMON, KnownFolders.FOLDERID_ProgramFilesCommon),
+ PROGRAM_FILES_COMMONX86(ShlObj.CSIDL_PROGRAM_FILES_COMMONX86, KnownFolders.FOLDERID_ProgramFilesCommonX86),
+ PROGRAMS(ShlObj.CSIDL_PROGRAMS, KnownFolders.FOLDERID_Programs),
+ RECENT(ShlObj.CSIDL_RECENT, KnownFolders.FOLDERID_Recent),
+ RESOURCES(ShlObj.CSIDL_RESOURCES, KnownFolders.FOLDERID_ResourceDir),
+ RESOURCES_LOCALIZED(ShlObj.CSIDL_RESOURCES_LOCALIZED, KnownFolders.FOLDERID_LocalizedResourcesDir),
+ SEND_TO(ShlObj.CSIDL_SENDTO, KnownFolders.FOLDERID_SendTo),
+ START_MENU(ShlObj.CSIDL_STARTMENU, KnownFolders.FOLDERID_StartMenu),
+ STARTUP(ShlObj.CSIDL_STARTUP, KnownFolders.FOLDERID_Startup),
+ SYSTEM(ShlObj.CSIDL_SYSTEM, KnownFolders.FOLDERID_System),
+ SYSTEMX86(ShlObj.CSIDL_SYSTEMX86, KnownFolders.FOLDERID_SystemX86),
+ TEMPLATES(ShlObj.CSIDL_TEMPLATES, KnownFolders.FOLDERID_Templates),
+ WINDOWS(ShlObj.CSIDL_WINDOWS, KnownFolders.FOLDERID_Windows);
+
+ private int csidl;
+ private Guid.GUID guid;
+ WindowsSpecialFolders(int csidl, Guid.GUID guid) {
+ this.csidl = csidl;
+ this.guid = guid;
+ }
+
+ public String getPath() {
+ if(SystemUtilities.isWindowsXP()) {
+ return Shell32Util.getSpecialFolderPath(csidl, false);
+ }
+ return Shell32Util.getKnownFolderPath(guid);
+ }
+
+ @Override
+ public String toString() {
+ return getPath();
+ }
+}
diff --git a/src/qz/installer/assets/linux-shortcut.desktop.in b/src/qz/installer/assets/linux-shortcut.desktop.in
new file mode 100644
index 000000000..ae668353a
--- /dev/null
+++ b/src/qz/installer/assets/linux-shortcut.desktop.in
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Type=Application
+Name=%ABOUT_TITLE%
+Exec="%COMMAND%" %PARAM%
+Path=%DESTINATION%
+Icon=%DESTINATION%/%LINUX_ICON%
+MimeType=application/x-qz;x-scheme-handler/qz;
+Terminal=false
\ No newline at end of file
diff --git a/ant/linux/linux-udev.rules.in b/src/qz/installer/assets/linux-udev.rules.in
similarity index 59%
rename from ant/linux/linux-udev.rules.in
rename to src/qz/installer/assets/linux-udev.rules.in
index 587582337..506f274de 100644
--- a/ant/linux/linux-udev.rules.in
+++ b/src/qz/installer/assets/linux-udev.rules.in
@@ -1,2 +1,2 @@
-# ${project.name} usb override settings
+# %ABOUT_TITLE% usb override settings
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"
diff --git a/src/qz/installer/assets/mac-launchagent.plist.in b/src/qz/installer/assets/mac-launchagent.plist.in
new file mode 100644
index 000000000..69f214fe9
--- /dev/null
+++ b/src/qz/installer/assets/mac-launchagent.plist.in
@@ -0,0 +1,18 @@
+
+
+
+
+ Label%PACKAGE_NAME%
+ KeepAlive
+
+ SuccessfulExit
+ AfterInitialDemand
+
+ RunAtLoad
+ ProgramArguments
+
+ %COMMAND%
+ %PARAM%
+
+
+
\ No newline at end of file
diff --git a/src/qz/installer/certificate/CertificateChainBuilder.java b/src/qz/installer/certificate/CertificateChainBuilder.java
new file mode 100644
index 000000000..bed3ee977
--- /dev/null
+++ b/src/qz/installer/certificate/CertificateChainBuilder.java
@@ -0,0 +1,142 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.*;
+import java.util.Calendar;
+
+import org.bouncycastle.asn1.*;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.X500NameBuilder;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.*;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import qz.common.Constants;
+
+import static qz.installer.certificate.KeyPairWrapper.Type.*;
+
+public class CertificateChainBuilder {
+ public static final String[] DEFAULT_HOSTNAMES = {"localhost", "localhost.qz.io" };
+
+ private static int KEY_SIZE = 2048;
+ public static int CA_CERT_AGE = 7305; // 20 years
+ public static int SSL_CERT_AGE = 825; // Per https://support.apple.com/HT210176
+
+ private String[] hostNames;
+
+ public CertificateChainBuilder(String ... hostNames) {
+ Security.addProvider(new BouncyCastleProvider());
+ if(hostNames.length > 0) {
+ this.hostNames = hostNames;
+ } else {
+ this.hostNames = DEFAULT_HOSTNAMES;
+ }
+ }
+
+ public KeyPairWrapper createCaCert() throws IOException, GeneralSecurityException, OperatorException {
+ KeyPair keyPair = createRsaKey();
+
+ X509v3CertificateBuilder builder = createX509Cert(keyPair, CA_CERT_AGE);
+
+ builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(1))
+ .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign + KeyUsage.cRLSign))
+ .addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
+
+ // Signing
+ ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate());
+ X509CertificateHolder certHolder = builder.build(sign);
+
+ // Convert to java-friendly format
+ return new KeyPairWrapper(CA, keyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
+ }
+
+ public KeyPairWrapper createSslCert(KeyPairWrapper caKeyPairWrapper) throws IOException, GeneralSecurityException, OperatorException {
+ KeyPair sslKeyPair = createRsaKey();
+ X509v3CertificateBuilder builder = createX509Cert(sslKeyPair, SSL_CERT_AGE);
+
+ JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils();
+
+ builder.addExtension(Extension.authorityKeyIdentifier, false, utils.createAuthorityKeyIdentifier(caKeyPairWrapper.getCert()))
+ .addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
+ .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature + KeyUsage.keyEncipherment))
+ .addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}))
+ .addExtension(Extension.subjectAlternativeName, false, buildSan(hostNames))
+ .addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(sslKeyPair.getPublic()));
+
+ // Signing
+ ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caKeyPairWrapper.getKey());
+ X509CertificateHolder certHolder = builder.build(sign);
+
+ // Convert to java-friendly format
+ return new KeyPairWrapper(SSL, sslKeyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
+ }
+
+ private static KeyPair createRsaKey() throws GeneralSecurityException {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+ keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ private static X509v3CertificateBuilder createX509Cert(KeyPair keyPair, int age, String ... hostNames) {
+ String cn = hostNames.length > 0 ? hostNames[0] : DEFAULT_HOSTNAMES[0];
+ X500Name name = new X500NameBuilder()
+ .addRDN(BCStyle.C, Constants.ABOUT_COUNTRY)
+ .addRDN(BCStyle.ST, Constants.ABOUT_STATE)
+ .addRDN(BCStyle.L, Constants.ABOUT_CITY)
+ .addRDN(BCStyle.O, Constants.ABOUT_COMPANY)
+ .addRDN(BCStyle.OU, Constants.ABOUT_COMPANY)
+ .addRDN(BCStyle.EmailAddress, Constants.ABOUT_EMAIL)
+ .addRDN(BCStyle.CN, cn)
+ .build();
+ BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
+ Calendar notBefore = Calendar.getInstance();
+ Calendar notAfter = Calendar.getInstance();
+ notBefore.add(Calendar.DAY_OF_YEAR, -1);
+ notAfter.add(Calendar.DAY_OF_YEAR, age - 1);
+
+ return new JcaX509v3CertificateBuilder(name, serial, notBefore.getTime(), notAfter.getTime(), name, keyPair.getPublic());
+ }
+
+ /**
+ * Builds subjectAlternativeName extension; iterates and detects IPv4 or hostname
+ */
+ private static GeneralNames buildSan(String ... hostNames) {
+ GeneralName[] gn = new GeneralName[hostNames.length];
+ for (int i = 0; i < hostNames.length; i++) {
+ int gnType = isIp(hostNames[i]) ? GeneralName.iPAddress : GeneralName.dNSName;
+ gn[i] = new GeneralName(gnType, hostNames[i]);
+ }
+ return GeneralNames.getInstance(new DERSequence(gn));
+ }
+
+ private static boolean isIp(String ip) {
+ try {
+ String[] split = ip.split("\\.");
+ if (split.length != 4) return false;
+ for (int i = 0; i < 4; ++i) {
+ int p = Integer.parseInt(split[i]);
+ if (p > 255 || p < 0) return false;
+ }
+ return true;
+ } catch (Exception ignore) {}
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/qz/installer/certificate/CertificateManager.java b/src/qz/installer/certificate/CertificateManager.java
new file mode 100644
index 000000000..1b2e97210
--- /dev/null
+++ b/src/qz/installer/certificate/CertificateManager.java
@@ -0,0 +1,417 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.operator.OperatorException;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.utils.FileUtilities;
+import qz.utils.SystemUtilities;
+
+import java.io.*;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.security.*;
+import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.*;
+
+import static qz.utils.FileUtilities.*;
+import static qz.installer.certificate.KeyPairWrapper.Type.*;
+
+/**
+ * Stores and maintains reading and writing of certificate related files
+ */
+public class CertificateManager {
+ private static final Logger log = LoggerFactory.getLogger(CertificateManager.class);
+
+ public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12";
+ public static String DEFAULT_KEYSTORE_EXTENSION = ".p12";
+
+ public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt";
+
+ private static String DEFAULT_HOST_SCOPE = "0.0.0.0";
+ private static int DEFAULT_PASSWORD_BITS = 100;
+
+ private boolean needsInstall;
+ private SslContextFactory sslContextFactory;
+ private KeyPairWrapper sslKeyPair;
+ private KeyPairWrapper caKeyPair;
+
+ private Properties properties;
+ private char[] password;
+
+ /**
+ * For internal certs
+ */
+ public CertificateManager(boolean forceNew, String ... hostNames) throws IOException, GeneralSecurityException, OperatorException {
+ Security.addProvider(new BouncyCastleProvider());
+ sslKeyPair = new KeyPairWrapper(SSL);
+ caKeyPair = new KeyPairWrapper(CA);
+
+ if (!forceNew) {
+ // order is important: ssl, ca
+ properties = loadProperties(sslKeyPair, caKeyPair);
+ }
+
+ if(properties == null) {
+ log.warn("Warning, SSL properties won't be loaded from disk... we'll try to create them...");
+
+ CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
+ caKeyPair = cb.createCaCert();
+ sslKeyPair = cb.createSslCert(caKeyPair);
+
+ // Create CA
+ properties = createKeyStore(CA)
+ .writeCert(CA)
+ .writeKeystore(null, CA);
+
+ // Create SSL
+ properties = createKeyStore(SSL)
+ .writeCert(SSL)
+ .writeKeystore(properties, SSL);
+
+ // Save properties
+ saveProperties();
+ }
+ }
+
+ /**
+ * For trusted PEM-formatted certs
+ */
+ public CertificateManager(File trustedPemKey, File trustedPemCert) throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ needsInstall = false;
+ sslKeyPair = new KeyPairWrapper(SSL);
+
+ // Assumes ssl/privkey.pem, ssl/fullchain.pem
+ properties = createTrustedKeystore(trustedPemKey, trustedPemCert)
+ .writeKeystore(properties, SSL);
+
+ // Save properties
+ saveProperties();
+ }
+
+ /**
+ * For trusted PKCS12-formatted certs
+ */
+ public CertificateManager(File pkcs12File, char[] password) throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ needsInstall = false;
+ sslKeyPair = new KeyPairWrapper(SSL);
+
+ // Assumes direct pkcs12 import
+ this.password = password;
+ sslKeyPair.init(pkcs12File, password);
+
+ // Save it back, but to a location we can find
+ properties = writeKeystore(null, SSL);
+
+ // Save properties
+ saveProperties();
+ }
+
+ public void renewCertChain(String ... hostNames) throws Exception {
+ CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
+ sslKeyPair = cb.createSslCert(caKeyPair);
+ createKeyStore(SSL).writeKeystore(properties, SSL);
+ reloadSslContextFactory();
+ }
+
+ public KeyPairWrapper getSslKeyPair() {
+ return sslKeyPair;
+ }
+
+ public KeyPairWrapper getCaKeyPair() {
+ return caKeyPair;
+ }
+
+ public KeyPairWrapper getKeyPair(KeyPairWrapper.Type type) {
+ switch(type) {
+ case SSL:
+ return sslKeyPair;
+ case CA:
+ default:
+ return caKeyPair;
+ }
+ }
+
+ public Properties getProperties() {
+ return properties;
+ }
+
+ private char[] getPassword() {
+ if (password == null) {
+ if(caKeyPair != null && caKeyPair.getPassword() != null) {
+ // Reuse existing
+ password = caKeyPair.getPassword();
+ } else {
+ // Create new
+ BigInteger bi = new BigInteger(DEFAULT_PASSWORD_BITS, new SecureRandom());
+ password = bi.toString(16).toCharArray();
+ log.info("Created a random {} bit password: {}", DEFAULT_PASSWORD_BITS, new String(password));
+ }
+ }
+ return password;
+ }
+
+ public SslContextFactory configureSslContextFactory() {
+ sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
+ sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
+ sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
+ return sslContextFactory;
+ }
+
+ public void reloadSslContextFactory() throws Exception {
+ if(isSslActive()) {
+ sslContextFactory.reload(sslContextFactory -> {
+ sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
+ sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
+ sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
+ });
+ } else {
+ log.warn("SSL isn't active, can't reload");
+ }
+ }
+
+ public boolean isSslActive() {
+ return sslContextFactory != null;
+ }
+
+ public boolean needsInstall() {
+ return needsInstall;
+ }
+
+ public CertificateManager createKeyStore(KeyPairWrapper.Type type) throws IOException, GeneralSecurityException {
+ KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
+ KeyStore keyStore = KeyStore.getInstance(DEFAULT_KEYSTORE_FORMAT);
+ keyStore.load(null, password);
+
+ List chain = new ArrayList<>();
+ chain.add(keyPair.getCert());
+
+ // Add ca to ssl cert chain
+ if (keyPair.getType() == SSL) {
+ chain.add(caKeyPair.getCert());
+ }
+ keyStore.setEntry(caKeyPair.getAlias(), new KeyStore.TrustedCertificateEntry(caKeyPair.getCert()), null);
+ keyStore.setKeyEntry(keyPair.getAlias(), keyPair.getKey(), getPassword(), chain.toArray(new X509Certificate[chain.size()]));
+ keyPair.init(keyStore, getPassword());
+ return this;
+ }
+
+ public CertificateManager createTrustedKeystore(File p12Store, String password) throws Exception {
+ sslKeyPair = new KeyPairWrapper(SSL);
+ sslKeyPair.init(p12Store, password.toCharArray());
+ return this;
+ }
+
+ public CertificateManager createTrustedKeystore(File pemKey, File pemCert) throws Exception {
+ sslKeyPair = new KeyPairWrapper(SSL);
+
+ // Private Key
+ PEMParser pem = new PEMParser(new FileReader(pemKey));
+ Object parsedObject = pem.readObject();
+
+ PrivateKeyInfo privateKeyInfo = parsedObject instanceof PEMKeyPair ? ((PEMKeyPair)parsedObject).getPrivateKeyInfo() : (PrivateKeyInfo)parsedObject;
+ PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
+ KeyFactory factory = KeyFactory.getInstance("RSA");
+ PrivateKey key = factory.generatePrivate(privateKeySpec);
+
+ List certs = new ArrayList<>();
+ X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
+ if(certHolder != null) {
+ certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
+ }
+
+ // Certificate
+ pem = new PEMParser(new FileReader(pemCert));
+ while((certHolder = (X509CertificateHolder)pem.readObject()) != null) {
+ certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
+ }
+
+ // Keystore
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ ks.load(null);
+
+ for (int i = 0; i < certs.size(); i++) {
+ ks.setCertificateEntry(sslKeyPair.getAlias() + "_" + i, certs.get(i));
+ }
+
+ KeyStore keyStore = KeyStore.getInstance("PKCS12");
+ keyStore.load(null);
+ keyStore.setKeyEntry(sslKeyPair.getAlias(), key, getPassword(), certs.toArray(new X509Certificate[certs.size()]));
+
+ sslKeyPair.init(keyStore, getPassword());
+ return this;
+ }
+
+ public static void writeCert(X509Certificate data, File dest) throws IOException {
+ JcaMiscPEMGenerator cert = new JcaMiscPEMGenerator(data);
+ JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(dest.toPath(), StandardOpenOption.CREATE)));
+ writer.writeObject(cert.generate());
+ writer.close();
+ FileUtilities.inheritParentPermissions(dest.toPath());
+ log.info("Wrote Cert: \"{}\"", dest);
+ }
+
+ public CertificateManager writeCert(KeyPairWrapper.Type type) throws IOException {
+ KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
+ File certFile = new File(getWritableLocation("ssl"), keyPair.getAlias() + DEFAULT_CERTIFICATE_EXTENSION);
+
+ writeCert(keyPair.getCert(), certFile);
+ FileUtilities.inheritParentPermissions(certFile.toPath());
+ if(keyPair.getType() == CA) {
+ needsInstall = true;
+ }
+ return this;
+ }
+
+ public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) throws GeneralSecurityException, IOException {
+ File sslDir = getWritableLocation("ssl");
+ KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
+
+ File keyFile = new File(sslDir, keyPair.getAlias() + DEFAULT_KEYSTORE_EXTENSION);
+ keyPair.getKeyStore().store(Files.newOutputStream(keyFile.toPath(), StandardOpenOption.CREATE), getPassword());
+ FileUtilities.inheritParentPermissions(keyFile.toPath());
+ log.info("Wrote {} Key: \"{}\"", DEFAULT_KEYSTORE_FORMAT, keyFile);
+
+ if (props == null) {
+ props = new Properties();
+ }
+ props.putIfAbsent(String.format("%s.keystore", keyPair.propsPrefix()), keyFile.toString());
+ props.putIfAbsent(String.format("%s.storepass", keyPair.propsPrefix()), new String(getPassword()));
+ props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias());
+
+ if (keyPair.getType() == SSL) {
+ props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), DEFAULT_HOST_SCOPE);
+ }
+
+
+ return props;
+ }
+
+ public static File getWritableLocation(String ... subDirs) throws IOException {
+ // Get an array of preferred directories
+ ArrayList locs = new ArrayList<>();
+
+ if (subDirs.length == 0) {
+ // Assume root directory is next to jar (e.g. qz-tray.properties)
+ Path appPath = SystemUtilities.detectAppPath();
+ // Handle null path, such as running from IDE
+ if(appPath != null) {
+ locs.add(appPath);
+ }
+ // Fallback on a directory we can normally write to
+ locs.add(SHARED_DIR);
+ locs.add(USER_DIR);
+ // Last, fallback on a directory we won't ever see again :/
+ locs.add(TEMP_DIR);
+ } else {
+ // Assume non-root directories are for ssl (e.g. certs, keystores)
+ locs.add(Paths.get(SHARED_DIR.toString(), subDirs));
+ // Fallback on a directory we can normally write to
+ locs.add(Paths.get(USER_DIR.toString(), subDirs));
+ // Last, fallback on a directory we won't ever see again :/
+ locs.add(Paths.get(TEMP_DIR.toString(), subDirs));
+ }
+
+ // Find a suitable write location
+ File path = null;
+ for(Path loc : locs) {
+ if (loc == null) continue;
+ boolean isPreferred = locs.indexOf(loc) == 0;
+ path = loc.toFile();
+ path.mkdirs();
+ if (path.canWrite()) {
+ log.debug("Writing to {}", loc);
+ if(!isPreferred) {
+ log.warn("Warning, {} isn't the preferred write location, but we'll use it anyway", loc);
+ }
+ return path;
+ } else {
+ log.debug("Can't write to {}, trying the next...", loc);
+ }
+ }
+ throw new IOException("Can't find a suitable write location. SSL will fail.");
+ }
+
+ public static Properties loadProperties(KeyPairWrapper... keyPairs) {
+ log.info("Try to find SSL properties file...");
+ Path[] locations = {SystemUtilities.detectAppPath(), SHARED_DIR, USER_DIR};
+
+ Properties props = null;
+ for(Path location : locations) {
+ if (location == null) continue;
+ try {
+ for(KeyPairWrapper keyPair : keyPairs) {
+ props = loadKeyPair(keyPair, location, props);
+ }
+ // We've loaded without Exception, return
+ log.info("Found {}/{}.properties", location, Constants.PROPS_FILE);
+ return props;
+ } catch(Exception ignore) {
+ log.warn("Properties couldn't be loaded at {}, trying fallback...", location, ignore);
+ }
+ }
+ log.info("Could not get SSL properties from file.");
+ return null;
+ }
+
+ public static Properties loadKeyPair(KeyPairWrapper keyPair, Path parent, Properties existing) throws Exception {
+ Properties props;
+ if (existing == null) {
+ props = new Properties();
+ props.load(new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties")));
+ } else {
+ props = existing;
+ }
+
+ String ks = props.getProperty(String.format("%s.keystore", keyPair.propsPrefix()));
+ String pw = props.getProperty(String.format("%s.storepass", keyPair.propsPrefix()), "");
+
+ if(ks == null || ks.trim().isEmpty()) {
+ if(keyPair.getType() == SSL) {
+ throw new IOException("Missing wss.keystore entry");
+ } else {
+ // CA is only needed for internal certs, return
+ return props;
+ }
+ }
+ File ksFile = Paths.get(ks).isAbsolute()? new File(ks):new File(parent.toFile(), ks);
+ if (ksFile.exists()) {
+ keyPair.init(ksFile, pw.toCharArray());
+ return props;
+ }
+ return null;
+ }
+
+ private void saveProperties() throws IOException {
+ File propsFile = new File(getWritableLocation(), Constants.PROPS_FILE + ".properties");
+ properties.store(new FileOutputStream(propsFile), null);
+ FileUtilities.inheritParentPermissions(propsFile.toPath());
+ log.info("Successfully created SSL properties file: {}", propsFile);
+ }
+}
diff --git a/src/qz/installer/certificate/ExpiryTask.java b/src/qz/installer/certificate/ExpiryTask.java
new file mode 100644
index 000000000..d00c83ea2
--- /dev/null
+++ b/src/qz/installer/certificate/ExpiryTask.java
@@ -0,0 +1,308 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.utils.ShellUtilities;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.*;
+
+import static qz.utils.FileUtilities.*;
+
+public class ExpiryTask extends TimerTask {
+ private static final Logger log = LoggerFactory.getLogger(CertificateManager.class);
+ public static final int DEFAULT_INITIAL_DELAY = 60 * 1000; // 1 minute
+ public static final int DEFAULT_CHECK_FREQUENCY = 3600 * 1000; // 1 hour
+ private static final int DEFAULT_GRACE_PERIOD_DAYS = 5;
+ private enum ExpiryState {VALID, EXPIRING, EXPIRED, MANAGED}
+
+ public enum CertProvider {
+ INTERNAL(Constants.ABOUT_COMPANY + ".*"),
+ LETS_ENCRYPT("Let's Encrypt.*"),
+ CA_CERT_ORG("CA Cert Signing.*"),
+ UNKNOWN;
+ String[] patterns;
+ CertProvider(String ... regexPattern) {
+ this.patterns = regexPattern;
+ }
+ }
+
+ private Timer timer;
+ private CertificateManager certificateManager;
+ private String[] hostNames;
+ private CertProvider certProvider;
+
+ public ExpiryTask(CertificateManager certificateManager) {
+ super();
+ this.certificateManager = certificateManager;
+ this.hostNames = parseHostNames();
+ this.certProvider = findCertProvider();
+ }
+
+ @Override
+ public void run() {
+ // Check for expiration
+ ExpiryState state = getExpiry(certificateManager.getSslKeyPair().getCert());
+ switch(state) {
+ case EXPIRING:
+ case EXPIRED:
+ log.info("Certificate ExpiryState {}, renewing/reloading...", state);
+ switch(certProvider) {
+ case INTERNAL:
+ if(renewInternalCert()) {
+ getExpiry();
+ }
+ break;
+ case CA_CERT_ORG:
+ case LETS_ENCRYPT:
+ if(renewExternalCert(certProvider)) {
+ getExpiry();
+ }
+ break;
+ case UNKNOWN:
+ default:
+ log.warn("Certificate can't be renewed/reloaded; ExpiryState: {}, CertProvider: {}", state, certProvider);
+ }
+ case VALID:
+ default:
+ }
+
+ }
+
+ public boolean renewInternalCert() {
+ try {
+ log.info("Requesting a new SSL certificate from {} ...", certificateManager.getCaKeyPair().getAlias());
+ certificateManager.renewCertChain(hostNames);
+ log.info("New SSL certificate created. Reloading SslContextFactory...");
+ certificateManager.reloadSslContextFactory();
+ log.info("Reloaded SSL successfully.");
+ return true;
+ }
+ catch(Exception e) {
+ log.error("Could not reload SSL certificate", e);
+ }
+ return false;
+ }
+
+ public ExpiryState getExpiry() {
+ return getExpiry(certificateManager.getSslKeyPair().getCert());
+ }
+
+ /**
+ * Returns true if the SSL certificate is generated by QZ Tray and expires inside the GRACE_PERIOD.
+ * GRACE_PERIOD is preferred for scheduling the renewals in advance, such as non-peak hours
+ */
+ public static ExpiryState getExpiry(X509Certificate cert) {
+ // Invalid
+ if (cert == null) {
+ log.error("Can't check for expiration, certificate is missing.");
+ return ExpiryState.EXPIRED;
+ }
+
+ Date expireDate = cert.getNotAfter();
+ Calendar now = Calendar.getInstance();
+ Calendar expires = Calendar.getInstance();
+ expires.setTime(expireDate);
+
+ // Expired
+ if (now.after(expires)) {
+ log.info("SSL certificate has expired {}. It must be renewed immediately.", expireDate);
+ return ExpiryState.EXPIRED;
+ }
+
+ // Expiring
+ expires.add(Calendar.DAY_OF_YEAR, -DEFAULT_GRACE_PERIOD_DAYS);
+ if (now.after(expires)) {
+ log.info("SSL certificate will expire in less than {} days: {}", DEFAULT_GRACE_PERIOD_DAYS, expireDate);
+ return ExpiryState.EXPIRING;
+ }
+
+ // Valid
+ int days = (int)Math.round((expireDate.getTime() - new Date().getTime()) / (double)86400000);
+ log.info("SSL certificate is still valid for {} more days: {}. We'll make a new one automatically when needed.", days, expireDate);
+ return ExpiryState.VALID;
+ }
+
+ private static boolean emailMatches(X509Certificate cert) {
+ try {
+ X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
+ String email = x500name.getRDNs(BCStyle.E)[0].getFirst().getValue().toString();
+ if (Constants.ABOUT_EMAIL.equals(email)) {
+ log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, CertProvider.INTERNAL);
+ return true;
+ }
+ }
+ catch(Exception ignore) {}
+ log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL);
+ return false;
+ }
+
+ public void schedule() {
+ schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY);
+ }
+
+ public void schedule(int delayMillis, int freqMillis) {
+ if(timer != null) {
+ timer.cancel();
+ timer.purge();
+ }
+ timer = new Timer();
+ timer.scheduleAtFixedRate(this, delayMillis, freqMillis);
+ }
+
+ public String[] parseHostNames() {
+ return parseHostNames(certificateManager.getSslKeyPair().getCert());
+ }
+
+ public CertProvider findCertProvider() {
+ return findCertProvider(certificateManager.getSslKeyPair().getCert());
+ }
+
+ public static CertProvider findCertProvider(X509Certificate cert) {
+ // Internal certs use CN=localhost, trust email instead
+ if (emailMatches(cert)) {
+ return CertProvider.INTERNAL;
+ }
+
+ String providerDN;
+
+ // check registered patterns to classify certificate
+ if(cert.getIssuerDN() != null && (providerDN = cert.getIssuerDN().getName()) != null) {
+ String cn = null;
+ try {
+ // parse issuer's DN
+ LdapName ldapName = new LdapName(providerDN);
+ for(Rdn rdn : ldapName.getRdns()) {
+ if(rdn.getType().equalsIgnoreCase("CN")) {
+ cn = (String)rdn.getValue();
+ break;
+ }
+ }
+
+ // compare cn to our pattern
+ if(cn != null) {
+ for(CertProvider provider : CertProvider.values()) {
+ for(String pattern : provider.patterns) {
+ if (cn.matches(pattern)) {
+ log.warn("Cert issuer detected as {}", provider.name());
+ return provider;
+ }
+ }
+ }
+ }
+ } catch(InvalidNameException ignore) {}
+ }
+
+ log.warn("A valid issuer couldn't be found, we won't know how to renew this cert when it expires");
+ return CertProvider.UNKNOWN;
+ }
+
+ public static String[] parseHostNames(X509Certificate cert) {
+ // Cache the SAN hosts for recreation
+ List hostNameList = new ArrayList<>();
+ try {
+ Collection> altNames = cert.getSubjectAlternativeNames();
+ if (altNames != null) {
+ for(List> altName : altNames) {
+ if(altName.size()< 1) continue;
+ switch((Integer)altName.get(0)) {
+ case GeneralName.dNSName:
+ case GeneralName.iPAddress:
+ Object data = altName.get(1);
+ if (data instanceof String) {
+ hostNameList.add(((String)data));
+ }
+ break;
+ default:
+ }
+ }
+ } else {
+ log.error("getSubjectAlternativeNames is null?");
+ }
+ log.debug("Parsed hostNames: {}", String.join(", ", hostNameList));
+ } catch(CertificateParsingException e) {
+ log.warn("Can't parse hostNames from this cert. Cert renewals will contain default values instead");
+ }
+ return hostNameList.toArray(new String[hostNameList.size()]);
+ }
+
+ public boolean renewExternalCert(CertProvider externalProvider) {
+ switch(externalProvider) {
+ case LETS_ENCRYPT:
+ return renewLetsEncryptCert(externalProvider);
+ case CA_CERT_ORG:
+ default:
+ log.error("Cert renewal for {} is not implemented", externalProvider);
+ }
+
+ return false;
+ }
+
+ private boolean renewLetsEncryptCert(CertProvider externalProvider) {
+ try {
+ File storagePath = CertificateManager.getWritableLocation("ssl");
+
+ // cerbot is much simpler than acme, let's use it
+ Path root = Paths.get(SHARED_DIR.toString(), "letsencrypt", "config");
+ log.info("Attempting to renew {}. Assuming certs are installed in {}...", externalProvider, root);
+ List cmds = new ArrayList(Arrays.asList("certbot", "--force-renewal", "certonly"));
+
+ cmds.add("--standalone");
+
+ cmds.add("--config-dir");
+ String config = Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "config").toString();
+ cmds.add(config);
+
+ cmds.add("--logs-dir");
+ cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "logs").toString());
+
+ cmds.add("--work-dir");
+ cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt").toString());
+
+ // append dns names
+ for(String hostName : hostNames) {
+ cmds.add("-d");
+ cmds.add(hostName);
+ }
+
+ if (ShellUtilities.execute(cmds.toArray(new String[cmds.size()]))) {
+ // Assume the cert is stored in a folder called "letsencrypt/config/live/"
+ Path keyPath = Paths.get(config, "live", hostNames[0], "privkey.pem");
+ Path certPath = Paths.get(config, "live", hostNames[0], "fullchain.pem"); // fullchain required
+ certificateManager.createTrustedKeystore(keyPath.toFile(), certPath.toFile());
+ log.info("Files imported, converted and saved. Reloading SslContextFactory...");
+ certificateManager.reloadSslContextFactory();
+ log.info("Reloaded SSL successfully.");
+ return true;
+ } else {
+ log.warn("Something went wrong renewing the LetsEncrypt certificate. Please run the certbot command manually to learn more.");
+ }
+ } catch(Exception e) {
+ log.error("Error renewing/reloading LetsEncrypt cert", e);
+ }
+ return false;
+ }
+
+}
diff --git a/src/qz/installer/certificate/KeyPairWrapper.java b/src/qz/installer/certificate/KeyPairWrapper.java
new file mode 100644
index 000000000..da7d947e1
--- /dev/null
+++ b/src/qz/installer/certificate/KeyPairWrapper.java
@@ -0,0 +1,130 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import qz.common.Constants;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Enumeration;
+
+/**
+ * Wrap handling of X509Certificate, PrivateKey and KeyStore conversion
+ */
+public class KeyPairWrapper {
+ public enum Type {CA, SSL}
+
+ private Type type;
+ private PrivateKey key;
+ private char[] password;
+ private X509Certificate cert;
+ private KeyStore keyStore; // for SSL
+
+ public KeyPairWrapper(Type type) {
+ this.type = type;
+ }
+
+ public KeyPairWrapper(Type type, KeyPair keyPair, X509Certificate cert) {
+ this.type = type;
+ this.key = keyPair.getPrivate();
+ this.cert = cert;
+ }
+
+ /**
+ * Load from disk
+ */
+ public void init(File keyFile, char[] password) throws IOException, GeneralSecurityException {
+ KeyStore keyStore = KeyStore.getInstance(keyFile.getName().endsWith(".jks") ? "JKS" : "PKCS12");
+ keyStore.load(new FileInputStream(keyFile), password);
+ init(keyStore, password);
+ }
+
+ /**
+ * Load from memory
+ */
+ public void init(KeyStore keyStore, char[] password) throws GeneralSecurityException {
+ this.keyStore = keyStore;
+ KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(password);
+ KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(getAlias(), param);
+ // the entry we assume is always wrong for pkcs12 imports, search for it instead
+ if(entry == null) {
+ Enumeration enumerator = keyStore.aliases();
+ while(enumerator.hasMoreElements()) {
+ String alias = enumerator.nextElement();
+ if(keyStore.isKeyEntry(alias)) {
+ this.password = password;
+ this.key = ((KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, param)).getPrivateKey();
+ this.cert = (X509Certificate)keyStore.getCertificate(alias);
+ return;
+ }
+ }
+ throw new GeneralSecurityException("Could not initialize the KeyStore for internal use");
+ }
+
+ this.password = password;
+ this.key = entry.getPrivateKey();
+ this.cert = (X509Certificate)keyStore.getCertificate(getAlias());
+ }
+
+ public X509Certificate getCert() {
+ return cert;
+ }
+
+ public PrivateKey getKey() {
+ return key;
+ }
+
+ public String getPasswordString() {
+ return new String(password);
+ }
+
+ public char[] getPassword() {
+ return password;
+ }
+
+ public static String getAlias(Type type) {
+ switch(type) {
+ case SSL:
+ return Constants.PROPS_FILE; // "qz-tray"
+ case CA:
+ default:
+ return "root-ca";
+ }
+ }
+
+ public String getAlias() {
+ return getAlias(getType());
+ }
+
+ public String propsPrefix() {
+ switch(type) {
+ case SSL:
+ return "wss";
+ case CA:
+ default:
+ return "ca";
+ }
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public KeyStore getKeyStore() {
+ return keyStore;
+ }
+}
\ No newline at end of file
diff --git a/src/qz/installer/certificate/LinuxCertificateInstaller.java b/src/qz/installer/certificate/LinuxCertificateInstaller.java
new file mode 100644
index 000000000..f35cd2ef6
--- /dev/null
+++ b/src/qz/installer/certificate/LinuxCertificateInstaller.java
@@ -0,0 +1,118 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.installer.Installer;
+import qz.utils.ShellUtilities;
+import qz.utils.SystemUtilities;
+
+import javax.swing.*;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import static qz.installer.Installer.PrivilegeLevel.*;
+
+/**
+ * @author Tres Finocchiaro
+ */
+public class LinuxCertificateInstaller extends NativeCertificateInstaller {
+ private static final Logger log = LoggerFactory.getLogger(LinuxCertificateInstaller.class);
+
+ private static String NSSDB = "sql:" + System.getenv("HOME") + "/.pki/nssdb";
+
+ private Installer.PrivilegeLevel certType;
+
+ public LinuxCertificateInstaller(Installer.PrivilegeLevel certType) {
+ setInstallType(certType);
+ findCertutil();
+ }
+
+ public Installer.PrivilegeLevel getInstallType() {
+ return certType;
+ }
+
+ public void setInstallType(Installer.PrivilegeLevel certType) {
+ this.certType = certType;
+ if (this.certType == SYSTEM) {
+ log.warn("Command \"certutil\" needs to run as USER. We'll try again on launch. Ignore warnings about SYSTEM store.");
+ }
+ }
+
+ public boolean remove(List idList) {
+ boolean success = true;
+ if(certType == SYSTEM) return false;
+ for(String nickname : idList) {
+ success = success && ShellUtilities.execute("certutil", "-d", NSSDB, "-D", "-n", nickname);
+ }
+ return success;
+ }
+
+ public List find() {
+ ArrayList nicknames = new ArrayList<>();
+ if(certType == SYSTEM) return nicknames;
+ try {
+ Process p = Runtime.getRuntime().exec(new String[] {"certutil", "-d", NSSDB, "-L"});
+ BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
+ String line;
+ while ((line = in.readLine()) != null) {
+ if (line.startsWith(Constants.ABOUT_COMPANY + " ")) {
+ nicknames.add(Constants.ABOUT_COMPANY);
+ break; // Stop reading input; nicknames can't appear more than once
+ }
+ }
+ in.close();
+ } catch(IOException e) {
+ log.warn("Could not get certificate nicknames", e);
+ }
+ return nicknames;
+ }
+
+ public boolean verify(File ignore) { return true; } // no easy way to validate a cert, assume it's installed
+
+ public boolean add(File certFile) {
+ if(certType == SYSTEM) return false;
+ // Create directories as needed
+ String[] parts = NSSDB.split(":", 2);
+ if(parts.length > 1) {
+ new File(parts[1]).mkdirs();
+ return ShellUtilities.execute("certutil", "-d", NSSDB, "-A", "-t", "TC", "-n", Constants.ABOUT_COMPANY, "-i", certFile.getPath());
+ }
+ log.warn("Something went wrong creating {}. HTTPS will fail on browsers which depend on it.", NSSDB);
+ return false;
+ }
+
+ private boolean findCertutil() {
+ if (!ShellUtilities.execute("which", "certutil")) {
+ if (SystemUtilities.isUbuntu() && certType == SYSTEM && promptCertutil()) {
+ return ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools");
+ } else {
+ log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on browsers which depend on it.");
+ }
+ }
+ return false;
+ }
+
+ private boolean promptCertutil() {
+ // Assume silent installs want certutil
+ if(Installer.IS_SILENT) {
+ return true;
+ }
+ SystemUtilities.setSystemLookAndFeel();
+ return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(null, "A critical component, \"certutil\" wasn't found. Attempt to fetch it now?");
+ }
+}
diff --git a/src/qz/installer/certificate/MacCertificateInstaller.java b/src/qz/installer/certificate/MacCertificateInstaller.java
new file mode 100644
index 000000000..c547a182b
--- /dev/null
+++ b/src/qz/installer/certificate/MacCertificateInstaller.java
@@ -0,0 +1,91 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.installer.Installer;
+import qz.utils.ShellUtilities;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MacCertificateInstaller extends NativeCertificateInstaller {
+ private static final Logger log = LoggerFactory.getLogger(MacCertificateInstaller.class);
+
+ public static final String USER_STORE = System.getProperty("user.home") + "/Library/Keychains/login.keychain"; // aka login.keychain-db
+ public static final String SYSTEM_STORE = "/Library/Keychains/System.keychain";
+ private String certStore;
+
+ public MacCertificateInstaller(Installer.PrivilegeLevel certType) {
+ setInstallType(certType);
+ }
+
+ public boolean add(File certFile) {
+ if (certStore.equals(USER_STORE)) {
+ // This will prompt the user
+ return ShellUtilities.execute("security", "add-trusted-cert", "-r", "trustRoot", "-k", certStore, certFile.getPath());
+ } else {
+ return ShellUtilities.execute("security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", certStore, certFile.getPath());
+ }
+ }
+
+ public boolean remove(List idList) {
+ boolean success = true;
+ for (String certId : idList) {
+ success = success && ShellUtilities.execute("security", "delete-certificate", "-Z", certId, certStore);
+ }
+ return success;
+ }
+
+ public List find() {
+ ArrayList hashList = new ArrayList<>();
+ try {
+ Process p = Runtime.getRuntime().exec(new String[] {"security", "find-certificate", "-e", Constants.ABOUT_EMAIL, "-Z", certStore});
+ BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
+ String line;
+ while ((line = in.readLine()) != null) {
+ if (line.contains("SHA-1") && line.contains(":")) {
+ hashList.add(line.split(":", 2)[1].trim());
+ }
+ }
+ in.close();
+ } catch(IOException e) {
+ log.warn("Could not get certificate list", e);
+ }
+ return hashList;
+ }
+
+ public boolean verify(File certFile) {
+ return ShellUtilities.execute( "security", "verify-cert", "-c", certFile.getPath());
+ }
+
+ public void setInstallType(Installer.PrivilegeLevel type) {
+ if (type == Installer.PrivilegeLevel.USER) {
+ certStore = USER_STORE;
+ } else {
+ certStore = SYSTEM_STORE;
+ }
+ }
+
+ public Installer.PrivilegeLevel getInstallType() {
+ if (certStore == USER_STORE) {
+ return Installer.PrivilegeLevel.USER;
+ } else {
+ return Installer.PrivilegeLevel.SYSTEM;
+ }
+ }
+}
diff --git a/src/qz/installer/certificate/NativeCertificateInstaller.java b/src/qz/installer/certificate/NativeCertificateInstaller.java
new file mode 100644
index 000000000..dcc14c7f9
--- /dev/null
+++ b/src/qz/installer/certificate/NativeCertificateInstaller.java
@@ -0,0 +1,92 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.installer.Installer;
+import qz.utils.SystemUtilities;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+public abstract class NativeCertificateInstaller {
+ private static final Logger log = LoggerFactory.getLogger(NativeCertificateInstaller.class);
+ protected static NativeCertificateInstaller instance;
+
+ public static NativeCertificateInstaller getInstance() {
+ return getInstance(SystemUtilities.isAdmin() ? Installer.PrivilegeLevel.SYSTEM : Installer.PrivilegeLevel.USER);
+ }
+ public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) {
+ if (instance == null) {
+ if (SystemUtilities.isWindows()) {
+ instance = new WindowsCertificateInstaller(type);
+ } else if(SystemUtilities.isMac()) {
+ instance = new MacCertificateInstaller(type);
+ } else {
+ instance = new LinuxCertificateInstaller(type);
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * Install a certificate from memory
+ */
+ public boolean install(X509Certificate cert) {
+ try {
+ File certFile = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
+ JcaMiscPEMGenerator generator = new JcaMiscPEMGenerator(cert);
+ JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(certFile.toPath(), StandardOpenOption.CREATE)));
+ writer.writeObject(generator.generate());
+ writer.close();
+
+ return install(certFile);
+ } catch(IOException e) {
+ log.warn("Could not install cert from temp file", e);
+ }
+ return false;
+ }
+
+ /**
+ * Install a certificate from disk
+ */
+ public boolean install(File certFile) {
+ String helper = instance.getClass().getSimpleName();
+ String store = instance.getInstallType().name();
+ if (remove(find())) {
+ log.info("Certificate removed from {} store using {}", store, helper);
+ } else {
+ log.warn("Could not remove certificate from {} store using {}", store, helper);
+ }
+ if (add(certFile)) {
+ log.info("Certificate added to {} store using {}", store, helper);
+ return true;
+ } else {
+ log.warn("Could not install certificate to {} store using {}", store, helper);
+ }
+ return false;
+ }
+
+ public abstract boolean add(File certFile);
+ public abstract boolean remove(List idList);
+ public abstract List find();
+ public abstract boolean verify(File certFile);
+ public abstract void setInstallType(Installer.PrivilegeLevel certType);
+ public abstract Installer.PrivilegeLevel getInstallType();
+}
diff --git a/src/qz/installer/certificate/WindowsCertificateInstaller.java b/src/qz/installer/certificate/WindowsCertificateInstaller.java
new file mode 100644
index 000000000..3a03be3de
--- /dev/null
+++ b/src/qz/installer/certificate/WindowsCertificateInstaller.java
@@ -0,0 +1,236 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import com.sun.jna.Memory;
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.Structure;
+import com.sun.jna.platform.win32.Kernel32Util;
+import com.sun.jna.platform.win32.WinNT;
+import com.sun.jna.win32.StdCallLibrary;
+import com.sun.jna.win32.W32APIOptions;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.openssl.PEMParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.installer.Installer;
+
+import java.io.*;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+
+public class WindowsCertificateInstaller extends NativeCertificateInstaller {
+ private static final Logger log = LoggerFactory.getLogger(WindowsCertificateInstaller.class);
+ private WinCrypt.HCERTSTORE store;
+ private byte[] certBytes;
+ private Installer.PrivilegeLevel certType;
+
+ public WindowsCertificateInstaller(Installer.PrivilegeLevel certType) {
+ setInstallType(certType);
+ }
+
+ public boolean add(File certFile) {
+ log.info("Writing certificate {} to {} store using Crypt32...", certFile, certType);
+ try {
+
+ byte[] bytes = getCertBytes(certFile);
+ Pointer pointer = new Memory(bytes.length);
+ pointer.write(0, bytes, 0, bytes.length);
+
+ boolean success = Crypt32.INSTANCE.CertAddEncodedCertificateToStore(
+ openStore(),
+ WinCrypt.X509_ASN_ENCODING,
+ pointer,
+ bytes.length,
+ Crypt32.CERT_STORE_ADD_REPLACE_EXISTING,
+ null
+ );
+ if(!success) {
+ log.warn(Kernel32Util.formatMessage(Native.getLastError()));
+ }
+
+ closeStore();
+
+ return success;
+ } catch(IOException e) {
+ log.warn("An error occurred installing the certificate", e);
+ } finally {
+ certBytes = null;
+ }
+ return false;
+ }
+
+ private byte[] getCertBytes(File certFile) throws IOException {
+ if(certBytes == null) {
+ PEMParser pem = new PEMParser(new FileReader(certFile));
+ X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
+ certBytes = certHolder.getEncoded();
+ }
+ return certBytes;
+ }
+
+ private WinCrypt.HCERTSTORE openStore() {
+ if(store == null) {
+ store = openStore(certType);
+ }
+ return store;
+ }
+
+ private void closeStore() {
+ if(store != null && closeStore(store)) {
+ store = null;
+ } else {
+ log.warn("Unable to close {} cert store", certType);
+ }
+ }
+
+ private static WinCrypt.HCERTSTORE openStore(Installer.PrivilegeLevel certType) {
+ log.info("Opening {} store using Crypt32...", certType);
+
+ WinCrypt.HCERTSTORE store = Crypt32.INSTANCE.CertOpenStore(
+ Crypt32.CERT_STORE_PROV_SYSTEM,
+ 0,
+ null,
+ certType == Installer.PrivilegeLevel.USER ? Crypt32.CERT_SYSTEM_STORE_CURRENT_USER : Crypt32.CERT_SYSTEM_STORE_LOCAL_MACHINE,
+ "ROOT"
+ );
+ if(store == null) {
+ log.warn(Kernel32Util.formatMessage(Native.getLastError()));
+ }
+ return store;
+ }
+
+ private static boolean closeStore(WinCrypt.HCERTSTORE certStore) {
+ boolean isClosed = Crypt32.INSTANCE.CertCloseStore(
+ certStore, 0
+ );
+ if(!isClosed) {
+ log.warn(Kernel32Util.formatMessage(Native.getLastError()));
+ }
+ return isClosed;
+ }
+
+ public boolean remove(List ignore) {
+ boolean success = true;
+
+ WinCrypt.CERT_CONTEXT hCertContext;
+ WinCrypt.CERT_CONTEXT pPrevCertContext = null;
+ while(true) {
+ hCertContext = Crypt32.INSTANCE.CertFindCertificateInStore(
+ openStore(),
+ WinCrypt.X509_ASN_ENCODING,
+ 0,
+ Crypt32.CERT_FIND_SUBJECT_STR,
+ Constants.ABOUT_EMAIL,
+ pPrevCertContext);
+
+ if(hCertContext == null) {
+ break;
+ }
+
+ pPrevCertContext = Crypt32.INSTANCE.CertDuplicateCertificateContext(hCertContext);
+
+ if(success = (success && Crypt32.INSTANCE.CertDeleteCertificateFromStore(hCertContext))) {
+ log.info("Successfully deleted certificate matching {}", Constants.ABOUT_EMAIL);
+ } else {
+ log.info("Could not delete certificate: {}", Kernel32Util.formatMessage(Native.getLastError()));
+ }
+ }
+
+ closeStore();
+ return success;
+ }
+
+ public List find() {
+ return null;
+ }
+
+ public void setInstallType(Installer.PrivilegeLevel type) {
+ this.certType = type;
+ }
+
+ public Installer.PrivilegeLevel getInstallType() {
+ return certType;
+ }
+
+ public boolean verify(File certFile) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(getCertBytes(certFile));
+ WinCrypt.DATA_BLOB thumbPrint = new WinCrypt.DATA_BLOB(md.digest());
+ WinNT.HANDLE cert = Crypt32.INSTANCE.CertFindCertificateInStore(
+ openStore(),
+ WinCrypt.X509_ASN_ENCODING,
+ 0,
+ Crypt32.CERT_FIND_SHA1_HASH,
+ thumbPrint,
+ null);
+
+ return cert != null;
+ } catch(IOException | NoSuchAlgorithmException e) {
+ log.warn("An error occurred verifying the cert is installed: {}", certFile, e);
+ }
+ return false;
+ }
+
+ /**
+ * The JNA's Crypt32 instance oversimplifies store handling, preventing user stores from being used
+ */
+ interface Crypt32 extends StdCallLibrary {
+ int CERT_SYSTEM_STORE_CURRENT_USER = 65536;
+ int CERT_SYSTEM_STORE_LOCAL_MACHINE = 131072;
+ int CERT_STORE_PROV_SYSTEM = 10;
+ int CERT_STORE_ADD_REPLACE_EXISTING = 3;
+ int CERT_FIND_SUBJECT_STR = 524295;
+ int CERT_FIND_SHA1_HASH = 65536;
+
+ Crypt32 INSTANCE = Native.loadLibrary("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS);
+
+ WinCrypt.HCERTSTORE CertOpenStore(int lpszStoreProvider, int dwMsgAndCertEncodingType, Pointer hCryptProv, int dwFlags, String pvPara);
+ boolean CertCloseStore(WinCrypt.HCERTSTORE hCertStore, int dwFlags);
+ boolean CertAddEncodedCertificateToStore(WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, Pointer pbCertEncoded, int cbCertEncoded, int dwAddDisposition, Pointer ppCertContext);
+ WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, String pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
+ WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, Structure pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
+ boolean CertDeleteCertificateFromStore(WinCrypt.CERT_CONTEXT pCertContext);
+ boolean CertFreeCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
+ WinCrypt.CERT_CONTEXT CertDuplicateCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
+ }
+
+ // Polyfill from JNA5+
+ @SuppressWarnings("UnusedDeclaration") //Library class
+ public static class WinCrypt {
+ public static int X509_ASN_ENCODING = 0x00000001;
+ public static class HCERTSTORE extends WinNT.HANDLE {
+ public HCERTSTORE() {}
+ public HCERTSTORE(Pointer p) {
+ super(p);
+ }
+ }
+ public static class CERT_CONTEXT extends WinNT.HANDLE {
+ public CERT_CONTEXT() {}
+ public CERT_CONTEXT(Pointer p) {
+ super(p);
+ }
+ }
+ public static class DATA_BLOB extends com.sun.jna.platform.win32.WinCrypt.DATA_BLOB {
+ // Wrap the constructor for code readability
+ public DATA_BLOB() {
+ super();
+ }
+ public DATA_BLOB(byte[] data) {
+ super(data);
+ }
+ }
+ }
+}
diff --git a/src/qz/installer/certificate/WindowsCertificateInstallerCli.java b/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
new file mode 100644
index 000000000..66c27de5b
--- /dev/null
+++ b/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
@@ -0,0 +1,136 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.installer.Installer;
+import qz.utils.ShellUtilities;
+import qz.utils.SystemUtilities;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Command Line technique for installing certificates on Windows
+ * Fallback class for when JNA is not available (e.g. Windows on ARM)
+ */
+@SuppressWarnings("UnusedDeclaration") //Library class
+public class WindowsCertificateInstallerCli extends NativeCertificateInstaller {
+ private static final Logger log = LoggerFactory.getLogger(WindowsCertificateInstallerCli.class);
+ private Installer.PrivilegeLevel certType;
+
+ public WindowsCertificateInstallerCli(Installer.PrivilegeLevel certType) {
+ setInstallType(certType);
+ }
+
+ public boolean add(File certFile) {
+ if (SystemUtilities.isWindowsXP()) return false;
+ if (certType == Installer.PrivilegeLevel.USER) {
+ // This will prompt the user
+ return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "-user", "Root", certFile.getPath());
+ } else {
+ return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "Root", certFile.getPath());
+ }
+ }
+
+ public boolean remove(List idList) {
+ if (SystemUtilities.isWindowsXP()) return false;
+ boolean success = true;
+ for (String certId : idList) {
+ if (certType == Installer.PrivilegeLevel.USER) {
+ success = success && ShellUtilities.execute("certutil.exe", "-delstore", "-user", "Root", certId);
+ } else {
+ success = success && ShellUtilities.execute("certutil.exe", "-delstore", "Root", certId);
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Returns a list of serials, if found
+ */
+ public List find() {
+ ArrayList serialList = new ArrayList<>();
+ try {
+ Process p;
+ if (certType == Installer.PrivilegeLevel.USER) {
+ p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "-user", "Root"});
+ } else {
+ p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "Root"});
+ }
+ BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
+ String line;
+ while ((line = in.readLine()) != null) {
+ if (line.contains("================")) {
+ // First line is serial
+ String serial = parseNextLine(in);
+ if (serial != null) {
+ // Second line is issuer
+ String issuer = parseNextLine(in);
+ if (issuer.contains("OU=" + Constants.ABOUT_COMPANY)) {
+ serialList.add(serial);
+ }
+ }
+ }
+ }
+ in.close();
+ } catch(Exception e) {
+ log.info("Unable to find a Trusted Root Certificate matching \"OU={}\"", Constants.ABOUT_COMPANY);
+ }
+ return serialList;
+ }
+
+ public boolean verify(File certFile) {
+ return verifyCert(certFile);
+ }
+
+ public static boolean verifyCert(File certFile) {
+ // -user also will check the root store
+ String dwErrorStatus = ShellUtilities.execute( new String[] {"certutil", "-user", "-verify", certFile.getPath() }, new String[] { "dwErrorStatus=" }, false, false);
+ if(!dwErrorStatus.isEmpty()) {
+ String[] parts = dwErrorStatus.split("[\r\n\\s]+");
+ for(String part : parts) {
+ if(part.startsWith("dwErrorStatus=")) {
+ log.info("Certificate validity says {}", part);
+ String[] status = part.split("=", 2);
+ if (status.length == 2) {
+ return status[1].trim().equals("0");
+ }
+ }
+ }
+ }
+ log.warn("Unable to determine certificate validity, you'll be prompted on startup");
+ return false;
+ }
+
+ public void setInstallType(Installer.PrivilegeLevel type) {
+ this.certType = type;
+ }
+
+ public Installer.PrivilegeLevel getInstallType() {
+ return certType;
+ }
+
+ private static String parseNextLine(BufferedReader reader) throws IOException {
+ String data = reader.readLine();
+ if (data != null) {
+ String[] split = data.split(":", 2);
+ if (split.length == 2) {
+ return split[1].trim();
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java b/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java
new file mode 100644
index 000000000..ecb9bccef
--- /dev/null
+++ b/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java
@@ -0,0 +1,158 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate.firefox;
+
+import com.github.zafarkhaja.semver.Version;
+import org.codehaus.jettison.json.JSONException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.installer.certificate.CertificateManager;
+import qz.installer.certificate.firefox.locator.AppAlias;
+import qz.installer.certificate.firefox.locator.AppLocator;
+import qz.utils.JsonWriter;
+import qz.utils.SystemUtilities;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+
+/**
+ * Installs the Firefox Policy file via Policy file or AutoConfig, depending on the version
+ */
+public class FirefoxCertificateInstaller {
+ protected static final Logger log = LoggerFactory.getLogger(FirefoxCertificateInstaller.class);
+
+ /**
+ * Versions are for Mozilla's official Firefox release.
+ * 3rd-party/clones may adopt Enterprise Policy support under
+ * different version numbers, adapt as needed.
+ */
+ private static final Version WINDOWS_POLICY_VERSION = Version.valueOf("62.0.0");
+ private static final Version MAC_POLICY_VERSION = Version.valueOf("63.0.0");
+ private static final Version LINUX_POLICY_VERSION = Version.valueOf("65.0.0");
+
+ private static String ENTERPRISE_ROOT_POLICY = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }";
+ private static String INSTALL_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION + "\"] } } }";
+ private static String REMOVE_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"/opt/" + Constants.PROPS_FILE + "/auth/root-ca.crt\"] } } }";
+
+ public static final String POLICY_LOCATION = "distribution/policies.json";
+ public static final String MAC_POLICY_LOCATION = "Contents/Resources/" + POLICY_LOCATION;
+
+ public static void install(X509Certificate cert, String ... hostNames) {
+ ArrayList appList = AppLocator.locate(AppAlias.FIREFOX);
+ for(AppLocator app : appList) {
+ if(honorsPolicy(app)) {
+ log.info("Installing Firefox ({}) enterprise root certificate policy {}", app.getName(), app.getPath());
+ installPolicy(app, cert);
+ } else {
+ log.info("Installing Firefox ({}) auto-config script {}", app.getName(), app.getPath());
+ try {
+ String certData = Base64.getEncoder().encodeToString(cert.getEncoded());
+ LegacyFirefoxCertificateInstaller.installAutoConfigScript(app, certData, hostNames);
+ } catch(CertificateEncodingException e) {
+ log.warn("Unable to install auto-config script to {}", app.getPath(), e);
+ }
+ }
+ }
+ }
+
+ public static void uninstall() {
+ ArrayList appList = AppLocator.locate(AppAlias.FIREFOX);
+ for(AppLocator app : appList) {
+ if(honorsPolicy(app)) {
+ if(SystemUtilities.isWindows() || SystemUtilities.isMac()) {
+ log.info("Skipping uninstall of Firefox enterprise root certificate policy {}", app.getPath());
+ } else {
+ try {
+ File policy = Paths.get(app.getPath(), POLICY_LOCATION).toFile();
+ if(policy.exists()) {
+ JsonWriter.write(Paths.get(app.getPath(), POLICY_LOCATION).toString(), INSTALL_CERT_POLICY, false, true);
+ }
+ } catch(IOException | JSONException e) {
+ log.warn("Unable to remove Firefox ({}) policy {}", app.getName(), e);
+ }
+ }
+
+ } else {
+ log.info("Uninstalling Firefox auto-config script {}", app.getPath());
+ LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(app);
+ }
+ }
+ }
+
+ public static boolean honorsPolicy(AppLocator app) {
+ if (app.getVersion() == null) {
+ log.warn("Firefox-compatible browser was found {}, but no version information is available", app.getPath());
+ return false;
+ }
+ if(SystemUtilities.isWindows()) {
+ return app.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION);
+ } else if (SystemUtilities.isMac()) {
+ return app.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION);
+ } else {
+ return app.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION);
+ }
+ }
+
+ public static void installPolicy(AppLocator app, X509Certificate cert) {
+ Path jsonPath = Paths.get(app.getPath(), SystemUtilities.isMac() ? MAC_POLICY_LOCATION : POLICY_LOCATION);
+ String jsonPolicy = SystemUtilities.isWindows() || SystemUtilities.isMac() ? ENTERPRISE_ROOT_POLICY : INSTALL_CERT_POLICY;
+ try {
+ if(jsonPolicy.equals(INSTALL_CERT_POLICY)) {
+ // Linux lacks the concept of "enterprise roots", we'll write it to a known location instead
+ File certFile = new File("/usr/lib/mozilla/certificates", Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
+
+ // Make sure we can traverse and read
+ File certs = new File("/usr/lib/mozilla/certificates");
+ certs.mkdirs();
+ certs.setReadable(true, false);
+ certs.setExecutable(true, false);
+ File mozilla = certs.getParentFile();
+ mozilla.setReadable(true, false);
+ mozilla.setExecutable(true, false);
+
+ // Make sure we can read
+ CertificateManager.writeCert(cert, certFile);
+ certFile.setReadable(true, false);
+ }
+
+ File jsonFile = jsonPath.toFile();
+
+ // Make sure we can traverse and read
+ File distribution = jsonFile.getParentFile();
+ distribution.mkdirs();
+ distribution.setReadable(true, false);
+ distribution.setExecutable(true, false);
+
+ if(jsonPolicy.equals(INSTALL_CERT_POLICY)) {
+ // Delete previous policy
+ JsonWriter.write(jsonPath.toString(), REMOVE_CERT_POLICY, false, true);
+ }
+
+ JsonWriter.write(jsonPath.toString(), jsonPolicy, false, false);
+
+ // Make sure ew can read
+ jsonFile.setReadable(true, false);
+ } catch(JSONException | IOException e) {
+ log.warn("Could not install enterprise policy {} to {}", jsonPolicy, jsonPath.toString(), e);
+ }
+ }
+
+ public static boolean checkRunning(AppLocator app, boolean isSilent) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java b/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java
new file mode 100644
index 000000000..c998d0686
--- /dev/null
+++ b/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java
@@ -0,0 +1,142 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate.firefox;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.installer.certificate.CertificateChainBuilder;
+import qz.installer.certificate.firefox.locator.AppLocator;
+import qz.utils.FileUtilities;
+import qz.utils.SystemUtilities;
+
+import java.io.*;
+import java.security.cert.CertificateEncodingException;
+import java.util.*;
+
+/**
+ * Legacy Firefox Certificate installer
+ *
+ * For old Firefox-compatible browsers still in the wild such as Firefox 52 ESR, SeaMonkey, WaterFox, etc.
+ */
+public class LegacyFirefoxCertificateInstaller {
+ private static final Logger log = LoggerFactory.getLogger(CertificateChainBuilder.class);
+
+ private static final String CFG_TEMPLATE = "assets/firefox-autoconfig.js.in";
+ private static final String CFG_FILE = Constants.PROPS_FILE + ".cfg";
+ private static final String PREFS_FILE = Constants.PROPS_FILE + ".js";
+ private static final String PREFS_DIR = "defaults/pref";
+ private static final String MAC_PREFIX = "Contents/Resources";
+
+ public static void installAutoConfigScript(AppLocator app, String certData, String ... hostNames) {
+ try {
+ writePrefsFile(app);
+ writeParsedConfig(app, certData, false, hostNames);
+ } catch(Exception e) {
+ log.warn("Error installing auto-config support for {}", app.getName(), e);
+ }
+ }
+
+ public static void uninstallAutoConfigScript(AppLocator app) {
+ try {
+ writeParsedConfig(app, "", true);
+ } catch(Exception e) {
+ log.warn("Error uninstalling auto-config support for {}", app.getName(), e);
+ }
+ }
+
+ public static File tryWrite(AppLocator app, boolean mkdirs, String ... paths) throws IOException {
+ String dir = app.getPath();
+ if (SystemUtilities.isMac()) {
+ dir += File.separator + MAC_PREFIX;
+ }
+ for (String path : paths) {
+ dir += File.separator + path;
+ }
+ File file = new File(dir);
+
+ if(mkdirs) file.mkdirs();
+ if(file.exists() && file.isDirectory() && file.canWrite()) {
+ return file;
+ }
+
+ throw new IOException(String.format("Directory does not exist or is not writable: %s", file));
+ }
+
+ public static void deleteFile(File parent, String ... paths) {
+ if(parent != null) {
+ String toDelete = parent.getPath();
+ for (String path : paths) {
+ toDelete += File.separator + path;
+ }
+ File deleteFile = new File(toDelete);
+ if (!deleteFile.exists()) {
+ } else if (new File(toDelete).delete()) {
+ log.info("Deleted old file: {}", toDelete);
+ } else {
+ log.warn("Could not delete old file: {}", toDelete);
+ }
+ }
+ }
+
+ public static void writePrefsFile(AppLocator app) throws Exception {
+ File prefsDir = tryWrite(app, true, PREFS_DIR);
+ deleteFile(prefsDir, "firefox-prefs.js"); // cleanup old version
+
+ // first check that there aren't other prefs files
+ String pref = "general.config.filename";
+ for (File file : prefsDir.listFiles()) {
+ try {
+ BufferedReader reader = new BufferedReader(new FileReader(file));
+ String line;
+ while((line = reader.readLine()) != null) {
+ if(line.contains(pref) && !line.contains(CFG_FILE)) {
+ throw new Exception(String.format("Browser already has %s defined in %s:\n %s", pref, file, line));
+ }
+ }
+ } catch(IOException ignore) {}
+ }
+
+ // write out the new prefs file
+ File prefsFile = new File(prefsDir, PREFS_FILE);
+ BufferedWriter writer = new BufferedWriter(new FileWriter(prefsFile));
+ String[] data = {
+ String.format("pref('%s', '%s');", pref, CFG_FILE),
+ "pref('general.config.obscure_value', 0);"
+ };
+ for (String line : data) {
+ writer.write(line + "\n");
+ }
+ writer.close();
+ prefsFile.setReadable(true, false);
+ }
+
+ private static void writeParsedConfig(AppLocator app, String certData, boolean uninstall, String ... hostNames) throws IOException, CertificateEncodingException{
+ if (hostNames.length == 0) hostNames = CertificateChainBuilder.DEFAULT_HOSTNAMES;
+
+ File cfgDir = tryWrite(app, false);
+ deleteFile(cfgDir, "firefox-config.cfg"); // cleanup old version
+ File dest = new File(cfgDir.getPath(), CFG_FILE);
+
+ HashMap fieldMap = new HashMap<>();
+ // Dynamic fields
+ fieldMap.put("%CERT_DATA%", certData);
+ fieldMap.put("%COMMON_NAME%", hostNames[0]);
+ fieldMap.put("%TIMESTAMP%", uninstall ? "-1" : "" + new Date().getTime());
+ fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.detectAppPath() != null ? SystemUtilities.detectAppPath().toString() : "" : "");
+ fieldMap.put("%UNINSTALL%", "" + uninstall);
+
+ FileUtilities.configureAssetFile(CFG_TEMPLATE, dest, fieldMap, LegacyFirefoxCertificateInstaller.class);
+ dest.setReadable(true, false);
+ }
+
+
+}
diff --git a/ant/firefox/firefox-config.cfg.in b/src/qz/installer/certificate/firefox/assets/firefox-autoconfig.js.in
similarity index 65%
rename from ant/firefox/firefox-config.cfg.in
rename to src/qz/installer/certificate/firefox/assets/firefox-autoconfig.js.in
index c3968b9ac..5366a96bf 100644
--- a/ant/firefox/firefox-config.cfg.in
+++ b/src/qz/installer/certificate/firefox/assets/firefox-autoconfig.js.in
@@ -1,21 +1,8 @@
-//##############################################################################
-//# Firefox AutoConfig for ${project.name} Software ${vendor.website} #
-//##############################################################################
-//# Copyright (C) 2017 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 #
-//# #
-//# NOTE: This certificate is unique and private to THIS PC ONLY. It was #
-//# created on-the-fly at install time for secure websockets to function with #
-//# ${project.name} software. #
-//# #
-//# For questions please visit ${vendor.website}/support #
-//##############################################################################
-
-
-var observer = {
+//
+// Firefox AutoConfig Certificate Installer for Legacy Firefox versions
+// This is part of the QZ Tray application
+//
+var serviceObserver = {
observe: function observe(aSubject, aTopic, aData) {
// Get NSS certdb object
var certdb = getCertDB();
@@ -32,15 +19,15 @@ var observer = {
// Compares the timestamp embedded in this script against that stored in the browser's about:config
function needsCert() {
try {
- return getPref("${project.filename}.installer.timestamp") != getInstallerTimestamp();
+ return getPref("%PROPS_FILE%.installer.timestamp") != "%TIMESTAMP%";
} catch(notfound) {}
return true;
}
// Installs the embedded base64 certificate into the browser
function installCertificate() {
- certdb.addCertFromBase64(getCertData(), "C,C,C", "${commonName} - ${vendor.company}");
- pref("${project.filename}.installer.timestamp", getInstallerTimestamp());
+ certdb.addCertFromBase64(getCertData(), "C,C,C", "%COMMON_NAME% - %ABOUT_COMPANY%");
+ pref("%PROPS_FILE%.installer.timestamp", "%TIMESTAMP%");
}
// Deletes the certificate, if it exists
@@ -49,18 +36,19 @@ var observer = {
var enumerator = certs.getEnumerator();
while (enumerator.hasMoreElements()) {
var cert = enumerator.getNext().QueryInterface(Components.interfaces.nsIX509Cert);
- if (cert.containsEmailAddress("${vendor.email}")) {
+ if (cert.containsEmailAddress("%ABOUT_EMAIL%")) {
try {
certdb.deleteCertificate(cert);
} catch (ignore) {}
}
}
+ pref("%PROPS_FILE%.installer.timestamp", "-1");
}
// Register the specified protocol to open with the specified application
function registerProtocol() {
// Only register if platform needs it (e.g. macOS)
- var trayApp = "${trayApp}";
+ var trayApp = "%APP_PATH%";
if (!trayApp) { return; }
try {
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
@@ -71,9 +59,9 @@ var observer = {
var lhandler = Components.classes["@mozilla.org/uriloader/local-handler-app;1"].createInstance(Components.interfaces.nsILocalHandlerApp);
lhandler.executable = file;
- lhandler.name = "${project.name}";
+ lhandler.name = "%PROPS_FILE%";
- var protocol = pservice.getProtocolHandlerInfo("${vendor.name}");
+ var protocol = pservice.getProtocolHandlerInfo("%DATA_DIR%");
protocol.preferredApplicationHandler = lhandler;
protocol.preferredAction = 2; // useHelperApp
protocol.alwaysAskBeforeHandling = false;
@@ -84,12 +72,12 @@ var observer = {
// De-register the specified protocol from opening with the specified application
function unregisterProtocol() {
// Only register if platform needs it (e.g. macOS)
- var trayApp = "${trayApp}";
+ var trayApp = "%APP_PATH%";
if (!trayApp) { return; }
try {
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
- hservice.remove(pservice.getProtocolHandlerInfo("${vendor.name}"));
+ hservice.remove(pservice.getProtocolHandlerInfo("%DATA_DIR%"));
} catch(ignore) {}
}
@@ -107,28 +95,23 @@ var observer = {
// The certificate to import (automatically generated by desktop installer)
function getCertData() {
- return "${certData}";
- }
-
- // The timestamp created by the desktop installer
- function getInstallerTimestamp() {
- return "${timestamp}";
+ return "%CERT_DATA%";
}
// Whether or not an uninstall should occur, flagged by the installer/uninstaller
function needsUninstall() {
try {
- if (getPref("${project.filename}.installer.timestamp") == "-1") {
+ if (getPref("%PROPS_FILE%.installer.timestamp") == "-1") {
return false;
}
}
catch(notfound) {
return false;
}
- return ${uninstall};
+ return %UNINSTALL%;
}
}
};
Components.utils.import("resource://gre/modules/Services.jsm");
-Services.obs.addObserver(observer, "profile-after-change", false);
\ No newline at end of file
+Services.obs.addObserver(serviceObserver, "profile-after-change", false);
\ No newline at end of file
diff --git a/src/qz/installer/certificate/firefox/locator/AppAlias.java b/src/qz/installer/certificate/firefox/locator/AppAlias.java
new file mode 100644
index 000000000..64b836ccd
--- /dev/null
+++ b/src/qz/installer/certificate/firefox/locator/AppAlias.java
@@ -0,0 +1,47 @@
+package qz.installer.certificate.firefox.locator;
+
+public enum AppAlias {
+ // Tor Browser intentionally excluded; Tor's proxy blocks localhost connections
+ FIREFOX(
+ // Alias([Vendor], Name)
+ new Alias("Firefox"), // macOS, Linux
+ new Alias("Mozilla", "Mozilla Firefox"), // Windows
+ new Alias("Mozilla", "SeaMonkey"),
+ new Alias("Mozilla", "Waterfox"),
+ new Alias("Mozilla", "Pale Moon"),
+ new Alias("Mozilla", "IceCat")
+ );
+ Alias[] aliases;
+ AppAlias(Alias... aliases) {
+ this.aliases = aliases;
+ }
+
+ public Alias[] getAliases() {
+ return aliases;
+ }
+
+ public boolean matches(AppLocator info) {
+ if (info.getName() != null && !info.isBlacklisted()) {
+ for (Alias alias : aliases) {
+ if (info.getName().toLowerCase().matches(alias.name.toLowerCase())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static class Alias {
+ public String vendor;
+ public String name;
+ public String posix;
+ public Alias(String name) {
+ this.name = name;
+ this.posix = name.replaceAll(" ", "").toLowerCase();
+ }
+ public Alias(String vendor, String name) {
+ this(name);
+ this.vendor = vendor;
+ }
+ }
+}
diff --git a/src/qz/installer/certificate/firefox/locator/AppLocator.java b/src/qz/installer/certificate/firefox/locator/AppLocator.java
new file mode 100644
index 000000000..891b1ddf1
--- /dev/null
+++ b/src/qz/installer/certificate/firefox/locator/AppLocator.java
@@ -0,0 +1,66 @@
+package qz.installer.certificate.firefox.locator;
+
+import com.github.zafarkhaja.semver.Version;
+import qz.utils.SystemUtilities;
+
+import java.util.ArrayList;
+
+public abstract class AppLocator {
+ String name;
+ String path;
+ Version version;
+ void setName(String name) {
+ this.name = name;
+ }
+
+ void setPath(String path) {
+ this.path = path;
+ }
+
+ void setVersion(String version) {
+ try {
+ // Less than three octets (e.g. "56.0") will fail parsing
+ while(version.split("\\.").length < 3) {
+ version = version + ".0";
+ }
+ if (version != null) {
+ this.version = Version.valueOf(version);
+ }
+ } catch(Exception ignore) {}
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public Version getVersion() {
+ return version;
+ }
+
+ abstract boolean isBlacklisted();
+
+ public static ArrayList locate(AppAlias appAlias) {
+ if (SystemUtilities.isWindows()) {
+ return WindowsAppLocator.findApp(appAlias);
+ } else if (SystemUtilities.isMac()) {
+ return MacAppLocator.findApp(appAlias);
+ }
+ return LinuxAppLocator.findApp(appAlias) ;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if(o instanceof AppLocator && o != null && path != null) {
+ if (SystemUtilities.isWindows()) {
+ return path.equalsIgnoreCase(((AppLocator)o).getPath());
+ } else {
+ return path.equals(((AppLocator)o).getPath());
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java b/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
new file mode 100644
index 000000000..6d047cc5d
--- /dev/null
+++ b/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
@@ -0,0 +1,81 @@
+package qz.installer.certificate.firefox.locator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class LinuxAppLocator extends AppLocator {
+ private static final Logger log = LoggerFactory.getLogger(LinuxAppLocator.class);
+
+ public LinuxAppLocator(String name, String path) {
+ setName(name);
+ setPath(path);
+ }
+
+ public static ArrayList findApp(AppAlias appAlias) {
+ ArrayList appList = new ArrayList<>();
+
+ // Workaround for calling "firefox --version" as sudo
+ String[] env = appendPaths("HOME=/tmp");
+
+ // Search for matching executable in all path values
+ for(AppAlias.Alias alias : appAlias.aliases) {
+
+ // Add non-standard app search locations (e.g. Fedora)
+ for (String dirname : appendPaths(alias.posix, "/usr/lib/$/bin", "/usr/lib64/$/bin")) {
+ File file = new File(dirname, alias.posix);
+ if (file.isFile() && file.canExecute()) {
+ try {
+ file = file.getCanonicalFile(); // fix symlinks
+ AppLocator info = new LinuxAppLocator(alias.name, file.getParentFile().getCanonicalPath());
+ appList.add(info);
+
+ // Call "--version" on executable to obtain version information
+ Process p = Runtime.getRuntime().exec(new String[] {file.getCanonicalPath(), "--version" }, env);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
+ String version = reader.readLine();
+ reader.close();
+ if (version != null) {
+ if(version.contains(" ")) {
+ String[] split = version.split(" ");
+ info.setVersion(split[split.length - 1]);
+ } else {
+ info.setVersion(version.trim());
+ }
+ }
+ break;
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ return appList;
+ }
+
+
+ /**
+ * Returns a PATH value with provided paths appended, replacing "$" with POSIX app name
+ * Useful for strange Firefox install locations (e.g. Fedora)
+ *
+ * Usage: appendPaths("firefox", "/usr/lib64");
+ *
+ */
+ public static String[] appendPaths(String posix, String ... prefixes) {
+ String newPath = System.getenv("PATH");
+ for (String prefix : prefixes) {
+ newPath = newPath + File.pathSeparator + prefix.replaceAll("\\$", posix);
+ }
+ return newPath.split(File.pathSeparator);
+ }
+
+ @Override
+ boolean isBlacklisted() {
+ return false;
+ }
+}
diff --git a/src/qz/installer/certificate/firefox/locator/MacAppLocator.java b/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
new file mode 100644
index 000000000..5d078195a
--- /dev/null
+++ b/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
@@ -0,0 +1,103 @@
+package qz.installer.certificate.firefox.locator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+import qz.utils.ShellUtilities;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+
+public class MacAppLocator extends AppLocator {
+ protected static final Logger log = LoggerFactory.getLogger(MacAppLocator.class);
+
+ private static String[] BLACKLIST = new String[]{ "/Volumes/", "/.Trash/", "/Applications (Parallels)/" };
+
+ /**
+ * Helper class for finding key/value siblings from the DDM
+ */
+ private enum SiblingNode {
+ NAME("_name"),
+ PATH("path"),
+ VERSION("version");
+
+ private String key;
+ private boolean wants;
+
+ SiblingNode(String key) {
+ this.key = key;
+ this.wants = false;
+ }
+
+ private boolean isKey(Node node) {
+ if (node.getNodeName().equals("key") && node.getTextContent().equals(key)) {
+ this.wants = true;
+ return true;
+ }
+ return false;
+ }
+
+ private void set(Node node, AppLocator info) {
+ switch(this) {
+ case NAME: info.setName(node.getTextContent()); break;
+ case PATH: info.setPath(node.getTextContent()); break;
+ case VERSION: info.setVersion(node.getTextContent()); break;
+ default: throw new UnsupportedOperationException(this.name() + " not supported");
+ }
+ wants = false;
+ }
+ }
+
+ public boolean isBlacklisted() {
+ for (String item : BLACKLIST) {
+ if (path != null && path.matches(Pattern.quote(item))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static ArrayList findApp(AppAlias appAlias) {
+ ArrayList appList = new ArrayList<>();
+ Document doc;
+
+ try {
+ // system_profile benchmarks about 30% better than lsregister
+ Process p = Runtime.getRuntime().exec(new String[] {"system_profiler", "SPApplicationsDataType", "-xml"}, ShellUtilities.envp);
+ doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream());
+ } catch(IOException | ParserConfigurationException | SAXException e) {
+ log.warn("Could not retrieve app listing for {}", appAlias.name(), e);
+ return appList;
+ }
+ doc.normalizeDocument();
+
+ NodeList nodeList = doc.getElementsByTagName("dict");
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ NodeList dict = nodeList.item(i).getChildNodes();
+ MacAppLocator info = new MacAppLocator();
+ for (int j = 0; j < dict.getLength(); j++) {
+ Node node = dict.item(j);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ for (SiblingNode sibling : SiblingNode.values()) {
+ if (sibling.wants) {
+ sibling.set(node, info);
+ break;
+ } else if(sibling.isKey(node)) {
+ break;
+ }
+ }
+ }
+ }
+ if (appAlias.matches(info)) {
+ appList.add(info);
+ }
+ }
+ return appList;
+ }
+}
diff --git a/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java b/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java
new file mode 100644
index 000000000..1b8302928
--- /dev/null
+++ b/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java
@@ -0,0 +1,77 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.certificate.firefox.locator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.utils.WindowsUtilities;
+
+
+import java.io.File;
+import java.util.ArrayList;
+
+import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE;
+
+public class WindowsAppLocator extends AppLocator {
+ protected static final Logger log = LoggerFactory.getLogger(MacAppLocator.class);
+ private static String REG_TEMPLATE = "Software\\%s%s\\%s%s";
+
+ public WindowsAppLocator(String name, String path, String version) {
+ setName(name);
+ setPath(path);
+ setVersion(version);
+ }
+ public static ArrayList findApp(AppAlias appAlias) {
+ ArrayList appList = new ArrayList<>();
+ for (AppAlias.Alias alias : appAlias.aliases) {
+ if (alias.vendor != null) {
+ String[] suffixes = new String[]{ "", " ESR"};
+ String[] prefixes = new String[]{ "", "WOW6432Node\\"};
+ for (String suffix : suffixes) {
+ for (String prefix : prefixes) {
+ String key = String.format(REG_TEMPLATE, prefix, alias.vendor, alias.name, suffix);
+ AppLocator appLocator = getAppInfo(alias.name, key, suffix);
+ if (appLocator != null && !appList.contains(appLocator)) {
+ appList.add(appLocator);
+ }
+ }
+ }
+ }
+ }
+ return appList;
+ }
+
+ public static AppLocator getAppInfo(String name, String key, String suffix) {
+ String version = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "CurrentVersion");
+ if (version != null) {
+ version = version.split(" ")[0]; // chop off (x86 ...)
+ if (!suffix.isEmpty()) {
+ if (key.endsWith(suffix)) {
+ key = key.substring(0, key.length() - suffix.length());
+ }
+ version = version + suffix;
+ }
+ String path = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe");
+ if (path != null) {
+ // SemVer: Replace spaces in suffixes with dashes
+ path = new File(path).getParent();
+ version = version.replaceAll(" ", "-");
+ return new WindowsAppLocator(name, path, version);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ boolean isBlacklisted() {
+ return false;
+ }
+}
diff --git a/src/qz/installer/shortcut/LinuxShortcutCreator.java b/src/qz/installer/shortcut/LinuxShortcutCreator.java
new file mode 100644
index 000000000..e9c3768f5
--- /dev/null
+++ b/src/qz/installer/shortcut/LinuxShortcutCreator.java
@@ -0,0 +1,44 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.shortcut;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.installer.LinuxInstaller;
+
+/**
+ * @author Tres Finocchiaro
+ */
+class LinuxShortcutCreator extends ShortcutCreator {
+
+ private static final Logger log = LoggerFactory.getLogger(LinuxShortcutCreator.class);
+ private static String DESKTOP = System.getProperty("user.home") + "/Desktop/";
+
+ public boolean canAutoStart() {
+ return Files.exists(Paths.get(LinuxInstaller.STARTUP_DIR, LinuxInstaller.SHORTCUT_NAME));
+ }
+ public void createDesktopShortcut() {
+ copyShortcut(LinuxInstaller.APP_LAUNCHER, DESKTOP);
+ }
+
+ private static void copyShortcut(String source, String target) {
+ try {
+ Files.copy(Paths.get(source), Paths.get(target));
+ } catch(IOException e) {
+ log.warn("Error creating shortcut {}", target, e);
+ }
+ }
+}
+
diff --git a/src/qz/deploy/MacDeploy.java b/src/qz/installer/shortcut/MacShortcutCreator.java
similarity index 65%
rename from src/qz/deploy/MacDeploy.java
rename to src/qz/installer/shortcut/MacShortcutCreator.java
index 23e371056..c88ba57b2 100644
--- a/src/qz/deploy/MacDeploy.java
+++ b/src/qz/installer/shortcut/MacShortcutCreator.java
@@ -7,7 +7,7 @@
* 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.deploy;
+package qz.installer.shortcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -16,15 +16,12 @@
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import qz.common.Constants;
-import qz.utils.ShellUtilities;
+import qz.installer.MacInstaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
-import java.io.File;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -32,37 +29,16 @@
/**
* @author Tres Finocchiaro
*/
-class MacDeploy extends DeployUtilities {
+class MacShortcutCreator extends ShortcutCreator {
- private static final Logger log = LoggerFactory.getLogger(MacDeploy.class);
-
- private String desktopShortcut = System.getProperty("user.home") + "/Desktop/" + getShortcutName();
-
- @Override
- public String getJarPath() {
- String jarPath = super.getJarPath();
- try {
- jarPath = URLDecoder.decode(jarPath, "UTF-8");
- }
- catch(UnsupportedEncodingException e) {
- log.error("Error decoding URL: {}", jarPath, e);
- }
-
- return jarPath;
- }
-
- private String getJarName() {
- return new File(getJarPath()).getName();
- }
+ private static final Logger log = LoggerFactory.getLogger(MacShortcutCreator.class);
+ private static String SHORTCUT_PATH = System.getProperty("user.home") + "/Desktop/" + Constants.ABOUT_TITLE;
/**
* Verify LaunchAgents plist file exists and parse it to verify it's enabled
*/
@Override
public boolean canAutoStart() {
- // FIXME: removeLegacyStartup should only run once per machine
- removeLegacyStartup();
-
// plist is stored as io.qz.plist
String parent = "/Library/LaunchAgents";
String[] parts = Constants.ABOUT_URL.split("/");
@@ -108,31 +84,11 @@ public boolean canAutoStart() {
return false;
}
- @Override
- public boolean createDesktopShortcut() {
- return ShellUtilities.execute(new String[] {"ln", "-sf", getAppPath(), desktopShortcut});
- }
-
- private boolean removeLegacyStartup() {
- return ShellUtilities.executeAppleScript(
- "tell application \"System Events\" to delete "
- + "every login item where name is \"" + getShortcutName() + "\" or "
- + "name is \"" + getJarName() + "\""
- );
- }
-
- /**
- * Returns path to executable jar or app bundle
- */
- private String getAppPath() {
- String target = getJarPath();
- if (target.contains("/Applications/")) {
- // Use the parent folder instead i.e. "/Applications/QZ Tray.app"
- File f = new File(getJarPath());
- if (f.getParent() != null) {
- return f.getParent();
- }
+ public void createDesktopShortcut() {
+ try {
+ Files.createSymbolicLink(Paths.get(MacInstaller.getAppPath()), Paths.get(SHORTCUT_PATH));
+ } catch(IOException e) {
+ log.warn("Could not create desktop shortcut {}", SHORTCUT_PATH, e);
}
- return target;
}
}
diff --git a/src/qz/installer/shortcut/ShortcutCreator.java b/src/qz/installer/shortcut/ShortcutCreator.java
new file mode 100644
index 000000000..6d7458d84
--- /dev/null
+++ b/src/qz/installer/shortcut/ShortcutCreator.java
@@ -0,0 +1,41 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.installer.shortcut;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.utils.SystemUtilities;
+
+/**
+ * Utility class for creating, querying and removing startup shortcuts and
+ * desktop shortcuts.
+ *
+ * @author Tres Finocchiaro
+ */
+public abstract class ShortcutCreator {
+ private static ShortcutCreator instance;
+ protected static final Logger log = LoggerFactory.getLogger(ShortcutCreator.class);
+ public abstract boolean canAutoStart();
+ public abstract void createDesktopShortcut();
+
+ public static ShortcutCreator getInstance() {
+ if (instance == null) {
+ if (SystemUtilities.isWindows()) {
+ instance = new WindowsShortcutCreator();
+ } else if (SystemUtilities.isMac()) {
+ instance = new MacShortcutCreator();
+ } else {
+ instance = new LinuxShortcutCreator();
+ }
+ }
+ return instance;
+ }
+}
diff --git a/src/qz/installer/shortcut/WindowsShortcutCreator.java b/src/qz/installer/shortcut/WindowsShortcutCreator.java
new file mode 100644
index 000000000..18f5090ef
--- /dev/null
+++ b/src/qz/installer/shortcut/WindowsShortcutCreator.java
@@ -0,0 +1,52 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ *
+ */
+
+package qz.installer.shortcut;
+
+import mslinks.ShellLink;
+import qz.common.Constants;
+import qz.installer.WindowsSpecialFolders;
+import qz.utils.SystemUtilities;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.*;
+
+/**
+ * @author Tres Finocchiaro
+ */
+public class WindowsShortcutCreator extends ShortcutCreator {
+ private static String SHORTCUT_NAME = Constants.ABOUT_TITLE + ".lnk";
+
+ public void createDesktopShortcut() {
+ createShortcut(WindowsSpecialFolders.DESKTOP.toString());
+ }
+
+ public boolean canAutoStart() {
+ return Files.exists(Paths.get(WindowsSpecialFolders.COMMON_STARTUP.toString(), SHORTCUT_NAME));
+ }
+
+ private void createShortcut(String folderPath) {
+ try {
+ ShellLink.createLink(getAppPath(), folderPath + File.separator + SHORTCUT_NAME);
+ }
+ catch(IOException ex) {
+ log.warn("Error creating desktop shortcut", ex);
+ }
+ }
+
+ /**
+ * Calculates .exe path from .jar
+ */
+ private static String getAppPath() {
+ return SystemUtilities.getJarPath().replaceAll(".jar$", ".exe");
+ }
+}
diff --git a/src/qz/printer/action/PrintHTML.java b/src/qz/printer/action/PrintHTML.java
index 48dd83afe..1a1624504 100644
--- a/src/qz/printer/action/PrintHTML.java
+++ b/src/qz/printer/action/PrintHTML.java
@@ -21,7 +21,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.common.Constants;
-import qz.deploy.DeployUtilities;
import qz.printer.PrintOptions;
import qz.printer.PrintOutput;
import qz.utils.PrintingUtilities;
diff --git a/src/qz/printer/action/WebApp.java b/src/qz/printer/action/WebApp.java
index 33bfda6f3..99029620f 100644
--- a/src/qz/printer/action/WebApp.java
+++ b/src/qz/printer/action/WebApp.java
@@ -28,7 +28,6 @@
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import qz.common.Constants;
-import qz.deploy.DeployUtilities;
import qz.utils.SystemUtilities;
import java.awt.image.BufferedImage;
@@ -120,7 +119,7 @@ public WebApp() {
public static synchronized void initialize() throws IOException {
//JavaFX native libs
if (SystemUtilities.isJar() && Constants.JAVA_VERSION.greaterThanOrEqualTo(Version.valueOf("11.0.0"))) {
- System.setProperty("java.library.path", new File(DeployUtilities.detectJarPath()).getParent() + "/libs/");
+ System.setProperty("java.library.path", new File(SystemUtilities.detectJarPath()).getParent() + "/libs/");
}
if (instance == null) {
diff --git a/src/qz/printer/info/WindowsPrinterMap.java b/src/qz/printer/info/WindowsPrinterMap.java
index 673b22870..71be2b00a 100644
--- a/src/qz/printer/info/WindowsPrinterMap.java
+++ b/src/qz/printer/info/WindowsPrinterMap.java
@@ -1,6 +1,7 @@
package qz.printer.info;
import com.sun.jna.platform.win32.Advapi32Util;
+import qz.utils.WindowsUtilities;
import javax.print.PrintService;
@@ -21,23 +22,16 @@ public synchronized NativePrinterMap putAll(PrintService[] services) {
synchronized void fillAttributes(NativePrinter printer) {
String keyName = printer.getPrinterId().replaceAll("\\\\", ",");
String key = "SYSTEM\\CurrentControlSet\\Control\\Print\\Printers\\" + keyName;
- String driver = getRegString(HKEY_LOCAL_MACHINE, key, "Printer Driver");
+ String driver = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "Printer Driver");
if (driver == null) {
key = "Printers\\Connections\\" + keyName;
- String guid = getRegString(HKEY_CURRENT_USER, key, "GuidPrinter");
+ String guid = WindowsUtilities.getRegString(HKEY_CURRENT_USER, key, "GuidPrinter");
if (guid != null) {
String serverName = keyName.replaceAll(",,(.+),.+", "$1");
key = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Print\\Providers\\Client Side Rendering Print Provider\\Servers\\" + serverName + "\\Printers\\" + guid;
- driver = getRegString(HKEY_LOCAL_MACHINE, key, "Printer Driver");
+ driver = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "Printer Driver");
}
}
printer.setDriver(driver);
}
-
- private static String getRegString(HKEY root, String key, String value) {
- if (Advapi32Util.registryKeyExists(root, key) && Advapi32Util.registryValueExists(root, key, value)) {
- return Advapi32Util.registryGetStringValue(root, key, value);
- }
- return null;
- }
}
diff --git a/src/qz/ui/AboutDialog.java b/src/qz/ui/AboutDialog.java
index 0640d16df..d3313885e 100644
--- a/src/qz/ui/AboutDialog.java
+++ b/src/qz/ui/AboutDialog.java
@@ -83,12 +83,9 @@ public void initComponents() {
lblUpdate = new JLabel();
updateButton = new JButton();
updateButton.setVisible(false);
- updateButton.addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent evt) {
- try { Desktop.getDesktop().browse(new URL(Constants.ABOUT_URL + "/download").toURI()); }
- catch(Exception e) { log.error("", e); }
- }
+ updateButton.addActionListener(evt -> {
+ try { Desktop.getDesktop().browse(new URL(Constants.ABOUT_DOWNLOAD_URL).toURI()); }
+ catch(Exception e) { log.error("", e); }
});
checkForUpdate();
versionBox.add(Box.createHorizontalStrut(12));
@@ -131,13 +128,13 @@ public void actionPerformed(ActionEvent evt) {
if (!limitedDisplay) {
LinkLabel lblLicensing = new LinkLabel("Licensing Information", 0.9f, false);
- lblLicensing.setLinkLocation(Constants.ABOUT_URL + "/licensing");
+ lblLicensing.setLinkLocation(Constants.ABOUT_LICENSING_URL);
LinkLabel lblSupport = new LinkLabel("Support Information", 0.9f, false);
- lblSupport.setLinkLocation(Constants.ABOUT_URL + "/support");
+ lblSupport.setLinkLocation(Constants.ABOUT_SUPPORT_URL);
LinkLabel lblPrivacy = new LinkLabel("Privacy Policy", 0.9f, false);
- lblPrivacy.setLinkLocation(Constants.ABOUT_URL + "/privacy");
+ lblPrivacy.setLinkLocation(Constants.ABOUT_PRIVACY_URL);
JPanel supportPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 80, 10));
supportPanel.add(lblLicensing);
diff --git a/src/qz/ui/LogDialog.java b/src/qz/ui/LogDialog.java
index 78f2655dc..db547adef 100644
--- a/src/qz/ui/LogDialog.java
+++ b/src/qz/ui/LogDialog.java
@@ -5,6 +5,7 @@
import org.apache.log4j.WriterAppender;
import qz.ui.component.IconCache;
import qz.ui.component.LinkLabel;
+import qz.utils.FileUtilities;
import qz.utils.SystemUtilities;
import javax.swing.*;
@@ -39,8 +40,8 @@ public LogDialog(JMenuItem caller, IconCache iconCache) {
public void initComponents() {
setIconImage(getImage(IconCache.Icon.LOG_ICON));
- LinkLabel logDirLabel = new LinkLabel("Open Log Location");
- logDirLabel.setLinkLocation(new File(SystemUtilities.getDataDirectory() + File.separator));
+ LinkLabel logDirLabel = new LinkLabel(FileUtilities.USER_DIR + File.separator);
+ logDirLabel.setLinkLocation(new File(FileUtilities.USER_DIR + File.separator));
setHeader(logDirLabel);
logArea = new JTextArea(ROWS, COLS);
diff --git a/src/qz/ui/component/LinkLabel.java b/src/qz/ui/component/LinkLabel.java
index 11251ccd1..987ab3134 100644
--- a/src/qz/ui/component/LinkLabel.java
+++ b/src/qz/ui/component/LinkLabel.java
@@ -69,17 +69,7 @@ public void actionPerformed(ActionEvent ae) {
}
public void setLinkLocation(final File filePath) {
- addActionListener(new ActionListener() {
- @Override
- public void actionPerformed(ActionEvent ae) {
- try {
- ShellUtilities.browseDirectory(filePath.isDirectory()? filePath.getPath():filePath.getParent());
- }
- catch(IOException ioe) {
- log.error("", ioe);
- }
- }
- });
+ addActionListener(ae -> ShellUtilities.browseDirectory(filePath.isDirectory()? filePath.getPath():filePath.getParent()));
}
diff --git a/src/qz/ui/tray/TaskbarTrayIcon.java b/src/qz/ui/tray/TaskbarTrayIcon.java
index 5f51331db..44b625112 100644
--- a/src/qz/ui/tray/TaskbarTrayIcon.java
+++ b/src/qz/ui/tray/TaskbarTrayIcon.java
@@ -44,7 +44,7 @@ public void windowClosing(WindowEvent e) {
addWindowListener(this);
}
- // fixes Linux taskbar title per http://hg.netbeans.org/core-main/rev/5832261b8434
+ // fixes Linux taskbar title per http://hg.netbeans.org/core-main/rev/5832261b8434, JDK-6528430
public static void setTaskBarTitle(String title) {
try {
Class> toolkit = Toolkit.getDefaultToolkit().getClass();
diff --git a/src/qz/utils/ArgParser.java b/src/qz/utils/ArgParser.java
new file mode 100644
index 000000000..6468babd0
--- /dev/null
+++ b/src/qz/utils/ArgParser.java
@@ -0,0 +1,220 @@
+/**
+ * @author Tres Finocchiaro
+ *
+ * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
+ *
+ * LGPL 2.1 This is free software. This software and source code are released under
+ * the "LGPL 2.1 License". A copy of this license should be distributed with
+ * this software. http://www.gnu.org/licenses/lgpl-2.1.html
+ */
+
+package qz.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.common.Constants;
+import qz.common.SecurityInfo;
+import qz.exception.MissingArgException;
+import qz.installer.Installer;
+import qz.installer.TaskKiller;
+import qz.installer.certificate.CertificateManager;
+
+import java.io.File;
+import java.util.*;
+
+import static qz.common.Constants.*;
+import static qz.utils.ArgParser.ExitStatus.*;
+
+public class ArgParser {
+ public enum ExitStatus {
+ SUCCESS(0),
+ GENERAL_ERROR(1),
+ USAGE_ERROR(2),
+ NO_AUTOSTART(3);
+ private int code;
+ ExitStatus(int code) {
+ this.code = code;
+ }
+ public int getCode() {
+ return code;
+ }
+ }
+
+ protected static final Logger log = LoggerFactory.getLogger(ArgParser.class);
+ private List args;
+ private ExitStatus exitStatus;
+
+ public ArgParser(String[] args) {
+ this.exitStatus = SUCCESS;
+ this.args = new ArrayList<>(Arrays.asList(args));
+ }
+ public List getArgs() {
+ return args;
+ }
+
+ public ExitStatus getExitStatus() {
+ return exitStatus;
+ }
+
+ public int getExitCode() {
+ return exitStatus.getCode();
+ }
+
+ /**
+ * Gets the requested flag status
+ */
+ public boolean hasFlag(String ... matches) {
+ for(String match : matches) {
+ if (args.contains(match)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the argument value immediately following a command
+ * @throws MissingArgException
+ */
+ public String valueOf(String ... matches) throws MissingArgException {
+ for(String match : matches) {
+ if (args.contains(match)) {
+ int index = args.indexOf(match) + 1;
+ if (args.size() >= index + 1) {
+ String val = args.get(index);
+ if(!val.trim().isEmpty()) {
+ return val;
+ }
+ }
+ throw new MissingArgException();
+ }
+ }
+ return null;
+ }
+
+ public ExitStatus processInstallerArgs(Installer.InstallType type, List args) {
+ try {
+ switch(type) {
+ case PREINSTALL:
+ return Installer.preinstall() ? SUCCESS : SUCCESS; // don't abort on preinstall
+ case INSTALL:
+ // Handle destination
+ String dest = valueOf("-d", "--dest");
+ // Handle silent installs
+ boolean silent = hasFlag("-s", "--silent");
+ Installer.install(dest, silent); // exception will set error
+ return SUCCESS;
+ case CERTGEN:
+ TaskKiller.killAll();
+
+ // Handle trusted SSL certificate
+ String trustedKey = valueOf("-k", "--key");
+ String trustedCert = valueOf("-c", "--cert");
+ String trustedPfx = valueOf("--pfx", "--pkcs12");
+ String trustedPass = valueOf("-p", "--pass");
+ if (trustedKey != null && trustedCert != null) {
+ File key = new File(trustedKey);
+ File cert = new File(trustedCert);
+ if(key.exists() && cert.exists()) {
+ new CertificateManager(key, cert); // exception will set error
+ return SUCCESS;
+ }
+ log.error("One or more trusted files was not found.");
+ throw new MissingArgException();
+ } else if((trustedKey != null || trustedCert != null || trustedPfx != null) && trustedPass != null) {
+ String pfxPath = trustedPfx == null ? (trustedKey == null ? trustedCert : trustedKey) : trustedPfx;
+ File pfx = new File(pfxPath);
+
+ if(pfx.exists()) {
+ new CertificateManager(pfx, trustedPass.toCharArray()); // exception will set error
+ return SUCCESS;
+ }
+ log.error("The provided pfx/pkcs12 file was not found: {}", pfxPath);
+ throw new MissingArgException();
+ } else {
+ // Handle localhost override
+ String hosts = valueOf("--host", "--hosts");
+ if (hosts != null) {
+ Installer.getInstance().certGen(true, hosts.split(";"));
+ return SUCCESS;
+ }
+ Installer.getInstance().certGen(true);
+ // Failure in this step is extremely rare, but
+ return SUCCESS; // exception will set error
+ }
+ case UNINSTALL:
+ Installer.uninstall();
+ return SUCCESS;
+ case SPAWN:
+ Installer.getInstance().spawn(args);
+ return SUCCESS;
+ default:
+ throw new UnsupportedOperationException("Installation type " + type + " is not yet supported");
+ }
+ } catch(MissingArgException e) {
+ log.error("Valid usage:\n java -jar {}.jar {}", PROPS_FILE, type.usage);
+ return USAGE_ERROR;
+ } catch(Exception e) {
+ log.error("Installation step {} failed", type, e);
+ return GENERAL_ERROR;
+ }
+ }
+
+ /**
+ * Attempts to intercept utility command line args.
+ * If intercepted, returns true and sets the exitStatus
to a usable integer
+ */
+ public boolean intercept() {
+ // Fist, handle installation commands (e.g. install, uninstall, certgen, etc)
+ for(Installer.InstallType installType : Installer.InstallType.values()) {
+ if (args.contains(installType.toString())) {
+ exitStatus = processInstallerArgs(installType, args);
+ return true;
+ }
+ }
+ try {
+ // Handle graceful autostart disabling
+ if (hasFlag("-A", "--honorautostart")) {
+ exitStatus = SUCCESS;
+ if(!FileUtilities.isAutostart()) {
+ exitStatus = NO_AUTOSTART;
+ return true;
+ }
+ // Don't intercept
+ exitStatus = SUCCESS;
+ return false;
+ }
+
+ // Handle version request
+ if (hasFlag("-v", "--version")) {
+ System.out.println(Constants.VERSION);
+ exitStatus = SUCCESS;
+ return true;
+ }
+ // Handle cert installation
+ String certFile;
+ if ((certFile = valueOf("-a", "--whitelist")) != null) {
+ exitStatus = FileUtilities.addToCertList(ALLOW_FILE, new File(certFile));
+ return true;
+ }
+ if ((certFile = valueOf("-b", "--blacklist")) != null) {
+ exitStatus = FileUtilities.addToCertList(BLOCK_FILE, new File(certFile));
+ return true;
+ }
+
+ // Print library list
+ if (hasFlag("-l", "--libinfo")) {
+ SecurityInfo.printLibInfo();
+ exitStatus = SUCCESS;
+ return true;
+ }
+ } catch(MissingArgException e) {
+ log.error("Invalid usage");
+ exitStatus = USAGE_ERROR;
+ } catch(Exception e) {
+ log.error("Internal error occured");
+ exitStatus = GENERAL_ERROR;
+ }
+ return false;
+ }
+}
diff --git a/src/qz/utils/FileUtilities.java b/src/qz/utils/FileUtilities.java
index 69f1a6657..b99768e06 100644
--- a/src/qz/utils/FileUtilities.java
+++ b/src/qz/utils/FileUtilities.java
@@ -10,6 +10,7 @@
package qz.utils;
import org.apache.commons.io.Charsets;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.text.translate.CharSequenceTranslator;
import org.apache.commons.lang3.text.translate.LookupTranslator;
import org.codehaus.jettison.json.JSONException;
@@ -26,17 +27,24 @@
import qz.common.Constants;
import qz.communication.FileIO;
import qz.communication.FileParams;
+import qz.installer.WindowsSpecialFolders;
import qz.exception.NullCommandException;
import qz.ws.PrintSocketServer;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.file.*;
+import java.nio.file.attribute.*;
+import java.text.SimpleDateFormat;
import java.util.*;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static qz.common.Constants.ALLOW_FILE;
/**
* Common static file i/o utilities
@@ -46,6 +54,100 @@
public class FileUtilities {
private static final Logger log = LoggerFactory.getLogger(FileUtilities.class);
+ public static final Path USER_DIR = getUserDirectory();
+ public static final Path SHARED_DIR = getSharedDirectory();
+ public static final Path TEMP_DIR = getTempDirectory();
+
+ /**
+ * Zips up the USER_DIR, places on desktop with timestamp
+ */
+ public static boolean zipLogs() {
+ String date = new SimpleDateFormat("yyyy-MM-dd_HHmm").format(new Date());
+ String filename = Constants.DATA_DIR + "-" + date + ".zip";
+ Path destination = Paths.get(System.getProperty("user.home"), "Desktop", filename);
+
+ try {
+ zipDirectory(USER_DIR, destination);
+ log.info("Zipped the contents of {} and placed the resulting files in {}", USER_DIR, destination);
+ return true;
+ } catch(IOException e) {
+ log.warn("Could not create zip file: {}", destination, e);
+ }
+ return false;
+ }
+
+ protected static void zipDirectory(Path sourceDir, Path outputFile) throws IOException {
+ final ZipOutputStream outputStream = new ZipOutputStream(new FileOutputStream(outputFile.toFile()));
+ Files.walkFileTree(sourceDir, new SimpleFileVisitor() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
+ try {
+ Path targetFile = sourceDir.relativize(file);
+ outputStream.putNextEntry(new ZipEntry(targetFile.toString()));
+ byte[] bytes = Files.readAllBytes(file);
+ outputStream.write(bytes, 0, bytes.length);
+ outputStream.closeEntry();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ outputStream.close();
+ }
+
+ /**
+ * Location where preferences and logs are kept
+ */
+ private static Path getUserDirectory() {
+ if(SystemUtilities.isWindows()) {
+ return Paths.get(WindowsSpecialFolders.ROAMING_APPDATA.getPath(), Constants.DATA_DIR);
+ } else if(SystemUtilities.isMac()) {
+ return Paths.get(System.getProperty("user.home"), "/Library/Application Support/", Constants.DATA_DIR);
+ } else {
+ return Paths.get(System.getProperty("user.home"), "." + Constants.DATA_DIR);
+ }
+ }
+
+ /**
+ * Location where shared preferences are kept, such as .autostart
+ */
+ private static Path getSharedDirectory() {
+ if(SystemUtilities.isWindows()) {
+ return Paths.get(WindowsSpecialFolders.PROGRAM_DATA.getPath(), Constants.DATA_DIR);
+ } else if(SystemUtilities.isMac()) {
+ return Paths.get("/Library/Application Support/", Constants.DATA_DIR);
+ } else {
+ return Paths.get("/srv/", Constants.DATA_DIR);
+ }
+ }
+
+ public static boolean childOf(File childFile, Path parentPath) {
+ Path child = childFile.toPath().toAbsolutePath();
+ Path parent = parentPath.toAbsolutePath();
+ if(SystemUtilities.isWindows()) {
+ return child.toString().toLowerCase().startsWith(parent.toString().toLowerCase());
+ }
+ return child.toString().startsWith(parent.toString());
+ }
+
+ public static Path inheritParentPermissions(Path filePath) {
+ if(SystemUtilities.isWindows()) {
+ // assume permissions are inherited
+ } else {
+ // assume permissions are not inherited
+ try {
+ FileAttribute> attributes = PosixFilePermissions.asFileAttribute(Files.getPosixFilePermissions(filePath.getParent()));
+ Files.setPosixFilePermissions(filePath, attributes.value());
+ // Remove execute flag
+ filePath.toFile().setExecutable(false, false);
+ } catch(IOException e) {
+ log.warn("Unable to inherit file permissions {}", filePath, e);
+ }
+ }
+ return filePath;
+
+ }
private static final String[] badExtensions = new String[] {
"exe", "pif", "paf", "application", "msi", "com", "cmd", "bat", "lnk", // Windows Executable program or script
@@ -113,17 +215,20 @@ public static Path getAbsolutePath(JSONObject params, RequestState request, bool
}
private static void initializeRootFolder(FileParams fileParams, String commonName) throws IOException {
- String parent = fileParams.isShared()? SystemUtilities.getSharedDataDirectory():SystemUtilities.getDataDirectory();
+ Path parent = fileParams.isShared()? SHARED_DIR:USER_DIR;
Path rootPath;
if (fileParams.isSandbox()) {
- rootPath = Paths.get(parent, Constants.SANDBOX_DIR, commonName);
+ rootPath = Paths.get(parent.toString(), FileIO.SANDBOX_DATA_SUFFIX, commonName);
} else {
- rootPath = Paths.get(parent, Constants.NOT_SANDBOX_DIR);
+ rootPath = Paths.get(parent.toString(), FileIO.GLOBAL_DATA_SUFFIX);
}
if (!Files.exists(rootPath)) {
Files.createDirectories(rootPath);
+ if(fileParams.isShared()) {
+ rootPath.toFile().setWritable(true, false);
+ }
}
}
@@ -142,11 +247,11 @@ public static Path createAbsolutePath(FileParams fileParams, String commonName)
if (fileParams.getPath().isAbsolute()) {
sanitizedPath = fileParams.getPath();
} else {
- String parent = fileParams.isShared()? SystemUtilities.getSharedDataDirectory():SystemUtilities.getDataDirectory();
+ Path parent = fileParams.isShared()? SHARED_DIR:USER_DIR;
if (fileParams.isSandbox()) {
- sanitizedPath = Paths.get(parent, Constants.SANDBOX_DIR, commonName).resolve(fileParams.getPath());
+ sanitizedPath = Paths.get(parent.toString(), FileIO.SANDBOX_DATA_SUFFIX, commonName).resolve(fileParams.getPath());
} else {
- sanitizedPath = Paths.get(parent, Constants.NOT_SANDBOX_DIR).resolve(fileParams.getPath());
+ sanitizedPath = Paths.get(parent.toString(), FileIO.GLOBAL_DATA_SUFFIX).resolve(fileParams.getPath());
}
}
@@ -166,7 +271,7 @@ public static boolean isGoodExtension(Path path) {
String[] tokens = fileName.split("\\.(?=[^.]+$)");
if (tokens.length == 2) {
String extension = tokens[1];
- for(String bad : FileUtilities.badExtensions) {
+ for(String bad : badExtensions) {
if (bad.equalsIgnoreCase(extension)) {
return false;
}
@@ -194,9 +299,9 @@ public static boolean isWhiteListed(Path path, boolean allowRootDir, boolean san
} else if (allowed.getValue().contains("|sandbox|")) {
Path p;
if (sandbox) {
- p = Paths.get(allowed.getKey().toString(), Constants.SANDBOX_DIR, commonName);
+ p = Paths.get(allowed.getKey().toString(), FileIO.SANDBOX_DATA_SUFFIX, commonName);
} else {
- p = Paths.get(allowed.getKey().toString(), Constants.NOT_SANDBOX_DIR);
+ p = Paths.get(allowed.getKey().toString(), FileIO.GLOBAL_DATA_SUFFIX);
}
if (cleanPath.startsWith(p) && (allowRootDir || !cleanPath.equals(p))) {
return true;
@@ -207,11 +312,17 @@ public static boolean isWhiteListed(Path path, boolean allowRootDir, boolean san
return false;
}
+ public static String getParentDirectory(String filePath) {
+ // Working path should always default to the JARs parent folder
+ int lastSlash = filePath.lastIndexOf(File.separator);
+ return lastSlash < 0? "":filePath.substring(0, lastSlash);
+ }
+
private static void populateWhiteList() {
whiteList = new ArrayList<>();
//default sandbox locations. More can be added through the properties file
- whiteList.add(new AbstractMap.SimpleEntry<>(Paths.get(SystemUtilities.getDataDirectory()), "|sandbox|"));
- whiteList.add(new AbstractMap.SimpleEntry<>(Paths.get(SystemUtilities.getSharedDataDirectory()), "|sandbox|"));
+ whiteList.add(new AbstractMap.SimpleEntry<>(USER_DIR, "|sandbox|"));
+ whiteList.add(new AbstractMap.SimpleEntry<>(SHARED_DIR, "|sandbox|"));
Properties props = PrintSocketServer.getTrayProperties();
if (props != null) {
@@ -269,12 +380,7 @@ private static void populateWhiteList() {
* @return {@code true} if restricted, {@code false} otherwise
*/
public static boolean isBadPath(String path) {
- if (SystemUtilities.isWindows()) {
- // Case insensitive
- return path.toLowerCase().contains(SystemUtilities.getDataDirectory().toLowerCase());
- }
-
- return path.contains(SystemUtilities.getDataDirectory());
+ return childOf(new File(path), USER_DIR);
}
/**
@@ -296,26 +402,6 @@ public static String escapeFileName(String fileName) {
return returnStringBuilder.toString();
}
- public static boolean isSymlink(String filePath) {
- log.info("Verifying symbolic link: {}", filePath);
- boolean returnVal = false;
- if (filePath != null) {
- File f = new File(filePath);
- if (f.exists()) {
- try {
- File canonicalFile = (f.getParent() == null? f:f.getParentFile().getCanonicalFile());
- returnVal = !canonicalFile.getCanonicalFile().equals(canonicalFile.getAbsoluteFile());
- }
- catch(IOException ex) {
- log.error("IOException checking for symlink", ex);
- }
- }
- }
-
- log.info("Symbolic link result: {}", returnVal);
- return returnVal;
- }
-
public static String readLocalFile(String file) throws IOException {
return new String(readFile(new DataInputStream(new FileInputStream(file))), Charsets.UTF_8);
}
@@ -396,29 +482,26 @@ public static File getFile(String name, boolean local) {
}
if (!fileMap.containsKey(name) || fileMap.get(name) == null) {
- String fileLoc;
- if (local) {
- fileLoc = SystemUtilities.getDataDirectory();
- } else {
- fileLoc = SystemUtilities.getSharedDirectory();
- }
-
- File locDir = new File(fileLoc);
- File file = new File(fileLoc + File.separator + name + ".dat");
+ File path = local ? USER_DIR.toFile() : SHARED_DIR.toFile();
+ File dat = Paths.get(path.toString(), name + ".dat").toFile();
try {
- locDir.mkdirs();
- file.createNewFile();
+ path.mkdirs();
+ dat.createNewFile();
+ if(!local) {
+ dat.setReadable(true, false);
+ dat.setWritable(true, false);
+ }
}
catch(IOException e) {
//failure is possible due to user permissions on shared files
if (local || (!name.equals(Constants.ALLOW_FILE) && !name.equals(Constants.BLOCK_FILE))) {
- log.warn("Cannot setup file {} ({})", fileLoc, local? "Local":"Shared", e);
+ log.warn("Cannot setup file {} ({})", dat, local? "Local":"Shared", e);
}
}
- if (file.exists()) {
- fileMap.put(name, file);
+ if (dat.exists()) {
+ fileMap.put(name, dat);
}
}
@@ -436,6 +519,17 @@ public static void deleteFile(String name) {
localFileMap.put(name, null);
}
+ public static ArgParser.ExitStatus addToCertList(String list, File certFile) throws Exception {
+ FileReader fr = new FileReader(certFile);
+ Certificate cert = new Certificate(IOUtils.toString(fr));
+ if(FileUtilities.printLineToFile(list, cert.data())) {
+ log.info("Successfully added {} to {} list", cert.getOrganization(), ALLOW_FILE);
+ return ArgParser.ExitStatus.SUCCESS;
+ }
+ log.error("Failed to add {} to {} list", cert.getOrganization(), ALLOW_FILE);
+ return ArgParser.ExitStatus.GENERAL_ERROR;
+ }
+
public static boolean deleteFromFile(String fileName, String deleteLine) {
File file = getFile(fileName, true);
File temp = getFile(Constants.TEMP_FILE, true);
@@ -461,4 +555,134 @@ public static boolean deleteFromFile(String fileName, String deleteLine) {
}
}
+ /**
+ *
+ * @return First line of ".autostart" file in user or shared space or "0" if blank. If neither are found, returns "1".
+ * @throws IOException
+ */
+ private static String readAutoStartFile() throws IOException {
+ log.debug("Checking for {} preference in user directory {}...", Constants.AUTOSTART_FILE, USER_DIR);
+ Path userAutoStart = Paths.get(USER_DIR.toString(), Constants.AUTOSTART_FILE);
+ List lines = null;
+ if (Files.exists(userAutoStart)) {
+ lines = Files.readAllLines(userAutoStart);
+ } else {
+ log.debug("Checking for {} preference in shared directory {}...", Constants.AUTOSTART_FILE, SHARED_DIR);
+ Path sharedAutoStart = Paths.get(SHARED_DIR.toString(), Constants.AUTOSTART_FILE);
+ if (Files.exists(sharedAutoStart)) {
+ lines = Files.readAllLines(sharedAutoStart);
+ }
+ }
+ if (lines == null) {
+ return "1";
+ } else if (lines.isEmpty()) {
+ log.warn("File {} is empty, this shouldn't happen.", Constants.AUTOSTART_FILE);
+ return "0";
+ } else {
+ String val = lines.get(0).trim();
+ log.debug("Autostart preference {} contains {}", Constants.AUTOSTART_FILE, val);
+ return val;
+ }
+ }
+
+ private static boolean writeAutoStartFile(String mode) throws IOException {
+ Path autostartFile = Paths.get(USER_DIR.toString(), Constants.AUTOSTART_FILE);
+ Files.write(autostartFile, mode.getBytes(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
+ return readAutoStartFile().equals(mode);
+ }
+
+ public static boolean setAutostart(boolean autostart) {
+ try {
+ return writeAutoStartFile(autostart ? "1": "0");
+ }
+ catch(IOException e) {
+ return false;
+ }
+ }
+
+ public static boolean isAutostart() {
+ try {
+ return "1".equals(readAutoStartFile());
+ }
+ catch(IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Configures the given embedded resource file using qz.common.Constants combined with the provided
+ * HashMap and writes to the specified location
+ *
+ * Will look for resource relative to relativeClass package location.
+ */
+ public static void configureAssetFile(String relativeAsset, File dest, HashMap additionalMappings, Class relativeClass) throws IOException {
+ // Static fields, parsed from qz.common.Constants
+ List fields = new ArrayList<>();
+ HashMap allMappings = (HashMap)additionalMappings.clone();
+ fields.addAll(Arrays.asList(Constants.class.getFields())); // public only
+ for(Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers())) { // static only
+ try {
+ String key = "%" + field.getName() + "%";
+ Object value = field.get(null);
+ if (value != null) {
+ if (value instanceof String) {
+ allMappings.putIfAbsent(key, (String)value);
+ } else if(value instanceof Boolean) {
+ allMappings.putIfAbsent(key, "" + field.getBoolean(null));
+ }
+ }
+ }
+ catch(IllegalAccessException e) {
+ // This should never happen; we are only using public fields
+ log.warn("{} occurred fetching a value for {}", e.getClass().getName(), field.getName(), e);
+ }
+ }
+ }
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(relativeClass.getResourceAsStream(relativeAsset)));
+ BufferedWriter writer = new BufferedWriter(new FileWriter(dest));
+
+ String line;
+ while((line = reader.readLine()) != null) {
+ for(Map.Entry mapping : allMappings.entrySet()) {
+ if (line.contains(mapping.getKey())) {
+ line = line.replaceAll(mapping.getKey(), mapping.getValue());
+ }
+ }
+ writer.write(line + "\n");
+ }
+ reader.close();
+ writer.close();
+ }
+
+
+ public static Path getTempDirectory() {
+ try {
+ return Files.createTempDirectory(Constants.DATA_DIR);
+ } catch(IOException e) {
+ log.warn("We couldn't get a temp directory for writing. This could cause some items to break");
+ }
+ return null;
+ }
+
+ public static void setPermissionsRecursively(Path toRecurse, boolean worldWrite) {
+ try (Stream paths = Files.walk(toRecurse)) {
+ paths.forEach((path)->{
+ if(SystemUtilities.isWindows() && worldWrite) {
+ // By default, NSIS sets owner to "Administrator", preventing non-admins from writing
+ // Add "Authenticated Users" write permission using
+ WindowsUtilities.setWritable(path);
+ }
+ if (path.toFile().isDirectory()) {
+ // Executable bit in Unix allows listing files
+ path.toFile().setExecutable(true, false);
+ }
+ path.toFile().setReadable(true, false);
+ path.toFile().setWritable(true, !worldWrite);
+ });
+ } catch (IOException e) {
+ log.warn("An error occurred setting permissions: {}", toRecurse);
+ }
+ }
}
diff --git a/src/qz/utils/JsonWriter.java b/src/qz/utils/JsonWriter.java
new file mode 100644
index 000000000..808b7de40
--- /dev/null
+++ b/src/qz/utils/JsonWriter.java
@@ -0,0 +1,134 @@
+/**
+ * @author Brett B.
+ *
+ * Copyright (C) 2019 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.utils;
+
+import org.apache.commons.io.Charsets;
+import org.apache.commons.io.FileUtils;
+import org.codehaus.jettison.json.JSONArray;
+import org.codehaus.jettison.json.JSONException;
+import org.codehaus.jettison.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+
+/**
+ * A minimally intrusive JSON writer
+ */
+public class JsonWriter {
+ protected static final Logger log = LoggerFactory.getLogger(JsonWriter.class);
+
+ public static boolean write(String path, String data, boolean overwrite, boolean delete) throws IOException, JSONException {
+ File f = new File(path);
+ if(!f.getParentFile().exists()) {
+ log.warn("Warning, the parent folder of {} does not exist, skipping.", path);
+ return false;
+ }
+
+ if (data == null) {
+ log.warn("Data is null, nothing to merge");
+ return true;
+ }
+
+ JSONObject config = f.exists() ? new JSONObject(FileUtils.readFileToString(f, Charsets.UTF_8)) : new JSONObject();
+ JSONObject append = new JSONObject(data);
+
+ if (!delete) {
+ merge(config, append, overwrite);
+ } else {
+ remove(config, append);
+ }
+
+ FileUtils.write(f, config.toString(2));
+
+ return true;
+ }
+
+ /**
+ * Appends all keys from {@code merger} to {@code base}
+ *
+ * @param base Root JSON object to merge into
+ * @param merger JSON Object of keys to merge
+ * @param overwrite If existing keys in {@code base} should be overwritten if defined in {@code merger}
+ */
+ private static void merge(JSONObject base, JSONObject merger, boolean overwrite) throws JSONException {
+ Iterator itr = merger.keys();
+ while(itr.hasNext()) {
+ String key = (String)itr.next();
+
+ Object baseVal = base.opt(key);
+ Object mergeVal = merger.opt(key);
+
+ if (baseVal == null) {
+ //add new key
+ base.put(key, mergeVal);
+ } else if (baseVal instanceof JSONObject && mergeVal instanceof JSONObject) {
+ //deep copy sub-keys
+ merge((JSONObject)baseVal, (JSONObject)mergeVal, overwrite);
+ } else if (overwrite) {
+ //force new key val if existing and allowed
+ base.put(key, mergeVal);
+ } else if (baseVal instanceof JSONArray && mergeVal instanceof JSONArray) {
+ JSONArray baseArr = (JSONArray)baseVal;
+ JSONArray mergeArr = (JSONArray)mergeVal;
+
+ //lists only merged if not overriding values
+ for(int i = 0; i < mergeArr.length(); i++) {
+ //check if value is already in the base array
+ boolean exists = false;
+ for(int j = 0; j < baseArr.length(); j++) {
+ if (baseArr.get(j).equals(mergeArr.get(i))) {
+ exists = true;
+ break;
+ }
+ }
+
+ if (!exists) {
+ baseArr.put(mergeArr.get(i));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes all keys in {@code deletion} from {@code base}
+ *
+ * @param base Root JSON object to delete from
+ * @param deletion JSON object of keys to delete
+ */
+ private static void remove(JSONObject base, JSONObject deletion) {
+ Iterator itr = deletion.keys();
+ while(itr.hasNext()) {
+ String key = (String)itr.next();
+
+ Object baseVal = base.opt(key);
+ Object delVal = deletion.opt(key);
+
+ if (baseVal instanceof JSONObject && delVal instanceof JSONObject) {
+ //only delete sub-keys
+ remove((JSONObject)baseVal, (JSONObject)delVal);
+ } else if (baseVal instanceof JSONArray && delVal instanceof JSONArray) {
+ //only delete elements in list
+ for(int i = 0; i < ((JSONArray)delVal).length(); i++) {
+ ((JSONArray)baseVal).remove(((JSONArray)delVal).opt(i));
+ }
+ } else if (baseVal != null) {
+ //delete entire key
+ base.remove(key);
+ }
+ }
+ }
+
+}
diff --git a/src/qz/utils/ShellUtilities.java b/src/qz/utils/ShellUtilities.java
index cba72fa06..adf970c10 100644
--- a/src/qz/utils/ShellUtilities.java
+++ b/src/qz/utils/ShellUtilities.java
@@ -14,16 +14,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.print.attribute.standard.PrinterResolution;
import java.awt.*;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
+import java.io.*;
+import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
-import java.util.Objects;
/**
* Utility class for managing all {@code Runtime.exec(...)} functions.
@@ -41,7 +37,7 @@ public class ShellUtilities {
static {
if (!SystemUtilities.isWindows()) {
// Cache existing; permit named overrides w/o full clobber
- Map env = new HashMap<>(System.getenv());
+ Map env = new HashMap<>(System.getenv());
if (SystemUtilities.isMac()) {
// Enable LANG overrides
env.put("SOFTWARE", "");
@@ -50,14 +46,14 @@ public class ShellUtilities {
env.put("LANG", "C");
String[] envp = new String[env.size()];
int i = 0;
- for (Map.Entry o : env.entrySet())
+ for(Map.Entry o : env.entrySet())
envp[i++] = o.getKey() + "=" + o.getValue();
ShellUtilities.envp = envp;
}
}
- public static boolean execute(String[] commandArray) {
+ public static boolean execute(String... commandArray) {
return execute(commandArray, false);
}
@@ -109,7 +105,7 @@ public static String execute(String[] commandArray, String[] searchFor) {
* {@code searchFor} is null ,then the first line of standard output
*/
public static String execute(String[] commandArray, String[] searchFor, boolean caseSensitive, boolean silent) {
- if(!silent) {
+ if (!silent) {
log.debug("Executing: {}", Arrays.toString(commandArray));
}
BufferedReader stdInput = null;
@@ -150,24 +146,30 @@ public static String execute(String[] commandArray, String[] searchFor, boolean
/**
* Executes a synchronous shell command and return the raw character result.
*
- * @param commandArray array of shell commands to execute
+ * @param commandArray array of shell commands to execute
* @return The entire raw standard output of command
*/
- public static String executeRaw(String[] commandArray) {
+ public static String executeRaw(String... commandArray) {
log.debug("Executing: {}", Arrays.toString(commandArray));
InputStreamReader in = null;
try {
Process p = Runtime.getRuntime().exec(commandArray, envp);
+ if(SystemUtilities.isWindows() && commandArray.length > 0 && commandArray[0].startsWith("wmic")) {
+ // Fix deadlock on old Windows versions https://stackoverflow.com/a/13367685/3196753
+ p.getOutputStream().close();
+ }
in = new InputStreamReader(p.getInputStream(), Charsets.UTF_8);
StringBuilder out = new StringBuilder();
int c;
- while((c=in.read()) != -1)
+ while((c = in.read()) != -1)
out.append((char)c);
return out.toString();
- } catch(IOException ex) {
+ }
+ catch(IOException ex) {
log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), Arrays.toString(envp), ex);
- } finally {
+ }
+ finally {
if (in != null) {
try { in.close(); } catch(Exception ignore) {}
}
@@ -180,7 +182,7 @@ public static String executeRaw(String[] commandArray) {
* Gets the computer's "hostname" from command line
*/
public static String getHostName() {
- return execute(new String[] {"hostname"}, new String[]{""});
+ return execute(new String[] {"hostname"}, new String[] {""});
}
/**
@@ -197,184 +199,38 @@ public static boolean executeAppleScript(String scriptBody) {
return false;
}
- return execute(new String[] {"osascript", "-e", scriptBody});
+ return execute("osascript", "-e", scriptBody);
}
- /**
- * Checks that the currently running OS is Apple and executes a native
- * AppleScript macro against the OS. Returns true if the
- * supplied searchValues are found within the standard output.
- *
- * @param scriptBody AppleScript text to execute
- * @param searchValues List of stdout strings to search for
- * @return true if the supplied searchValues are found within the standard output.
- */
- public static boolean executeAppleScript(String scriptBody, String ... searchValues) {
- if (!SystemUtilities.isMac()) {
- log.error("AppleScript can only be invoked from Apple OS");
- return false;
- }
-
- // Empty string returned by execute(...) means the values weren't found
- return !execute(new String[] {"osascript", "-e", scriptBody},
- searchValues).isEmpty();
+ public static void browseAppDirectory() {
+ browseDirectory(FileUtilities.getParentDirectory(SystemUtilities.getJarPath()));
}
- public static boolean setRegistryDWORD(String keyPath, String name, int data) {
- if (!SystemUtilities.isWindows()) {
- log.error("Reg commands can only be invoked from Windows");
- return false;
- }
-
- String reg = System.getenv("windir") + "\\system32\\reg.exe";
- return execute(
- new String[] {
- reg, "add", keyPath, "/f", "/v", name, "/t", "REG_DWORD", "/d", "" + data
- }
- );
- }
-
- public static String getRegistryString(String keyPath, String name) {
- String match = "REG_SZ";
- if (!SystemUtilities.isWindows()) {
- log.error("Reg commands can only be invoked from Windows");
- return null;
- }
-
- String reg = System.getenv("windir") + "\\system32\\reg.exe";
- String stdout = execute(
- new String[] {
- reg, "query", keyPath, "/v", name
- },
- new String[] {match}
- );
-
- if (!stdout.isEmpty()) {
- // Return the last element
- String[] parts = stdout.split("\\s+");
- return parts[parts.length - 1];
- }
-
- return null;
+ public static void browseDirectory(String directory) {
+ browseDirectory(new File(directory));
}
- public static int getRegistryDWORD(String keyPath, String name) {
- return getRegistryDWORD(keyPath, name, false);
+ public static void browseDirectory(Path path) {
+ browseDirectory(path.toFile());
}
- public static int getRegistryDWORD(String keyPath, String name, boolean silent) {
- String match = "0x";
- if (!SystemUtilities.isWindows()) {
- log.error("Reg commands can only be invoked from Windows");
- return -1;
- }
-
- String reg = System.getenv("windir") + "\\system32\\reg.exe";
- String stdout = execute(
- new String[] {
- reg, "query", keyPath, "/v", name
- },
- new String[] {match},
- true,
- silent
- );
-
- // Parse stdout looking for hex (i.e. "0x1B")
- if (!Objects.equals(stdout, "")) {
- for(String part : stdout.split(" ")) {
- if (part.startsWith(match)) {
- try {
- return Integer.parseInt(part.trim().split(match)[1], 16);
- }
- catch(NumberFormatException ignore) {}
+ public static void browseDirectory(File directory) {
+ try {
+ if (!SystemUtilities.isMac()) {
+ Desktop.getDesktop().open(directory);
+ } else {
+ // Mac tries to open the .app rather than browsing it. Instead, pass a child with -R to select it in finder
+ File[] files = directory.listFiles();
+ if (files != null && files.length > 0) {
+ ShellUtilities.execute("open", "-R", files[0].getCanonicalPath());
}
}
}
-
- return -1;
- }
-
- /**
- * Opens the specified path in the system-default file browser. Works around several OS limitations:
- * - Apple tries to launch .app
bundle directories as applications rather than browsing contents
- * - Linux has mixed support for Desktop.getDesktop()
. Adds xdg-open
fallback.
- * @param path The directory to browse
- * @throws IOException
- */
- public static void browseDirectory(String path) throws IOException {
- File directory = new File(path);
- if (SystemUtilities.isMac()) {
- // Mac tries to open the .app rather than browsing it. Instead, pass a child with -R to select it in finder
- File[] files = directory.listFiles();
- if (files != null && files.length > 0) {
- // Get first child
- File child = files[0];
- if (ShellUtilities.execute(new String[] {"open", "-R", child.getCanonicalPath()})) {
- return;
- }
+ catch(IOException io) {
+ if (SystemUtilities.isLinux()) {
+ // Fallback on xdg-open for Linux
+ ShellUtilities.execute("xdg-open", directory.getPath());
}
- } else {
- try {
- // The default, java recommended usage
- Desktop d = Desktop.getDesktop();
- d.open(directory);
- return;
- } catch (IOException io) {
- if (SystemUtilities.isLinux()) {
- // Fallback on xdg-open for Linux
- if (ShellUtilities.execute(new String[] {"xdg-open", path})) {
- return;
- }
- }
- throw io;
- }
- }
- throw new IOException("Unable to open " + path);
- }
-
- /**
- * Executes a native Registry delete/query command against the OS
- *
- * @param keyPath The path to the containing registry key
- * @param function "delete", or "query"
- * @param name the registry name to add, delete or query
- * @return true if the return code is zero
- */
- public static boolean executeRegScript(String keyPath, String function, String name) {
- return executeRegScript(keyPath, function, name, null);
- }
-
- /**
- * Executes a native Registry add/delete/query command against the OS
- *
- * @param keyPath The path to the containing registry key
- * @param function "add", "delete", or "query"
- * @param name the registry name to add, delete or query
- * @param data the registry data to add when using the "add" function
- * @return true if the return code is zero
- */
- public static boolean executeRegScript(String keyPath, String function, String name, String data) {
- if (!SystemUtilities.isWindows()) {
- log.error("Reg commands can only be invoked from Windows");
- return false;
- }
-
- String reg = System.getenv("windir") + "\\system32\\reg.exe";
- if ("delete".equals(function)) {
- return execute(new String[] {
- reg, function, keyPath, "/v", name, "/f"
- });
- } else if ("add".equals(function)) {
- return execute(new String[] {
- reg, function, keyPath, "/v", name, "/d", data, "/f"
- });
- } else if ("query".equals(function)) {
- return execute(new String[] {
- reg, function, keyPath, "/v", name
- });
- } else {
- log.error("Reg operation {} not supported.", function);
- return false;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/qz/utils/SystemUtilities.java b/src/qz/utils/SystemUtilities.java
index e12b86747..95484e76b 100644
--- a/src/qz/utils/SystemUtilities.java
+++ b/src/qz/utils/SystemUtilities.java
@@ -11,15 +11,21 @@
package qz.utils;
import com.github.zafarkhaja.semver.Version;
+import com.sun.jna.platform.win32.Advapi32Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.common.Constants;
import qz.common.TrayManager;
-import qz.deploy.DeployUtilities;
import javax.swing.*;
import java.awt.*;
import java.io.File;
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static com.sun.jna.platform.win32.WinReg.*;
/**
* Utility class for OS detection functions.
@@ -37,6 +43,7 @@ public class SystemUtilities {
private static String linuxRelease;
private static String classProtocol;
private static Version osVersion;
+ private static String jarPath;
/**
@@ -52,7 +59,7 @@ public static Version getOSVersion() {
String version = System.getProperty("os.version");
// Windows is missing patch release, read it from registry
if (isWindows()) {
- String patch = ShellUtilities.getRegistryString("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "ReleaseId");
+ String patch = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "ReleaseId");
if (patch != null) {
version += "." + patch.trim();
}
@@ -65,6 +72,14 @@ public static Version getOSVersion() {
return osVersion;
}
+ public static boolean isAdmin() {
+ if (SystemUtilities.isWindows()) {
+ return ShellUtilities.execute("net", "session");
+ } else {
+ return ShellUtilities.executeRaw("whoami").trim().equals("root");
+ }
+ }
+
/**
* Handle Java versioning nuances
* To eventually be replaced with java.lang.Runtime.Version
(JDK9+)
@@ -107,61 +122,49 @@ public static Version getJavaVersion() {
}
}
-
/**
- * Retrieve OS-specific Application Data directory such as:
- * {@code C:\Users\John\AppData\Roaming\qz} on Windows
- * -- or --
- * {@code /Users/John/Library/Application Support/qz} on Mac
- * -- or --
- * {@code /home/John/.qz} on Linux
+ * Determines the currently running Jar's absolute path on the local filesystem
*
- * @return Full path to the Application Data directory
+ * @return A String value representing the absolute path to the currently running
+ * jar
*/
- public static String getDataDirectory() {
- String parent;
- String folder = Constants.DATA_DIR;
-
- if (isWindows()) {
- parent = System.getenv("APPDATA");
- } else if (isMac()) {
- parent = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support";
- } else if (isUnix()) {
- parent = System.getProperty("user.home");
- folder = "." + folder;
- } else {
- parent = System.getProperty("user.dir");
+ public static String detectJarPath() {
+ try {
+ String jarPath = new File(SystemUtilities.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getCanonicalPath();
+ // Fix characters that get URL encoded when calling getPath()
+ return URLDecoder.decode(jarPath, "UTF-8");
+ } catch(IOException ex) {
+ log.error("Unable to determine Jar path", ex);
}
-
- return parent + File.separator + folder;
+ return null;
}
/**
- * Returns the OS shared data directory for FileIO operations. Must match
- * that defined in desktop installer scripts, which create directories
- * and grant read/write access to normal users.
- * access.
- * @return
+ * Returns the jar which we will create a shortcut for
+ *
+ * @return The path to the jar path which has been set
*/
- public static String getSharedDataDirectory() {
- String parent;
-
- if (isWindows()) {
- parent = System.getenv("PROGRAMDATA");
- } else if (isMac()) {
- parent = "/Library/Application Support/";
- } else {
- parent = "/srv/";
+ public static String getJarPath() {
+ if (jarPath == null) {
+ jarPath = detectJarPath();
}
-
- return parent + File.separator + Constants.DATA_DIR;
+ return jarPath;
}
- public static String getSharedDirectory() {
- String parent = DeployUtilities.getSystemShortcutCreator().getParentDirectory();
- String folder = Constants.SHARED_DATA_DIR;
-
- return parent + File.separator + folder;
+ /**
+ * Returns the app's path, based on the jar location
+ * or null if no .jar is found (such as running from IDE)
+ * @return
+ */
+ public static Path detectAppPath() {
+ String jarPath = detectJarPath();
+ if (jarPath != null) {
+ File jar = new File(jarPath);
+ if (jar.getPath().endsWith(".jar") && jar.exists()) {
+ return Paths.get(jar.getParent());
+ }
+ }
+ return null;
}
/**
@@ -182,6 +185,8 @@ public static boolean isWindows() {
return (OS_NAME.contains("win"));
}
+ public static boolean isWindowsXP() { return OS_NAME.contains("win") && OS_NAME.contains("xp"); }
+
/**
* Determine if the current Operating System is Mac OS
*
@@ -412,4 +417,23 @@ public static boolean isJar() {
}
return "jar".equals(classProtocol);
}
+
+ public static boolean isJDK() {
+ String path = System.getProperty("sun.boot.library.path");
+ if(path != null) {
+ String javacPath = "";
+ if(path.endsWith(File.separator + "bin")) {
+ javacPath = path;
+ } else {
+ int libIndex = path.lastIndexOf(File.separator + "lib");
+ if(libIndex > 0) {
+ javacPath = path.substring(0, libIndex) + File.separator + "bin";
+ }
+ }
+ if(!javacPath.isEmpty()) {
+ return new File(javacPath, "javac").exists() || new File(javacPath, "javac.exe").exists();
+ }
+ }
+ return false;
+ }
}
diff --git a/src/qz/utils/UbuntuUtilities.java b/src/qz/utils/UbuntuUtilities.java
index 5f2de203c..68ca2e523 100644
--- a/src/qz/utils/UbuntuUtilities.java
+++ b/src/qz/utils/UbuntuUtilities.java
@@ -15,6 +15,14 @@
import qz.ui.component.IconCache;
import java.awt.*;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
/**
* Utility class for Ubuntu OS specific functions.
@@ -76,14 +84,17 @@ public static Color getTrayColor() {
* @return the current running theme, or an empty String if it could not be determined.
*/
public static String getThemeName(String defaultTheme) {
- String themeName = ShellUtilities.execute(
- new String[] {
- "gconftool-2",
- "--get",
- "/desktop/gnome/shell/windows/theme"
- },
- null
- );
+ String themeName = "";
+ if(ShellUtilities.execute("which", "gconftool-2")) {
+ themeName = ShellUtilities.execute(
+ new String[] {
+ "gconftool-2",
+ "--get",
+ "/desktop/gnome/shell/windows/theme"
+ },
+ null
+ );
+ }
return themeName.isEmpty()? defaultTheme:themeName;
}
@@ -119,5 +130,4 @@ public static double getScaleFactor() {
}
return GtkUtilities.getScaleFactor();
}
-
}
diff --git a/src/qz/utils/WindowsUtilities.java b/src/qz/utils/WindowsUtilities.java
index 4c7d92bd9..4aaaa2f81 100644
--- a/src/qz/utils/WindowsUtilities.java
+++ b/src/qz/utils/WindowsUtilities.java
@@ -1,19 +1,40 @@
package qz.utils;
import com.github.zafarkhaja.semver.Version;
-import com.sun.jna.platform.win32.GDI32;
-import com.sun.jna.platform.win32.WinDef;
+import com.sun.jna.platform.win32.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import qz.common.Constants;
import java.awt.*;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.*;
+import java.util.List;
+
+import static com.sun.jna.platform.win32.WinReg.*;
+
+import static java.nio.file.attribute.AclEntryPermission.*;
+import static java.nio.file.attribute.AclEntryFlag.*;
public class WindowsUtilities {
+ protected static final Logger log = LoggerFactory.getLogger(WindowsUtilities.class);
public static boolean isDarkMode() {
- String path = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
- String name = "AppsUseLightTheme";
+ String key = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+
// 0 = Dark Theme. -1/1 = Light Theme
- return ShellUtilities.getRegistryDWORD(path, name, true) == 0;
+ Integer regVal;
+ if((regVal = getRegInt(HKEY_CURRENT_USER, key, "SystemUsesLightTheme")) != null) {
+ // Prefer system theme
+ return regVal == 0;
+ } else if((regVal = getRegInt(HKEY_CURRENT_USER, key, "AppsUseLightTheme")) != null) {
+ // Fallback on apps theme
+ return regVal == 0;
+ }
+ return false;
}
+
public static int getScaleFactor() {
if (Constants.JAVA_VERSION.lessThan(Version.valueOf("9.0.0"))) {
WinDef.HDC hdc = GDI32.INSTANCE.CreateCompatibleDC(null);
@@ -28,4 +49,102 @@ public static int getScaleFactor() {
}
return (int)(Toolkit.getDefaultToolkit().getScreenResolution() / 96.0);
}
+
+ // gracefully swallow InvocationTargetException
+ public static Integer getRegInt(HKEY root, String key, String value) {
+ try {
+ if (Advapi32Util.registryKeyExists(root, key) && Advapi32Util.registryValueExists(root, key, value)) {
+ return Advapi32Util.registryGetIntValue(root, key, value);
+ }
+ } catch(Exception e) {
+ log.warn("Couldn't get registry value {}\\{}\\{}", root, key, value);
+ }
+ return null;
+ }
+
+ // gracefully swallow InvocationTargetException
+ public static String getRegString(HKEY root, String key, String value) {
+ try {
+ if (Advapi32Util.registryKeyExists(root, key) && Advapi32Util.registryValueExists(root, key, value)) {
+ return Advapi32Util.registryGetStringValue(root, key, value);
+ }
+ } catch(Exception e) {
+ log.warn("Couldn't get registry value {}\\{}\\{}", root, key, value);
+ }
+ return null;
+ }
+
+ // gracefully swallow InvocationTargetException
+ public static boolean deleteRegKey(WinReg.HKEY root, String key) {
+ try {
+ if (Advapi32Util.registryKeyExists(root, key)) {
+ Advapi32Util.registryDeleteKey(root, key);
+ return true;
+ }
+ } catch(Exception e) {
+ log.warn("Couldn't delete value {}\\{}\\{}", root, key);
+ }
+ return false;
+ }
+
+ public static boolean addRegValue(WinReg.HKEY root, String key, String value, Object data) {
+ try {
+ // Recursively create keys as needed
+ String partialKey = "";
+ for(String section : key.split("\\\\")) {
+ if (partialKey.isEmpty()) {
+ partialKey += section;
+ } else {
+ partialKey += "\\" + section;
+ }
+ if(!Advapi32Util.registryKeyExists(root, partialKey)) {
+ Advapi32Util.registryCreateKey(root, partialKey);
+ }
+ }
+ if (data instanceof String) {
+ Advapi32Util.registrySetStringValue(root, key, value, (String)data);
+ } else if (data instanceof Integer) {
+ Advapi32Util.registrySetIntValue(root, key, value, (Integer)data);
+ } else {
+ throw new Exception("Registry values of type " + data.getClass() + " aren't supported");
+ }
+ return true;
+ } catch(Exception e) {
+ log.error("Could not write registry value {}\\{}\\{}", root, key, value, e);
+ }
+ return false;
+ }
+
+ public static void setWritable(Path path) {
+ try {
+ UserPrincipal authenticatedUsers = path.getFileSystem().getUserPrincipalLookupService()
+ .lookupPrincipalByName("Authenticated Users");
+ AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class);
+
+ // Create ACL to give "Authenticated Users" "modify" access
+ AclEntry entry = AclEntry.newBuilder()
+ .setType(AclEntryType.ALLOW)
+ .setPrincipal(authenticatedUsers)
+ .setFlags(DIRECTORY_INHERIT,
+ FILE_INHERIT)
+ .setPermissions(WRITE_NAMED_ATTRS,
+ WRITE_ATTRIBUTES,
+ DELETE,
+ WRITE_DATA,
+ READ_ACL,
+ APPEND_DATA,
+ READ_ATTRIBUTES,
+ READ_DATA,
+ EXECUTE,
+ SYNCHRONIZE,
+ READ_NAMED_ATTRS)
+ .build();
+
+ List acl = view.getAcl();
+ acl.add(0, entry); // insert before any DENY entries
+ view.setAcl(acl);
+ } catch(IOException e) {
+ log.warn("Could not set writable: {}", path, e);
+ }
+ }
}
diff --git a/src/qz/ws/HttpAboutServlet.java b/src/qz/ws/HttpAboutServlet.java
index 40ebb117b..b852d1c07 100644
--- a/src/qz/ws/HttpAboutServlet.java
+++ b/src/qz/ws/HttpAboutServlet.java
@@ -7,18 +7,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qz.common.AboutInfo;
-import qz.deploy.DeployUtilities;
+import qz.installer.certificate.CertificateManager;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.KeyStore;
import java.util.Iterator;
-import java.util.Properties;
/**
* HTTP JSON endpoint for serving QZ Tray information
@@ -28,6 +22,11 @@ public class HttpAboutServlet extends DefaultServlet {
private static final Logger log = LoggerFactory.getLogger(PrintSocketServer.class);
private static final int JSON_INDENT = 2;
+ private CertificateManager certificateManager;
+
+ public HttpAboutServlet(CertificateManager certificateManager) {
+ this.certificateManager = certificateManager;
+ }
@Override
@@ -50,7 +49,7 @@ private void generateHtmlResponse(HttpServletRequest request, HttpServletRespons
display.append(newTable());
- JSONObject aboutData = AboutInfo.gatherAbout(request.getServerName());
+ JSONObject aboutData = AboutInfo.gatherAbout(request.getServerName(), certificateManager);
try {
display.append(generateFromKeys(aboutData, true));
}
@@ -74,7 +73,7 @@ private void generateHtmlResponse(HttpServletRequest request, HttpServletRespons
}
private void generateJsonResponse(HttpServletRequest request, HttpServletResponse response) {
- JSONObject aboutData = AboutInfo.gatherAbout(request.getServerName());
+ JSONObject aboutData = AboutInfo.gatherAbout(request.getServerName(), certificateManager);
try {
response.setStatus(HttpServletResponse.SC_OK);
@@ -90,7 +89,7 @@ private void generateJsonResponse(HttpServletRequest request, HttpServletRespons
private void generateCertResponse(HttpServletRequest request, HttpServletResponse response) {
try {
String alias = request.getServletPath().split("/")[2];
- String certData = loadCertificate(alias);
+ String certData = AboutInfo.formatCert(certificateManager.getSslKeyPair().getCert().getEncoded());
if (certData != null) {
response.setStatus(HttpServletResponse.SC_OK);
@@ -108,23 +107,6 @@ private void generateCertResponse(HttpServletRequest request, HttpServletRespons
}
}
- private String loadCertificate(String alias) throws GeneralSecurityException, IOException {
- Properties sslProps = DeployUtilities.loadTrayProperties();
-
- if (sslProps != null) {
- KeyStore jks = KeyStore.getInstance("jks");
- jks.load(new FileInputStream(new File(sslProps.getProperty("wss.keystore"))), sslProps.getProperty("wss.storepass").toCharArray());
-
- if (jks.containsAlias(alias)) {
- return AboutInfo.formatCert(jks.getCertificate(alias).getEncoded());
- } else {
- return null;
- }
- } else {
- return null;
- }
- }
-
private StringBuilder generateFromKeys(JSONObject obj, boolean printTitle) throws JSONException {
StringBuilder rows = new StringBuilder();
diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java
index c8e494654..e3173e918 100644
--- a/src/qz/ws/PrintSocketClient.java
+++ b/src/qz/ws/PrintSocketClient.java
@@ -26,6 +26,7 @@
import javax.security.cert.CertificateParsingException;
import javax.usb.util.UsbUtil;
import java.awt.*;
+import java.io.EOFException;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.*;
@@ -159,6 +160,7 @@ public void onClose(Session session, int closeCode, String reason) {
@OnWebSocketError
public void onError(Session session, Throwable error) {
+ if (error instanceof EOFException) return;
log.error("Connection error", error);
trayManager.displayErrorMessage(error.getMessage());
}
@@ -598,8 +600,8 @@ private void processMessage(Session session, JSONObject json, SocketConnection c
FileParams fileParams = new FileParams(params);
Path absPath = FileUtilities.getAbsolutePath(params, request, false);
- Files.createDirectories(absPath.getParent());
Files.write(absPath, fileParams.getData(), StandardOpenOption.CREATE, fileParams.getAppendMode());
+ FileUtilities.inheritParentPermissions(absPath);
sendResult(session, UID, null);
break;
}
@@ -668,7 +670,8 @@ private boolean allowedFromDialog(RequestState request, String prompt, Point pos
private Point findDialogPosition(Session session, JSONObject positionData) {
Point pos = new Point(0, 0);
- if (session.getRemoteAddress().getAddress().isLoopbackAddress() && positionData != null) {
+ if (session.getRemoteAddress().getAddress().isLoopbackAddress() && positionData != null
+ && !positionData.isNull("x") && !positionData.isNull("y")) {
pos.move(positionData.optInt("x"), positionData.optInt("y"));
}
diff --git a/src/qz/ws/PrintSocketServer.java b/src/qz/ws/PrintSocketServer.java
index abd5f0bb1..30d257d96 100644
--- a/src/qz/ws/PrintSocketServer.java
+++ b/src/qz/ws/PrintSocketServer.java
@@ -10,7 +10,6 @@
package qz.ws;
-import org.apache.commons.io.IOUtils;
import org.apache.log4j.Level;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.rolling.FixedWindowRollingPolicy;
@@ -21,7 +20,6 @@
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.MultiException;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter;
import org.eclipse.jetty.websocket.server.pathmap.ServletPathSpec;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
@@ -29,13 +27,13 @@
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import qz.auth.Certificate;
import qz.common.Constants;
-import qz.common.SecurityInfo;
import qz.common.TrayManager;
-import qz.deploy.DeployUtilities;
+import qz.installer.Installer;
+import qz.installer.certificate.ExpiryTask;
+import qz.installer.certificate.CertificateManager;
+import qz.utils.ArgParser;
import qz.utils.FileUtilities;
-import qz.utils.SystemUtilities;
import javax.swing.*;
import java.io.*;
@@ -60,52 +58,33 @@ public class PrintSocketServer {
private static final AtomicInteger insecurePortIndex = new AtomicInteger(0);
private static TrayManager trayManager;
- private static Properties trayProperties;
+ private static CertificateManager certificateManager;
private static boolean headless;
public static void main(String[] args) {
- List sArgs = Arrays.asList(args);
-
- if (sArgs.contains("-a") || sArgs.contains("--whitelist")) {
- int fileIndex = Math.max(sArgs.indexOf("-a"), sArgs.indexOf("--whitelist")) + 1;
- addToList(Constants.ALLOW_FILE, new File(sArgs.get(fileIndex)));
- System.exit(0);
- }
- if (sArgs.contains("-b") || sArgs.contains("--blacklist")) {
- int fileIndex = Math.max(sArgs.indexOf("-b"), sArgs.indexOf("--blacklist")) + 1;
- addToList(Constants.BLOCK_FILE, new File(sArgs.get(fileIndex)));
- System.exit(0);
- }
- // Print library list and exits
- if (sArgs.contains("-l") || sArgs.contains("--libinfo")) {
- String format = "%-40s%s%n";
- System.out.printf(format, "LIBRARY NAME:", "VERSION:");
- SortedMap libVersions = SecurityInfo.getLibVersions();
- for (Map.Entry entry: libVersions.entrySet()) {
- if (entry.getValue() == null) {
- System.out.printf(format, entry.getKey(), "(unknown)");
- } else {
- System.out.printf(format, entry.getKey(), entry.getValue());
- }
- }
- System.exit(0);
- }
- if (sArgs.contains("-h") || sArgs.contains("--headless")) {
- headless = true;
+ ArgParser parser = new ArgParser(args);
+ if(parser.intercept()) {
+ System.exit(parser.getExitCode());
}
- if (sArgs.contains("-v") || sArgs.contains("--version")) {
- System.out.println(Constants.VERSION);
- System.exit(0);
- }
-
+ headless = parser.hasFlag("-h", "--headless");
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);
setupFileLogging();
+ try {
+ // Gets and sets the SSL info, properties file
+ certificateManager = Installer.getInstance().certGen(false);
+ // Reoccurring (e.g. hourly) cert expiration check
+ new ExpiryTask(certificateManager).schedule();
+ } catch(Exception e) {
+ log.error("Something went critically wrong loading HTTPS", e);
+ }
+ Installer.getInstance().addUserSettings();
+
try {
log.info("Starting {} {}", Constants.ABOUT_TITLE, Constants.VERSION);
SwingUtilities.invokeAndWait(() -> trayManager = new TrayManager(headless));
@@ -118,25 +97,9 @@ public static void main(String[] args) {
log.warn("The web socket server is no longer running");
}
- private static void addToList(String list, File certFile) {
- try {
- FileReader fr = new FileReader(certFile);
- Certificate cert = new Certificate(IOUtils.toString(fr));
-
- if (FileUtilities.printLineToFile(list, cert.data())) {
- log.info("Successfully added {} to {} list", cert.getOrganization(), list);
- } else {
- log.warn("Failed to add certificate to {} list (Insufficient user privileges)", list);
- }
- }
- catch(Exception e) {
- log.error("Failed to add certificate:", e);
- }
- }
-
private static void setupFileLogging() {
FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
- rollingPolicy.setFileNamePattern(SystemUtilities.getDataDirectory() + File.separator + Constants.LOG_FILE + ".log.%i");
+ rollingPolicy.setFileNamePattern(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log.%i");
rollingPolicy.setMaxIndex(Constants.LOG_ROTATIONS);
SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy(Constants.LOG_SIZE);
@@ -144,7 +107,7 @@ private static void setupFileLogging() {
RollingFileAppender fileAppender = new RollingFileAppender();
fileAppender.setLayout(new PatternLayout("%d{ISO8601} [%p] %m%n"));
fileAppender.setThreshold(Level.DEBUG);
- fileAppender.setFile(SystemUtilities.getDataDirectory() + File.separator + Constants.LOG_FILE + ".log");
+ fileAppender.setFile(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log");
fileAppender.setRollingPolicy(rollingPolicy);
fileAppender.setTriggeringPolicy(triggeringPolicy);
fileAppender.setEncoding("UTF-8");
@@ -157,24 +120,15 @@ private static void setupFileLogging() {
public static void runServer() {
final AtomicBoolean running = new AtomicBoolean(false);
-
- trayProperties = getTrayProperties();
-
while(!running.get() && securePortIndex.get() < SECURE_PORTS.size() && insecurePortIndex.get() < INSECURE_PORTS.size()) {
Server server = new Server(getInsecurePortInUse());
-
- if (trayProperties != null) {
+ if (certificateManager != null) {
// Bind the secure socket on the proper port number (i.e. 9341), add it as an additional connector
- SslContextFactory sslContextFactory = new SslContextFactory();
- sslContextFactory.setKeyStorePath(trayProperties.getProperty("wss.keystore"));
- sslContextFactory.setKeyStorePassword(trayProperties.getProperty("wss.storepass"));
- sslContextFactory.setKeyManagerPassword(trayProperties.getProperty("wss.keypass"));
-
- SslConnectionFactory sslConnection = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
+ SslConnectionFactory sslConnection = new SslConnectionFactory(certificateManager.configureSslContextFactory(), HttpVersion.HTTP_1_1.asString());
HttpConnectionFactory httpConnection = new HttpConnectionFactory(new HttpConfiguration());
ServerConnector connector = new ServerConnector(server, sslConnection, httpConnection);
- connector.setHost(trayProperties.getProperty("wss.host"));
+ connector.setHost(certificateManager.getProperties().getProperty("wss.host"));
connector.setPort(getSecurePortInUse());
server.addConnector(connector);
} else {
@@ -195,7 +149,7 @@ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse
filter.getFactory().getPolicy().setMaxTextMessageSize(MAX_MESSAGE_SIZE);
// Handle HTTP landing page
- ServletHolder httpServlet = new ServletHolder(new HttpAboutServlet());
+ ServletHolder httpServlet = new ServletHolder(new HttpAboutServlet(certificateManager));
httpServlet.setInitParameter("resourceBase","/");
context.addServlet(httpServlet, "/");
context.addServlet(httpServlet, "/json");
@@ -238,13 +192,6 @@ public static TrayManager getTrayManager() {
return trayManager;
}
- public static Properties getTrayProperties() {
- if (trayProperties == null) {
- trayProperties = DeployUtilities.loadTrayProperties();
- }
- return trayProperties;
- }
-
public static int getSecurePortInUse() {
return SECURE_PORTS.get(securePortIndex.get());
}
@@ -253,4 +200,8 @@ public static int getInsecurePortInUse() {
return INSECURE_PORTS.get(insecurePortIndex.get());
}
+ public static Properties getTrayProperties() {
+ return certificateManager.getProperties();
+ }
+
}
diff --git a/test/qz/installer/InstallerTests.java b/test/qz/installer/InstallerTests.java
new file mode 100644
index 000000000..a79e41332
--- /dev/null
+++ b/test/qz/installer/InstallerTests.java
@@ -0,0 +1,230 @@
+package qz.installer;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMReader;
+import qz.installer.certificate.CertificateChainBuilder;
+import qz.installer.certificate.ExpiryTask;
+import qz.installer.certificate.CertificateManager;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+
+public class InstallerTests {
+
+ public static void main(String ... args) throws Exception {
+ // runInstallerTests();
+ runExpiryTests();
+ }
+
+ public static void runInstallerTests() throws Exception {
+ CertificateChainBuilder.SSL_CERT_AGE = 1;
+ Installer installer = Installer.getInstance();
+ // installer.install();
+ CertificateManager certificateManager = installer.certGen(true);
+ new ExpiryTask(certificateManager).schedule(1000, 1000);
+ Thread.sleep(5000);
+ installer.removeCerts();
+ }
+ public static void runExpiryTests() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ String[] testCerts = { QZ_INDUSTRIES_CERT, CA_CERT_ORG_CERT, LETS_ENCRYPT_CERT };
+
+ HashMap certmap = new HashMap<>();
+ certmap.put(ExpiryTask.CertProvider.INTERNAL, QZ_INDUSTRIES_CERT);
+ certmap.put(ExpiryTask.CertProvider.CA_CERT_ORG, CA_CERT_ORG_CERT);
+ certmap.put(ExpiryTask.CertProvider.LETS_ENCRYPT, LETS_ENCRYPT_CERT);
+
+
+ for(String testCert : testCerts) {
+ X509Certificate cert = loadCert(testCert);
+ ExpiryTask.findCertProvider(cert);
+ ExpiryTask.getExpiry(cert);
+ ExpiryTask.parseHostNames(cert);
+ }
+ }
+
+ public static X509Certificate loadCert(String cert) throws IOException {
+ PEMReader reader = new PEMReader(new StringReader(cert));
+ return (X509Certificate)reader.readObject();
+ }
+
+ private static String QZ_INDUSTRIES_CERT = "-----BEGIN CERTIFICATE-----\n" +
+ "MIIFDjCCA/agAwIBAgIGAW3W19xeMA0GCSqGSIb3DQEBCwUAMIGaMQswCQYDVQQG\n" +
+ "EwJVUzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNhbmFzdG90YTEbMBkGA1UECgwS\n" +
+ "UVogSW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMx\n" +
+ "HDAaBgkqhkiG9w0BCQEWDXN1cHBvcnRAcXouaW8xEjAQBgNVBAMMCWxvY2FsaG9z\n" +
+ "dDAeFw0xOTEwMTUyMzEyMTNaFw0yMjAxMTgwMDEyMTNaMIGaMQswCQYDVQQGEwJV\n" +
+ "UzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNhbmFzdG90YTEbMBkGA1UECgwSUVog\n" +
+ "SW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMxHDAa\n" +
+ "BgkqhkiG9w0BCQEWDXN1cHBvcnRAcXouaW8xEjAQBgNVBAMMCWxvY2FsaG9zdDCC\n" +
+ "ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8Hfp8Hujhr6OCTJYLPnluv\n" +
+ "XgDi92eX8nkW+HkpWjgDwjv59VqIiycSGTxp5GCozvDF7zHbrSICVOlHa1iFXv3w\n" +
+ "8EpWTIKxfqiNDZohnq38R1lVGwfPC97pzaqu5CWvjTmUD5T/Cl5RnZEvnKoXvxAA\n" +
+ "9/Eikzz7TGr2BL56rJFmwYRosEd2tvyxV4o/m1t/PSU9cAi1GzWpuwRbmFl34cvV\n" +
+ "tMPeWUz315zy8Qw9cz4ktb1O/H+5BWXdpb9DRUS9QG6sS1Esi9jIZ7rPjm+Gqj3P\n" +
+ "mcsev9jVlex7C0eMG3QVLpOiurPxKYkGHH9F9W6PXvKEk/jWjFFxbpy380iqTb8C\n" +
+ "AwEAAaOCAVYwggFSMIHMBgNVHSMEgcQwgcGAFCNVfcjxztjhZUuVHS5vsRDzVvhb\n" +
+ "oYGgpIGdMIGaMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNh\n" +
+ "bmFzdG90YTEbMBkGA1UECgwSUVogSW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJR\n" +
+ "WiBJbmR1c3RyaWVzLCBMTEMxHDAaBgkqhkiG9w0BCQEWDXN1cHBvcnRAcXouaW8x\n" +
+ "EjAQBgNVBAMMCWxvY2FsaG9zdIIGAW3W19ucMAwGA1UdEwEB/wQCMAAwDgYDVR0P\n" +
+ "AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAlBgNVHREE\n" +
+ "HjAcgglsb2NhbGhvc3SCD2xvY2FsaG9zdC5xei5pbzAdBgNVHQ4EFgQUf2fwQ8IJ\n" +
+ "pdlT4+ghS0BP/V91ix0wDQYJKoZIhvcNAQELBQADggEBAHFiDZ7jItbHjpxxOHYF\n" +
+ "g6O61+7ETEPy0JGIPWxiysNCDfKyxuaVQ0UZ3/r6g5uQs3GjiQRIFxTmBk0hFTYB\n" +
+ "ONS2P0ugyED+C5wJADDcILa8SAF0EwrFX/6f3TnG+Qvn3jBRUCnjKTMfpnSlgMTk\n" +
+ "/wm1Jg10gUEXGHWGagw4YPVwMvBaWWYEFPC/emlONcAkZv4gfPZJ61bZgstqF+bZ\n" +
+ "WQM1GF1TOO8x/2KgguTknxc1EI4SmWN3Zl58BY8sf95yribLmKFW2VwbOHqfs0/d\n" +
+ "lFDMhix3cTURGvpyt+ZM4KXD9VkFpLIqRe1Qj02BPXS4GDNPQ+3xPbFOpvIKeYhf\n" +
+ "cGk=\n" +
+ "-----END CERTIFICATE-----";
+
+ private static String CA_CERT_ORG_CERT = "-----BEGIN CERTIFICATE-----\n" +
+ "MIIHnjCCBYagAwIBAgIDE4H4MA0GCSqGSIb3DQEBDQUAMHkxEDAOBgNVBAoTB1Jv\n" +
+ "b3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEiMCAGA1UEAxMZ\n" +
+ "Q0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYSc3VwcG9y\n" +
+ "dEBjYWNlcnQub3JnMB4XDTE4MDMxNzExMTMxNloXDTIwMDMxNjExMTMxNlowYTEL\n" +
+ "MAkGA1UEBhMCQVUxDDAKBgNVBAgTA05TVzEPMA0GA1UEBxMGU3lkbmV5MRQwEgYD\n" +
+ "VQQKEwtDQWNlcnQgSW5jLjEdMBsGA1UEAxMUY29tbXVuaXR5LmNhY2VydC5vcmcw\n" +
+ "ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKY4Bz8s5f0AK56dGIl8y1\n" +
+ "qnLyNhJr2pxJF9PInO33meBiCqpoTWpPHyIO51NGeySrlW35ZXUzp6tBMptXQict\n" +
+ "J7PkQcSf+lEn1AmRtWHIFNf/uM5IlgoomKktbAkkK+PLOtDBuZ40sKnRY1ooJ9ZK\n" +
+ "UnOrb5puz1D+JHp8JYxkPfknCNAZLeNPXqn9QqnpFKk8/c2CrVF8hShk/k5t2Dpr\n" +
+ "Q0Et9FkPOYBru9p5LQXQBA5QKPg1ESAVKYxRLbR4tJ02we6rOKWgLCnETlMmdjky\n" +
+ "NgaDG6dg79wNKu/uuYyQSXaAnJU67RGXNxIpudOlZ0c2+467mWDFaUHY4yzGTquq\n" +
+ "OGhMDXJu2fe7kDcBP8qH9YeIhN1WSLSnN4cbIP9UVxZXNfZ0WnA2Drj8iGlpL48v\n" +
+ "vBzuUD6EZ+WTeOkoapb0CRGAB+wdMQ6Tg+87tx8vUkhilk3NZ3kKRzOoDKiDisK9\n" +
+ "/WFh8aU7Eq62V15TmzOOkCHmXME1KH2CuzG4MQzalFz8ahRQQnezEMt91uHvCZya\n" +
+ "t5lcGr9W57FnYcxG6KqUO4iV6HWmJYXYhl5PfpEKzKktceH1PnuDptnE8mtdJW1T\n" +
+ "8p43ubgcAGxEvsq6nbeY76b1xlIkq1/NEL3BPDSoz+Tnz5MwLKjHQcqA7Av/KRH3\n" +
+ "VBnw4YI0VtGxZnz4wjyA8wIDAQABo4ICRTCCAkEwDAYDVR0TAQH/BAIwADAOBgNV\n" +
+ "HQ8BAf8EBAMCA6gwNAYDVR0lBC0wKwYIKwYBBQUHAwIGCCsGAQUFBwMBBglghkgB\n" +
+ "hvhCBAEGCisGAQQBgjcKAwMwMwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzABhhdo\n" +
+ "dHRwOi8vb2NzcC5jYWNlcnQub3JnLzAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8v\n" +
+ "Y3JsLmNhY2VydC5vcmcvcmV2b2tlLmNybDCCAYEGA1UdEQSCAXgwggF0ghRjb21t\n" +
+ "dW5pdHkuY2FjZXJ0Lm9yZ6AiBggrBgEFBQcIBaAWDBRjb21tdW5pdHkuY2FjZXJ0\n" +
+ "Lm9yZ4Ibbm9jZXJ0LmNvbW11bml0eS5jYWNlcnQub3JnoCkGCCsGAQUFBwgFoB0M\n" +
+ "G25vY2VydC5jb21tdW5pdHkuY2FjZXJ0Lm9yZ4IZY2VydC5jb21tdW5pdHkuY2Fj\n" +
+ "ZXJ0Lm9yZ6AnBggrBgEFBQcIBaAbDBljZXJ0LmNvbW11bml0eS5jYWNlcnQub3Jn\n" +
+ "ghBlbWFpbC5jYWNlcnQub3JnoB4GCCsGAQUFBwgFoBIMEGVtYWlsLmNhY2VydC5v\n" +
+ "cmeCF25vY2VydC5lbWFpbC5jYWNlcnQub3JnoCUGCCsGAQUFBwgFoBkMF25vY2Vy\n" +
+ "dC5lbWFpbC5jYWNlcnQub3JnghVjZXJ0LmVtYWlsLmNhY2VydC5vcmegIwYIKwYB\n" +
+ "BQUHCAWgFwwVY2VydC5lbWFpbC5jYWNlcnQub3JnMA0GCSqGSIb3DQEBDQUAA4IC\n" +
+ "AQBWaOcDYaF25eP9eJTBUItFKkK3ppq7eN0qT9qyrWVxhRMWtAYcjW8hfSOx5xPS\n" +
+ "4bYL8RJz+1NNyzZqbyhvHt9JnCn1g2HllSD1HTHSMxZZrdjWq/9XxnmG55u2CUfo\n" +
+ "hN1M0qmUJvvWv0T4YWMwhv94tKrThDXnvqa4S+JfnTZQTLPAVq+iTKr+bsdB7pkI\n" +
+ "D59SJdE9tRsrb1wfbBbEpYw2LBZo7Jje4E9FmtnMraGxZtFsHhpZvYAnEt80eFts\n" +
+ "ccSOlhqowW9Hqx0pg55Sq9Wrj9T+AxTx/6sAJL4qxm7CRjeIAqW5fksvA4yXgYaq\n" +
+ "g6M2uIcRMEeafN8bHy1LOXkZDAcbusPfAGenMdE/p5B0K45Rlx3+dfNUjHyF4+ob\n" +
+ "FOVNxgPcfCZ2lJrgvJbw9tBGqC13yPUlkywQ+7QSJgTPbWrnXLIu7fz5SmCxk5KD\n" +
+ "zsq4F4YsaeBIYeHOsJLbqeqftm3eNBESphOvXlZKMGRMiThVWIaX5PIZB5OKgyE3\n" +
+ "C5CvKcv5qv1CeI7qFtLkq28QKCqJJIfTDvArEq/O5P2d+yQetYkWN5mzCJqT/kB+\n" +
+ "y74nu6kCBoZNWBZHDKeM6NkZD1/wI47S2A4cmE7SiGx3AcNRhmrXhvnSD7u7cGVD\n" +
+ "b5yw6z+JqFRMqMm0SuSx5X2oKNKfnqY77fIx6dtY8F5Scg==\n" +
+ "-----END CERTIFICATE-----\n" +
+ " 1 s:/O=Root CA/OU=http://www.cacert.org/CN=CA Cert Signing Authority/emailAddress=support@cacert.org\n" +
+ " i:/O=Root CA/OU=http://www.cacert.org/CN=CA Cert Signing Authority/emailAddress=support@cacert.org\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290\n" +
+ "IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB\n" +
+ "IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA\n" +
+ "Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO\n" +
+ "BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi\n" +
+ "MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ\n" +
+ "ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\n" +
+ "CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ\n" +
+ "8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6\n" +
+ "zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y\n" +
+ "fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7\n" +
+ "w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc\n" +
+ "G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k\n" +
+ "epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q\n" +
+ "laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ\n" +
+ "QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU\n" +
+ "fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826\n" +
+ "YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w\n" +
+ "ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY\n" +
+ "gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe\n" +
+ "MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0\n" +
+ "IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy\n" +
+ "dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw\n" +
+ "czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0\n" +
+ "dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl\n" +
+ "aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC\n" +
+ "AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg\n" +
+ "b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB\n" +
+ "ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc\n" +
+ "nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg\n" +
+ "18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c\n" +
+ "gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl\n" +
+ "Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY\n" +
+ "sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T\n" +
+ "SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF\n" +
+ "CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum\n" +
+ "GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk\n" +
+ "zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW\n" +
+ "omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD\n" +
+ "-----END CERTIFICATE-----";
+
+ private static String LETS_ENCRYPT_CERT = "-----BEGIN CERTIFICATE-----\n" +
+ "MIIFTTCCBDWgAwIBAgISA/Qu8kKrD8kLzdY+/WPM8whbMA0GCSqGSIb3DQEBCwUA\n" +
+ "MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD\n" +
+ "ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTA4MjgxMzQ0MzdaFw0x\n" +
+ "OTExMjYxMzQ0MzdaMBYxFDASBgNVBAMTC2J1aWxkLnF6LmlvMIIBIjANBgkqhkiG\n" +
+ "9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9Q/StADlpSnsShayw4SV4dIbiOiiEYwqBlB7\n" +
+ "FYFF7LfZdREXlYBaTH46hUJI1ooUfsfnNTnYHac6tCEwr9wQnnobO7ACtuYENrVN\n" +
+ "HiuzYtMGN90mqf2+PXhHb+xGpBrD36fmq4Ix3aIc5o4lKxFY4IstfbTbYDanF1Q4\n" +
+ "qUIRUSdAJdgJqmJB2hwlFvjzeBGV4h6vgmiEsATawGoSDMLdWsFpiEnYLTfyvvhY\n" +
+ "5L4e2O9roBOEQ/YJbWVrewh6LYs6s6SbbNkKttQNSGUFVeW6u8q5+yHi2chSXlwW\n" +
+ "+o1SdjE6yw9laHp/nog5gyg95O2xm36YA3mRgfoAEfimwFwf2wIDAQABo4ICXzCC\n" +
+ "AlswDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD\n" +
+ "AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTRuEPSdvHr2SkCIpArJG34rUOPLzAf\n" +
+ "BgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEw\n" +
+ "LgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcw\n" +
+ "LwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcv\n" +
+ "MBYGA1UdEQQPMA2CC2J1aWxkLnF6LmlvMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcG\n" +
+ "CysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5\n" +
+ "cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDvAHYAb1N2rDHwMRnYmQCkURX/\n" +
+ "dxUcEdkCwQApBo2yCJo32RMAAAFs2K+GMAAABAMARzBFAiAI6WH6tspPGgp6W3KI\n" +
+ "n3Ihkb5OqS4KjGFbWNxsJq+/FgIhAJ0zLvFPdlivXpJd/Vn/+xKIBeAs9Ens2uxS\n" +
+ "A34B35oyAHUAY/Lbzeg7zCzPC3KEJ1drM6SNYXePvXWmOLHHaFRL2I0AAAFs2K+F\n" +
+ "FwAABAMARjBEAiAEYpsT6YoIByfh2SHOjuvICRUejlAHVS6bbPN+hvV+4gIgS6pt\n" +
+ "7MtF6GA83AF3lVZPCSnUKp3VvqcEjchf493wHAowDQYJKoZIhvcNAQELBQADggEB\n" +
+ "AH1Nr3BfiCG6iRUtGpaxoIv1J2XDmxAfz5kEtoErwo/oPTz2xY8UyYa1WFlCyJU1\n" +
+ "JWvGrbpT3MQXbdrLsSyT2HQRwEKzXr/u8rRSj18cqggwi8T/f9HgZXjf4ly19uYU\n" +
+ "5GqLBsPwO8BVzawr/bnI0viH1uVpcIQA/rW63LkOL8bMv16zW27mnoEAo8NG1YZU\n" +
+ "IEuCfMH/wFfkbmcw549l2PqIidVqSvWPltLlGdkNJYobFvyg5ThWXNb57cNIMb1k\n" +
+ "Egy5O7RqmVycOdt6//M5KrluWDUS/qi+7oAllGJ9AnFVDttmKuklrhGmwRv/ezN7\n" +
+ "gUtpN5eb5M1XxvExz3fXxfM=\n" +
+ "-----END CERTIFICATE-----\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/\n" +
+ "MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" +
+ "DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow\n" +
+ "SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT\n" +
+ "GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC\n" +
+ "AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF\n" +
+ "q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8\n" +
+ "SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0\n" +
+ "Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA\n" +
+ "a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj\n" +
+ "/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T\n" +
+ "AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG\n" +
+ "CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv\n" +
+ "bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k\n" +
+ "c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw\n" +
+ "VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC\n" +
+ "ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz\n" +
+ "MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu\n" +
+ "Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF\n" +
+ "AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo\n" +
+ "uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/\n" +
+ "wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu\n" +
+ "X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG\n" +
+ "PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6\n" +
+ "KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==\n" +
+ "-----END CERTIFICATE-----";
+}
diff --git a/test/qz/installer/browser/AppFinderTests.java b/test/qz/installer/browser/AppFinderTests.java
new file mode 100644
index 000000000..929e956e3
--- /dev/null
+++ b/test/qz/installer/browser/AppFinderTests.java
@@ -0,0 +1,35 @@
+package qz.installer.browser;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import qz.installer.certificate.firefox.locator.AppAlias;
+import qz.installer.certificate.firefox.locator.AppLocator;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+public class AppFinderTests {
+ private static final Logger log = LoggerFactory.getLogger(AppFinderTests.class);
+
+ public static void main(String ... args) throws Exception {
+ runTest(AppAlias.FIREFOX);
+ }
+
+ private static void runTest(AppAlias app) {
+ Date begin = new Date();
+ ArrayList appList = AppLocator.locate(app);
+
+ StringBuilder output = new StringBuilder("Found apps:\n");
+ for (AppLocator info : appList) {
+ output.append(String.format(" name: '%s', path: '%s', version: '%s'\n",
+ info.getName(),
+ info.getPath(),
+ info.getVersion()
+ ));
+ }
+
+ Date end = new Date();
+ log.debug(output.toString());
+ log.debug("Time to find find {}: {}s", app.name(), (end.getTime() - begin.getTime())/1000.0f);
+ }
+}
\ No newline at end of file
diff --git a/test/qz/utils/JsonWriterTests.java b/test/qz/utils/JsonWriterTests.java
new file mode 100644
index 000000000..b3a945b57
--- /dev/null
+++ b/test/qz/utils/JsonWriterTests.java
@@ -0,0 +1,46 @@
+package qz.utils;
+
+import org.codehaus.jettison.json.JSONException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class JsonWriterTests {
+
+ private static final Logger log = LoggerFactory.getLogger(JsonWriterTests.class);
+
+ private static String DEFAULT_PATH = "/Applications/Firefox.app/Contents/Resources/distribution/policies.json";
+ private static String DEFAULT_DATA = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }";
+ private static boolean DEFAULT_OVERWRITE = false;
+ private static boolean DEFAULT_DELETE = false;
+
+ public static void main(String... args) {
+ String usingPath = DEFAULT_PATH;
+ if (args.length > 0) {
+ usingPath = args[0];
+ }
+ String usingData = DEFAULT_DATA;
+ if (args.length > 1) {
+ usingData = args[1];
+ }
+ boolean usingOverwrite = DEFAULT_OVERWRITE;
+ if (args.length > 2) {
+ usingOverwrite = Boolean.parseBoolean(args[2]);
+ }
+ boolean usingDeletion = DEFAULT_DELETE;
+ if (args.length > 3) {
+ usingDeletion = Boolean.parseBoolean(args[3]);
+ }
+
+ try {
+ JsonWriter.write(usingPath, usingData, usingOverwrite, usingDeletion);
+ }
+ catch(JSONException jsone) {
+ log.error("Failed to read JSON", jsone);
+ }
+ catch(IOException ioe) {
+ log.error("Failed to access file", ioe);
+ }
+ }
+}