Richard Suchenwirth 2006-09-17 - CGI is a mighty complex protocol for web servers to provide active content (i.e. the results of a script). The following is just a poor subset of CGI, but then again, it's not much code either. And testing it was great fun :^)
I started from DustMote, a web server for static content in 41 lines of code, and added the following features:
Here's my extended version of DustMote, still short at 42 lines of code:
# DustMotePlus - with a subset of CGI support set root c:/html set default index.htm set port 80 set encoding iso8859-1 proc bgerror msg {puts stdout "bgerror: $msg\n$::errorInfo"} proc answer {socketChannel host2 port2} { fileevent $socketChannel readable [list serve $socketChannel] } proc serve sock { fconfigure $sock -blocking 0 gets $sock line if {[fblocked $sock]} return fileevent $sock readable "" set tail / regexp {(/[^ ?]*)(\?[^ ]*)?} $line -> tail args if {[string match */ $tail]} {append tail $::default} set name [string map {%20 " "} $::root$tail] if {[file readable $name]} { puts $sock "HTTP/1.0 200 OK" if {[file extension $name] eq ".tcl"} { set ::env(QUERY_STRING) [string range $args 1 end] set name [list |tclsh $name] } else { puts $sock "Content-Type: text/html;charset=$::encoding\n" } set inchan [open $name] fconfigure $inchan -translation binary fconfigure $sock -translation binary fcopy $inchan $sock -command [list done $inchan $sock] } else { puts $sock "HTTP/1.0 404 Not found\n" close $sock } } proc done {file sock bytes {msg {}}} { close $file close $sock } socket -server answer $port puts "Server ready..." vwait forever
And here's a little "CGI" script I tested it with (save as time.tcl):
# time.tcl - tiny CGI script. if ![info exists env(QUERY_STRING)] {set env(QUERY_STRING) " "} puts "Content-type: text/html\n" puts "<html><head><title>Tiny CGI time server</title></head> <body><h1>Time server</h1> Time now is: [clock format [clock seconds]]<br> Query was: $env(QUERY_STRING) <hr> <a href=index.htm>Index</a> </body></html>"
MS prefers to use fcopy (so that the output proceeds in the background and the server is responsive to new queries while the cgi script is running). - RS: thanks - integrated in the above code :^)
DDG I prefere to use namespaces and I was adding some more MimeTypes because I like and need CSS and JavaScript.
# DustMotePlusPlus - now about 120 lines of code # with a subset of CGI support # namespace dmhttpd and more MimeTypes # I borrowed some code from jcw (from the nice wiki webserver) # and implemented url2procedure mappings as well # try something like /monitor?arg1=1&arg2=2 # ddg namespace eval dmhttpd { variable root /home/dgroth/html variable default index.htm variable port 8088 variable encoding iso8859-1 variable MimeTypes array set MimeTypes { {} "text/plain" .txt "text/plain" .css "text/css" .js "text/javascript" .htm "text/html;charset=$encoding" .html "text/html;charset=$encoding" .tml "text/html;charset=$encoding" .gif "image/gif" .jpg "image/jpeg" .ico "image/ico" .png "image/png" } } proc dmhttpd::answer {socketChannel host2 port2} { fileevent $socketChannel readable [list ::[namespace current]::serve $socketChannel] } proc dmhttpd::serve {sock} { variable root variable MimeTypes variable default fconfigure $sock -blocking 0 gets $sock line if {[fblocked $sock]} return fileevent $sock readable "" set tail / regexp {(/[^ ?]*)(\?[^ ]*)?} $line -> tail args if {[string match */ $tail]} {append tail $default} set name [string map {%20 " "} $root$tail] if {[info proc $tail] eq "$tail" && $tail ne ""} { # Service URL-procedure if {[catch {set res [eval {$tail} [QueryMap [string range $args 1 end]]]} err]} { puts [format "Error: %s: %s" $tail $err] } else { # Emit headers # puts $sock "HTTP/1.0 200 OK" puts $sock "Content-Type: text/html\n" puts $sock $res close $sock #cleanupChannel $channel } } elseif {[file readable $name]} { puts $sock "HTTP/1.0 200 OK" if {[file extension $name] eq ".tcl"} { set ::env(QUERY_STRING) [string range $args 1 end] set name [list |tclsh $name] } else { set ext [string tolower [file extension $name]] puts $sock "Content-Type: $MimeTypes($ext)\n" } set inchan [open $name] fconfigure $inchan -translation binary fconfigure $sock -translation binary fcopy $inchan $sock -command [list ::[namespace current]::done $inchan $sock] } else { puts $sock "HTTP/1.0 404 Not found\n" close $sock } } proc dmhttpd::done {file sock bytes {msg {}}} { close $file close $sock } proc dmhttpd::/monitor {args} { # Emit body # append res [subst { <html> <body> <h3>[clock format [clock seconds]]</h3> }] after 1 ; # Simulate blocking call append res "Args: $args [llength $args]" append res { </body> </html> } return $res } proc dmhttpd::CgiMap {data} { # jcw wiki webserver # @c Decode url-encoded strings regsub -all {\+} $data { } data regsub -all {([][$\\])} $data {\\\1} data regsub -all {%([0-9a-fA-F][0-9a-fA-F])} $data {[format %c 0x\1]} data return [subst $data] } proc dmhttpd::QueryMap {query} { # jcw wiki webserver # @c Decode url-encoded query into key/value pairs set res [list] regsub -all {[&=]} $query { } query regsub -all { } $query { {} } query; # Othewise we lose empty values foreach {key val} $query { lappend res [CgiMap $key] [CgiMap $val] } return $res } if {[info script] eq $argv0} { proc bgerror msg {puts stdout "bgerror: $msg\n$::errorInfo"} if {[llength $argv] == 1} { set port [lindex $argv 0] } else { set port 8088 } socket -server ::dmhttpd::answer $port puts "Server ready on port $port ..." vwait forever }
2011-12-18 14:38:51 Lectus: This is a very interesting piece of code! It actually makes it easy to create simple web apps or add a web interface to an existing code. I tested it and here is my result:
index.htm (HTML code to show a form where the user can input data):
<FORM action="http://127.0.0.1:8088/name.tcl" method="get"> <P> Type your name: <INPUT type="text" name="firstname"><BR> <INPUT type="submit" value="Send"> <INPUT type="reset"> </P> </FORM>
I tested it on localhost, so I used 127.0.0.1.
name.tcl (receives the query string and do whatever with the data):
if ![info exists env(QUERY_STRING)] {set env(QUERY_STRING) " "} puts "Content-type: text/html\n" puts "<html><head><title>Tiny CGI server</title></head>" regexp {firstname=(.+)} $env(QUERY_STRING) -> name puts "<body><h1>Hello $name!</h1> <br> <a href=index.htm>Index</a> </body></html>"
This demonstrates how to pass and process query strings. I can already see how useful is this. For example, one could pass the query string, process it and store data in a sqlite database.