eris

Very small inetd http server
git clone https://git.woozle.org/neale/eris.git

Neale Pickett  ·  2015-02-26

test.sh

  1#! /bin/sh
  2
  3: ${HTTPD:=./eris}
  4: ${HTTPD_CGI:=./eris -c}
  5: ${HTTPD_IDX:=./eris -d}
  6
  7tests=0
  8successes=0
  9failures=0
 10
 11H () {
 12    section="$*"
 13    printf "\n%-20s " "$*"
 14}
 15
 16title() {
 17    thistest="$1"
 18    tests=$(expr $tests + 1)
 19}
 20
 21successes=0
 22pass () {
 23    printf '.'
 24    successes=$(expr $successes + 1)
 25}
 26
 27failures=0
 28fail () {
 29    printf '(%s)' "$thistest"
 30    failures=$(expr $failures + 1)
 31}
 32
 33d () {
 34    tr '\r\n' '#%'
 35}
 36
 37
 38###
 39### Make web space
 40###
 41mkdir -p default
 42echo james > default/index.html
 43touch default/a
 44
 45cat <<'EOD' > default/a.cgi
 46#! /bin/sh
 47echo 'Content-type: text/plain'
 48echo
 49set | sort
 50ls *.cgi
 51EOD
 52chmod +x default/a.cgi
 53
 54cat <<'EOD' > default/mongo.cgi
 55#! /bin/sh
 56echo 'Content-type: application/octet-stream'
 57echo
 58dd if=/dev/zero bs=1000 count=800 2>/dev/null
 59echo 'james'
 60EOD
 61chmod +x default/mongo.cgi
 62
 63cat <<'EOD' > default/redir.cgi
 64#! /bin/sh
 65echo "Location: http://example.com/froot"
 66EOD
 67chmod +x default/redir.cgi
 68
 69cat <<'EOD' > default/status.cgi
 70#! /bin/sh
 71case "$PATH_INFO" in
 72/empty)
 73	echo "Status"
 74	exit 0
 75	;;
 76/nostat)
 77	echo "Status: "
 78	;;
 79*)
 80	echo "Status: 300 wat"
 81	;;
 82esac
 83echo "Merf: merf"
 84echo
 85echo "james"
 86EOD
 87chmod +x default/status.cgi
 88
 89mkdir -p default/empty
 90mkdir -p default/subdir
 91touch default/subdir/a
 92touch default/subdir/.hidden
 93###
 94###
 95###
 96
 97echo "HTTPD: $HTTPD  "
 98echo "CGI:   $HTTPD_CGI  "
 99echo "IDX:   $HTTPD_IDX  "
100
101
102
103
104H "Basic tests"
105
106title "GET /index.html"
107printf 'GET /index.html HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | d | grep -q 'HTTP/1.0 200 OK#%Server: eris/[0-9.a-z]*#%Connection: close#%Content-Type: text/html; charset=UTF-8#%Content-Length: 6#%Last-Modified: ..., .. ... 20.. ..:..:.. GMT#%#%james%' && pass || fail
108
109title "GET /"
110printf 'GET / HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | d | grep -q 'HTTP/1.0 200 OK#%Server: eris/[0-9.a-z]*#%Connection: close#%Content-Type: text/html; charset=UTF-8#%Content-Length: 6#%Last-Modified: ..., .. ... 20.. ..:..:.. GMT#%#%james%' && pass || fail
111
112title "Keepalive"
113printf 'GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\n\r\n' | $HTTPD 2>/dev/null | grep -c 'james' | grep -q 2 && pass || fail
114
115title "POST"
116printf 'POST / HTTP/1.0\r\nContent-Type: a\r\nContent-Length: 5\r\n\r\njames' | $HTTPD 2>/dev/null | d | grep -q 'HTTP/1.0 405 ' && pass || fail
117
118title "HTTP/1.2"
119printf 'GET / HTTP/1.2\r\n\r\n' | $HTTPD 2>/dev/null | d | grep -q 'HTTP/1.. 505 .*ction: close' && pass || fail
120
121title "HTTP/1.12"
122printf 'GET / HTTP/1.12\r\n\r\n' | $HTTPD 2>/dev/null | d | grep -q 'HTTP/1.. 505 .*ction: close' && pass || fail
123
124title "Bare newline"
125printf 'GET / HTTP/1.0\n\n' | $HTTPD 2>/dev/null | grep -q 'james' && pass || fail
126
127title "No trailing slash"
128printf 'GET /empty HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | d | grep -q '301 Redirect#%.*Location: /empty/#%#%' && pass || fail
129
130title "No version after query_string"
131printf 'GET /?\r\n\r\n' | $HTTPD 2>/dev/null | d | grep -q 'HTTP/0.9' && pass || fail
132
133title "Logging /"
134(printf 'GET / HTTP/1.1\r\nHost: host\r\n\r\n' | 
135    PROTO=TCP TCPREMOTEPORT=1234 TCPREMOTEIP=10.0.0.2 $HTTPD >/dev/null) 2>&1 | grep -q '^10.0.0.2:1234 200 6 host (null) (null) /$' && pass || fail
136
137title "Logging /index.html"
138(printf 'GET /index.html HTTP/1.1\r\nHost: host\r\n\r\n' | 
139    PROTO=TCP TCPREMOTEPORT=1234 TCPREMOTEIP=10.0.0.2 $HTTPD >/dev/null) 2>&1 | grep -q '^10.0.0.2:1234 200 6 host (null) (null) /index.html$' && pass || fail
140
141title "Logging busybox"
142(printf 'GET /index.html HTTP/1.1\r\nHost: host\r\n\r\n' | 
143    PROTO=TCP TCPREMOTEADDR=[::1]:8765 $HTTPD >/dev/null) 2>&1 | grep -Fxq '[::1]:8765 200 6 host (null) (null) /index.html' && pass || fail
144
145title "Logging stunnel"
146(printf 'GET /index.html HTTP/1.1\r\nHost: host\r\n\r\n' | 
147    REMOTE_HOST=::1 REMOTE_PORT=8765 $HTTPD >/dev/null) 2>&1 | grep -Fxq '::1:8765 200 6 host (null) (null) /index.html' && pass || fail
148
149
150
151H "Options"
152
153title "-."
154printf 'GET /eris HTTP/1.0\r\n\r\n' | $HTTPD -. 2>/dev/null | grep -q 'HTTP/1.. 200 OK' && pass || fail
155
156
157
158H "Tomfoolery"
159
160title "Non-header"
161printf 'GET / HTTP/1.0\r\na: b\r\nfoo\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 400 ' && pass || fail
162
163title "Huge header field"
164(printf 'GET / HTTP/1.0\r\nHeader: '
165 dd if=/dev/zero bs=1k count=9 2>/dev/null | tr '\0' '.'
166 printf '\r\n\r\n') | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 431 ' && pass || fail
167
168title "Too many headers"
169(printf 'GET / HTTP/1.0\r\n'
170 for i in $(seq 500); do
171     printf 'Header: val\r\n'
172 done
173 printf '\r\n') | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 431 ' && pass || fail
174 
175 title "Directory traversal"
176 printf 'GET /../default/index.html HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 404' && pass || fail
177 
178 title "Escaped directory traversal"
179 printf 'GET /%%2e%%2e/default/index.html HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 404' && pass || fail
180
181
182H "If-Modified-Since"
183
184title "Has been modified"
185printf 'GET / HTTP/1.0\r\nIf-Modified-Since: Sun, 27 Feb 1980 12:12:12 GMT\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 200 ' && pass || fail
186
187title "Exact same date"
188ims=$(printf 'GET / HTTP/1.0\r\n\r\n' | $HTTPD 2>/dev/null | awk -F ': ' '/Last-Modified/ {print $2;}')
189printf 'GET / HTTP/1.0\r\nIf-Modified-Since: %s\r\n\r\n' "$ims" | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 304 ' && pass || fail
190
191title "RFC 822 Date"
192printf 'GET / HTTP/1.0\r\nIf-Modified-Since: Sun, 27 Feb 2030 12:12:12 GMT\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 304 ' && pass || fail
193
194title "RFC 850 Date"
195printf 'GET / HTTP/1.0\r\nIf-Modified-Since: Sunday, 27-Feb-30 12:12:12 GMT\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 304 ' && pass || fail
196
197title "RFC 850 Thursday"
198printf 'GET / HTTP/1.0\r\nIf-Modified-Since: Thursday, 27-Feb-30 12:12:12 GMT\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 304 ' && pass || fail
199
200title "ANSI C Date"
201printf 'GET / HTTP/1.0\r\nIf-Modified-Since: Sun Feb 27 12:12:12 2030\r\n\r\n' | $HTTPD 2>/dev/null | grep -q 'HTTP/1.. 304 ' && pass || fail
202
203title "ims persist"
204printf 'GET / HTTP/1.1\r\nIf-Modified-Since: %s\r\n\r\nGET / HTTP/1.0\r\n\r\n' "$ims" | $HTTPD 2>/dev/null | d | grep -q 'HTTP/1.. 304.*HTTP/1.. 200' && pass || fail
205
206title "Logging"
207(printf 'GET / HTTP/1.0\r\nIf-Modified-Since: %s\r\n\r\n' "$ims" | $HTTPD > /dev/null) 2>&1 | grep -q '304' && pass || fail
208
209
210
211H "Directory indexing"
212
213title "Basic index"
214printf 'GET /empty/ HTTP/1.0\r\n\r\n' | $HTTPD_IDX 2>/dev/null | d | grep -Fq '<h1>Directory Listing: /empty/</h1><pre>%<a href="../">Parent Directory</a>%</pre>' && pass || fail
215title "Hidden file"
216printf 'GET /subdir/ HTTP/1.0\r\n\r\n' | $HTTPD_IDX 2>/dev/null | grep -q 'hidden' && fail || pass
217
218title "Logging"
219(printf 'GET /empty/ HTTP/1.0\r\n\r\n' |
220    PROTO=TCP TCPREMOTEPORT=1234 TCPREMOTEIP=10.0.0.2 $HTTPD_IDX >/dev/null) 2>&1 | grep -q '^10.0.0.2:1234 200 0 (null) (null) (null) /empty/$' && pass || fail
221
222
223H "CGI"
224
225title "Basic CGI"
226printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | \
227    $HTTPD_CGI 2>/dev/null | d | grep -Eq 'HTTP/1.0 200 OK#%Server: .*#%Connection: close#%Pragma: no-cache#%Content-type: text/plain#%#%.*%GATEWAY_INTERFACE=.?CGI/1.1.?%' && pass || fail
228
229title "CGI chdir"
230printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | \
231    $HTTPD_CGI 2>/dev/null | d | grep -Eq '%a.cgi%' && pass || fail
232
233title "REQUEST_METHOD"
234printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | \
235    $HTTPD_CGI 2>/dev/null | grep -Eq 'REQUEST_METHOD=.?GET.?$' && pass || fail
236
237title "GET with arguments"
238printf 'GET /a.cgi?foo HTTP/1.0\r\n\r\n' | \
239    $HTTPD_CGI 2>/dev/null | grep -Eq 'QUERY_STRING=.?foo.?$' && pass || fail
240
241title "GET with complex args"
242printf 'GET /a.cgi?t=New+Mexico+Land+Of+Enchantment&s=LG8+LV32+R4+G32+LG32+Y4+LG4 HTTP/1.0\r\n\r\n' | \
243    $HTTPD_CGI 2>/dev/null | d | grep -Fq 't=New+Mexico' && pass || fail
244
245title "POST"
246printf 'POST /a.cgi HTTP/1.0\r\nContent-Type: moo\r\nContent-Length: 3\r\n\r\narf' | \
247    $HTTPD_CGI 2>/dev/null | d | grep -Eq '%CONTENT_LENGTH=.?3.?%CONTENT_TYPE=.?moo.?%' && pass || fail
248
249title "PATH_INFO"
250printf 'GET /a.cgi/merf HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | grep -Eq '^PATH_INFO=.?/merf.?$' && pass || fail
251
252title "SERVER_PROTOCOL"
253printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | d | grep -Eq '%SERVER_PROTOCOL=.?HTTP/1.0[^#%]?%[^#%]' && pass || fail
254
255title "Large response"
256printf 'GET /mongo.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | grep -q james && pass || fail
257
258title "Redirect"
259printf 'GET /redir.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | grep -Fq 'Location: http://example.com/froot' && pass || fail
260
261title "Status"
262printf 'GET /status.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>&1 | d | grep -q '^HTTP/1.0 300 wat#%.*.null. 300 6' && pass || fail
263
264title "Status bug"
265printf 'GET /status.cgi/empty HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | d | grep -q '^HTTP/1.0 500 ' && pass || fail
266
267title "No status"
268printf 'GET /status.cgi/nostat HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | d | grep -q '^HTTP/1.0 500 ' && pass || fail
269
270
271H "Timeouts"
272
273title "Read timeout"
274(sleep 2.1; printf 'GET / HTTP/1.0\r\n\r\n') | $HTTPD 2>/dev/null | grep -q '.' && fail || pass
275
276
277H "CONNECT handler"
278
279title "Basic test"
280printf 'CONNECT /etc HTTP/1.1\r\n\r\n' | $HTTPD -o /bin/ls | grep -q passwd && pass || fail
281
282
283H "fnord bugs"
284
285# 1. Should return directory listing of /; instead segfaults
286title "Directory indexing of /"
287printf 'GET / HTTP/1.0\r\nHost: empty\r\n\r\n' | $HTTPD_IDX 2>/dev/null | grep -q 200 && pass || fail
288
289# 2. Should output \r\n\r\n; instead outputs \r\n\n
290title "CGI output bare newlines"
291printf 'GET /a.cgi HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | d | grep -q '#%#%' && pass || fail
292
293## Note: fnord gets a pass on this since it only claims to be an HTTP/1.0
294## server.  Eris is not 1.1 compliant either, but it at least tries to fake it.  You
295## should consider how much of HTTP/1.1 you want before deploying either.  In practice,
296## with browsers, both seems sufficient.  Some tools, notably httperf, fail
297## with fnord.
298# 3. Should process both requests; instead drops second
299title "Multiple requests in one packet"
300printf 'GET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\nGET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n' | $HTTPD 2>/dev/null | grep -c '^HTTP/1.' | grep -q 2 && pass || fail
301
302## Skip: eris ignores Accept header (fnord does too, as this bug demonstrates)
303# 4. Should return 406 Not Acceptable; instead ignores Accept header
304#title "Accept header"
305#printf 'GET / HTTP/1.0\r\nAccept: nothing\r\n\r\n' | $HTTPD 2>/dev/null | grep 406 && pass || fail
306
307# 5. Should serve second request as default MIME-Type (text/plain); instead uses previous mime type
308title "Second MIME-Type"
309(printf 'GET / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n'
310 ls / > /dev/null    # Delay required to work around test #3
311 printf 'GET /a HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\n\r\n') | $HTTPD 2>/dev/null | grep -q 'text/plain\|application/octet-stream' && pass || fail
312
313## Skip: eris doesn't allow POST to static HTML
314# 6. Should consume POST data; instead tries to read POST data as second request
315#title "POST to static HTML"
316#(printf 'POST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\n';
317# ls / > /dev/null
318# printf 'aPOST / HTTP/1.1\r\nHost: a\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nContent-Length: 1\r\n\r\na') | $HTTPD 2>/dev/null | grep -c '200 OK' | grep -q 2 && pass || fail
319
320# 7. HTTP/1.1 should default to keepalive; instead connection is closed
321title "HTTP/1.1 default keepalive"
322(printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n'
323 ls / >/dev/null
324 printf 'GET / HTTP/1.1\r\nHost: a\r\n\r\n') | $HTTPD 2>/dev/null | grep -c '^HTTP/' | grep -q 2 && pass || fail
325
326# 8. Should parse "Thursday"; instead assumes all day names are 6 characters long
327title "RFC 850 Date"
328printf 'GET / HTTP/1.0\r\nIf-Modified-Since: Thursday, 27-Feb-30 12:12:12 GMT\r\n\r\n' | $HTTPD 2>/dev/null | grep -q '304 Not Changed' && pass || fail
329
330# 9. Should set PATH_INFO to /; instead sets it to /index.html
331title "PATH_INFO=/"
332printf 'GET /a.cgi/ HTTP/1.0\r\n\r\n' | $HTTPD_CGI 2>/dev/null | grep -Eq 'PATH_INFO=.?/.?$' && pass || fail
333
334echo
335echo
336echo "$successes of $tests tests passed ($failures failed)."
337
338exit $failures