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