#  tinyhttpd.tcl --
#  
#       This file is part of The Coccinella application. It implements a tiny
#       http server.
#      
#  Copyright (c) 2002-2003  Mats Bengtsson
#  This source file is distributed under the BSD licens.
#  
#  See the README file for license, bugs etc.
#  
# $Id: tinyhttpd.tcl,v 1.16 2004/09/13 09:05:18 matben Exp $

# ########################### USAGE ############################################
#
#   NAME
#      tinyhttpd - a tiny http server
#      
#   COMMANDS
#      ::tinyhttpd::start ?-option value ...?
#      ::tinyhttpd::stop
#      ::tinyhttpd::addmimemappings mimeList
#      ::tinyhttpd::setmimemappings mimeList
#      ::tinyhttpd::anyactive
#      ::tinyhttpd::bytestransported
#      ::tinyhttpd::avergaebytespersec
#      ::tinyhttpd::cleanup
#      
# ##############################################################################

package require Tcl 8.4
package require uriencode

package provide tinyhttpd 1.0

namespace eval ::tinyhttpd:: {
        
    # Keep track of useful things.
    variable gstate
    variable httpMsg
    variable html
    variable http
    variable opts
    variable this
    variable timing
    variable uid 0
        
    switch -- $::tcl_platform(platform) {
	macintosh {
	    set this(sep) :
	}
	windows {
	    set this(sep) {\\}
	}
	unix {
	    set this(sep) /
	}
	default {
	    set this(sep) /
	}
    }
    set this(path) [file dirname [info script]]
    set this(httpvers) 1.0

    set gstate(debug) 0
    
    array set httpMsg {
      200 OK
      404 "File not found on server."
    }

    # Use variables to store typical html return messages instead of files.
    set html(404) {<HTML><HEAD>
	<TITLE>File Not Found</TITLE>
	</HEAD><BODY BGCOLOR="#FFA6FF" TEXT=black>
	<FONT SIZE="5" COLOR="#CC0033" FACE="Arial,Helvetica,Verdana,sans-serif">
	<B> Error 404: The file was not found on the server. </B></FONT><P>
	<FONT SIZE="2" FACE="Arial,Helvetica,Verdana,sans-serif">
	But you can find shiny almost brand new cars at honest Mats
	used cars sales. 
	</FONT>
	</BODY></HTML>
    }
    
    # Some standard responses. Use with 'format'. Careful with spaces!
    set http(headdirlist) \
"HTTP/$this(httpvers) 200 OK
Server: tinyhttpd/1.0
Last-Modified: %s
Content-Type: text/html"

    set http(404) \
"HTTP/$this(httpvers) 404 $httpMsg(404)
Content-Type: text/html\n"

    set http(404GET) \
"HTTP/$this(httpvers) 404 $httpMsg(404)
Content-Type: text/html
Content-Length: [string length $html(404)]\n\n$html(404)"

    set http(200) \
"HTTP/$this(httpvers) 200 OK
Server: tinyhttpd/1.0
Last-Modified: %s
Content-Type: %s
Content-Length: %s\n"
    
    # These shall be used with 'format' to make html dir listings.
    set html(dirhead) "<HTML><HEAD><TITLE> %s </TITLE></HEAD>\n\
      <BODY bgcolor=#FFFFFF>\n\n\
      <!-- HTML generated by tinyhttpd -->\n\n\
      <TABLE nowrap border=0 cellpadding=2>\n\
      <TR>\n\
      \t<TD align=center nowrap height=19> %s objects </TD>\n\
      \t<TD align=left nowrap><B> Name </B></TD>\n\
      \t<TD align=right nowrap> Size </TD>\n\
      \t<TD align=left nowrap></TD>\n\
      \t<TD align=left nowrap> Type </TD>\n\
      \t<TD align=left nowrap> Date </TD>\n\
      </TR>\n\
      <TR>\n\
      \t<TD colspan=6 height=10><HR></TD>\n\
      </TR>\n"
    set html(dirline) "<TR>\n\
      \t<TD align=center nowrap height=17><a href=\"%s\">\
      <img border=0 width=16 height=16 src=\"%s\"></a></TD>\n\
      \t<TD align=left nowrap><a href=\"%s\"> %s </a></TD>\n\
      \t<TD align=right nowrap> - </TD>\n\
      \t<TD align=left nowrap></TD>\n\
      \t<TD align=left nowrap> Directory </TD>\n\
      \t<TD align=left nowrap> %s </TD>\n</TR>\n"
    set html(fileline) "<TR>\n\
      \t<TD align=center nowrap height=17><a href=\"%s\">\
      <img border=0 width=16 height=16 src=\"%s\"></a></TD>\n\
      \t<TD align=left nowrap><a href=\"%s\"> %s </a></TD>\n\
      \t<TD align=right nowrap> %s </TD>\n\
      \t<TD align=left nowrap></TD>\n\
      \t<TD align=left nowrap> %s </TD>\n\
      \t<TD align=left nowrap> %s </TD>\n</TR>\n"
    set html(dirbottom) "</TABLE>\n<BR>\n<BR>\n</BODY>\n</HTML>\n"
    
    # Default mapping from file suffix to Mime type.
    variable suffToMimeType
    array set suffToMimeType {
      .txt      text/plain
      .html     text/html
      .htm      text/html
      .text     text/plain
      .gif      image/gif
      .jpeg     image/jpeg
      .jpg      image/jpeg
      .png      image/png
      .tif      image/tiff
      .tiff     image/tiff
      .mov      video/quicktime
      .mpg      video/mpeg
      .mpeg     video/mpeg
      .mp4      video/mpeg4
      .avi      video/x-msvideo
      .aif      audio/aiff
      .aiff     audio/aiff
      .au       audio/basic
      .wav      audio/wav
      .mid      audio/midi
      .mp3      audio/mpeg
      .sdp      application/sdp
      .ps       application/postscript
      .eps      application/postscript
      .pdf      application/pdf
      .rtf      application/rtf
      .rtp      application/x-rtsp
      .rtsp     application/x-rtsp
      .tcl      application/x-tcl
      .zip      application/x-zip
      .sit      application/x-stuffit
      .gz       application/x-zip
    }
}

# tinyhttpd::start --
#
#       Start the http server.
#
# Arguments:
#       args:
#           -chunk                8192
#           -defaultindexfile     index.html
#	    -directorylisting     0
#	    -httpdrelativepath    httpd
#	    -log                  0
#	    -logfile              httpdlog.txt
#	    -myaddr               ""
#	    -port                 80
#	    -rootdirectory        thisPath
#       
# Results:
#       server socket opened.

proc ::tinyhttpd::start {args} {
    variable opts
    variable gstate
    variable this
        
    # Any configuration options. Start with defaults, overwrite with args.
    array set opts {
	-chunk                8192
	-defaultindexfile     index.html
	-directorylisting     0
	-httpdrelativepath    httpd
	-log                  0
	-logfile              httpdlog.txt
	-myaddr               ""
	-port                 80
    }
    set opts(-rootdirectory) [info script]
    array set opts $args
    if {[file pathtype $opts(-rootdirectory)] != "absolute"} {
	return -code error "the path must be an absolute path"
    }
    set servopts ""
    if {$opts(-myaddr) != ""} {
	set servopts [list -myaddr $opts(-myaddr)]
    }
    if {[catch {
	eval {socket -server [namespace current]::NewChannel} $servopts \
	  $opts(-port)} msg]} {
	return -code error "Couldn't start server socket: $msg."
    }	
    set gstate(sock) $msg

    # Log file
    if {$opts(-log)} {
	if {[catch {open $opts(-logfile) a} fd]} {
	    catch {close $gstate(sock)}
	    return -code error "Failed open the log file: $opts(-logfile): $fd"
	}
	set gstate(logfd) $fd
	fconfigure $gstate(logfd) -buffering none
    }
    
    # Icon unix style paths when returning directory listings.
    set httpdRelPathList [split $opts(-httpdrelativepath) $this(sep)]
    set httpdRelPath     [join $httpdRelPathList /]
    set gstate(folderIconRelPath) /$httpdRelPath/macfoldericon.gif
    set gstate(fileIconRelPath)   /$httpdRelPath/textfileicon.gif
    set absIconPath [file join $opts(-rootdirectory) $opts(-httpdrelativepath) \
      macfoldericon.gif]
    if {![file exists $absIconPath]} {
	return -code error "Failed localizing macfoldericon.gif"
    }
        
    LogMsg "Tiny Httpd started"

    return ""
}

# tinyhttpd::NewChannel --
# 
#       Callback procedure for the server socket. Gets called whenever a new
#       client connects to the server socket.
#
# Arguments:
#	token	The token for the internal state array.
#       
# Results:
#       token

proc ::tinyhttpd::NewChannel {s ip port} {
    variable uid

    Debug 2 "NewChannel:: s=$s, ip=$ip, port=$port"

    # Initialize the state variable, an array.  We'll return the
    # name of this array as the token for the transaction.
    
    set token [namespace current]::[incr uid]
    variable $token
    upvar 0 $token state
	
    array set state {
	currentsize       0
	state             "connected"
	status            ""
    }
    array set state [list     \
      s             $s        \
      ip            $ip       \
      port          $port]    
    
    # Everything should be done with 'fileevent'.
    fconfigure $s -blocking 0 -buffering line
    fileevent $s readable [list [namespace current]::HandleRequest $token]
    
    return $token
}

# tinyhttpd::HandleRequest --
#
#       Initiates a GET or HEAD request. A sequence of callbacks completes it.
#       
# Arguments:
#	token	The token for the internal state array.
#       
# Results:
#       new fileevent procedure registered.

proc ::tinyhttpd::HandleRequest {token} {    
    variable $token
    upvar 0 $token state    
    variable gstate

    Debug 2 "HandleRequest:: $token"
    
    set s $state(s)
    set state(state) reading

    # If client closes socket.
    if {[catch {eof $s} iseof] || $iseof} {
	Finish $token eof
    } elseif {[fblocked $s]} {
	Debug 2 "\tblocked"
	return
    }
        
    # If end-of-file or because of insufficient data in nonblocking mode,
    # then gets returns -1.
    if {[catch {
	set nbytes [gets $s line]
    }]} {
	Finish $token eof
	return
    }
    
    # Ignore any leading empty lines (RFC 2616, 4.1).
    if {$nbytes == 0} {
	return
    } elseif {$nbytes != -1} {
	set state(line) $line
	Debug 2 "\tline=$line"

	# We only implement the GET and HEAD operations.
	if {[regexp -nocase {^(GET|HEAD) +([^ ]*) +HTTP/([0-9]+\.[0-9]+)} \
	  $line match cmd path reqvers]} {    
	    LogMsg "$cmd: $path"
	    set state(cmd) $cmd

	    # The unix style, uri encoded path relative -rootdirectory,
	    # starting with a "/".
	    set state(path) $path
	    set state(reqvers) $reqvers
	    
	    # Set fileevent to read the sequence of 'key: value' lines.
	    fileevent $s readable   \
	      [list [namespace current]::Event $token]
	} else {
	    LogMsg "unknown request: $line"
	    Finish $token "unknown request: $line"
	}
    } else {

	# If end-of-file or because of insufficient data in nonblocking mode,
	# then gets returns -1.
	return
    }
}
	    
# tinyhttpd::Event --
#
#       Reads and processes a 'key: value' line, reschedules itself if not blank
#       line, else calls 'Respond' to initiate a file transfer.
#
# Arguments:
#	token	The token for the internal state array.
#       
# Results:

proc ::tinyhttpd::Event {token} {    
    variable $token
    upvar 0 $token state    
    variable gstate
    
    set s $state(s)
    if {[catch {eof $s} iseof] || $iseof} {
	Debug 2 "Event:: eof s"
	Finish $token eof
	return
    } elseif {[fblocked $s]} {
	Debug 2 "Event:: blocked s"
	return
    }
    
    # If end-of-file or because of insufficient data in nonblocking mode,
    # then gets returns -1.
    if {[catch {
	set nbytes [gets $s line]
	Debug 3 "Event:: nbytes=$nbytes, line=$line"
    }]} {
	Finish $token eof
	return
    }
    if {$nbytes == -1} {
	return
    } elseif {$nbytes > 0} {
	
	# Keep track of request meta data.
	if {[regexp -nocase {^([^:]+):(.+)$} $line x key value]} {
	    lappend state(meta) $key [string trim $value]
	}
	if {[regexp -nocase {^content-type:(.+)$} $line x type]} {
	    set state(content-type) [string trim $type]
	} elseif {[regexp -nocase {^content-range:(.+)$} $line x range]} {
	    set state(content-range) [string trim $range]
	}
	
    } elseif {$nbytes == 0} {
	
	# First empty line, set up file transfer.
	fileevent $s readable {}
	Respond $token
    }
}

# tinyhttpd::Respond --
#
#       Responds the client with the HTTP protocol, and then a number of 
#       'key: value' lines. File transfer initiated.
#       
# Arguments:
#	token	The token for the internal state array.
#       
# Results:
#       fcopy called.

proc ::tinyhttpd::Respond {token} {
    global  this tcl_platform
    
    variable $token
    upvar 0 $token state    
    variable opts
    variable gstate
    variable suffToMimeType
    variable httpMsg
    variable http
    variable timing
    
    Debug 2 "Respond:: $token"
            
    set s $state(s)
    set cmd $state(cmd)
    set inRelPath [string trimleft $state(path) /]
        
    # Decode requested file path.
    set inRelPath [uriencode::decodefile $inRelPath]
    Debug 2 "\tinRelPath=$inRelPath"
    
    # Join incoming decoded file path with our base directory,
    # and normalize, i.e. remove all ../
    set localPath [file normalize [file join $opts(-rootdirectory) $inRelPath]]
    Debug 2 "\tlocalPath=$localPath"
    
    # If no actual file given then search for the '-defaultindexfile',
    # or if no one, possibly return directory listing.
    
    if {[file isdirectory $localPath]} {
	set defFile [file join $localPath $opts(-defaultindexfile)]
	if {[file exists $defFile]} {
	    set localPath $defFile
	} elseif {$opts(-directorylisting)} {
	    
	    # No default html file exists, return directory listing.
	    set modTime [clock format [file mtime $localPath]  \
	      -format "%a, %d %b %Y %H:%M:%S GMT" -gmt 1]
	    if {[catch {
		puts $s [format $http(headdirlist) $modTime]
		
		if {[string equal $cmd "GET"]} {
		    set html [BuildHtmlDirectoryListing $state(path)]
		    puts $s "Content-Length: [string length $html]"
		    puts $s "\n"
		    puts $s $html
		} else {
		    puts $s "\n"
		}
	    } err]} {
		Finish $token $err
		return
	    }
	    Finish $token
	    Debug 2 "\tNo default html file exists, return directory listing."
	    return
	} else {
	    if {[catch {
		if {[string equal $cmd "GET"]} {
		    puts $s $http(404GET)
		} else {
		    puts $s $http(404)
		}
	    } err]} {
		Finish $token $err
		return
	    }
	    Finish $token ok
	    return
	}
    }
    set fext [string tolower [file extension $localPath]]
    if {[info exists suffToMimeType($fext)]} {
	set mime $suffToMimeType($fext)
    } else {
	set mime "application/octet-stream"
    }
    
    # Check that the file is there and opens correctly.
    if {$localPath == "" || [catch {open $localPath r} fd]}  {
	if {[catch {
	    if {[string equal $cmd "GET"]} {
		puts $s $http(404GET)
	    } else {
		puts $s $http(404)
	    }
	} err]} {
	    Finish $token $err
	    return
	}
	Finish $token ok
	Debug 2 "\topen $localPath failed"
	return
    } else  {
	
	# Put stuff.
	set state(fd) $fd
	set size [file size $localPath]
	set state(size) $size
	set modTime [clock format [file mtime $localPath]  \
	  -format "%a, %d %b %Y %H:%M:%S GMT" -gmt 1]
	set data [format $http(200) $modTime $mime $size]
	if {[catch {
	    puts $s $data
	    flush $s
	} err]} {
	    Finish $token $err
	    return
	}
	Debug 3 "\tdata='$data'"
    }
    if {[string equal $cmd "HEAD"]}  {
	Finish $token ok
	return
    }
    
    # If binary data.
    if {![string match "text/*" $mime]} {
	fconfigure $fd -translation binary
	fconfigure $s -translation binary
    }
    flush $s
    
    # Seems necessary (?) to avoid blocking the UI. BAD!!!
    #update
    
    # Background copy. Be sure to switch off all fileevents on channel.
    fileevent $s readable {}
    fileevent $s writable {}
    fconfigure $s -buffering full
    set timing($token) [list [clock clicks -milliseconds] 0]
    
    # Initialize the stream copy.
    CopyStart $s $token
}

# tinyhttpd::CopyStart --
# 
#       The callback procedure for fcopy when copying from a disk file
#       to the socket. 
#       
# Arguments:
#	s	The socket to copy to
#	token	The token for the internal state array.
#       bytes     number of bytes in this chunk.
#       error     any error.
#       
# Results:
#       possibly reschedules itself.

proc ::tinyhttpd::CopyStart {s token} {    
    variable $token
    upvar 0 $token state    
    variable gstate
    variable opts
    
    Debug 4 "CopyStart::"

    if {[catch {
	fcopy $state(fd) $s -size $opts(-chunk) -command  \
	  [list [namespace current]::CopyDone $token]
    } err]} {
	Finish $token $err
    } 
}

# tinyhttpd::CopyDone
#
#	fcopy completion callback
#
# Arguments
#	token	The token for the internal state array.
#	bytes	The amount transfered
#
# Side Effects
#	Invokes callbacks

proc ::tinyhttpd::CopyDone {token bytes {error {}}} {    
    variable $token
    variable timing
    upvar 0 $token state

    Debug 4 "CopyDone::"

    set s $state(s)
    set fd $state(fd)
    incr state(currentsize) $bytes
    lappend timing($token) [clock clicks -milliseconds] $state(currentsize)

    # At this point the token may have been reset
    if {[string length $error] > 0} {
	Finish $token $error
    } elseif {[catch {eof $s} iseof] || $iseof} {
	Finish $token eof
    } elseif {[catch {eof $fd} iseof] || $iseof} {
	Finish $token
    } else {
	CopyStart $s $token
    }
}

proc ::tinyhttpd::Finish {token {errmsg ""}} {
    variable $token
    upvar 0 $token state    
    
    LogMsg "Finish errmsg=$errmsg"
    Debug 2 "Finish errmsg=$errmsg"
    
    if {[string length $errmsg]} {
	set state(status) $errmsg
    } else {
	set state(status) ok
    }
    set state(state) finished
    catch {close $state(s)}
    if {[info exists state(fd)]} {
	catch {close $state(fd)}
    }
    unset state
}

# tinyhttpd::stop --
# 
#       Closes the server socket. This stops new connections to take place,
#       but existing connections are kept alive.
#       
# Arguments:
#       
# Results:

proc ::tinyhttpd::stop { } {    
    variable gstate
    variable opts
    
    catch {close $gstate(sock)}
    LogMsg "Tiny Httpd stopped"
    if {$opts(-log)} {
	catch {close $gstate(logfd)}
    }
}

# tinyhttpd::anyactive --
# 
# 

proc ::tinyhttpd::anyactive { } {

    return [expr {[llength [getTokenList]] > 1} ? 1 : 0]
}

proc tinyhttpd::getTokenList { } {
    
    set ns [namespace current]
    return [concat  \
      [info vars ${ns}::\[0-9\]] \
      [info vars ${ns}::\[0-9\]\[0-9\]] \
      [info vars ${ns}::\[0-9\]\[0-9\]\[0-9\]] \
      [info vars ${ns}::\[0-9\]\[0-9\]\[0-9\]\[0-9\]] \
      [info vars ${ns}::\[0-9\]\[0-9\]\[0-9\]\[0-9\]\[0-9\]]]
}

# tinyhttpd::bytestransported --
# 
# 

proc ::tinyhttpd::bytestransported { } {
    variable timing
    
    set totbytes 0
    foreach key [array names timing "*"] {
	incr totbytes [lindex $timing($key) end]
    }
    return $totbytes
}

# tinyhttpd::avergaebytespersec --
# 
# 

proc ::tinyhttpd::avergaebytespersec { } {
    variable timing
    
    set totbytes 0
    set totms 0
    foreach key [array names timing "*"] {
	incr totbytes [lindex $timing($key) end]
	incr totms [expr [lindex $timing($key) end-1] - \
	  [lindex $timing($key) 0]]
    }
    return [expr 1000 * $totbytes / [expr $totms + 1]]
}

# tinyhttpd::cleanup --
# 
# 

proc ::tinyhttpd::cleanup { } {
    variable gstate
    variable opts
    variable timing
    
    # Not sure precisely what to do here.
    catch {close $gstate(sock)}
    if {$opts(-log)} {
	catch {close $gstate(logfd)}
    }   
    unset -nocomplain timing

    foreach token [getTokenList] {
	Finish $token reset
    }
}

# tinyhttpd::BuildHtmlDirectoryListing --
#
#       Returns the html code that describes the directory inPath.
#       
# Arguments:
#       inPath      the unix style, uri encoded path relative -rootdirectory,
#                   starting with a "/".
#       
# Results:
#       A complete html page describing the directory.

proc ::tinyhttpd::BuildHtmlDirectoryListing {inPath} {    
    variable this
    variable html
    variable gstate
    variable opts
    
    Debug 2 "BuildHtmlDirectoryListing: inPath=$inPath"
            
    # Add the absolute '-rootdirectory' path with the relative 'inPath' to 
    # form the absolute path of the directory.
    set inRelPath [string trimleft $inPath /]

    # Decode requested file path.
    set inRelPath [uriencode::decodefile $inRelPath]

    # Join incoming decoded file path with our base directory,
    # and normalize, i.e. remove all ../
    set localAbsPath  \
      [file normalize [file join $opts(-rootdirectory) $inRelPath]]
    set nativePath [file nativename $localAbsPath]
    
    Debug 3 "localAbsPath=$localAbsPath\n\tnativePath=$nativePath"
    set fileIcon   $gstate(fileIconRelPath)
    set folderIcon $gstate(folderIconRelPath)
    
    # Start by finding the directory content. 
    set allFiles [glob -directory $nativePath -nocomplain *]
    set totN [llength $allFiles]
    
    # Build the complete html page dynamically.    
    set thisDir [string trim [lindex [file split $localAbsPath] end] "\\/"]
    set htmlStuff [format $html(dirhead) $thisDir $totN]
    
    # Loop over all files and directories in our directory.
    foreach f $allFiles {
	
	if {[catch {clock format [file mtime $f]  \
	  -format "%a %d %b %Y, %H.%M"} dateAndTime]} {
	    set dateAndTime --
	}
	set tail [file tail $f]
	
	# The link in html must be the encoded relative unix path.
	set link "[string trimright $inPath /]/[uriencode::quotepath $tail]"
	
	# Is file or directory?
	if {[file isdirectory $f]} {
	    append link /
	    append htmlStuff [format $html(dirline) $link  \
	      $folderIcon $link $tail $dateAndTime]
	} else {
	    if {[catch {file size $f} bytes]} {
		set bytes 0
	    }
	    set formBytes [FormatBytesText $bytes]
	    append htmlStuff [format $html(fileline) $link  \
	      $fileIcon $link $tail $formBytes $tail $dateAndTime]
	}
    }
    
    # And the end.
    append htmlStuff $html(dirbottom)
    return $htmlStuff
}    

# tinyhttpd::setmimemappings --
#
#       Set the mapping from file suffix to Mime type.
#       Removes old ones.
#
# Arguments:
#       suff2MimeList
#       
# Results:
#       namespace variable 'suffToMimeType' set.

proc ::tinyhttpd::setmimemappings {suff2MimeList} {
    variable suffToMimeType
    
    # Clear out the old, set the new.
    unset -nocomplain suffToMimeType
    array set suffToMimeType $suff2MimeList
}

# tinyhttpd::addmimemappings --
#
#       Adds the mapping from file suffix to Mime type.
#       Keeps or replaces old ones.
#
# Arguments:
#       suff2MimeList
#       
# Results:
#       namespace variable 'suffToMimeType' set.

proc ::tinyhttpd::addmimemappings {suff2MimeList} {
    variable suffToMimeType
    
    array set suffToMimeType $suff2MimeList
}

# tinyhttpd::FormatBytesText --
#
#

proc ::tinyhttpd::FormatBytesText {bytes} {
    
    if {$bytes < 1} {
	return 0
    }
    set log10 [expr log10($bytes)]
    if {$log10 >= 6} {
	set text "[format "%3.1f" [expr $bytes/1000000.0]]M"
    } elseif {$log10>= 3} {
	set text "[format "%3.1f" [expr $bytes/1000.0]]k"
    } else {
	set text $bytes
    }
    return $text
}

proc ::tinyhttpd::Debug {num str} {
    variable gstate
    
    if {$num <= $gstate(debug)} {
	puts $str
    }
}

proc ::tinyhttpd::LogMsg {msg} {
    variable opts
    
    if {$opts(-log)} {
	catch {
	    puts $gstate(logfd) "[clock format [clock clicks]]: $msg"
	}
    }
}

#-------------------------------------------------------------------------------

