# # phttpd.tcl -- # # Simple Sample httpd/1.0 server in 250 lines of Tcl. # Stephen Uhler / Brent Welch (c) 1996 Sun Microsystems. # # Modified to use namespaces, direct url-to-procedure access # and thread pool package. Grown little larger since ;) # # Usage: # phttpd::create port # # port Tcp port where the server listens # # Example: # # # tclsh8.6 # % source phttpd.tcl # % phttpd::create 5000 # % vwait forever # # Starts the server on the port 5000. Also, look at the Httpd array # definition in the "phttpd" namespace declaration to find out # about other options you may put on the command line. # # You can use: http://localhost:5000/monitor URL to test the # server functionality. # # Copyright (c) 2002 by Zoran Vasiljevic. # # See the file "license.terms" for information on usage and # redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. # ----------------------------------------------------------------------------- package require Tcl 8.4 package require Thread 2.5 # # Modify the following in order to load the # example Tcl implementation of threadpools. # Per default, the C-level threadpool is used. # if {0} { eval [set TCL_TPOOL {source ../tpool/tpool.tcl}] } namespace eval phttpd { variable Httpd; # Internal server state and config params variable MimeTypes; # Cache of file-extension/mime-type variable HttpCodes; # Portion of well-known http return codes variable ErrorPage; # Format of error response page in html array set Httpd { -name phttpd -vers 1.0 -root "." -index index.htm } array set HttpCodes { 400 "Bad Request" 401 "Not Authorized" 404 "Not Found" 500 "Server error" } array set MimeTypes { {} "text/plain" .txt "text/plain" .htm "text/html" .htm "text/html" .gif "image/gif" .jpg "image/jpeg" .png "image/png" } set ErrorPage { Error: %1$s %2$s

%3$s

Problem in accessing "%4$s" on this server.


%5$s/%6$s Server at %7$s Port %8$s } } # # phttpd::create -- # # Start the server by listening for connections on the desired port. # # Arguments: # port # args # # Side Effects: # None.. # # Results: # None. # proc phttpd::create {port args} { variable Httpd set arglen [llength $args] if {$arglen} { if {$arglen % 2} { error "wrong \# args, should be: key1 val1 key2 val2..." } set opts [array names Httpd] foreach {arg val} $args { if {[lsearch $opts $arg] == -1} { error "unknown option \"$arg\"" } set Httpd($arg) $val } } # # Create thread pool with max 8 worker threads. # if {[info exists ::TCL_TPOOL] == 0} { # # Using the internal C-based thread pool # set initcmd "source ../phttpd/phttpd.tcl" } else { # # Using the Tcl-level hand-crafted thread pool # append initcmd "source ../phttpd/phttpd.tcl" \n $::TCL_TPOOL } set Httpd(tpid) [tpool::create -maxworkers 8 -initcmd $initcmd] # # Start the server on the given port. Note that we wrap # the actual accept with a helper after/idle callback. # This is a workaround for a well-known Tcl bug. # socket -server [namespace current]::_Accept $port } # # phttpd::_Accept -- # # Helper procedure to solve Tcl shared-channel bug when responding # to incoming connection and transfering the channel to other thread(s). # # Arguments: # sock incoming socket # ipaddr IP address of the remote peer # port Tcp port used for this connection # # Side Effects: # None. # # Results: # None. # proc phttpd::_Accept {sock ipaddr port} { after idle [list [namespace current]::Accept $sock $ipaddr $port] } # # phttpd::Accept -- # # Accept a new connection from the client. # # Arguments: # sock # ipaddr # port # # Side Effects: # None.. # # Results: # None. # proc phttpd::Accept {sock ipaddr port} { variable Httpd # # Setup the socket for sane operation # fconfigure $sock -blocking 0 -translation {auto crlf} # # Detach the socket from current interpreter/tnread. # One of the worker threads will attach it again. # thread::detach $sock # # Send the work ticket to threadpool. # tpool::post -detached $Httpd(tpid) [list [namespace current]::Ticket $sock] } # # phttpd::Ticket -- # # Job ticket to run in the thread pool thread. # # Arguments: # sock # # Side Effects: # None.. # # Results: # None. # proc phttpd::Ticket {sock} { thread::attach $sock fileevent $sock readable [list [namespace current]::Read $sock] # # End of processing is signalized here. # This will release the worker thread. # vwait [namespace current]::done } # # phttpd::Read -- # # Read data from client and parse incoming http request. # # Arguments: # sock # # Side Effects: # None. # # Results: # None. # proc phttpd::Read {sock} { variable Httpd variable data set data(sock) $sock while {1} { if {[catch {gets $data(sock) line} readCount] || [eof $data(sock)]} { return [Done] } if {![info exists data(state)]} { set pat {(POST|GET) ([^?]+)\??([^ ]*) HTTP/1\.[0-9]} if {[regexp $pat $line x data(proto) data(url) data(query)]} { set data(state) mime continue } else { Log error "bad request line: (%s)" $line Error 400 return [Done] } } # string compare $readCount 0 maps -1 to -1, 0 to 0, and > 0 to 1 set state [string compare $readCount 0],$data(state),$data(proto) switch -- $state { "0,mime,GET" - "0,query,POST" { Respond return [Done] } "0,mime,POST" { set data(state) query set data(query) "" } "1,mime,POST" - "1,mime,GET" { if [regexp {([^:]+):[ ]*(.*)} $line dummy key value] { set data(mime,[string tolower $key]) $value } } "1,query,POST" { append data(query) $line set clen $data(mime,content-length) if {($clen - [string length $data(query)]) <= 0} { Respond return [Done] } } default { if [eof $data(sock)] { Log error "unexpected eof; client closed connection" return [Done] } else { Log error "bad http protocol state: %s" $state Error 400 return [Done] } } } } } # # phttpd::Done -- # # Close the connection socket # # Arguments: # s # # Side Effects: # None.. # # Results: # None. # proc phttpd::Done {} { variable done variable data close $data(sock) if {[info exists data]} { unset data } set done 1 ; # Releases the request thread (See Ticket procedure) } # # phttpd::Respond -- # # Respond to the query. # # Arguments: # s # # Side Effects: # None.. # # Results: # None. # proc phttpd::Respond {} { variable data if {[info commands $data(url)] == $data(url)} { # # Service URL-procedure # if {[catch { puts $data(sock) "HTTP/1.0 200 OK" puts $data(sock) "Date: [Date]" puts $data(sock) "Last-Modified: [Date]" } err]} { Log error "client closed connection prematurely: %s" $err return } if {[catch {$data(url) data} err]} { Log error "%s: %s" $data(url) $err } } else { # # Service regular file path # set mypath [Url2File $data(url)] if {![catch {open $mypath} i]} { if {[catch { puts $data(sock) "HTTP/1.0 200 OK" puts $data(sock) "Date: [Date]" puts $data(sock) "Last-Modified: [Date [file mtime $mypath]]" puts $data(sock) "Content-Type: [ContentType $mypath]" puts $data(sock) "Content-Length: [file size $mypath]" puts $data(sock) "" fconfigure $data(sock) -translation binary -blocking 0 fconfigure $i -translation binary fcopy $i $data(sock) close $i } err]} { Log error "client closed connection prematurely: %s" $err } } else { Log error "%s: %s" $data(url) $i Error 404 } } } # # phttpd::ContentType -- # # Convert the file suffix into a mime type. # # Arguments: # path # # Side Effects: # None.. # # Results: # None. # proc phttpd::ContentType {path} { # @c Convert the file suffix into a mime type. variable MimeTypes set type "text/plain" catch {set type $MimeTypes([file extension $path])} return $type } # # phttpd::Error -- # # Emit error page # # Arguments: # s # code # # Side Effects: # None.. # # Results: # None. # proc phttpd::Error {code} { variable Httpd variable HttpCodes variable ErrorPage variable data append data(url) "" set msg \ [format $ErrorPage \ $code \ $HttpCodes($code) \ $HttpCodes($code) \ $data(url) \ $Httpd(-name) \ $Httpd(-vers) \ [info hostname] \ 80 \ ] if {[catch { puts $data(sock) "HTTP/1.0 $code $HttpCodes($code)" puts $data(sock) "Date: [Date]" puts $data(sock) "Content-Length: [string length $msg]" puts $data(sock) "" puts $data(sock) $msg } err]} { Log error "client closed connection prematurely: %s" $err } } # # phttpd::Date -- # # Generate a date string in HTTP format. # # Arguments: # seconds # # Side Effects: # None.. # # Results: # None. # proc phttpd::Date {{seconds 0}} { # @c Generate a date string in HTTP format. if {$seconds == 0} { set seconds [clock seconds] } clock format $seconds -format {%a, %d %b %Y %T %Z} -gmt 1 } # # phttpd::Log -- # # Log an httpd transaction. # # Arguments: # reason # format # args # # Side Effects: # None.. # # Results: # None. # proc phttpd::Log {reason format args} { set messg [eval format [list $format] $args] set stamp [clock format [clock seconds] -format "%d/%b/%Y:%H:%M:%S"] puts stderr "\[$stamp\]\[-thread[thread::id]-\] $reason: $messg" } # # phttpd::Url2File -- # # Convert a url into a pathname. # # Arguments: # url # # Side Effects: # None.. # # Results: # None. # proc phttpd::Url2File {url} { variable Httpd lappend pathlist $Httpd(-root) set level 0 foreach part [split $url /] { set part [CgiMap $part] if [regexp {[:/]} $part] { return "" } switch -- $part { "." { } ".." {incr level -1} default {incr level} } if {$level <= 0} { return "" } lappend pathlist $part } set file [eval file join $pathlist] if {[file isdirectory $file]} { return [file join $file $Httpd(-index)] } else { return $file } } # # phttpd::CgiMap -- # # Decode url-encoded strings. # # Arguments: # data # # Side Effects: # None.. # # Results: # None. # proc phttpd::CgiMap {data} { regsub -all {\+} $data { } 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] } # # phttpd::QueryMap -- # # Decode url-encoded query into key/value pairs. # # Arguments: # query # # Side Effects: # None.. # # Results: # None. # proc phttpd::QueryMap {query} { set res [list] regsub -all {[&=]} $query { } query regsub -all { } $query { {} } query; # Othewise we lose empty values foreach {key val} $query { lappend res [CgiMap $key] [CgiMap $val] } return $res } # # monitor -- # # Procedure used to test the phttpd server. It responds on the # http://:/monitor # # Arguments: # array # # Side Effects: # None.. # # Results: # None. # proc /monitor {array} { upvar $array data ; # Holds the socket to remote client # # Emit headers # puts $data(sock) "HTTP/1.0 200 OK" puts $data(sock) "Date: [phttpd::Date]" puts $data(sock) "Content-Type: text/html" puts $data(sock) "" # # Emit body # puts $data(sock) [subst {

[clock format [clock seconds]]

}] after 1 ; # Simulate blocking call puts $data(sock) [subst { }] } # EOF $RCSfile: phttpd.tcl,v $ # Emacs Setup Variables # Local Variables: # mode: Tcl # indent-tabs-mode: nil # tcl-basic-offset: 4 # End: