Skip to content

Instantly share code, notes, and snippets.

@davidlee
Created August 20, 2008 13:01
Show Gist options
  • Save davidlee/6362 to your computer and use it in GitHub Desktop.
Save davidlee/6362 to your computer and use it in GitHub Desktop.
#!/usr/bin/env zsh
#
# ZWS 1.2 (2006-03-22)
#
# Copyright © 2004-2006, Adam Chodorowski. All rights reserved.
# This file is part of the ZWS program, which is distributed under
# the terms of version 2 of the GNU General Public License.
# ===========================================================================
# Initialization.
emulate -RL zsh
setopt PUSHD_SILENT
setopt EXTENDED_GLOB
export TZ=GMT
# ===========================================================================
# Options and defaults.
declare -a args # Options set on the command line.
declare -A opts # Options set on the command line merged with defaults.
declare -A defs # Defaults.
defs[-p]=4280 # Default port.
defs[-r]=WWW # Default root directory.
defs[-l]=ZWS.log # Default log file.
# ===========================================================================
# Important utility functions.
# ---------------------------------------------------------------------------
# Prints an optional error message and exits the program with the return code
# of the previous command or with 1 if the previous return code was 0.
# $1 = Optional error message to print before exiting.
die()
{
local rc=${?/#0/1}
if [[ -n $1 ]]; then echo "Error: $1"; fi
exit $rc
}
# ===========================================================================
# Load modules and functions.
zmodload zsh/stat || die "Could not load module 'zsh/stat'."
zmodload zsh/datetime || die "Could not load module 'zsh/datetime'."
zmodload zsh/net/tcp || die "Could not load module 'zsh/net/tcp'."
autoload -U tcp_proxy
# ===========================================================================
# Functions.
# ---------------------------------------------------------------------------
# Prints debug output.
# $* = line to write
debug()
{
[[ -n $opts[-d] ]] && echo -E $* >>$opts[-l]
}
# ---------------------------------------------------------------------------
# Reads a single line, removing any CR characters (since clients normally
# send CRLF as end-of-line).
get_line()
{
local line
read -r line
echo -E "$line" | tr -d "\r"
}
# ---------------------------------------------------------------------------
# Writes a single line, using CRLF as end-of-line.
# $* = line to write
put_line()
{
echo -nE "$*"
echo -e "\r"
}
# ---------------------------------------------------------------------------
# Identifies the mime-type of a file.
# $1 = path to file
identify()
{
case "$1" in
*.css) echo text/css;; # Needed by some clients; file(1) doesn't recognize CSS files as such.
*.mpeg) echo video/mpeg;; # Workaround for file(1) 4.13 (gentoo) printing garbage.
*) file -ibL "$1";;
esac
}
# ---------------------------------------------------------------------------
# Parses HTTP Range header field.
# Assumes headers have been parsed and are stored in r_headers.
declare r_range_type
declare r_range_start
declare r_range_end
parse_range()
{
local string=$(echo "$r_headers[Range]" | tr -d "\ ")
if [[ ! -z "$string" ]]; then
r_range_type=$(echo "$string" | cut -f1 -d\=)
r_range_start=$(echo "$string" | cut -f2 -d\= | cut -f1 -d-)
r_range_end=$(echo "$string" | cut -f2 -d\= | cut -f2 -d-)
fi
}
# ---------------------------------------------------------------------------
# Sends file to client.
# $1 = path to file
# $2 = optional response code and message
send_file()
{
if [[ ! -z "$r_version" ]]; then
local length=$(stat +size "$1") # Complete length of file
local count="$length" # Amount of bytes to actually send
local skip=0 # Amount of bytes to skip
if [[ -z "$2" ]]; then
parse_range
debug hdr_rng_start $r_range_start
debug hdr_rng_end $r_range_end
if [[ "$r_range_type" == "bytes" ]]; then
if [[ ! -z "$r_range_start" ]]; then
skip=$(($r_range_start))
fi
if [[ ! -z "$r_range_end" ]]; then
count=$(($r_range_end - $r_range_start + 1))
else
count=$(($length - $skip))
fi
debug snd_206
put_line HTTP/1.1 206 Partial Content
else
debug snd_200
put_line HTTP/1.0 200 OK
fi
else
debug snd_custom
put_line "$2"
fi
put_line Connection: Close
put_line Accept-Ranges: bytes
put_line Date: $(date +"%a, %d %b %Y %H:%M:%S")
put_line Last-Modified: $(strftime "%a, %d %b %Y %H:%M:%S GMT" $(stat +mtime "$1"))
put_line Content-Type: $(identify "$1")
put_line Content-Length: "$count"
if [[ "$r_range_type" == "bytes" ]]; then
put_line Content-Range: ${skip}-$((${count} + ${skip} - 1))/${length}
fi
put_line
if [[ "$r_range_type" == "bytes" ]]; then
dd if="$1" bs=1 skip=$skip count=$count 2>/dev/null
return
fi
fi
dd bs=64k if="$1" 2>/dev/null
}
# ---------------------------------------------------------------------------
# Sends redirect to client.
# $1 = path to redirect to
send_redirect()
{
# FIXME: Handle HTTP/0.9 clients
put_line HTTP/1.0 302 Moved
put_line Location: $1
put_line Connection: Close
put_line Date: $(date +"%a, %d %b %Y %H:%M:%S")
put_line Content-Type: "text/html; charset=ISO-8859-1"
put_line
}
# ---------------------------------------------------------------------------
# Sends error message to client.
# $1 = error code
send_error()
{
local message
if [[ ! -z "$r_version" ]]; then
local description
case "$1" in
400) description="Bad Request";;
404) description="Not Found";;
501) description="Method Not Implemented";;
esac
message="HTTP/1.0 $1 $description"
fi
send_file "Errors/$1" "$message"
}
# ---------------------------------------------------------------------------
# Canonicalizes paths and replaces character entities.
# $1 - path to canonicalize
# FIXME:
#
# 1) /foo/../ -> /
# 2) /./ -> /
# 3) // -> /
#
# sed commands:
# 1) sed -e 's%/[^/]*/\.\./%/%'
# 2) sed -e 's%/\./%/%'
# 3) sed -e 's%//%/%'
make_replace_re()
{
local rs
local -i c_tab=0x09 c_and=0x26 c_slash=0x2f c_bslash=0x5c
for ((i = $1; i <= $2; i += 1)); do
if [[ $i == $c_tab ]]; then
rs+='s/%09/\t/g;'
elif [[ $i == $c_and || $i == $c_slash || $i == $c_bslash ]]; then
rs+="$(printf 's/%%%02x/\%b/g;' $i $(printf '\\x%02x' $i))"
else
rs+="$(printf 's/%%%02x/%b/g;' $i $(printf '\\x%02x' $i))"
fi
done
echo -E "$rs"
}
canonicalize()
{
if [[ -z "$replace_re" ]]; then
local r0="$(make_replace_re 0x09 0x09)"
local r1="$(make_replace_re 0x20 0x7f)"
local r2="$(make_replace_re 0xa0 0xff)"
replace_re="$r0$r1$r2"
fi
echo -E "$1" | sed -e "$replace_re"
}
escape()
{
#FIXME not complete
echo -E "$1" | sed -e 's/ö/%f6/g;s/ü/%fc/g;s/ /%20/g;s/"/%22/g;s/?/%3f/g;s/\[/%5b/g;s/\]/%5d/g;s/{/%7b/g;s/}/%7d/g'
}
# ---------------------------------------------------------------------------
# Parses the request line and headers.
declare r_method
declare r_path
declare r_query
declare r_version
declare -A r_headers
parse_request()
{
local -a parts
request_line=$(get_line)
debug "$request_line"
r_method=$(echo "$request_line" | cut -f1 -d\ )
r_path=$(echo "$request_line" | cut -f2 -d\ )
r_version=$(echo "$request_line" | cut -f3 -d\ )
parts=(${(s:?:)r_path})
r_path=$parts[1]
r_query=$parts[2]
if [[ -n $r_version ]]; then
# Parse HTTP/1.0+ headers
while true; do
header_line=$(get_line)
debug hdr "$header_line"
if [[ -z "$header_line" ]]; then break; fi
key=$(echo $header_line | cut -f1 -d:)
value=$(echo $header_line | cut -f2- -d:)
r_headers+=($key $value)
done
fi
}
# ---------------------------------------------------------------------------
# Lists a directory.
# $1 = directory
list()
{
local search
# Process query
local -a assignments
local -A vars
assignments=(${(s:&:)r_query})
for assignment in $assignments; do
local -a tmp
tmp=(${(s:=:)assignment})
if [[ $tmp[1] == "search" ]]; then
search=$tmp[2]
break
fi
done
search=$(canonicalize "$search")
# FIXME: This is ugly...
put_line HTTP/1.0 200 OK
put_line Connection: Close
put_line Date: $(date +"%a, %d %b %Y %H:%M:%S")
put_line Content-Type: text/html; charset=ISO-8859-1
put_line
pushd "$opts[-r]/${r_path:h}"
if [[ $? != 0 ]]; then
echo "Error: Could not change to directory."
return
fi
local dir="${r_path:h}"
local root_name="/"
# Title
local title=$dir
if [[ -n "$search" ]]; then title+=" [${search//\"/&quot;}]"; fi
echo "<html><head><title>$title</title></head>"
echo "<body>"
# Navigation and search
echo "<font size=\"+2\"><b>"
echo "<form method=\"get\" action=\"$r_path\">"
echo -n "<a href=\"/\">${root_name}</a>"
local slash=""
local href="/"
for part in ${(s:/:)dir}; do
href+="${part}/"
echo -n "${slash}<a href=\"$(escape ${href})\">${part}</a>"
slash=${slash:-"/"}
done
if [[ -n "$search" ]]; then
echo -n " [$search]"
fi
echo " {<input type=\"text\" name=\"search\" value=\"${search//\"/&quot;}\"><input type=\"submit\" value=\"Search\">}"
echo "</form>"
echo "</font></b>"
# List
echo "<table>"
echo "<tr><th></th><th align=\"left\">Name</th><th align=\"right\">Size</th></tr>"
local glob
if [[ -n "$search" ]]; then glob="(#i)**/*${search}*(N)"
else glob="*(N)"
fi
local -i ndirs=0 nfiles=0 tsize=0
local -a gfiles gall
for item in ${~glob}; do
if [[ -d $item ]]; then gall+=($item); ndirs+=1
else gfiles+=($item); nfiles+=1
fi
done
gall+=($gfiles)
for item in $gall; do
local -a parts
local -a directory_parts
local file=""
local directory=""
parts=(${(s:/:)item})
file=$parts[-1]
directory_parts=($parts[1,-2])
directory=${(j:/:)directory_parts}
local href=$(escape "$item")
local dirhref=$(escape "$directory")/
local size="" icon="" mime=""
if [[ -d "$item" ]]; then
icon=Directory
href+="/"
else
size=$(stat +size "$item")
tsize+=$size
icon=File
mime=$(identify "$item")
fi
echo -n "<tr><td><img title=\"$mime\" src=\"/Icons/$icon\" width=\"22\" height=\"22\"></td><td>"
[[ -n "$directory" ]] && echo -n "<a href=\"$dirhref\">${directory}/</a>"
echo "<a href=\"$href\">$file</a></td><td align=\"right\">$size</td></tr>"
done
echo "</table>"
echo "<p>Total $tsize bytes in $nfiles files (and $ndirs directories).</p>"
[[ -n $show_generator ]] && echo "<center><font size=\"-1\">Powered by <a href=\"http://www.chodorowski.com/software/ZWS/\">ZWS</a></font></center>"
echo "</body></html>"
# Restore directory
popd
}
# ---------------------------------------------------------------------------
# Handles a single connection.
serve()
{
parse_request
if [[ $r_method != "GET" ]]; then
send_error 501
return
fi
debug rpl_pre "$r_path"
r_path=$(canonicalize "$r_path")
debug rpl_post "$r_path"
echo "$r_path" | grep "\.\." >/dev/null 2>&2
if [[ $? == 0 ]]; then
send_error 400
return
fi
if [[ "$r_path" != "" ]]; then
# Append index.html if the path has a trailing '/'
# and there is no regular file with that name.
if [[ ! -f "$opts[-r]/$r_path" && "$r_path[-1]" == "/" ]]; then
r_path="${r_path}index.html"
fi
debug "snd path $r_path"
if [[ -f "$opts[-r]/$r_path" ]]; then
send_file "$opts[-r]/$r_path"
elif [[ -d "$opts[-r]/$r_path" ]]; then
send_redirect "$r_path/"
elif [[ -n "$opts[-i]" && "${r_path:t}" == "index.html" ]]; then
list "$r_path"
else
send_error 404
fi
fi
}
# ---------------------------------------------------------------------------
# Parses and processes options.
# $* = Command line options.
parse_options()
{
local name=$1; shift
zparseopts -D -K -E -A opts -a args -- p: r: l: i d h
if [[ $? != 0 || -n $* || -n ${(kM)opts:#-h} ]]; then
echo "Usage: ${name:t} [-p PORT] [-r DIRECTORY] [-l FILE] [-dih]"
echo "-p PORT Specify port to listen on (default: 4280)."
echo "-r DIRECTORY Specify root directory (default: WWW)."
echo "-l FILE Specify path to log file (default: ZWS.log)."
echo "-i Enable automatic directory listing."
echo "-d Enable debug mode."
echo "-h Print this help message."
exit 5
fi
# Merge in defaults.
for opt in ${(k)defs}; do
if [[ -z ${(kM)opts:#$opt} ]]; then
opts[$opt]=$defs[$opt]
fi
done
# Set value of parameterless options to something for simpler conditionals.
for opt in ${(k)opts}; do
opts[$opt]=${opts[$opt]:-on}
done
# Transform relative paths into absolute ones.
for opt in -r -l; do
opts[$opt]=${opts[$opt]/(#m)(#s)[^\/]/$PWD/$MATCH}
done
# Dump options (if in debug mode).
if [[ -n $opts[-d] ]]; then
debug args after processing:
for opt in ${(k)opts}; do
debug "$opt = $opts[$opt]"
done
fi
}
# ===========================================================================
# Main.
parse_options $0 $*
if [[ -z $ZWS_SERVE ]]; then
if [[ -n $opts[-d] ]]; then
ZWS_SERVE=on tcp_proxy $opts[-p] $0 $args
else
tcp_proxy $opts[-p] serve
fi
else
serve
f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment