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