1# 2# phttpd.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, direct url-to-procedure access 8# and thread pool package. Grown little larger since ;) 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 phttpd.tcl 19# % phttpd::create 5000 20# % vwait forever 21# 22# Starts the server on the port 5000. Also, look at the Httpd array 23# definition in the "phttpd" 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) 2002 by Zoran Vasiljevic. 30# 31# See the file "license.terms" for information on usage and 32# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. 33# 34# ----------------------------------------------------------------------------- 35# Rcsid: @(#)$Id: phttpd.tcl,v 1.5 2002/12/13 20:55:07 vasiljevic Exp $ 36# 37 38package require Tcl 8.4 39package require Thread 2.5 40 41# 42# Modify the following in order to load the 43# example Tcl implementation of threadpools. 44# Per default, the C-level threadpool is used. 45# 46 47if {0} { 48 eval [set TCL_TPOOL {source ../tpool/tpool.tcl}] 49} 50 51namespace eval phttpd { 52 53 variable Httpd; # Internal server state and config params 54 variable MimeTypes; # Cache of file-extension/mime-type 55 variable HttpCodes; # Portion of well-known http return codes 56 variable ErrorPage; # Format of error response page in html 57 58 array set Httpd { 59 -name phttpd 60 -vers 1.0 61 -root "." 62 -index index.htm 63 } 64 array set HttpCodes { 65 400 "Bad Request" 66 401 "Not Authorized" 67 404 "Not Found" 68 500 "Server error" 69 } 70 array set MimeTypes { 71 {} "text/plain" 72 .txt "text/plain" 73 .htm "text/html" 74 .htm "text/html" 75 .gif "image/gif" 76 .jpg "image/jpeg" 77 .png "image/png" 78 } 79 set ErrorPage { 80 <title>Error: %1$s %2$s</title> 81 <h1>%3$s</h1> 82 <p>Problem in accessing "%4$s" on this server.</p> 83 <hr> 84 <i>%5$s/%6$s Server at %7$s Port %8$s</i> 85 } 86} 87 88# 89# phttpd::create -- 90# 91# Start the server by listening for connections on the desired port. 92# 93# Arguments: 94# port 95# args 96# 97# Side Effects: 98# None.. 99# 100# Results: 101# None. 102# 103 104proc phttpd::create {port args} { 105 106 variable Httpd 107 108 set arglen [llength $args] 109 if {$arglen} { 110 if {$arglen % 2} { 111 error "wrong \# args, should be: key1 val1 key2 val2..." 112 } 113 set opts [array names Httpd] 114 foreach {arg val} $args { 115 if {[lsearch $opts $arg] == -1} { 116 error "unknown option \"$arg\"" 117 } 118 set Httpd($arg) $val 119 } 120 } 121 122 # 123 # Create thread pool with max 8 worker threads. 124 # 125 126 if {[info exists ::TCL_TPOOL] == 0} { 127 # 128 # Using the internal C-based thread pool 129 # 130 set initcmd "source ../phttpd/phttpd.tcl" 131 } else { 132 # 133 # Using the Tcl-level hand-crafted thread pool 134 # 135 append initcmd "source ../phttpd/phttpd.tcl" \n $::TCL_TPOOL 136 } 137 138 set Httpd(tpid) [tpool::create -maxworkers 8 -initcmd $initcmd] 139 140 # 141 # Start the server on the given port. Note that we wrap 142 # the actual accept with a helper after/idle callback. 143 # This is a workaround for a well-known Tcl bug. 144 # 145 146 socket -server [namespace current]::_Accept $port 147} 148 149# 150# phttpd::_Accept -- 151# 152# Helper procedure to solve Tcl shared-channel bug when responding 153# to incoming connection and transfering the channel to other thread(s). 154# 155# Arguments: 156# sock incoming socket 157# ipaddr IP address of the remote peer 158# port Tcp port used for this connection 159# 160# Side Effects: 161# None. 162# 163# Results: 164# None. 165# 166 167proc phttpd::_Accept {sock ipaddr port} { 168 after idle [list [namespace current]::Accept $sock $ipaddr $port] 169} 170 171# 172# phttpd::Accept -- 173# 174# Accept a new connection from the client. 175# 176# Arguments: 177# sock 178# ipaddr 179# port 180# 181# Side Effects: 182# None.. 183# 184# Results: 185# None. 186# 187 188proc phttpd::Accept {sock ipaddr port} { 189 190 variable Httpd 191 192 # 193 # Setup the socket for sane operation 194 # 195 196 fconfigure $sock -blocking 0 -translation {auto crlf} 197 198 # 199 # Detach the socket from current interpreter/tnread. 200 # One of the worker threads will attach it again. 201 # 202 203 thread::detach $sock 204 205 # 206 # Send the work ticket to threadpool. 207 # 208 209 tpool::post -detached $Httpd(tpid) [list [namespace current]::Ticket $sock] 210} 211 212# 213# phttpd::Ticket -- 214# 215# Job ticket to run in the thread pool thread. 216# 217# Arguments: 218# sock 219# 220# Side Effects: 221# None.. 222# 223# Results: 224# None. 225# 226 227proc phttpd::Ticket {sock} { 228 229 thread::attach $sock 230 fileevent $sock readable [list [namespace current]::Read $sock] 231 232 # 233 # End of processing is signalized here. 234 # This will release the worker thread. 235 # 236 237 vwait [namespace current]::done 238} 239 240 241# 242# phttpd::Read -- 243# 244# Read data from client and parse incoming http request. 245# 246# Arguments: 247# sock 248# 249# Side Effects: 250# None. 251# 252# Results: 253# None. 254# 255 256proc phttpd::Read {sock} { 257 258 variable Httpd 259 variable data 260 261 set data(sock) $sock 262 263 while {1} { 264 if {[catch {gets $data(sock) line} readCount] || [eof $data(sock)]} { 265 return [Done] 266 } 267 if {![info exists data(state)]} { 268 set pat {(POST|GET) ([^?]+)\??([^ ]*) HTTP/1\.[0-9]} 269 if {[regexp $pat $line x data(proto) data(url) data(query)]} { 270 set data(state) mime 271 continue 272 } else { 273 Log error "bad request line: (%s)" $line 274 Error 400 275 return [Done] 276 } 277 } 278 279 # string compare $readCount 0 maps -1 to -1, 0 to 0, and > 0 to 1 280 281 set state [string compare $readCount 0],$data(state),$data(proto) 282 switch -- $state { 283 "0,mime,GET" - "0,query,POST" { 284 Respond 285 return [Done] 286 } 287 "0,mime,POST" { 288 set data(state) query 289 set data(query) "" 290 } 291 "1,mime,POST" - "1,mime,GET" { 292 if [regexp {([^:]+):[ ]*(.*)} $line dummy key value] { 293 set data(mime,[string tolower $key]) $value 294 } 295 } 296 "1,query,POST" { 297 append data(query) $line 298 set clen $data(mime,content-length) 299 if {($clen - [string length $data(query)]) <= 0} { 300 Respond 301 return [Done] 302 } 303 } 304 default { 305 if [eof $data(sock)] { 306 Log error "unexpected eof; client closed connection" 307 return [Done] 308 } else { 309 Log error "bad http protocol state: %s" $state 310 Error 400 311 return [Done] 312 } 313 } 314 } 315 } 316} 317 318# 319# phttpd::Done -- 320# 321# Close the connection socket 322# 323# Arguments: 324# s 325# 326# Side Effects: 327# None.. 328# 329# Results: 330# None. 331# 332 333proc phttpd::Done {} { 334 335 variable done 336 variable data 337 338 close $data(sock) 339 340 if {[info exists data]} { 341 unset data 342 } 343 344 set done 1 ; # Releases the request thread (See Ticket procedure) 345} 346 347# 348# phttpd::Respond -- 349# 350# Respond to the query. 351# 352# Arguments: 353# s 354# 355# Side Effects: 356# None.. 357# 358# Results: 359# None. 360# 361 362proc phttpd::Respond {} { 363 364 variable data 365 366 if {[info commands $data(url)] == $data(url)} { 367 368 # 369 # Service URL-procedure 370 # 371 372 if {[catch { 373 puts $data(sock) "HTTP/1.0 200 OK" 374 puts $data(sock) "Date: [Date]" 375 puts $data(sock) "Last-Modified: [Date]" 376 } err]} { 377 Log error "client closed connection prematurely: %s" $err 378 return 379 } 380 if {[catch {$data(url) data} err]} { 381 Log error "%s: %s" $data(url) $err 382 } 383 384 } else { 385 386 # 387 # Service regular file path 388 # 389 390 set mypath [Url2File $data(url)] 391 if {![catch {open $mypath} i]} { 392 if {[catch { 393 puts $data(sock) "HTTP/1.0 200 OK" 394 puts $data(sock) "Date: [Date]" 395 puts $data(sock) "Last-Modified: [Date [file mtime $mypath]]" 396 puts $data(sock) "Content-Type: [ContentType $mypath]" 397 puts $data(sock) "Content-Length: [file size $mypath]" 398 puts $data(sock) "" 399 fconfigure $data(sock) -translation binary -blocking 0 400 fconfigure $i -translation binary 401 fcopy $i $data(sock) 402 close $i 403 } err]} { 404 Log error "client closed connection prematurely: %s" $err 405 } 406 } else { 407 Log error "%s: %s" $data(url) $i 408 Error 404 409 } 410 } 411} 412 413# 414# phttpd::ContentType -- 415# 416# Convert the file suffix into a mime type. 417# 418# Arguments: 419# path 420# 421# Side Effects: 422# None.. 423# 424# Results: 425# None. 426# 427 428proc phttpd::ContentType {path} { 429 430 # @c Convert the file suffix into a mime type. 431 432 variable MimeTypes 433 434 set type "text/plain" 435 catch {set type $MimeTypes([file extension $path])} 436 437 return $type 438} 439 440# 441# phttpd::Error -- 442# 443# Emit error page 444# 445# Arguments: 446# s 447# code 448# 449# Side Effects: 450# None.. 451# 452# Results: 453# None. 454# 455 456proc phttpd::Error {code} { 457 458 variable Httpd 459 variable HttpCodes 460 variable ErrorPage 461 variable data 462 463 append data(url) "" 464 set msg \ 465 [format $ErrorPage \ 466 $code \ 467 $HttpCodes($code) \ 468 $HttpCodes($code) \ 469 $data(url) \ 470 $Httpd(-name) \ 471 $Httpd(-vers) \ 472 [info hostname] \ 473 80 \ 474 ] 475 if {[catch { 476 puts $data(sock) "HTTP/1.0 $code $HttpCodes($code)" 477 puts $data(sock) "Date: [Date]" 478 puts $data(sock) "Content-Length: [string length $msg]" 479 puts $data(sock) "" 480 puts $data(sock) $msg 481 } err]} { 482 Log error "client closed connection prematurely: %s" $err 483 } 484} 485 486# 487# phttpd::Date -- 488# 489# Generate a date string in HTTP format. 490# 491# Arguments: 492# seconds 493# 494# Side Effects: 495# None.. 496# 497# Results: 498# None. 499# 500 501proc phttpd::Date {{seconds 0}} { 502 503 # @c Generate a date string in HTTP format. 504 505 if {$seconds == 0} { 506 set seconds [clock seconds] 507 } 508 clock format $seconds -format {%a, %d %b %Y %T %Z} -gmt 1 509} 510 511# 512# phttpd::Log -- 513# 514# Log an httpd transaction. 515# 516# Arguments: 517# reason 518# format 519# args 520# 521# Side Effects: 522# None.. 523# 524# Results: 525# None. 526# 527 528proc phttpd::Log {reason format args} { 529 530 set messg [eval format [list $format] $args] 531 set stamp [clock format [clock seconds] -format "%d/%b/%Y:%H:%M:%S"] 532 533 puts stderr "\[$stamp\]\[-thread[thread::id]-\] $reason: $messg" 534} 535 536# 537# phttpd::Url2File -- 538# 539# Convert a url into a pathname. 540# 541# Arguments: 542# url 543# 544# Side Effects: 545# None.. 546# 547# Results: 548# None. 549# 550 551proc phttpd::Url2File {url} { 552 553 variable Httpd 554 555 lappend pathlist $Httpd(-root) 556 set level 0 557 558 foreach part [split $url /] { 559 set part [CgiMap $part] 560 if [regexp {[:/]} $part] { 561 return "" 562 } 563 switch -- $part { 564 "." { } 565 ".." {incr level -1} 566 default {incr level} 567 } 568 if {$level <= 0} { 569 return "" 570 } 571 lappend pathlist $part 572 } 573 574 set file [eval file join $pathlist] 575 576 if {[file isdirectory $file]} { 577 return [file join $file $Httpd(-index)] 578 } else { 579 return $file 580 } 581} 582 583# 584# phttpd::CgiMap -- 585# 586# Decode url-encoded strings. 587# 588# Arguments: 589# data 590# 591# Side Effects: 592# None.. 593# 594# Results: 595# None. 596# 597 598proc phttpd::CgiMap {data} { 599 600 regsub -all {\+} $data { } data 601 regsub -all {([][$\\])} $data {\\\1} data 602 regsub -all {%([0-9a-fA-F][0-9a-fA-F])} $data {[format %c 0x\1]} data 603 604 return [subst $data] 605} 606 607# 608# phttpd::QueryMap -- 609# 610# Decode url-encoded query into key/value pairs. 611# 612# Arguments: 613# query 614# 615# Side Effects: 616# None.. 617# 618# Results: 619# None. 620# 621 622proc phttpd::QueryMap {query} { 623 624 set res [list] 625 626 regsub -all {[&=]} $query { } query 627 regsub -all { } $query { {} } query; # Othewise we lose empty values 628 629 foreach {key val} $query { 630 lappend res [CgiMap $key] [CgiMap $val] 631 } 632 return $res 633} 634 635# 636# monitor -- 637# 638# Procedure used to test the phttpd server. It responds on the 639# http://<hostname>:<port>/monitor 640# 641# Arguments: 642# array 643# 644# Side Effects: 645# None.. 646# 647# Results: 648# None. 649# 650 651proc /monitor {array} { 652 653 upvar $array data ; # Holds the socket to remote client 654 655 # 656 # Emit headers 657 # 658 659 puts $data(sock) "HTTP/1.0 200 OK" 660 puts $data(sock) "Date: [phttpd::Date]" 661 puts $data(sock) "Content-Type: text/html" 662 puts $data(sock) "" 663 664 # 665 # Emit body 666 # 667 668 puts $data(sock) [subst { 669 <html> 670 <body> 671 <h3>[clock format [clock seconds]]</h3> 672 }] 673 674 after 1 ; # Simulate blocking call 675 676 puts $data(sock) [subst { 677 </body> 678 </html> 679 }] 680} 681 682# EOF $RCSfile: phttpd.tcl,v $ 683# Emacs Setup Variables 684# Local Variables: 685# mode: Tcl 686# indent-tabs-mode: nil 687# tcl-basic-offset: 4 688# End: 689 690