#!/bin/sh # shellcheck disable=SC1090 disable=SC2059 disable=SC2164 disable=SC2181 disable=SC2317 # webmin-setup-repo.sh # Sets up a stable, prerelease, or unstable repository to provide Webmin and # Usermin packages on Debian-based and RPM-based systems # Default values that can be overridden repo_host="download.webmin.com" repo_download="https://$repo_host" repo_download_prerelease="https://rc.download.webmin.dev" repo_download_unstable="https://download.webmin.dev" repo_key="developers-key.asc" repo_key_server="$repo_download" repo_key_suffix="webmin-developers" repo_key_name="Webmin Developers" repo_name="webmin-stable" repo_name_prerelease="webmin-prerelease" repo_name_unstable="webmin-unstable" repo_rpm_pathname="/download/newkey/yum" repo_deb_pathname="/download/newkey/repository" repo_component="main" repo_dist="stable" repo_section="contrib" repo_description="Webmin Stable" repo_description_prerelease="Webmin Pre-release" repo_description_unstable="Webmin Unstable" install_check_binary="/usr/bin/webmin" install_message="Webmin and Usermin can be installed with:" install_packages="webmin usermin" repo_auth_user="" repo_auth_pass="" # Repository mode (stable, prerelease, unstable) repo_mode="stable" download_curl="/usr/bin/curl" download="$download_curl -q -f -sS -L -O" force_setup=0 tmp_dir="" # Colors NORMAL="$(tput sgr0 2>/dev/null || echo '')" GREEN="$(tput setaf 2 2>/dev/null || echo '')" RED="$(tput setaf 1 2>/dev/null || echo '')" BOLD="$(tput bold 2>/dev/null || echo '')" ITALIC="$(tput sitm 2>/dev/null || echo '')" # Print the usage message and exit usage() { if [ -n "${1-}" ]; then echo "${RED}Error:${NORMAL} Unknown or invalid argument: $1" fi echo cat << EOF Usage: $(basename "$0") [OPTIONS] General options: -f, --force Force setup without confirmation -h, --help Display this help message Repository types: --stable Set up the stable repo, built with extra testing --prerelease Set up the pre-release repo built from latest tag --unstable Set up unstable repo built from the latest commit Repository configuration: --host= Main repository host --repo-rpm-path= Repository path for RPM-based systems --repo-deb-path= Repository path for DEB-based systems --prerelease-host= Pre-release repository host --unstable-host= Unstable repository host --key= Repository signing key file --key-server= Repository signing key download server --key-name= Repository key name for display --key-suffix= Repository key suffix for file naming --auth-user= Repository authentication username --auth-pass= Repository authentication password --pkg-prefs= Package preferences for repository --repo-prefs= Optional preferences for repository Repository metadata: --name= Base name for repository (default: webmin) --description= Description for repository (default: Webmin) --component= Repository component (default: main) --section= Repository section (default: contrib) --dist= Distribution name (default: stable) Post-installation options: --check-binary= Binary to check in post-install --install-message= Message to show in post-install if binary not found --install-packages= Packages to suggest for installation EOF exit 1 } # Print a success or failure status line and exit on failure post_status() { status="$1" message="$2" if [ "$status" -eq 0 ]; then echo " .. done" else if [ -n "$message" ]; then echo " .. failed : $message" else echo " .. failed" fi exit "$status" fi } # Parse CLI arguments and select the active repository settings process_args() { for arg in "$@"; do case "$arg" in --stable) repo_mode="stable" ;; --prerelease|--pre-release|--rc) repo_mode="prerelease" ;; --unstable|--testing|-t) repo_mode="unstable" ;; -f|--force) force_setup=1 ;; -h|--help) usage ;; --host=*) repo_host="${arg#*=}" repo_download="https://$repo_host" repo_key_server="$repo_download" ;; --repo-rpm-path=*) repo_rpm_pathname="${arg#*=}" [ "$repo_rpm_pathname" = "/" ] && repo_rpm_pathname="" repo_rpm_pathname_set=1 ;; --repo-deb-path=*) repo_deb_pathname="${arg#*=}" [ "$repo_deb_pathname" = "/" ] && repo_deb_pathname="" repo_deb_pathname_set=1 ;; --prerelease-host=*) repo_download_prerelease="https://${arg#*=}" ;; --unstable-host=*) repo_download_unstable="https://${arg#*=}" ;; --key=*) repo_key="${arg#*=}" ;; --key-server=*) key_server="${arg#*=}" key_server="${key_server%/}" repo_key_server="$key_server" ;; --key-name=*) repo_key_name="${arg#*=}" ;; --key-suffix=*) repo_key_suffix="${arg#*=}" ;; --auth-user=*) repo_auth_user="${arg#*=}" ;; --auth-pass=*) repo_auth_pass="${arg#*=}" ;; --pkg-prefs=*) repo_pkg_prefs="${arg#*=}" ;; --repo-prefs=*) repo_prefs="${arg#*=}" ;; --name=*) base_name="${arg#*=}" repo_name="${base_name}-stable" repo_name_prerelease="${base_name}-prerelease" repo_name_unstable="${base_name}-unstable" ;; --description=*) base_description="${arg#*=}" repo_description="$base_description Stable" repo_description_prerelease="${base_description} Pre-release" repo_description_unstable="${base_description} Unstable" ;; --component=*) repo_component="${arg#*=}" ;; --section=*) repo_section="${arg#*=}" ;; --dist=*) repo_dist="${arg#*=}" ;; --check-binary=*) install_check_binary="${arg#*=}" ;; --install-message=*) install_message="${arg#*=}" ;; --install-packages=*) install_packages="${arg#*=}" ;; *) usage "$arg" ;; esac done # Set active repo variables based on mode case "$repo_mode" in prerelease) active_repo_name="$repo_name_prerelease" active_repo_description="$repo_description_prerelease" active_repo_download="$repo_download_prerelease" if [ "$repo_dist" = "stable" ]; then repo_dist="webmin" fi ;; unstable) active_repo_name="$repo_name_unstable" active_repo_description="$repo_description_unstable" active_repo_download="$repo_download_unstable" if [ "$repo_dist" = "stable" ]; then repo_dist="webmin" fi ;; *) active_repo_name="$repo_name" active_repo_description="$repo_description" active_repo_download="$repo_download" ;; esac } # Ensure the script is running with root privileges check_permission() { if [ "$(id -u)" -ne 0 ]; then echo "${RED}Error:${NORMAL} \`$(basename "$0")\` must be run as root!" >&2 exit 1 fi } # Create a temporary working directory, preferring mktemp when available create_tmp_dir() { if command -pv mktemp 1>/dev/null 2>&1; then tmp_candidate=$(mktemp -d 2>/dev/null) if [ $? -eq 0 ] && [ -n "$tmp_candidate" ]; then printf '%s\n' "$tmp_candidate" return 0 fi fi for candidate in "/tmp" "/var/tmp"; do if [ ! -d "$candidate" ] || [ ! -w "$candidate" ]; then continue fi tmp_try=0 while [ "$tmp_try" -lt 10 ]; do tmp_candidate="$candidate/webmin-setup-repo.$$" [ "$tmp_try" -gt 0 ] && tmp_candidate="$tmp_candidate.$tmp_try" if ( umask 077 && mkdir "$tmp_candidate" ) 2>/dev/null; then printf '%s\n' "$tmp_candidate" return 0 fi tmp_try=$((tmp_try + 1)) done done return 1 } # Create a temporary file in a specific directory for safe replacement writes create_tmp_file() { tmp_parent="$1" tmp_prefix="$2" if [ ! -d "$tmp_parent" ] || [ ! -w "$tmp_parent" ]; then return 1 fi if command -pv mktemp 1>/dev/null 2>&1; then tmp_candidate=$(mktemp "$tmp_parent/$tmp_prefix.XXXXXX" 2>/dev/null) if [ $? -eq 0 ] && [ -n "$tmp_candidate" ]; then printf '%s\n' "$tmp_candidate" return 0 fi fi tmp_try=0 while [ "$tmp_try" -lt 10 ]; do tmp_candidate="$tmp_parent/$tmp_prefix.$$" [ "$tmp_try" -gt 0 ] && tmp_candidate="$tmp_candidate.$tmp_try" if ( umask 077 && set -C && : > "$tmp_candidate" ) 2>/dev/null; then printf '%s\n' "$tmp_candidate" return 0 fi tmp_try=$((tmp_try + 1)) done return 1 } # Create and switch into the temporary working directory prepare_tmp() { tmp_dir=$(create_tmp_dir) if [ $? -ne 0 ] || [ -z "$tmp_dir" ]; then echo "${RED}Error:${NORMAL} Failed to create temporary working directory!" exit 1 fi trap cleanup_tmp EXIT trap 'cleanup_tmp; exit 1' HUP INT TERM cd "$tmp_dir" 1>/dev/null 2>&1 if [ $? -ne 0 ]; then echo "${RED}Error:${NORMAL} Failed to switch to temporary working directory!" exit 1 fi } # Remove the temporary working directory on exit cleanup_tmp() { if [ -n "$tmp_dir" ] && [ -d "$tmp_dir" ]; then rm -rf "$tmp_dir" fi } # Detect the current OS family and choose the package manager commands detect_os() { osrelease="/etc/os-release" if [ ! -f "$osrelease" ]; then echo "${RED}Error:${NORMAL} Cannot detect OS!" exit 1 fi . "$osrelease" if [ -n "${ID_LIKE}" ]; then osid="$ID_LIKE" else osid="$ID" fi if [ -z "$osid" ]; then echo "${RED}Error:${NORMAL} Failed to detect OS!" exit 1 fi osid_debian_like=$(echo "$osid" | grep "debian\|ubuntu") osid_rhel_like=$(echo "$osid" | grep "rhel\|fedora\|centos\|openEuler") osid_suse_like=$(echo "$osid" | grep "suse") repoid_debian_like=debian if [ -n "${ID}" ]; then repoid_debian_like="${ID}" fi if [ -n "$osid_debian_like" ]; then package_type=deb install_cmd="apt-get install --install-recommends" install="$install_cmd --quiet --assume-yes" clean="apt-get clean" update="apt-get update" elif [ -n "$osid_rhel_like" ]; then package_type=rpm repo_extra_opts="" if command -pv dnf 1>/dev/null 2>&1; then install_cmd="dnf install" install="$install_cmd -y" clean="dnf clean all" update="dnf makecache" else install_cmd="yum install" install="$install_cmd -y" clean="yum clean all" update="yum makecache" fi elif [ -n "$osid_suse_like" ]; then package_type=rpm install_cmd="zypper install" install="$install_cmd -y" clean="zypper clean" update="zypper refresh" repo_extra_opts="autorefresh=1" else echo "${RED}Error:${NORMAL} Unknown OS : $osid" exit 1 fi } # Derive package-manager-specific repository file paths set_os_variables() { # Debian-based debian_repo_file="/etc/apt/sources.list.d/$active_repo_name.list" # RPM-based rpm_repo_dir="/etc/yum.repos.d" if [ -n "$osid_suse_like" ]; then rpm_repo_dir="/etc/zypp/repos.d" fi rpm_repo_file="$rpm_repo_dir/$active_repo_name.repo" } # Show the repository mode warning and ask for confirmation when needed ask_confirmation() { # Format description so only the first word keeps its case and the rest is # lowercased repo_desc_formatted=$(echo "$active_repo_description" | \ sed 's/\([^ ]*\)\(.*\)/\1\L\2/') # Show special messages for prerelease and unstable repos case "$repo_mode" in prerelease) printf \ "\e[48;5;236;38;5;208;1mPre-release builds are automated from the latest tagged release\e[0m\n" ;; unstable) printf \ "\e[48;5;236;38;5;160;1mUnstable builds are automated experimental versions designed for\e[0m\n" printf \ "\e[48;5;236;38;5;160;1mdevelopment, often containing critical bugs and breaking changes\e[0m\n" ;; esac if [ "$force_setup" != "1" ]; then printf "Setup ${repo_desc_formatted} repository? (y/N) " read -r sslyn if [ "$sslyn" != "y" ] && [ "$sslyn" != "Y" ]; then exit 0 fi fi } # Ensure a supported download tool is available check_downloader() { if [ ! -x "$download_curl" ]; then if [ -x "/usr/bin/wget" ]; then download="/usr/bin/wget -nv" elif [ -x "/usr/bin/fetch" ]; then download="/usr/bin/fetch" else echo " Installing required ${ITALIC}curl${NORMAL} from OS repos .." install_output=$($install curl 2>&1) post_status $? "$(echo "$install_output" | tr '\n' ' ')" fi fi } # Install GnuPG on Debian-like systems when it is required check_gpg() { if [ -n "$osid_debian_like" ]; then if [ ! -x /usr/bin/gpg ]; then echo " Installing required ${ITALIC}gnupg${NORMAL} from OS repos .." $update 1>/dev/null 2>&1 install_output=$($install gnupg 2>&1) post_status $? "$(echo "$install_output" | tr '\n' ' ')" fi fi } # Parse package preference rules for the requested package manager enforce_package_priority() { repo_pkg_pref=$1 disttarget=$2 # Save and set IFS to newline only old_ifs=$IFS IFS=' ' # Extract all entries for the target distribution for entry in $repo_pkg_pref; do case "$entry" in ${disttarget}:*) # Extract the action and the rest of the entry action=${entry#"$disttarget:"} action=${action%%:*} entry=${entry#"$disttarget:$action:"} entry=$disttarget:$entry # Remove the prefix spec="${entry#"${disttarget}":}" # Parse package=priority=version format package="${spec%%=*}" remainder="${spec#*=}" # Check if remainder is empty or just a package name if [ "$remainder" = "$spec" ]; then printf "%s\t%s %s %s\n" "${action:-}" "$package" "" "" else priority="${remainder%%=*}" version="${remainder#*=}" [ "$version" = "$remainder" ] && version="" printf "%s\t%s %s %s\n" "${action:-}" "$package" "${priority:-}" "${version:-}" fi ;; esac done # Restore IFS IFS=$old_ifs } # Download the configured repository signing key files download_key() { echo " Downloading $repo_key_name key .." for key in $repo_key; do download_out=$($download "$repo_key_server/$key" 2>&1) if [ $? -ne 0 ]; then post_status 1 "$(printf '%s : %s' "$repo_key_server/$key" "$download_out" | tr '\n' ' ')" fi done post_status 0 "" } # Extract RPM-specific repository preference lines rpm_repo_prefs() { for pref in $repo_prefs; do if echo "$pref" | grep "^rpm:" >/dev/null 2>&1; then val=$(echo "$pref" | sed 's/^rpm://') printf '%s\n' "$val" fi done } # Install keys, configure repository files, and refresh metadata setup_repos() { # Format description so only the first word keeps its case and the rest is # lowercased repo_desc_formatted=$(echo "$active_repo_description" | \ sed 's/\([^ ]*\)\(.*\)/\1\L\2/') # Defaults for unstable and prerelease repos when no custom paths are set case "$repo_mode" in prerelease|unstable) if [ -z "$repo_rpm_pathname_set" ]; then repo_rpm_pathname="" repo_rpm_pathname_set=1 fi if [ -z "$repo_deb_pathname_set" ]; then repo_deb_pathname="" repo_deb_pathname_set=1 fi ;; esac # Construct auth URL if credentials provided repo_auth_url="$active_repo_download" if [ -n "$repo_auth_user" ] && [ -n "$repo_auth_pass" ]; then protocol="${repo_auth_url%%://*}" rest="${repo_auth_url#*://}" repo_auth_url="${protocol}://$repo_auth_user:$repo_auth_pass@$rest" fi case "$package_type" in rpm) echo " Installing $repo_key_name key .." mkdir -p "/etc/pki/rpm-gpg" combined_key="/etc/pki/rpm-gpg/RPM-GPG-KEY-$repo_key_suffix" # Build a single keyring file from all the key files, making sure each key # ends with a newline so armor blocks stay separated { for key in $repo_key; do cat "$key" printf '\n' done } > "$combined_key" rpm --import "$combined_key" echo " .. done" # Configure packages extra preferences if given if [ -n "$repo_pkg_prefs" ]; then repo_pkg_prefs_rs=$(enforce_package_priority "$repo_pkg_prefs" "rpm") repo_extra_opts=$(printf '%s\n' "$repo_pkg_prefs_rs" | awk -F'\t' ' function trim(s) { sub(/^[ \t]+/, "", s) sub(/[ \t]+$/, "", s) return s } { key = trim($1) val = trim($2) if (key in opts) opts[key] = opts[key] " " val else opts[key] = val } END { for (k in opts) printf "%s=%s\n", k, trim(opts[k]) }') if [ -n "$repo_pkg_prefs_rs" ]; then echo " Setting up package exclusion for repository .." echo " .. done" fi fi # Configure the repository echo " Setting up ${repo_desc_formatted} repository .." if [ -z "$repo_rpm_pathname_set" ]; then repo_url="$active_repo_download$repo_rpm_pathname" else repo_url="$repo_auth_url$repo_rpm_pathname" fi repo_extra_opts_caller=$(rpm_repo_prefs) cat << EOF > "$rpm_repo_file" [$active_repo_name] name=$active_repo_description baseurl=$repo_url enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-$repo_key_suffix gpgcheck=1 EOF # Append non-empty options if they exist to keep config file clean [ -n "$repo_extra_opts" ] && \ printf '%s\n' "$repo_extra_opts" >> "$rpm_repo_file" [ -n "$repo_extra_opts_caller" ] && \ printf '%s\n' "$repo_extra_opts_caller" >> "$rpm_repo_file" echo " .. done" echo " Downloading repository metadata .." update_output=$($update 2>&1) post_status $? "$update_output" ;; deb) rm -f \ "/usr/share/keyrings/debian-$repo_key_suffix.gpg" \ "/usr/share/keyrings/$repoid_debian_like-$repo_key_suffix.gpg" echo " Installing $repo_key_name key .." combined_keyring="/usr/share/keyrings/$repoid_debian_like-$repo_key_suffix.gpg" # Build a single keyring file from all the key files, making sure each key # ends with a newline so armor blocks stay separated { for key in $repo_key; do cat "$key" printf '\n' done } | gpg --dearmor > "$combined_keyring" post_status $? # Set correct permissions on the repo key in case the system uses a restrictive umask chmod 644 "/usr/share/keyrings/$repoid_debian_like-$repo_key_suffix.gpg" # Remove any existing entries for this repo from sources.list to avoid # conflicts if [ -f /etc/apt/sources.list ]; then tmp=$(create_tmp_file "/etc/apt" "sources.list") if [ $? -ne 0 ] || [ -z "$tmp" ]; then post_status 1 "Failed to create temporary \`sources.list\`" fi grep -vF "$repo_host" /etc/apt/sources.list >"$tmp" status=$? if [ "$status" -le 1 ]; then chmod 644 "$tmp" if [ $? -ne 0 ]; then rm -f "$tmp" post_status 1 "Failed to set permissions on temporary \`sources.list\`" fi mv "$tmp" /etc/apt/sources.list status=$? if [ "$status" -ne 0 ]; then post_status "$status" "Failed to replace \`/etc/apt/sources.list\`" fi else rm -f "$tmp" post_status "$status" "Failed to update \`/etc/apt/sources.list\`" fi fi # Configure packages priority if provided debian_repo_prefs="/etc/apt/preferences.d/$repoid_debian_like-$repo_dist-package-priority" if [ -n "$repo_pkg_prefs" ]; then # Clear the file first : > "$debian_repo_prefs" # Process all matching packages pkg_lines=$(enforce_package_priority "$repo_pkg_prefs" "deb") tab=$(printf '\t') while IFS="$tab" read -r action rest || [ -n "$action" ]; do [ -n "$action" ] || continue # shellcheck disable=SC2086 IFS=' ' set -- $rest package=$1 priority=$2 version=$3 [ -n "$package" ] || continue # Process the action case $action in # Set package priority pin) if [ -n "$version" ]; then pin_line="Pin: version /$version\$/" else pin_line="Pin: release *" fi cat >> "$debian_repo_prefs" < "$debian_repo_file" # Handle APT authentication if credentials provided if [ -n "$repo_auth_user" ] && [ -n "$repo_auth_pass" ]; then mkdir -p "/etc/apt/auth.conf.d" auth_file="/etc/apt/auth.conf.d/$active_repo_name.conf" auth_domain="${active_repo_download#*://}" auth_domain="${auth_domain%%/*}" # Remove existing entry for this domain if exists if [ -f "$auth_file" ]; then sed -i "/machine $auth_domain/d" "$auth_file" fi # Add new authentication entry echo "machine $auth_domain login $repo_auth_user password $repo_auth_pass" >> "$auth_file" chmod 600 "$auth_file" fi echo " .. done" echo " Cleaning repository metadata .." $clean 1>/dev/null 2>&1 echo " .. done" echo " Downloading repository metadata .." update_output=$($update 2>&1) post_status $? "$update_output" ;; *) echo "${RED}Error:${NORMAL} Cannot set up repositories on this system." exit 1 ;; esac } # Print the final installation hint when Webmin is not yet installed final_msg() { if [ "$install_check_binary" != "0" ] && [ ! -x "$install_check_binary" ]; then echo "$install_message" echo " ${GREEN}${BOLD}${ITALIC}$install_cmd $install_packages${NORMAL}" fi exit 0 } # Main process_args "$@" check_permission prepare_tmp detect_os set_os_variables ask_confirmation check_downloader check_gpg download_key setup_repos final_msg