Posted to tcl by Napier at Tue May 26 09:21:50 GMT 2015view pretty
## Other required packages. lappend auto_path /remote/Store/Prg/tclpkg package require uri package require base64 package require tls ## Tuning parameters. set tuning { header_lines_max 30 request_timeout 5000 zip_minimum 0 zip_level 9 } ## Put anything httpd into an own namespace. namespace eval ::httpd { ## Accept incoming connection. variable Directory /remote/Store/Common/Dash/atv/ proc accept {sock ip port} { puts "Accept from $ip $port" ## Start coroutine for client. chan event $sock readable [coroutine ::httpd::reader$sock apply {{sock ip port} { ## Return the coroutine command on first call so "chan event" can remember it. yield [info coroutine] ## This any the parts after subsequent "yields" are called automatically by the "chan event" mechanism. try { ## Start a timeout for the requests. set timeout [after [dict get $::tuning request_timeout] [list ::httpd::timeout $sock [info coroutine]]] ## Do nonblocking I/O on client socket. chan configure $sock -blocking 0 ## Read requests subsequently. while {1} { ## HTTP headers are ascii encoded with CRLF line ends, line buffering is fine. chan configure $sock -encoding ascii -translation crlf -buffering line ## Read the request line. set request {} while {$request eq {}} { ## Get request. chan gets $sock request ## Return control to the event loop in the blocked case. if {[chan blocked $sock]} yield ## End coroutine when client has closed the channel. if {[chan eof $sock]} return } ## Default header values. set headers {} dict set headers Accept-Encoding "identity;q=0.001" ## Read additional header lines. for {set i 0} {$i < [dict get $::tuning header_lines_max]} {incr i} { ## Read header line. chan gets $sock headerline ## Return control to the event loop in the blocked case. if {[chan blocked $sock]} yield ## It's an error to have an eof before header end (empty line). if {[chan eof $sock]} { throw {HTTPD REQUEST_HEADER CONNECTION_CLOSED} "connection closed by client during read of HTTP request header"} ## Break loop on last header line. if {$headerline eq {}} break ## This is a regular header line. ## Remember field name and value. Repeated field values are lappended. set sep [string first ":" $headerline] dict lappend headers [string range $headerline 0 $sep-1] [string trim [string range $headerline $sep+1 end]] } ## Complain about too many header lines. if {$i == [dict get $::tuning header_lines_max]} { throw {HTTPD REQUEST_HEADER TOO_MANY_LINES} "too many header lines in HTTP request" } ## Join appended header fields with comma,space (RFC2616, section 4.2). dict for {name values} $headers { dict set headers $name [join $values ", "] } ## Get HTTP method, protocol version and URL. lassign $request method url version ## Parse "Accept-Encoding" header. Defaults to "identity" if none is present. set accepted_encodings [parseHeaderList [dict get $headers Accept-Encoding]] ## Respond by method. switch -- $method { HEAD - GET { ## Handle the single request. set data [handleRequest $method $url $version $headers {}] ## Sort out clients which don't accept zipped content at all. if {$accepted_encodings ne "identity"} { ## Check if content is worth it (long enough, not already zipped internally). if {[string length [dict get $data content]] >= [dict get $::tuning zip_minimum]} { switch -glob -- [dict get $data content-type] { "text/*" { ## Go through list of accepted encodings. foreach enc $accepted_encodings { switch -- $enc { deflate - x-deflate { ## Zip content as raw LZW stream. dict set data content [zlib deflate [dict get $data content] [dict get $::tuning zip_level]] ## Add header field. dict set data headers Content-Encoding $enc ## Do not apply another encoding. break } gzip - x-gzip { ## Zip content as GZIP stream (see RFC 1952). dict set data content [zlib gzip [dict get $data content] -level [dict get $::tuning zip_level]] ## Add header field. dict set data headers Content-Encoding $enc ## Do not apply another encoding. break } compress { ## Zip content as ZLIB compressed stream. dict set data content [zlib compress [dict get $data content] [dict get $::tuning zip_level]] ## Add header field. dict set data headers Content-Encoding $enc ## Do not apply another encoding. break } } } } "image/*" { dict set data headers Accept-Ranges bytes } } } } ## Send result header. chan configure $sock -encoding ascii -translation crlf -buffering full puts $sock "$version [dict get $data code] OK" puts $sock "Content-Type: [dict get $data content-type]" puts $sock "Content-Length: [string length [dict get $data content]]" foreach {field value} [dict get $data headers] { puts $sock "$field: $value" } puts $sock "" } default { throw {HTTPD REQUEST_METHOD UNSUPPORTED} "unsupported HTTP method in request" } } switch -- $method { GET { ## Send result. chan configure $sock -translation binary puts -nonewline $sock [dict get $data content] } } ## Flush output before reading next request. chan flush $sock } } trap {HTTPD REQUEST_HEADER TOO_MANY_LINES} {} { puts stderr "HTTPD REQUEST_HEADER TOO_MANY_LINES $ip" } trap {HTTPD REQUEST_HEADER CONNECTION_CLOSED} {} { puts stderr "HTTPD REQUEST_HEADER CONNECTION_CLOSED $ip" } trap {HTTPD REQUEST_METHOD UNSUPPORTED} {} { puts stderr "HTTPD REQUEST_METHOD UNSUPPORTED $ip" } trap {POSIX ECONNABORTED} {} { puts stderr "SSL ERROR $ip" } on error {} { puts stderr "$::errorCode $::errorInfo" } finally { close $sock after cancel $timeout } } ::httpd} $sock $ip $port] } ## Handle timeout. proc timeout {sock coroutine_id} { ## Close the channel. close $sock ## Remove the coroutine rename $coroutine_id {} } ## Parse lists in HTTP header fields. proc parseHeaderList {list} { ## Go through all list items. foreach item [split $list ","] { ## First subfield is a name. set type [string trimleft [lindex [split $item ";"] 0]] ## Parse other subfields. RF2616 demands quality "q=..." is the second field, but we are more generous. set q 1.0 set ext {} foreach subfield [lrange [split $item ";"] 1 end] { lassign [split $subfield "="] subfield_name subfield_value switch -- [string trimleft $subfield_name] { q {set q $subfield_value} default {append ext $subfield} } } ## Remember item name by quality. ## Any extension is appended to the type. dict lappend ql $q [concat $type $ext] } ## Return list items sorted by q value. Remove "q=0" row dict unset ql 0 set result {} foreach {q types} [lsort -stride 2 -real -decreasing $ql] { lappend result {*}$types } return $result } ## Handle a single HTTP request. proc handleRequest {method url version headers indata} { puts "Handle Request" puts "$method" puts "$url" puts "$version" puts "$headers" puts "$indata" if {$url == "/artwork.png"} {set data [serveImage -name artwork.png]} else {set data ""} dict set result code "200" # charset=[encoding system] dict set result content $data dict set result content-type "image/png" dict set result headers {} return $result } proc serveImage {args} { #### ::Tools::readFile ### Reads the specified file and returns its contents ## Arguments: # -name : The name of the file to read in the modules Common Directory. variable Directory if {[dict exists $args -name]} {set name [dict get $args -name]} else {UCL 5 "ERROR: No Name provided to ::Tools::createFile"; return} if {[info exists Directory]} {} else {UCL 5 "ERROR: Variable ::DashOS::Directory Doesn't Exist"; return} set filePath "${Directory}/${name}" if {![file isfile $filePath]} {return -1} set f [open $filePath rb] set contents [read -nonewline $f] close $f return $contents } } ## Prepare the server. #::tls::init \ -certfile server-public.pem \ -keyfile server-private.pem \ -ssl2 1 -ssl3 1 -tls1 0 \ -require 0 -request 0 #::tls::socket -server ::httpd::accept 9005 socket -server ::httpd::accept 9005 ## Start Tcl event loop. vwait forever