1# 2# uhttpd.tcl -- 3# 4# Simple Sample httpd/1.0 server in 250 lines of Tcl. 5# Stephen Uhler / Brent Welch (c) 1996 Sun Microsystems. 6# 7# Modified to use namespaces and direct url-to-procedure access (zv). 8# Eh, due to this, and nicer indenting, it's now 150 lines longer :-) 9# 10# Usage: 11# phttpd::create port 12# 13# port Tcp port where the server listens 14# 15# Example: 16# 17# # tclsh8.4 18# % source uhttpd.tcl 19# % uhttpd::create 5000 20# % vwait forever 21# 22# Starts the server on the port 5000. Also, look at the Httpd array 23# definition in the "uhttpd" namespace declaration to find out 24# about other options you may put on the command line. 25# 26# You can use: http://localhost:5000/monitor URL to test the 27# server functionality. 28# 29# Copyright (c) Stephen Uhler / Brent Welch (c) 1996 Sun Microsystems. 30# Copyright (c) 2002 by Zoran Vasiljevic. 31# 32# See the file "license.terms" for information on usage and 33# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. 34# 35# ----------------------------------------------------------------------------- 36# Rcsid: @(#)$Id: uhttpd.tcl,v 1.3 2002/12/13 20:55:08 vasiljevic Exp $ 37# 38 39namespace eval uhttpd { 40 41 variable Httpd; # Internal server state and config params 42 variable MimeTypes; # Cache of file-extension/mime-type 43 variable HttpCodes; # Portion of well-known http return codes 44 variable ErrorPage; # Format of error response page in html 45 46 array set Httpd { 47 -name uhttpd 48 -vers 1.0 49 -root "" 50 -index index.htm 51 } 52 array set HttpCodes { 53 400 "Bad Request" 54 401 "Not Authorized" 55 404 "Not Found" 56 500 "Server error" 57 } 58 array set MimeTypes { 59 {} "text/plain" 60 .txt "text/plain" 61 .htm "text/html" 62 .htm "text/html" 63 .gif "image/gif" 64 .jpg "image/jpeg" 65 .png "image/png" 66 } 67 set ErrorPage { 68 <title>Error: %1$s %2$s</title> 69 <h1>%3$s</h1> 70 <p>Problem in accessing "%4$s" on this server.</p> 71 <hr> 72 <i>%5$s/%6$s Server at %7$s Port %8$s</i> 73 } 74} 75 76proc uhttpd::create {port args} { 77 78 # @c Start the server by listening for connections on the desired port. 79 80 variable Httpd 81 set arglen [llength $args] 82 83 if {$arglen} { 84 if {$arglen % 2} { 85 error "wrong \# arguments, should be: key1 val1 key2 val2..." 86 } 87 set opts [array names Httpd] 88 foreach {arg val} $args { 89 if {[lsearch $opts $arg] == -1} { 90 error "unknown option \"$arg\"" 91 } 92 set Httpd($arg) $val 93 } 94 } 95 96 set Httpd(port) $port 97 set Httpd(host) [info hostname] 98 99 socket -server [namespace current]::Accept $port 100} 101 102proc uhttpd::respond {s status contype data {length 0}} { 103 104 puts $s "HTTP/1.0 $status" 105 puts $s "Date: [Date]" 106 puts $s "Content-Type: $contype" 107 108 if {$length} { 109 puts $s "Content-Length: $length" 110 } else { 111 puts $s "Content-Length: [string length $data]" 112 } 113 114 puts $s "" 115 puts $s $data 116} 117 118proc uhttpd::Accept {newsock ipaddr port} { 119 120 # @c Accept a new connection from the client. 121 122 variable Httpd 123 upvar \#0 [namespace current]::Httpd$newsock data 124 125 fconfigure $newsock -blocking 0 -translation {auto crlf} 126 127 set data(ipaddr) $ipaddr 128 fileevent $newsock readable [list [namespace current]::Read $newsock] 129} 130 131proc uhttpd::Read {s} { 132 133 # @c Read data from client 134 135 variable Httpd 136 upvar \#0 [namespace current]::Httpd$s data 137 138 if {[catch {gets $s line} readCount] || [eof $s]} { 139 return [Done $s] 140 } 141 if {$readCount == -1} { 142 return ;# Insufficient data on non-blocking socket ! 143 } 144 if {![info exists data(state)]} { 145 set pat {(POST|GET) ([^?]+)\??([^ ]*) HTTP/1\.[0-9]} 146 if {[regexp $pat $line x data(proto) data(url) data(query)]} { 147 return [set data(state) mime] 148 } else { 149 Log error "bad request line: %s" $line 150 Error $s 400 151 return [Done $s] 152 } 153 } 154 155 # string compare $readCount 0 maps -1 to -1, 0 to 0, and > 0 to 1 156 157 set state [string compare $readCount 0],$data(state),$data(proto) 158 switch -- $state { 159 "0,mime,GET" - "0,query,POST" { 160 Respond $s 161 } 162 "0,mime,POST" { 163 set data(state) query 164 set data(query) "" 165 } 166 "1,mime,POST" - "1,mime,GET" { 167 if [regexp {([^:]+):[ ]*(.*)} $line dummy key value] { 168 set data(mime,[string tolower $key]) $value 169 } 170 } 171 "1,query,POST" { 172 append data(query) $line 173 set clen $data(mime,content-length) 174 if {($clen - [string length $data(query)]) <= 0} { 175 Respond $s 176 } 177 } 178 default { 179 if [eof $s] { 180 Log error "unexpected eof; client closed connection" 181 return [Done $s] 182 } else { 183 Log error "bad http protocol state: %s" $state 184 Error $s 400 185 return [Done $s] 186 } 187 } 188 } 189} 190 191proc uhttpd::Done {s} { 192 193 # @c Close the connection socket and discard token 194 195 close $s 196 unset [namespace current]::Httpd$s 197} 198 199proc uhttpd::Respond {s} { 200 201 # @c Respond to the query. 202 203 variable Httpd 204 upvar \#0 [namespace current]::Httpd$s data 205 206 if {[uplevel \#0 info proc $data(url)] == $data(url)} { 207 208 # 209 # Service URL-procedure first 210 # 211 212 if {[catch { 213 puts $s "HTTP/1.0 200 OK" 214 puts $s "Date: [Date]" 215 puts $s "Last-Modified: [Date]" 216 } err]} { 217 Log error "client closed connection prematurely: %s" $err 218 return [Done $s] 219 } 220 set data(sock) $s 221 if {[catch {$data(url) data} err]} { 222 Log error "%s: %s" $data(url) $err 223 } 224 225 } else { 226 227 # 228 # Service regular file path next. 229 # 230 231 set mypath [Url2File $data(url)] 232 if {![catch {open $mypath} i]} { 233 if {[catch { 234 puts $s "HTTP/1.0 200 OK" 235 puts $s "Date: [Date]" 236 puts $s "Last-Modified: [Date [file mtime $mypath]]" 237 puts $s "Content-Type: [ContentType $mypath]" 238 puts $s "Content-Length: [file size $mypath]" 239 puts $s "" 240 fconfigure $s -translation binary -blocking 0 241 fconfigure $i -translation binary 242 fcopy $i $s 243 close $i 244 } err]} { 245 Log error "client closed connection prematurely: %s" $err 246 } 247 } else { 248 Log error "%s: %s" $data(url) $i 249 Error $s 404 250 } 251 } 252 253 Done $s 254} 255 256proc uhttpd::ContentType {path} { 257 258 # @c Convert the file suffix into a mime type. 259 260 variable MimeTypes 261 262 set type "text/plain" 263 catch {set type $MimeTypes([file extension $path])} 264 265 return $type 266} 267 268proc uhttpd::Error {s code} { 269 270 # @c Emit error page. 271 272 variable Httpd 273 variable HttpCodes 274 variable ErrorPage 275 276 upvar \#0 [namespace current]::Httpd$s data 277 278 append data(url) "" 279 set msg \ 280 [format $ErrorPage \ 281 $code \ 282 $HttpCodes($code) \ 283 $HttpCodes($code) \ 284 $data(url) \ 285 $Httpd(-name) \ 286 $Httpd(-vers) \ 287 $Httpd(host) \ 288 $Httpd(port) \ 289 ] 290 if {[catch { 291 puts $s "HTTP/1.0 $code $HttpCodes($code)" 292 puts $s "Date: [Date]" 293 puts $s "Content-Length: [string length $msg]" 294 puts $s "" 295 puts $s $msg 296 } err]} { 297 Log error "client closed connection prematurely: %s" $err 298 } 299} 300 301proc uhttpd::Date {{seconds 0}} { 302 303 # @c Generate a date string in HTTP format. 304 305 if {$seconds == 0} { 306 set seconds [clock seconds] 307 } 308 clock format $seconds -format {%a, %d %b %Y %T %Z} -gmt 1 309} 310 311proc uhttpd::Log {reason format args} { 312 313 # @c Log an httpd transaction. 314 315 set messg [eval format [list $format] $args] 316 set stamp [clock format [clock seconds] -format "%d/%b/%Y:%H:%M:%S"] 317 318 puts stderr "\[$stamp\] $reason: $messg" 319} 320 321proc uhttpd::Url2File {url} { 322 323 # @c Convert a url into a pathname (this is probably not right) 324 325 variable Httpd 326 327 lappend pathlist $Httpd(-root) 328 set level 0 329 330 foreach part [split $url /] { 331 set part [CgiMap $part] 332 if [regexp {[:/]} $part] { 333 return "" 334 } 335 switch -- $part { 336 "." { } 337 ".." {incr level -1} 338 default {incr level} 339 } 340 if {$level <= 0} { 341 return "" 342 } 343 lappend pathlist $part 344 } 345 346 set file [eval file join $pathlist] 347 348 if {[file isdirectory $file]} { 349 return [file join $file $Httpd(-index)] 350 } else { 351 return $file 352 } 353} 354 355proc uhttpd::CgiMap {data} { 356 357 # @c Decode url-encoded strings 358 359 regsub -all {\+} $data { } data 360 regsub -all {([][$\\])} $data {\\\1} data 361 regsub -all {%([0-9a-fA-F][0-9a-fA-F])} $data {[format %c 0x\1]} data 362 363 return [subst $data] 364} 365 366proc uhttpd::QueryMap {query} { 367 368 # @c Decode url-encoded query into key/value pairs 369 370 set res [list] 371 372 regsub -all {[&=]} $query { } query 373 regsub -all { } $query { {} } query; # Othewise we lose empty values 374 375 foreach {key val} $query { 376 lappend res [CgiMap $key] [CgiMap $val] 377 } 378 return $res 379} 380 381proc /monitor {array} { 382 383 upvar $array data ; # Holds the socket to remote client 384 385 # 386 # Emit headers 387 # 388 389 puts $data(sock) "HTTP/1.0 200 OK" 390 puts $data(sock) "Date: [uhttpd::Date]" 391 puts $data(sock) "Content-Type: text/html" 392 puts $data(sock) "" 393 394 # 395 # Emit body 396 # 397 398 puts $data(sock) [subst { 399 <html> 400 <body> 401 <h3>[clock format [clock seconds]]</h3> 402 }] 403 404 after 1 ; # Simulate blocking call 405 406 puts $data(sock) [subst { 407 </body> 408 </html> 409 }] 410} 411 412# EOF $RCSfile: uhttpd.tcl,v $ 413# Emacs Setup Variables 414# Local Variables: 415# mode: Tcl 416# indent-tabs-mode: nil 417# tcl-basic-offset: 4 418# End: 419 420