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