Lightweight HTTP server in BASH with PHP support
By grapsus on Wednesday 15 September 2010, 00:06 - Permalink
No kidding, I wrote this HTTP server in Bourne Shell. It supports most of HTTP 1.0 headers, Keep-alive requests, directory listing and PHP scripts. By its nature, this piece of software is not secure (it is fun though) and isn't intended for production purposes : <insert the usual NO WARRANTY boilerplate bullshit here>.
I tested it with PHPMyAdmin which I consider to be heavy PHP software and it works pretty well. It is not well commented, really I just wrote it for fun, learning BASH and HTTP protocol.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | #!/bin/bash # Written by Alexis Bezverkhyy <alexis@grapsus.net> in 2008 # This is free and unencumbered software released into the public domain. # For more information, please refer to <http://unlicense.org/> # This script should be run via inetd, first parameter is WWW root path # Uncomment for debugging #exec 2>/tmp/log ; set -x NUM="$RANDOM" DOCUMENT_ROOT="$1" KEEP_ALIVE="keep-alive" while [ "$KEEP_ALIVE" == "keep-alive" ] ; do KEEP_ALIVE="close" for i in seq 1 5; do read -t 5 line if [ -n "$line" ] ; then break; fi done if grep -sqv 'HTTP' <<< "$line" ; then exit ; fi #echo `date`" BEGIN $line" >> /tmp/"$NUM"-log REQUEST_METHOD=`cut -d ' ' -f 1 <<< "$line"` REQUEST_URI=`cut -d ' ' -f 2 <<< "$line" | sed 's/%20/ /'` SCRIPT_NAME=`cut -d '?' -f 1 <<< "$REQUEST_URI"` SCRIPT_FILENAME=`sed -e 's#//#/#' -e 's#/$##' <<< "$DOCUMENT_ROOT$SCRIPT_NAME"` QUERY_STRING='' if grep -sq '?' <<< "$REQUEST_URI" ; then QUERY_STRING=`cut -d '?' -f 2 <<< "$REQUEST_URI"` fi while read -t 1 line ; do line=`strings <<< "$line"` if grep -sqi '^Content-length' <<< "$line" ; then CONTENT_LENGTH=`cut -d ' ' -f 2 <<< "$line"` elif grep -sqi '^Content-type' <<< "$line" ; then CONTENT_TYPE=`cut -d ' ' -f 2 <<< "$line"` elif grep -sqi '^Connection' <<< "$line" ; then KEEP_ALIVE=`cut -d ' ' -f 2 <<< "$line"` elif grep -sqi '^Cookie' <<< "$line" ; then HTTP_COOKIE=`sed 's/Cookie:[ ]*//i' <<< "$line"` fi if [ -z "$line" -a "$REQUEST_METHOD" == "POST" -a -n "$CONTENT_LENGTH" ] ; then read -n "$CONTENT_LENGTH" line echo "$line" > /tmp/"$NUM"-post break elif [ -z "$line" ] ; then break fi done # some security if grep -sq '\.\.' <<< "$SCRIPT_FILENAME" || ( namei "$SCRIPT_FILENAME" | grep -sq '\->') ; then SCRIPT_FILENAME='./' fi if [ -d "$SCRIPT_FILENAME" ] ; then echo -en 'HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n' dir=`sed 's#'"$DOCUMENT_ROOT"'##' <<< "$SCRIPT_FILENAME"` if [ -z "$dir" ] ; then dir='/' ; parent='/' else parent=`sed 's#/[^/]\+$##' <<< "$dir"` if [ -z "$parent" ] ; then parent='/' ; fi fi echo "<html><head><title>Index of $dir</title></head> <body><h3>Index of $dir</h3> <table> <tr> <td><b>Name</b></td> <td><b>Last modified</b></td> <td><b>Size</b></td> </tr> <tr><td colspan=\"3\">[D] <a href=\"$parent\">..</a></td></tr>" for item in "$SCRIPT_FILENAME"/* ; do if [ "$item" == "$SCRIPT_FILENAME"'/*' ] ; then break ; fi name=`basename "$item"` link=`sed 's#'"$DOCUMENT_ROOT"'##' <<< "$item"` stat=`ls -lhd --time-style='+%d-%m-%y#%H:%m' "$item"` mtime=`cut -d ' ' -f 6 <<< "$stat" | sed 's/#/ /'` size=`cut -d ' ' -f 5 <<< "$stat"` echo "<tr><td>" if [ -L "$item" ] ; then echo "[S] $name<br/>" elif [ -d "$item" ] ; then echo '[D] <a href="'"$link"'">'"$name"'</a><br/>' else echo '[F] <a href="'"$link"'">'"$name"'</a><br/>' fi echo "</td><td>$mtime</td><td>$size</td></tr>" done echo "</table></body></html>" elif [ -f "$SCRIPT_FILENAME" ] ; then mime='text/html' if grep -Esqv '\.(php|htm|html)$' <<< "$SCRIPT_FILENAME" ; then mime=`file -b --mime-type $SCRIPT_FILENAME` fi if grep -sq '\.php$' <<< "$SCRIPT_FILENAME" ; then for var in `env | cut -d '=' -f 1` ; do if [ "$var" != "PATH" -a "$var" != "PWD" -a "$var" != "LANG" -a "$var" != "SHLVL" ] ; then export -n "$var" fi done export REQUEST_URI REQUEST_METHOD QUERY_STRING DOCUMENT_ROOT SCRIPT_FILENAME \ SCRIPT_NAME CONTENT_LENGTH CONTENT_TYPE GATEWAY_INTERFACE='CGI/1.1' \ HTTP_HOST=`hostname -i` HTTP_COOKIE REDIRECT_STATUS=1 if [ "$REQUEST_METHOD" == "GET" ] ; then php-cgi $SCRIPT_FILENAME \ `tr '&' ' ' <<< "$QUERY_STRING"` > /tmp/"$NUM"-php else php-cgi $SCRIPT_FILENAME \ `tr '&' ' ' <<< "$QUERY_STRING"` > /tmp/"$NUM"-php < /tmp/"$NUM"-post fi HTTP_STATUS=`grep -i '^Status: .*$' /tmp/"$NUM"-php | cut -d ' ' -f 2` if [ -z "$HTTP_STATUS" ] ; then HTTP_STATUS='200' fi OUT="head" cat /tmp/"$NUM"-php | while read ; do if [ "$OUT" = 'head' ] ; then REPLY=$(strings <<< "$REPLY") if [ -z "$REPLY" ] ; then OUT='body' continue fi fi echo "$REPLY" >> /tmp/"$NUM"-php-"$OUT" done echo -en "HTTP/1.0 $HTTP_STATUS OK\r\nContent-type: $mime\r\nContent-length:"\ `ls -l /tmp/"$NUM"-php-body | cut -d ' ' -f 5`"\r\nConnection: $KEEP_ALIVE\r\n" cat /tmp/"$NUM"-php-head echo -en "\r\n" cat /tmp/"$NUM"-php-body else echo -en "HTTP/1.0 200 OK\r\nContent-type: $mime\r\nContent-length: "\ `ls -l "$SCRIPT_FILENAME" | cut -d ' ' -f 5`"\r\nConnection: $KEEP_ALIVE\r\n\r\n" cat "$SCRIPT_FILENAME" fi rm -f /tmp/"$NUM"-php /tmp/"$NUM"-php-body /tmp/"$NUM"-php-head /tmp/"$NUM"-post # /tmp/"$NUM"-log else echo -en 'HTTP/1.0 404 NOT FOUND\n\rContent-type: text/plain\r\n\r\n404 File not found' fi #echo `date`" END" >> /tmp/"$NUM"-log done |
Here's the inetd configuration I use to run it :
8080 stream tcp nowait grapsus /usr/sbin/tcpd /home/grapsus/bin/http.sh /home/grapsus/www
I know BASH supports sockets, but this support is disabled in most Unix distributions (especially on Debian).
Let me know what you think about it or the improvements you made.
Comments
Yay security holes. This also isn't using bash as the HTTP server. You're using inetd as the server. This would be horribly inefficient and insecure. Your code style is also horrible.
BAD: if grep -sq '\.php$' <<< "$filename" instead of [[ $filename = *.php ]]
BAD: it parses ls -l
BAD: it uses tr '&' ' ' <<< "$QUERY_STRING" instead of parameter expansion
BAD: Race issue with logs that opens security hole
BAD: other things
I appreciate your comment, but I think you didn't read the whole post.
I said that this "server" isn't secure, because I wrote it in one or two hours just for fun (the bet was to be able to run PHPMyAdmin) and I also said that actual TCP server is provided by inetd because socket support is disabled in Bash on most distributions.
If I wanted some security, I would have tried to handle errors and this code would have been 5 times longer. I didn't do so because I don't believe one can write a secure web server using Bash.
It was also written two years ago when I was discovering Bash scripts. Now I know some things can be done in a less inefficient way.
All in all, if you're not happy with it, just bring your own code or modify this one, it belongs to everybody.
Understanding what it is...good job! As you noted, it's not for production use but for fun and I think it's awesome. :).