#!/bin/sh
# shhttp 1.0 -- Shell HTTP Server
# Copyright (C) 2009 Joerg Walter
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
if [ -z "$REMOTE_HOST" ]; then
if [ -n "$TCPREMOTEIP" ]; then
REMOTE_HOST="$TCPREMOTEIP"
else
grep [D]OC: "$0" | sed -e 's/ [D]OC://'
exit 1
fi
fi
# DOC: shhttp 1.0 Documentation
# DOC: ========================
# DOC:
# DOC: shhttp is a minimal inetd-style HTTP server written as shell script.
# DOC:
# DOC: shhttp Copyright (C) 2009 Joerg Walter
# DOC: This program comes with ABSOLUTELY NO WARRANTY. This is free software,
# DOC: and you are welcome to redistribute it under certain conditions.
# DOC: See the GNU General Public License Version 3 for details.
# DOC:
# DOC: Features:
# DOC: - small: total size less than 7.5 kiB—including docs
# DOC: - fast: avoids forks and subprocesses
# DOC: - standalone: doesn't depend on external tools
# DOC: - compatible: works with bash and busybox-ash as /bin/sh
# DOC: - flexible: can run on a read-only file system if logging is off
# DOC: - standards-compatible: HTTP/1.0, CGI/1.1—yes, in theory it can run PHP
# DOC: - documented: run shhttp manually from the shell to get full documentation
# DOC: - supports HTTP Basic authentication
# DOC: - optionally provides Apache-style access logs
# DOC: - provides helper functions for writing simple shell CGI scripts
# DOC:
# DOC: Limitations:
# DOC: - leaves out the exotic bits of HTTP/1.0
# DOC: - implements CGI/1.1 only, i.e. doesn't even serve plain files
# DOC: - needs optional tools for some features (bash or date for logging,
# DOC: base64 and md5sum for HTTP Basic authentication)
# DOC: - needs inetd-style parent server that sets either REMOTE_HOST or TCPREMOTEIP
# DOC: (e.g., stunnel, xinetd, or daemontools)
# DOC:
# DOC: Recommended usage is with stunnel for SSL.
# DOC:
# DOC:
# DOC: Configuration / Files:
# DOC: ----------------------
# DOC:
# DOC: Two subdirectories are used: "cgi-bin" and "htdocs"
# DOC: "cgi-bin" must hold all scripts. The first URL path component selects the
# DOC: executed script. If no path or an empty path component is given,
# DOC: then "cgi-bin/index" is used.
# DOC: "htdocs" is used to resolve PATH_TRANSLATED for the scripts to use.
# DOC:
# DOC: Edit this script to change these settings:
export SERVER_NAME="castle.local" # DOC: Passed to CGI scripts
export SERVER_PORT="443" # DOC: Passed to CGI scripts
export LOGFILE="access.log" # DOC: Enable Apache-style access log if set
# Script start
##############
cd "`dirname "$0"`"
# Utilities for scripts
#######################
# DOC:
# DOC:
# DOC: Helper functions for use in sourced scripts
# DOC: -------------------------------------------
# DOC:
# DOC: die http_status_code status-message # send a minimal error page and exits
die() {
local code="$1"
shift
local msg="$*"
echo "HTTP/1.0 $code $msg"
echo "Content-Type: text/html"
echo "Connection: close"
echo ""
echo "
$msg$msg
"
log $code
exit 0
}
# DOC: log http_status_code [content_length] # log request in access log, if enabled
log() {
[ -z "$LOGFILE" ] && return
local code="$1"
local len="$2"
echo "$REMOTE_ADDR - ${REMOTE_USER--} [$(date_format %d/%b/%Y:%H:%M:%S %z)] \"$REQUEST_METHOD $REQUEST_URI $REQUEST_PROTOCOL\" $code ${len--} \"${HTTP_REFERER--}\" \"${HTTP_USER_AGENT}\"" >> $LOGFILE
}
# DOC: uri_decode encoded_string # print uri-decoded string
uri_decode() {
local encoded="$1"
local out=""
encoded="${encoded//+/ }XXX"
while [ -z "${encoded%%*%[0-9a-fA-F][0-9a-fA-F]*}" ]; do
local prefix="${encoded%%%[0-9a-fA-F][0-9a-fA-F]*}"
local char="${encoded:$((${#prefix}+1)):2}"
out="$out$prefix`printf \\\\x$char.`"
out="${out%.}"
encoded="${encoded:$((${#prefix}+3))}"
done
echo -n "$out${encoded%XXX}"
}
# DOC: upcase string # print upper-cased string
upcase() {
local str="$1"
local out=""
local lower="abcdefghijklmnopqrstuvwxyz"
local upper="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
while [ "${#out}" != "${#str}" ]; do
local inch="${str:${#out}:1}"
local pos="${lower%$inch*}"
local outch="${upper:${#pos}:1}$inch"
out="$out${outch:0:1}"
done
echo -n "$out"
}
# DOC: date_format strftime-format # print formatted date (needs either "bash" or "date")
date_format() {
if type date > /dev/null 2>&1; then
date +"$*"
elif type bash > /dev/null 2>&1 || [ -n "$BASH" ]; then
[ -z "$BASH" ] && BASH=bash
local date="$(echo "PS1='XXX\\D{$*}XXX'" | $BASH -i +o history 2>&1)"
date="${date%XXX*}"
echo "${date##*XXX}"
else
echo "date/time unknown"
fi
}
# DOC: Utility variables: CR NL TAB SPC
CR="$(echo -e '\015')"
NL="$(echo -e '\012')"
TAB="$(echo -e '\011')"
SPC=" "
export IFS="$SPC$TAB$CR$NL"
read method url protocol rest
case "$method" in
GET | HEAD | POST) ;;
*) die 400 Bad Request;;
esac
# Environment setup
###################
export GATEWAY_INTERFACE="CGI/1.1"
export SERVER_PROTOCOL="HTTP/1.0"
export SERVER_SOFTWARE="shhttp 1.0"
export REMOTE_ADDR="$REMOTE_HOST"
export REQUEST_METHOD="$method"
export REQUEST_URI="$url"
export REQUEST_PROTOCOL="$protocol"
export QUERY_STRING="${url#*\?}"
[ "$QUERY_STRING" == "$url" ] && QUERY_STRING=""
url="${url%%\?*}"
url="${url#/}"
export SCRIPT_NAME="/${url%%/*}"
export PATH_INFO="$(uri_decode "${url:$((${#SCRIPT_NAME}-1))}")"
export PATH_TRANSLATED="$PWD/htdocs/$PATH_INFO"
[ "$SCRIPT_NAME" == "/" ] && SCRIPT_NAME="/index"
export SCRIPT_PATH="$PWD/cgi-bin$SCRIPT_NAME"
unset method url protocol rest
# Headers
#########
if [ -n "$REQUEST_PROTOCOL" ]; then
while read header value && [ -n "$header" ]; do
header="${header%:}"
value="${value%$CR}"
header="$(upcase "${header//-/_}")"
[ -z "${header%%*[^A-Z_]*}" ] && die 400 Bad Request
export HTTP_$header="$value"
done
fi
# Special handling for some headers
###################################
# DOC:
# DOC:
# DOC: Security
# DOC: --------
# DOC:
# DOC: HTTP Basic authentication is supported if a file called "passwd" exists.
if [ -n "$HTTP_AUTHORIZATION" -a -f passwd ]; then
set -- $HTTP_AUTHORIZATION
export AUTH_TYPE="$1"
export REMOTE_USER="$(echo "$2" | base64 -d)"
REMOTE_USER="${REMOTE_USER%%:*}"
# DOC: Password file format: echo -n "user:pass" | md5sum >> passwd
# DOC: Each line may have arbitrary pre-/suffixes, e.g. plain-text user names,
# DOC: other password file formats' data, etc.
# DOC: Note that there is no actual access control, only authentication. Scripts
# DOC: may deny access based on information from the CGI environment variables.
if ! grep "$(echo "$2" | base64 -d | md5sum)" passwd > /dev/null 2>&1; then
die 403 Forbidden
fi
unset HTTP_AUTHORIZATION
fi
if [ -n "$HTTP_CONTENT_TYPE" ]; then
export CONTENT_TYPE="$HTTP_CONTENT_TYPE"
fi
if [ -n "$HTTP_CONTENT_LENGTH" ]; then
export CONTENT_LENGTH="$HTTP_CONTENT_LENGTH"
fi
# Security
##########
# DOC:
# DOC: Further restrictions:
# DOC: - no access to hidden files (including "..") for scripts or PATH_INFO
[ -z "${SCRIPT_NAME##.*}" ] && die 400 Bad Request
[ -z "${PATH_TRANSLATED%%*/.*}" ] && die 400 Bad Request
# DOC: - no access to symlinks in cgi-bin
# DOC: - only non-escaped script names are allowed
[ ! -f "$SCRIPT_PATH" -o -L "$SCRIPT_PATH" ] && die 404 Not Found
# Execution
###########
# DOC: - scripts that are executable are forked/executed
if [ -x "$SCRIPT_PATH" ]; then
exec "$SCRIPT_NAME"
die 500 Server Error
# DOC: - scripts that are not executable but have the set-gid-bit set are sourced
elif [ -g "$SCRIPT_PATH" ]; then
. "$SCRIPT_PATH"
true
# DOC: - any other permissions result in a "404 Not Found" error
else
die 404 Not Found
fi