Files
RepoFoundry/src/lib/http.sh
T
TirsvadandClaude Sonnet 5.5 613a288dea Fix the defects found by the live end-to-end run, add RC-017
- Close stdin for ssh, git, curl and the framework scripts, so a real ssh
  no longer swallows answers meant for later prompts.
- Count LICENSE and LICENSE + README.md (what Gitea creates) as content
  the script made, so a partly created repository can be reused.
- Document the observed token scopes (write:user for user-owned Gitea
  repositories) and the Gitea README.md in the README.
- Correct criterion 2 of MIL-002 (new Proposed version row).
- Record the run and the final security review as RC-017.
- Tests: stdin regression, initial_only reuse, other files count as
  content, example config needs its address edited.

Task: MIL-003#6
Task: MIL-002#1
Refs #20

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 21:41:47 +08:00

88 lines
3.6 KiB
Bash

# shellcheck shell=bash
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
# http.sh - The one safe place that runs curl: tokens stay off the command line.
#
# Part of create-project.sh: sourced by it, never run on its own.
#
# Provides: describe_http_status, describe_curl_error, http_request
describe_http_status() {
case "$1" in
401) printf 'authentication failed: the token is missing, expired or invalid' ;;
403) printf 'the token is valid but not allowed to do this (check its scopes)' ;;
404) printf 'not found (check the name, the owner and the token access)' ;;
409 | 422) printf 'rejected (the name may already exist or be invalid)' ;;
429) printf 'rate limited; wait and try again' ;;
5??) printf 'the server reported an error; try again later' ;;
*) printf 'unexpected HTTP status %s' "$1" ;;
esac
}
describe_curl_error() {
case "$1" in
6) printf 'could not resolve the host name' ;;
7) printf 'could not connect' ;;
28) printf 'the request timed out' ;;
35 | 51 | 58 | 60) printf 'the TLS connection failed' ;;
*) printf 'curl failed with exit code %s' "$1" ;;
esac
}
# http_request METHOD URL SCHEME TOKEN [BODY]
# SCHEME is "token" (Gitea) or "bearer" (GitHub). The token goes into a
# private curl config file, never onto the command line where other users
# could see it. Redirects are not followed, so the token is only ever sent
# to the host named in URL. On success HTTP_STATUS and HTTP_BODY_FILE are
# set; on a network failure the function returns 1 with HTTP_ERROR set.
# shellcheck disable=SC2034 # HTTP_* are results read by the callers
http_request() {
local method="$1" url="$2" scheme="$3" token="$4" body="${5:-}"
local header config_file body_file out_file host curl_status=0
local data_args=()
[[ $method =~ ^(GET|POST|PUT|PATCH|DELETE)$ ]] ||
die "internal error: unsupported HTTP method"
is_valid_request_url "$url" ||
die "refusing to call an invalid or non-https URL"
is_valid_token "$token" || die "refusing to send a malformed token"
case "$scheme" in
token) header="Authorization: token $token" ;;
bearer) header="Authorization: Bearer $token" ;;
*) die "internal error: unknown authentication scheme" ;;
esac
make_temp_file
config_file="$REPLY"
make_temp_file
out_file="$REPLY"
{
printf 'url = "%s"\n' "$url"
printf 'request = "%s"\n' "$method"
printf 'header = "%s"\n' "$header"
printf 'header = "Accept: application/json"\n'
printf 'header = "User-Agent: %s/%s"\n' "$PROJECT_NAME" "$VERSION"
} >"$config_file"
if [[ -n $body ]]; then
make_temp_file
body_file="$REPLY"
printf '%s' "$body" >"$body_file"
printf 'header = "Content-Type: application/json"\n' >>"$config_file"
data_args=(--data-binary "@$body_file")
fi
# curl's own error text is dropped: the exit code is mapped to a message
# that never contains the request.
HTTP_STATUS="$(curl --silent --max-time "$HTTP_TIMEOUT_SECONDS" \
--connect-timeout 10 --output "$out_file" --write-out '%{http_code}' \
--config "$config_file" "${data_args[@]}" </dev/null 2>/dev/null)" || curl_status=$?
# The configuration file holds the token and the body may hold another one
# (the mirror password): remove both now instead of at exit.
rm -f -- "$config_file" ${body_file:+"$body_file"}
if ((curl_status != 0)); then
host="${url#https://}"
host="${host%%/*}"
HTTP_STATUS=0
HTTP_ERROR="could not reach $host: $(describe_curl_error "$curl_status")"
return 1
fi
HTTP_BODY_FILE="$out_file"
HTTP_ERROR=""
}