zshttpd

This is my first zsh/net/tcp toy, a webserver (httpd) written in zsh. Thanks to very powerful zle's non-blocking fd event handler mechanism, this webserver can co-exist with your command-line.

Really a toy, but by this you can imagine what you can do (with Ajax) in Zsh!

I dream this (+ ssh port-forwarding) to be an alternative to webmin (web based unix admin). IMHO, the source of power, flexibility and simplicity of Unix (in Administration Task) is at command-line. Most webmin hides it completely, so it loose many, and become not for experts.

But with this zshttpd, you don't need to worry about that. Imagine future another webmin(zwebmin?), based on zshttpd. Even if pre-defined zwebmin module is not enough good for your current task, you can always type your command. And then you can bookmark your new command-line, which is retrieved from running zsh's history!

To try this, save this as zshttpd.zsh, source it, do zshttpd $YOUR_DOCROOT and access http://localhost:8080/.

typeset -A ZSHTTPD
: ${ZSHTTPD[port]=8080}
: ${ZSHTTPD[verbose]=0}
: ${ZSHTTPD[host]=localhost}
: ${ZSHTTPD[listenfd]=''}
: ${ZSHTTPD[docroot]=''}

typeset -A ZSHTTPD_MIME
ZSHTTPD_MIME=(
    html  text/html
    txt   text/plain
    js    text/javascript
    css   text/css

    gif   image/gif
    jpg   image/jpeg
    jpeg  image/jpeg
)

# -i: ignore error
zmodload -i zsh/net/tcp
zmodload -i zsh/stat

function zshttpd {
    ZSHTTPD[docroot]=$1; shift
    local name value
    for name value in $*
    do
      ZSHTTPD[$name]=$value
    done
    ZSHTTPD[orig_prompt]=$PROMPT
    PROMPT="H $ZSHTTPD[orig_prompt]"
    zshttpd_listen
}

function zshttpd_listen {
    ztcp -l -v $ZSHTTPD[port] || return 1
    ZSHTTPD[listenfd]=$REPLY
    zle -F $ZSHTTPD[listenfd] zshttpd_accept
}

function zshttpd_print_header {
    local code=$1 type=$2 name value; shift; shift
    print HTTP/1.1 $code
    print Host: localhost
    print Connection: keep-alive
    print Content-type: $type
    for name value in $*
    do
      print $name $value
    done
    print ""
}

function zshttpd_accept {
    (($ZSHTTPD[verbose] > 1)) && set -x
    ztcp -a $ZSHTTPD[listenfd] || {
	print "Can't accept?"
	return 1
    }
    local fd=$REPLY
    # Should do access restriction

    local -a query
    local -A request header
    local stat
    while zshttpd_read_header request query header $fd; do
	local target=$ZSHTTPD[docroot]$request[url]
	if [[ -d $target ]] && [[ -r ${target}/index.html ]]; then
	    target=${target}/index.html
	fi
	local mtype=${ZSHTTPD_MIME[$target:e]:-text/plain}
	
	if [[ -r $target ]]; then
	    stat -H stat $target
	    zshttpd_print_header >&$fd "200 Ok" $mtype \
		Content-Length: $stat[size]
	    cat >&$fd $target
	    (($ZSHTTPD[verbose])) && print -l "($request[url])" ${query}
	    # zle -U $request[url]
	else
	    zshttpd_print_header >&$fd "404 Not found" text/plain
	    print >&$fd "not found: $request"
	fi
	(($request[keepalive])) || {
	    (($ZSHTTPD[verbose])) && print closing $fd
	    break
	}
    done
    ztcp -c -v $fd
    (($ZSHTTPD[verbose])) && print closed $fd
    set +x
}

function zshttpd_read_header {
    ((ARGC == 4)) || {
	print 1>&2 "Usage: $0 requestVar queryVar headerVar fd#"
	return 1
    }
    #
    local requestVar=$1 queryVar=$2 headerVar=$3 fd=$4
    local -A _header
    local method url version
    #
    read -r -u $fd method url version || return 1
    # print 1>&2 "method=<$method> url=<$url>"
    #
    local key value
    while read -r -u $fd key value && [[ $key != $'\r' ]] && [[ -n $value ]]
    do
      value=${value%$'\r'}
      case $key in
	  (*:) # Same property will be overwritten.
	  _header[$key:l]=$value
	  ;;
	  (*)  # Continuation by leading space is not supported.
	  ;;
      esac
    done

    local qstr=''
    case $method in
	(GET)
	local q=$url[(i)\?]
	# print 1>&2 "q=$q; url=<$url>; url#=($#url)"
	if (($q <= $#url)); then
	    qstr=$url[$q+1,$#url]
	    url=$url[1,$q-1]
	fi
	;;
	(POST)
	# How can I use indirection on assoc array?
	# ${{(P)headerVar}[content-length:]} doesn't work.
	read -r -u $fd -k $_header[content-length:] qstr
	;;
	(*)
	;;
    esac
    local -a _query
    if [[ -n $qstr ]]; then
	set -A _query "${(ps:\0:)"$(zshttpd_parse_query $qstr)"}"
    fi

    integer keepalive=0
    if [[ $_header[connection:] == keep-alive ]]; then
	keepalive=1
    fi    

    # pass all results.
    set -A $requestVar method $method url $url version $version \
	keepalive $keepalive
    set -A $headerVar ${(kv)_header}
    set -A $queryVar $_query
    return 0
}

function zshttpd_parse_query {
    # use this via ${(ps:\0:)"$(this command)"}
    perl -Mstrict -MCGI -we '
      my $cgi = new CGI(shift);
      print join("\0", map {"$_\0" . join("\t", $cgi->param($_))} $cgi->param)
    ' $1
}
 
code/scripts/zshttpd.txt · Last modified: 2010/01/05 09:20 (external edit)