# -*- Tcl -*- $ # # The XOTcl class Httpd implements an HTTP/1.0 and HTTP/1.1 server with # basic functionality. # # Gustaf Neumann (neumann@wu-wien.ac.at) package provide xotcl::comm::httpd 2.0 set VERSION [package present xotcl::comm::httpd] package require XOTcl 2.0 #package require xotcl::comm::httpAccess package require xotcl::comm::connection package require xotcl::trace package require xotcl::comm::mime namespace eval ::xotcl::comm::httpd { namespace import -force ::xotcl::* Class Httpd -parameter { {port 80} ipaddr {root ./} {logdir "[set ::xotcl::logdir]"} {httpdWrk Httpd::Wrk} {redirects [list]} {workerTimeout 10000} } Httpd proc Date seconds {clock format $seconds -format {%a, %d %b %Y %T GMT} -gmt true} Httpd instproc checkRoot {} { my instvar root set root [string trimright $root /] if {![file isdir $root]} { puts stderr "Warning: create root directory '$root'" file mkdir $root } # make directory absolute set currentdir [pwd] cd $root set root [pwd] #puts stderr "[self] root=$root" cd $currentdir } proc ! string { set f [open [::xotcl::tmpdir]log w+]; puts $f "[clock format [clock seconds]] $string" close $f} Httpd instproc init args { my instvar port logdir logfile redirects if {![my exists workerMixins]} { my set workerMixins {} #puts stderr "resetting workermixins of [self]" } next set proto [string trim [namespace tail [my info class]] :d] puts stderr "Starting XOTcl [string toupper $proto] server $::VERSION\ [string tolower $proto]://[info hostname]:$port/" # Start a server by listening on the port if {[my exists ipaddr]} {set ip "-myaddr [my set ipaddr]"} {set ip ""} my set listen [eval [list socket -server [list [self] accept]] $ip $port] #my set listen [socket -server [list [self] accept] $port] my checkRoot if {![file isdir $logdir]} {file mkdir $logdir} set logfile [open $logdir/serverlog-$port a+] my array set requiresBody \ {GET 0 HEAD 0 POST 1 PUT 1 DELETE 0 OPTIONS 0 TRACE 0} } Httpd instproc destroy {} { # destructor catch {close [my set listen]} catch {close [my set logfile]} next } Httpd instproc accept {socket ipaddr port} { # Accept a new connection and set up a handler #puts stderr "using workermixins of [self] {[my set workerMixins]}" [my set httpdWrk] new -childof [self] -socket $socket -ipaddr $ipaddr \ -port $port -mixin [my set workerMixins] } Httpd instproc redirect list { foreach {pattern hostport} $list { my lappend redirects $pattern $hostport } } Class Httpd::Wrk -parameter {socket port ipaddr} Httpd::Wrk array set codes { 200 {Data follows} 201 {Created} 204 {No Content} 302 {Moved Temporarily} 304 {Not Modified} 400 {Bad Request} 401 {Unauthorized} 402 {Payment Required} 403 {Forbidden} 404 {Not Found} 405 {Method Not Allowed} 406 {Not Acceptable} 408 {Request Timeout} 411 {Length Required} 500 {Internal Server Error} 503 {Service Unavailable} 504 {Service Temporarily Unavailable} } Httpd::Wrk instproc formData {} {my set formData} Httpd::Wrk instproc init args { # Constructor my instvar socket port ipaddr my set formData [list] my set replyHeaderFields [list] next my makeConnection $socket my log Connect "$ipaddr $port" [self]::connection translation {auto crlf} [self]::connection event readable [self] firstLine } Httpd::Wrk instproc makeConnection {socket} { Connection create [self]::connection -socket $socket -req [self] } Httpd::Wrk instproc close {} { # logical close of a single request #my showCall my instvar version timeout meta set eof [[self]::connection eof] if {$version > 1.0 && !$eof} { #my showMsg "!EOF in http/$version" [self]::connection flush set timeout [after [[my info parent] workerTimeout] [self] destroy] ### reset parameters, worker will be potentially reused if {[array exists meta]} { unset meta array set meta {} } unset version if {[my exists user]} { my unset user my unset realm } foreach c [my set formData] { $c destroy } my set replyHeaderFields [list] my set formData {} #my showVars [self]::connection translation {auto crlf} [self]::connection event readable [self] firstLine } elseif {$eof} { #my showMsg "Destroy in http/$version" # the client side has closed the connection my destroy } else { #my showMsg "!EOF in http/$version ???" # we close the connection actively (e.g. forced by an error) [self]::connection flush #puts stderr "DESTROY----this line should never show up" my destroy } } Httpd::Wrk instproc destroy {} { #my showCall if {[my isobject [self]::connection]} { [self]::connection close } next } Httpd::Wrk instproc freeConnection {} { } Httpd::Wrk instproc firstLine {} { # Read the first line of the request #my showCall my instvar method resourceName hasFormData query fileName \ version timeout if {[info exists timeout]} { after cancel $timeout unset timeout } my lappend replyHeaderFields Date [Httpd Date [clock seconds]] set n [[self]::connection gets firstLine] if {$n > 0} { #::puts stderr "[self] firstline=<$firstLine>" # parse request line, ignore HTTP version for now if {[regexp {^(POST|GET|PUT|HEAD|OPTIONS) ([^?]+)(\??)([^ ]*) *HTTP/(.*)$} \ $firstLine _ method resourceName hasFormData query version]} { set resourceName [string trimright [string trimleft $resourceName ./] " "] # construct filename [my info parent] instvar root set fileName $root/[url decodeName $resourceName] #puts stderr ---[encoding convertfrom utf-8 $fileName]---- set fileName [encoding convertfrom utf-8 $fileName] # # Avoid directory traversal attacks # set fileName [file normalize $fileName] if {![string match $root/* $fileName]} { set fileName $root } # my decode-formData $query my log Query $firstLine if {[my exists forceVersion1.0]} { set version 1.0 } [self]::connection makePersistent [expr {$version > 1.0}] [self]::connection event readable [self] header } else { set version 1.0 set resourceName ??? set method ??? my log Error "bad first line:$firstLine" my replyCode 400 my replyErrorMsg } } elseif {![[self]::connection eof]} { #my showMsg "+++ not completed EOF=[[self]::connection eof]" } else { set version 1.0 #my showMsg "+++ n=negative ($n) EOF=[[self]::connection eof] version set to 1.0" my close } } Httpd::Wrk instproc header {} { # Read the header #my showCall my instvar method data if {[[self]::connection gets line] > 0} { #puts stderr line=$line if {[regexp -nocase {^([^:]+): *(.+)$} $line _ key value]} { my set meta([string tolower $key]) $value } } else { #puts stderr line-EMPTY if {[my exists meta(content-length)] && [my set meta(content-length)]>0} { #puts stderr "we have content-length [my set meta(content-length)]" set data "" [self]::connection translation binary [self]::connection event readable [self] receive-body } elseif {[my exists meta(content-type)] && [regexp -nocase {multipart/form-data; *boundary=} \ [my set meta(content-type)]]} { #puts stderr "formdata" set data "" [self]::connection event readable [self] receive-body } else { #puts stderr "no-content-length, triggering respond" [self]::connection event readable [self] "" [my info parent] instvar requiresBody if {$requiresBody($method)} { my replyCode 411 my replyErrorMsg } else { my check-redirect } } } } Httpd::Wrk instproc receive-body {} { ;# ... now we have to read the body #my showCall my instvar method data meta set d [[self]::connection read] if {$d ne ""} { append data $d #my showMsg "datal=[string length $data], cl=$meta(content-length)" if {[string length $data] >= $meta(content-length)} { [self]::connection event readable [self] "" if {$method eq "POST"} { my decode-POST-query } my check-redirect } } else { ;# 0 byte, must be eof... my showMsg "received 0 bytes" [self]::connection event readable [self] "" if {[string length $data] < $meta(content-length)} { my replyCode 404 my replyErrorMsg } else { my check-redirect } } } Httpd::Wrk instproc unmodified mtime { my instvar meta if {[info exists meta(if-modified-since)]} { set ms $meta(if-modified-since) regexp {^([^;]+);(.*)$} $ms _ ms options if {[catch {set mss [clock scan $ms]}]} { regsub -all -- {-} $ms " " ms if {[catch {set mss [clock scan $ms]}]} { set ms [lreplace $ms end end] set mss [clock scan $ms] } } return [expr {$mtime <= $mss}] } return 0 } Httpd::Wrk instproc check-redirect {} { [my info parent] instvar redirects my instvar resourceName hasFormData query set resource $resourceName$hasFormData$query foreach {pattern hostport} $redirects { #puts stderr "match <$pattern> <$resource> [regexp $pattern $resource]" if {[regexp $pattern $resource]} { #puts stderr "do redirect to $hostport/$resource" my replyCode 302 location $hostport/$resource my replyErrorMsg return } } my respond } Httpd::Wrk instproc respond {} { # Respond to the query # the request was read completely... This method is wellsuited for mixins! my respond-[my set method] } Httpd::Wrk instproc respond-GET {} { #my showCall my instvar fileName my sendFile $fileName } Httpd::Wrk instproc respond-HEAD {} { # Respond to the query my instvar fileName if {[file readable $fileName]} { my replyCode 200 \ Last-Modified [Httpd Date [file mtime $fileName]] \ Content-Type [Mime guessContentType $fileName] \ Content-Length [file size $fileName] [self]::connection puts "" #my log Done "$fileName [Mime guessContentType $fileName]" my close } else { my replyCode 404 my replyErrorMsg } } Httpd::Wrk instproc respond-OPTIONS {} { # Respond to the query my replyCode 200 \ Allow "OPTIONS, GET, HEAD, POST" \ Public "OPTIONS, GET, HEAD, POST" [self]::connection puts "" my close } Httpd::Wrk instproc respond-PUT {} { my instvar data method fileName my replyCode [expr {[file writable $fileName] ? 200 : 201}] [self]::connection puts "" set out [open $fileName w] fconfigure $out -translation binary puts -nonewline $out $data my log Done "$fileName [Mime guessContentType $fileName]" close $out my close } Httpd::Wrk instproc respond-CGI {} { my instvar fileName if {[file executable $fileName]} { my replyCode 200 [self]::connection puts [exec $fileName] ;# no parameter handling yet my close } else { my replyCode 403 my replyErrorMsg } } Httpd::Wrk instproc new-formData {} { set arg [Object create [self]::[my autoname formData]] my lappend formData $arg return $arg } Httpd::Wrk instproc decode-formData {query} { #my showCall foreach pair [split [string trimleft $query \n] &] { set arg [my new-formData] if {[regexp {^(.+)=(.*)$} $pair _ name content]} { $arg set name [url decodeItem $name] $arg set content [url decodeItem $content] } else { $arg set content [url decodeItem $pair] } } } Httpd::Wrk instproc decode-POST-query {} { if {[my exists meta(content-type)]} { set ct [my set meta(content-type)] if {[regexp -nocase {application/x-www-form-urlencoded} $ct]} { #my showMsg "ordinary FORM" my decode-formData [my set data] return } elseif {[regexp -nocase {multipart/form-data; *boundary=(.*)$} $ct \ _ boundary]} { #my showMsg "multipart FORM" set parts [my set data] set bl [expr {[string length $boundary]+2}] while {[set endIDX [string first --$boundary $parts]] > -1} { set part [string range $parts $bl [expr {$endIDX-1}]] if {[set endHD [string first \r\n\r\n $part]] > -1} { set arg [my new-formData] if {[catch {Mime multipart-decode-header \ [string range $part 0 [expr {$endHD-1}]] \ $arg} msg]} { my replyCode 406 my replyErrorMsg $msg return 0 } $arg set content [string range $part \ [expr {$endHD + 4}] \ [expr {[string length $part] -3}]] #$arg showVars } set parts [string range $parts [expr {$endIDX+2}] end] } } } } Httpd::Wrk instproc respond-POST {} { my replyCode 405 my replyErrorMsg #my respond-CGI } Httpd::Wrk instproc replyErrorMsg {{msg ""} args} { my instvar replyCode [self class] instvar codes foreach {tag value} $args {[self]::connection puts "$tag: $value"} my sendText "\nStatus Code: $replyCode\n\ $msg

\n\ Status Code $replyCode: $codes($replyCode)
\n\ Resource Name: [my set resourceName]\n" my close ;# close must be last call } Httpd::Wrk instproc replyCode {code args} { #my showCall my instvar version [self class] instvar codes my set replyCode $code [self]::connection puts "HTTP/$version $code $codes($code)" foreach {tag value} [my set replyHeaderFields] {[self]::connection puts "$tag: $value"} foreach {tag value} $args {[self]::connection puts "$tag: $value"} if {$code >= 400} { my log Error "$code $codes($code)\tmeta: [my array get meta]" } else { my log Done "$code $codes($code)" } } Httpd::Wrk instproc sendText {response {type text/html}} { #my showCall [self]::connection puts "Content-Type: $type" # bei einer leeren Responses blockieren Klienten und melden Fehler if {$response eq ""} { set response " " } [self]::connection puts "Content-Length: [string length $response]\n" if {[my set method] ne "HEAD"} { [self]::connection fconfigure -translation {auto binary} [self]::connection puts-nonewline $response } else { my showMsg HEAD! } } Httpd::Wrk instproc sendMsg {response {type text/html}} { # my showCall my replyCode 200 my sendText $response $type my close } Httpd::Wrk instproc sendDir {dirName} { [my info parent] instvar root set title "Directory listing" set reply "$title

$title

\n\n" set oldpwd [pwd] cd $root set dirs ""; set files "" foreach f [lsort -dictionary [glob -nocomplain ./$dirName/*]] { set full [file join $root $f] set pname [string trimleft $f ./] if {[file isdir $full]} { append pname / } if {![catch {set size [file size $full]}]} { # it is not a broken link set entry "" append entry \ \ " \ " \ \n if {[string match */ $pname]} {append dirs $entry} else {append files $entry} } } append reply $dirs $files "
"$pname" " $size " [clock format [file mtime $full]]
\n" cd $oldpwd my sendMsg $reply return } Httpd::Wrk instproc sendFile {fn {type ""}} { #my showCall if {[file isdirectory $fn]} { set full [file join $fn index.html] if {[file readable $full]} { set fn $full } else { my sendDir [my set resourceName] return } } #puts stderr "readable '$fn' [file readable $fn]" if {[file readable $fn]} { set mtime [file mtime $fn] if {[my unmodified $mtime]} { my replyCode 304 my replyErrorMsg return } if {$type eq ""} {set type [Mime guessContentType $fn]} my replyCode 200 \ Last-Modified [Httpd Date $mtime] \ Content-Type $type \ Content-Length [file size $fn] [self]::connection puts "" [self]::connection fconfigure -translation binary ;#-buffersize 65536 set localFile [open $fn] fconfigure $localFile -translation binary -buffersize 65536 fcopy $localFile [[self]::connection set socket] \ -command [list [self] fcopy-end $localFile] } else { my replyCode 404 my replyErrorMsg } } Httpd::Wrk instproc fcopy-end {localFile args} { # End of fcopy close $localFile [self]::connection fconfigure -blocking false ;# fconfigure changes blocking in 8.3.2! my close } Httpd::Wrk instproc log {reason arg} { # trivial logging my instvar port ipaddr if {[my exists user]} { set user [my set user]/[my set realm] } {set user -} [my info parent] instvar logfile puts $logfile "[clock format [clock seconds]] $user $ipaddr:$port\t$reason\t$arg" flush $logfile } ######################################################################### Class Httpsd -superclass Httpd -parameter { {port 443} {httpdWrk Httpsd::Wrk} {requestCert 0} {requireValidCert 0} {certfile filename.crt} {keyfile filename.key} {cafile cacert.pem} {infoCb {}} } Httpsd instproc init args { package require tls proc tls::password {} { puts stderr "getting passwd" return pemp } next } Class Httpsd::Wrk -superclass Httpd::Wrk Httpsd::Wrk instproc firstLine {} { my set forceVersion1.0 1 my lappend replyHeaderFields Connection close next } Httpsd::Wrk instproc makeConnection {socket} { Connection create [self]::connection -socket $socket -req [self] [my info parent] instvar \ keyfile certfile cafile infoCb requestCert requireValidCert # SSL-enable a regular Tcl channel - it need not be a socket, but # must provide bi-directional flow. Also setting session parameters # for SSL handshake. www.sensus.org/tcl/tls.htm # -request bool --> Request a certificate from peer during SSL # handshake. (default: true) # -require bool --> Require a valid certificate from peer during SSL # handshake. If this is set to true then -request must also be set # to true. (default: false) # -server bool --> Handshake as server if true, else handshake as # client.(default: false) [self]::connection importSSL -server 1 \ -certfile $certfile \ -keyfile $keyfile \ -cafile $cafile \ -request $requestCert \ -require $requireValidCert \ -command $infoCb } ######################################################################### ### ### Mixin-Classes for respond patterns ### mixes into Http and Httpd::Wrk ### Class Httpd::Responder Httpd::Responder instproc init args { next my lappend workerMixins Httpd::Responder::Wrk my set respondpatterns {} # Example how to register new methods: regexp is matched with the triple # (HTTP-METHOD URL HASFORMDATA) where HASFORMDATA is empty when no # parameters are given. The parsed components of the url etc. are # available as instvars my actions {^GET cgi[-]bin [?]} respond-CGI } Httpd::Responder instproc actions {regexp method} { my lappend respondpatterns $regexp $method } Class Httpd::Responder::Wrk Httpd::Responder::Wrk instproc respond {} { my instvar fileName method resourceName hasFormData [my info parent] instvar respondpatterns ### auch das ist ein kandidat fuer eine chain of responsibility foreach {pattern action} $respondpatterns { if {[regexp $pattern "$method $resourceName $hasFormData"]} { my $action return } } next } ### ### Mixin-Classes for Access Control ### mixes into Http and Httpd::Wrk ### Class Httpd::AccessControl Httpd::AccessControl abstract instproc protectedResource {fn method varAuthMethod varRealm} Httpd::AccessControl abstract instproc credentialsNotOk {wrk credentials authMethod realm} Httpd::AccessControl abstract instproc addRealmFile {realm authFile} Httpd::AccessControl abstract instproc addRealmEntry {realm passwds} Httpd::AccessControl abstract instproc protectDir {realm path methods} Class Httpd::AccessControl::Wrk Httpd::AccessControl::Wrk instproc respond {} { my instvar fileName method digestChallengeData set controller [my info parent] if {[$controller protectedResource $fileName $method authMethod realm]} { #my showMsg "*** Protected resource: $fileName $method" if {![my exists meta(authorization)] || [$controller credentialsNotOk [self] \ [my set meta(authorization)] $authMethod $realm]} { my unauthorizedAccess $realm return } } next } ########################################################################### ## Basic Access Control ########################################################################### Class Httpd::BasicAccessControl -superclass Httpd::AccessControl Httpd::BasicAccessControl instproc initWorkerMixins {} { my lappend workerMixins [self class]::Wrk } Httpd::BasicAccessControl instproc init args { next my initWorkerMixins } Httpd::BasicAccessControl instproc protectedResource {fn method varAuthMethod varRealm} { #my showCall # check whether access to $fn via $method is protected upvar [self callinglevel] $varAuthMethod authMethod $varRealm realm # we check only the current directory, not the parent directories if {[string match */ $fn]} { set path $fn } else { set path [file dirname $fn]/ } foreach i [list $path $path:$method] { if {[my exists protected($i)]} { set realm [my set protected($i)] set authMethod Basic return 1 } } return 0 } Httpd::BasicAccessControl instproc credentialsNotOk {wrk credentials authMethod realm} { # check whether $credentials are sufficient for $realm regexp {^(.*):(.*)$} [base64 decode [lindex $credentials 1]] _ user pwd #puts stderr "passwd($realm:$user)=[my exists passwd($realm:$user)]" $wrk set user $user $wrk set realm $realm if {[my exists passwd($realm:$user)]} { return [expr {[my set passwd($realm:$user)] != $pwd}] } return 1 } Httpd::BasicAccessControl instproc addRealmEntry {realm passwds} { if {[llength $passwds] == 1} { my addRealmFile [lindex $passwds 0] } else { foreach {name pwd} $passwds { #puts stderr "realm='$realm' adding user: $name pw: $pwd" my set passwd($realm:$name) $pwd } } } Httpd::BasicAccessControl instproc addRealmFile {realm authFile} { set FILE [open $authFile r] while {![eof $FILE]} { foreach {name pwd} [split [gets $FILE] :] { my addRealmEntry $realm [list $name $pwd] } } close $FILE } Httpd::BasicAccessControl instproc protectDir {realm path methods} { my instvar root my checkRoot set resource $root/$path ;# resources are currently directories if {$methods == {}} { my set protected($resource) $realm ;#for every method } else { foreach m $methods { my set protected($resource:$m) $realm ;#for selected methods } } } Class Httpd::BasicAccessControl::Wrk -superclass Httpd::AccessControl::Wrk Httpd::BasicAccessControl::Wrk instproc unauthorizedAccess {realm} { my set digestChallengeData(realm) $realm my replyCode 401 www-authenticate "Basic realm=\"$realm\"" my replyErrorMsg "Unauthorized request for realm '$realm'" } ########################################################################### ## Digest Access Control ########################################################################### Class Httpd::DigestAccessControl -superclass Httpd::BasicAccessControl Httpd::DigestAccessControl instproc init args { package require tcu next my lappend workerMixins [self class]::Wrk } Httpd::DigestAccessControl instproc credentialsNotOk {wrk credentials authMethod realm} { # check whether $credentials are sufficient for $realm my showMsg "Digest Authentication ..." # HELP FD: hier muss ich noch überprüfen, ob die digest-header # (credentials) ok sind. Hier habe ich probleme auf die sachen, # die der worker gesendet (bspw. nonce) hat zu kommen. Ich # weiß, man kann mit [my info children] daran kommen. Aber, # was ist, wenn man mehrere Worker hat? ## Fredj, das sollte kein Problem sein: das credentialsNotOk wird ## vom aktuellen worker (respond) aufgerufen. man kann dem *NotOk ## den worker mitgeben, oder die beiden Methoden etwas umorganisieren. return } Class Httpd::DigestAccessControl::Wrk -superclass Httpd::BasicAccessControl::Wrk Httpd::DigestAccessControl::Wrk instproc unauthorizedAccess {realm} { my set digestChallengeData(realm) $realm my replyCode 401 www-authenticate "Digest [my digestChallenge]" my replyErrorMsg "Unauthorized request for realm '$realm'" } Httpd::DigestAccessControl::Wrk instproc digestChallenge {} { my showCall my instvar digestChallengeData my mkDigestChallengeData set digestResponse {} foreach {t v} [array get digestChallengeData] { append digestResponse "$t = \"$v\", " } regsub {, $} $digestResponse {} digestResponse return $digestResponse } Httpd::DigestAccessControl::Wrk instproc mkDigestChallengeData {} { my showCall my instvar digestChallengeData # RFC 2617 # challenge = "Digest" digest-challenge # digest-challenge = 1#( realm | [ domain ] | nonce | # [ opaque ] |[ stale ] | [ algorithm ] | # [ qop-options ] | [auth-param] ) # domain = "domain" "=" <"> URI ( 1*SP URI ) <"> # URI = absoluteURI | abs_path # nonce = "nonce" "=" nonce-value # nonce-value = quoted-string # opaque = "opaque" "=" quoted-string # stale = "stale" "=" ( "true" | "false" ) # algorithm = "algorithm" "=" ( "MD5" | "MD5-sess" | token ) # qop-options = "qop" "=" <"> 1#qop-value <"> # qop-value = "auth" | "auth-int" | token # FD: hier würde man die nötigen parametern (nonce,domain,opaque, # etc.) berechnen und in dem asso. Array speichern. # FD: minimale Anforderung set digestChallengeData(nonce) [my genNonce] set digestChallengeData(opaque) [base64 encode [self]:my-self-spcified-string] set digestChallengeData(algorithm) "MD5" ;#default set digestChallengeData(qop) "auth" set digestChallengeData(domain) [array names [my info parent]::protected] } Httpd::DigestAccessControl::Wrk instproc genNonce {} { my showCall my instvar digestChallengeData set timeStamp [clock seconds] set nonce [base64 encode [md5 $timeStamp:[self]]] return $nonce } # # example usage: #Httpd h1 -port 8081 -root [glob ~/wafe] #Httpd h2 -port 9086 -root $root \ -mixin {Httpd::Responder Httdp::BasicAccessControl} \ -addRealmEntry test {test test} -protectDir test "" {} \ -redirect {^(mailman|pipermail|cgi-bin) http://alice.wu-wien.ac.at:80} namespace export Httpd Httpsd namespace eval Httpd { namespace export Wrk \ AccessControl BasicAccessControl DigestAccessControl \ Responder } namespace eval Httpsd { namespace export Wrk } #namespace eval Responder {namespace export Wrk} #namespace eval AccessControl {namespace export Wrk} #namespace eval BasicAccessControl {namespace export Wrk} #namespace eval DigestAccessControl {namespace export Wrk} } namespace import ::xotcl::comm::httpd::* namespace eval Httpd {namespace import ::xotcl::comm::httpd::Httpd::*} namespace eval Httpsd {namespace import ::xotcl::comm::httpd::Httpsd::*} #namespace eval Responder {namespace import ::xotcl::comm::httpd::Responder::*} #namespace eval AccessControl {namespace import ::xotcl::comm::httpd::AccessControl::*} #namespace eval BasicAccessControl {namespace import ::xotcl::comm::httpd::BasicAccessControl::*} #namespace eval DigestAccessControl {namespace import ::xotcl::comm::httpd::DigestAccessControl::*}