# Author: Christophe Casalegno / Brain 0verride
# Contact: christophe.casalegno@scalarx.com
# boap: Brain OVH Api Player
# Version 1.1.5
#
# Copyright (c) 2020 Christophe Casalegno
#
# 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 <https://www.gnu.org/licenses/>
#
# The license is available on this server here:
# https://www.christophe-casalegno.com/licences/gpl-3.0.txt

set -o pipefail

CONFIG_FILE="${BOAP_CONFIG:-./secret.cfg}"
OVH_API_BASE_FROM_ENV="${OVH_API_BASE-}"

if [[ ! -r "$CONFIG_FILE" ]]; then
    echo "Error: config file not found or unreadable: $CONFIG_FILE" >&2
    exit 1
fi

# secret.cfg format:
# CONSUMER_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# APP_KEY="xxxxxxxxxxxxxxxx"
# APP_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Optional endpoint override:
# OVH_API_BASE="https://eu.api.ovh.com/1.0"
# Other common endpoints:
# OVH Europe legacy/default wrapper endpoint: https://eu.api.ovh.com/1.0
# OVH Europe current v1 endpoint:             https://eu.api.ovh.com/v1
# OVH Europe old alias:                       https://api.ovh.com/1.0
# OVH Canada/North-America:                   https://ca.api.ovh.com/1.0
# OVH US:                                     https://api.us.ovhcloud.com/1.0
# shellcheck disable=SC1090

source "$CONFIG_FILE"

# Let an inline environment override win over secret.cfg, useful for endpoint tests.
if [[ -n "$OVH_API_BASE_FROM_ENV" ]]; then
    OVH_API_BASE="$OVH_API_BASE_FROM_ENV"
fi

function trim_var()
{
    local name value
    name="$1"
    value="${!name-}"
    # Credentials generated by OVHcloud are opaque ASCII tokens. CRLF or copied spaces
    # make X-Ovh-Application invalid, so normalise only surrounding whitespace/CR.
    value="${value//$'\r'/}"
    value="${value//$'\n'/}"
    value="${value#"${value%%[![:space:]]*}"}"
    value="${value%"${value##*[![:space:]]}"}"
    printf -v "$name" '%s' "$value"
}

trim_var CONSUMER_KEY
trim_var APP_KEY
trim_var APP_SECRET

: "${CONSUMER_KEY:?Missing CONSUMER_KEY in $CONFIG_FILE}"
: "${APP_KEY:?Missing APP_KEY in $CONFIG_FILE}"
: "${APP_SECRET:?Missing APP_SECRET in $CONFIG_FILE}"

# Keep the same default family as the official wrappers for endpoint='ovh-eu'.
# You can still force /v1 from secret.cfg or the environment.

OVH_API_BASE="${OVH_API_BASE:-https://eu.api.ovh.com/1.0}"
OVH_API_BASE="${OVH_API_BASE%/}"

CONTENT_TYPE='Content-Type:application/json;charset=utf-8'
ACCEPT_TYPE='Accept:application/json;charset=utf-8'
CURL_CONNECT_TIMEOUT="${BOAP_CONNECT_TIMEOUT:-5}"
CURL_MAX_TIME="${BOAP_MAX_TIME:-30}"

if command -v python3 >/dev/null 2>&1; then
    PYTHON_BIN="python3"
elif command -v python >/dev/null 2>&1; then
    PYTHON_BIN="python"
else
    echo "Error: python3 or python is required" >&2
    exit 1
fi

function json_pretty()
{
    "$PYTHON_BIN" -c '
from __future__ import print_function
import json, sys
raw = sys.stdin.read()
if not raw.strip():
    sys.exit(0)
try:
    obj = json.loads(raw)
    print(json.dumps(obj, indent=2, ensure_ascii=False))
except Exception:
    sys.stdout.write(raw)
'
}

function json_get_field()
{
    local field
    field="$1"
    "$PYTHON_BIN" -c '
from __future__ import print_function
import json, sys
field = sys.argv[1]
raw = sys.stdin.read()
if not raw.strip():
    sys.exit(0)
try:
    obj = json.loads(raw)
except Exception:
    sys.exit(0)
if isinstance(obj, dict) and field in obj:
    value = obj[field]
    if value is None:
        sys.exit(0)
    print(value)
' "$field"
}

function json_lines()
{
    "$PYTHON_BIN" -c '
from __future__ import print_function
import json, sys
try:
    string_types = (basestring,)
except NameError:
    string_types = (str,)
raw = sys.stdin.read()
if not raw.strip():
    sys.exit(0)
obj = json.loads(raw)
if isinstance(obj, list):
    for item in obj:
        if isinstance(item, string_types) or isinstance(item, (int, float, bool)) or item is None:
            print(item)
        else:
            print(json.dumps(item, ensure_ascii=False))
elif isinstance(obj, string_types):
    print(obj)
else:
    print(json.dumps(obj, indent=2, ensure_ascii=False))
'
}

function json_scalar()
{
    "$PYTHON_BIN" -c '
from __future__ import print_function
import json, sys
try:
    string_types = (basestring,)
except NameError:
    string_types = (str,)
raw = sys.stdin.read()
if not raw.strip():
    sys.exit(0)
obj = json.loads(raw)
if isinstance(obj, string_types):
    sys.stdout.write(obj)
elif isinstance(obj, list):
    for item in obj:
        if isinstance(item, string_types) or isinstance(item, (int, float, bool)) or item is None:
            print(item)
        else:
            print(json.dumps(item, ensure_ascii=False))
else:
    print(json.dumps(obj, indent=2, ensure_ascii=False))
'
}

function json_escape_string()
{
    "$PYTHON_BIN" -c '
from __future__ import print_function
import json, sys
value = sys.argv[1]
sys.stdout.write(json.dumps(value))
' "$1"
}

function urlencode_segment()
{
    "$PYTHON_BIN" -c '
from __future__ import print_function
import sys
try:
    from urllib.parse import quote
except ImportError:
    from urllib import quote
value = sys.argv[1]
if sys.version_info[0] < 3:
    value = value.encode("utf-8")
sys.stdout.write(quote(value, safe=""))
' "$1"
}

function require_arg()
{
    local value="$1"
    local usage="$2"
    if [[ -z "$value" ]]; then
        echo "Usage: $usage" >&2
        exit 2
    fi
}

function api_request()
{
    local req_type endpoint data query timestamp sig_key the_sig http_code body_file curl_args curl_rc

    req_type="${1:?Missing HTTP method}"
    endpoint="${2:?Missing endpoint}"
    data="${3-}"
    req_type="${req_type^^}"

    if [[ "$endpoint" != /* ]]; then
        echo "Error: endpoint must start with /" >&2
        return 2
    fi

    query="${OVH_API_BASE}${endpoint}"

    timestamp="$(curl -fsS --connect-timeout "$CURL_CONNECT_TIMEOUT" --max-time "$CURL_MAX_TIME" "${OVH_API_BASE}/auth/time")" || {
        echo "Error: unable to retrieve OVH API timestamp from ${OVH_API_BASE}/auth/time" >&2
        return 1
    }

    sig_key="${APP_SECRET}+${CONSUMER_KEY}+${req_type}+${query}+${data}+${timestamp}"
    the_sig='$1$'"$(printf '%s' "$sig_key" | sha1sum | awk '{print $1}')"

    body_file="$(mktemp)" || return 1

    curl_args=(
        -sS
        --connect-timeout "$CURL_CONNECT_TIMEOUT"
        --max-time "$CURL_MAX_TIME"
        --request "$req_type"
        --url "$query"
        --header "$ACCEPT_TYPE"
        --header "X-Ovh-Application:${APP_KEY}"
        --header "X-Ovh-Consumer:${CONSUMER_KEY}"
        --header "X-Ovh-Timestamp:${timestamp}"
        --header "X-Ovh-Signature:${the_sig}"
        --output "$body_file"
        --write-out '%{http_code}'
    )

    # Mimic the official wrappers: no JSON Content-Type and no request body for
    # bodyless calls. The signed body remains the empty string.
    case "$req_type" in
        GET|DELETE)
            ;;
        POST|PUT|PATCH)
            if [[ -n "$data" ]]; then
                curl_args+=(--header "$CONTENT_TYPE" --data-binary "$data")
            fi
            ;;
        *)
            echo "Error: unsupported HTTP method: $req_type" >&2
            rm -f "$body_file"
            return 2
            ;;
    esac

    if [[ "${BOAP_DEBUG:-0}" == "1" ]]; then
        echo "DEBUG: ${req_type} ${query}" >&2
    fi

    http_code="$(curl "${curl_args[@]}")"
    curl_rc=$?
    if [[ $curl_rc -ne 0 ]]; then
        rm -f "$body_file"
        return "$curl_rc"
    fi

    if [[ ! "$http_code" =~ ^[0-9]{3}$ ]]; then
        echo "Error: invalid HTTP status returned by curl: $http_code" >&2
        cat "$body_file" >&2
        rm -f "$body_file"
        return 1
    fi

    if (( http_code < 200 || http_code >= 300 )); then
        echo "HTTP ${http_code} ${req_type} ${endpoint}" >&2
        json_pretty < "$body_file" >&2
        if grep -q '"errorCode"[[:space:]]*:[[:space:]]*"INVALID_KEY"' "$body_file" 2>/dev/null; then
            cat >&2 <<'HINT'
Hint: OVHcloud says the Application Key is invalid for this API endpoint.
Run: ./boap.sh diag /dedicated/server
If every endpoint returns INVALID_KEY, rotate/recreate AK/AS/CK.
HINT
        fi
        rm -f "$body_file"
        return 1
    fi

    cat "$body_file"
    rm -f "$body_file"
}

function api_pretty()
{
    local raw rc
    raw="$(api_request "$@")"
    rc=$?
    if [[ $rc -ne 0 ]]; then
        return "$rc"
    fi
    printf '%s' "$raw" | json_pretty
}

function api_lines()
{
    local raw rc
    raw="$(api_request "$@")"
    rc=$?
    if [[ $rc -ne 0 ]]; then
        return "$rc"
    fi
    printf '%s' "$raw" | json_lines
}

function api_scalar()
{
    local raw rc
    raw="$(api_request "$@")"
    rc=$?
    if [[ $rc -ne 0 ]]; then
        return "$rc"
    fi
    printf '%s' "$raw" | json_scalar
}

function answer()
{
    local answer_type="$1"
    case "$answer_type" in
        notexist)
            echo "Sorry, this option doesn't exist. Use ./boap.sh help to print help"
            ;;
    esac
}

function ip_list()
{
    api_lines "GET" "/ip"
}

function ip_info()
{
    require_arg "${OPTION2:-}" "./boap.sh ip_info {IP address or CIDR block}"
    local ip_block firewalled_ip mitigation_ip ip ip_enc
    ip_block="$(urlencode_segment "$OPTION2")"

    echo "IP: ${OPTION2} informations"
    echo '-----------------------------------------'
    api_pretty "GET" "/ip/${ip_block}"
    echo ''

    echo "Reverse list for: ${OPTION2}"
    echo '-----------------------------------------'
    api_pretty "GET" "/ip/${ip_block}/reverse"
    echo ''

    echo "Phishing list for: ${OPTION2}"
    echo '-----------------------------------------'
    api_pretty "GET" "/ip/${ip_block}/phishing"
    echo ''

    echo "Anti-hack list for: ${OPTION2}"
    echo '-----------------------------------------'
    api_pretty "GET" "/ip/${ip_block}/antihack"
    echo ''

    echo "Anti-spam list for: ${OPTION2}"
    echo '-----------------------------------------'
    api_pretty "GET" "/ip/${ip_block}/spam"
    echo ''

    echo "Firewall list for: ${OPTION2}"
    echo '-----------------------------------------'
    firewalled_ip="$(api_lines "GET" "/ip/${ip_block}/firewall")" || return 1
    for ip in $firewalled_ip; do
        ip_enc="$(urlencode_segment "$ip")"
        api_pretty "GET" "/ip/${ip_block}/firewall/${ip_enc}"
    done
    echo ''

    echo "Mitigation list (Anti-DDOS) for: ${OPTION2}"
    echo '-----------------------------------------'
    mitigation_ip="$(api_lines "GET" "/ip/${ip_block}/mitigation")" || return 1
    for ip in $mitigation_ip; do
        ip_enc="$(urlencode_segment "$ip")"
        api_pretty "GET" "/ip/${ip_block}/mitigation/${ip_enc}"
    done
    echo ''
}

function bill_list()
{
    api_lines "GET" "/me/bill"
}

function bill_infos()
{
    require_arg "${OPTION2:-}" "./boap.sh bill_infos {invoice reference}"
    local bill_ref
    bill_ref="$(urlencode_segment "$OPTION2")"
    api_pretty "GET" "/me/bill/${bill_ref}" | grep -v -i "url\|password\|text"
}

function servers_list()
{
    api_lines "GET" "/dedicated/server"
}

function server_info()
{
    require_arg "${OPTION2:-}" "./boap.sh server_info {servername}"
    local server inter_ids id id_enc
    server="$(urlencode_segment "$OPTION2")"

    api_pretty "GET" "/dedicated/server/${server}"
    api_pretty "GET" "/dedicated/server/${server}/ips"
    api_pretty "GET" "/dedicated/server/${server}/serviceInfos"

    inter_ids="$(api_lines "GET" "/dedicated/server/${server}/intervention")" || return 1
    inter_ids="$(printf '%s\n' "$inter_ids" | head -3)"
    for id in $inter_ids; do
        id_enc="$(urlencode_segment "$id")"
        echo "$id"
        api_pretty "GET" "/dedicated/server/${server}/intervention/${id_enc}"
    done
}

function server_reboot()
{
    require_arg "${OPTION2:-}" "./boap.sh server_reboot {servername}"
    local server
    server="$(urlencode_segment "$OPTION2")"
    api_pretty "POST" "/dedicated/server/${server}/reboot" ""
}

function zone_export()
{
    require_arg "${OPTION2:-}" "./boap.sh zone_export {domain name}"
    local zone
    zone="$(urlencode_segment "$OPTION2")"
    api_scalar "GET" "/domain/zone/${zone}/export"
}

function nasha_list()
{
    api_lines "GET" "/dedicated/nasha"
}

function nasha_info()
{
    require_arg "${OPTION2:-}" "./boap.sh nasha_info {zpool name}"
    local nasha partition_list counter_part part part_enc
    nasha="$(urlencode_segment "$OPTION2")"

    api_pretty "GET" "/dedicated/nasha/${nasha}"

    partition_list="$(api_lines "GET" "/dedicated/nasha/${nasha}/partition")" || return 1
    counter_part=1
    for part in $partition_list; do
        part_enc="$(urlencode_segment "$part")"
        echo "Partition $counter_part: $part"
        api_pretty "GET" "/dedicated/nasha/${nasha}/partition/${part_enc}"
        echo "Access list:"
        api_pretty "GET" "/dedicated/nasha/${nasha}/partition/${part_enc}/access"
        ((counter_part++))
    done

    api_pretty "GET" "/dedicated/nasha/${nasha}/serviceInfos"
}

function lb_list()
{
    api_lines "GET" "/ipLoadbalancing"
}

function lb_info()
{
    require_arg "${OPTION2:-}" "./boap.sh lb_info {lb id}"
    local lb
    lb="$(urlencode_segment "$OPTION2")"
    api_pretty "GET" "/ipLoadbalancing/${lb}"
    api_pretty "GET" "/ipLoadbalancing/${lb}/status"
    api_pretty "GET" "/ipLoadbalancing/${lb}/serviceInfos"
}

function lb_farms()
{
    require_arg "${OPTION2:-}" "./boap.sh lb_farms {lb id}"
    local lb httpfarm_list tcpfarm_list farm farm_enc
    lb="$(urlencode_segment "$OPTION2")"

    echo "------------ HTTP FARMS ------------"
    httpfarm_list="$(api_lines "GET" "/ipLoadbalancing/${lb}/http/farm")" || return 1
    for farm in $httpfarm_list; do
        farm_enc="$(urlencode_segment "$farm")"
        api_pretty "GET" "/ipLoadbalancing/${lb}/http/farm/${farm_enc}"
    done

    echo "----------- TCP FARMS -------------"
    tcpfarm_list="$(api_lines "GET" "/ipLoadbalancing/${lb}/tcp/farm")" || return 1
    for farm in $tcpfarm_list; do
        farm_enc="$(urlencode_segment "$farm")"
        api_pretty "GET" "/ipLoadbalancing/${lb}/tcp/farm/${farm_enc}"
    done
}

function lb_frontends()
{
    require_arg "${OPTION2:-}" "./boap.sh lb_frontends {lb id}"
    local lb front_list front front_enc
    lb="$(urlencode_segment "$OPTION2")"

    front_list="$(api_lines "GET" "/ipLoadbalancing/${lb}/http/frontend")" || return 1
    for front in $front_list; do
        front_enc="$(urlencode_segment "$front")"
        api_pretty "GET" "/ipLoadbalancing/${lb}/http/frontend/${front_enc}"
    done
}

function status()
{
    case "${OPTION2:-}" in
        planned|Planned|p|P)
            api_pretty "GET" "/status/task?status=planned"
            ;;
        inprogress|Inprogress|i|I)
            api_pretty "GET" "/status/task?status=inProgress"
            ;;
        finished|Finished|f|F)
            api_pretty "GET" "/status/task?status=finished"
            ;;
        *)
            answer notexist
            ;;
    esac
}

function vrack_list()
{
    api_lines "GET" "/vrack"
}

function vrack_info()
{
    require_arg "${OPTION2:-}" "./boap.sh vrack_info {vrack id}"
    local vrack
    vrack="$(urlencode_segment "$OPTION2")"

    echo "vRack ${OPTION2} (informations):"
    echo '----------------------------------------'
    api_pretty "GET" "/vrack/${vrack}"
    api_pretty "GET" "/vrack/${vrack}/serviceInfos"

    echo ''
    echo "Dedicated Server(s) in ${OPTION2}:"
    echo '----------------------------------------'
    api_lines "GET" "/vrack/${vrack}/dedicatedServer"

    echo ''
    echo "Public Cloud project(s) in ${OPTION2}:"
    echo '----------------------------------------'
    api_lines "GET" "/vrack/${vrack}/cloudProject"

    echo ''
    echo "Dedicated Cloud(s) in ${OPTION2}:"
    echo '----------------------------------------'
    api_lines "GET" "/vrack/${vrack}/dedicatedCloud"

    echo ''
    echo "IP Load balancer(s) in ${OPTION2}:"
    echo '----------------------------------------'
    api_lines "GET" "/vrack/${vrack}/ipLoadbalancing"
}

function whoami()
{
    api_pretty "GET" "/me"
}

function ck_payload_boap()
{
    cat <<'JSON'
{
  "accessRules": [
    {"method":"GET","path":"/me"},
    {"method":"GET","path":"/ip"},
    {"method":"GET","path":"/ip/*"},
    {"method":"GET","path":"/me/bill"},
    {"method":"GET","path":"/me/bill/*"},
    {"method":"GET","path":"/dedicated/server"},
    {"method":"GET","path":"/dedicated/server/*"},
    {"method":"POST","path":"/dedicated/server/*/reboot"},
    {"method":"GET","path":"/domain/zone/*"},
    {"method":"GET","path":"/dedicated/nasha"},
    {"method":"GET","path":"/dedicated/nasha/*"},
    {"method":"GET","path":"/ipLoadbalancing"},
    {"method":"GET","path":"/ipLoadbalancing/*"},
    {"method":"GET","path":"/status/task"},
    {"method":"GET","path":"/vrack"},
    {"method":"GET","path":"/vrack/*"}
  ],
  "redirection":"https://localhost/boap"
}
JSON
}

function ck_payload_readonly()
{
    cat <<'JSON'
{
  "accessRules": [
    {"method":"GET","path":"/*"}
  ],
  "redirection":"https://localhost/boap"
}
JSON
}

function ck_payload_full()
{
    cat <<'JSON'
{
  "accessRules": [
    {"method":"GET","path":"/*"},
    {"method":"POST","path":"/*"},
    {"method":"PUT","path":"/*"},
    {"method":"DELETE","path":"/*"}
  ],
  "redirection":"https://localhost/boap"
}
JSON
}

function request_ck()
{
    local profile payload body_file http_code curl_rc validation_url consumer_key state
    local error_code message class_name

    profile="${OPTION2:-boap}"

    case "$profile" in
        boap)
            payload="$(ck_payload_boap)"
            ;;
        readonly)
            payload="$(ck_payload_readonly)"
            ;;
        full)
            payload="$(ck_payload_full)"
            ;;
        *)
            echo "Usage: ./boap.sh request_ck [boap|readonly|full]" >&2
            return 2
            ;;
    esac

    body_file="$(mktemp)" || return 1

    http_code="$(curl -sS \
        --connect-timeout "$CURL_CONNECT_TIMEOUT" \
        --max-time "$CURL_MAX_TIME" \
        --request POST \
        --url "${OVH_API_BASE}/auth/credential" \
        --header "$ACCEPT_TYPE" \
        --header "$CONTENT_TYPE" \
        --header "X-Ovh-Application:${APP_KEY}" \
        --data-binary "$payload" \
        --output "$body_file" \
        --write-out '%{http_code}')"
    curl_rc=$?

    if [[ $curl_rc -ne 0 ]]; then
        rm -f "$body_file"
        return "$curl_rc"
    fi

    if [[ ! "$http_code" =~ ^2 ]]; then
        echo "HTTP ${http_code} POST /auth/credential" >&2
        json_pretty < "$body_file" >&2
        rm -f "$body_file"
        return 1
    fi

    validation_url="$(json_get_field validationUrl < "$body_file")"
    consumer_key="$(json_get_field consumerKey < "$body_file")"
    state="$(json_get_field state < "$body_file")"

    echo "Consumer Key request created on: ${OVH_API_BASE}"
    echo "Profile: ${profile}"
    echo "State: ${state:-unknown}"
    echo
    echo "1. Open this validation URL in a browser and approve it:"
    echo "${validation_url}"
    echo
    echo "2. After validation, put this value in secret.cfg:"
    echo "CONSUMER_KEY=\"${consumer_key}\""
    echo
    echo "Keep APP_KEY and APP_SECRET unchanged. Do not paste the Consumer Key in chat/logs."

    rm -f "$body_file"
}

function sha256_short()
{
    if command -v sha256sum >/dev/null 2>&1; then
        printf '%s' "$1" | sha256sum | awk '{print substr($1,1,12)}'
    elif command -v shasum >/dev/null 2>&1; then
        printf '%s' "$1" | shasum -a 256 | awk '{print substr($1,1,12)}'
    else
        printf 'n/a'
    fi
}

function is_invalid_application_key_response()
{
    local error_code="$1"
    local message="$2"
    case "$error_code" in
        INVALID_KEY|INVALID_APPLICATION_KEY)
            return 0
            ;;
    esac
    # OVHcloud does not always include errorCode on /auth/credential.
    # The message variants below mean the same thing at the AK validation layer.
    case "${message,,}" in
        *"invalid application key"*|*"application key is invalid"*)
            return 0
            ;;
    esac
    return 1
}

function probe_one_endpoint()
{
    local base endpoint req_type data timestamp query sig_key the_sig body_file http_code curl_rc error_code message class_name

    base="${1%/}"
    endpoint="${2:-/me}"
    req_type="${3:-GET}"
    data="${4-}"
    req_type="${req_type^^}"

    if [[ "$endpoint" != /* ]]; then
        printf '%-36s ERROR endpoint must start with /\n' "$base"
        return 2
    fi

    timestamp="$(curl -fsS --connect-timeout "$CURL_CONNECT_TIMEOUT" --max-time "$CURL_MAX_TIME" "${base}/auth/time" 2>/dev/null)" || {
        printf '%-36s FAIL auth/time unavailable\n' "$base"
        return 1
    }

    query="${base}${endpoint}"
    sig_key="${APP_SECRET}+${CONSUMER_KEY}+${req_type}+${query}+${data}+${timestamp}"
    the_sig='$1$'"$(printf '%s' "$sig_key" | sha1sum | awk '{print $1}')"
    body_file="$(mktemp)" || return 1

    local curl_args=(
        -sS
        --connect-timeout "$CURL_CONNECT_TIMEOUT"
        --max-time "$CURL_MAX_TIME"
        --request "$req_type"
        --url "$query"
        --header "$ACCEPT_TYPE"
        --header "$CONTENT_TYPE"
        --header "X-Ovh-Application:${APP_KEY}"
        --header "X-Ovh-Consumer:${CONSUMER_KEY}"
        --header "X-Ovh-Timestamp:${timestamp}"
        --header "X-Ovh-Signature:${the_sig}"
        --output "$body_file"
        --write-out '%{http_code}'
    )

    case "$req_type" in
        GET|DELETE) ;;
        POST|PUT|PATCH) curl_args+=(--data "$data") ;;
        *)
            rm -f "$body_file"
            printf '%-36s ERROR unsupported method %s\n' "$base" "$req_type"
            return 2
            ;;
    esac

    http_code="$(curl "${curl_args[@]}" 2>/dev/null)"
    curl_rc=$?
    if [[ $curl_rc -ne 0 ]]; then
        rm -f "$body_file"
        printf '%-36s FAIL curl rc=%s\n' "$base" "$curl_rc"
        return "$curl_rc"
    fi

    error_code="$(json_get_field errorCode < "$body_file")"
    message="$(json_get_field message < "$body_file")"
    class_name="$(json_get_field class < "$body_file")"

    if [[ "$http_code" =~ ^2 ]]; then
        printf '%-36s OK   HTTP %s\n' "$base" "$http_code"
        if [[ "${BOAP_DIAG_BODY:-0}" == "1" ]]; then
            json_pretty < "$body_file"
        fi
        rm -f "$body_file"
        return 0
    fi

    if is_invalid_application_key_response "$error_code" "$message"; then
        printf '%-36s FAIL HTTP %s %-22s %s\n' "$base" "$http_code" "${error_code:-INVALID_KEY}" "$message"
        rm -f "$body_file"
        return 1
    fi

    if [[ -n "$error_code" ]]; then
        printf '%-36s HIT  HTTP %s %-22s %s\n' "$base" "$http_code" "$error_code" "$message"
        rm -f "$body_file"
        return 3
    elif [[ -n "$class_name" || -n "$message" ]]; then
        printf '%-36s HIT  HTTP %s %-22s %s\n' "$base" "$http_code" "$class_name" "$message"
        rm -f "$body_file"
        return 3
    else
        printf '%-36s HIT  HTTP %s\n' "$base" "$http_code"
        if [[ "${BOAP_DIAG_BODY:-0}" == "1" ]]; then
            cat "$body_file"
            echo
        fi
        rm -f "$body_file"
        return 3
    fi
}

function probe_appkey_one_endpoint()
{
    local base body_file http_code curl_rc payload error_code message class_name validation_url consumer_key state

    base="${1%/}"
    # Valid /auth/credential payload. v1.1.3 used redirection:"", which OVHcloud
    # rejects before the Application Key diagnostic becomes meaningful.
    payload='{"accessRules":[{"method":"GET","path":"/me"}],"redirection":"https://localhost/boap-ovh-api-diagnostic"}'

    curl -fsS --connect-timeout "$CURL_CONNECT_TIMEOUT" --max-time "$CURL_MAX_TIME" "${base}/auth/time" >/dev/null 2>&1 || {
        printf '%-36s FAIL auth/time unavailable\n' "$base"
        return 1
    }

    body_file="$(mktemp)" || return 1

    http_code="$(curl -sS \
        --connect-timeout "$CURL_CONNECT_TIMEOUT" \
        --max-time "$CURL_MAX_TIME" \
        --request POST \
        --url "${base}/auth/credential" \
        --header "$ACCEPT_TYPE" \
        --header "$CONTENT_TYPE" \
        --header "X-Ovh-Application:${APP_KEY}" \
        --data-binary "$payload" \
        --output "$body_file" \
        --write-out '%{http_code}' 2>/dev/null)"
    curl_rc=$?
    if [[ $curl_rc -ne 0 ]]; then
        rm -f "$body_file"
        printf '%-36s FAIL curl rc=%s\n' "$base" "$curl_rc"
        return "$curl_rc"
    fi

    error_code="$(json_get_field errorCode < "$body_file")"
    message="$(json_get_field message < "$body_file")"
    class_name="$(json_get_field class < "$body_file")"

    if [[ "$http_code" =~ ^2 ]]; then
        validation_url="$(json_get_field validationUrl < "$body_file")"
        consumer_key="$(json_get_field consumerKey < "$body_file")"
        state="$(json_get_field state < "$body_file")"
        printf '%-36s OK   HTTP %s APP_KEY_VALID state=%s\n' "$base" "$http_code" "${state:-unknown}"
        if [[ -n "$validation_url" ]]; then
            printf '  validationUrl: %s\n' "$validation_url"
        fi
        if [[ -n "$consumer_key" ]]; then
            printf '  pendingConsumerKey: %s...%s\n' "${consumer_key:0:6}" "${consumer_key: -6}"
        fi
        rm -f "$body_file"
        return 0
    fi

    if is_invalid_application_key_response "$error_code" "$message"; then
        printf '%-36s FAIL HTTP %s %-22s %s\n' "$base" "$http_code" "${error_code:-INVALID_KEY}" "$message"
        rm -f "$body_file"
        return 1
    fi

    if [[ -n "$error_code" ]]; then
        printf '%-36s ??   HTTP %s %-22s %s\n' "$base" "$http_code" "$error_code" "$message"
    elif [[ -n "$class_name" || -n "$message" ]]; then
        printf '%-36s ??   HTTP %s %-22s %s\n' "$base" "$http_code" "$class_name" "$message"
    else
        printf '%-36s ??   HTTP %s\n' "$base" "$http_code"
    fi
    if [[ "${BOAP_DIAG_BODY:-0}" == "1" ]]; then
        cat "$body_file"
        echo
    fi
    rm -f "$body_file"
    return 3
}

function appkey_diag()
{
    local endpoints base ok inconclusive seen rc

    endpoints=(
        "${OVH_API_BASE}"
        "https://eu.api.ovh.com/1.0"
        "https://api.ovh.com/1.0"
        "https://eu.api.ovh.com/v1"
        "https://ca.api.ovh.com/1.0"
        "https://api.us.ovhcloud.com/1.0"
        "https://eu.api.soyoustart.com/1.0"
        "https://ca.api.soyoustart.com/1.0"
        "https://eu.api.kimsufi.com/1.0"
        "https://ca.api.kimsufi.com/1.0"
    )

    echo "BOAP Application Key diagnostic"
    echo "Config: ${CONFIG_FILE}"
    echo "This command calls POST /auth/credential with only X-Ovh-Application."
    echo "It does not use APP_SECRET, CONSUMER_KEY or request signatures."
    echo "Side effect: on a valid endpoint it creates a pending GET /me consumer key."
    echo "AK length: ${#APP_KEY}, sha256 prefix: $(sha256_short "$APP_KEY")"
    echo
    echo "Endpoint probe:"

    ok=1
    inconclusive=0
    seen=" "
    for base in "${endpoints[@]}"; do
        base="${base%/}"
        [[ -z "$base" ]] && continue
        if [[ "$seen" == *" $base "* ]]; then
            continue
        fi
        seen+="$base "
        probe_appkey_one_endpoint "$base"
        rc=$?
        case "$rc" in
            0) ok=0 ;;
            3) inconclusive=1 ;;
        esac
    done

    echo
    if [[ $ok -eq 0 ]]; then
        echo "Only OK means the Application Key is definitively valid on that endpoint."
        echo "Use the OK endpoint in OVH_API_BASE, then test ./boap.sh diag /me."
        return 0
    fi
    if [[ $inconclusive -eq 1 ]]; then
        echo "No endpoint produced a definitive OK. At least one response was inconclusive."
        echo "Run: BOAP_DIAG_BODY=1 ./boap.sh appkey_diag"
        return 3
    fi
    echo "Every tested endpoint rejected this Application Key before CK/signature evaluation."
    echo "At that point the AK is wrong, revoked, copied from the wrong credential set, or generated on another API family."
    return 1
}

function diag()
{
    local endpoint req_type data endpoints base ok
    endpoint="${OPTION2:-/me}"
    req_type="${OPTION3:-GET}"
    data="${4-}"

    endpoints=(
        "${OVH_API_BASE}"
        "https://eu.api.ovh.com/1.0"
        "https://api.ovh.com/1.0"
        "https://eu.api.ovh.com/v1"
        "https://ca.api.ovh.com/1.0"
        "https://api.us.ovhcloud.com/1.0"
        "https://eu.api.soyoustart.com/1.0"
        "https://ca.api.soyoustart.com/1.0"
        "https://eu.api.kimsufi.com/1.0"
        "https://ca.api.kimsufi.com/1.0"
    )

    echo "BOAP diagnostic"
    echo "Config: ${CONFIG_FILE}"
    echo "Test: ${req_type^^} ${endpoint}"
    echo "AK length: ${#APP_KEY}, sha256 prefix: $(sha256_short "$APP_KEY")"
    echo "AS length: ${#APP_SECRET}, sha256 prefix: $(sha256_short "$APP_SECRET")"
    echo "CK length: ${#CONSUMER_KEY}, sha256 prefix: $(sha256_short "$CONSUMER_KEY")"

    if [[ ! "$APP_KEY" =~ ^[A-Za-z0-9]+$ ]]; then
        echo "Warning: APP_KEY contains non-alphanumeric characters after trimming." >&2
    fi
    if [[ ! "$APP_SECRET" =~ ^[A-Za-z0-9]+$ ]]; then
        echo "Warning: APP_SECRET contains non-alphanumeric characters after trimming." >&2
    fi
    if [[ ! "$CONSUMER_KEY" =~ ^[A-Za-z0-9]+$ ]]; then
        echo "Warning: CONSUMER_KEY contains non-alphanumeric characters after trimming." >&2
    fi

    echo
    echo "Endpoint probe:"
    ok=1
    local seen=" "
    for base in "${endpoints[@]}"; do
        base="${base%/}"
        [[ -z "$base" ]] && continue
        if [[ "$seen" == *" $base "* ]]; then
            continue
        fi
        seen+="$base "
        probe_one_endpoint "$base" "$endpoint" "$req_type" "$data"
        case $? in
            0|3) ok=0 ;;
        esac
    done

    echo
    if [[ $ok -eq 0 ]]; then
        echo "OK means the signed call succeeded. HIT means the endpoint answered but reported a credential/signature/right issue."
        echo "For INVALID_KEY on signed calls, first run ./boap.sh appkey_diag with v1.1.4 or newer."
    else
        echo "No endpoint recognised the Application Key for this probe. If all lines show INVALID_KEY, the AK is revoked, from another API family, or not the real Application Key."
        echo "Generate a fresh token from the matching API portal, then update APP_KEY, APP_SECRET and CONSUMER_KEY together."
    fi
    return "$ok"
}

function help()
{
    cat <<'HELP_TEXT'
List of commands and options:
-------------------------------
./boap.sh ip_list: list IP addresses and blocks in CIDR format under your NIC
./boap.sh ip_info {IP address}: list information about this IP address or IP block in CIDR format
./boap.sh bill_list: list invoice references under your NIC
./boap.sh bill_infos {invoice reference}: print invoice details
./boap.sh servers_list: list all servers under your NIC
./boap.sh server_info {servername}: list information about this server
./boap.sh server_reboot {servername}: reboot your server, use with caution
./boap.sh nasha_list: list all NAS-HA services under your NIC
./boap.sh nasha_info {zpool name}: list information about your NAS-HA
./boap.sh lb_list: list all load balancers under your NIC
./boap.sh lb_info {lb id}: list information about your load balancer
./boap.sh lb_farms {lb id}: list the farms under this load balancer
./boap.sh lb_frontends {lb id}: list the frontends under this load balancer
./boap.sh status {planned | inprogress | finished}: list incidents or maintenances linked to your services
./boap.sh vrack_list: list all vRacks under your NIC
./boap.sh vrack_info {vrack id}: list information about your vRack
./boap.sh zone_export {domain name}: export DNS zone from OVHcloud
./boap.sh whoami: test credentials with GET /me, requires GET /me right
./boap.sh diag [/endpoint] [GET|POST|PUT|DELETE]: probe common OVH/SYS/Kimsufi API endpoints
./boap.sh appkey_diag: test APP_KEY only with valid POST /auth/credential, no AS/CK/signature
./boap.sh request_ck [boap|readonly|full]: request a new Consumer Key for the current APP_KEY

Config:
BOAP_CONFIG=./secret.cfg ./boap.sh servers_list
OVH_API_BASE can be set in secret.cfg.
Default: https://eu.api.ovh.com/1.0

Useful diagnostics:
./boap.sh diag /me
./boap.sh diag /dedicated/server
BOAP_DIAG_BODY=1 ./boap.sh diag /me
./boap.sh appkey_diag
./boap.sh request_ck boap
HELP_TEXT
}

OPTION1="${1:-}"
OPTION2="${2:-}"
OPTION3="${3:-}"

case "$OPTION1" in
    ip_list)        ip_list ;;
    ip_info)        ip_info ;;
    bill_list)      bill_list ;;
    bill_infos)     bill_infos ;;
    servers_list)   servers_list ;;
    server_info)    server_info ;;
    server_reboot)  server_reboot ;;
    nasha_list)     nasha_list ;;
    nasha_info)     nasha_info ;;
    lb_list)        lb_list ;;
    lb_info)        lb_info ;;
    lb_farms)       lb_farms ;;
    lb_frontends)   lb_frontends ;;
    status)         status ;;
    vrack_list)     vrack_list ;;
    vrack_info)     vrack_info ;;
    zone_export)    zone_export ;;
    whoami)         whoami ;;
    diag)           diag ;;
    appkey_diag)    appkey_diag ;;
    request_ck|ck_request) request_ck ;;
    help|-h|--help) help ;;
    *)              answer notexist ;;
esac
