Skip to content

Instantly share code, notes, and snippets.

@mentha
Last active March 10, 2018 03:52
Show Gist options
  • Save mentha/78d29ffe9cc2ca833caa3d756af85989 to your computer and use it in GitHub Desktop.
Save mentha/78d29ffe9cc2ca833caa3d756af85989 to your computer and use it in GitHub Desktop.
Simple HTTP Server in shell command language
#!/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 $?
@mentha
Copy link
Author

mentha commented Mar 10, 2018

Features:

  • Directory listing
  • CGI support (not fully compatible)

Dependencies:

  • POSIX-compliant shell and utilities
  • One of
    • Nmap Ncat
    • Busybox nc
    • GNU netcat
    • tcpsvd
    • inetd (inetd mode only)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment