Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/tests/rig/range.sh

14.4 KiB, 35 runs, executable

created by r1870400018:17844, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1#!/usr/bin/env bash
2#
3# Stands a real Steel up over HTTPS, serves a ten megabyte file of known bytes,
4# and asks curl for windows of it. Every body is compared against the same window
5# cut out of the file with dd, byte for byte, so a window taken from the wrong
6# offset fails rather than merely looking plausible.
7#
8# fe2o3_steel/tests/rig/range.sh # run it
9# RIG_PORT=9445 fe2o3_steel/tests/rig/range.sh
10# RIG_KEEP=1 fe2o3_steel/tests/rig/range.sh # leave the directory behind
11#
12# Exits non-zero if any check fails. See README.md for what the rig knows about
13# starting a Steel that a first reading of the code does not tell you.
14
15set -u
16
17HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18ROOT="$(cd "$HERE/../../.." && pwd)"
19PORT="${RIG_PORT:-9445}"
20PASS='rig-test-passphrase-not-a-secret'
21B="https://localhost:$PORT"
22TARGET="${CARGO_TARGET_DIR:-$ROOT/target}"
23
24# Ten megabytes, and a byte pattern with a period that shares no factor with any
25# power of two -- so a window read from the wrong offset holds different bytes
26# rather than the same repeating ones.
27SIZE=10485760
28PERIOD=251
29
30RIG_DIR="$(mktemp -d -t steel-range-XXXXXX)"
31export RIG_DIR
32
33cleanup() {
34 [ -n "${STEEL_PID:-}" ] && kill "$STEEL_PID" 2>/dev/null
35 [ -n "${HOLD_PID:-}" ] && kill "$HOLD_PID" 2>/dev/null
36 [ -n "${STEEL_PID:-}" ] && wait "$STEEL_PID" 2>/dev/null
37 if [ "${RIG_KEEP:-0}" = "1" ]; then
38 echo "rig left at $RIG_DIR"
39 else
40 rm -rf "$RIG_DIR"
41 fi
42}
43trap cleanup EXIT INT TERM
44
45pass=0; fail=0
46ok() { pass=$((pass+1)); echo " PASS $1"; }
47no() { fail=$((fail+1)); echo " FAIL $1${2:+ -- $2}"; }
48check() { if [ "$2" = "$3" ]; then ok "$1"; else no "$1" "expected '$3', got '$2'"; fi; }
49has() { if echo "$2" | grep -qi -- "$3"; then ok "$1"; else no "$1" "did not contain '$3'"; fi; }
50hasnt() { if echo "$2" | grep -qi -- "$3"; then no "$1" "contained '$3'"; else ok "$1"; fi; }
51
52MEDIA="$RIG_DIR/www/public/clip.mp4"
53
54# One header field off a response, lower-cased, carriage return stripped.
55field() { grep -i "^$2:" "$1" | tr -d '\r' | sed "s/^[^:]*: *//" | tail -1; }
56
57# A window of the served file, cut with dd -- the oracle every body is compared
58# against. `skip_bytes` and `count_bytes` make the offsets exact rather than
59# block-aligned.
60slice() { # slice <start> <len> <out>
61 dd if="$MEDIA" of="$3" bs=65536 skip="$1" count="$2" \
62 iflag=skip_bytes,count_bytes status=none
63}
64
65# GET with an optional Range, keeping the headers and the body apart, then check
66# the status, the two length fields and the bytes themselves.
67#
68# ranged <name> <curl range args...> -- <status> <content-range> <start> <len>
69ranged() {
70 local name="$1"; shift
71 local args=()
72 while [ "$1" != "--" ]; do args+=("$1"); shift; done
73 shift
74 local want_status="$1" want_cr="$2" start="$3" len="$4"
75
76 local h="$RIG_DIR/h.$$" b="$RIG_DIR/b.$$" want="$RIG_DIR/w.$$"
77 local code
78 code=$(curl -sk $HTTPARG "${args[@]}" -D "$h" -o "$b" -w '%{http_code}' "$B/clip.mp4")
79 check "$name: status" "$code" "$want_status"
80
81 local cr; cr=$(field "$h" content-range)
82 check "$name: content-range" "$cr" "$want_cr"
83
84 local cl; cl=$(field "$h" content-length)
85 check "$name: content-length" "$cl" "$len"
86
87 has "$name: advertises byte ranges" "$(field "$h" accept-ranges)" "bytes"
88
89 local got; got=$(stat -c %s "$b")
90 check "$name: body length" "$got" "$len"
91
92 if [ "$len" -gt 0 ]; then
93 slice "$start" "$len" "$want"
94 if cmp -s "$want" "$b"; then ok "$name: body matches the file byte for byte"
95 else no "$name: body differs from the file"; fi
96 rm -f "$want"
97 fi
98 rm -f "$h" "$b"
99}
100
101echo "== building =="
102cargo build --release -p oxedyne_fe2o3_steel --bin steel --manifest-path "$ROOT/Cargo.toml" \
103 2>&1 | grep -E "^error|Finished" | tail -1
104[ -x "$TARGET/release/steel" ] || { echo "no binary at $TARGET/release/steel"; exit 1; }
105
106echo "== laying out $RIG_DIR =="
107mkdir -p "$RIG_DIR/www/public" "$RIG_DIR/www/src/styles"
108cp "$TARGET/release/steel" "$RIG_DIR/steel"
109sed -e "s|@PORT@|$PORT|g" "$HERE/range_config.jdat.in" > "$RIG_DIR/config.jdat"
110
111# The file under test: a counting pattern, so every window is distinguishable
112# from every other one. Named `.mp4` because the content type a recording is
113# served under is part of what makes a browser offer to seek in it.
114python3 - "$MEDIA" "$SIZE" "$PERIOD" <<'PY'
115import sys
116path, size, period = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
117pattern = bytes(range(period))
118whole = (pattern * (size // period + 1))[:size]
119with open(path, "wb") as f:
120 f.write(whole)
121PY
122: > "$RIG_DIR/www/public/empty.mp4"
123printf 'x' > "$RIG_DIR/www/public/one.mp4"
124# A file worth encoding, for the coding checks: a stylesheet is highly redundant,
125# so the encoded form is a fraction of the weight and the two lengths cannot be
126# confused for each other.
127#
128# A stylesheet rather than a page, deliberately. The rig runs Steel in
129# development mode, where an HTML document is rewritten on the way out to carry
130# the refresh hook -- so the file on disk is not the entity sent, and `stat`
131# would be the wrong oracle for its length. `text/css` is compressible and is not
132# a document, so what is on disk is what goes out. Not `styles.css`, which the
133# development CSS bundler owns.
134BULK="$RIG_DIR/www/public/bulk.css"
135python3 - "$BULK" <<'PY'
136import sys
137rule = ".a-class-name-that-repeats { margin: 0; padding: 0; color: #123456; }\n"
138with open(sys.argv[1], "w") as f:
139 f.write(rule * 4000)
140PY
141BULK_LEN=$(stat -c %s "$BULK")
142echo " $(stat -c %s "$MEDIA") bytes at clip.mp4, $BULK_LEN bytes at bulk.css"
143
144echo "== wallet =="
145python3 "$HERE/make_wallet.py" > "$RIG_DIR/wallet.out" 2>&1
146[ -f "$RIG_DIR/wallet.jdat" ] || { echo "no wallet; see $RIG_DIR/wallet.out"; RIG_KEEP=1; exit 1; }
147echo " made"
148
149echo "== starting =="
150mkfifo "$RIG_DIR/ctl"
151sleep 900 > "$RIG_DIR/ctl" &
152HOLD_PID=$!
153( cd "$RIG_DIR" && STEEL_ADMIN_PASS="$PASS" exec ./steel server -d < ctl > server.log 2>&1 ) &
154STEEL_PID=$!
155
156for _ in $(seq 1 40); do
157 sleep 1
158 curl -sk -o /dev/null --max-time 2 "$B/clip.mp4" -r 0-0 && break
159done
160if ! curl -sk -o /dev/null --max-time 5 -r 0-0 "$B/clip.mp4"; then
161 echo "server did not come up; see $RIG_DIR/server.log"
162 RIG_KEEP=1
163 exit 1
164fi
165echo " up on $PORT"
166
167echo
168echo "== what the connection actually negotiated =="
169# Steel offers `http/1.1` over ALPN and nothing else, so `--http2` negotiates
170# 1.1 and falls back. The sweep is repeated under it regardless: the file layer
171# must behave identically whichever way the client asked.
172for arg in "" "--http2"; do
173 v=$(curl -sk $arg -o /dev/null -w '%{http_version}' "$B/clip.mp4" -r 0-0)
174 echo " curl ${arg:-(default)} -> HTTP/$v"
175done
176
177for HTTPARG in "" "--http2"; do
178 label="${HTTPARG:-http1.1}"
179 echo
180 echo "== the sweep, ${label} =="
181
182 ranged "whole file" -- 200 "" 0 $SIZE
183 ranged "first hundred" -r 0-99 -- 206 "bytes 0-99/$SIZE" 0 100
184 ranged "from a hundred" -r 100- -- 206 "bytes 100-$((SIZE-1))/$SIZE" 100 $((SIZE-100))
185 ranged "last fifty" -r -50 -- 206 "bytes $((SIZE-50))-$((SIZE-1))/$SIZE" $((SIZE-50)) 50
186 ranged "one byte" -r 0-0 -- 206 "bytes 0-0/$SIZE" 0 1
187 ranged "the last byte" -r $((SIZE-1))- \
188 -- 206 "bytes $((SIZE-1))-$((SIZE-1))/$SIZE" $((SIZE-1)) 1
189 # The seek a player actually makes: a window from the middle, which is the
190 # case an off-by-one in the offset makes plausible rubbish of.
191 ranged "a window in the middle" -r 5242880-5243391 \
192 -- 206 "bytes 5242880-5243391/$SIZE" 5242880 512
193 # More than there is, from a start that exists: clamped, not refused.
194 ranged "an end past the end" -r $((SIZE-10))-99999999 \
195 -- 206 "bytes $((SIZE-10))-$((SIZE-1))/$SIZE" $((SIZE-10)) 10
196 # A suffix longer than the file is the whole file.
197 ranged "a suffix longer than the file" -r -99999999 \
198 -- 206 "bytes 0-$((SIZE-1))/$SIZE" 0 $SIZE
199
200 echo
201 echo " -- the refusals, ${label} --"
202 h="$RIG_DIR/h.$$"
203
204 code=$(curl -sk $HTTPARG -r 999999999- -D "$h" -o /dev/null -w '%{http_code}' "$B/clip.mp4")
205 check "a start past the end: status" "$code" "416"
206 check "a start past the end: content-range" "$(field "$h" content-range)" "bytes */$SIZE"
207
208 code=$(curl -sk $HTTPARG -H "Range: bytes=-0" -D "$h" -o /dev/null -w '%{http_code}' \
209 "$B/clip.mp4")
210 check "the last nothing: status" "$code" "416"
211 check "the last nothing: content-range" "$(field "$h" content-range)" "bytes */$SIZE"
212
213 code=$(curl -sk $HTTPARG -r 0-99 -D "$h" -o /dev/null -w '%{http_code}' "$B/empty.mp4")
214 check "an empty file satisfies nothing: status" "$code" "416"
215 check "an empty file satisfies nothing: content-range" \
216 "$(field "$h" content-range)" "bytes */0"
217
218 echo
219 echo " -- what is ignored rather than refused, ${label} --"
220 # RFC 9110 §14.2: a unit we do not implement, and a field that does not
221 # parse, are ignored -- the client gets the file, not a rejection.
222 for bad in "items=0-9" "bytes=99-10" "bytes=abc-def" "bytes=" "chunks=0-"; do
223 code=$(curl -sk $HTTPARG -H "Range: $bad" -D "$h" -o /dev/null -w '%{http_code}' \
224 "$B/clip.mp4")
225 check "'$bad' is ignored, not refused" "$code" "200"
226 check "'$bad' gets the whole file" "$(field "$h" content-length)" "$SIZE"
227 done
228 # Several ranges at once: recognised, and answered with the whole file
229 # rather than a multipart body.
230 code=$(curl -sk $HTTPARG -H "Range: bytes=0-49,100-149" -D "$h" -o /dev/null \
231 -w '%{http_code}' "$B/clip.mp4")
232 check "several ranges are answered whole" "$code" "200"
233 check "and carry the whole length" "$(field "$h" content-length)" "$SIZE"
234 hasnt "and no multipart body" "$(field "$h" content-type)" "multipart"
235
236 echo
237 echo " -- HEAD, ${label} --"
238 hd=$(curl -sk $HTTPARG -I "$B/clip.mp4")
239 has "HEAD advertises byte ranges" "$hd" "accept-ranges: bytes"
240 has "HEAD states the full length" "$hd" "content-length: $SIZE"
241 has "HEAD names the recording's type" "$hd" "content-type: video/mp4"
242 body=$(curl -sk $HTTPARG -I -o "$RIG_DIR/hb.$$" -w '%{size_download}' "$B/clip.mp4")
243 check "HEAD sends no body" "$body" "0"
244 # RFC 9110 §14.2 defines range handling for GET alone and requires a server
245 # to ignore the field on any other method. So a HEAD carrying a Range is
246 # answered about the whole file: a 206 there would name a window nobody can
247 # read and understate the size of the thing being asked about.
248 hd=$(curl -sk $HTTPARG -I -H "Range: bytes=0-99" -D - -o /dev/null "$B/clip.mp4")
249 has "a ranged HEAD ignores the range" "$hd" "200 OK"
250 hasnt "and names no window" "$hd" "content-range"
251 has "and states the full length" "$hd" "content-length: $SIZE"
252
253 echo
254 echo " -- a HEAD of something worth encoding, ${label} --"
255 # The GET is encoded, and says so.
256 hd=$(curl -sk $HTTPARG -H "Accept-Encoding: gzip" -D - -o /dev/null "$B/bulk.css")
257 has "a GET that accepts gzip is encoded" "$hd" "content-encoding: gzip"
258 has "and says it varies by coding" "$hd" "vary: accept-encoding"
259 # The HEAD is not: encoding it would mean reading and compressing the whole
260 # file to throw the result away. So it reports the identity length, which is
261 # what a GET accepting no coding would be told, and names no coding it has
262 # not applied.
263 hd=$(curl -sk $HTTPARG -I -H "Accept-Encoding: gzip" -D - -o /dev/null "$B/bulk.css")
264 has "a HEAD that accepts gzip is not encoded" "$hd" "200 OK"
265 hasnt "and names no coding" "$hd" "content-encoding"
266 has "and states the identity length" "$hd" "content-length: $BULK_LEN"
267 has "and still says it varies by coding" "$hd" "vary: accept-encoding"
268 # The property a live monitor rests on: a plain HEAD is the GET's byte count.
269 hd=$(curl -sk $HTTPARG -I -D - -o /dev/null "$B/bulk.css")
270 has "a plain HEAD states the GET's byte count" "$hd" "content-length: $BULK_LEN"
271 ch="$RIG_DIR/ch.$$"
272 curl -sk $HTTPARG -D "$ch" -o /dev/null "$B/bulk.css"
273 check "and the GET agrees" "$(field "$ch" content-length)" "$BULK_LEN"
274 rm -f "$ch"
275
276 echo
277 echo " -- a one byte file, ${label} --"
278 code=$(curl -sk $HTTPARG -r 0- -D "$h" -o /dev/null -w '%{http_code}' "$B/one.mp4")
279 check "the only byte: status" "$code" "206"
280 check "the only byte: content-range" "$(field "$h" content-range)" "bytes 0-0/1"
281 code=$(curl -sk $HTTPARG -r 1- -D "$h" -o /dev/null -w '%{http_code}' "$B/one.mp4")
282 check "one past it: status" "$code" "416"
283
284 rm -f "$h" "$RIG_DIR/hb.$$"
285done
286
287echo
288echo "== the connection survives a window =="
289# A body shorter or longer than the Content-Length promised desynchronises every
290# message after it, which only shows up when two requests share a connection.
291# `--next` starts a second request on the same connection rather than a second
292# connection, which is the only way the desynchronisation shows.
293two=$(curl -sk -r 0-99 -o "$RIG_DIR/a1" -w '%{http_code} ' "$B/clip.mp4" \
294 --next -sk -r 200-299 -o "$RIG_DIR/a2" -w '%{http_code}' "$B/clip.mp4")
295check "two windows down one connection" "$two" "206 206"
296slice 0 100 "$RIG_DIR/e1"; slice 200 100 "$RIG_DIR/e2"
297if cmp -s "$RIG_DIR/e1" "$RIG_DIR/a1" && cmp -s "$RIG_DIR/e2" "$RIG_DIR/a2"
298then ok "and both windows are the right bytes"
299else no "a window on a kept-alive connection came out wrong"; fi
300
301if [ "${RIG_TRANSCRIPT:-0}" = "1" ]; then
302 echo
303 echo "== transcripts =="
304 # The exchanges themselves, for a report or a bug: request line, request
305 # fields, status line, response fields. Bodies are dropped.
306 for spec in "an un-ranged GET::" \
307 "the first hundred bytes:-r 0-99:" \
308 "from a hundred to the end:-r 100-:" \
309 "the last fifty bytes:-r -50:" \
310 "a start past the end:-r 999999999-:" \
311 "several ranges at once::-H Range: bytes=0-49,100-149" \
312 "a HEAD:-I:"
313 do
314 name="${spec%%:*}"; rest="${spec#*:}"
315 args="${rest%%:*}"; hdr="${rest#*:}"
316 echo
317 echo "--- $name ---"
318 if [ -n "$hdr" ]; then
319 curl -sk -o /dev/null -v $args -H "${hdr#-H }" "$B/clip.mp4" 2>&1 \
320 | grep -E "^[<>] " | grep -v "^> $" | grep -v "^< $"
321 else
322 curl -sk -o /dev/null -v $args "$B/clip.mp4" 2>&1 \
323 | grep -E "^[<>] " | grep -v "^> $" | grep -v "^< $"
324 fi
325 done
326fi
327
328echo
329echo "== $pass passed, $fail failed =="
330[ "$fail" = "0" ] || RIG_KEEP="${RIG_KEEP:-0}"
331exit $([ "$fail" = "0" ] && echo 0 || echo 1)