forked from avleen/bashttpd
/
bashttpd
executable file
·269 lines (224 loc) · 6.86 KB
/
bashttpd
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#!/usr/bin/env bash
#
# A simple, configurable HTTP server written in bash.
#
# See LICENSE for licensing information.
#
# Original author: Avleen Vig, 2012
# Reworked by: Josh Cartwright, 2012
warn() { echo "WARNING: $@" >&2; }
[ -r bashttpd.conf ] || {
cat >bashttpd.conf <<'EOF'
#
# bashttpd.conf - configuration for bashttpd
#
# The behavior of bashttpd is dictated by the evaluation
# of rules specified in this configuration file. Each rule
# is evaluated until one is matched. If no rule is matched,
# bashttpd will serve a 500 Internal Server Error.
#
# The format of the rules are:
# on_uri_match REGEX command [args]
# unconditionally command [args]
#
# on_uri_match:
# On an incoming request, the URI is checked against the specified
# (bash-supported extended) regular expression, and if encounters a match the
# specified command is executed with the specified arguments.
#
# For additional flexibility, on_uri_match will also pass the results of the
# regular expression match, ${BASH_REMATCH[@]} as additional arguments to the
# command.
#
# unconditionally:
# Always serve via the specified command. Useful for catchall rules.
#
# The following commands are available for use:
#
# serve_file FILE
# Statically serves a single file.
#
# serve_dir_with_tree DIRECTORY
# Statically serves the specified directory using 'tree'. It must be
# installed and in the PATH.
#
# serve_dir_with_ls DIRECTORY
# Statically serves the specified directory using 'ls -al'.
#
# serve_dir DIRECTORY
# Statically serves a single directory listing. Will use 'tree' if it is
# installed and in the PATH, otherwise, 'ls -al'
#
# serve_dir_or_file_from DIRECTORY
# Serves either a directory listing (using serve_dir) or a file (using
# serve_file). Constructs local path by appending the specified root
# directory, and the URI portion of the client request.
#
# serve_static_string STRING
# Serves the specified static string with Content-Type text/plain.
#
# Examples of rules:
#
# on_uri_match '^/issue$' serve_file "/etc/issue"
#
# When a client's requested URI matches the string '/issue', serve them the
# contents of /etc/issue
#
# on_uri_match 'root' serve_dir /
#
# When a client's requested URI has the word 'root' in it, serve up
# a directory listing of /
#
# DOCROOT=/var/www/html
# on_uri_match '/(.*)' serve_dir_or_file_from "$DOCROOT"
# When any URI request is made, attempt to serve a directory listing
# or file content based on the request URI, by mapping URI's to local
# paths relative to the specified "$DOCROOT"
#
unconditionally serve_static_string 'Hello, world! You can configure bashttpd by modifying bashttpd.conf.'
# More about commands:
#
# It is possible to somewhat easily write your own commands. An example
# may help. The following example will serve "Hello, $x!" whenever
# a client sends a request with the URI /say_hello_to/$x:
#
# serve_hello() {
# add_response_header "Content-Type" "text/plain"
# send_response_ok_exit <<< "Hello, $2!"
# }
# on_uri_match '^/say_hello_to/(.*)$' serve_hello
#
# Like mentioned before, the contents of ${BASH_REMATCH[@]} are passed
# to your command, so its possible to use regular expression groups
# to pull out info.
#
# With this example, when the requested URI is /say_hello_to/Josh, serve_hello
# is invoked with the arguments '/say_hello_to/Josh' 'Josh',
# (${BASH_REMATCH[0]} is always the full match)
EOF
warn "Created bashttpd.conf using defaults. Please review it/configure before running bashttpd again."
exit 1
}
recv() { echo "< $@" >&2; }
send() { echo "> $@" >&2;
printf '%s\r\n' "$*"; }
[[ $UID = 0 ]] && warn "It is not recommended to run bashttpd as root."
DATE=$(date +"%a, %d %b %Y %H:%M:%S %Z")
declare -a RESPONSE_HEADERS=(
"Date: $DATE"
"Expires: $DATE"
"Server: Slash Bin Slash Bash"
)
add_response_header() {
RESPONSE_HEADERS+=("$1: $2")
}
declare -a HTTP_RESPONSE=(
[200]="OK"
[400]="Bad Request"
[403]="Forbidden"
[404]="Not Found"
[405]="Method Not Allowed"
[500]="Internal Server Error"
)
send_response() {
local code=$1
send "HTTP/1.0 $1 ${HTTP_RESPONSE[$1]}"
for i in "${RESPONSE_HEADERS[@]}"; do
send "$i"
done
send
while read -r line; do
send "$line"
done
}
send_response_ok_exit() { send_response 200; exit 0; }
fail_with() {
send_response "$1" <<< "$1 ${HTTP_RESPONSE[$1]}"
exit 1
}
serve_file() {
local file=$1
read -r CONTENT_TYPE < <(file -b --mime-type "$file") && \
add_response_header "Content-Type" "$CONTENT_TYPE"
read -r CONTENT_LENGTH < <(stat -c'%s' "$file") && \
add_response_header "Content-Length" "$CONTENT_LENGTH"
send_response_ok_exit < "$file"
}
serve_dir_with_tree()
{
local dir="$1" tree_vers tree_opts basehref x
add_response_header "Content-Type" "text/html"
# The --du option was added in 1.6.0.
read x tree_vers x < <(tree --version)
[[ $tree_vers == v1.6* ]] && tree_opts="--du"
send_response_ok_exit < \
<(tree -H "$2" -L 1 "$tree_opts" -D "$dir")
}
serve_dir_with_ls()
{
local dir=$1
add_response_header "Content-Type" "text/plain"
send_response_ok_exit < \
<(ls -la "$dir")
}
serve_dir() {
local dir=$1
# If `tree` is installed, use that for pretty output.
which tree &>/dev/null && \
serve_dir_with_tree "$@"
serve_dir_with_ls "$@"
fail_with 500
}
serve_dir_or_file_from() {
local URL_PATH=$1/$3
shift
# sanitize URL_PATH
URL_PATH=${URL_PATH//[^a-zA-Z0-9_~\-\.\/]/}
[[ $URL_PATH == *..* ]] && fail_with 400
# Serve index file if exists in requested directory
[[ -d $URL_PATH && -f $URL_PATH/index.html && -r $URL_PATH/index.html ]] && \
URL_PATH="$URL_PATH/index.html"
if [[ -f $URL_PATH ]]; then
[[ -r $URL_PATH ]] && \
serve_file "$URL_PATH" "$@" || fail_with 403
elif [[ -d $URL_PATH ]]; then
[[ -x $URL_PATH ]] && \
serve_dir "$URL_PATH" "$@" || fail_with 403
fi
fail_with 404
}
serve_static_string() {
add_response_header "Content-Type" "text/plain"
send_response_ok_exit <<< "$1"
}
on_uri_match() {
local regex=$1
shift
[[ $REQUEST_URI =~ $regex ]] && \
"$@" "${BASH_REMATCH[@]}"
}
unconditionally() {
"$@" "$REQUEST_URI"
}
# Request-Line HTTP RFC 2616 $5.1
read -r line || fail_with 400
# strip trailing CR if it exists
line=${line%%$'\r'}
recv "$line"
read -r REQUEST_METHOD REQUEST_URI REQUEST_HTTP_VERSION <<<"$line"
[ -n "$REQUEST_METHOD" ] && \
[ -n "$REQUEST_URI" ] && \
[ -n "$REQUEST_HTTP_VERSION" ] \
|| fail_with 400
# Only GET is supported at this time
[ "$REQUEST_METHOD" = "GET" ] || fail_with 405
declare -a REQUEST_HEADERS
while read -r line; do
line=${line%%$'\r'}
recv "$line"
# If we've reached the end of the headers, break.
[ -z "$line" ] && break
REQUEST_HEADERS+=("$line")
done
source bashttpd.conf
fail_with 500