Last active
March 10, 2018 03:52
-
-
Save mentha/78d29ffe9cc2ca833caa3d756af85989 to your computer and use it in GitHub Desktop.
Simple HTTP Server in shell command language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/sh | |
# This is free and unencumbered software released into the public domain. | |
if (file &> /dev/null; test $? -eq 127); then | |
file () { | |
# file -Lbi file | |
local l_mime | |
case "$(echo "$2" | rev | cut -d . -f1 | rev)" in | |
html) l_mime=text/html;; | |
css) l_mime=text/css;; | |
js) l_mime=application/javascript;; | |
txt) l_mime=text/plain;; | |
c) l_mime=text/plain;; | |
h) l_mime=text/plain;; | |
sh) l_mime=text/plain;; | |
py) l_mime=text/plain;; | |
conf) l_mime=text/plain;; | |
*) l_mime=application/octet-stream;; | |
esac | |
echo "$l_mime" | |
} | |
fi | |
procopts () { | |
local l_opt | |
eval "$(showdefconf)" | |
while getopts 'idvb:p:r:s:c:g' l_opt | |
do | |
case $l_opt in | |
i)HTTPD_INETD=1;; | |
d)HTTPD_DAEMONIZE=1;; | |
v)HTTPD_VERBOSE=1;; | |
b)HTTPD_IP="$OPTARG";; | |
p)HTTPD_PORT="$OPTARG";; | |
r)HTTPD_ROOT="$OPTARG";; | |
s)HTTPD_CGIREGEX="$OPTARG";; | |
c)source "$OPTARG" || return 1;; | |
g)showdefconf; exit 0;; | |
?)return 1;; | |
esac | |
done | |
return 0 | |
} | |
showhelp () { | |
printf 'Usage: httpd\n' | |
printf 'Simple HTTP server\n' | |
printf '\n' | |
printf '\t-i Inetd mode\n' | |
printf '\t-d Fork into background and log to syslog\n' | |
printf '\t-v Verbose\n' | |
printf '\t-b IP Bind to IP (default ::)\n' | |
printf '\t-p PORT Bind to PORT (default 8080)\n' | |
printf '\t-r ROOT Set webroot to ROOT (default .)\n' | |
printf '\t-s REGEX CGI path regex\n' | |
printf '\t-c FILE Configuration file\n' | |
printf '\t-g Print default configuration file to stdout\n' | |
} | |
showdefconf () { | |
cut -b2- << EOF | |
# Configuration for httpd.sh | |
# The built-in default conf is sourced before parsing options. | |
# If you used -c option, the conf file would be sourced when processing | |
# that option, and may override options given before it. | |
# Inetd mode | |
HTTPD_INETD=0 | |
# Daemonize | |
HTTPD_DAEMONIZE=0 | |
# Verbose logging (include debug) | |
HTTPD_VERBOSE=0 | |
# bind address | |
HTTPD_IP='::' | |
# bind port | |
HTTPD_PORT='8080' | |
# webroot | |
HTTPD_ROOT='.' | |
# Rewrite sanitized path using sed script | |
# Setting it to empty string disables this function | |
HTTPD_REWRITE='' | |
# HTTP Basic authentication | |
# Setting it to empty string disables this function | |
HTTPD_BASIC_AUTH='' | |
# Example for user 'user' and password 'passw0rd' | |
# HTTPD_BASIC_AUTH='dXNlcjpwYXNzdzByZA==' | |
# CGI regex | |
# requests matching this regex would be passed to CGI | |
# Setting it to empty string disables this function | |
HTTPD_CGIREGEX='' | |
# CGI header blacklist regex | |
# HTTP headers matching this regex would be passed in variables with | |
# HTTP_BLACKLISTED_ prefix | |
HTTPD_CGI_HEADER_BLACKLIST='^PROXY$' | |
# 'Proxy' header would be passed in HTTP_BLACKLISTED_PROXY | |
EOF | |
} | |
log () { | |
local l_loglvl="$1" | |
local l_logfmt="$2" | |
shift 2 | |
local l_logmsg="$(printf "$l_logfmt" "$@")" | |
if [ $HTTPD_DAEMONIZE -eq 0 ]; then | |
printf '[' 1>&2 | |
tput bold 1>&2 | |
printf "$l_loglvl" 1>&2 | |
tput sgr0 1>&2 | |
printf ']' 1>&2 | |
echo "$l_logmsg" 1>&2 | |
else | |
logger "$l_loglvl: $l_logmsg" | |
fi | |
} | |
log_error () { | |
log ERROR "$@" | |
} | |
log_info () { | |
log INFO "$@" | |
} | |
log_debug () { | |
if [ $HTTPD_VERBOSE -eq 0 ]; then | |
return 0 | |
fi | |
log DEBUG "$@" | |
} | |
toupper () { | |
tr '[[:lower:]]' '[[:upper:]]' | |
} | |
shquote () { | |
printf "'$(echo "$1" | sed 's/'\''/\\'\''/g')'" | |
} | |
urlsanitize () { | |
local l_url="$1" | |
local l_url="$(printf "$(echo "$l_url" | sed 's/@/\\x/g')")" | |
while echo "$l_url" | grep '//' &> /dev/null; do | |
l_nurl=$(echo "$l_url" | sed 's@//@/@g;') | |
if [ "$l_url" = "$l_nurl" ]; then | |
return 1 | |
fi | |
l_url="$l_nurl" | |
done | |
while echo "$l_url" | grep '/\.\./' &> /dev/null; do | |
l_nurl=$(echo "$l_url" | sed 's@/[^/]*/\.\.\(/\)@\1@g') | |
if [ "$l_url" = "$l_nurl" ]; then | |
return 1 | |
fi | |
l_url="$l_nurl" | |
done | |
echo "$l_url" | |
log_debug 'Sanitize URL "%s" -> "%s"' "$1" "$l_url" | |
return 0 | |
} | |
httphandler () { | |
log_debug 'Entering HTTP handler' | |
local l_start | |
read -r l_start | |
log_debug 'Got start line %s' "$(shquote "$l_start")" | |
# 'national' in URL not supported | |
echo "$l_start" | grep -E '^[A-Z]+ /(([-a-zA-Z0-9$_.!*'\''(),:@&=+]|%[0-9a-fA-F]{2})+(/([-a-zA-Z0-9$_.!*'\''(),:@&=+]|%[0-9a-fA-F]{2})*)*)?(;([-a-zA-Z0-9$_.!*'\''(),:@&=+/]|%[0-9a-fA-F]{2})*)*(\?([-a-zA-Z0-9$_.!*'\''(),;/?:@&=+]|%[0-9a-fA-F]{2})*)? HTTP/[0-9]+.[0-9]+'$'\r''$' &> /dev/null | |
if [ $? -ne 0 ]; then | |
# Invalid request | |
printf 'HTTP/1.0 400 Bad Request\r\n\r\n' | |
return | |
fi | |
log_debug 'Valid start line' | |
REQUEST_METHOD="$(echo "$l_start" | cut -d' ' -f1 | toupper)" | |
export REQUEST_METHOD | |
log_debug 'Got method %s' "$REQUEST_METHOD" | |
REQUEST_URI="$(echo "$l_start" | cut -d' ' -f2)" | |
export REQUEST_URI | |
log_debug 'Got URI %s' "$REQUEST_URI" | |
QUERY_STRING="$(echo "$REQUEST_URI" | cut -d'?' -f2-)" | |
export QUERY_STRING | |
log_debug 'Got QS %s' "$QUERY_STRING" | |
local l_path | |
l_path="$(urlsanitize "$(echo "$REQUEST_URI" | cut -d'?' -f1)")" | |
if [ $? -ne 0 ]; then | |
# urlsanitize failed, rejecting | |
printf 'HTTP/1.0 403 Forbidden\r\n\r\n' | |
return | |
fi | |
if [ -n "$HTTPD_REWRITE" ]; then | |
l_path="$(echo "$l_path" | sed "$HTTPD_REWRITE")" | |
log_debug 'URL rewritten to "%s"' "$l_path" | |
fi | |
local l_head | |
read -r l_head | |
while [ "$l_head" != $'\r' ] | |
do | |
local l_hn="$(echo "$l_head" | cut -d: -f1 | toupper | sed 's/[^A-Z0-9]/_/g')" | |
local l_hv="$(echo "$l_head" | cut -d: -f2- | cut -b2- | rev | cut -b2- | rev)" | |
log_debug 'Got head line <%s>: <%s>' "$l_hn" "$l_hv" | |
local l_prefix="HTTP_" | |
if (echo "$l_hn" | grep "$HTTPD_CGI_HEADER_BLACKLIST" &> /dev/null); then | |
l_prefix="HTTP_BLACKLISTED_" | |
fi | |
eval "${l_prefix}${l_hn}=$(shquote "$l_hv"); export ${l_prefix}${l_hn}" | |
read -r l_head | |
done | |
if [ -n "$HTTPD_BASIC_AUTH" ] && [ "$HTTP_AUTHORIZATION" != "Basic $HTTPD_BASIC_AUTH" ]; then | |
# auth fail | |
printf 'HTTP/1.0 401 Authorization required\r\n\r\n' | |
return | |
fi | |
local l_httpresp | |
if [ -n "$HTTPD_CGIREGEX" ] && (echo "$l_path" | grep "$HTTPD_CGIREGEX" &> /dev/null); then | |
log_debug 'Pass to CGI %s' "$HTTPD_ROOT$l_path" | |
callcgi "$HTTPD_ROOT$l_path" | |
else | |
log_debug 'Pass to default handler' | |
httpd_defhandler "$l_path" | |
fi | |
log_info '%s - - [%s] "%s" %d -' '-' "$(date '+%d/%b/%Y:%H:%M:%S %z')" "$(echo "$l_start" | rev | cut -b2- | rev)" "$l_httpresp" | |
} | |
httpcode2msg () { | |
case "$1" in | |
200) echo OK;; | |
206) echo Partial Content;; | |
302) echo Found;; | |
400) echo Bad Requets;; | |
401) echo Authorization Required;; | |
403) echo Forbidden;; | |
404) echo Not Found;; | |
500) echo Internal Server Error;; | |
esac | |
} | |
callcgi () { | |
log_debug 'calling cgi' | |
local l_cgi="$1" | |
local l_ret | |
l_ret="$("$l_cgi")" | |
if [ $? -ne 0 ]; then | |
log_debug 'cgi failed' | |
printf 'HTTP/1.0 500 Internal Server Error\r\n\r\n' | |
l_httpresp=500 | |
return | |
fi | |
log_debug 'cgi succeed' | |
if (echo "$l_ret" | head -n1 | grep '^Status:' &> /dev/null); then | |
log_debug 'cgi supplied status code' | |
local l_stat=$(echo "$l_ret" | head -n1 | sed 's@^Status: \([0-9]\+\).*$@\1@') | |
printf 'HTTP/1.0 %d %s\r\n' "$l_stat" "$(httpcode2msg "$l_stat")" | |
log_debug 'outputting remaining content' | |
echo "$l_ret" | tail -n+2 | |
l_httpresp="$l_stat" | |
return | |
else | |
log_debug 'use status code 200' | |
printf 'HTTP/1.0 200 OK\r\n\r\n' | |
echo "$l_ret" | |
l_httpresp="$l_stat" | |
return | |
fi | |
} | |
httpd_defhandler () { | |
local l_tgtpath="$HTTPD_ROOT$1" | |
log_debug 'defhandler: path "%s"' "$l_tgtpath" | |
if [ ! -e "$l_tgtpath" ]; then | |
printf 'HTTP/1.0 404 Not Found\r\n\r\n' | |
l_httpresp=404 | |
return | |
fi | |
if [ -d "$l_tgtpath" ]; then | |
if [ "$(echo "$l_tgtpath" | rev | cut -b1)" != '/' ]; then | |
printf "HTTP/1.0 302 Found\r\nLocation: $1/\r\n\r\n" | |
l_httpresp=302 | |
return | |
fi | |
# listing | |
cd "$l_tgtpath" | |
local l_listing="<html><body>$(ls | (while read l_f; do | |
printf '<p><a href="%s">%s</a></p>' "$l_f" "$l_f" | |
done))</body></html>" | |
printf 'HTTP/1.0 200 Forbidden\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n%s' $(expr $(echo "$l_listing" | wc -c) - 1) "$l_listing" | |
l_httpresp=200 | |
return | |
fi | |
if [ -f "$l_tgtpath" ]; then | |
printf "HTTP/1.0 200 OK\r\nContent-Type: $(file -Lbi "$l_tgtpath" | cut -d';' -f1)\r\nContent-Length: $(ls -Ll "$l_tgtpath" | awk '{ print $5 }')\r\n\r\n" | |
cat "$l_tgtpath" | |
l_httpresp=200 | |
return | |
fi | |
printf 'HTTP/1.0 403 Forbidden\r\n\r\n' | |
l_httpresp=403 | |
return | |
} | |
mkcmdline () { | |
local l_cmdline | |
for a in "$@" | |
do | |
if [ -n "$l_cmdline" ]; then | |
l_cmdline="$l_cmdline $(shquote "$a")" | |
else | |
l_cmdline="$(shquote "$a")" | |
fi | |
done | |
echo "$l_cmdline" | |
} | |
probeserver () { | |
# runserver <ip> <port> <prog> [<args> ...] | |
if (nc &> /dev/null; test $? -ne 127); then | |
log_debug 'found nc' | |
if (nc --help 2>&1 | head -n1 | grep 'nmap' &> /dev/null); then | |
# nmap ncat | |
runserver () { | |
log_info 'Starting server using nmap ncat' | |
local l_ip="$1" | |
local l_port="$2" | |
shift 2 | |
nc -lk "$l_ip" "$l_port" -c "$(mkcmdline "$@")" | |
} | |
return 0 | |
elif (nc --help 2>&1 | head -n1 | grep 'BusyBox' &> /dev/null); then | |
# busybox nc | |
runserver () { | |
log_info 'Starting server using busybox nc' | |
local l_ip="$1" | |
local l_port="$2" | |
shift 2 | |
nc -s "$l_ip" -lkp "$l_port" -e "$@" | |
} | |
return 0 | |
elif (nc < /dev/null 2>&1 | grep '^Cmd line' &> /dev/null); then | |
# gnu netcat | |
runserver () { | |
log_info 'Starting server using gnu netcat' | |
local l_ip="$1" | |
local l_port="$2" | |
shift 2 | |
nc -s "$l_ip" -lkp "$l_port" -c "$(mkcmdline "$@")" | |
} | |
return 0 | |
fi | |
log_debug 'unsupported nc' | |
fi | |
if (busybox nc &> /dev/null; test $? -ne 127); then | |
log_debug 'found busybox nc' | |
# busybox nc | |
runserver () { | |
log_info 'Starting server using busybox nc' | |
local l_ip="$1" | |
local l_port="$2" | |
shift 2 | |
busybox nc -s "$l_ip" -lkp "$l_port" -e "$@" | |
} | |
return 0 | |
fi | |
if (tcpsvd &> /dev/null; test $? -ne 127); then | |
log_debug 'found tcpsvd' | |
# tcpsvd cannot parse ipv6 addresses | |
runserver () { | |
log_info 'Starting server using tcpsvd' | |
tcpsvd "$@" | |
} | |
return 0 | |
fi | |
return 1 | |
} | |
main () { | |
local l_progname="$1" | |
shift | |
procopts "$@" | |
if [ $? -ne 0 ]; then | |
showhelp | |
exit 1 | |
fi | |
log_debug 'procopts end' | |
if [ $HTTPD_INETD -ne 0 ]; then | |
httphandler | |
exit 0 | |
fi | |
probeserver | |
if [ $? -ne 0 ]; then | |
log_error 'No suitable server found' | |
exit 1 | |
fi | |
if [ "$HTTPD_DAEMONIZE" -ne 0 ]; then | |
log_info 'Running server in background' | |
runserver "$HTTPD_IP" "$HTTPD_PORT" "$l_progname" "$@" -i & | |
disown | |
else | |
runserver "$HTTPD_IP" "$HTTPD_PORT" "$l_progname" "$@" -i | |
fi | |
} | |
main "$0" "$@" | |
exit $? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Features:
Dependencies: