paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

test_reverse_proxy.sh (22113B)


      1 #!/bin/bash
      2 #
      3 # Reverse-proxy integration tests for paivana.
      4 #
      5 # Starts upstream HTTP servers (one per language: C/MHD, Go, Python, Rust)
      6 # and a paivana-httpd instance running with -n (paywall disabled), then
      7 # exercises the reverse-proxy behaviors with curl, wget, and a custom
      8 # libcurl / raw-socket pipelining client.
      9 #
     10 # Progress markers: each test prints "<description> " with no newline,
     11 # then "OK" on pass or "FAIL: <detail>" on failure.  Failure exits
     12 # non-zero, which make(1) treats as a TEST FAILURE.
     13 #
     14 # Environment variables honored:
     15 #   PAIVANA_HTTPD   path to paivana-httpd binary (default: built
     16 #                   in the sibling src/backend tree)
     17 #   SRCDIR          source dir containing .py / .rs / .go sources
     18 #                   (default: directory of this script)
     19 #   BUILDDIR        directory holding upstream_mhd, upstream_go,
     20 #                   upstream_rs, pipeline_client (default: $PWD)
     21 #   KEEP_TMP=1      keep the scratch dir and log files after exit
     22 #
     23 set -u
     24 
     25 function die() {
     26     echo "FAIL: $*" >&2
     27     exit 1
     28 }
     29 
     30 function msg() {
     31     printf '%s ' "$*"
     32 }
     33 
     34 function ok() {
     35     echo "OK"
     36 }
     37 
     38 function fail() {
     39     echo "FAIL: $*"
     40     dump_logs
     41     exit 1
     42 }
     43 
     44 function here() {
     45     cd -- "$(dirname -- "$0")" && pwd
     46 }
     47 
     48 SRCDIR="${SRCDIR:-$(here)}"
     49 BUILDDIR="${BUILDDIR:-$PWD}"
     50 
     51 # Default to the in-tree build path.
     52 PAIVANA_HTTPD="${PAIVANA_HTTPD:-$BUILDDIR/../backend/paivana-httpd}"
     53 if [ ! -x "$PAIVANA_HTTPD" ];
     54 then
     55     # Try the source layout (e.g. when tests are run from source tree)
     56     alt="$SRCDIR/../backend/paivana-httpd"
     57     if [ -x "$alt" ];
     58     then
     59         PAIVANA_HTTPD="$alt"
     60     fi
     61 fi
     62 
     63 if [ ! -x "$PAIVANA_HTTPD" ];
     64 then
     65     echo "SKIP: paivana-httpd binary not found (looked at $PAIVANA_HTTPD)" >&2
     66     exit 77
     67 fi
     68 
     69 # Binaries/commands for upstreams
     70 UPSTREAM_MHD="$BUILDDIR/upstream_mhd"
     71 UPSTREAM_GO="$BUILDDIR/upstream_go"
     72 UPSTREAM_RS="$BUILDDIR/upstream_rs"
     73 PIPELINE_CLIENT="$BUILDDIR/pipeline_client"
     74 
     75 # Ports (fixed, but we still wait/retry binding; if they're busy the
     76 # test bails out early so the user can rerun.)
     77 PAIVANA_PORT=18500
     78 MHD_PORT=18401
     79 GO_PORT=18402
     80 PY_PORT=18403
     81 RS_PORT=18404
     82 DEAD_PORT=18499   # nothing should be listening here
     83 
     84 TMPDIR="$(mktemp -d -t paivana-tests.XXXXXX)"
     85 LOGDIR="$TMPDIR/logs"
     86 mkdir -p "$LOGDIR"
     87 
     88 # paivana normally resolves its config.d via the install prefix.
     89 # Tests run against the uninstalled build tree, so point the
     90 # project's base-config override at an empty directory: no
     91 # auxiliary config snippets are needed for reverse-proxy tests.
     92 BASE_CONFIG_DIR="$TMPDIR/configd"
     93 mkdir -p "$BASE_CONFIG_DIR"
     94 export PAIVANA_BASE_CONFIG="$BASE_CONFIG_DIR"
     95 
     96 PIDS=()
     97 PAIVANA_PID=""
     98 
     99 function dump_logs() {
    100     echo "-- logs in $LOGDIR --" >&2
    101     for f in "$LOGDIR"/*.log; do
    102         [ -e "$f" ] || continue
    103         echo "==> $f <==" >&2
    104         tail -n 40 "$f" >&2
    105     done
    106 }
    107 
    108 function cleanup() {
    109     set +e
    110     for p in "${PIDS[@]:-}";
    111     do
    112         [ -n "$p" ] && kill -TERM "$p" 2>/dev/null
    113     done
    114     [ -n "$PAIVANA_PID" ] && kill -TERM "$PAIVANA_PID" 2>/dev/null
    115     # Give them a moment to exit cleanly
    116     sleep 0.2
    117     for p in "${PIDS[@]:-}";
    118     do
    119         [ -n "$p" ] && kill -KILL "$p" 2>/dev/null
    120     done
    121     [ -n "$PAIVANA_PID" ] && kill -KILL "$PAIVANA_PID" 2>/dev/null
    122     if [ "${KEEP_TMP:-0}" = "1" ];
    123     then
    124         echo "Temp files kept in $TMPDIR" >&2
    125     else
    126         rm -rf "$TMPDIR"
    127     fi
    128 }
    129 trap cleanup EXIT
    130 trap 'echo "FAIL: interrupted" >&2; exit 1' INT TERM
    131 
    132 function wait_for_port() {
    133     # Block until a TCP port accepts a connection (max ~5s).
    134     # NOTE: must run the /dev/tcp probe in a subshell — `exec` on the
    135     # parent shell with a failing redirection would terminate bash in
    136     # non-interactive mode (the 2>/dev/null does not suppress that).
    137     local host="$1" port="$2" tries=50
    138     while [ "$tries" -gt 0 ];
    139     do
    140         if ( exec 7<>"/dev/tcp/$host/$port" ) 2>/dev/null;
    141         then
    142             return 0
    143         fi
    144         sleep 0.1
    145         tries=$((tries - 1))
    146     done
    147     return 1
    148 }
    149 
    150 # Start a background upstream; record pid in PIDS.
    151 function start_bg() {
    152     local name="$1" port="$2"; shift 2
    153     local log="$LOGDIR/$name.log"
    154     ( exec "$@" "$port" ) >"$log" 2>&1 &
    155     local pid=$!
    156     PIDS+=("$pid")
    157     if ! wait_for_port 127.0.0.1 "$port";
    158     then
    159         echo "FAIL: $name did not start on port $port" >&2
    160         tail -n 20 "$log" >&2
    161         exit 1
    162     fi
    163 }
    164 
    165 function start_paivana() {
    166     # $1 = upstream base URL
    167     local dest="$1"
    168     local cfg="$TMPDIR/paivana.conf"
    169     sed -e "s|@DEST@|$dest|g" -e "s|@PORT@|$PAIVANA_PORT|g" \
    170         "$SRCDIR/test_reverse_proxy.conf.in" > "$cfg"
    171     local log="$LOGDIR/paivana.log"
    172     ( exec "$PAIVANA_HTTPD" -c "$cfg" -n -L WARNING ) >"$log" 2>&1 &
    173     PAIVANA_PID=$!
    174     if ! wait_for_port 127.0.0.1 "$PAIVANA_PORT";
    175     then
    176         echo "FAIL: paivana-httpd did not start on port $PAIVANA_PORT" >&2
    177         tail -n 20 "$log" >&2
    178         exit 1
    179     fi
    180 }
    181 
    182 function stop_paivana() {
    183     if [ -n "$PAIVANA_PID" ];
    184     then
    185         kill -TERM "$PAIVANA_PID" 2>/dev/null
    186         wait "$PAIVANA_PID" 2>/dev/null
    187         PAIVANA_PID=""
    188     fi
    189 }
    190 
    191 # Start all upstreams that we have binaries for.
    192 function start_upstreams() {
    193     start_bg mhd "$MHD_PORT" "$UPSTREAM_MHD"
    194     if [ -x "$UPSTREAM_GO" ];
    195     then
    196         start_bg go "$GO_PORT" "$UPSTREAM_GO"
    197     else
    198         echo "NOTE: upstream_go not built, skipping Go upstream tests" >&2
    199         GO_PORT=""
    200     fi
    201     if [ -x "$UPSTREAM_RS" ];
    202     then
    203         start_bg rs "$RS_PORT" "$UPSTREAM_RS"
    204     else
    205         echo "NOTE: upstream_rs not built, skipping Rust upstream tests" >&2
    206         RS_PORT=""
    207     fi
    208     if command -v python3 >/dev/null 2>&1;
    209     then
    210         local log="$LOGDIR/py.log"
    211         ( exec python3 "$SRCDIR/upstream_py.py" "$PY_PORT" ) >"$log" 2>&1 &
    212         PIDS+=("$!")
    213         if ! wait_for_port 127.0.0.1 "$PY_PORT";
    214         then
    215             echo "FAIL: upstream_py did not start on port $PY_PORT" >&2
    216             tail -n 20 "$log" >&2
    217             exit 1
    218         fi
    219     else
    220         echo "NOTE: python3 not available, skipping Python upstream tests" >&2
    221         PY_PORT=""
    222     fi
    223 }
    224 
    225 ######################################################################
    226 # Test helpers
    227 ######################################################################
    228 
    229 PAIVANA_URL() { echo "http://127.0.0.1:$PAIVANA_PORT$1"; }
    230 
    231 # GET via curl; verifies status code and a substring of the body.
    232 function test_get() {
    233     local desc="$1" path="$2" want_status="$3" want_sub="$4"
    234     msg "$desc"
    235     local out status
    236     out="$(curl -sS -o "$TMPDIR/body" -w '%{http_code}' "$(PAIVANA_URL "$path")" 2>"$TMPDIR/err")" \
    237         || { fail "curl: $(cat "$TMPDIR/err")"; }
    238     status="$out"
    239     [ "$status" = "$want_status" ] || fail "status=$status want=$want_status"
    240     if [ -n "$want_sub" ] && ! grep -q -- "$want_sub" "$TMPDIR/body";
    241     then
    242         fail "body missing substring '$want_sub' (got: $(tr -d '\n' <"$TMPDIR/body" | head -c 120))"
    243     fi
    244     ok
    245 }
    246 
    247 # HEAD
    248 function test_head() {
    249     local desc="$1" path="$2" want_status="$3"
    250     msg "$desc"
    251     local status
    252     status="$(curl -sS -I -o /dev/null -w '%{http_code}' "$(PAIVANA_URL "$path")" 2>"$TMPDIR/err")" \
    253         || fail "curl: $(cat "$TMPDIR/err")"
    254     [ "$status" = "$want_status" ] || fail "status=$status want=$want_status"
    255     ok
    256 }
    257 
    258 # Generic method with optional body; checks status and body substring.
    259 function test_method() {
    260     local desc="$1" method="$2" path="$3" body="$4" want_status="$5" want_sub="$6"
    261     msg "$desc"
    262     local args=(-sS -o "$TMPDIR/body" -w '%{http_code}' -X "$method" "$(PAIVANA_URL "$path")")
    263     if [ -n "$body" ];
    264     then
    265         args+=(--data-binary "@$body")
    266     fi
    267     local status
    268     status="$(curl "${args[@]}" 2>"$TMPDIR/err")" \
    269         || fail "curl: $(cat "$TMPDIR/err")"
    270     [ "$status" = "$want_status" ] || \
    271         fail "status=$status want=$want_status; body=$(head -c 200 "$TMPDIR/body")"
    272     if [ -n "$want_sub" ] && ! grep -q -- "$want_sub" "$TMPDIR/body";
    273     then
    274         fail "body missing substring '$want_sub' (got: $(head -c 200 "$TMPDIR/body"))"
    275     fi
    276     ok
    277 }
    278 
    279 ######################################################################
    280 # Test battery — runs against whichever upstream we've pointed
    281 # paivana at.
    282 ######################################################################
    283 
    284 function run_battery() {
    285     local label="$1"
    286 
    287     test_get "[$label] GET /hello (basic proxy pass-through)" \
    288         /hello 200 "Hello from"
    289 
    290     test_get "[$label] GET /status/201 (2xx status forwarding)" \
    291         /status/201 201 "status 201"
    292 
    293     test_get "[$label] GET /status/404 (4xx status forwarding)" \
    294         /status/404 404 "status 404"
    295 
    296     test_get "[$label] GET /status/500 (5xx status forwarding)" \
    297         /status/500 500 "status 500"
    298 
    299     test_head "[$label] HEAD /hello" /hello 200
    300 
    301     # Medium response body (128 KiB): exercises streaming download
    302     test_get "[$label] GET /large/131072 (128 KiB response)" \
    303         /large/131072 200 ""
    304     local sz
    305     sz="$(wc -c <"$TMPDIR/body" | tr -d ' ')"
    306     msg "[$label] verify 128 KiB body length"
    307     [ "$sz" = "131072" ] || fail "got $sz bytes, expected 131072"
    308     ok
    309 
    310     # POST /echo — round-trip body
    311     printf 'hello-payload-%s' "$label" >"$TMPDIR/post_body"
    312     test_method "[$label] POST /echo (body round-trip)" \
    313         POST /echo "$TMPDIR/post_body" 200 "hello-payload-$label"
    314 
    315     # POST /upload — byte count
    316     dd if=/dev/urandom of="$TMPDIR/rnd" bs=1024 count=64 status=none
    317     test_method "[$label] POST /upload (64 KiB binary upload)" \
    318         POST /upload "$TMPDIR/rnd" 200 "Received 65536 bytes"
    319 
    320     # PUT /put
    321     test_method "[$label] PUT /put (PUT forwarding)" \
    322         PUT /put "$TMPDIR/post_body" 200 "PUT received"
    323 
    324     # PATCH /patch
    325     test_method "[$label] PATCH /patch (PATCH forwarding)" \
    326         PATCH /patch "$TMPDIR/post_body" 200 "PATCH received"
    327 
    328     # DELETE /item/1 with empty body
    329     msg "[$label] DELETE /item/1 (204 No Content)"
    330     local status
    331     status="$(curl -sS -X DELETE -o /dev/null -w '%{http_code}' "$(PAIVANA_URL /item/1)" 2>"$TMPDIR/err")" \
    332         || fail "curl: $(cat "$TMPDIR/err")"
    333     [ "$status" = "204" ] || fail "status=$status"
    334     ok
    335 
    336     # OPTIONS — server should echo 204 + Allow
    337     msg "[$label] OPTIONS /anything (204 + Allow header)"
    338     local opts
    339     opts="$(curl -sS -X OPTIONS -D "$TMPDIR/hdrs" -o /dev/null -w '%{http_code}' "$(PAIVANA_URL /hello)" 2>"$TMPDIR/err")" \
    340         || fail "curl: $(cat "$TMPDIR/err")"
    341     [ "$opts" = "204" ] || fail "status=$opts"
    342     grep -qi '^allow:' "$TMPDIR/hdrs" || fail "no Allow header returned"
    343     ok
    344 
    345     # Header propagation: X-Forwarded-For must be added by paivana.
    346     msg "[$label] GET /echo-headers (X-Forwarded-For added)"
    347     curl -sS -o "$TMPDIR/body" "$(PAIVANA_URL /echo-headers)" 2>"$TMPDIR/err" \
    348         || fail "curl: $(cat "$TMPDIR/err")"
    349     grep -qi '^x-forwarded-for:' "$TMPDIR/body" || \
    350         fail "upstream did not see X-Forwarded-For; headers:\n$(cat "$TMPDIR/body")"
    351     grep -qi '^x-forwarded-proto:' "$TMPDIR/body" || \
    352         fail "upstream did not see X-Forwarded-Proto"
    353     grep -qi '^via:' "$TMPDIR/body" || \
    354         fail "upstream did not see Via: paivana"
    355     ok
    356 
    357     # RFC 9110 §7.6.3: client's Via chain must be preserved and our
    358     # pseudonym *appended* to it, not replaced.
    359     msg "[$label] client Via is preserved and paivana is appended"
    360     curl -sS -H 'Via: 1.1 alpha.example, 2.0 beta.example' \
    361          -o "$TMPDIR/body" "$(PAIVANA_URL /echo-headers)" 2>"$TMPDIR/err" \
    362         || fail "curl: $(cat "$TMPDIR/err")"
    363     local via
    364     via="$(grep -i '^via:' "$TMPDIR/body" | tr -d '\r')"
    365     [ -n "$via" ] || fail "no Via header at upstream"
    366     # Expect: "Via: 1.1 alpha.example, 2.0 beta.example, 1.1 paivana"
    367     case "$via" in
    368         *"alpha.example"*"beta.example"*paivana*) ;;
    369         *) fail "Via not appended correctly: '$via'";;
    370     esac
    371     ok
    372 
    373     # RFC 9110 §7.6.1: headers named in the client's Connection
    374     # header are hop-by-hop and must not be forwarded upstream.
    375     msg "[$label] headers named in Connection: are stripped"
    376     curl -sS \
    377          -H 'Connection: X-Custom-Hop, X-Other-Hop' \
    378          -H 'X-Custom-Hop: must-not-forward' \
    379          -H 'X-Other-Hop: neither' \
    380          -H 'X-Keep: keep-this' \
    381          -o "$TMPDIR/body" "$(PAIVANA_URL /echo-headers)" 2>"$TMPDIR/err" \
    382         || fail "curl: $(cat "$TMPDIR/err")"
    383     if grep -qi '^x-custom-hop:' "$TMPDIR/body";
    384     then
    385         fail "X-Custom-Hop leaked to upstream (Connection list ignored)"
    386     fi
    387     if grep -qi '^x-other-hop:' "$TMPDIR/body";
    388     then
    389         fail "X-Other-Hop leaked to upstream (Connection list ignored)"
    390     fi
    391     grep -qi '^x-keep:.*keep-this' "$TMPDIR/body" || \
    392         fail "X-Keep (not named in Connection) was incorrectly dropped"
    393     ok
    394 
    395     # Custom request header must be forwarded.
    396     msg "[$label] custom request header X-Test is forwarded"
    397     curl -sS -H 'X-Test: dingbat-42' \
    398          -o "$TMPDIR/body" "$(PAIVANA_URL /echo-headers)" 2>"$TMPDIR/err" \
    399         || fail "curl: $(cat "$TMPDIR/err")"
    400     grep -qi '^x-test:.*dingbat-42' "$TMPDIR/body" || \
    401         fail "upstream did not see X-Test: dingbat-42"
    402     ok
    403 
    404     # Response header passthrough: upstream sets X-Upstream.
    405     msg "[$label] upstream response header X-Upstream is forwarded"
    406     curl -sS -D "$TMPDIR/hdrs" -o /dev/null "$(PAIVANA_URL /hello)" 2>"$TMPDIR/err" \
    407         || fail "curl: $(cat "$TMPDIR/err")"
    408     grep -qi '^x-upstream:' "$TMPDIR/hdrs" || \
    409         fail "X-Upstream header not forwarded back to client"
    410     ok
    411 }
    412 
    413 ######################################################################
    414 # Cross-cutting tests (do not depend on which upstream is used).
    415 ######################################################################
    416 
    417 function test_method_not_allowed() {
    418     msg "unsupported HTTP method (TRACE) yields 405"
    419     local status
    420     status="$(curl -sS -X TRACE -o /dev/null -w '%{http_code}' \
    421                    "$(PAIVANA_URL /hello)" 2>"$TMPDIR/err")" \
    422         || fail "curl: $(cat "$TMPDIR/err")"
    423     [ "$status" = "405" ] || fail "status=$status want=405"
    424     ok
    425 }
    426 
    427 function test_upload_too_big() {
    428     msg "upload exceeding 1 MiB buffer yields 413"
    429     dd if=/dev/zero of="$TMPDIR/big" bs=1024 count=2048 status=none
    430     local status
    431     status="$(curl -sS -X POST --data-binary "@$TMPDIR/big" \
    432                    -o /dev/null -w '%{http_code}' \
    433                    "$(PAIVANA_URL /upload)" 2>"$TMPDIR/err")" \
    434         || fail "curl: $(cat "$TMPDIR/err")"
    435     # 413 == Content Too Large; some builds report 500 on hook close
    436     [ "$status" = "413" ] || fail "status=$status want=413"
    437     ok
    438 }
    439 
    440 function test_upload_too_big_early() {
    441     # Open a raw TCP connection and send a POST whose Content-Length
    442     # already exceeds the buffer cap, but DON'T send any body bytes.
    443     # Paivana must reject on the Content-Length header alone (during
    444     # MHD's HEADERS_PROCESSED callback), respond with 413 and close
    445     # the connection.  If the early-reject path is missing the server
    446     # would block waiting for a body that never arrives and we'd hit
    447     # the timeout, which fails the test rather than masquerading as
    448     # a pass.
    449     msg "Content-Length exceeding 1 MiB triggers early 413 (no body sent)"
    450     local out
    451     out="$( ( exec 3<>"/dev/tcp/127.0.0.1/$PAIVANA_PORT"
    452               printf 'POST /upload HTTP/1.1\r\nHost: 127.0.0.1:%s\r\nContent-Length: 10485760\r\nConnection: close\r\n\r\n' \
    453                      "$PAIVANA_PORT" >&3
    454               timeout 5 cat <&3 ) 2>"$TMPDIR/err")" \
    455         || fail "raw POST failed (timeout or socket error): $(cat "$TMPDIR/err")"
    456     case "$out" in
    457         'HTTP/1.1 413'*) ;;
    458         *) fail "expected 413 status line, got: $(echo "$out" | head -c 80)";;
    459     esac
    460     ok
    461 }
    462 
    463 function test_upload_too_big_no_continue() {
    464     # When the client opts in to 100-continue, paivana must NOT send
    465     # the interim 100 response if it has already decided to reject
    466     # the upload — it should jump straight to 413.  Use curl with
    467     # `Expect: 100-continue` and verbose tracing, then assert that
    468     # no `< HTTP/1.1 100` line appeared on the wire.
    469     msg "rejection suppresses 100 Continue when client opts in"
    470     dd if=/dev/zero of="$TMPDIR/big" bs=1024 count=2048 status=none
    471     local status
    472     status="$(curl -sSv -X POST -H 'Expect: 100-continue' \
    473                    --expect100-timeout 5 \
    474                    --data-binary "@$TMPDIR/big" \
    475                    -o /dev/null -w '%{http_code}' \
    476                    "$(PAIVANA_URL /upload)" 2>"$TMPDIR/trace")" \
    477         || fail "curl: $(cat "$TMPDIR/trace")"
    478     [ "$status" = "413" ] || fail "status=$status want=413"
    479     if grep -q '^< HTTP/1.1 100' "$TMPDIR/trace";
    480     then
    481         fail "server sent 100 Continue before 413; trace: $(grep '^<' "$TMPDIR/trace")"
    482     fi
    483     ok
    484 }
    485 
    486 function test_upload_too_big_chunked() {
    487     # Chunked transfer-encoding has no Content-Length, so paivana
    488     # cannot know the upload is too big until it actually reaches
    489     # the cap mid-stream.  This test guards the fallback
    490     # drain-then-reject path that runs in BODY_RECEIVING /
    491     # FULL_REQ_RECEIVED.
    492     msg "chunked upload exceeding 1 MiB still yields 413 (drain path)"
    493     dd if=/dev/zero of="$TMPDIR/big" bs=1024 count=2048 status=none
    494     local status
    495     status="$(curl -sS -X POST \
    496                    -H 'Transfer-Encoding: chunked' \
    497                    -H 'Content-Length:' \
    498                    --data-binary "@$TMPDIR/big" \
    499                    -o /dev/null -w '%{http_code}' \
    500                    "$(PAIVANA_URL /upload)" 2>"$TMPDIR/err")" \
    501         || fail "curl: $(cat "$TMPDIR/err")"
    502     [ "$status" = "413" ] || fail "status=$status want=413"
    503     ok
    504 }
    505 
    506 function test_upstream_down() {
    507     msg "upstream down yields 502 Bad Gateway"
    508     # Re-point paivana at a port with nothing listening.
    509     stop_paivana
    510     start_paivana "http://127.0.0.1:$DEAD_PORT"
    511     local status
    512     status="$(curl -sS -o "$TMPDIR/body" -w '%{http_code}' \
    513                    --max-time 10 \
    514                    "$(PAIVANA_URL /hello)" 2>"$TMPDIR/err")" \
    515         || fail "curl: $(cat "$TMPDIR/err")"
    516     [ "$status" = "502" ] || fail "status=$status want=502"
    517     grep -qi 'bad gateway' "$TMPDIR/body" || \
    518         fail "no 'Bad Gateway' in body"
    519     ok
    520 }
    521 
    522 # curl with multiple URLs on one command line uses HTTP keep-alive
    523 # (not true pipelining, but exercises the same code path in paivana
    524 # of handling successive requests on one TCP connection).
    525 function test_keepalive_curl() {
    526     msg "curl keep-alive: 3 sequential GETs on one connection"
    527     local out
    528     out="$(curl -sS --http1.1 \
    529                 -w '\n@status=%{http_code}\n' \
    530                 "$(PAIVANA_URL /hello)" \
    531                 "$(PAIVANA_URL /hello)" \
    532                 "$(PAIVANA_URL /hello)" 2>"$TMPDIR/err")" \
    533         || fail "curl: $(cat "$TMPDIR/err")"
    534     local count
    535     count="$(printf '%s\n' "$out" | grep -c '^Hello from')"
    536     [ "$count" = "3" ] || fail "got $count Hello lines; want 3; out:\n$out"
    537     ok
    538 }
    539 
    540 function test_wget_basic() {
    541     msg "wget fetch (third-party client interop)"
    542     if ! command -v wget >/dev/null 2>&1; then
    543         echo "SKIP (wget missing)"
    544         return
    545     fi
    546     local body
    547     body="$(wget -qO- --timeout=5 "$(PAIVANA_URL /hello)")" \
    548         || fail "wget failed"
    549     echo "$body" | grep -q '^Hello from' \
    550         || fail "unexpected body from wget: $body"
    551     ok
    552 }
    553 
    554 function test_pipelined() {
    555     msg "HTTP/1.1 pipelined requests (4 back-to-back on one TCP socket)"
    556     if [ ! -x "$PIPELINE_CLIENT" ]; then
    557         echo "SKIP (pipeline_client not built)"
    558         return
    559     fi
    560     local out
    561     out="$("$PIPELINE_CLIENT" 127.0.0.1 "$PAIVANA_PORT" \
    562               /hello /status/201 /hello /status/404 2>"$TMPDIR/err")" \
    563         || fail "pipeline_client: $(cat "$TMPDIR/err")"
    564     printf '%s\n' "$out" >"$TMPDIR/pipeline.out"
    565     local n
    566     n="$(grep -c '^--- response' "$TMPDIR/pipeline.out")"
    567     [ "$n" = "4" ] || fail "got $n responses, want 4; output:\n$out"
    568     # Order preserved: responses must match the request sequence.
    569     grep -q '^--- response 0: status=200' "$TMPDIR/pipeline.out" \
    570         || fail "response 0: wrong status; out:\n$out"
    571     grep -q '^--- response 1: status=201' "$TMPDIR/pipeline.out" \
    572         || fail "response 1: wrong status; out:\n$out"
    573     grep -q '^--- response 2: status=200' "$TMPDIR/pipeline.out" \
    574         || fail "response 2: wrong status; out:\n$out"
    575     grep -q '^--- response 3: status=404' "$TMPDIR/pipeline.out" \
    576         || fail "response 3: wrong status; out:\n$out"
    577     ok
    578 }
    579 
    580 ######################################################################
    581 # Drive the tests.
    582 ######################################################################
    583 
    584 echo "=== paivana reverse-proxy tests ==="
    585 echo "Temp dir: $TMPDIR"
    586 echo "Paivana binary: $PAIVANA_HTTPD"
    587 echo "Source dir: $SRCDIR"
    588 echo "Build dir: $BUILDDIR"
    589 
    590 start_upstreams
    591 
    592 # --- C / libmicrohttpd upstream ---------------------------------------
    593 start_paivana "http://127.0.0.1:$MHD_PORT"
    594 run_battery "mhd"
    595 
    596 test_method_not_allowed
    597 test_upload_too_big
    598 test_upload_too_big_early
    599 test_upload_too_big_no_continue
    600 test_upload_too_big_chunked
    601 test_keepalive_curl
    602 test_wget_basic
    603 test_pipelined
    604 
    605 stop_paivana
    606 
    607 # --- Go upstream ------------------------------------------------------
    608 if [ -n "$GO_PORT" ];
    609 then
    610     start_paivana "http://127.0.0.1:$GO_PORT"
    611     run_battery "go"
    612     test_pipelined
    613     stop_paivana
    614 fi
    615 
    616 # --- Python upstream --------------------------------------------------
    617 if [ -n "$PY_PORT" ];
    618 then
    619     start_paivana "http://127.0.0.1:$PY_PORT"
    620     run_battery "py"
    621     test_pipelined
    622     stop_paivana
    623 fi
    624 
    625 # --- Rust upstream ----------------------------------------------------
    626 if [ -n "$RS_PORT" ];
    627 then
    628     start_paivana "http://127.0.0.1:$RS_PORT"
    629     run_battery "rs"
    630     test_pipelined
    631     stop_paivana
    632 fi
    633 
    634 # --- Upstream-down test (runs last because it restarts paivana) -------
    635 test_upstream_down
    636 stop_paivana
    637 
    638 echo "=== all tests passed ==="
    639 exit 0