Created
August 20, 2008 13:01
-
-
Save davidlee/6362 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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//\"/"}]"; 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//\"/"}\"><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