#!/bin/sh
# Generated by scripts/msb-trust/build.sh; edit scripts/msb-trust/ instead.
# Explicit certificate setup for applications with their own trust stores.
set -eu

SUPPORTED_APPLICATIONS="chrome java"

# Shared application selection, dependency reporting, and command dispatch.
CA_CERT=/.msb/tls/ca.pem

fail() {
    printf 'msb-trust: %s\n' "$*" >&2
    exit 1
}

usage() (
    cat <<'HELP'
Usage: sh msb-trust.sh <application> [application ...]
       sh msb-trust.sh all

Configure application trust stores to trust the sandbox's TLS interception CA.
Name the applications to configure, or use all for supported applications whose
tools are installed. Every selected application is attempted; successful imports
remain in place if another application fails. Missing tools are summarized with
installation instructions and one retry command. Packages are not installed.
Exit status is nonzero if setup fails or an explicitly requested app lacks tools.
With all, missing tools are skipped without making the command fail.

Supported applications:
HELP
    for application in $SUPPORTED_APPLICATIONS; do
        printf '  %s' "$application"
        if command -v "${application}_description" >/dev/null 2>&1; then
            printf '  %s' "$("${application}_description")"
        fi
        printf '\n'
    done
    printf '\nRun sh msb-trust.sh <application> --help for application-specific requirements.\n'
)

require_ca() {
    [ -r "$CA_CERT" ] || fail "no readable interception CA at $CA_CERT; enable TLS interception first"
}

# Compare the first PEM certificate, ignoring surrounding comments and metadata.
certificate_data() {
    awk '
        /-----BEGIN CERTIFICATE-----/ { inside = 1; next }
        /-----END CERTIFICATE-----/ {
            if (inside && length(data)) { print data; found = 1 }
            exit
        }
        inside { gsub(/[[:space:]]/, ""); data = data $0 }
        END { if (!found) exit 1 }
    '
}

application_supported() (
    for candidate in $SUPPORTED_APPLICATIONS; do
        [ "$1" != "$candidate" ] || return 0
    done
    return 1
)

application_missing_tools() (
    missing=
    for tool in $("${1}_tools"); do
        if ! command -v "$tool" >/dev/null 2>&1; then
            missing="${missing:+$missing }$tool"
        fi
    done
    printf '%s\n' "$missing"
)

application_available() (
    if command -v "${1}_available" >/dev/null 2>&1; then
        "${1}_available"
    else
        [ -z "$(application_missing_tools "$1")" ]
    fi
)

unique_words() (
    seen=" "
    for word do
        case "$seen" in
            *" $word "*) continue ;;
        esac
        printf '%s\n' "$word"
        seen="$seen$word "
    done
)

missing_tools() (
    manager=
    for candidate in apt-get dnf; do
        if command -v "$candidate" >/dev/null 2>&1; then
            manager=$candidate
            break
        fi
    done

    printf 'Missing tools:\n'
    for application do
        printf '  %s: %s\n' "$application" "$(application_missing_tools "$application")"
    done

    printf '\nInstall only the tools for applications you use.\n'
    packages=
    unmapped_tools=
    for application do
        dependencies=$(application_missing_tools "$application")
        # Hook output contains fixed command/package names, never user input.
        # shellcheck disable=SC2086
        application_packages=$("${application}_packages" "$manager" $dependencies)
        if [ -n "$application_packages" ]; then
            packages="$packages $application_packages"
        fi
        if command -v "${application}_install_hint" >/dev/null 2>&1; then
            ("${application}_install_hint")
        elif [ -z "$application_packages" ]; then
            unmapped_tools="$unmapped_tools $dependencies"
        fi
    done
    # shellcheck disable=SC2086
    for tool in $(unique_words $unmapped_tools); do
        printf 'Install %s using your distribution package manager.\n' "$tool"
    done

    if [ -n "$packages" ]; then
        printf '\nInstall missing packages as root:\n  '
        case "$manager" in
            apt-get) printf 'apt-get update && apt-get install -y' ;;
            dnf) printf 'dnf install -y' ;;
        esac
        # shellcheck disable=SC2086
        for package in $(unique_words $packages); do
            printf ' %s' "$package"
        done
        printf '\n'
    fi
)

progress() {
    if [ "$batch" = false ]; then
        printf 'msb-trust: %s\n' "$*"
    fi
}

main() {
    [ "$#" -gt 0 ] || { usage >&2; exit 2; }
    if [ "$#" -eq 1 ]; then
        case "$1" in
            -h|--help) usage; return ;;
        esac
    elif [ "$#" -eq 2 ]; then
        case "$2" in
            -h|--help)
                application_supported "$1" || fail "unsupported application: $1"
                "${1}_help"
                return
                ;;
        esac
    fi

    all=false
    if [ "$1" = all ]; then
        [ "$#" -eq 1 ] || { usage >&2; exit 2; }
        all=true
        # The registry contains fixed application names, not user input.
        # shellcheck disable=SC2086
        set -- $SUPPORTED_APPLICATIONS
    fi

    # Validate the whole request before changing any trust store.
    for application do
        if ! application_supported "$application"; then
            usage >&2
            fail "unsupported application: $application"
        fi
    done

    # Requests are validated before splitting and deduplicating these fixed names.
    # shellcheck disable=SC2046
    set -- $(unique_words "$@")
    retry="$*"
    batch=false
    if [ "$all" = true ]; then
        retry=all
        batch=true
    elif [ "$#" -gt 1 ]; then
        batch=true
    fi

    # A missing CA affects the whole explicit request, not an individual app.
    if [ "$all" = false ]; then
        require_ca
    fi

    configured=
    missing=
    failed=
    status=0
    for application do
        if ! application_available "$application"; then
            missing="$missing $application"
            if [ "$all" = false ]; then
                status=1
            fi
            continue
        fi

        require_ca
        # Handlers explicitly guard fallible operations: conditional calls disable
        # errexit. A handler failure must not stop the remaining applications.
        if ("${application}_configure"); then
            configured="$configured $application"
        else
            failed="$failed $application"
            status=1
        fi
    done

    if [ "$batch" = true ]; then
        if [ -z "$configured" ]; then
            printf 'No applications configured.\n'
        else
            printf 'Configured:\n'
            for application in $configured; do
                printf '  %s\n' "$application"
            done
            printf '\nRestart configured applications to use the updated trust stores.\n'
        fi
    fi
    if [ -n "$failed" ]; then
        printf '\nFailed:\n'
        for application in $failed; do
            printf '  %s (see error above)\n' "$application"
        done
    fi
    if [ -n "$missing" ]; then
        printf '\n'
        # shellcheck disable=SC2086
        missing_tools $missing
    fi
    if [ -n "$missing$failed" ]; then
        printf '\nThen rerun:\n  sh msb-trust.sh %s\n' "$retry"
    fi
    return "$status"
}

# Chrome/Chromium

chrome_description() { printf "Import the CA into the current user's Chrome/Chromium NSS store\n"; }
chrome_tools() { printf 'certutil\n'; }
chrome_packages() {
    case "$1" in
        apt-get) printf 'libnss3-tools\n' ;;
        dnf) printf 'nss-tools\n' ;;
    esac
}

chrome_help() {
    cat <<'HELP'
Usage: sh msb-trust.sh chrome

Import the sandbox CA into the current user's Chrome/Chromium NSS store.
Requires certutil (libnss3-tools on Debian/Ubuntu, nss-tools on Fedora).
Close the browser first. Run as the browser's user, with HOME set accordingly.

Uses an existing ~/.pki/nssdb before ~/.local/share/pki/nssdb, matching Chrome.
For a fresh home, creates ~/.pki/nssdb for compatibility with older browsers.
Existing certificates are preserved. Password-protected stores require manual setup.
Use certutil interactively for those stores:
  certutil -A -d sql:/path/to/nssdb -n microsandbox-interception \
    -t 'C,,' -i /.msb/tls/ca.pem
Restart the browser after changing trust.
HELP
}

chrome_configure() {
    command -v certutil >/dev/null 2>&1 || {
        fail "certutil not found"
    }
    case "${HOME:-}" in
        /*) ;;
        *) fail "set HOME to the browser user's absolute home directory" ;;
    esac

    store=$HOME/.pki/nssdb
    if [ ! -d "$store" ] && [ -d "$HOME/.local/share/pki/nssdb" ]; then
        store=$HOME/.local/share/pki/nssdb
    fi
    alias=microsandbox-interception
    progress "configuring $store; use the browser's user and HOME, with the browser closed"

    # Only initialize an empty directory; never reset an existing NSS database.
    if [ ! -f "$store/cert9.db" ]; then
        (umask 077; mkdir -p "$store") || fail "cannot create NSS store at $store"
        contents=$(ls -A "$store") || fail "cannot inspect NSS directory $store"
        [ -z "$contents" ] || fail "NSS directory $store is not empty but has no cert9.db; inspect it before retrying"
        certutil -N -d "sql:$store" --empty-password || fail "cannot initialize NSS store at $store"
    fi
    certificates=$(certutil -L -d "sql:$store") || fail "cannot read NSS store at $store"

    if existing=$(certutil -L -d "sql:$store" -n "$alias" -a 2>/dev/null); then
        expected=$(certificate_data < "$CA_CERT") || fail "cannot read sandbox CA"
        actual=$(printf '%s\n' "$existing" | certificate_data) || fail "cannot read existing certificate"
        [ "$actual" = "$expected" ] || fail "alias $alias contains a different certificate in $store; review and remove that alias explicitly before retrying"

        trust=$(printf '%s\n' "$certificates" | awk -v name="$alias" '$1 == name { print $NF }') || fail "cannot read NSS trust flags"
        case "${trust%%,*}" in
            *C*) progress 'Chrome/Chromium already trusts this sandbox CA'; return ;;
        esac
        certutil -M -d "sql:$store" -n "$alias" -t "C,," -f /dev/null || fail "cannot update NSS trust; check write permissions. For password-protected stores, run: sh msb-trust.sh chrome --help"
    else
        certutil -A -d "sql:$store" -n "$alias" -t "C,," -i "$CA_CERT" -f /dev/null || fail "Chrome CA import failed; check write permissions. For password-protected stores, run: sh msb-trust.sh chrome --help"
    fi

    progress "imported sandbox CA into $store; restart Chrome/Chromium to use it"
}

# Java

java_description() { printf "Import the CA into the JDK's default trust store\n"; }
java_tools() { printf 'keytool\n'; }
java_packages() { :; } # The user must select the JDK their application uses.

java_available() {
    # An explicit JAVA_HOME is configuration to validate, not an absent app.
    [ -n "${JAVA_HOME:-}" ] || command -v keytool >/dev/null 2>&1
}

java_install_hint() {
    printf 'For Java, install a JDK with keytool, or set JAVA_HOME to your existing JDK.\n'
}

java_help() {
    cat <<'HELP'
Usage: sh msb-trust.sh java

Import the sandbox's TLS interception CA into the JDK's default trust store.
Uses $JAVA_HOME/bin/keytool when JAVA_HOME is set, otherwise keytool on PATH.
Requires write access to the store. Existing certificates are preserved.

Set MSB_JAVA_STORE_PASSWORD if the store password is not "changeit".
Applications with a custom trust store need to import the CA into that store.

For a read-only JDK:
  1. Copy the JDK's lib/security/cacerts to a writable file.
  2. Import the CA into that copy:
     keytool -importcert -noprompt -alias microsandbox-interception \
       -file /.msb/tls/ca.pem -keystore /path/to/cacerts
  3. Set -Djavax.net.ssl.trustStore=/path/to/cacerts when launching Java.
     If using JAVA_TOOL_OPTIONS, keep any existing options and configure it
     for future sandbox execs, not just the setup script's shell.

Restart Java processes and Gradle daemons after changing trust.
HELP
}

java_configure() {
    if [ -n "${JAVA_HOME:-}" ]; then
        keytool=$JAVA_HOME/bin/keytool
        [ -x "$keytool" ] || fail "keytool is not executable at $keytool; set JAVA_HOME to your application's JDK"
    else
        keytool=$(command -v keytool) || {
            fail "keytool not found"
        }
    fi

    # Pass the password through the environment, not the process arguments.
    MSB_JAVA_STORE_PASSWORD=${MSB_JAVA_STORE_PASSWORD-changeit}
    export MSB_JAVA_STORE_PASSWORD
    alias=microsandbox-interception

    if existing=$("$keytool" -exportcert -rfc -cacerts -alias "$alias" \
        -storepass:env MSB_JAVA_STORE_PASSWORD 2>/dev/null); then
        expected=$(certificate_data < "$CA_CERT") || fail "cannot read sandbox CA"
        actual=$(printf '%s\n' "$existing" | certificate_data) || fail "cannot read existing certificate"
        if [ "$actual" = "$expected" ]; then
            progress 'Java already trusts this sandbox CA'
            return
        fi

        fail "alias $alias contains a different certificate; review it with keytool -list -cacerts -alias $alias, then remove that alias explicitly before retrying"
    fi

    if ! import_output=$("$keytool" -importcert -noprompt -cacerts -alias "$alias" \
        -file "$CA_CERT" -storepass:env MSB_JAVA_STORE_PASSWORD 2>&1); then
        printf '%s\n' "$import_output" >&2
        fail "Java CA import failed; check write permissions or set MSB_JAVA_STORE_PASSWORD for a non-default password. For read-only or custom stores, run: sh msb-trust.sh java --help"
    fi

    progress 'imported sandbox CA into the JDK trust store; restart existing Java processes to use it'
}

main "$@"
