#!/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