#!/bin/sh # sh-httpd - A small shell-script web server with CGI 1.1 support # Copyright (C) 2000 Charles Steinkuehler # # 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; either version 2 of the License, or (at your # option) any later version. See . # # 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. VERSION=0.4 NAME="ShellHTTPD" . /etc/sh-httpd.conf TAB=' ' CR=`echo -e -n "\r"` LF=' ' IFS=" $TAB$LF" OIFS=$IFS PATH="/sbin:/bin:/usr/sbin:/usr/bin" TMPSUFFIX=$$ qt () { "$@" >/dev/null 2>&1 ; } bname() { local IFS='/' set -- $1 eval rc="\$$#" [ "$rc" = "" ] && eval rc="\$$(($# - 1))" echo "$rc" } dname() { echo "$1" | sed '/^\/$/c\ / s/\/*$// s/[^/]*$// /./!c\ . s/\/$//' } rm_temp() { qt rm /tmp/sh-httpd.*$TMPSUFFIX } kill_jobs() { local IFS SIG JOB SIG=$1 IFS=$LF for JOB in `jobs` ; do IFS=' ' set -- $JOB kill -$SIG $2 done } abort() { local CNT qt kill_jobs 15 CNT=1 while [ -n "`jobs`" ] ; do sleep 1 CNT=$(( ${CNT} + 1 )) [ "$CNT" -ge 5 ] && { qt kill_jobs 9 ; break ; } done rm_temp print_error "$@" } toupper() { echo "$1" | sed y/\ abcdefghijklmnopqrstuvwxyz-/\ ABCDEFGHIJKLMNOPQRSTUVWXYZ_/ } log() { # $1 = response code # $2 = response size [ -z "$LOGFILE" ] && return [ -f "$LOGFILE" -a ! -w "$LOGFILE" ] && return echo "${REMOTE_HOST:-$REMOTE_ADDR} - - [$REQ_DATE] \"$REQUEST\" \ ${1:--} ${2:--} \"${HTTP_REFERER:--}\" \"${HTTP_USER_AGENT:--}\"" \ >> "$LOGFILE" } print_header() { echo -e "HTTP/1.0 $1\r" echo -e "Server: $NAME/$VERSION\r" echo -e "Date: $REQ_DATE\r" echo -e "Connection: close\r" } print_error() { local COUNT print_header "$1 $2" echo -e "Content-type: text/html\r" echo -e "\r" echo "$1 $2" echo "

$1 $2

" echo "$3" count=1 while [ $count -le 5 ] ; do echo "" echo "" count=$(( $count + 1 )) done echo "" exit 1 } guess_content_type() { set -- `sed -n "/${1##*.}[ $TAB]/P" $MIME_TYPES` echo "Content-type: ${2:-$DEFCONTENT}" } export_cgi() { # HTTP_ headers exported previously export SERVER_SOFTWARE="$NAME/$VERSION" export SERVER_NAME # Set in /etc/sh-httpd.conf export SERVER_ADDR # Set in /etc/sh-httpd.conf export GATEWAY_INTERFACE="CGI/1.1" export SERVER_PROTOCOL="${PROTOCOL:-HTTP/0.9}" export SERVER_PORT # Set in /etc/sh-httpd.conf export REQUEST_METHOD="$COMMAND" export REQUEST_URI="$URI" export DOCUMENT_ROOT="$DOCROOT" # Set previously: export PATH_INFO export PATH_TRANSLATED export SCRIPT_NAME export SCRIPT_FILENAME export QUERY_STRING export REMOTE_HOST export REMOTE_ADDR export REMOTE_PORT # Currently not supported: #export AUTH_TYPE #export REMOTE_USER #export REMOTE_IDENT #export CONTENT_TYPE #export CONTENT_LENGTH } file_stats() { set -- `TZ=UDC ls -ln $LSDATEFLAG $1` LEN=$5 MOD="$6 $7 $8 $9 ${10}" } do_cgi() { # Verify CGI script is executible [ ! -x "$LOCALURL" ] && { log 403 0 print_error 403 "Forbidden" "Document not executible: $URL" } SCRIPT_NAME=$URL SCRIPT_FILENAME="$DOCROOT$URL" export_cgi OUTPUT=/tmp/sh-httpd.$TMPSUFFIX # Setup command line args, if appropriate case $QUERY_STRING in *=*) set -- "" ;; *) IFS='+' set -- $QUERY_STRING IFS=$OIFS ;; esac $LOCALURL "$@" > $OUTPUT & CNT=1 while [ -n "`jobs`" ] ; do sleep 1 CNT=$(( $CNT + 1 )) if [ "$CNT" -ge "$TIMEOUT" ] ; then log 500 0 abort 500 "Internal Server Error" "CGI Timeout: $URL" fi done file_stats $OUTPUT STATUS="200 OK" case $FILE in nph-*) if [ -n "${PROTOCOL}" ] ; then read VERSION STATUS REASON echo "$VERSION $STATUS $REASON" fi while read -r HEADER HEADERDATA [ "x${HEADER%${CR}}" != x ] do echo "$HEADER $HEADERDATA" done echo -e "\r" log "${STATUS%${STATUS#???}}" "$LEN" [ "$COMMAND" != HEAD ] && cat ;; *) HEADERS="" IFS=' :' CONTENT="$DEFCONTENT" while read -r HEADER HEADERDATA [ "x${HEADER%${CR}}" != x ] do HEADERU=`toupper "${HEADER%$CR}"` HEADERDATA="${HEADERDATA%$CR}" case ${HEADERU} in STATUS) STATUS="$HEADERDATA" ;; LOCATION) LOC="$HEADERDATA" ;; CONTENT_TYPE) CONTENT="$HEADERDATA" ;; *) HEADERS="$HEADERS$HEADER: $HEADERDATA$CR$LF" ;; esac done IFS=$OIFS if [ -n "$LOC" ] ; then if [ "x$LOC" != "x${LOC#*://}" ] ; then STATUS="302 Moved Temporarily" else # Send different file URI=$LOC unset DIR NURL LEN CGI do_get exit fi fi log "${STATUS%${STATUS#???}}" "$LEN" print_header "$STATUS" echo -e "Content-type: $CONTENT\r" [ "x${LOC}" != "x${LOC#*://}" ] && echo -e "Location: $LOC\r" echo -n "$HEADERS" echo -e "\r" [ "$COMMAND" != HEAD ] && cat ;; esac < $OUTPUT qt rm $OUTPUT } do_get() { local DIR NURL LEN CGI VERSION STATUS REASON if [ ! -d $DOCROOT ]; then log 404 0 print_error 404 "Not Found" "No such file or directory" fi # Split URI into base and query string at ? IFS='?' set -- $URI QUERY_STRING="$2" URL="$1" IFS=$OIFS # Test for CGI prefix pattern & split into URL and extra path info for pattern in $SCRIPT_ALIAS ; do if [ "x$URL" != "x${URL#$pattern*/}" ] ; then NURL=${URL#$pattern*/} IFS='/' set -- $NURL PATH_INFO=${NURL#$1} PATH_TRANSLATED=$DOCROOT$PATH_INFO URL=${URL%$PATH_INFO} CGI="Y" break fi done IFS=$OIFS # Use default file if URL ends in trailing slash if [ -z "${URL##*/}" ]; then for index in $DEFINDEX ; do NURL=$URL$index [ -f "$DOCROOT/$NURL" ] && break done URL=$NURL fi DIR="`dname $URL`" FILE="`bname $URL`" # Check for existance of directory if [ ! -d "$DOCROOT/$DIR" ]; then log 404 0 print_error 404 "Not Found" "Directory not found: $DIR" else cd "$DOCROOT/$DIR" LOCALURL="`pwd`/$FILE" fi # Verify URL is not outside DOCROOT and file exists [ "x$LOCALURL" = "x${LOCALURL#$DOCROOT}" -o ! -f "$LOCALURL" ] && { log 404 0 print_error 404 "Not Found" "File not found: $URL" } # Verify we can read the file [ ! -r "$LOCALURL" ] && { log 403 0 print_error 403 "Forbidden" "Access prohibited: $URL" } # Test for CGI suffix if [ "$CGI" != "Y" ] ; then for pattern in ${SCRIPT_SUFFIX} ; do if [ "$FILE" != "${FILE%$pattern}" ] ; then CGI="Y" break fi done fi if [ "$CGI" = "Y" ] ; then do_cgi else print_header "200 OK" guess_content_type $LOCALURL file_stats $LOCALURL echo -e "Content-length: $LEN\r" echo -e "Last-modified: $MOD\r\n\r" log 200 $LEN [ "$COMMAND" != HEAD ] && cat $LOCALURL fi sleep 1 } read_request() { local HEADER local HEADERDATA read COMMAND URI PROTOCOL COMMAND=${COMMAND%"${CR}"} URI=${URI%"$CR"} PROTOCOL=${PROTOCOL%"${CR}"} REQUEST="$COMMAND $URI $PROTOCOL" if [ -n "$PROTOCOL" ] ; then IFS=' :' while read -r HEADER HEADERDATA [ "x$HEADER" != "x$CR" ] do HEADER=`toupper "$HEADER"` HEADERDATA="${HEADERDATA%${CR}}" setvar HTTP_$HEADER "$HEADERDATA" export HTTP_$HEADER done IFS=$OIFS fi REQ_DATE="`date -uR`" case $COMMAND in GET|HEAD) do_get ;; *) print_error 501 "Not Implemented" "$COMMAND" ;; esac } # # main() # # Don't send any shell error messages to the client! exec 2>/dev/null trap "rm_temp" 0 set -- `getpeername -n` REMOTE_ADDR=$1 REMOTE_HOST="" REMOTE_PORT=$2 # Check client address against access list while [ -n "$CLIENT_ADDRS" ] ; do for pattern in $CLIENT_ADDRS ; do if [ "x$REMOTE_ADDR" != "x${REMOTE_ADDR##${pattern}}" ] ; then break 2 fi done exit 1 done read_request exit 0