├── COPYING ├── README ├── modules ├── cgi.mod ├── controller+php.mod ├── dirlist.mod ├── php.mod └── static.mod ├── shttpd └── tools ├── render.rb └── route.rb /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Stephen Paul Weber 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | Original concept: http://www.linuxscrew.com/2007/09/06/web-server-on-bash-in-one-line/ 16 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = shttpd = 2 | 3 | shttpd is an HTTP server written entirely in POSIX shell script. 4 | 5 | == Why? == 6 | 7 | Because you can! Also, there are some real advantages: 8 | 9 | * Compact, understandable codebase 10 | * Trivial to configure/extend 11 | * Run from anywhere and just start serving! 12 | * Get lots of nice debug output in your terminal 13 | 14 | == Running == 15 | 16 | To start an instance of the server just run: 17 | 18 | ./shttpd -p -C -m modules/dirlist.mod:modules/static.mod 19 | 20 | You can list whatever modules you want to load (order matters!) but this will get you started. 21 | 22 | == Extending == 23 | 24 | Writing modules is easy! Look a the ones in the modules directory for examples. 25 | 26 | Basically, you write some shell code that gets sourced into the server, and manipulate the environment according to your needs. 27 | 28 | == Deploying == 29 | 30 | Each instance of the server can only handle one connection, so you will need some other server to round-robin proxy. 31 | 32 | You may also configure inetd to call the server with -i 33 | -------------------------------------------------------------------------------- /modules/cgi.mod: -------------------------------------------------------------------------------- 1 | # Add CGI support to shttpd 2 | 3 | call_cgi() { 4 | # TODO: REMOTE_ADDR (is that even possible with nc?) 5 | GATEWAY_INTERFACE="CGI/1.1" 6 | PATH_INFO="/$path" 7 | SCRIPT_NAME="/$path" 8 | SERVER_NAME="$HOSTNAME" 9 | CONTENT_LENGTH="$HTTP_CONTENT_LENGTH" 10 | CONTENT_TYPE="$HTTP_CONTENT_TYPE" 11 | SCRIPT_FILENAME="$PATH_TRANSLATED" 12 | export QUERY_STRING REQUEST_METHOD PATH_TRANSLATED SERVER_PORT SERVER_SOFTWARE SERVER_PROTOCOL GATEWAY_INTERFACE PATH_INFO SCRIPT_NAME SERVER_NAME CONTENT_LENGTH CONTENT_TYPE SCRIPT_FILENAME 13 | if [ "$1" != "-n" ]; then # Supress output 14 | printf "%b" "HTTP/1.1\r\n" 15 | else 16 | shift 17 | fi 18 | "$@" 19 | } 20 | 21 | if [ -f "$PATH_TRANSLATED" -a -x "$PATH_TRANSLATED" ]; then 22 | call_cgi "$PATH_TRANSLATED" 23 | exit 24 | fi 25 | -------------------------------------------------------------------------------- /modules/controller+php.mod: -------------------------------------------------------------------------------- 1 | # Bind controllers in any language to views in PHP 2 | # Requires cgi.mod 3 | 4 | controller="`echo "$path" | cut -d/ -f1`" 5 | action="`echo "$path" | cut -d/ -f2-`" 6 | if [ -z "$controller" ]; then 7 | controller=main 8 | fi 9 | if [ -z "$action" -o "$action" = "$controller" ]; then 10 | action=index 11 | fi 12 | 13 | if [ -x "controllers/$controller" ]; then 14 | CONTROLLER_VARS="`call_cgi -n "controllers/$controller" "$action"`" 15 | if [ $? -eq 0 ]; then 16 | if [ -r "views/$controller/$action.php" ]; then 17 | REDIRECT_STATUS="200" 18 | export REDIRECT_STATUS CONTROLLER_VARS 19 | script="`mktemp -t shttpd$$.php.XXXXXX`" 20 | echo "" > "$script" 24 | cat "views/$controller/$action.php" >> "$script" 25 | PATH_TRANSLATED="$script" 26 | call_cgi php-cgi "$script" 27 | else 28 | echo "$CONTROLLER_VARS" 29 | fi 30 | else 31 | echo "$CONTROLLER_VARS" 1>&2 32 | echo "$CONTROLLER_VARS" 33 | fi 34 | exit 35 | fi 36 | -------------------------------------------------------------------------------- /modules/dirlist.mod: -------------------------------------------------------------------------------- 1 | # Add directory listing support to shttpd 2 | 3 | ls_switches="-oghF" 4 | 5 | if [ -d "$PATH_TRANSLATED" ]; then 6 | if [ -n "$path" -a "`echo "$path" | cut -c${#path}-`" != / ]; then 7 | printf "%b" "HTTP/1.1 301 Redirect\r\n" 8 | printf "%b" "Location: /$path/\r\n" 9 | exit 10 | fi 11 | printf "%b" "HTTP/1.1 200 OK\r\n" 12 | printf "%b" "Content-Type: text/html\r\n\r\n" 13 | echo "" 14 | echo "/$path - Listing" 15 | echo "" 16 | echo "" 17 | IFS=" 18 | " 19 | echo "" 29 | echo "" 30 | exit 31 | fi 32 | -------------------------------------------------------------------------------- /modules/php.mod: -------------------------------------------------------------------------------- 1 | # PHP-CGI module for shttpd 2 | # Requires CGI module to be loaded 3 | 4 | if echo "$PATH_TRANSLATED" | grep '.*\.php' > /dev/null; then 5 | if [ -e "$PATH_TRANSLATED" ]; then 6 | REDIRECT_STATUS="200" 7 | export REDIRECT_STATUS 8 | fi 9 | call_cgi php-cgi "$PATH_TRANSLATED" 10 | exit 11 | fi 12 | -------------------------------------------------------------------------------- /modules/static.mod: -------------------------------------------------------------------------------- 1 | # Serve static files with shttpd 2 | 3 | if [ -f "$PATH_TRANSLATED" -a -r "$PATH_TRANSLATED" ]; then 4 | printf "%b" "HTTP/1.1 200 OK\r\n" 5 | printf "%b" "Content-Type: `file -b --mime "$PATH_TRANSLATED"`\r\n" 6 | printf "%b" "Content-Length: `wc -c < "$PATH_TRANSLATED"`\r\n" 7 | printf "%b" "Expires: `date -uRd '1 hour'`\r\n" 8 | if type md5sum > /dev/null 2>&1; then 9 | printf "%b" "ETag: \"`md5sum < "$PATH_TRANSLATED"`\"\r\n" 10 | elif type md5 > /dev/null 2>&1; then 11 | printf "%b" "ETag: \"`md5 -q "$PATH_TRANSLATED"`\"\r\n" 12 | fi 13 | printf "%b" "\r\n" 14 | cat "$PATH_TRANSLATED" 15 | exit 16 | fi 17 | -------------------------------------------------------------------------------- /shttpd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SERVER_SOFTWARE="shttpd" 4 | readonly SERVER_SOFTWARE 5 | export SERVER_SOFTWARE 6 | 7 | original_pwd="`pwd`" 8 | readonly original_pwd 9 | export original_pwd 10 | 11 | process_request() { 12 | read REQUEST_METHOD path SERVER_PROTOCOL 13 | 14 | # Read request headers 15 | z="123"; while [ ${#z} -gt 2 ]; do 16 | read z 17 | if [ ${#z} -gt 2 ]; then 18 | var=HTTP_`echo "$z" | cut -d":" -f1 | tr [:lower:] [:upper:] | tr - _` 19 | eval "$var=\"`echo "$z" | cut -d" " -f2-`\"" 20 | export $var 21 | fi 22 | done 23 | 24 | # Chop off query string 25 | QUERY_STRING="`echo "$path" | cut -d"?" -f2-`" 26 | if [ "$QUERY_STRING" = "$path" ]; then 27 | unset QUERY_STRING 28 | else 29 | path="`echo "$path" | cut -d"?" -f1`" 30 | fi 31 | 32 | urldecode() { 33 | i=1 34 | while [ $i -le ${#1} ]; do 35 | c="`echo "$1" | cut -c$i`" 36 | if [ "x$c" = "x%" ]; then 37 | printf "%b" "\0$(echo "ibase=16;obase=8;$(echo "$1" | cut -c$((i+1))-$((i+2)))" | bc)" 38 | i=$((i+3)) 39 | else 40 | printf "%s" "$c" 41 | i=$((i+1)) 42 | fi 43 | done 44 | } 45 | 46 | path="`urldecode "$path"`" # URLdecode the path 47 | 48 | path="`echo "$path" | sed -e "s/^[\/\.]*//" | sed -e"s/[\/\.]\{2,\}//"`" # Filter path for security 49 | PATH_TRANSLATED="`pwd`/$path" # Get absolute path 50 | 51 | # Include modules 52 | if [ -n "$modules" ]; then 53 | IFS=":" 54 | for m in $modules; do 55 | . "$original_pwd/$m" 56 | done 57 | fi 58 | 59 | # If we got this far, none of the modules can handle this path 60 | printf "%b" "HTTP/1.1 404 Not Found\r\n" 61 | printf "%b" "\r\n404 Not Found" 62 | } 63 | 64 | # Process switches 65 | while getopts p:h:m:C:i c; do 66 | case $c in 67 | p) 68 | SERVER_PORT="$OPTARG" 69 | ;; 70 | h) 71 | HOSTNAME="$OPTARG" 72 | ;; 73 | m) 74 | modules="$OPTARG" 75 | ;; 76 | C) 77 | cd "$OPTARG" 78 | ;; 79 | i) 80 | inetd_mode=1 81 | ;; 82 | *) 83 | echo "Usage: $0 [-p port] [-h host] [-m modules] [-C document root]" 84 | exit 1 85 | ;; 86 | esac 87 | done 88 | shift $(($OPTIND - 1)) 89 | 90 | # Set defaults 91 | if [ -z "$SERVER_PORT" ]; then 92 | SERVER_PORT="8080" 93 | fi 94 | if [ -z "$HOSTNAME" ]; then 95 | HOSTNAME="`hostname`" 96 | fi 97 | export SERVER_PORT HOSTNAME modules 98 | 99 | if [ -n "$inetd_mode" ]; then 100 | process_request 101 | else 102 | # Detect -q 103 | if nc -h 2>&1 | grep -- -q > /dev/null; then 104 | NC="nc -q0" 105 | else 106 | echo "WARNING: nc has no -q, connections may not close properly." 1>&2 107 | NC="nc" 108 | fi 109 | # Detect broken -p 110 | if nc -h 2>&1 | grep -- -p | grep remote > /dev/null; then 111 | NC="$NC -vvl" 112 | else 113 | NC="$NC -vvlp" 114 | fi 115 | 116 | # Run server loop 117 | pipehack="`mktemp -t shttpd$$.XXXXXX`" 118 | rm "$pipehack" && mkfifo "$pipehack" 119 | :;while [ $? -eq 0 -o $? -eq 141 ]; do 120 | $NC "$SERVER_PORT" < "$pipehack" | ( process_request ) > "$pipehack" 121 | done 122 | 123 | # Cleanup 124 | rm -f "$pipehack" 125 | fi 126 | -------------------------------------------------------------------------------- /tools/render.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class Object 4 | def render(*args) 5 | puts instance_variables.inject( 6 | {:args => args.inject({}) do |h,i| 7 | if i.is_a?Hash 8 | h.merge!(i) 9 | else 10 | h[args.index(i)] = i 11 | end 12 | end } 13 | ) { |hsh, var| 14 | hsh[var.to_s.sub(/^./,'')] = instance_variable_get(var) 15 | hsh 16 | }.to_json 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /tools/route.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | 3 | class Object 4 | def route 5 | @cgi = CGI.new 6 | self.send((ARGV[0] || 'main').intern) 7 | end 8 | # Proxy onto CGI 9 | def params 10 | @cgi.params.inject({}) do |hsh, i| 11 | if i[1].is_a?(Array) && i[1].length < 2 12 | hsh[i[0]] = i[1][0] 13 | else 14 | hsh[i[0]] = i[1] 15 | end 16 | hsh 17 | end 18 | end 19 | def method_missing(method, *args) 20 | @cgi.send(method, *args) 21 | end 22 | end 23 | --------------------------------------------------------------------------------