#! /usr/bin/env python | |
import sys,os,subprocess,glob,re | |
class Bash2Py(object): __slots__ = ["val"] def __init__(self, value=''): self.val = value def setValue(self, value=None): self.val = value return value | |
def GetVariable(name, local=locals()): if name in local: return local[name] if name in globals(): return globals()[name] return None def Make(name, local=locals()): ret = GetVariable(name, local) if ret is None: ret = Bash2Py(0) globals()[name] = ret return ret def Str(value): if isinstance(value, list): return " ".join(value) if isinstance(value, basestring): return value return str(value) def Array(value): if isinstance(value, list): return value if isinstance(value, basestring): return value.strip().split(' ') return [ value ] def Glob(value): ret = glob.glob(value) if (len(ret) < 1): ret = [ value ] return ret | |
class Expand(object): @staticmethod def at(): if (len(sys.argv) < 2): return [] return sys.argv[1:] @staticmethod def star(in_quotes): if (in_quotes): if (len(sys.argv) < 2): return "" return " ".join(sys.argv[1:]) return Expand.at() | |
#!/usr/bin/env bash | |
# | |
# A simple, configurable HTTP server written in bash. | |
# | |
# See LICENSE for licensing information. | |
# | |
# Original author: Avleen Vig, 2012 | |
# Reworked by: Josh Cartwright, 2012 | |
warn() { echo "WARNING: $@" >&2; } | |
# | |
# A simple, configurable HTTP server written in bash. | |
# | |
# See LICENSE for licensing information. | |
# | |
# Original author: Avleen Vig, 2012 | |
# Reworked by: Josh Cartwright, 2012 | |
def warn () : print("WARNING: "+Str(Expand.at()),stderr=subprocess.STDOUT) | |
[ -r bashttpd.conf ] || { cat >bashttpd.conf <<'EOF' # # bashttpd.conf - configuration for bashttpd # # The behavior of bashttpd is dictated by the evaluation # of rules specified in this configuration file. Each rule # is evaluated until one is matched. If no rule is matched, # bashttpd will serve a 500 Internal Server Error. # # The format of the rules are: # on_uri_match REGEX command [args] # unconditionally command [args] # # on_uri_match: # On an incoming request, the URI is checked against the specified # (bash-supported extended) regular expression, and if encounters a match the # specified command is executed with the specified arguments. # # For additional flexibility, on_uri_match will also pass the results of the # regular expression match, ${BASH_REMATCH[@]} as additional arguments to the # command. # # unconditionally: # Always serve via the specified command. Useful for catchall rules. # # The following commands are available for use: # # serve_file FILE # Statically serves a single file. # # serve_dir_with_tree DIRECTORY # Statically serves the specified directory using 'tree'. It must be # installed and in the PATH. # # serve_dir_with_ls DIRECTORY # Statically serves the specified directory using 'ls -al'. # # serve_dir DIRECTORY # Statically serves a single directory listing. Will use 'tree' if it is # installed and in the PATH, otherwise, 'ls -al' # # serve_dir_or_file_from DIRECTORY # Serves either a directory listing (using serve_dir) or a file (using # serve_file). Constructs local path by appending the specified root # directory, and the URI portion of the client request. # # serve_static_string STRING # Serves the specified static string with Content-Type text/plain. # # Examples of rules: # # on_uri_match '^/issue$' serve_file "/etc/issue" # # When a client's requested URI matches the string '/issue', serve them the # contents of /etc/issue # # on_uri_match 'root' serve_dir / # # When a client's requested URI has the word 'root' in it, serve up # a directory listing of / # # DOCROOT=/var/www/html # on_uri_match '/(.*)' serve_dir_or_file_from "$DOCROOT" # When any URI request is made, attempt to serve a directory listing # or file content based on the request URI, by mapping URI's to local # paths relative to the specified "$DOCROOT" # unconditionally serve_static_string 'Hello, world! You can configure bashttpd by modifying bashttpd.conf.' # More about commands: # # It is possible to somewhat easily write your own commands. An example # may help. The following example will serve "Hello, $x!" whenever # a client sends a request with the URI /say_hello_to/$x: # # serve_hello() { # add_response_header "Content-Type" "text/plain" # send_response_ok_exit <<< "Hello, $2!" # } # on_uri_match '^/say_hello_to/(.*)$' serve_hello # # Like mentioned before, the contents of ${BASH_REMATCH[@]} are passed # to your command, so its possible to use regular expression groups # to pull out info. # # With this example, when the requested URI is /say_hello_to/Josh, serve_hello # is invoked with the arguments '/say_hello_to/Josh' 'Josh', # (${BASH_REMATCH[0]} is always the full match) EOF warn "Created bashttpd.conf using defaults. Please review it/configure before running bashttpd again." exit 1 } | |
if not os.access("bashttpd.conf",R_OK): subprocess.Popen("cat",shell=True,stdout=file('bashttpd.conf','wb'),stdin=subprocess.PIPE) _rc0.communicate("#\n# bashttpd.conf - configuration for bashttpd\n#\n# The behavior of bashttpd is dictated by the evaluation\n# of rules specified in this configuration file. Each rule\n# is evaluated until one is matched. If no rule is matched,\n# bashttpd will serve a 500 Internal Server Error.\n#\n# The format of the rules are:\n# on_uri_match REGEX command [args]\n# unconditionally command [args]\n#\n# on_uri_match:\n# On an incoming request, the URI is checked against the specified\n# (bash-supported extended) regular expression, and if encounters a match the\n# specified command is executed with the specified arguments.\n#\n# For additional flexibility, on_uri_match will also pass the results of the\n# regular expression match, ${BASH_REMATCH[@]} as additional arguments to the\n# command.\n#\n# unconditionally:\n# Always serve via the specified command. Useful for catchall rules.\n#\n# The following commands are available for use:\n#\n# serve_file FILE\n# Statically serves a single file.\n#\n# serve_dir_with_tree DIRECTORY\n# Statically serves the specified directory using 'tree'. It must be\n# installed and in the PATH.\n#\n# serve_dir_with_ls DIRECTORY\n# Statically serves the specified directory using 'ls -al'.\n#\n# serve_dir DIRECTORY\n# Statically serves a single directory listing. Will use 'tree' if it is\n# installed and in the PATH, otherwise, 'ls -al'\n#\n# serve_dir_or_file_from DIRECTORY\n# Serves either a directory listing (using serve_dir) or a file (using\n# serve_file). Constructs local path by appending the specified root\n# directory, and the URI portion of the client request.\n#\n# serve_static_string STRING\n# Serves the specified static string with Content-Type text/plain.\n#\n# Examples of rules:\n#\n# on_uri_match '^/issue$' serve_file \"/etc/issue\"\n#\n# When a client's requested URI matches the string '/issue', serve them the\n# contents of /etc/issue\n#\n# on_uri_match 'root' serve_dir /\n#\n# When a client's requested URI has the word 'root' in it, serve up\n# a directory listing of /\n#\n# DOCROOT=/var/www/html\n# on_uri_match '/(.*)' serve_dir_or_file_from \"$DOCROOT\"\n# When any URI request is made, attempt to serve a directory listing\n# or file content based on the request URI, by mapping URI's to local\n# paths relative to the specified \"$DOCROOT\"\n#\n\nunconditionally serve_static_string 'Hello, world! You can configure bashttpd by modifying bashttpd.conf.'\n\n# More about commands:\n#\n# It is possible to somewhat easily write your own commands. An example\n# may help. The following example will serve \"Hello, $x!\" whenever\n# a client sends a request with the URI /say_hello_to/$x:\n#\n# serve_hello() {\n# add_response_header \"Content-Type\" \"text/plain\"\n# send_response_ok_exit <<< \"Hello, $2!\"\n# }\n# on_uri_match '^/say_hello_to/(.*)$' serve_hello\n#\n# Like mentioned before, the contents of ${BASH_REMATCH[@]} are passed\n# to your command, so its possible to use regular expression groups\n# to pull out info.\n#\n# With this example, when the requested URI is /say_hello_to/Josh, serve_hello\n# is invoked with the arguments '/say_hello_to/Josh' 'Josh',\n# (${BASH_REMATCH[0]} is always the full match)\n") _rc0 = _rc0.wait() warn("Created bashttpd.conf using defaults. Please review it/configure before running bashttpd again.") exit(1) | |
recv() { echo "< $@" >&2; } | |
def recv () : print("< "+Str(Expand.at()),stderr=subprocess.STDOUT) | |
send() { echo "> $@" >&2; printf '%s\r\n' "$*"; } | |
def send () : print("> "+Str(Expand.at()),stderr=subprocess.STDOUT) print( "%s\\r\\n" % (Expand.star(1)) ) | |
[[ $UID = 0 ]] && warn "It is not recommended to run bashttpd as root." | |
if str(UID.val) == "0": warn("It is not recommended to run bashttpd as root.") | |
DATE=$(date +"%a, %d %b %Y %H:%M:%S %Z") | |
DATE=Bash2Py(os.popen("date +\"%a, %d %b %Y %H:%M:%S %Z\"").read().rstrip("\n")) | |
declare -a RESPONSE_HEADERS=( "Date: $DATE" "Expires: $DATE" "Server: Slash Bin Slash Bash" ) | |
RESPONSE_HEADERS=Bash2Py("(Date: "+str(DATE.val)+" Expires: "+str(DATE.val)+" Server: Slash Bin Slash Bash)") | |
add_response_header() { RESPONSE_HEADERS+=("$1: $2") } | |
def add_response_header (_p1,_p2) : global RESPONSE_HEADERS Make("RESPONSE_HEADERS").setValue("("+str(_p1)+": "+str(_p2)+")") | |
declare -a HTTP_RESPONSE=( [200]="OK" [400]="Bad Request" [403]="Forbidden" [404]="Not Found" [405]="Method Not Allowed" [500]="Internal Server Error" ) | |
HTTP_RESPONSE=Bash2Py(Glob("([200]=OK [400]=Bad Request [403]=Forbidden [404]=Not Found [405]=Method Not Allowed [500]=Internal Server Error)")) | |
send_response() { local code=$1 send "HTTP/1.0 $1 ${HTTP_RESPONSE[$1]}" for i in "${RESPONSE_HEADERS[@]}"; do send "$i" done send while read -r line; do send "$line" done } | |
def send_response (_p1) : global HTTP_RESPONSE global RESPONSE_HEADERS global i global line Make("code").setValue(_p1) send("HTTP/1.0 "+str(_p1)+" "+str(HTTP_RESPONSE.val[_p1] ])) for Make("i").val in Array(RESPONSE_HEADERS.val[@] ]): send(i.val) send() while (line = Bash2Py(raw_input())): send(line.val) | |
send_response_ok_exit() { send_response 200; exit 0; } | |
def send_response_ok_exit () : send_response(200) exit(0) | |
fail_with() { send_response "$1" <<< "$1 ${HTTP_RESPONSE[$1]}" exit 1 } | |
def fail_with (_p1) : send_response(_p1) exit(1) | |
serve_file() { local file=$1 read -r CONTENT_TYPE < <(file -b --mime-type "$file") && \ add_response_header "Content-Type" "$CONTENT_TYPE" read -r CONTENT_LENGTH < <(stat -c'%s' "$file") && \ add_response_header "Content-Length" "$CONTENT_LENGTH" send_response_ok_exit < "$file" } | |
def serve_file (_p1) : global CONTENT_TYPE global CONTENT_LENGTH Make("file").setValue(_p1) if CONTENT_TYPE = Bash2Py(raw_input()): add_response_header("Content-Type", CONTENT_TYPE.val) if CONTENT_LENGTH = Bash2Py(raw_input()): add_response_header("Content-Length", CONTENT_LENGTH.val) send_response_ok_exit() | |
serve_dir_with_tree() { local dir="$1" tree_vers tree_opts basehref x add_response_header "Content-Type" "text/html" # The --du option was added in 1.6.0. read x tree_vers x < <(tree --version) [[ $tree_vers == v1.6* ]] && tree_opts="--du" send_response_ok_exit < \ <(tree -H "$2" -L 1 "$tree_opts" -D "$dir") } | |
def serve_dir_with_tree (_p1) : global tree_vers global tree_opts Make("dir").setValue(_p1) "tree_vers" "tree_opts" "basehref" "x" add_response_header("Content-Type", "text/html") # The --du option was added in 1.6.0. | |
x = Bash2Py(raw_input()) if str(tree_vers.val) == Str(Glob("v1.6*")): Make("tree_opts").setValue("--du") send_response_ok_exit() | |
serve_dir_with_ls() { local dir=$1 add_response_header "Content-Type" "text/plain" send_response_ok_exit < \ <(ls -la "$dir") } | |
def serve_dir_with_ls (_p1) : Make("dir").setValue(_p1) add_response_header("Content-Type", "text/plain") send_response_ok_exit() | |
serve_dir() { local dir=$1 # If `tree` is installed, use that for pretty output. which tree &>/dev/null && \ serve_dir_with_tree "$@" serve_dir_with_ls "$@" fail_with 500 } | |
def serve_dir (_p1) : Make("dir").setValue(_p1) # If `tree` is installed, use that for pretty output. | |
if subprocess.call("which" + " " + "tree",shell=True,stderr=subprocess.STDOUT,stdout=file('/dev/null','wb')) : serve_dir_with_tree(Expand.at()) serve_dir_with_ls(Expand.at()) fail_with(500) | |
serve_dir_or_file_from() { local URL_PATH=$1/$3 shift # sanitize URL_PATH URL_PATH=${URL_PATH//[^a-zA-Z0-9_~\-\.\/]/} [[ $URL_PATH == *..* ]] && fail_with 400 # Serve index file if exists in requested directory [[ -d $URL_PATH && -f $URL_PATH/index.html && -r $URL_PATH/index.html ]] && \ URL_PATH="$URL_PATH/index.html" if [[ -f $URL_PATH ]]; then [[ -r $URL_PATH ]] && \ serve_file "$URL_PATH" "$@" || fail_with 403 elif [[ -d $URL_PATH ]]; then [[ -x $URL_PATH ]] && \ serve_dir "$URL_PATH" "$@" || fail_with 403 fi fail_with 404 } | |
def serve_dir_or_file_from (_p1,_p2,_p3) : Make("URL_PATH").setValue(str(_p1)+"/"+str(_p3)) _rc0 = subprocess.call(["shift"],shell=True) # sanitize URL_PATH | |
URL_PATH=Bash2Py(URL_PATH.val//[^a-zA-Z0-9_os.path.expanduser("~)\-\.\/]/) if str(URL_PATH.val) == Str(Glob("*..*")): fail_with(400) # Serve index file if exists in requested directory | |
if os.path.isdir(str(URL_PATH.val)) and os.path.isfile(str(URL_PATH.val)+"/index.html") and os.access(str(URL_PATH.val)+"/index.html",R_OK): Make("URL_PATH").setValue(str(URL_PATH.val)+"/index.html") if (os.path.isfile(str(URL_PATH.val)) ): if not if os.access(str(URL_PATH.val),R_OK): serve_file(URL_PATH.val, Expand.at()): fail_with(403) elif (os.path.isdir(str(URL_PATH.val)) ): if not if os.access(str(URL_PATH.val),X_OK): serve_dir(URL_PATH.val, Expand.at()): fail_with(403) fail_with(404) | |
serve_static_string() { add_response_header "Content-Type" "text/plain" send_response_ok_exit <<< "$1" } | |
def serve_static_string () : add_response_header("Content-Type", "text/plain") send_response_ok_exit() | |
on_uri_match() { local regex=$1 shift [[ $REQUEST_URI =~ $regex ]] && \ "$@" "${BASH_REMATCH[@]}" } | |
def on_uri_match (_p1) : global REQUEST_URI global BASH_REMATCH Make("regex").setValue(_p1) _rc0 = subprocess.call(["shift"],shell=True) if re.search(str(regex.val),str(REQUEST_URI.val)): subprocess.call([Str(Expand.at()),str(BASH_REMATCH.val[@] ])],shell=True) | |
unconditionally() { "$@" "$REQUEST_URI" } | |
def unconditionally () : global REQUEST_URI subprocess.call([Str(Expand.at()),str(REQUEST_URI.val)],shell=True) | |
# Request-Line HTTP RFC 2616 $5.1 | |
read -r line || fail_with 400 | |
# Request-Line HTTP RFC 2616 $5.1 | |
if not line = Bash2Py(raw_input()): fail_with(400) | |
# strip trailing CR if it exists | |
line=${line%%$'\r'} | |
# strip trailing CR if it exists | |
line=Bash2Py(line.val%%"\r") | |
recv "$line" | |
recv(line.val) | |
read -r REQUEST_METHOD REQUEST_URI REQUEST_HTTP_VERSION <<<"$line" | |
REQUEST_METHOD = Bash2Py(raw_input()) | |
[ -n "$REQUEST_METHOD" ] && \ [ -n "$REQUEST_URI" ] && \ [ -n "$REQUEST_HTTP_VERSION" ] \ || fail_with 400 | |
if not if if str(REQUEST_METHOD.val) != '': str(REQUEST_URI.val) != '': str(REQUEST_HTTP_VERSION.val) != '': fail_with(400) | |
# Only GET is supported at this time | |
[ "$REQUEST_METHOD" = "GET" ] || fail_with 405 | |
# Only GET is supported at this time | |
if not REQUEST_METHOD.val "=""GET" != '': fail_with(405) | |
declare -a REQUEST_HEADERS | |
REQUEST_HEADERS="" | |
while read -r line; do line=${line%%$'\r'} recv "$line" # If we've reached the end of the headers, break. [ -z "$line" ] && break REQUEST_HEADERS+=("$line") done | |
while (line = Bash2Py(raw_input())): Make("line").setValue(line.val%%"\r") recv(line.val) # If we've reached the end of the headers, break. | |
if str(line.val) == '': break Make("REQUEST_HEADERS").setValue("("+str(line.val)+")") | |
source bashttpd.conf | |
_rc0 = subprocess.call(["source","bashttpd.conf"],shell=True) | |
fail_with 500 | |
fail_with(500) | |
ÿ |