From ef49e7bb0b7663b4d072f030584edc9897049fe6 Mon Sep 17 00:00:00 2001 From: Kroese Date: Tue, 21 Jul 2026 14:05:28 +0200 Subject: [PATCH] feat: Refactor Windows answer file handling into separate script (#1923) --- src/answer.sh | 1228 ++++++++++++++++++++++++++++++++++++++++++++++++ src/define.sh | 856 --------------------------------- src/entry.sh | 1 + src/install.sh | 672 ++++++-------------------- 4 files changed, 1381 insertions(+), 1376 deletions(-) create mode 100644 src/answer.sh diff --git a/src/answer.sh b/src/answer.sh new file mode 100644 index 00000000..87127fb9 --- /dev/null +++ b/src/answer.sh @@ -0,0 +1,1228 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +validateResolution() { + + local name="$1" + local value="$2" + local minimum="$3" + local number + + if [[ ! "$value" =~ ^[0-9]+$ ]] || [ "${#value}" -gt 5 ]; then + error "The $name variable must be between $minimum and 16384!" + return 1 + fi + + number=$((10#$value)) + + if [ "$number" -lt "$minimum" ] || [ "$number" -gt 16384 ]; then + error "The $name variable must be between $minimum and 16384!" + return 1 + fi + + return 0 +} + +validateProductKey() { + + local value="$1" + + [ -z "$value" ] && return 0 + + if [[ ! "$value" =~ ^[A-Za-z0-9]{5}(-[A-Za-z0-9]{5}){4}$ ]]; then + error "The KEY variable must contain a valid 25-character product key!" + return 1 + fi + + return 0 +} + +validateComputerName() { + + local value="$1" + + [ -z "$value" ] && return 0 + + if [ "${#value}" -gt 15 ]; then + error "The HOST variable cannot contain more than 15 characters!" + return 1 + fi + + if [[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$ ]]; then + error "The HOST variable may only contain letters, digits, and hyphens, and cannot start or end with a hyphen!" + return 1 + fi + + if [[ "$value" =~ ^[0-9]+$ ]]; then + error "The HOST variable cannot contain only digits!" + return 1 + fi + + return 0 +} + +validateWorkgroup() { + + local value="$1" + local safe="" + + [ -z "$value" ] && return 0 + + if [ "${#value}" -gt 15 ]; then + error "The WORKGROUP variable cannot contain more than 15 characters!" + return 1 + fi + + safe=$(printf '%s' "$value" | tr -d '"/\\[]:;|=,+*?<>') || return 1 + + if [[ "$safe" != "$value" ]]; then + error "The WORKGROUP variable contains characters that are not valid in a NetBIOS name!" + return 1 + fi + + if [[ "$value" =~ ^[.[:space:]]+$ ]]; then + error "The WORKGROUP variable cannot consist only of spaces or periods!" + return 1 + fi + + return 0 +} + +validateMembership() { + + if [ -n "$DOMAIN" ] && [ -n "$WORKGROUP" ]; then + error "The DOMAIN and WORKGROUP variables cannot be used together!" + return 1 + fi + + if [ -n "$DOMAIN_OU" ] && [ -z "$DOMAIN" ]; then + error "The DOMAIN_OU variable requires DOMAIN to be specified!" + return 1 + fi + + validateWorkgroup "$WORKGROUP" || return 1 + return 0 +} + +validatePassword() { + + local value="$1" + local desc="${2:-}" + local suffix="" + + [ -n "$desc" ] && suffix=" for $desc" + + if [ "${#value}" -gt 127 ]; then + error "The PASSWORD variable cannot contain more than 127 characters$suffix!" + return 1 + fi + + if [[ "$value" =~ [[:cntrl:]] ]]; then + error "The PASSWORD variable cannot contain control characters$suffix!" + return 1 + fi + + return 0 +} + +escapeXMLSed() { + + local s + + s=$(escapeXML "$1") || return 1 + s=${s//\\/\\\\} + s=${s//&/\\&} + s=${s//|/\\|} + + printf '%s' "$s" + return 0 +} + +validateUsername() { + + local value="$1" + local type="$2" + local maximum + + case "$type" in + "local" ) + maximum=20 + [ -z "$value" ] && return 0 + ;; + "domain" ) + maximum=256 + + if [ -z "$value" ]; then + error "The USERNAME variable does not contain a valid domain account name!" + return 1 + fi ;; + * ) + return 1 ;; + esac + + if [ "${#value}" -gt "$maximum" ]; then + if [[ "$type" == "domain" ]]; then + error "The USERNAME variable cannot contain more than $maximum characters for a domain account!" + else + error "The USERNAME variable cannot contain more than $maximum characters!" + fi + return 1 + fi + + if [[ "$value" =~ [[:cntrl:]] ]]; then + error "The USERNAME variable cannot contain control characters!" + return 1 + fi + + case "$value" in + *'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* | *'%'* | *'@'* ) + if [[ "$type" == "domain" ]]; then + error "The domain account name contains characters that are not supported by Windows unattended setup!" + else + error "The USERNAME variable contains characters that are not supported by Windows local accounts!" + fi + return 1 ;; + esac + + if [[ "$value" == *"." ]]; then + error "The USERNAME variable cannot end with a period!" + return 1 + fi + + if [[ "$value" =~ ^[.[:space:]]+$ ]]; then + error "The USERNAME variable cannot consist only of spaces or periods!" + return 1 + fi + + case "${value^^}" in + "NONE" ) + error "The USERNAME value \"NONE\" is reserved by Windows!" + return 1 ;; + "ADMINISTRATOR" | "GUEST" | "DEFAULTACCOUNT" | "WDAGUTILITYACCOUNT" | "WSIACCOUNT" ) + if [[ "$type" == "local" ]]; then + error "The USERNAME value \"$value\" is reserved for a built-in Windows account!" + return 1 + fi ;; + esac + + return 0 +} + +validateDomainName() { + + local value="$1" + local name="${2:-DOMAIN}" + + if [ -z "$value" ]; then + error "The $name variable must contain a valid domain name!" + return 1 + fi + + if [[ "$value" == *"://"* ]]; then + error "The $name variable must contain a domain name, not a URL!" + return 1 + fi + + if [ "${#value}" -gt 255 ] || + [[ "$value" =~ [[:cntrl:]] ]] || + [[ "$value" =~ [[:space:]] ]] || + [[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$ ]]; then + + error "The $name variable does not contain a valid domain name!" + return 1 + fi + + return 0 +} + +setXML() { + + local file="/custom.xml" + + CUSTOM_XML="" + + if [ -d "$file" ]; then + error "The bind $file maps to a file that does not exist!" && exit 67 + fi + + [ ! -f "$file" ] || [ ! -s "$file" ] && file="$STORAGE/custom.xml" + [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/custom.xml" + [ ! -f "$file" ] || [ ! -s "$file" ] && file="$1" + [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/$DETECTED.xml" + [ ! -f "$file" ] || [ ! -s "$file" ] && return 1 + + case "$file" in + "/custom.xml" | "$STORAGE/custom.xml" ) CUSTOM_XML="Y" ;; + esac + + XML="$file" + return 0 +} + +updateDomain() { + + local asset="$1" + local domain account auth pass pw + local cred_domain ou arch tmp result + + domain=$(escapeXML "$2") || return 1 + account=$(escapeXML "$3") || return 1 + auth=$(escapeXML "$4") || return 1 + pass=$(escapeXML "$5") || return 1 + pw="$6" + ou=$(escapeXML "$7") || return 1 + + arch=$(sed -n -E \ + '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \ + "$asset") || return 1 + + [ -z "$arch" ] && return 1 + + cred_domain="$domain" + + case "$4" in + *@* ) cred_domain="" ;; + esac + + grep -Eq 'Microsoft-Windows-UnattendedJoin|])' "$asset" && return 1 + + tmp=$(mktemp -d) || return 1 + result="$tmp/answer.xml" + + if ! DOMAIN_XML="$domain" ACCOUNT_XML="$account" \ + AUTH_XML="$auth" PASS_XML="$pass" \ + CRED_DOMAIN="$cred_domain" PW="$pw" OU_XML="$ou" \ + ARCH_XML="$arch" \ + awk ' + /]*pass="specialize"[^>]*>/ { section = "specialize" } + /]*pass="oobeSystem"[^>]*>/ { section = "oobeSystem" } + section == "oobeSystem" && /])/ { in_accounts = 1 } + section == "oobeSystem" && /])/ { in_autologon = 1 } + + section == "oobeSystem" && in_accounts && !accounts_added && + /])/ { + print " \n" \ + " \n" \ + " \n" \ + " " ENVIRON["ACCOUNT_XML"] "\n" \ + " Administrators\n" \ + " \n" \ + " " ENVIRON["DOMAIN_XML"] "\n" \ + " \n" \ + " " + accounts_added = 1 + } + + section == "oobeSystem" && in_autologon && + /^[[:space:]]*.*<\/Username>[[:space:]]*$/ { + print " " ENVIRON["ACCOUNT_XML"] "\n" \ + " " ENVIRON["DOMAIN_XML"] "" + autologon_added = 1 + next + } + + section == "oobeSystem" && in_autologon && + /^[[:space:]]*])/ { next } + + section == "oobeSystem" && in_autologon && + /^[[:space:]]*.*<\/Value>[[:space:]]*$/ { + print " " ENVIRON["PW"] "" + password_added = 1 + next + } + + section == "specialize" && !join_added && + /^[[:space:]]*<\/settings>[[:space:]]*$/ { + print " \n" \ + " \n" \ + " " + + if (ENVIRON["CRED_DOMAIN"] != "") { + print " " ENVIRON["CRED_DOMAIN"] "" + } + + print " " ENVIRON["AUTH_XML"] "\n" \ + " " ENVIRON["PASS_XML"] "\n" \ + " \n" \ + " " ENVIRON["DOMAIN_XML"] "" + + if (ENVIRON["OU_XML"] != "") { + print " " ENVIRON["OU_XML"] "" + } + + print " \n" \ + " " + + join_added = 1 + } + + { print } + + section == "oobeSystem" && /<\/AutoLogon>/ { in_autologon = 0 } + section == "oobeSystem" && /<\/UserAccounts>/ { in_accounts = 0 } + /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } + + END { exit !(join_added && accounts_added && autologon_added && password_added) } + ' "$asset" > "$result" || + ! mv -f "$result" "$asset"; then + + rm -rf "$tmp" || true + return 1 + fi + + rm -rf "$tmp" || return 1 + return 0 +} + +updateWorkgroup() { + + local asset="$1" + local workgroup arch tmp result + + workgroup=$(escapeXML "$2") || return 1 + arch=$(sed -n -E '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' "$asset") || return 1 + [ -z "$arch" ] && return 1 + + grep -q 'Microsoft-Windows-UnattendedJoin' "$asset" && return 1 + + tmp=$(mktemp -d) || return 1 + result="$tmp/answer.xml" + + if ! WORKGROUP_XML="$workgroup" ARCH_XML="$arch" awk ' + /]*pass="specialize"[^>]*>/ { section = "specialize" } + + section == "specialize" && !workgroup_added && + /^[[:space:]]*<\/settings>[[:space:]]*$/ { + print " \n" \ + " \n" \ + " " ENVIRON["WORKGROUP_XML"] "\n" \ + " \n" \ + " " + workgroup_added = 1 + } + + { print } + + /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } + END { exit !workgroup_added } + ' "$asset" > "$result" || + ! mv -f "$result" "$asset"; then + + rm -rf "$tmp" || true + return 1 + fi + + rm -rf "$tmp" || return 1 + return 0 +} + +updateXML() { + + local asset="$1" + local language="$2" + local app value culture region keyboard edition + local user user_xml auth_user admin pass pw + local domain qualifier host workgroup key + + [ -z "$WIDTH" ] && WIDTH="1280" + [ -z "$HEIGHT" ] && HEIGHT="720" + + validateResolution "WIDTH" "$WIDTH" 320 || return 1 + validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 + validateMembership || return 1 + validateComputerName "$HOST" || return 1 + validateProductKey "$KEY" || return 1 + validatePassword "$PASSWORD" || return 1 + + app=$(escapeXMLSed "$APP for $ENGINE") || return 1 + + sed -i "s|>Windows for Docker<|>$app<|g" "$asset" || return 1 + sed -i -E "s|[^<]*|$HEIGHT|g" "$asset" || return 1 + sed -i -E "s|[^<]*|$WIDTH|g" "$asset" || return 1 + + culture=$(getLanguage "$language" "culture") || return 1 + + if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then + value=$(escapeXMLSed "$culture") || return 1 + sed -i "s|en-US|$value|g" "$asset" || return 1 + fi + + region="$REGION" + [ -z "$region" ] && region="$culture" + + if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then + value=$(escapeXMLSed "$region") || return 1 + sed -i "s|en-US|$value|g" "$asset" || return 1 + sed -i "s|en-US|$value|g" "$asset" || return 1 + fi + + keyboard="$KEYBOARD" + [ -z "$keyboard" ] && keyboard="$culture" + + if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then + value=$(escapeXMLSed "$keyboard") || return 1 + sed -i "s|en-US|$value|g" "$asset" || return 1 + sed -i "s|0409:00000409|$value|g" "$asset" || return 1 + fi + + if [ -n "$HOST" ]; then + host=$(escapeXMLSed "$HOST") || return 1 + sed -i -E "s|[^<]*|$host|g" "$asset" || return 1 + fi + + domain="$DOMAIN" + workgroup="$WORKGROUP" + + if [ -n "$domain" ]; then + + if [ -z "$USERNAME" ]; then + error "The USERNAME variable must be specified when joining a domain!" + return 1 + fi + + if [ -z "$PASSWORD" ]; then + error "The PASSWORD variable must be specified when joining a domain!" + return 1 + fi + + validateDomainName "$domain" || return 1 + + auth_user="$USERNAME" + qualifier="" + + if [[ "$auth_user" == *\\* ]]; then + error "The USERNAME variable must use either \"user\" or \"user@domain\" format!" + return 1 + fi + + case "$auth_user" in + *@* ) + user="${auth_user%%@*}" + qualifier="${auth_user#*@}" + + if [ -z "$user" ] || [ -z "$qualifier" ] || [[ "$qualifier" == *@* ]]; then + error "The USERNAME variable does not contain a valid domain account name!" + return 1 + fi + + validateDomainName "$qualifier" "USERNAME" || return 1 + + if [[ "${qualifier,,}" != "${domain,,}" ]]; then + error "The domain in the USERNAME variable must match the DOMAIN variable!" + return 1 + fi + ;; + * ) + user="$auth_user" + ;; + esac + + validateUsername "$user" "domain" || return 1 + + if [[ "${user,,}" == "docker" ]]; then + error "The USERNAME variable must be changed from its default value when joining a domain!" + return 1 + fi + + if [[ "$PASSWORD" == "admin" ]]; then + error "The PASSWORD variable must be changed from its default value when joining a domain!" + return 1 + fi + + else + + user="$USERNAME" + validateUsername "$user" "local" || return 1 + + if [ -n "$user" ]; then + user_xml=$(escapeXMLSed "$user") || return 1 + + sed -i "s|-name \"Docker\"|-name \"\$env:USERNAME\"|g" "$asset" || return 1 + sed -i 's|where name="Docker"|where name="%USERNAME%"|g' "$asset" || return 1 + sed -i "s|Docker|$user_xml|g" "$asset" || return 1 + sed -i "s|Docker|$user_xml|g" "$asset" || return 1 + sed -i "s|Docker|$user_xml|g" "$asset" || return 1 + fi + + fi + + if [ -n "$domain" ]; then + pass="admin" + else + [ -n "$PASSWORD" ] && pass="$PASSWORD" || pass="admin" + fi + + pw=$(printf '%s' "${pass}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1 + admin=$(printf '%s' "${pass}AdministratorPassword" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1 + + sed -i -z -E "s#([[:space:]]*|>[^<]*)#\1>$pw#g" "$asset" || return 1 + sed -i -z -E "s#([[:space:]]*|>[^<]*)#\1>$admin#g" "$asset" || return 1 + sed -i -E "s|[^<]*</PlainText>|<PlainText>false</PlainText>|g" "$asset" || return 1 + + if [ -n "$domain" ]; then + + pw=$(printf '%s' "${PASSWORD}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1 + + if ! updateDomain "$asset" "$domain" "$user" \ + "$auth_user" "$PASSWORD" "$pw" "$DOMAIN_OU"; then + error "Failed to add domain configuration to answer file!" + return 1 + fi + + elif [ -n "$workgroup" ]; then + + if ! updateWorkgroup "$asset" "$workgroup"; then + error "Failed to add workgroup configuration to answer file!" + return 1 + fi + + fi + + if disabled "$AUTOLOGIN"; then + sed -i -E '/^[[:space:]]*<AutoLogon([[:space:]>])/,/^[[:space:]]*<\/AutoLogon>[[:space:]]*$/d' "$asset" || return 1 + fi + + if [ -n "$EDITION" ]; then + case "${EDITION,,}" in + "core" ) edition="STANDARDCORE" ;; + * ) edition="${EDITION^^}" ;; + esac + + edition=$(escapeXMLSed "$edition") || return 1 + sed -i "s|SERVERSTANDARD</Value>|SERVER$edition</Value>|g" "$asset" || return 1 + fi + + if [ -n "$KEY" ]; then + key=$(escapeXMLSed "$KEY") || return 1 + sed -i -E '/^[[:space:]]*<ProductKey>[[:space:]]*$/,/^[[:space:]]*<\/ProductKey>[[:space:]]*$/d' "$asset" || return 1 + sed -i -E "s|<ProductKey>[^<]*</ProductKey>|<ProductKey>$key</ProductKey>|g" "$asset" || return 1 + sed -i "s|</UserData>| <ProductKey>\n <Key>$key</Key>\n <WillShowUI>OnError</WillShowUI>\n </ProductKey>\n </UserData>|g" "$asset" || return 1 + fi + + if ! xmllint --nonet --noout "$asset"; then + error "The generated answer file is not valid XML!" + return 1 + fi + + return 0 +} + +escapeSIFValue() { + + local s="$1" + + s=${s//%/%%} + s=${s//\"/\"\"} + + printf '%s' "$s" + return 0 +} + +escapeRegistryValue() { + + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' +} + +validateLegacyText() { + + local name="$1" + local value="$2" + local desc="${3:-}" + local suffix="" + + [ -n "$desc" ] && suffix=" for $desc" + + if [[ "$value" =~ [[:cntrl:]] ]]; then + error "The $name variable cannot contain control characters$suffix!" + return 1 + fi + + if [[ "$value" == *'"'* ]]; then + error "The $name variable cannot contain double quotes$suffix!" + return 1 + fi + + return 0 +} + +validateLegacyUsername() { + + local value="$1" + local desc="${2:-}" + local suffix="" + + [ -n "$desc" ] && suffix=" for $desc" + + if [ -z "$value" ]; then + error "The USERNAME variable cannot be empty$suffix!" + return 1 + fi + + if [ "${#value}" -gt 20 ]; then + error "The USERNAME variable cannot contain more than 20 characters$suffix!" + return 1 + fi + + if [[ "$value" =~ [[:cntrl:]] ]]; then + error "The USERNAME variable cannot contain control characters$suffix!" + return 1 + fi + + case "$value" in + *'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* ) + error "The USERNAME variable contains unsupported characters$suffix!" + return 1 ;; + esac + + if [[ "$value" == *"." ]]; then + error "The USERNAME variable cannot end with a period$suffix!" + return 1 + fi + + if [[ "$value" =~ ^[.[:space:]]+$ ]]; then + error "The USERNAME variable cannot consist only of spaces or periods$suffix!" + return 1 + fi + + if [[ "${value^^}" == "GUEST" ]]; then + error "The USERNAME value \"$value\" is reserved for a built-in Windows account$suffix!" + return 1 + fi + + return 0 +} + +detectLegacy() { + + local dir="$1" + local find + + find=$(find "$dir" -maxdepth 1 -type d -iname WIN95 -print -quit) + [ -n "$find" ] && DETECTED="win95" && return 0 + + find=$(find "$dir" -maxdepth 1 -type d -iname WIN98 -print -quit) + [ -n "$find" ] && DETECTED="win98" && return 0 + + find=$(find "$dir" -maxdepth 1 -type d -iname WIN9X -print -quit) + [ -n "$find" ] && DETECTED="win9x" && return 0 + + find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_W.40 -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_S.40 -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_TS.40 -print -quit) + [ -n "$find" ] && DETECTED="winnt4" && return 0 + + find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_NT.5 -print -quit) + + if [ -n "$find" ]; then + + find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IA.5 -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_ID.5 -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IP.5 -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IS.5 -print -quit) + [ -n "$find" ] && DETECTED="win2k" && return 0 + + fi + + find=$(find "$dir" -maxdepth 1 -iname WIN51 -print -quit) + + if [ -n "$find" ]; then + + find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AP -print -quit) + [ -n "$find" ] && DETECTED="winxpx64" && return 0 + + find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IC -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IP -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname setupxp.htm -print -quit) + [ -n "$find" ] && DETECTED="winxpx86" && return 0 + + find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IS -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IA -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IB -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51ID -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IL -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AA -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AD -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AS -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51MA -print -quit) + [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51MD -print -quit) + [ -n "$find" ] && DETECTED="win2003r2" && return 0 + + fi + + return 1 +} + +legacyInstall() { + + local pid="" + local file="" + local dir="$2" + local desc="$3" + local driver="$4" + local drivers="/tmp/drivers" + + if [ -n "$DOMAIN" ]; then + error "The DOMAIN variable is not supported for $desc!" + return 1 + fi + + ETFS="[BOOT]/Boot-NoEmul.img" + + if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then + error "Failed to locate file \"$ETFS\" in $desc ISO image!" && return 1 + fi + + local arch target + [ -d "$dir/AMD64" ] && arch="amd64" || arch="x86" + [[ "${arch,,}" == "x86" ]] && target="$dir/I386" || target="$dir/AMD64" + + if [ ! -d "$target" ]; then + error "Failed to locate directory \"$target\" in $desc ISO image!" && return 1 + fi + + if [[ "${driver,,}" == "xp" || "${driver,,}" == "2k3" ]]; then + + local msg="Adding drivers to image..." + info "$msg" && html "$msg" + + rm -rf "$drivers" || return 1 + mkdir -p "$drivers" || return 1 + + if ! bsdtar -xf /var/drivers.txz -C "$drivers"; then + error "Failed to extract drivers!" && return 1 + fi + + if [ ! -f "$drivers/viostor/$driver/$arch/viostor.sys" ]; then + error "Failed to locate required storage drivers!" && return 1 + fi + + cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$target" || return 1 + + mkdir -p "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1 + cp -L "$drivers/viostor/$driver/$arch/viostor.cat" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1 + cp -L "$drivers/viostor/$driver/$arch/viostor.inf" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1 + cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1 + + if [ ! -f "$drivers/NetKVM/$driver/$arch/netkvm.sys" ]; then + error "Failed to locate required network drivers!" && return 1 + fi + + mkdir -p "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1 + cp -L "$drivers/NetKVM/$driver/$arch/netkvm.cat" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1 + cp -L "$drivers/NetKVM/$driver/$arch/netkvm.inf" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1 + cp -L "$drivers/NetKVM/$driver/$arch/netkvm.sys" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1 + + file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1 + + if [ -z "$file" ]; then + error "The file TXTSETUP.SIF could not be found!" && return 1 + fi + + sed -i '/^\[SCSI.Load\]/s/$/\nviostor=viostor.sys,4/' "$file" || return 1 + sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\nviostor.sys=1,,,,,,4_,4,1,,,1,4/' "$file" || return 1 + sed -i '/^\[SCSI\]/s/$/\nviostor=\"Red Hat VirtIO SCSI Disk Device\"/' "$file" || return 1 + sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00000000=\"viostor\"/' "$file" || return 1 + sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00020000=\"viostor\"/' "$file" || return 1 + sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00021AF4=\"viostor\"/' "$file" || return 1 + + if [ ! -d "$drivers/sata/xp/$arch" ]; then + error "Failed to locate required SATA drivers!" && return 1 + fi + + mkdir -p "$dir/\$OEM\$/\$1/Drivers/sata" || return 1 + cp -Lr "$drivers/sata/xp/$arch/." "$dir/\$OEM\$/\$1/Drivers/sata" || return 1 + cp -Lr "$drivers/sata/xp/$arch/." "$target" || return 1 + + sed -i '/^\[SCSI.Load\]/s/$/\niaStor=iaStor.sys,4/' "$file" || return 1 + sed -i '/^\[FileFlags\]/s/$/\niaStor.sys = 16/' "$file" || return 1 + sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.cat = 1,,,,,,,1,0,0/' "$file" || return 1 + sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.inf = 1,,,,,,,1,0,0/' "$file" || return 1 + sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.sys = 1,,,,,,4_,4,1,,,1,4/' "$file" || return 1 + sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.sys = 1,,,,,,,1,0,0/' "$file" || return 1 + sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaahci.cat = 1,,,,,,,1,0,0/' "$file" || return 1 + sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaAHCI.inf = 1,,,,,,,1,0,0/' "$file" || return 1 + sed -i '/^\[SCSI\]/s/$/\niaStor=\"Intel\(R\) SATA RAID\/AHCI Controller\"/' "$file" || return 1 + sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_8086\&DEV_2922\&CC_0106=\"iaStor\"/' "$file" || return 1 + + rm -rf "$drivers" || return 1 + + fi + + local key setup + setup=$(find "$target" -maxdepth 1 -type f -iname setupp.ini -print -quit) || return 1 + + if [ -n "$setup" ] && [ -z "$KEY" ]; then + + pid=$(<"$setup") || return 1 + pid="${pid%$'\r'}" + + if [[ "$driver" == "2k" ]]; then + + echo "${pid:0:$((${#pid})) - 3}270" > "$setup" || return 1 + + else + + if [[ "$pid" == *"270" ]]; then + + warn "this version of $desc requires a volume license key (VLK), it will ask for one during installation." + + else + + file=$(find "$target" -maxdepth 1 -type f -iname PID.INF -print -quit) || return 1 + + if [ -n "$file" ]; then + + if [[ "$driver" == "2k3" ]]; then + + key=$(grep -i -A 2 "StagingKey" "$file" | tail -n 2 | head -n 1) || key="" + + else + + key="${pid:$((${#pid})) - 8:5}" + if [[ "${pid^^}" == *"OEM" ]]; then + key=$(grep -i -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key="" + else + key=$(grep -i -m 1 -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key="" + fi + key="${key#*= }" + + fi + + key="${key%$'\r'}" + [[ "${#key}" == "29" ]] && KEY="$key" + + fi + + if [ -z "$KEY" ]; then + + # These are NOT pirated keys, they come from official MS documentation. + + case "${driver,,}" in + "xp" ) + + if [[ "${arch,,}" == "x86" ]]; then + # Windows XP Professional x86 generic trial key (no activation) + KEY="DR8GV-C8V6J-BYXHG-7PYJR-DB66Y" + else + # Windows XP Professional x64 generic trial key (no activation) + KEY="B2RBK-7KPT9-4JP6X-QQFWM-PJD6G" + fi ;; + + "2k3" ) + + if [[ "${arch,,}" == "x86" ]]; then + # Windows Server 2003 Standard x86 generic trial key (no activation) + KEY="QKDCQ-TP2JM-G4MDG-VR6F2-P9C48" + else + # Windows Server 2003 Standard x64 generic trial key (no activation) + KEY="P4WJG-WK3W7-3HM8W-RWHCK-8JTRY" + fi ;; + + esac + + echo "${pid:0:$((${#pid})) - 3}000" > "$setup" || return 1 + + fi + + fi + fi + + fi + + validateProductKey "$KEY" || return 1 + + local product="" + [ -n "$KEY" ] && product="ProductID=$KEY" + + mkdir -p "$dir/\$OEM\$" || return 1 + + if ! addFolder "$dir"; then + error "Failed to add OEM folder to image!" && return 1 + fi + + local oem="" + local install="$dir/\$OEM\$/\$1/OEM/install.bat" + [ -f "$install" ] && oem="\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat\\\"\"" + + [ -z "$WIDTH" ] && WIDTH="1280" + [ -z "$HEIGHT" ] && HEIGHT="720" + + validateResolution "WIDTH" "$WIDTH" 320 || return 1 + validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 + validateMembership || return 1 + validateComputerName "$HOST" || return 1 + validateLegacyText "APP" "$APP" "$desc" || return 1 + validateLegacyText "ENGINE" "$ENGINE" "$desc" || return 1 + + XHEX=$(printf '%08x\n' "$((10#$WIDTH))") || return 1 + YHEX=$(printf '%08x\n' "$((10#$HEIGHT))") || return 1 + + local username="${USERNAME:-Docker}" + local password="${PASSWORD:-admin}" + local workgroup="${WORKGROUP:-WORKGROUP}" + + local sifHost sifUsername sifPassword sifOrganization sifWorkgroup + local regUsername regPassword + + validateLegacyUsername "$username" "$desc" || return 1 + validatePassword "$password" "$desc" || return 1 + + sifHost=$(escapeSIFValue "${HOST:-*}") || return 1 + sifUsername=$(escapeSIFValue "$username") || return 1 + sifPassword=$(escapeSIFValue "$password") || return 1 + sifOrganization=$(escapeSIFValue "$APP for $ENGINE") || return 1 + sifWorkgroup=$(escapeSIFValue "$workgroup") || return 1 + regUsername=$(escapeRegistryValue "$username") || return 1 + regPassword=$(escapeRegistryValue "$password") || return 1 + + find "$target" -maxdepth 1 -type f -iname winnt.sif -delete || return 1 + + { echo "[Data]" + echo " AutoPartition=1" + echo " MsDosInitiated=\"0\"" + echo " UnattendedInstall=\"Yes\"" + echo " AutomaticUpdates=\"Yes\"" + echo "" + echo "[Unattended]" + echo " UnattendSwitch=Yes" + echo " UnattendMode=FullUnattended" + echo " FileSystem=NTFS" + echo " OemSkipEula=Yes" + echo " OemPreinstall=Yes" + echo " Repartition=Yes" + echo " WaitForReboot=\"No\"" + echo " DriverSigningPolicy=\"Ignore\"" + echo " NonDriverSigningPolicy=\"Ignore\"" + printf '%s\n' " OemPnPDriversPath=\"Drivers\viostor;Drivers\NetKVM;Drivers\sata\"" + echo " NoWaitAfterTextMode=1" + echo " NoWaitAfterGUIMode=1" + echo " FileSystem=ConvertNTFS" + echo " ExtendOemPartition=0" + echo " Hibernation=\"No\"" + echo "" + echo "[GuiUnattended]" + echo " OEMSkipRegional=1" + echo " OemSkipWelcome=1" + echo " AdminPassword=\"$sifPassword\"" + echo " TimeZone=0" + if disabled "$AUTOLOGIN"; then + echo " AutoLogon=No" + else + echo " AutoLogon=Yes" + echo " AutoLogonCount=65432" + fi + echo "" + echo "[UserData]" + echo " FullName=\"$sifUsername\"" + echo " ComputerName=\"$sifHost\"" + echo " OrgName=\"$sifOrganization\"" + echo " $product" + echo "" + echo "[Identification]" + echo " JoinWorkgroup = \"$sifWorkgroup\"" + echo "" + echo "[Display]" + echo " BitsPerPel=32" + echo " XResolution=$WIDTH" + echo " YResolution=$HEIGHT" + echo "" + echo "[Networking]" + echo " InstallDefaultComponents=Yes" + echo "" + echo "[Branding]" + echo " BrandIEUsingUnattended=Yes" + echo "" + echo "[URL]" + echo " Home_Page = http://www.google.com" + echo " Search_Page = http://www.google.com" + echo "" + echo "[TerminalServices]" + echo " AllowConnections=1" + echo "" + } | unix2dos > "$target/WINNT.SIF" || return 1 + + if [[ "$driver" == "2k3" ]]; then + { echo "[Components]" + echo " TerminalServer=On" + echo "" + echo "[LicenseFilePrintData]" + echo " AutoMode=PerServer" + echo " AutoUsers=5" + echo "" + } | unix2dos >> "$target/WINNT.SIF" || return 1 + fi + + { echo "Windows Registry Editor Version 5.00" + echo "" + echo "[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Security]" + echo "\"FirstRunDisabled\"=dword:00000001" + echo "\"UpdatesDisableNotify\"=dword:00000001" + echo "\"FirewallDisableNotify\"=dword:00000001" + echo "\"AntiVirusDisableNotify\"=dword:00000001" + echo "" + echo "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\wscsvc]" + echo "\"Start\"=dword:00000004" + echo "" + echo "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\GloballyOpenPorts\List]" + echo "\"3389:TCP\"=\"3389:TCP:*:Enabled:@xpsp2res.dll,-22009\"" + echo "" + echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Applets\Tour]" + echo "\"RunCount\"=dword:00000000" + echo "" + echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]" + echo "\"HideFileExt\"=dword:00000000" + echo "" + echo "[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer]" + echo "\"NoWelcomeScreen\"=\"1\"" + echo "" + echo "[HKEY_CURRENT_USER\Software\Microsoft\Internet Connection Wizard]" + echo "\"Completed\"=\"1\"" + echo "\"Desktopchanged\"=\"1\"" + echo "" + echo "[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]" + if disabled "$AUTOLOGIN"; then + echo "\"AutoAdminLogon\"=\"0\"" + else + echo "\"AutoAdminLogon\"=\"1\"" + echo "\"DefaultUserName\"=\"$regUsername\"" + echo "\"DefaultPassword\"=\"$regPassword\"" + echo "\"DefaultDomainName\"=\"Dockur\"" + fi + echo "" + printf '%s\n' "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Video\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]" + echo "\"DefaultSettings.BitsPerPel\"=dword:00000020" + echo "\"DefaultSettings.XResolution\"=dword:$XHEX" + echo "\"DefaultSettings.YResolution\"=dword:$YHEX" + echo "" + printf '%s\n' "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current\System\CurrentControlSet\Control\VIDEO\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]" + echo "\"DefaultSettings.BitsPerPel\"=dword:00000020" + echo "\"DefaultSettings.XResolution\"=dword:$XHEX" + echo "\"DefaultSettings.YResolution\"=dword:$YHEX" + echo "" + echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce]" + printf '%s\n' "\"ScreenSaver\"=\"reg add \\\"HKCU\\\\Control Panel\\\\Desktop\\\" /f /v \\\"SCRNSAVE.EXE\\\" /t REG_SZ /d \\\"off\\\"\"" + printf '%s\n' "\"ScreenSaverOff\"=\"reg add \\\"HKCU\\\\Control Panel\\\\Desktop\\\" /f /v \\\"ScreenSaveActive\\\" /t REG_SZ /d \\\"0\\\"\"" + printf '%s\n' "\"SharedDrive\"=\"cmd /C net use Z: \\\\\\\\host.lan\\\\Data /persistent:yes\"" + echo "$oem" + echo "" + } | unix2dos > "$dir/\$OEM\$/install.reg" || return 1 + + if [[ "$driver" == "2k" ]]; then + { echo "[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Runonce]" + echo "\"^SetupICWDesktop\"=-" + echo "" + } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 + fi + + if [[ "$driver" == "2k3" ]]; then + { echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\srvWiz]" + echo "@=dword:00000000" + echo "" + echo "[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ServerOOBE\SecurityOOBE]" + echo "\"DontLaunchSecurityOOBE\"=dword:00000000" + echo "" + } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 + fi + + { echo "Set WshShell = WScript.CreateObject(\"WScript.Shell\")" + echo "Set WshNetwork = WScript.CreateObject(\"WScript.Network\")" + echo "Set Domain = GetObject(\"WinNT://\" & WshNetwork.ComputerName)" + echo "" + echo "Function DecodeSID(binSID)" + echo " ReDim o(LenB(binSID))" + echo "" + echo " For i = 1 To LenB(binSID)" + echo " o(i-1) = AscB(MidB(binSID, i, 1))" + echo " Next" + echo "" + echo " sid = \"S-\" & CStr(o(0)) & \"-\" & OctetArrayToString _" + echo " (Array(o(2), o(3), o(4), o(5), o(6), o(7)))" + echo " For i = 8 To (4 * o(1) + 4) Step 4" + echo " sid = sid & \"-\" & OctetArrayToString _" + echo " (Array(o(i+3), o(i+2), o(i+1), o(i)))" + echo " Next" + echo "" + echo " DecodeSID = sid" + echo "End Function" + echo "" + echo "Function OctetArrayToString(arr)" + echo " v = 0" + echo " For i = 0 To UBound(arr)" + echo " v = v * 256 + arr(i)" + echo " Next" + echo "" + echo " OctetArrayToString = CStr(v)" + echo "End Function" + echo "" + echo "For Each DomainItem in Domain" + echo " If DomainItem.Class = \"User\" Then" + echo " sid = DecodeSID(DomainItem.Get(\"objectSID\"))" + echo " If Left(sid, 9) = \"S-1-5-21-\" And Right(sid, 4) = \"-500\" Then" + echo " LocalAdminADsPath = DomainItem.ADsPath" + echo " Exit For" + echo " End If" + echo " End If" + echo "Next" + echo "" + echo "Call Domain.MoveHere(LocalAdminADsPath, \"$username\")" + echo "" + echo "Set oLink = WshShell.CreateShortcut(WshShell.SpecialFolders(\"Desktop\") & \"\\Shared.lnk\")" + echo "With oLink" + printf '%s\n' " .TargetPath = \"\\\\host.lan\\Data\"" + echo " .Save" + echo "End With" + echo "Set oLink = Nothing" + echo "" + } | unix2dos > "$dir/\$OEM\$/install.vbs" || return 1 + + { echo "[COMMANDS]" + echo "\"REGEDIT /s install.reg\"" + echo "\"Wscript install.vbs\"" + echo "" + } | unix2dos > "$dir/\$OEM\$/cmdlines.txt" || return 1 + + return 0 +} + +legacyPrepare() { + + local iso="$1" + local dir="$2" + local desc="$3" + + local tmp="$TMP/boot-images" + local image="$tmp/eltorito_img1_bios.img" + + ETFS="boot.img" + + [ -s "$dir/$ETFS" ] && return 0 + rm -f "$dir/$ETFS" || return 1 + rm -rf "$tmp" || return 1 + + if ! LC_ALL=C xorriso \ + -no_rc \ + -osirrox on \ + -indev "$iso" \ + -extract_boot_images "$tmp" >/dev/null 2>&1; then + rm -rf "$tmp" || true + error "Failed to extract boot image from $desc ISO!" + return 1 + fi + + if [ ! -s "$image" ]; then + rm -rf "$tmp" || true + error "Failed to locate BIOS boot image in $desc ISO!" + return 1 + fi + + if ! mv -f "$image" "$dir/$ETFS"; then + rm -rf "$tmp" || true + error "Failed to save boot image from $desc ISO!" + return 1 + fi + + rm -rf "$tmp" || return 1 + return 0 +} + +return 0 diff --git a/src/define.sh b/src/define.sh index 4e1aa6d4..8eab982b 100644 --- a/src/define.sh +++ b/src/define.sh @@ -1298,864 +1298,8 @@ validVersion() { return 1 } -validateResolution() { - - local name="$1" - local value="$2" - local minimum="$3" - local number - - if [[ ! "$value" =~ ^[0-9]+$ ]] || [ "${#value}" -gt 5 ]; then - error "The $name variable must be between $minimum and 16384!" - return 1 - fi - - number=$((10#$value)) - - if [ "$number" -lt "$minimum" ] || [ "$number" -gt 16384 ]; then - error "The $name variable must be between $minimum and 16384!" - return 1 - fi - - return 0 -} - -validateProductKey() { - - local value="$1" - - [ -z "$value" ] && return 0 - - if [[ ! "$value" =~ ^[A-Za-z0-9]{5}(-[A-Za-z0-9]{5}){4}$ ]]; then - error "The KEY variable must contain a valid 25-character product key!" - return 1 - fi - - return 0 -} - -validateComputerName() { - - local value="$1" - - [ -z "$value" ] && return 0 - - if [ "${#value}" -gt 15 ]; then - error "The HOST variable cannot contain more than 15 characters!" - return 1 - fi - - if [[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$ ]]; then - error "The HOST variable may only contain letters, digits, and hyphens, and cannot start or end with a hyphen!" - return 1 - fi - - if [[ "$value" =~ ^[0-9]+$ ]]; then - error "The HOST variable cannot contain only digits!" - return 1 - fi - - return 0 -} - -validateWorkgroup() { - - local value="$1" - local safe="" - - [ -z "$value" ] && return 0 - - if [ "${#value}" -gt 15 ]; then - error "The WORKGROUP variable cannot contain more than 15 characters!" - return 1 - fi - - safe=$(printf '%s' "$value" | tr -d '"/\\[]:;|=,+*?<>') || return 1 - - if [[ "$safe" != "$value" ]]; then - error "The WORKGROUP variable contains characters that are not valid in a NetBIOS name!" - return 1 - fi - - if [[ "$value" =~ ^[.[:space:]]+$ ]]; then - error "The WORKGROUP variable cannot consist only of spaces or periods!" - return 1 - fi - - return 0 -} - -validateMembership() { - - if [ -n "$DOMAIN" ] && [ -n "$WORKGROUP" ]; then - error "The DOMAIN and WORKGROUP variables cannot be used together!" - return 1 - fi - - if [ -n "$DOMAIN_OU" ] && [ -z "$DOMAIN" ]; then - error "The DOMAIN_OU variable requires DOMAIN to be specified!" - return 1 - fi - - validateWorkgroup "$WORKGROUP" || return 1 - return 0 -} - -validateLegacyText() { - - local name="$1" - local value="$2" - local desc="${3:-}" - local suffix="" - - [ -n "$desc" ] && suffix=" for $desc" - - if [[ "$value" =~ [[:cntrl:]] ]]; then - error "The $name variable cannot contain control characters$suffix!" - return 1 - fi - - if [[ "$value" == *'"'* ]]; then - error "The $name variable cannot contain double quotes$suffix!" - return 1 - fi - - return 0 -} - -validateLegacyUsername() { - - local value="$1" - local desc="${2:-}" - local suffix="" - - [ -n "$desc" ] && suffix=" for $desc" - - if [ -z "$value" ]; then - error "The USERNAME variable cannot be empty$suffix!" - return 1 - fi - - if [ "${#value}" -gt 20 ]; then - error "The USERNAME variable cannot contain more than 20 characters$suffix!" - return 1 - fi - - if [[ "$value" =~ [[:cntrl:]] ]]; then - error "The USERNAME variable cannot contain control characters$suffix!" - return 1 - fi - - case "$value" in - *'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* ) - error "The USERNAME variable contains unsupported characters$suffix!" - return 1 ;; - esac - - if [[ "$value" == *"." ]]; then - error "The USERNAME variable cannot end with a period$suffix!" - return 1 - fi - - if [[ "$value" =~ ^[.[:space:]]+$ ]]; then - error "The USERNAME variable cannot consist only of spaces or periods$suffix!" - return 1 - fi - - if [[ "${value^^}" == "GUEST" ]]; then - error "The USERNAME value \"$value\" is reserved for a built-in Windows account$suffix!" - return 1 - fi - - return 0 -} - -validatePassword() { - - local value="$1" - local desc="${2:-}" - local suffix="" - - [ -n "$desc" ] && suffix=" for $desc" - - if [ "${#value}" -gt 127 ]; then - error "The PASSWORD variable cannot contain more than 127 characters$suffix!" - return 1 - fi - - if [[ "$value" =~ [[:cntrl:]] ]]; then - error "The PASSWORD variable cannot contain control characters$suffix!" - return 1 - fi - - return 0 -} - -escapeSIFValue() { - - local s="$1" - - s=${s//%/%%} - s=${s//\"/\"\"} - - printf '%s' "$s" - return 0 -} - -escapeRegistryValue() { - - printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -} - -addFolder() { - - local src="$1" - local folder="/oem" file="" - local dest="$src/\$OEM\$/\$1/OEM" - - [ ! -d "$folder" ] && folder="/OEM" - [ ! -d "$folder" ] && folder="$STORAGE/oem" - [ ! -d "$folder" ] && folder="$STORAGE/OEM" - [ ! -d "$folder" ] && folder="" - - [ -z "$folder" ] && [ -z "$COMMAND" ] && return 0 - - local msg="Adding OEM files to image..." - info "$msg" && html "$msg" - - mkdir -p "$dest" || return 1 - - if [ -n "$folder" ]; then - cp -Lr "$folder/." "$dest" || return 1 - fi - - file=$(find "$dest" -maxdepth 1 -type f -iname install.bat -print -quit) || return 1 - - if [ -n "$COMMAND" ]; then - - [ -z "$file" ] && file="$dest/install.bat" - - if [ -s "$file" ]; then - printf '\n' >> "$file" || return 1 - fi - - printf '%s\n' "$COMMAND" >> "$file" || return 1 - - fi - - if [ -f "$file" ]; then - if ! unix2dos -q "$file"; then - error "Failed to convert $file to DOS format!" - return 1 - fi - fi - - return 0 -} - -prepareInstall() { - - local pid="" - local file="" - local dir="$2" - local desc="$3" - local driver="$4" - local drivers="/tmp/drivers" - - if [ -n "$DOMAIN" ]; then - error "The DOMAIN variable is not supported for $desc!" - return 1 - fi - - ETFS="[BOOT]/Boot-NoEmul.img" - - if [ ! -f "$dir/$ETFS" ] || [ ! -s "$dir/$ETFS" ]; then - error "Failed to locate file \"$ETFS\" in $desc ISO image!" && return 1 - fi - - local arch target - [ -d "$dir/AMD64" ] && arch="amd64" || arch="x86" - [[ "${arch,,}" == "x86" ]] && target="$dir/I386" || target="$dir/AMD64" - - if [ ! -d "$target" ]; then - error "Failed to locate directory \"$target\" in $desc ISO image!" && return 1 - fi - - if [[ "${driver,,}" == "xp" || "${driver,,}" == "2k3" ]]; then - - local msg="Adding drivers to image..." - info "$msg" && html "$msg" - - rm -rf "$drivers" || return 1 - mkdir -p "$drivers" || return 1 - - if ! bsdtar -xf /var/drivers.txz -C "$drivers"; then - error "Failed to extract drivers!" && return 1 - fi - - if [ ! -f "$drivers/viostor/$driver/$arch/viostor.sys" ]; then - error "Failed to locate required storage drivers!" && return 1 - fi - - cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$target" || return 1 - - mkdir -p "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1 - cp -L "$drivers/viostor/$driver/$arch/viostor.cat" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1 - cp -L "$drivers/viostor/$driver/$arch/viostor.inf" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1 - cp -L "$drivers/viostor/$driver/$arch/viostor.sys" "$dir/\$OEM\$/\$1/Drivers/viostor" || return 1 - - if [ ! -f "$drivers/NetKVM/$driver/$arch/netkvm.sys" ]; then - error "Failed to locate required network drivers!" && return 1 - fi - - mkdir -p "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1 - cp -L "$drivers/NetKVM/$driver/$arch/netkvm.cat" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1 - cp -L "$drivers/NetKVM/$driver/$arch/netkvm.inf" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1 - cp -L "$drivers/NetKVM/$driver/$arch/netkvm.sys" "$dir/\$OEM\$/\$1/Drivers/NetKVM" || return 1 - - file=$(find "$target" -maxdepth 1 -type f -iname TXTSETUP.SIF -print -quit) || return 1 - - if [ -z "$file" ]; then - error "The file TXTSETUP.SIF could not be found!" && return 1 - fi - - sed -i '/^\[SCSI.Load\]/s/$/\nviostor=viostor.sys,4/' "$file" || return 1 - sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\nviostor.sys=1,,,,,,4_,4,1,,,1,4/' "$file" || return 1 - sed -i '/^\[SCSI\]/s/$/\nviostor=\"Red Hat VirtIO SCSI Disk Device\"/' "$file" || return 1 - sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00000000=\"viostor\"/' "$file" || return 1 - sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00020000=\"viostor\"/' "$file" || return 1 - sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_1AF4\&DEV_1001\&SUBSYS_00021AF4=\"viostor\"/' "$file" || return 1 - - if [ ! -d "$drivers/sata/xp/$arch" ]; then - error "Failed to locate required SATA drivers!" && return 1 - fi - - mkdir -p "$dir/\$OEM\$/\$1/Drivers/sata" || return 1 - cp -Lr "$drivers/sata/xp/$arch/." "$dir/\$OEM\$/\$1/Drivers/sata" || return 1 - cp -Lr "$drivers/sata/xp/$arch/." "$target" || return 1 - - sed -i '/^\[SCSI.Load\]/s/$/\niaStor=iaStor.sys,4/' "$file" || return 1 - sed -i '/^\[FileFlags\]/s/$/\niaStor.sys = 16/' "$file" || return 1 - sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.cat = 1,,,,,,,1,0,0/' "$file" || return 1 - sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.inf = 1,,,,,,,1,0,0/' "$file" || return 1 - sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.sys = 1,,,,,,4_,4,1,,,1,4/' "$file" || return 1 - sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaStor.sys = 1,,,,,,,1,0,0/' "$file" || return 1 - sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaahci.cat = 1,,,,,,,1,0,0/' "$file" || return 1 - sed -i '/^\[SourceDisksFiles.'"$arch"'\]/s/$/\niaAHCI.inf = 1,,,,,,,1,0,0/' "$file" || return 1 - sed -i '/^\[SCSI\]/s/$/\niaStor=\"Intel\(R\) SATA RAID\/AHCI Controller\"/' "$file" || return 1 - sed -i '/^\[HardwareIdsDatabase\]/s/$/\nPCI\\VEN_8086\&DEV_2922\&CC_0106=\"iaStor\"/' "$file" || return 1 - - rm -rf "$drivers" || return 1 - - fi - - local key setup - setup=$(find "$target" -maxdepth 1 -type f -iname setupp.ini -print -quit) || return 1 - - if [ -n "$setup" ] && [ -z "$KEY" ]; then - - pid=$(<"$setup") || return 1 - pid="${pid%$'\r'}" - - if [[ "$driver" == "2k" ]]; then - - echo "${pid:0:$((${#pid})) - 3}270" > "$setup" || return 1 - - else - - if [[ "$pid" == *"270" ]]; then - - warn "this version of $desc requires a volume license key (VLK), it will ask for one during installation." - - else - - file=$(find "$target" -maxdepth 1 -type f -iname PID.INF -print -quit) || return 1 - - if [ -n "$file" ]; then - - if [[ "$driver" == "2k3" ]]; then - - key=$(grep -i -A 2 "StagingKey" "$file" | tail -n 2 | head -n 1) || key="" - - else - - key="${pid:$((${#pid})) - 8:5}" - if [[ "${pid^^}" == *"OEM" ]]; then - key=$(grep -i -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key="" - else - key=$(grep -i -m 1 -A 2 "$key" "$file" | tail -n 2 | head -n 1) || key="" - fi - key="${key#*= }" - - fi - - key="${key%$'\r'}" - [[ "${#key}" == "29" ]] && KEY="$key" - - fi - - if [ -z "$KEY" ]; then - - # These are NOT pirated keys, they come from official MS documentation. - - case "${driver,,}" in - "xp" ) - - if [[ "${arch,,}" == "x86" ]]; then - # Windows XP Professional x86 generic trial key (no activation) - KEY="DR8GV-C8V6J-BYXHG-7PYJR-DB66Y" - else - # Windows XP Professional x64 generic trial key (no activation) - KEY="B2RBK-7KPT9-4JP6X-QQFWM-PJD6G" - fi ;; - - "2k3" ) - - if [[ "${arch,,}" == "x86" ]]; then - # Windows Server 2003 Standard x86 generic trial key (no activation) - KEY="QKDCQ-TP2JM-G4MDG-VR6F2-P9C48" - else - # Windows Server 2003 Standard x64 generic trial key (no activation) - KEY="P4WJG-WK3W7-3HM8W-RWHCK-8JTRY" - fi ;; - - esac - - echo "${pid:0:$((${#pid})) - 3}000" > "$setup" || return 1 - - fi - - fi - fi - - fi - - validateProductKey "$KEY" || return 1 - - local product="" - [ -n "$KEY" ] && product="ProductID=$KEY" - - mkdir -p "$dir/\$OEM\$" || return 1 - - if ! addFolder "$dir"; then - error "Failed to add OEM folder to image!" && return 1 - fi - - local oem="" - local install="$dir/\$OEM\$/\$1/OEM/install.bat" - [ -f "$install" ] && oem="\"Script\"=\"cmd /C start \\\"Install\\\" \\\"cmd /C C:\\\\OEM\\\\install.bat\\\"\"" - - [ -z "$WIDTH" ] && WIDTH="1280" - [ -z "$HEIGHT" ] && HEIGHT="720" - - validateResolution "WIDTH" "$WIDTH" 320 || return 1 - validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 - validateMembership || return 1 - validateComputerName "$HOST" || return 1 - validateLegacyText "APP" "$APP" "$desc" || return 1 - validateLegacyText "ENGINE" "$ENGINE" "$desc" || return 1 - - XHEX=$(printf '%08x\n' "$((10#$WIDTH))") || return 1 - YHEX=$(printf '%08x\n' "$((10#$HEIGHT))") || return 1 - - local username="${USERNAME:-Docker}" - local password="${PASSWORD:-admin}" - local workgroup="${WORKGROUP:-WORKGROUP}" - - local sifHost sifUsername sifPassword sifOrganization sifWorkgroup - local regUsername regPassword - - validateLegacyUsername "$username" "$desc" || return 1 - validatePassword "$password" "$desc" || return 1 - - sifHost=$(escapeSIFValue "${HOST:-*}") || return 1 - sifUsername=$(escapeSIFValue "$username") || return 1 - sifPassword=$(escapeSIFValue "$password") || return 1 - sifOrganization=$(escapeSIFValue "$APP for $ENGINE") || return 1 - sifWorkgroup=$(escapeSIFValue "$workgroup") || return 1 - regUsername=$(escapeRegistryValue "$username") || return 1 - regPassword=$(escapeRegistryValue "$password") || return 1 - - find "$target" -maxdepth 1 -type f -iname winnt.sif -delete || return 1 - - { echo "[Data]" - echo " AutoPartition=1" - echo " MsDosInitiated=\"0\"" - echo " UnattendedInstall=\"Yes\"" - echo " AutomaticUpdates=\"Yes\"" - echo "" - echo "[Unattended]" - echo " UnattendSwitch=Yes" - echo " UnattendMode=FullUnattended" - echo " FileSystem=NTFS" - echo " OemSkipEula=Yes" - echo " OemPreinstall=Yes" - echo " Repartition=Yes" - echo " WaitForReboot=\"No\"" - echo " DriverSigningPolicy=\"Ignore\"" - echo " NonDriverSigningPolicy=\"Ignore\"" - printf '%s\n' " OemPnPDriversPath=\"Drivers\viostor;Drivers\NetKVM;Drivers\sata\"" - echo " NoWaitAfterTextMode=1" - echo " NoWaitAfterGUIMode=1" - echo " FileSystem=ConvertNTFS" - echo " ExtendOemPartition=0" - echo " Hibernation=\"No\"" - echo "" - echo "[GuiUnattended]" - echo " OEMSkipRegional=1" - echo " OemSkipWelcome=1" - echo " AdminPassword=\"$sifPassword\"" - echo " TimeZone=0" - if disabled "$AUTOLOGIN"; then - echo " AutoLogon=No" - else - echo " AutoLogon=Yes" - echo " AutoLogonCount=65432" - fi - echo "" - echo "[UserData]" - echo " FullName=\"$sifUsername\"" - echo " ComputerName=\"$sifHost\"" - echo " OrgName=\"$sifOrganization\"" - echo " $product" - echo "" - echo "[Identification]" - echo " JoinWorkgroup = \"$sifWorkgroup\"" - echo "" - echo "[Display]" - echo " BitsPerPel=32" - echo " XResolution=$WIDTH" - echo " YResolution=$HEIGHT" - echo "" - echo "[Networking]" - echo " InstallDefaultComponents=Yes" - echo "" - echo "[Branding]" - echo " BrandIEUsingUnattended=Yes" - echo "" - echo "[URL]" - echo " Home_Page = http://www.google.com" - echo " Search_Page = http://www.google.com" - echo "" - echo "[TerminalServices]" - echo " AllowConnections=1" - echo "" - } | unix2dos > "$target/WINNT.SIF" || return 1 - - if [[ "$driver" == "2k3" ]]; then - { echo "[Components]" - echo " TerminalServer=On" - echo "" - echo "[LicenseFilePrintData]" - echo " AutoMode=PerServer" - echo " AutoUsers=5" - echo "" - } | unix2dos >> "$target/WINNT.SIF" || return 1 - fi - - { echo "Windows Registry Editor Version 5.00" - echo "" - echo "[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Security]" - echo "\"FirstRunDisabled\"=dword:00000001" - echo "\"UpdatesDisableNotify\"=dword:00000001" - echo "\"FirewallDisableNotify\"=dword:00000001" - echo "\"AntiVirusDisableNotify\"=dword:00000001" - echo "" - echo "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\wscsvc]" - echo "\"Start\"=dword:00000004" - echo "" - echo "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\GloballyOpenPorts\List]" - echo "\"3389:TCP\"=\"3389:TCP:*:Enabled:@xpsp2res.dll,-22009\"" - echo "" - echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Applets\Tour]" - echo "\"RunCount\"=dword:00000000" - echo "" - echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]" - echo "\"HideFileExt\"=dword:00000000" - echo "" - echo "[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer]" - echo "\"NoWelcomeScreen\"=\"1\"" - echo "" - echo "[HKEY_CURRENT_USER\Software\Microsoft\Internet Connection Wizard]" - echo "\"Completed\"=\"1\"" - echo "\"Desktopchanged\"=\"1\"" - echo "" - echo "[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]" - if disabled "$AUTOLOGIN"; then - echo "\"AutoAdminLogon\"=\"0\"" - else - echo "\"AutoAdminLogon\"=\"1\"" - echo "\"DefaultUserName\"=\"$regUsername\"" - echo "\"DefaultPassword\"=\"$regPassword\"" - echo "\"DefaultDomainName\"=\"Dockur\"" - fi - echo "" - printf '%s\n' "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Video\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]" - echo "\"DefaultSettings.BitsPerPel\"=dword:00000020" - echo "\"DefaultSettings.XResolution\"=dword:$XHEX" - echo "\"DefaultSettings.YResolution\"=dword:$YHEX" - echo "" - printf '%s\n' "[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Hardware Profiles\Current\System\CurrentControlSet\Control\VIDEO\{23A77BF7-ED96-40EC-AF06-9B1F4867732A}\0000]" - echo "\"DefaultSettings.BitsPerPel\"=dword:00000020" - echo "\"DefaultSettings.XResolution\"=dword:$XHEX" - echo "\"DefaultSettings.YResolution\"=dword:$YHEX" - echo "" - echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce]" - printf '%s\n' "\"ScreenSaver\"=\"reg add \\\"HKCU\\\\Control Panel\\\\Desktop\\\" /f /v \\\"SCRNSAVE.EXE\\\" /t REG_SZ /d \\\"off\\\"\"" - printf '%s\n' "\"ScreenSaverOff\"=\"reg add \\\"HKCU\\\\Control Panel\\\\Desktop\\\" /f /v \\\"ScreenSaveActive\\\" /t REG_SZ /d \\\"0\\\"\"" - printf '%s\n' "\"SharedDrive\"=\"cmd /C net use Z: \\\\\\\\host.lan\\\\Data /persistent:yes\"" - echo "$oem" - echo "" - } | unix2dos > "$dir/\$OEM\$/install.reg" || return 1 - - if [[ "$driver" == "2k" ]]; then - { echo "[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Runonce]" - echo "\"^SetupICWDesktop\"=-" - echo "" - } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 - fi - - if [[ "$driver" == "2k3" ]]; then - { echo "[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\srvWiz]" - echo "@=dword:00000000" - echo "" - echo "[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ServerOOBE\SecurityOOBE]" - echo "\"DontLaunchSecurityOOBE\"=dword:00000000" - echo "" - } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 - fi - - { echo "Set WshShell = WScript.CreateObject(\"WScript.Shell\")" - echo "Set WshNetwork = WScript.CreateObject(\"WScript.Network\")" - echo "Set Domain = GetObject(\"WinNT://\" & WshNetwork.ComputerName)" - echo "" - echo "Function DecodeSID(binSID)" - echo " ReDim o(LenB(binSID))" - echo "" - echo " For i = 1 To LenB(binSID)" - echo " o(i-1) = AscB(MidB(binSID, i, 1))" - echo " Next" - echo "" - echo " sid = \"S-\" & CStr(o(0)) & \"-\" & OctetArrayToString _" - echo " (Array(o(2), o(3), o(4), o(5), o(6), o(7)))" - echo " For i = 8 To (4 * o(1) + 4) Step 4" - echo " sid = sid & \"-\" & OctetArrayToString _" - echo " (Array(o(i+3), o(i+2), o(i+1), o(i)))" - echo " Next" - echo "" - echo " DecodeSID = sid" - echo "End Function" - echo "" - echo "Function OctetArrayToString(arr)" - echo " v = 0" - echo " For i = 0 To UBound(arr)" - echo " v = v * 256 + arr(i)" - echo " Next" - echo "" - echo " OctetArrayToString = CStr(v)" - echo "End Function" - echo "" - echo "For Each DomainItem in Domain" - echo " If DomainItem.Class = \"User\" Then" - echo " sid = DecodeSID(DomainItem.Get(\"objectSID\"))" - echo " If Left(sid, 9) = \"S-1-5-21-\" And Right(sid, 4) = \"-500\" Then" - echo " LocalAdminADsPath = DomainItem.ADsPath" - echo " Exit For" - echo " End If" - echo " End If" - echo "Next" - echo "" - echo "Call Domain.MoveHere(LocalAdminADsPath, \"$username\")" - echo "" - echo "Set oLink = WshShell.CreateShortcut(WshShell.SpecialFolders(\"Desktop\") & \"\\Shared.lnk\")" - echo "With oLink" - printf '%s\n' " .TargetPath = \"\\\\host.lan\\Data\"" - echo " .Save" - echo "End With" - echo "Set oLink = Nothing" - echo "" - } | unix2dos > "$dir/\$OEM\$/install.vbs" || return 1 - - { echo "[COMMANDS]" - echo "\"REGEDIT /s install.reg\"" - echo "\"Wscript install.vbs\"" - echo "" - } | unix2dos > "$dir/\$OEM\$/cmdlines.txt" || return 1 - - return 0 -} - -prepareLegacy() { - - local iso="$1" - local dir="$2" - local desc="$3" - - local tmp="$TMP/boot-images" - local image="$tmp/eltorito_img1_bios.img" - - ETFS="boot.img" - - [ -s "$dir/$ETFS" ] && return 0 - rm -f "$dir/$ETFS" || return 1 - rm -rf "$tmp" || return 1 - - if ! LC_ALL=C xorriso \ - -no_rc \ - -osirrox on \ - -indev "$iso" \ - -extract_boot_images "$tmp" >/dev/null 2>&1; then - rm -rf "$tmp" || true - error "Failed to extract boot image from $desc ISO!" - return 1 - fi - - if [ ! -s "$image" ]; then - rm -rf "$tmp" || true - error "Failed to locate BIOS boot image in $desc ISO!" - return 1 - fi - - if ! mv -f "$image" "$dir/$ETFS"; then - rm -rf "$tmp" || true - error "Failed to save boot image from $desc ISO!" - return 1 - fi - - rm -rf "$tmp" || return 1 - return 0 -} - -detectLegacy() { - - local dir="$1" - local find - - find=$(find "$dir" -maxdepth 1 -type d -iname WIN95 -print -quit) - [ -n "$find" ] && DETECTED="win95" && return 0 - - find=$(find "$dir" -maxdepth 1 -type d -iname WIN98 -print -quit) - [ -n "$find" ] && DETECTED="win98" && return 0 - - find=$(find "$dir" -maxdepth 1 -type d -iname WIN9X -print -quit) - [ -n "$find" ] && DETECTED="win9x" && return 0 - - find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_W.40 -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_S.40 -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_TS.40 -print -quit) - [ -n "$find" ] && DETECTED="winnt4" && return 0 - - find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_NT.5 -print -quit) - - if [ -n "$find" ]; then - - find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IA.5 -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_ID.5 -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IP.5 -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname CDROM_IS.5 -print -quit) - [ -n "$find" ] && DETECTED="win2k" && return 0 - - fi - - find=$(find "$dir" -maxdepth 1 -iname WIN51 -print -quit) - - if [ -n "$find" ]; then - - find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AP -print -quit) - [ -n "$find" ] && DETECTED="winxpx64" && return 0 - - find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IC -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IP -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname setupxp.htm -print -quit) - [ -n "$find" ] && DETECTED="winxpx86" && return 0 - - find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IS -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IA -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IB -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51ID -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51IL -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AA -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AD -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51AS -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51MA -print -quit) - [ -z "$find" ] && find=$(find "$dir" -maxdepth 1 -type f -iname WIN51MD -print -quit) - [ -n "$find" ] && DETECTED="win2003r2" && return 0 - - fi - - return 1 -} - -skipVersion() { - - local id="$1" - - case "${id,,}" in - "win9"* | "winxp"* | "win2k"* | "win2003"* ) - return 0 ;; - esac - - return 1 -} - isCompatible() { return 0 } -setMachine() { - - local id="$1" - local iso="$2" - local dir="$3" - local desc="$4" - - case "${id,,}" in - "win9"* ) - ETFS="[BOOT]/Boot-1.44M.img" ;; - "win2k"* ) - if ! prepareInstall "$iso" "$dir" "$desc" "2k"; then - error "Failed to prepare $desc ISO!" && return 1 - fi ;; - "winxp"* ) - if ! prepareInstall "$iso" "$dir" "$desc" "xp"; then - error "Failed to prepare $desc ISO!" && return 1 - fi ;; - "win2003"* ) - if ! prepareInstall "$iso" "$dir" "$desc" "2k3"; then - error "Failed to prepare $desc ISO!" && return 1 - fi ;; - esac - - case "${id,,}" in - "win9"* ) - USB="no" - VGA="cirrus" - DISK_TYPE="auto" - MACHINE="pc-i440fx-2.4" - BOOT_MODE="windows_legacy" - [ -z "${ADAPTER:-}" ] && ADAPTER="pcnet" ;; - "win2k"* ) - VGA="cirrus" - MACHINE="pc" - USB="pci-ohci" - DISK_TYPE="auto" - BOOT_MODE="windows_legacy" - [ -z "${ADAPTER:-}" ] && ADAPTER="rtl8139" ;; - "winxp"* | "win2003"* ) - DISK_TYPE="blk" - BOOT_MODE="windows_legacy" - [ -z "${SOUND:-}" ] && SOUND="usb-audio" ;; - "winvista"* | "win7"* | "win2008"* ) - BOOT_MODE="windows_legacy" ;; - esac - - case "${id,,}" in - "winxp"* | "win2003"* | "winvistax86"* | "win7x86"* | "win2008r2x86"* ) - # Prevent bluescreen if 64 bit PCI hole size is >2G. - ARGS="-global q35-pcihost.x-pci-hole64-fix=false" ;; - esac - - return 0 -} - return 0 diff --git a/src/entry.sh b/src/entry.sh index a0d57f87..988c512b 100644 --- a/src/entry.sh +++ b/src/entry.sh @@ -14,6 +14,7 @@ cd /run . server.sh # Start webserver . define.sh # Define versions . mido.sh # Download Windows +. answer.sh # Setup answer files . install.sh # Run installation . disk.sh # Initialize disks . display.sh # Initialize graphics diff --git a/src/install.sh b/src/install.sh index cd955298..d780508e 100644 --- a/src/install.sh +++ b/src/install.sh @@ -61,6 +61,57 @@ backup () { return 0 } +findFile() { + + local dir file base + local fname="$1" + local boot="$STORAGE/windows.boot" + + dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit) + [ ! -d "$dir" ] && dir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$fname" -print -quit) + + if [ -d "$dir" ]; then + if ! hasData || [ ! -f "$boot" ]; then + error "The bind $dir maps to a file that does not exist!" && return 1 + fi + fi + + file=$(find / -maxdepth 1 -type f -iname "$fname" -print -quit) + [ ! -s "$file" ] && file=$(find "$STORAGE" -maxdepth 1 -type f -iname "$fname" -print -quit) + + if [ ! -s "$file" ] && [[ "${VERSION,,}" != "http"* ]]; then + base=$(basename "$VERSION") + file="$STORAGE/$base" + fi + + if [ ! -f "$file" ] || [ ! -s "$file" ]; then + return 0 + fi + + local size + size="$(stat -c%s "$file")" + [ -z "$size" ] || [[ "$size" == "0" ]] && return 0 + + ISO="$file" + CUSTOM="$file" + BOOT="$STORAGE/windows.$size.iso" + + return 0 +} + +detectCustom() { + + CUSTOM="" + + ! findFile "custom.iso" && return 1 + [ -n "$CUSTOM" ] && return 0 + + ! findFile "boot.iso" && return 1 + [ -n "$CUSTOM" ] && return 0 + + return 0 +} + skipInstall() { local iso="$1" @@ -193,24 +244,6 @@ startInstall() { return 0 } -checkFreeSpace() { - - local dir="$1" - local size="$2" - local size_gb space space_gb - - size_gb=$(formatBytes "$size") - space=$(df --output=avail -B 1 "$dir" | tail -n 1) - space_gb=$(formatBytes "$space") - - if (( size > space )); then - error "Not enough free space in $STORAGE, have $space_gb available but need at least $size_gb." - return 1 - fi - - return 0 -} - finishInstall() { local iso="$1" @@ -330,54 +363,21 @@ abortInstall() { return 1 } -findFile() { +checkFreeSpace() { - local dir file base - local fname="$1" - local boot="$STORAGE/windows.boot" + local dir="$1" + local size="$2" + local size_gb space space_gb - dir=$(find / -maxdepth 1 -type d -iname "$fname" -print -quit) - [ ! -d "$dir" ] && dir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$fname" -print -quit) + size_gb=$(formatBytes "$size") + space=$(df --output=avail -B 1 "$dir" | tail -n 1) + space_gb=$(formatBytes "$space") - if [ -d "$dir" ]; then - if ! hasData || [ ! -f "$boot" ]; then - error "The bind $dir maps to a file that does not exist!" && return 1 - fi + if (( size > space )); then + error "Not enough free space in $STORAGE, have $space_gb available but need at least $size_gb." + return 1 fi - file=$(find / -maxdepth 1 -type f -iname "$fname" -print -quit) - [ ! -s "$file" ] && file=$(find "$STORAGE" -maxdepth 1 -type f -iname "$fname" -print -quit) - - if [ ! -s "$file" ] && [[ "${VERSION,,}" != "http"* ]]; then - base=$(basename "$VERSION") - file="$STORAGE/$base" - fi - - if [ ! -f "$file" ] || [ ! -s "$file" ]; then - return 0 - fi - - local size - size="$(stat -c%s "$file")" - [ -z "$size" ] || [[ "$size" == "0" ]] && return 0 - - ISO="$file" - CUSTOM="$file" - BOOT="$STORAGE/windows.$size.iso" - - return 0 -} - -detectCustom() { - - CUSTOM="" - - ! findFile "custom.iso" && return 1 - [ -n "$CUSTOM" ] && return 0 - - ! findFile "boot.iso" && return 1 - [ -n "$CUSTOM" ] && return 0 - return 0 } @@ -767,28 +767,16 @@ detectLanguage() { return 0 } -setXML() { +skipVersion() { - local file="/custom.xml" + local id="$1" - CUSTOM_XML="" - - if [ -d "$file" ]; then - error "The bind $file maps to a file that does not exist!" && exit 67 - fi - - [ ! -f "$file" ] || [ ! -s "$file" ] && file="$STORAGE/custom.xml" - [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/custom.xml" - [ ! -f "$file" ] || [ ! -s "$file" ] && file="$1" - [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/$DETECTED.xml" - [ ! -f "$file" ] || [ ! -s "$file" ] && return 1 - - case "$file" in - "/custom.xml" | "$STORAGE/custom.xml" ) CUSTOM_XML="Y" ;; + case "${id,,}" in + "win9"* | "winxp"* | "win2k"* | "win2003"* ) + return 0 ;; esac - XML="$file" - return 0 + return 1 } detectImage() { @@ -885,6 +873,62 @@ detectImage() { return 0 } +setMachine() { + + local id="$1" + local iso="$2" + local dir="$3" + local desc="$4" + + case "${id,,}" in + "win9"* ) + ETFS="[BOOT]/Boot-1.44M.img" ;; + "win2k"* ) + if ! legacyInstall "$iso" "$dir" "$desc" "2k"; then + error "Failed to prepare $desc ISO!" && return 1 + fi ;; + "winxp"* ) + if ! legacyInstall "$iso" "$dir" "$desc" "xp"; then + error "Failed to prepare $desc ISO!" && return 1 + fi ;; + "win2003"* ) + if ! legacyInstall "$iso" "$dir" "$desc" "2k3"; then + error "Failed to prepare $desc ISO!" && return 1 + fi ;; + esac + + case "${id,,}" in + "win9"* ) + USB="no" + VGA="cirrus" + DISK_TYPE="auto" + MACHINE="pc-i440fx-2.4" + BOOT_MODE="windows_legacy" + [ -z "${ADAPTER:-}" ] && ADAPTER="pcnet" ;; + "win2k"* ) + VGA="cirrus" + MACHINE="pc" + USB="pci-ohci" + DISK_TYPE="auto" + BOOT_MODE="windows_legacy" + [ -z "${ADAPTER:-}" ] && ADAPTER="rtl8139" ;; + "winxp"* | "win2003"* ) + DISK_TYPE="blk" + BOOT_MODE="windows_legacy" + [ -z "${SOUND:-}" ] && SOUND="usb-audio" ;; + "winvista"* | "win7"* | "win2008"* ) + BOOT_MODE="windows_legacy" ;; + esac + + case "${id,,}" in + "winxp"* | "win2003"* | "winvistax86"* | "win7x86"* | "win2008r2x86"* ) + # Prevent bluescreen if 64 bit PCI hole size is >2G. + ARGS="-global q35-pcihost.x-pci-hole64-fix=false" ;; + esac + + return 0 +} + prepareImage() { local iso="$1" @@ -907,465 +951,53 @@ prepareImage() { return 1 fi - prepareLegacy "$iso" "$dir" "$desc" && return 0 + legacyPrepare "$iso" "$dir" "$desc" && return 0 error "Failed to extract boot image from ISO image!" return 1 } -escapeXMLSed() { +addFolder() { - local s + local src="$1" + local folder="/oem" file="" + local dest="$src/\$OEM\$/\$1/OEM" - s=$(escapeXML "$1") || return 1 - s=${s//\\/\\\\} - s=${s//&/\\&} - s=${s//|/\\|} + [ ! -d "$folder" ] && folder="/OEM" + [ ! -d "$folder" ] && folder="$STORAGE/oem" + [ ! -d "$folder" ] && folder="$STORAGE/OEM" + [ ! -d "$folder" ] && folder="" - printf '%s' "$s" - return 0 -} + [ -z "$folder" ] && [ -z "$COMMAND" ] && return 0 -validateUsername() { + local msg="Adding OEM files to image..." + info "$msg" && html "$msg" - local value="$1" - local type="$2" - local maximum + mkdir -p "$dest" || return 1 - case "$type" in - "local" ) - maximum=20 - [ -z "$value" ] && return 0 - ;; - "domain" ) - maximum=256 + if [ -n "$folder" ]; then + cp -Lr "$folder/." "$dest" || return 1 + fi - if [ -z "$value" ]; then - error "The USERNAME variable does not contain a valid domain account name!" - return 1 - fi ;; - * ) - return 1 ;; - esac + file=$(find "$dest" -maxdepth 1 -type f -iname install.bat -print -quit) || return 1 - if [ "${#value}" -gt "$maximum" ]; then - if [[ "$type" == "domain" ]]; then - error "The USERNAME variable cannot contain more than $maximum characters for a domain account!" - else - error "The USERNAME variable cannot contain more than $maximum characters!" + if [ -n "$COMMAND" ]; then + + [ -z "$file" ] && file="$dest/install.bat" + + if [ -s "$file" ]; then + printf '\n' >> "$file" || return 1 fi - return 1 + + printf '%s\n' "$COMMAND" >> "$file" || return 1 + fi - if [[ "$value" =~ [[:cntrl:]] ]]; then - error "The USERNAME variable cannot contain control characters!" - return 1 - fi - - case "$value" in - *'"'* | *'/'* | *\\* | *'['* | *']'* | *':'* | *';'* | *'|'* | *'='* | *','* | *'+'* | *'*'* | *'?'* | *'<'* | *'>'* | *'%'* | *'@'* ) - if [[ "$type" == "domain" ]]; then - error "The domain account name contains characters that are not supported by Windows unattended setup!" - else - error "The USERNAME variable contains characters that are not supported by Windows local accounts!" - fi - return 1 ;; - esac - - if [[ "$value" == *"." ]]; then - error "The USERNAME variable cannot end with a period!" - return 1 - fi - - if [[ "$value" =~ ^[.[:space:]]+$ ]]; then - error "The USERNAME variable cannot consist only of spaces or periods!" - return 1 - fi - - case "${value^^}" in - "NONE" ) - error "The USERNAME value \"NONE\" is reserved by Windows!" - return 1 ;; - "ADMINISTRATOR" | "GUEST" | "DEFAULTACCOUNT" | "WDAGUTILITYACCOUNT" | "WSIACCOUNT" ) - if [[ "$type" == "local" ]]; then - error "The USERNAME value \"$value\" is reserved for a built-in Windows account!" - return 1 - fi ;; - esac - - return 0 -} - -updateDomain() { - - local asset="$1" - local domain account auth pass pw - local cred_domain ou arch tmp result - - domain=$(escapeXML "$2") || return 1 - account=$(escapeXML "$3") || return 1 - auth=$(escapeXML "$4") || return 1 - pass=$(escapeXML "$5") || return 1 - pw="$6" - ou=$(escapeXML "$7") || return 1 - - arch=$(sed -n -E \ - '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \ - "$asset") || return 1 - - [ -z "$arch" ] && return 1 - - cred_domain="$domain" - - case "$4" in - *@* ) cred_domain="" ;; - esac - - grep -Eq 'Microsoft-Windows-UnattendedJoin|<DomainAccounts([[:space:]/>])' "$asset" && return 1 - - tmp=$(mktemp -d) || return 1 - result="$tmp/answer.xml" - - if ! DOMAIN_XML="$domain" ACCOUNT_XML="$account" \ - AUTH_XML="$auth" PASS_XML="$pass" \ - CRED_DOMAIN="$cred_domain" PW="$pw" OU_XML="$ou" \ - ARCH_XML="$arch" \ - awk ' - /<settings[^>]*pass="specialize"[^>]*>/ { section = "specialize" } - /<settings[^>]*pass="oobeSystem"[^>]*>/ { section = "oobeSystem" } - section == "oobeSystem" && /<UserAccounts([[:space:]>])/ { in_accounts = 1 } - section == "oobeSystem" && /<AutoLogon([[:space:]>])/ { in_autologon = 1 } - - section == "oobeSystem" && in_accounts && !accounts_added && - /<AdministratorPassword([[:space:]>])/ { - print " <DomainAccounts>\n" \ - " <DomainAccountList wcm:action=\"add\">\n" \ - " <DomainAccount wcm:action=\"add\">\n" \ - " <Name>" ENVIRON["ACCOUNT_XML"] "</Name>\n" \ - " <Group>Administrators</Group>\n" \ - " </DomainAccount>\n" \ - " <Domain>" ENVIRON["DOMAIN_XML"] "</Domain>\n" \ - " </DomainAccountList>\n" \ - " </DomainAccounts>" - accounts_added = 1 - } - - section == "oobeSystem" && in_autologon && - /^[[:space:]]*<Username>.*<\/Username>[[:space:]]*$/ { - print " <Username>" ENVIRON["ACCOUNT_XML"] "</Username>\n" \ - " <Domain>" ENVIRON["DOMAIN_XML"] "</Domain>" - autologon_added = 1 - next - } - - section == "oobeSystem" && in_autologon && - /^[[:space:]]*<Domain([[:space:]/>])/ { next } - - section == "oobeSystem" && in_autologon && - /^[[:space:]]*<Value>.*<\/Value>[[:space:]]*$/ { - print " <Value>" ENVIRON["PW"] "</Value>" - password_added = 1 - next - } - - section == "specialize" && !join_added && - /^[[:space:]]*<\/settings>[[:space:]]*$/ { - print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \ - " <Identification>\n" \ - " <Credentials>" - - if (ENVIRON["CRED_DOMAIN"] != "") { - print " <Domain>" ENVIRON["CRED_DOMAIN"] "</Domain>" - } - - print " <Username>" ENVIRON["AUTH_XML"] "</Username>\n" \ - " <Password>" ENVIRON["PASS_XML"] "</Password>\n" \ - " </Credentials>\n" \ - " <JoinDomain>" ENVIRON["DOMAIN_XML"] "</JoinDomain>" - - if (ENVIRON["OU_XML"] != "") { - print " <MachineObjectOU>" ENVIRON["OU_XML"] "</MachineObjectOU>" - } - - print " </Identification>\n" \ - " </component>" - - join_added = 1 - } - - { print } - - section == "oobeSystem" && /<\/AutoLogon>/ { in_autologon = 0 } - section == "oobeSystem" && /<\/UserAccounts>/ { in_accounts = 0 } - /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } - - END { exit !(join_added && accounts_added && autologon_added && password_added) } - ' "$asset" > "$result" || - ! mv -f "$result" "$asset"; then - - rm -rf "$tmp" || true - return 1 - fi - - rm -rf "$tmp" || return 1 - return 0 -} - -validateDomainName() { - - local value="$1" - local name="${2:-DOMAIN}" - - if [ -z "$value" ]; then - error "The $name variable must contain a valid domain name!" - return 1 - fi - - if [[ "$value" == *"://"* ]]; then - error "The $name variable must contain a domain name, not a URL!" - return 1 - fi - - if [ "${#value}" -gt 255 ] || - [[ "$value" =~ [[:cntrl:]] ]] || - [[ "$value" =~ [[:space:]] ]] || - [[ ! "$value" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$ ]]; then - - error "The $name variable does not contain a valid domain name!" - return 1 - fi - - return 0 -} - -updateWorkgroup() { - - local asset="$1" - local workgroup arch tmp result - - workgroup=$(escapeXML "$2") || return 1 - arch=$(sed -n -E '0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' "$asset") || return 1 - [ -z "$arch" ] && return 1 - - grep -q 'Microsoft-Windows-UnattendedJoin' "$asset" && return 1 - - tmp=$(mktemp -d) || return 1 - result="$tmp/answer.xml" - - if ! WORKGROUP_XML="$workgroup" ARCH_XML="$arch" awk ' - /<settings[^>]*pass="specialize"[^>]*>/ { section = "specialize" } - - section == "specialize" && !workgroup_added && - /^[[:space:]]*<\/settings>[[:space:]]*$/ { - print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \ - " <Identification>\n" \ - " <JoinWorkgroup>" ENVIRON["WORKGROUP_XML"] "</JoinWorkgroup>\n" \ - " </Identification>\n" \ - " </component>" - workgroup_added = 1 - } - - { print } - - /^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } - END { exit !workgroup_added } - ' "$asset" > "$result" || - ! mv -f "$result" "$asset"; then - - rm -rf "$tmp" || true - return 1 - fi - - rm -rf "$tmp" || return 1 - return 0 -} - -updateXML() { - - local asset="$1" - local language="$2" - local app value culture region keyboard edition - local user user_xml auth_user admin pass pw - local domain qualifier host workgroup key - - [ -z "$WIDTH" ] && WIDTH="1280" - [ -z "$HEIGHT" ] && HEIGHT="720" - - validateResolution "WIDTH" "$WIDTH" 320 || return 1 - validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 - validateMembership || return 1 - validateComputerName "$HOST" || return 1 - validateProductKey "$KEY" || return 1 - validatePassword "$PASSWORD" || return 1 - - app=$(escapeXMLSed "$APP for $ENGINE") || return 1 - - sed -i "s|>Windows for Docker<|>$app<|g" "$asset" || return 1 - sed -i -E "s|<VerticalResolution>[^<]*</VerticalResolution>|<VerticalResolution>$HEIGHT</VerticalResolution>|g" "$asset" || return 1 - sed -i -E "s|<HorizontalResolution>[^<]*</HorizontalResolution>|<HorizontalResolution>$WIDTH</HorizontalResolution>|g" "$asset" || return 1 - - culture=$(getLanguage "$language" "culture") || return 1 - - if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then - value=$(escapeXMLSed "$culture") || return 1 - sed -i "s|<UILanguage>en-US</UILanguage>|<UILanguage>$value</UILanguage>|g" "$asset" || return 1 - fi - - region="$REGION" - [ -z "$region" ] && region="$culture" - - if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then - value=$(escapeXMLSed "$region") || return 1 - sed -i "s|<UserLocale>en-US</UserLocale>|<UserLocale>$value</UserLocale>|g" "$asset" || return 1 - sed -i "s|<SystemLocale>en-US</SystemLocale>|<SystemLocale>$value</SystemLocale>|g" "$asset" || return 1 - fi - - keyboard="$KEYBOARD" - [ -z "$keyboard" ] && keyboard="$culture" - - if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then - value=$(escapeXMLSed "$keyboard") || return 1 - sed -i "s|<InputLocale>en-US</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 - sed -i "s|<InputLocale>0409:00000409</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 - fi - - if [ -n "$HOST" ]; then - host=$(escapeXMLSed "$HOST") || return 1 - sed -i -E "s|<ComputerName>[^<]*</ComputerName>|<ComputerName>$host</ComputerName>|g" "$asset" || return 1 - fi - - domain="$DOMAIN" - workgroup="$WORKGROUP" - - if [ -n "$domain" ]; then - - if [ -z "$USERNAME" ]; then - error "The USERNAME variable must be specified when joining a domain!" + if [ -f "$file" ]; then + if ! unix2dos -q "$file"; then + error "Failed to convert $file to DOS format!" return 1 fi - - if [ -z "$PASSWORD" ]; then - error "The PASSWORD variable must be specified when joining a domain!" - return 1 - fi - - validateDomainName "$domain" || return 1 - - auth_user="$USERNAME" - qualifier="" - - if [[ "$auth_user" == *\\* ]]; then - error "The USERNAME variable must use either \"user\" or \"user@domain\" format!" - return 1 - fi - - case "$auth_user" in - *@* ) - user="${auth_user%%@*}" - qualifier="${auth_user#*@}" - - if [ -z "$user" ] || [ -z "$qualifier" ] || [[ "$qualifier" == *@* ]]; then - error "The USERNAME variable does not contain a valid domain account name!" - return 1 - fi - - validateDomainName "$qualifier" "USERNAME" || return 1 - - if [[ "${qualifier,,}" != "${domain,,}" ]]; then - error "The domain in the USERNAME variable must match the DOMAIN variable!" - return 1 - fi - ;; - * ) - user="$auth_user" - ;; - esac - - validateUsername "$user" "domain" || return 1 - - if [[ "${user,,}" == "docker" ]]; then - error "The USERNAME variable must be changed from its default value when joining a domain!" - return 1 - fi - - if [[ "$PASSWORD" == "admin" ]]; then - error "The PASSWORD variable must be changed from its default value when joining a domain!" - return 1 - fi - - else - - user="$USERNAME" - validateUsername "$user" "local" || return 1 - - if [ -n "$user" ]; then - user_xml=$(escapeXMLSed "$user") || return 1 - - sed -i "s|-name \"Docker\"|-name \"\$env:USERNAME\"|g" "$asset" || return 1 - sed -i 's|where name="Docker"|where name="%USERNAME%"|g' "$asset" || return 1 - sed -i "s|<Name>Docker</Name>|<Name>$user_xml</Name>|g" "$asset" || return 1 - sed -i "s|<FullName>Docker</FullName>|<FullName>$user_xml</FullName>|g" "$asset" || return 1 - sed -i "s|<Username>Docker</Username>|<Username>$user_xml</Username>|g" "$asset" || return 1 - fi - - fi - - if [ -n "$domain" ]; then - pass="admin" - else - [ -n "$PASSWORD" ] && pass="$PASSWORD" || pass="admin" - fi - - pw=$(printf '%s' "${pass}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1 - admin=$(printf '%s' "${pass}AdministratorPassword" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1 - - sed -i -z -E "s#(<Password>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$pw</Value>#g" "$asset" || return 1 - sed -i -z -E "s#(<AdministratorPassword>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$admin</Value>#g" "$asset" || return 1 - sed -i -E "s|<PlainText>[^<]*</PlainText>|<PlainText>false</PlainText>|g" "$asset" || return 1 - - if [ -n "$domain" ]; then - - pw=$(printf '%s' "${PASSWORD}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0) || return 1 - - if ! updateDomain "$asset" "$domain" "$user" \ - "$auth_user" "$PASSWORD" "$pw" "$DOMAIN_OU"; then - error "Failed to add domain configuration to answer file!" - return 1 - fi - - elif [ -n "$workgroup" ]; then - - if ! updateWorkgroup "$asset" "$workgroup"; then - error "Failed to add workgroup configuration to answer file!" - return 1 - fi - - fi - - if disabled "$AUTOLOGIN"; then - sed -i -E '/^[[:space:]]*<AutoLogon([[:space:]>])/,/^[[:space:]]*<\/AutoLogon>[[:space:]]*$/d' "$asset" || return 1 - fi - - if [ -n "$EDITION" ]; then - case "${EDITION,,}" in - "core" ) edition="STANDARDCORE" ;; - * ) edition="${EDITION^^}" ;; - esac - - edition=$(escapeXMLSed "$edition") || return 1 - sed -i "s|SERVERSTANDARD</Value>|SERVER$edition</Value>|g" "$asset" || return 1 - fi - - if [ -n "$KEY" ]; then - key=$(escapeXMLSed "$KEY") || return 1 - sed -i -E '/^[[:space:]]*<ProductKey>[[:space:]]*$/,/^[[:space:]]*<\/ProductKey>[[:space:]]*$/d' "$asset" || return 1 - sed -i -E "s|<ProductKey>[^<]*</ProductKey>|<ProductKey>$key</ProductKey>|g" "$asset" || return 1 - sed -i "s|</UserData>| <ProductKey>\n <Key>$key</Key>\n <WillShowUI>OnError</WillShowUI>\n </ProductKey>\n </UserData>|g" "$asset" || return 1 - fi - - if ! xmllint --nonet --noout "$asset"; then - error "The generated answer file is not valid XML!" - return 1 fi return 0