#!/bin/sh

# auto-apt-proxy - automatic detector of common APT proxy settings
# Copyright (c) Antonio Terceiro
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

set -eu

log() {
  echo "[$(date --iso-8601=seconds)] auto-apt-proxy:" "$@" >&2
}

if [ -z "${AUTO_APT_PROXY_DEBUG:-}" ]; then
  log_debug() {
    :
  }
else
  log_debug() {
    log "DEBUG:" "$@"
  }
fi

log_error() {
  log "ERROR:" "$@"
}

log_debug_if_present() {
  item="${1}"
  msg="${2}"
  if [ -z "${item}" ]; then return; fi
  log_debug "${msg}"
}

uid=$(id -u)
cache_dir=${TMPDIR:-/tmp}/.auto-apt-proxy-${uid}
if [ -d "${cache_dir}" ]; then
  # require existing cache dir to be owned by the current user and have the
  # correct permissions
  owner_and_mode="$(stat --format=%u:%f "${cache_dir}")"
  if [ "${owner_and_mode}" != "${uid}:41c0" ]; then
    log_error "insecure cache dir ${cache_dir}. Must be owned by UID ${uid} and have permissions 700" >&2
    exit 1
  fi
elif [ -z "${AUTO_APT_PROXY_NO_CACHE:-}" ]; then
  mkdir -m 0700 "${cache_dir}"
fi

output="${cache_dir}/output"
cleanup() {
  rm -f "$output"
}
trap cleanup INT EXIT TERM

proxy_url() {
  case "$1" in
    *:*)
      echo "http://[$1]:$2"
    ;;
    *)
      echo "http://$1:$2"
    ;;
  esac
}

with_timeout() {
  timeout 1 "$@"
}

hit() {
  with_timeout /usr/lib/apt/apt-helper \
    -o Acquire::http::Proxy=DIRECT -o Acquire::Retries=0 \
    download-file "$@" "$output" 2>&1
}

set_proxy() {
  log_debug "detected proxy:" "$@"
  echo "$@"
}

cache_ttl=60 # seconds
cache() {
  local cache_file="${cache_dir}/cache"
  local lock_file="${cache_dir}/lock"
  local cache_age
  local cached_proxy
  (
    flock 9

    # invalidate stale cache
    if [ -f "${cache_file}" ]; then
      ts=$(stat --format=%Y "${cache_file}")
      now=$(date +%s)
      cache_age=$((now - ts))
      if [ "${cache_age}" -gt "${cache_ttl}" ]; then
        rm -f "${cache_file}"
      fi
    fi

    if [ -f "${cache_file}" ]; then
      # read cache
      if [ -s "${cache_file}" ]; then
        cached_proxy=$(cat "${cache_file}")
        log_debug "using cached proxy: ${cached_proxy} (ttl = $((cache_ttl - cache_age))s)"
        echo "${cached_proxy}"
      else
        log_debug "using cache: no proxy detected (ttl = $((cache_ttl - cache_age))s)"
      fi
    else
      # update cache
      "$@" > "$cache_file" || true
      cat "${cache_file}"
    fi
  ) 9> "${lock_file}"
}

detect_apt_cacher() {
  local ip
  local proxy
  ip="$1"
  proxy="$(proxy_url "$ip" 3142)"
  log_debug "looking for apt-cacher at ${proxy}"
  hit -o "Acquire::http::Proxy::${ip}=DIRECT" "$proxy" >/dev/null 2>&1 || true;
  if [ -s "$output" ] && grep -q -i '<title>Apt-cacher' "$output"; then
    set_proxy "$proxy"
    return 0
  fi
  return 1
}

detect_apt_cacher_ng() {
  local ip
  local proxy
  ip="$1"
  proxy="$(proxy_url "$ip" 3142)"
  log_debug "looking for apt-cacher-ng at ${proxy}"
  if hit -o "Acquire::http::Proxy::${ip}=DIRECT" "$proxy" | grep -q -i 'HTTP.*406'; then
    set_proxy "$proxy"
    return 0
  fi
  return 1
}

detect_approx() {
  local ip
  local proxy
  ip="$1"
  proxy="$(proxy_url "$ip" 9999)"
  log_debug "looking for approx at ${proxy}"
  hit -o "Acquire::http::Proxy::${ip}=DIRECT" "$proxy" >/dev/null 2>&1 || true;
  if [ -s "$output" ] && grep -q -i '<title>approx\s*server</title>' "$output"; then
    set_proxy "$proxy"
    return 0
  fi
  return 1
}

# NOTE: This does NOT check MDNS/DNS-SD (avahi/zeroconf/bonjour) records.
#       If you want that, use squid-deb-proxy-client, which depends on avahi.
detect_squid_deb_proxy() {
  local ip
  local proxy
  ip="$1"
  proxy="$(proxy_url "$ip" 8000)"
  log_debug "looking for squid at ${proxy}"
  if hit -oDebug::acquire::http=1 -o "Acquire::http::Proxy::${ip}=DIRECT" "$proxy" 2>&1 | grep -q 'Via: .*squid-deb-proxy'; then
    set_proxy "$proxy"
    return 0
  fi
  return 1
}

get_search_domains() {
  awk '{ if ($1 == "search") { $1=""; print($0) } }' /etc/resolv.conf 2>/dev/null
}

# NOTE: This does NOT check MDNS/DNS-SD (avahi/zeroconf/bonjour) records.
#       If you want that, use squid-deb-proxy-client, which depends on avahi.
#
# FIXME: if there are multiple matching SRV records, we should make a
#        weighted random choice from the one(s) with the highest priority.
#        For now, we make a uniformly random choice from all records (shuf + exit).
#
# NOTE: We don't check that it "looks like" a known apt proxy (hit + grep -q).
#       This is because
#        1) the other detectors are just GUESSING hosts and ports.
#           You might accidentally run a non-apt-proxy on 127.0.0.1:9999, but
#           you can't accidentally create an _apt_proxy SRV record!
#        2) refactoring the grep -q's out of detect_* is tedious and boring.
#        3) there's no grep -q for squid, which I want to use. ;-)
#
# NOTE: no need for if/then/else and return 0/1 because:
#        * if awk matches something, it prints it and exits zero.
#        * if hostname or apt-helper fail, awk matches nothing, so exits non-zero.
#        * set -e ignores errors from apt-helper (no pipefail) and hostname (no ???).
detect_DNS_SRV_record() {
  local domain
  domain="$(hostname --domain 2>/dev/null)"
  search_domains=$(get_search_domains)
  for d in $domain $search_domains; do
    log_debug "Looking up SRV entry for _apt_proxy._tcp.${d}"
    result=$(
      /usr/lib/apt/apt-helper srv-lookup _apt_proxy._tcp."${d}" 2>/dev/null |
      shuf |
      awk '/^[^#]/{print "http://" $1 ":" $4;found=1;exit}END{exit !found}'
    )
    if [ -n "${result}" ]; then
      set_proxy "${result}"
      return 0
    fi
  done
  return 1
}

detect_avahi_local() {
  local data hostname ip port check
  if ! command -v avahi-browse >/dev/null 2>/dev/null; then
    log_debug "avahi-browse not available; skipping mDNS lookup"
    return 1
  fi

  log_debug "Looking up _apt_proxy._tcp on mDNS (avahi)"

  data=$(avahi-browse --terminate --no-db-lookup --resolve --parsable _apt_proxy._tcp | awk -F ';' '/^=;/ { print($7, $8, $9); exit}')
  if [ -z "${data}" ]; then
    return 1
  fi

  ip=$(echo "${data}" | awk '{print($2)}')
  port=$(echo "${data}" | awk '{print($3)}')

  if [ -n "${AUTO_APT_PROXY_AVAHI_NAME:-}" ]; then
    hostname="$(echo "${data}" | awk '{print($1)}')"
    proxy="http://${hostname}:${port}"
  else
    proxy="$(proxy_url "${ip}" "${port}")"
  fi

  check="$(LANG=C.UTF-8 hit -o "Acquire::http::Proxy::${ip}=DIRECT" "${proxy}" || true)"
  if echo "${check}" | grep -q '111: Connection refused\|101: Network is unreachable'; then
    return 1
  fi

  set_proxy "${proxy}"
}


if command -v ip >/dev/null; then
  ip_tool="ip"
elif busybox ip --help >/dev/null 2>&1; then
  ip_tool="busybox ip"
else
  ip_tool="true"
fi

find_gateway() {
  $ip_tool "$@" route | awk '/^default via/ { print($3) }'
}

resolve_getent() {
  log_debug "Resolving hostname: getent" "$@"
  with_timeout getent "$@" | awk '/[[:blank:]]/ { if ($2 == "STREAM") {print($1)} }' | uniq
}

get_container_host() {
  if [ -f /run/.containerenv ]; then
    # podman
    resolve_getent ahostsv4 host.containers.internal 2>/dev/null || resolve_getent ahostsv6 host.containers.internal 2>/dev/null || true
  elif [ -f /.dockerenv ] || systemd-detect-virt --quiet --container 2>/dev/null; then
    # for any other type of container, look for the gateway
    find_gateway
  fi
}

v4_localhost() {
  echo 127.0.0.1
}

v6_localhost() {
  echo ::1
}

v4_gateway() {
  local ret
  ret=$(find_gateway)
  log_debug_if_present "${ret}" "IPv4 gateway: ${ret}"
  echo "${ret}"
}

v6_gateway() {
  local ret
  ret=$(find_gateway -6)
  log_debug_if_present "${ret}" "IPv6 gateway: ${ret}"
  echo "${ret}"
}

v4_explicit_proxy_etc_hosts() {
  awk '/^[0-9.]+[[:blank:]]/ { for (i=1;i<=NF;i++) { if($i == "apt-proxy") {print($1); exit} } }' /etc/hosts
}

v4_explicit_proxy() {
  local ret
  ret=$(resolve_getent ahostsv4 apt-proxy)
  log_debug_if_present "${ret}" "apt-proxy IPv4: ${ret}"
  echo "${ret}"
}

v6_explicit_proxy_etc_hosts() {
  awk '/^[0-9a-f:]+[[:blank:]]/ { for (i=1;i<=NF;i++) { if($i == "apt-proxy") {print($1); exit} } }' /etc/hosts
}

v6_explicit_proxy() {
  local ret
  ret=$(resolve_getent ahostsv6 apt-proxy)
  log_debug_if_present "${ret}" "apt-proxy IPv6: ${ret}"
  echo "${ret}"
}

v4_addresses() {
  local ret
  ret=$($ip_tool -4 route | awk '{if($10 == "linkdown") {print($9)}}')
  log_debug_if_present "${ret}" "Local IPv4 addessses: ${ret}"
  echo "${ret}"
}

# TODO look for local v6 addessses as well

container_host() {
  local ret
  ret=$(get_container_host)
  log_debug_if_present "${ret}" "Container host: ${ret}"
  echo "${ret}"
}

detect_proxy_at() {
  local ip
  ip="$1"
  detect_apt_cacher_ng "$ip"   && return 0
  detect_approx "$ip"          && return 0
  detect_apt_cacher "$ip"      && return 0
  detect_squid_deb_proxy "$ip" && return 0
  return 1
}

detect_from() {
  for get_ips in "$@"; do
    for ip in $(${get_ips}); do
      if detect_proxy_at "${ip}"; then
        return 0
      fi
    done
  done
  return 1
}

__detect__() {
  # Check for a local running proxy
  # TODO check local IPv6 addresses
  detect_from v4_localhost v4_addresses v6_localhost && return 0

  # For containers, check the host first
  detect_from container_host && return 0

  # Check for explicitly configured proxies
  detect_from v4_explicit_proxy_etc_hosts v6_explicit_proxy_etc_hosts && return 0
  detect_DNS_SRV_record && return 0
  detect_avahi_local && return 0

  # Check special hosts on the network
  detect_from v4_gateway v6_gateway && return 0

  log_debug "no proxy detected"
  return 0
}

detect() {
  if [ -z "${AUTO_APT_PROXY_NO_CACHE:-}" ]; then
    cache __detect__
  else
    __detect__
  fi
}

if [ $# -eq 0 ]; then
  detect
else
  case "$1" in
    ftp://*|http://*|https://*|file://*)
      # APT mode: first argument is an URI
      detect
      ;;
    *)
      # wrapper mode: execute command using the detected proxy
      proxy=$(detect || true)
      if [ -n "$proxy" ]; then
        export http_proxy="$proxy"
        export HTTP_PROXY="$proxy"
      fi
      exec "$@"
  esac
fi
