diff --git a/Dockerfile b/Dockerfile index df157cd..d2a8dd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,10 +15,7 @@ RUN apt-get update \ COPY ./src /run/ COPY ./assets /run/assets - ADD https://github.com/qemus/virtiso/raw/master/virtio-win.iso /run/drivers.iso -ADD https://raw.githubusercontent.com/ElliotKillick/Mido/main/Mido.sh /run/mido.sh - RUN chmod +x /run/*.sh EXPOSE 8006 3389 diff --git a/readme.md b/readme.md index af8926d..336d9c4 100644 --- a/readme.md +++ b/readme.md @@ -56,7 +56,7 @@ docker run -it --rm -p 8006:8006 --device=/dev/kvm --cap-add NET_ADMIN dockurr/w - Start the container and get some coffee. - - Connect to port 8006 of the container in your web browser. + - Connect to [port 8006](http://localhost:8006) of the container in your web browser. - Sit back and relax while the magic happens, the whole installation will be performed fully automatic. @@ -127,7 +127,7 @@ docker run -it --rm -p 8006:8006 --device=/dev/kvm --cap-add NET_ADMIN dockurr/w Then follow these steps: - - Start the container and connect to port 8006 of the container in your web browser. After the download is finished, you will see the Windows installation screen. + - Start the container and connect to [port 8006](http://localhost:8006) of the container in your web browser. After the download is finished, you will see the Windows installation screen. - Start the installation by clicking ```Install now```. On the next screen, press 'OK' when prompted to ```Load driver``` and select the ```VirtIO SCSI``` driver from the list that matches your Windows version. So for Windows 11, select ```D:\amd64\w11\vioscsi.inf``` and click 'Next'. @@ -143,16 +143,16 @@ docker run -it --rm -p 8006:8006 --device=/dev/kvm --cap-add NET_ADMIN dockurr/w - Now your Windows installation is ready for use. Enjoy it, and don't forget to star this repo! - * ### How do I install an unsupported version? + * ### How do I install a custom image? - You can specify an URL in the `VERSION` environment variable, in order to download a custom ISO image: + In order to download a custom ISO image, specify an URL in the `VERSION` environment variable: ```yaml environment: VERSION: "https://example.com/win.iso" ``` - During the installation you may need to add some drivers as described in [manual installation](https://github.com/dockur/windows/tree/master?tab=readme-ov-file#how-do-i-perform-a-manual-installation) above. + Make sure your `/storage` folder is empty before starting the container. Alternatively, you can also place a file called `custom.iso` in that folder to skip the download. * ### How do I pass-through a disk? diff --git a/src/install.sh b/src/install.sh index 2fe7e90..1e57ecd 100644 --- a/src/install.sh +++ b/src/install.sh @@ -2,7 +2,6 @@ set -Eeuo pipefail : "${MANUAL:=""}" -: "${EXTERNAL:=""}" : "${VERSION:="win11x64"}" [[ "${VERSION,,}" == "11" ]] && VERSION="win11x64" @@ -48,78 +47,104 @@ fi MSG="Windows is being started, please wait..." -BASE="custom.iso" -if [ ! -f "$STORAGE/$BASE" ]; then +if [[ "$EXTERNAL" != [Yy1]* ]]; then - if [[ "$EXTERNAL" != [Yy1]* ]]; then - - BASE="$VERSION.iso" - if [ ! -f "$STORAGE/$BASE" ]; then - MSG="Windows is being downloaded, please wait..." - fi - - else - - BASE=$(basename "${VERSION%%\?*}") - : "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}" - BASE=$(echo "$BASE" | sed -e 's/[^A-Za-z0-9._-]/_/g') - - if [ ! -f "$STORAGE/$BASE" ]; then - MSG="Image '$BASE' is being downloaded, please wait..." - fi + BASE="$VERSION.iso" + if [ ! -f "$STORAGE/$BASE" ]; then + MSG="Windows is being downloaded, please wait..." fi + +else + + BASE=$(basename "${VERSION%%\?*}") + : "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}" + BASE=$(echo "$BASE" | sed -e 's/[^A-Za-z0-9._-]/_/g') + + if [ ! -f "$STORAGE/$BASE" ]; then + MSG="Image '$BASE' is being downloaded, please wait..." + fi + fi +[[ "${BASE,,}" == "custom."* ]] && BASE="target.iso" + html "$MSG" - -[ -f "$STORAGE/$BASE" ] && return 0 - TMP="$STORAGE/tmp" -rm -rf "$TMP" + +if [ -f "$STORAGE/$BASE" ]; then + rm -rf "$TMP" + return 0 +fi + mkdir -p "$TMP" ISO="$TMP/$BASE" rm -f "$ISO" -if [[ "$EXTERNAL" != [Yy1]* ]]; then +CUSTOM="custom.iso" - SCRIPT="$TMP/mido.sh" - - rm -f "$SCRIPT" - cp /run/mido.sh "$SCRIPT" - chmod +x "$SCRIPT" - cd "$TMP" - bash "$SCRIPT" "$VERSION" - rm -f "$SCRIPT" - cd /run +[ ! -f "$STORAGE/$CUSTOM" ] && CUSTOM="custom.img" +[ ! -f "$STORAGE/$CUSTOM" ] && CUSTOM="Custom.iso" +[ ! -f "$STORAGE/$CUSTOM" ] && CUSTOM="Custom.img" +[ ! -f "$STORAGE/$CUSTOM" ] && CUSTOM="custom.ISO" +[ ! -f "$STORAGE/$CUSTOM" ] && CUSTOM="custom.IMG" +[ ! -f "$STORAGE/$CUSTOM" ] && CUSTOM="CUSTOM.ISO" +[ ! -f "$STORAGE/$CUSTOM" ] && CUSTOM="CUSTOM.IMG" +if [ ! -f "$STORAGE/$CUSTOM" ]; then + CUSTOM="" else - - info "Downloading $BASE as boot image..." - - # Check if running with interactive TTY or redirected to docker log - if [ -t 1 ]; then - PROGRESS="--progress=bar:noscroll" - else - PROGRESS="--progress=dot:giga" - fi - - { wget "$VERSION" -O "$ISO" -q --no-check-certificate --show-progress "$PROGRESS"; rc=$?; } || : - - (( rc != 0 )) && echo && error "Failed to download $VERSION, reason: $rc" && exit 60 - + ISO="$STORAGE/$CUSTOM" fi -[ ! -f "$ISO" ] && echo && error "Failed to download $VERSION" && exit 61 +if [ ! -f "$ISO" ]; then + if [[ "$EXTERNAL" != [Yy1]* ]]; then + + cd "$TMP" + /run/mido.sh "$VERSION" + cd /run + + else + + info "Downloading $BASE as boot image..." + + # Check if running with interactive TTY or redirected to docker log + if [ -t 1 ]; then + PROGRESS="--progress=bar:noscroll" + else + PROGRESS="--progress=dot:giga" + fi + + { wget "$VERSION" -O "$ISO" -q --no-check-certificate --show-progress "$PROGRESS"; rc=$?; } || : + + (( rc != 0 )) && echo && error "Failed to download $VERSION, reason: $rc" && exit 60 + + fi + + [ ! -f "$ISO" ] && echo && error "Failed to download $VERSION" && exit 61 +fi SIZE=$(stat -c%s "$ISO") +SIZE_GB=$(( (SIZE + 1073741823)/1073741824 )) if ((SIZE<10000000)); then echo && error "Invalid ISO file: Size is smaller than 10 MB" && exit 62 fi -MSG="Extracting downloaded ISO image..." +SPACE=$(df --output=avail -B 1 "$TMP" | tail -n 1) +SPACE_GB=$(( (SPACE + 1073741823)/1073741824 )) + +if (( SIZE > SPACE )); then + error "Not enough free space in $STORAGE, have $SPACE_GB GB available but need at least $SIZE_GB GB." && exit 63 +fi + +if [ -n "$CUSTOM" ]; then + MSG="Extracting custom ISO image..." +else + MSG="Extracting downloaded ISO image..." +fi + echo && info "$MSG" && html "$MSG" DIR="$TMP/unpack" @@ -138,11 +163,13 @@ if [ ! -f "$DIR/$ETFS" ] || [ ! -f "$DIR/$EFISYS" ]; then else warn "failed to locate file 'efisys_noprompt.bin' in ISO image, $FB" fi - mv "$ISO" "$STORAGE/$BASE" + mv -f "$ISO" "$STORAGE/$BASE" rm -rf "$TMP" echo && return 0 fi +[ -z "$CUSTOM" ] && rm -f "$ISO" + if [ -z "$MANUAL" ]; then MANUAL="N" @@ -156,11 +183,10 @@ fi XML="" if [[ "$MANUAL" != [Yy1]* ]]; then - if [[ "$EXTERNAL" != [Yy1]* ]]; then - XML="$VERSION.xml" + [[ "$EXTERNAL" != [Yy1]* ]] && XML="$VERSION.xml" - else + if [ ! -f "/run/assets/$XML" ]; then MSG="Detecting Windows version from ISO image..." info "$MSG" && html "$MSG" @@ -190,7 +216,12 @@ if [[ "$MANUAL" != [Yy1]* ]]; then if [ -n "$DETECTED" ]; then XML="$DETECTED.xml" - echo "Detected image of type '$DETECTED', will apply autounattend.xml file." + + if [ -f "/run/assets/$XML" ]; then + echo "Detected image of type '$DETECTED', will apply an autounattend.xml file." + else + warn "detected image of type '$DETECTED', but no matching .xml file exists, $FB." + fi else if [ -z "$NAME" ]; then @@ -255,25 +286,30 @@ if [ -f "$ASSET" ]; then echo -else - if [ -n "$XML" ]; then - warn "XML file '$XML' does not exist, $FB" && echo - fi fi CAT="BOOT.CAT" LABEL="${BASE%.*}" LABEL="${LABEL::30}" -ISO="$TMP/$LABEL.tmp" -rm -f "$ISO" +OUT="$TMP/$LABEL.tmp" +rm -f "$OUT" + +SPACE=$(df --output=avail -B 1 "$TMP" | tail -n 1) +SPACE_GB=$(( (SPACE + 1073741823)/1073741824 )) + +if (( SIZE > SPACE )); then + error "Not enough free space in $STORAGE, have $SPACE_GB GB available but need at least $SIZE_GB GB." && exit 63 +fi MSG="Generating new ISO image for installation..." info "$MSG" && html "$MSG" genisoimage -b "$ETFS" -no-emul-boot -c "$CAT" -iso-level 4 -J -l -D -N -joliet-long -relaxed-filenames -quiet -V "$LABEL" -udf \ - -boot-info-table -eltorito-alt-boot -eltorito-boot "$EFISYS" -no-emul-boot -o "$ISO" -allow-limited-size "$DIR" + -boot-info-table -eltorito-alt-boot -eltorito-boot "$EFISYS" -no-emul-boot -o "$OUT" -allow-limited-size "$DIR" -mv "$ISO" "$STORAGE/$BASE" +[ -n "$CUSTOM" ] && rm -f "$STORAGE/$CUSTOM" + +mv "$OUT" "$STORAGE/$BASE" rm -rf "$TMP" html "Successfully prepared image for installation..." diff --git a/src/mido.sh b/src/mido.sh new file mode 100644 index 0000000..bab64b2 --- /dev/null +++ b/src/mido.sh @@ -0,0 +1,766 @@ +#!/bin/sh + +# Copyright (C) 2024 Elliot Killick +# Licensed under the MIT License. See LICENSE file for details. + +[ "$DEBUG" ] && set -x + +# Prefer Dash shell for greater security if available +if [ "$BASH" ] && command -v dash > /dev/null; then + exec dash "$0" "$@" +fi + +# Test for 4-bit color (16 colors) +# Operand "colors" is undefined by POSIX +# If the operand doesn't exist, the terminal probably doesn't support color and the program will continue normally without it +if [ "0$(tput colors 2> /dev/null)" -ge 16 ]; then + RED='\033[0;31m' + BLUE='\033[0;34m' + GREEN='\033[0;32m' + NC='\033[0m' +fi + +# Avoid printing messages as potential terminal escape sequences +echo_ok() { printf "%b%s%b" "${GREEN}[+]${NC} " "$1" "\n" >&2; } +echo_info() { printf "%b%s%b" "${BLUE}[i]${NC} " "$1" "\n" >&2; } +echo_err() { printf "%b%s%b" "${RED}[!]${NC} " "$1" "\n" >&2; } + +# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/fold.html +format() { fold -s; } + +word_count() { echo $#; } + +usage() { + echo "Mido - The Secure Microsoft Windows Downloader" + echo "" + echo "Usage: $0 ..." + echo "" + echo "Download specified list of Windows media." + echo "" + echo "Specify \"all\", or one or more of the following Windows media:" + echo " win7x64-ultimate" + echo " win81x64" + echo " win10x64" + echo " win11x64" + echo " win81x64-enterprise-eval" + echo " win10x64-enterprise-eval" + echo " win11x64-enterprise-eval" + echo " win10x64-enterprise-ltsc-eval (most secure)" + echo " win2008r2" + echo " win2012r2-eval" + echo " win2016-eval" + echo " win2019-eval" + echo " win2022-eval" + echo "" + echo "Each ISO download takes between 3 - 7 GiBs (average: 5 GiBs)." + echo "" + echo "Updates" + echo "-------" + echo "All the downloads provided here are the most up-to-date releases that Microsoft provides. This is ensured by programmatically checking Microsoft's official download pages to get the latest download link. In other cases, the Windows version in question is no longer supported by Microsoft meaning a direct download link (stored in Mido) will always point to the most up-to-date release." | format + echo "" + echo "Remember to update Windows to the latest patch level after installation." + echo "" + echo "Overuse" + echo "-------" + echo "Newer consumer versions of Windows including win81x64, win10x64, and win11x64 are downloaded through Microsoft's gated download web interface. Do not overuse this interface. Microsoft may be quick to do ~24 hour IP address bans after only a few download requests (especially if they are done in quick succession). Being temporarily banned from one of these downloads (e.g. win11x64) doesn't cause you to be banned from any of the other downloads provided through this interface." | format + echo "" + echo "Privacy Preserving Technologies" + echo "-------------------------------" + echo "The aforementioned Microsoft gated download web interface is currently blocking Tor (and similar technologies). They say this is to prevent people in restricted regions from downloading certain Windows media they shouldn't have access to. This is fine by most standards because Tor is too slow for large downloads anyway and we have checksum verification for security." | format + echo "" + echo "Language" + echo "--------" + echo "All the downloads provided here are for English (United States). This helps to great simplify maintenance and minimize the user's fingerprint. If another language is desired then that can easily be configured in Windows once it's installed." | format + echo "" + echo "Architecture" + echo "------------" + echo "All the downloads provided here are for x86-64 (x64). This is the only architecture Microsoft ships Windows Server in.$([ -d /run/qubes ] && echo ' Also, the only architecture Qubes OS supports.')" | format +} + +# Media naming scheme info: +# Windows Server has no architecture because Microsoft only supports amd64 for this version of Windows (the last version to support x86 was Windows Server 2008 without the R2) +# "eval" is short for "evaluation", it's simply the license type included with the Windows installation (only exists on enterprise/server) and must be specified in the associated answer file +# "win7x64" has the "ultimate" edition appended to it because it isn't "multi-edition" like the other Windows ISOs (for multi-edition ISOs the edition is specified in the associated answer file) + +readonly win7x64_ultimate="win7x64-ultimate.iso" +readonly win81x64="win81x64.iso" +readonly win10x64="win10x64.iso" +readonly win11x64="win11x64.iso" +readonly win81x64_enterprise_eval="win81x64-enterprise-eval.iso" +readonly win10x64_enterprise_eval="win10x64-enterprise-eval.iso" +readonly win11x64_enterprise_eval="win11x64-enterprise-eval.iso" +readonly win10x64_enterprise_ltsc_eval="win10x64-enterprise-ltsc-eval.iso" +readonly win2008r2="win2008r2.iso" +readonly win2012r2_eval="win2012r2-eval.iso" +readonly win2016_eval="win2016-eval.iso" +readonly win2019_eval="win2019-eval.iso" +readonly win2022_eval="win2022-eval.iso" + +parse_args() { + for arg in "$@"; do + if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then + usage + exit + fi + done + + if [ $# -lt 1 ]; then + usage >&2 + exit 1 + fi + + # Append to media_list so media is downloaded in the order they're passed in + for arg in "$@"; do + case "$arg" in + win7x64-ultimate) + media_list="$media_list $win7x64_ultimate" + ;; + win81x64) + media_list="$media_list $win81x64" + ;; + win10x64) + media_list="$media_list $win10x64" + ;; + win11x64) + media_list="$media_list $win11x64" + ;; + win81x64-enterprise-eval) + media_list="$media_list $win81x64_enterprise_eval" + ;; + win10x64-enterprise-eval) + media_list="$media_list $win10x64_enterprise_eval" + ;; + win11x64-enterprise-eval) + media_list="$media_list $win11x64_enterprise_eval" + ;; + win10x64-enterprise-ltsc-eval) + media_list="$media_list $win10x64_enterprise_ltsc_eval" + ;; + win2008r2) + media_list="$media_list $win2008r2" + ;; + win2012r2-eval) + media_list="$media_list $win2012r2_eval" + ;; + win2016-eval) + media_list="$media_list $win2016_eval" + ;; + win2019-eval) + media_list="$media_list $win2019_eval" + ;; + win2022-eval) + media_list="$media_list $win2022_eval" + ;; + all) + media_list="$win7x64_ultimate $win81x64 $win10x64 $win11x64 $win81x64_enterprise_eval $win10x64_enterprise_eval $win11x64_enterprise_eval $win10x64_enterprise_ltsc_eval $win2008r2 $win2012r2_eval $win2016_eval $win2019_eval $win2022_eval" + break + ;; + *) + echo_err "Invalid Windows media specified: $arg" + exit 1 + ;; + esac + done +} + +handle_curl_error() { + error_code="$1" + + fatal_error_action=2 + + case "$error_code" in + 6) + echo_err "Failed to resolve Microsoft servers! Is there an Internet connection? Exiting..." + return "$fatal_error_action" + ;; + 7) + echo_err "Failed to contact Microsoft servers! Is there an Internet connection or is the server down?" + ;; + 8) + echo_err "Microsoft servers returned a malformed HTTP response!" + ;; + 22) + echo_err "Microsoft servers returned a failing HTTP status code!" + ;; + 23) + echo_err "Failed at writing Windows media to disk! Out of disk space or permission error? Exiting..." + return "$fatal_error_action" + ;; + 26) + echo_err "Ran out of memory during download! Exiting..." + return "$fatal_error_action" + ;; + 36) + echo_err "Failed to continue earlier download!" + ;; + 63) + echo_err "Microsoft servers returned an unexpectedly large response!" + ;; + # POSIX defines exit statuses 1-125 as usable by us + # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02 + $((error_code <= 125))) + # Must be some other server or network error (possibly with this specific request/file) + # This is when accounting for all possible errors in the curl manual assuming a correctly formed curl command and HTTP(S) request, using only the curl features we're using, and a sane build + echo_err "Miscellaneous server or network error!" + ;; + 126 | 127) + echo_err "Curl command not found! Please install curl and try again. Exiting..." + return "$fatal_error_action" + ;; + # Exit statuses are undefined by POSIX beyond this point + *) + case "$(kill -l "$error_code")" in + # Signals defined to exist by POSIX: + # https://pubs.opengroup.org/onlinepubs/009695399/basedefs/signal.h.html + INT) + echo_err "Curl was interrupted!" + ;; + # There could be other signals but these are most common + SEGV | ABRT) + echo_err "Curl crashed! Failed exploitation attempt? Please report any core dumps to curl developers. Exiting..." + return "$fatal_error_action" + ;; + *) + echo_err "Curl terminated due to a fatal signal!" + ;; + esac + esac + + return 1 +} + +part_ext=".PART" +unverified_ext=".UNVERIFIED" + +scurl_file() { + out_file="$1" + tls_version="$2" + url="$3" + + part_file="${out_file}${part_ext}" + + # --location: Microsoft likes to change which endpoint these downloads are stored on but is usually kind enough to add redirects + # --fail: Return an error on server errors where the HTTP response code is 400 or greater + curl --progress-bar --location --output "$part_file" --continue-at - --max-filesize 10G --fail --proto =https "--tlsv$tls_version" --http1.1 -- "$url" || { + error_code=$? + handle_curl_error "$error_code" + error_action=$? + + # Clean up and make sure future resumes don't happen from bad download resume files + if [ -f "$out_file" ]; then + # If file is empty, bad HTTP code, or bad download resume file + if [ ! -s "$out_file" ] || [ "$error_code" = 22 ] || [ "$error_code" = 36 ]; then + echo_info "Deleting failed download..." + rm -f "$out_file" + fi + fi + + return "$error_action" + } + + # Full downloaded succeeded, ready for verification check + mv "$part_file" "${out_file}" +} + +manual_verification() { + media_verification_failed_list="$1" + checksum_verification_failed_list="$2" + + echo_info "Manual verification instructions" + echo " 1. Get checksum (may already be done for you):" >&2 + echo " sha256sum " >&2 + echo "" >&2 + echo " 2. Verify media:" >&2 + echo " Web search: https://duckduckgo.com/?q=%22CHECKSUM_HERE%22" >&2 + echo " Onion search: https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/?q=%22CHECKSUM_HERE%22" >&2 + echo " \"No results found\" or unexpected results indicates the media has been modified and should not be used." >&2 + echo "" >&2 + echo " 3. Remove the $unverified_ext extension from the file after performing or deciding to skip verification (not recommended):" >&2 + echo " mv $unverified_ext " >&2 + echo "" >&2 + + for media in $media_verification_failed_list; do + # Read current checksum in list and then read remaining checksums back into the list (effectively running "shift" on the variable) + # POSIX sh doesn't support indexing so do this instead to iterate both lists at once + # POSIX sh doesn't support here-strings (<<<). We could also use the "cut" program but that's not a builtin + IFS=' ' read -r checksum checksum_verification_failed_list << EOF +$checksum_verification_failed_list +EOF + + echo " ${media}${unverified_ext} = $checksum" >&2 + echo " Web search: https://duckduckgo.com/?q=%22$checksum%22" >&2 + echo " Onion search: https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/?q=%22$checksum%22" >&2 + echo " mv ${media}${unverified_ext} $media" >&2 + echo "" >&2 + done + + echo " Theses searches can be performed in a web/Tor browser or more securely using" >&2 + echo " ddgr (Debian/Fedora packages available) terminal search tool if preferred." >&2 + echo " Once validated, consider updating the checksums in Mido by submitting a pull request on GitHub." >&2 + + # If you're looking for a single secondary source to cross-reference checksums then try here: https://files.rg-adguard.net/search + # This site is recommended by the creator of Rufus in the Fido README and has worked well for me +} + +consumer_download() { + # Copyright (C) 2024 Elliot Killick + # Licensed under the MIT License. See LICENSE file for details. + # + # This function is from the Mido project: + # https://github.com/ElliotKillick/Mido + + # Download newer consumer Windows versions from behind gated Microsoft API + + out_file="$1" + # Either 8, 10, or 11 + windows_version="$2" + + url="https://www.microsoft.com/en-us/software-download/windows$windows_version" + case "$windows_version" in + 8 | 10) url="${url}ISO" ;; + esac + + user_agent="Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0" + # uuidgen: For MacOS (installed by default) and other systems (e.g. with no /proc) that don't have a kernel interface for generating random UUIDs + session_id="$(cat /proc/sys/kernel/random/uuid 2> /dev/null || uuidgen --random)" + + # Get product edition ID for latest release of given Windows version + # Product edition ID: This specifies both the Windows release (e.g. 22H2) and edition ("multi-edition" is default, either Home/Pro/Edu/etc., we select "Pro" in the answer files) in one number + # This is a request we make that Fido doesn't. Fido manually maintains a list of all the Windows release/edition product edition IDs in its script (see: $WindowsVersions array). This is helpful for downloading older releases (e.g. Windows 10 1909, 21H1, etc.) but we always want to get the newest release which is why we get this value dynamically + # Also, keeping a "$WindowsVersions" array like Fido does would be way too much of a maintenance burden + # Remove "Accept" header that curl sends by default (match Fido requests) + iso_download_page_html="$(curl -sS --user-agent "$user_agent" --header "Accept:" --max-filesize 1M --fail --proto =https --tlsv1.2 --http1.1 -- "$url")" || { + handle_curl_error $? + return $? + } + + # tr: Filter for only numerics to prevent HTTP parameter injection + # head -c was recently added to POSIX: https://austingroupbugs.net/view.php?id=407 + product_edition_id="$(echo "$iso_download_page_html" | grep -Eo '