all repos — dot @ 0a0471fef4fab3c9040d5b929e314a7aad787dbf

Shell, editor and terminal configuration, and the tools that go with them

bin/dot-request (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
#!/bin/bash

# Submit a change request from a machine that cannot push.
#
# A checkout cloned over https cannot publish anything: a change made on such a
# machine is lost at the next pull. So the change is written out, signed, and
# handed to whoever can apply it -- see "Can this machine push?" in AGENTS.md.
#
#   dot-request --key ~/.config/dot/inbox-key < request.md
#   some-agent-writing-a-request | dot-request --key ~/.config/dot/inbox-key
#
# The request is markdown, and its first line is its title (`# ...`). Say which
# files change, give the diff or the exact replacement text rather than a
# description of it, why, and how to verify it.
#
# It posts to the request inbox, which is a public endpoint and named here on
# purpose -- see "Can this machine push?" in AGENTS.md. Reaching it proves
# nothing on its own: a request is stored only if it carries a signature from a
# key authorised for that inbox. Set DOT_REQUEST_URL to point somewhere else
# when testing.
#
#   --inbox NAME     file into another inbox than dot. The key must be
#                    authorised there too, and signs for that inbox only.
#   --compress       send it xz-compressed (xz -9e), whatever its size
#   --no-compress    send it as it is, whatever its size
#
# By default a signed request over 400 KB goes xz-compressed and a smaller one
# as it is: the inbox takes 400 KB on the wire, and more than that only
# compressed, up to what the inbox allows once decompressed. Compressing needs
# xz; without it, a request that must be compressed is refused, not sent plain.
#
# Runs under macOS's /bin/bash 3.2.

set -euo pipefail

key=""
inbox="dot"
compress=""   # empty: decided by size
compress_over=$((400 * 1024))

usage() {
    sed -n '3,32p' "$0" | sed 's/^# \{0,1\}//'
    exit "${1:-0}"
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --key) key="${2:-}"; shift 2 ;;
        --key=*) key="${1#--key=}"; shift ;;
        --inbox) inbox="${2:-}"; shift 2 ;;
        --inbox=*) inbox="${1#--inbox=}"; shift ;;
        --compress) compress=yes; shift ;;
        --no-compress) compress=no; shift ;;
        -h|--help) usage 0 ;;
        *) >&2 echo "dot-request: unknown argument: $1"; usage 1 ;;
    esac
done

# The inbox is a path segment there and a signature namespace here: held to
# the same names the inbox itself accepts.
if [[ ! "${inbox}" =~ ^[A-Za-z0-9_-]{1,32}$ ]]; then
    >&2 echo "dot-request: --inbox: '${inbox}' is not an inbox name"
    exit 1
fi
url="${DOT_REQUEST_URL:-https://req.soarez.net/inbox/${inbox}}"

if [[ -z "${key}" ]]; then
    >&2 echo "dot-request: --key is required (the ssh key this machine signs with)"
    exit 1
fi
if [[ ! -r "${key}" ]]; then
    >&2 echo "dot-request: cannot read ${key}"
    exit 1
fi
if [[ -t 0 ]]; then
    >&2 echo "dot-request: the request is read from stdin"
    usage 1
fi

tmp="$(mktemp -d)"
trap 'rm -rf "${tmp}"' EXIT
cat > "${tmp}/request.md"

if [[ ! -s "${tmp}/request.md" ]]; then
    >&2 echo "dot-request: empty request"
    exit 1
fi
if ! head -1 "${tmp}/request.md" | grep -q '^# '; then
    >&2 echo "dot-request: the first line must be a markdown heading, '# something'"
    exit 1
fi

# The namespace names the inbox this request is for, and the store verifies
# against the same one. It is what stops a request signed here being replayed
# into a different inbox, and what stops this key being made to sign for
# anything else.
ssh-keygen -Y sign -q -f "${key}" -n "sz-request:${inbox}" "${tmp}/request.md"
cat "${tmp}/request.md" "${tmp}/request.md.sig" > "${tmp}/signed.md"

# What is signed is the text; compression is only how it travels, undone by
# the inbox before it checks anything.
size="$(wc -c < "${tmp}/signed.md" | tr -d ' ')"
if [[ -z "${compress}" ]]; then
    if [[ "${size}" -gt "${compress_over}" ]]; then compress=yes; else compress=no; fi
fi
encoding=()
body="${tmp}/signed.md"
if [[ "${compress}" == yes ]]; then
    if ! command -v xz >/dev/null; then
        >&2 echo "dot-request: ${size} bytes needs compressing, and there is no xz here"
        exit 1
    fi
    xz -9e -c "${tmp}/signed.md" > "${tmp}/signed.md.xz"
    body="${tmp}/signed.md.xz"
    encoding=(-H 'Content-Encoding: xz')
fi

code="$(curl -sS -o "${tmp}/reply" -w '%{http_code}' \
    -X POST -H 'Content-Type: text/markdown' ${encoding[@]+"${encoding[@]}"} \
    --data-binary "@${body}" "${url}")"

if [[ "${code}" == "201" ]]; then
    echo "submitted: $(cat "${tmp}/reply")"
else
    >&2 echo "dot-request: rejected (HTTP ${code}): $(cat "${tmp}/reply")"
    exit 1
fi