# pddpserver.tcl
# Synopsis
# not to be run by itself (see pddpboot.tcl)
# based on:
# Simple Sample httpd/1.[01] server
# Stephen Uhler (c) 1996-1997 Sun Microsystems
# http://cvs.sourceforge.net/viewcvs.py/tclhttpd/tclhttpd/bin/mini/mini1.1.tcl
# modified by krzYszcz (2005):
# putting per-server data and all commands in a namespace "::pddp"
# supporting sourcing from within Pd, through the "pddpboot.tcl" wrapper
# inserting the .pd handler
# lots of other changes, too many to list here (run "diff" if curious...)
if {![namespace exists ::pddp]} {
puts stderr "Error: invalid invocation of pddpserver (boot pddp first)"
puts stderr "exiting..."
exit 1
}
if {$::pddp::testrun} { ;# true if sourced from standalone "pddpboot.tcl"
puts stderr "Loading pddpserver, test run..."
proc bgerror {msg} {
global errorInfo
puts stderr "bgerror: $msg\n$errorInfo"
}
} else {
puts stderr "Loading pddpserver"
catch {console show}
}
namespace eval ::pddp {
variable thePort 0
variable theState
variable theMimeTypes
variable theErrors
variable theErrorFormat
# "theState" contains the server state:
# root: the root of the document directory
# default: default document name
# listen: the main listening socket id
# naccepts: a count of accepted connections so far
# maxtime: the max time (msec) allowed to complete an http request
# maxused: the max # of requests for a socket
array set theState {
root ""
default index.html
listen ""
naccepts 0
nrequests 0
nerrors 0
maxtime 600000
maxused 25
bufsize 32768
}
set theState(root) $env(HOME)
array set theMimeTypes {
{} text/plain
.txt text/plain
.html text/html
.gif image/gif
.jpg image/jpeg
.pd text/html
}
# HTTP/1.[01] error codes (the ones we use)
array set theErrors {
204 {No Content}
400 {Bad Request}
404 {Not Found}
405 {Method Not Allowed}
408 {Request Timeout}
411 {Length Required}
419 {Expectation Failed}
500 {Internal Server Error}
503 {Service Unavailable}
504 {Service Temporarily Unavailable}
505 {HTTP Version Not Supported}
}
# Generic error response
set theErrorFormat {
Error: %1$s
Got the error: %2$s
while trying to obtain %3$s
}
}
proc ::pddp::srvUse {{root {}} {port 0}} {
variable theState
if {[string length $theState(listen)]} {
if {[string length $root] && ![string equal $root $theState(root)]} {
srvLog $theState(listen) Warning "Redirection attempt for $root"
}
} else {
srvStart $root $port
}
}
# Start the server by listening for connections on the desired port.
proc ::pddp::srvStart {{root {}} {port 0}} {
variable thePort
variable theState
puts stderr "Starting pddp server on [info hostname]"
if {[string length $root]} {
set theState(root) $root
}
# we do not handle multiple pddpservers, LATER rethink
srvStop
array set theState [list naccepts 0 nrequests 0 nerrors 0]
for { set thePort $port } {$thePort < 65535 } {incr thePort } {
if {[catch {set theState(listen) \
[socket -server ::pddp::srvAccept $thePort]} res]} {
if {$thePort == 0} {
# FIXME this is a critical error
set thePort 32768
}
} else { break }
}
if {$thePort == 65535} {
srvLog none Error "Could not find port available for listening"
} else {
if {$thePort == 0} {
set thePort [lindex [fconfigure $theState(listen) -sockname] 2]
}
srvLog $theState(listen) Port $thePort
srvLog $theState(listen) Root directory \"$root\"
}
after 120 update ;# FIXME might be needed on windows they say, test there
return $thePort
}
proc ::pddp::srvStop {} {
variable thePort
variable theState
if {[string length $theState(listen)]} {
if {[catch {close $theState(listen)} res]} {
srvLog $theState(listen) Warning [list $res while closing socket]
} else {
srvLog $theState(listen) Closed.
}
set theState(listen) ""
update
}
}
# Accept a new connection from the server and set up a handler
# to read the request from the client.
proc ::pddp::srvAccept {sock ipaddr port} {
variable theState
variable theSockData$sock
# reject remote requests, LATER revisit
if {[string equal $ipaddr "127.0.0.1"]} {
incr theState(naccepts)
srvReset $sock $theState(maxused)
srvLog $sock Connect $ipaddr $port
} else {
srvLog $sock Warning "rejecting remote connection request from $ipaddr"
srvSockDone $sock 1
}
}
# Initialize or reset the socket state
proc ::pddp::srvReset {sock nlft} {
variable theState
upvar 0 ::pddp::theSockData$sock sockData
array set sockData [list state start linemode 1 version 0 nleft $nlft]
set sockData(cancel) \
[after $theState(maxtime) [list srvTimeout $sock]]
fconfigure $sock -blocking 0 -buffersize $theState(bufsize) \
-translation {auto crlf}
fileevent $sock readable [list ::pddp::srvRead $sock]
}
# Read data from a client request
# 1) read the request line
# 2) read the mime headers
# 3) read the additional data (if post && content-length not satisfied)
proc ::pddp::srvRead {sock} {
variable theState
upvar 0 ::pddp::theSockData$sock sockData
# Use line mode to read the request and the mime headers
if {$sockData(linemode)} {
set readCount [gets $sock line]
set state [string compare $readCount 0],$sockData(state)
switch -glob -- $state {
1,start {
if {[regexp {(HEAD|POST|GET) ([^?]+)\??([^ ]*) HTTP/1.([01])} \
$line x sockData(proto) sockData(url) \
sockData(query) sockData(version)]} {
set sockData(state) mime
incr theState(nrequests)
srvLog $sock Request $sockData(nleft) $line
} else {
srvError $sock 400 $line
}
}
0,start {
srvLog $sock Warning "Initial blank line fetching request"
}
1,mime {
if {[regexp {([^:]+):[ ]*(.*)} $line {} key value]} {
set key [string tolower $key]
set sockData(key) $key
if {[info exists sockData(mime,$key)]} {
append sockData(mime,$key) ", $value"
} else {
set sockData(mime,$key) $value
}
} elseif {[regexp {^[ ]+(.+)} $line {} value] && \
[info exists sockData(key)]} {
append sockData(mime,$sockData($key)) " " $value
} else {
srvError $sock 400 $line
}
}
0,mime {
if {$sockData(proto) == "POST" && \
[info exists sockData(mime,content-length)]} {
set sockData(linemode) 0
set sockData(count) $sockData(mime,content-length)
if {$sockData(version) && \
[info exists sockData(mime,expect)]} {
if {$sockData(mime,expect) == "100-continue"} {
puts $sock "100 Continue HTTP/1.1\n"
flush $sock
} else {
srvError $sock 419 $sockData(mime,expect)
}
}
fconfigure $sock -translation {binary crlf}
} elseif {$sockData(proto) != "POST"} {
srvRespond $sock
} else {
srvError $sock 411 "Confusing mime headers"
}
}
-1,* {
if {[eof $sock]} {
srvLog $sock Error "Broken connection fetching request"
srvSockDone $sock 1
} else {
puts stderr "Partial read, retrying"
}
}
default {
srvError $sock 404 "Invalid http state: $state,[eof $sock]"
}
}
# Use counted mode to get the post data
} elseif {![eof $sock]} {
append sockData(postdata) [read $sock $sockData(count)]
set sockData(count) [expr {$sockData(mime,content-length) - \
[string length $sockData(postdata)]}]
if {$sockData(count) == 0} {
srvRespond $sock
}
} else {
srvLog $sock Error "Broken connection reading POST data"
srvSockDone $sock 1
}
}
# Done with the socket, either close it, or set up for next fetch
# sock: The socket I'm done with
# doclose: If true, close the socket, otherwise set up for reuse
proc ::pddp::srvSockDone {sock doclose} {
variable theState
upvar 0 ::pddp::theSockData$sock sockData
after cancel $sockData(cancel)
set nleft [incr sockData(nleft) -1]
unset sockData
if {$doclose} {
close $sock
} else {
srvReset $sock $nleft
}
return ""
}
# A timeout happened
proc ::pddp::srvTimeout {sock} {
srvError $sock 408
}
# FIXME test if "path" has its patch window already open...
proc ::pddp::srvPdHandler {sock path} {
set dir [file dirname $path]
set tail [file tail $path]
if {[catch {pd [concat pd open $tail $dir \;]}]} {
srvError $sock 504
} else {
srvError $sock 204
# FIXME raise; focus (test on windows)
}
}
# Handle file system queries. This is a place holder for a more
# generic dispatch mechanism.
proc ::pddp::srvRespond {sock} {
variable theState
variable theUrlCache
upvar 0 ::pddp::theSockData$sock sockData
regsub {(^http://[^/]+)?} $sockData(url) {} url
if {[info exists theUrlCache($url)]} {
set mypath $theUrlCache($url)
} else {
set mypath [srvUrl2File $theState(root) $url]
if {[file isdirectory $mypath]} {
append mypath / $theState(default)
}
set theUrlCache($url) $mypath
}
if {[string length $mypath] == 0} {
srvError $sock 400
} elseif {![file readable $mypath]} {
if {[string equal [file tail $mypath] "favicon.ico"]} {
srvError $sock 204 ;# FIXME design something
} else {
srvError $sock 404 $mypath
}
} else {
set ext [file extension $mypath]
if {[string equal $ext ".pd"]} {
srvPdHandler $sock $mypath
return
}
puts $sock "HTTP/1.$sockData(version) 200 Data follows"
puts $sock "Date: [srvGetDate [clock seconds]]"
puts $sock "Last-Modified: [srvGetDate [file mtime $mypath]]"
puts $sock "Content-Type: [srvContentType $ext]"
puts $sock "Content-Length: [file size $mypath]"
## Should also close socket if recvd connection close header
set doclose [expr {$sockData(nleft) == 0}]
if {$doclose} {
puts $sock "Connection close:"
} elseif {$sockData(version) == 0 && \
[info exists sockData(mime,connection)]} {
if {$sockData(mime,connection) == "Keep-Alive"} {
set doclose 0
puts $sock "Connection: Keep-Alive"
}
}
puts $sock ""
flush $sock
if {$sockData(proto) != "HEAD"} {
set in [open $mypath]
fconfigure $sock -translation binary
fconfigure $in -translation binary
fcopy $in $sock -command \
[list ::pddp::srvCopyDone $in $sock $doclose]
} else {
srvSockDone $sock $doclose
}
}
}
# Callback when file is done being output to client
# in: The fd for the file being copied
# sock: The client socket
# doclose: close the socket if true
# bytes: The # of bytes copied
# error: The error message (if any)
proc ::pddp::srvCopyDone {in sock doclose bytes {error {}}} {
close $in
srvLog $sock Done $bytes bytes
srvSockDone $sock $doclose
}
# Convert the file suffix into a mime type.
proc ::pddp::srvContentType {ext} {
variable theMimeTypes
set type text/plain
catch {set type $theMimeTypes($ext)}
return $type
}
# Respond with an error reply
# sock: The socket handle to the client
# code: The httpd error code
# args: Additional information for error logging
proc ::pddp::srvError {sock code args} {
variable theState
variable theErrors
variable theErrorFormat
upvar 0 ::pddp::theSockData$sock sockData
append sockData(url) ""
incr theState(nerrors)
set message [format $theErrorFormat $code $theErrors($code) $sockData(url)]
append head "HTTP/1.$sockData(version) $code $theErrors($code)" \n
append head "Date: [srvGetDate [clock seconds]]" \n
append head "Connection: close" \n
append head "Content-Length: [string length $message]" \n
# Because there is an error condition, the socket may be "dead"
catch {
fconfigure $sock -translation crlf
puts -nonewline $sock $head\n$message
flush $sock
} reason
srvSockDone $sock 1
if {$code < 300} {set status Status} else {set status Error}
srvLog $sock $status $code $theErrors($code) $args $reason
}
# Generate a date string in HTTP format.
proc ::pddp::srvGetDate {seconds} {
return [clock format $seconds -format {%a, %d %b %Y %T %Z}]
}
# Log an Httpd transaction.
# This should be replaced as needed.
proc ::pddp::srvLog {sock args} {
puts stderr "pddp log ($sock): $args"
}
# Convert a url into a pathname. (UNIX version only)
# This is probably not right, and belongs somewhere else.
# - Remove leading http://... if any
# - Collapse all /./ and /../ constructs
# - expand %xx sequences -> disallow "/"'s and "."'s due to expansions
proc ::pddp::srvUrl2File {root url} {
regsub -all {//+} $url / url ;# collapse multiple /'s
while {[regsub -all {/\./} $url / url]} {} ;# collapse /./
while {[regsub -all {/\.\.(/|$)} $url /\x81\\1 url]} {} ;# mark /../
while {[regsub "/\[^/\x81]+/\x81/" $url / url]} {} ;# collapse /../
if {![regexp "\x81|%2\[eEfF]" $url]} { ;# invalid /../, / or . ?
return $root[srvCgiMap $url]
} else {
return ""
}
}
# Decode url-encoded strings.
proc ::pddp::srvCgiMap {data} {
regsub -all {([][$\\])} $data {\\\1} data
regsub -all {%([0-9a-fA-F][0-9a-fA-F])} $data {[format %c 0x\1]} data
return [subst $data]
}
if {$::pddp::testrun} { ;# true if tested as a standalone script
if {$argc > 1} {
set root [lindex $argv 1]
set port [lindex $argv 2]
if {![string is integer -strict $port]} {
set port 32768
}
} else {
set root $env(HOME)
set port 32768
}
::pddp::srvStart $root $port
vwait forever
}