#!/usr/bin/env bash
# osdl-push — Air-gap ISO transfer from jumphost/bastion → isolated host
# Channel: os.webservice.digital
#
# Transfers an ISO plus verification material (checksum file, optional GPG
# signature / public key) over SCP/SFTP. Supports ProxyJump through a bastion.
# Never contacts untrusted binary hosts for the payload itself — you supply
# the local ISO (typically obtained on the connected jumphost via osdl get).
set -euo pipefail

OSDL_PUSH_VERSION="1.2.0"
OSDL_DEFAULT_API="${OSDL_API:-https://os.webservice.digital/api/v1/latest.json}"

die()  { printf 'osdl-push: error: %s\n' "$*" >&2; exit 1; }
warn() { printf 'osdl-push: warning: %s\n' "$*" >&2; }
info() { printf 'osdl-push: %s\n' "$*" >&2; }

need_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

usage() {
  cat <<'EOF'
osdl-push — push ISO + checksum/key material to an air-gapped host (SCP)

USAGE
  osdl-push --host <user@host|host> --iso <path> [options]

REQUIRED
  --host <target>       SSH target: user@hostname, user@ip, or host (uses $USER)
  --iso <path>          Local ISO (or image) file on the jumphost

OPTIONS
  --remote-dir <path>   Remote destination directory (default: ~/os-media)
  --port <n>            SSH port (default: 22)
  --jump <bastion>      ProxyJump / bastion hop (user@bastion)
  --identity <key>      SSH private key path (-i)
  --checksum <file>     Local checksum file (SHA256SUMS / *.sha256 / CHECKSUM)
  --sig <file>          Detached GPG signature for the checksum file
  --key <file>          Public keyring / key file to copy (repeatable)
  --distro <id>         Gateway distro id — auto-fetch official checksum URL when online
  --sha256 <hex>        Explicit SHA256 if no checksum file (writes remote .sha256)
  --sha512 <hex>        Explicit SHA512 if no checksum file
  --name <filename>     Remote ISO basename (default: local basename)
  --print               Print scp/ssh plan only; do not transfer
  --no-manifest         Skip generating remote MANIFEST.txt
  --strict-host-key     Disable StrictHostKeyChecking=accept-new (require known_hosts)
  -h, --help            This help

ENVIRONMENT
  OSDL_API              Metadata API (for --distro checksum discovery)
  OSDL_SSH              Extra ssh/scp options (space-separated)

EXAMPLES
  # Connected jumphost → air-gapped node
  osdl get debian --verify -o /var/tmp/debian.iso
  osdl-push --host ops@10.10.50.20 --iso /var/tmp/debian.iso --distro debian

  # Via bastion / jump host
  osdl-push --host airgap@192.168.99.10 --jump bastion@edge.example \
    --iso ./rocky-minimal.iso --checksum ./CHECKSUM --sig ./CHECKSUM.asc \
    --key ./RPM-GPG-KEY-rockyofficial --remote-dir /srv/iso

  # Print-only rehearsal
  osdl-push --host airgap@host --iso ./tails.iso --sha256 <hex> --print

SECURITY
  • Prefer verifying the ISO on the jumphost before push (osdl verify / sha*sum).
  • Copy official checksum + GPG material; hash-only is necessary but not sufficient.
  • Review --print output before first use in production air-gap corridors.
EOF
}

HOST=""
ISO=""
REMOTE_DIR="~/os-media"
PORT="22"
JUMP=""
IDENTITY=""
CHECKSUM_FILE=""
SIG_FILE=""
KEYS=()
DISTRO=""
SHA256=""
SHA512=""
REMOTE_NAME=""
DO_PRINT=0
NO_MANIFEST=0
STRICT_HK=0
API_URL="$OSDL_DEFAULT_API"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --host) HOST="${2:-}"; shift 2 ;;
    --iso) ISO="${2:-}"; shift 2 ;;
    --remote-dir) REMOTE_DIR="${2:-}"; shift 2 ;;
    --port) PORT="${2:-}"; shift 2 ;;
    --jump) JUMP="${2:-}"; shift 2 ;;
    --identity) IDENTITY="${2:-}"; shift 2 ;;
    --checksum) CHECKSUM_FILE="${2:-}"; shift 2 ;;
    --sig) SIG_FILE="${2:-}"; shift 2 ;;
    --key) KEYS+=("${2:-}"); shift 2 ;;
    --distro) DISTRO="${2:-}"; shift 2 ;;
    --sha256) SHA256="${2:-}"; shift 2 ;;
    --sha512) SHA512="${2:-}"; shift 2 ;;
    --name) REMOTE_NAME="${2:-}"; shift 2 ;;
    --print) DO_PRINT=1; shift ;;
    --no-manifest) NO_MANIFEST=1; shift ;;
    --strict-host-key) STRICT_HK=1; shift ;;
    --api) API_URL="${2:-}"; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) die "unknown option: $1 (try --help)" ;;
  esac
done

[[ -n "$HOST" ]] || die "--host is required"
[[ -n "$ISO" ]] || die "--iso is required"
[[ -f "$ISO" ]] || die "ISO not found: $ISO"

# Normalize host (allow bare IP/hostname)
if [[ "$HOST" != *@* ]]; then
  HOST="${USER:-root}@$HOST"
fi

REMOTE_NAME="${REMOTE_NAME:-$(basename "$ISO")}"
need_cmd ssh
need_cmd scp

# Optional: discover checksum URL from gateway when --distro set
fetch_distro_checksum_urls() {
  local distro="$1"
  command -v curl >/dev/null 2>&1 || return 1
  command -v python3 >/dev/null 2>&1 || return 1
  local json
  json=$(curl -fsSL -A "osdl-push/${OSDL_PUSH_VERSION}" --connect-timeout 10 --max-time 45 \
    -H 'Accept: application/json' "$API_URL" 2>/dev/null) || return 1
  OSDL_JSON="$json" OSDL_DISTRO="$distro" python3 - <<'PY'
import json, os, sys
data = json.loads(os.environ["OSDL_JSON"])
needle = os.environ["OSDL_DISTRO"].lower()
d = None
for x in data.get("distributions", []):
    if x.get("id", "").lower() == needle:
        d = x; break
    for a in x.get("aliases") or []:
        if str(a).lower() == needle:
            d = x; break
if not d:
    sys.exit(2)
cs = d.get("checksums") or {}
p = d.get("primary_iso") or {}
print(f"SHA256SUMS_URL={cs.get('sha256sums_url') or ''}")
print(f"SHA512SUMS_URL={cs.get('sha512sums_url') or ''}")
print(f"GPG_URL={cs.get('sha256sums_gpg_url') or ''}")
print(f"ISO_SHA256={p.get('sha256') or ''}")
print(f"ISO_SHA512={p.get('sha512') or ''}")
print(f"ISO_NAME={p.get('filename') or ''}")
PY
}

STAGE=$(mktemp -d)
trap 'rm -rf "$STAGE"' EXIT
BUNDLE="$STAGE/bundle"
mkdir -p "$BUNDLE/keys"

info "staging bundle in $BUNDLE"
cp -a "$ISO" "$BUNDLE/$REMOTE_NAME"

# Checksum file priority: explicit file → distro fetch → explicit hex
if [[ -n "$CHECKSUM_FILE" ]]; then
  [[ -f "$CHECKSUM_FILE" ]] || die "checksum file not found: $CHECKSUM_FILE"
  cp -a "$CHECKSUM_FILE" "$BUNDLE/$(basename "$CHECKSUM_FILE")"
  CHECKSUM_BASENAME=$(basename "$CHECKSUM_FILE")
elif [[ -n "$DISTRO" ]]; then
  info "resolving official checksum metadata for distro=$DISTRO"
  if evals=$(fetch_distro_checksum_urls "$DISTRO"); then
    eval "$evals"
    if [[ -z "$SHA256" && -n "${ISO_SHA256:-}" ]]; then SHA256="$ISO_SHA256"; fi
    if [[ -z "$SHA512" && -n "${ISO_SHA512:-}" ]]; then SHA512="$ISO_SHA512"; fi
    # Prefer downloading official checksum listing when URL available
    CS_URL="${SHA256SUMS_URL:-${SHA512SUMS_URL:-}}"
    if [[ -n "$CS_URL" ]] && command -v curl >/dev/null 2>&1; then
      CS_NAME=$(basename "${CS_URL%%\?*}")
      [[ -n "$CS_NAME" && "$CS_NAME" != "/" ]] || CS_NAME="SHA256SUMS"
      if curl -fsSL -A "osdl-push/${OSDL_PUSH_VERSION}" --connect-timeout 10 --max-time 60 \
          "$CS_URL" -o "$BUNDLE/$CS_NAME"; then
        CHECKSUM_BASENAME="$CS_NAME"
        info "fetched checksum listing: $CS_NAME"
      else
        warn "could not fetch checksum URL; will emit local digest file if hash known"
      fi
    fi
    if [[ -n "${GPG_URL:-}" ]] && [[ -z "$SIG_FILE" ]] && command -v curl >/dev/null 2>&1; then
      SIG_NAME=$(basename "${GPG_URL%%\?*}")
      if curl -fsSL -A "osdl-push/${OSDL_PUSH_VERSION}" --connect-timeout 10 --max-time 60 \
          "$GPG_URL" -o "$BUNDLE/$SIG_NAME" 2>/dev/null; then
        SIG_FILE="$BUNDLE/$SIG_NAME"
        info "fetched signature/key material: $SIG_NAME"
      fi
    fi
  else
    warn "metadata lookup failed for --distro $DISTRO (offline?); continuing with local material only"
  fi
fi

if [[ -z "${CHECKSUM_BASENAME:-}" ]]; then
  if [[ -n "$SHA256" ]]; then
    CHECKSUM_BASENAME="${REMOTE_NAME}.sha256"
    # GNU coreutils style: HASH *filename
    printf '%s *%s\n' "$SHA256" "$REMOTE_NAME" >"$BUNDLE/$CHECKSUM_BASENAME"
    info "wrote local SHA256 digest file: $CHECKSUM_BASENAME"
  elif [[ -n "$SHA512" ]]; then
    CHECKSUM_BASENAME="${REMOTE_NAME}.sha512"
    printf '%s *%s\n' "$SHA512" "$REMOTE_NAME" >"$BUNDLE/$CHECKSUM_BASENAME"
    info "wrote local SHA512 digest file: $CHECKSUM_BASENAME"
  else
    # Last resort: compute on jumphost
    if command -v sha256sum >/dev/null 2>&1; then
      CHECKSUM_BASENAME="${REMOTE_NAME}.sha256"
      (cd "$BUNDLE" && sha256sum -b "$REMOTE_NAME" >"$CHECKSUM_BASENAME")
      info "computed SHA256 on jumphost → $CHECKSUM_BASENAME"
    else
      die "no checksum file/hash provided and sha256sum unavailable"
    fi
  fi
fi

if [[ -n "$SIG_FILE" && -f "$SIG_FILE" ]]; then
  cp -a "$SIG_FILE" "$BUNDLE/$(basename "$SIG_FILE")"
fi

for k in "${KEYS[@]+"${KEYS[@]}"}"; do
  [[ -f "$k" ]] || die "key file not found: $k"
  cp -a "$k" "$BUNDLE/keys/$(basename "$k")"
done

# MANIFEST for the air-gapped operator
if [[ "$NO_MANIFEST" -eq 0 ]]; then
  cat >"$BUNDLE/MANIFEST.txt" <<EOF
OSDL AIR-GAP TRANSFER MANIFEST
==============================
Generated (UTC): $(date -u +%Y-%m-%dT%H:%M:%SZ)
Channel:         os.webservice.digital
Tool:            osdl-push ${OSDL_PUSH_VERSION}
Source host:     $(hostname -f 2>/dev/null || hostname)
Target host:     ${HOST}
Remote dir:      ${REMOTE_DIR}
ISO filename:    ${REMOTE_NAME}
Checksum file:   ${CHECKSUM_BASENAME:-none}
Distro hint:     ${DISTRO:-n/a}

VERIFY ON AIR-GAPPED HOST
-------------------------
cd ${REMOTE_DIR}
# Preferred when official SUMS file present:
sha256sum -c ${CHECKSUM_BASENAME} 2>/dev/null || sha512sum -c ${CHECKSUM_BASENAME}

# If digest file is single-line HASH *file:
#   sha256sum -c ${REMOTE_NAME}.sha256

# GPG (when signature + key present):
#   gpg --import keys/*
#   gpg --verify <checksum.sig> <checksum-file>

OPERATOR NOTES
--------------
1. Confirm ISO bit-identity with the published digest before imaging media.
2. Prefer GPG verification of the official checksum file when keys are available.
3. This bundle was assembled on a jumphost; treat the transport path as trusted.
EOF
fi

# SSH/SCP option assembly
SSH_OPTS=(-p "$PORT")
SCP_OPTS=(-P "$PORT" -p)
if [[ "$STRICT_HK" -eq 0 ]]; then
  SSH_OPTS+=(-o "StrictHostKeyChecking=accept-new")
  SCP_OPTS+=(-o "StrictHostKeyChecking=accept-new")
fi
if [[ -n "$JUMP" ]]; then
  SSH_OPTS+=(-J "$JUMP")
  SCP_OPTS+=(-J "$JUMP")
fi
if [[ -n "$IDENTITY" ]]; then
  [[ -f "$IDENTITY" ]] || die "identity file not found: $IDENTITY"
  SSH_OPTS+=(-i "$IDENTITY")
  SCP_OPTS+=(-i "$IDENTITY")
fi
# shellcheck disable=SC2206
EXTRA=( ${OSDL_SSH:-} )
SSH_OPTS+=("${EXTRA[@]+"${EXTRA[@]}"}")
SCP_OPTS+=("${EXTRA[@]+"${EXTRA[@]}"}")

REMOTE_DIR_EXPANDED="$REMOTE_DIR"

plan() {
  echo "# osdl-push plan (print-only)"
  echo "ssh ${SSH_OPTS[*]} $(printf '%q' "$HOST") mkdir -p $(printf '%q' "$REMOTE_DIR_EXPANDED")"
  echo "scp ${SCP_OPTS[*]} -r $(printf '%q' "$BUNDLE")/. $(printf '%q' "${HOST}:${REMOTE_DIR_EXPANDED}/")"
  echo "# then on remote:"
  echo "#   cd ${REMOTE_DIR_EXPANDED} && sha256sum -c ${CHECKSUM_BASENAME}"
}

if [[ "$DO_PRINT" -eq 1 ]]; then
  plan
  exit 0
fi

info "ensuring remote directory: $REMOTE_DIR_EXPANDED"
ssh "${SSH_OPTS[@]}" "$HOST" "mkdir -p $(printf '%q' "$REMOTE_DIR_EXPANDED")"

info "transferring bundle → ${HOST}:${REMOTE_DIR_EXPANDED}/"
# Copy contents (not nested bundle dir name)
scp "${SCP_OPTS[@]}" -r "$BUNDLE"/. "${HOST}:${REMOTE_DIR_EXPANDED}/"

info "transfer complete"
info "remote verify hint: ssh ${HOST} 'cd ${REMOTE_DIR_EXPANDED} && sha256sum -c ${CHECKSUM_BASENAME} || sha512sum -c ${CHECKSUM_BASENAME}'"
printf 'osdl-push: OK  bundle delivered to %s:%s\n' "$HOST" "$REMOTE_DIR_EXPANDED"
