# xmlrpc0.3 # Written by Eric Yeh # # Server API: # xmlrpc::serve # Note: all callable functions should be defined in the global scope # # Client API: # xmlrpc::call url methodName params # url is of the form "http://hostname:port" # methodName is the name of the method to call # and params is a list of arguments to the method # where each argument is a "typed tcl" value defined below # xmlrpc::buildRequest # return an XML-RPC client request # xmlrpc::marshall # return a marshalled "typed tcl" value # xmlrpc::unmarshall # return an unmarshalled "typed tcl" value # xmlrpc::assoc # return a cons of a list if the key matches # # Typed Tcl values: # Because Tcl has no types for variables, all values will be represented # as a 2 element tuple of the form: # {type, value} where value is the original value # and type is a string describing its type. # Valid types (case sensitive, must be lowercase): # int # boolean # string # double # dateTime.iso8601 # base64 # struct # array # Note: # When marshalling dictionaries(tcl arrays), tcl has no # way of creating unnamed dictionaries. Therefore, # the way to use a dictionary is to create it as normal, # and refer to its name in the "tcl type". # For example: # set dict(first) {string eric} # xmlrpc::marshall {struct dict} # # the marshall procedure will attempt to "find" dict # using upvar(yuck!) and checking the global scope. # # Unmarshalling of a dictionary results in a 2 element # list of the form (remaining, alist) # where remaining is unused marshalled data (should be empty) # and alist is an A-list. # An A-list has the form: # {key, datum} where key is the key and datum is its value. # The method "assoc" is provided to access information from # this data structure. It behaves like the LISP assoc, in that # it will return the (key, datum) pair if a match is found. # TODO: # -currently server functions can't return dictionaries # -add more error handling # -Check for [{}] in unmarshalling # -Empty dictionaries package provide xmlrpc 0.3 namespace eval xmlrpc { namespace export call buildRequest marshall unmarshall assoc namespace export serve variable READSIZE 4096; # read size variable WS "\[ |\n|\t\|\r]"; # WhiteSpace variable W "\[^ |\n|\t\]"; # a word with no spaces variable DIGIT "\[0-9\]"; # Digit variable response ""; # response to return variable acceptfd ""; # socket to listen on variable DEBUG 0; # debug } # Given a port, create a new socket # and start listening on it # proc xmlrpc::serve {port} { variable acceptfd set acceptfd [socket -server xmlrpc::serveOnce $port] return $acceptfd } # Accept a new connection # proc xmlrpc::serveOnce {sock addr port} { variable READSIZE debug "in serveOnce: addr: $addr" debug "in serveOnce: port: $port" fconfigure $sock -translation {lf lf} -buffersize $READSIZE fconfigure $sock -blocking off fileevent $sock readable [list xmlrpc::doRequest $sock] } # Given a socket, # Handle an XML-RPC request # proc xmlrpc::doRequest {sock} { variable WS set res [readHeader $sock] set headerStatus [lindex $res 0]; # Header + Status set body [lindex $res 1]; # Body, if any set RE "\[^\n\]+\n(.*)" if {![regexp $RE $headerStatus {} header]} { return [errReturn "Malformed Request"] } set body [getBody $sock $header $body] set RE "<\?xml.version=."; # xml version append RE "\[^\?\]+.\?>$WS*"; # version number append RE "$WS*"; # methodCall tag append RE ""; # methodName tag append RE "(\[a-zA-Z0-9_:\/\\.\]+)"; # method Name append RE "$WS*"; # end methodName tag append RE "(.*)"; # parameters, if any append RE ".*"; # end methodCall tag if {![regexp $RE $body {} mname params]} { return [errReturn "Malformed methodCall"] } set args {} set param [string range $params 8 end] set param [string trim $param] while {[string range $param 0 6] == "" || [string range $param 0 7] == ""} { # check for empty element if {[string range $param 0 7] == ""} { lappend args {} set param [string range $param 8 end] set param [string trim $param] continue } set param [string range $param 7 end] set param [string trim $param] set res [unmarshall $param] set param [lindex $res 0] set el [lindex $res 1] lappend args $el if {[string range $param 0 7] != ""} { return [errReturn "Invalid End Param"] } set param [string range $param 8 end] set param [string trim $param] } if {$param != ""} { return [errReturn "Invalid End Params"] } if {[catch {set result [eval ::$mname $args]}]} { set response [buildFault 100 "eval() failed"] } else { set response [buildResponse $result] } debug "in doRequest: response:\n$response" puts -nonewline $sock $response flush $sock catch {close $sock} } # Given a "typed tcl" value, # build an XML-RPC response # proc buildResponse {result} { # build the body set body "\n" append body "\n" append body "\t\n" append body "\t\t\n" append body [xmlrpc::marshall $result 3 2] append body "\n\t\t\n" append body "\t\n" append body "\n" set lenbod [string length $body] # build the header set header "HTTP/1.1 200 OK\n" append header "Content-Type: text/xml\n" append header "Content-length: $lenbod\n" set response "$header\n$body" return $response #return [string trim $response] } # Given an error code (integer) # and an errmsg (string) # build an XML-RPC fault # proc buildFault {errcode errmsg} { set err(faultCode) [list int $errcode] set err(faultString) [list string $errmsg] # build the body set body "\n" append body "\n" append body "\t\n" append body [xmlrpc::marshall {struct err} 2] append body "\t\n" append body "\n" set lenbod [string length $body] # build the header set header "HTTP/1.1 200 OK\n" append header "Content-Type: text/xml\n" append header "Content-length: $lenbod\n" set response "$header\n$body" return [string trim $response] } # send an XML-RPC request # proc xmlrpc::call {url method methodName params {ntabs 4} {distance 3}} { variable READSIZE variable response global readdone global xmlcall set readdone 0 set xmlcall 1 set RE {http://([^:]+):([0-9]+)} if {![regexp $RE $url {} host port]} { return [errReturn "Malformed URL"] } set sock [socket $host $port] fconfigure $sock -translation {lf lf} -buffersize $READSIZE fconfigure $sock -blocking off if {[catch {set request [buildRequest $method $methodName $params $ntabs $distance]}]} { return } puts -nonewline $sock $request flush $sock fileevent $sock readable [list xmlrpc::getResponse $sock] vwait readdone catch {close $sock} if {$readdone > 0} { return $response } else { return [errReturn "xmlrpc::call failed"] } } # Given a socket to read on, # get and parse the response from the server # proc xmlrpc::getResponse {sock} { variable response global readdone set res [readHeader $sock] set headerStatus [lindex $res 0]; # Header + Status set body [lindex $res 1]; # Body, if any set header [parseHTTPCode $headerStatus] set body [getBody $sock $header $body] set response [parseResponse $body] set readdone 1 } # Given a socket to read on, # a string of header information # and a string, body, # return a string representing the entire body # proc xmlrpc::getBody {sock header body} { set res [parseHTTPHeaders $header] set headersl [lindex $res 1]; # A-list of headers set expLenl [assoc "Content-Length" $headersl] if {$expLenl == {}} { return [errReturn "No Content-Length found"] } set expLen [lindex $expLenl 1] set body [readBody $body $expLen $sock] return $body } # Given a socket to read on, # Return a 2 element list of the form: # {header, body} where both are strings # Note: header will include the first line which is the status # proc xmlrpc::readHeader {sock} { set buffer "" while {1} { if {[catch {set buff [nbRead $sock]}]} { return [errReturn "Premature eof"] } append buffer $buff set nindex [string first "\n\n" $buffer] if {$nindex > 0} { break } set bindex [string first "\r\n\r\n" $buffer] if {$bindex > 0} { break } } if {$nindex > 0} { set header [string range $buffer 0 [expr $nindex - 1]] set body [string range $buffer [expr $nindex + 2] end] } elseif {$bindex > 0} { set header [string range $buffer 0 [expr $bindex - 1]] set body [string range $buffer [expr $bindex + 4] end] } return [list $header $body] } # Given the body buffer, # the number of bytes expected in the body (Content-Length) # and a socket to read on, # return the entire body buffer # proc xmlrpc::readBody {body expLen sock} { set newbody $body while {1} { if {[catch {set buff [nbRead $sock]}]} { return [errReturn "Premature eof"] } append newbody $buff set bodLen [string length $newbody] if {$bodLen == $expLen} { break } elseif {$bodLen > $expLen} { return [errReturn "Content-length:$expLen does not match Body Length:$bodLen"] } } return $newbody } # Given a string, str, # check the HTTP status # and return the unused portion of str # proc xmlrpc::parseHTTPCode {str} { variable DIGIT set RE "HTTP/"; # HTTP message append RE "($DIGIT+\\.*$DIGIT*)."; # version append RE "($DIGIT+)."; # status code append RE "(\[^\n\]+)\n(.*)"; # status message if {![regexp $RE $str {} vern status code rest]} { return [errReturn "Unrecognized HTTP code:\n$str"] } if {$status != "200"} { return [errReturn "Bad HTTP status: $status"] } return $rest } # Given a string, str, # return a 2 element list of the form: # {remaining, alist} # where remaining is the unused portion of str # and alist is an A-list of header information # proc xmlrpc::parseHTTPHeaders {str} { set headers {} set remain {} set remainp 0 set RE {([^:]+):(.*)} set parts [split $str "\n"] foreach {part} $parts { if {$part == "" && !$remainp} { set remainp 1 continue } if {$remainp} { lappend remain $part continue } if {![regexp $RE $part {} key value]} { return [errReturn "Unrecognized HTTP Header format: $part"] } set value [string trim $value] lappend headers [list $key $value] } set rest [join $remain "\n"] return [list $rest $headers] } # Given a string, str # parse the response from the server # returning the unmarshalled data # proc xmlrpc::parseResponse {str} { variable WS set RE "<\?xml.version=."; # xml version append RE "(\[^\?\]+).\?>$WS*"; # version number append RE "$WS*"; # method response tag append RE "$WS*"; # params tag append RE "$WS*"; # param tag append RE "(.*)"; # value append RE "$WS*"; # end param tag append RE "$WS*"; # end params tag append RE ""; # end method response tag if {![regexp $RE $str {} vern value]} { set RE "<\?xml.version=."; # xml version append RE "(\[^\?\]+).\?>$WS*"; # version number append RE "$WS*"; # method response tag append RE "$WS*"; # fault tag append RE "(.*)$WS*"; # fault values append RE "$WS*"; # end fault tag append RE ""; # end method response tag if {![regexp $RE $str {} vern value]} { return [errReturn "Unrecognized response from server"] } } set result [unmarshall $value] return $result } # Given a non-blocking file descriptor, fd # do a read # proc xmlrpc::nbRead {fd} { variable READSIZE fileevent $fd readable "" set buffer "" while {1} { if {[eof $fd]} { catch {close $fd} break } set temp [read $fd $READSIZE] if {$temp == ""} { break } append buffer $temp } return $buffer } # Given a methodName, # and a list of parameters, # return an XML-RPC request # proc xmlrpc::buildRequest {method methodName params {ntabs 4} {distance 2}} { # build the body set body "\n" append body "\n" append body "\t$methodName\n" if {$params != {}} { append body "\t\t\n" foreach {param} $params { append body "\t\t\t\n" append body [xmlrpc::marshall $param $ntabs $distance] append body "\n\t\t\t\n" } append body "\t\t\n" } append body "\n" # set body [regsub -all "\n" $body "\r\n"] set lenbod [string length $body] # build the header set header "POST /$method HTTP/1.0\n" append header "Content-Type: text/xml\n" append header "Content-length: $lenbod\n" # set header [regsub -all "\n" $header "\r\n"] # set request "$header\r\n$body" set request "$header\n$body" return $request } # Given a "typed tcl" value # return the marshalled representation # proc xmlrpc::marshall {param {ntabs 0} {distance 1}} { if {![validParam $param]} { return [errReturn "Malformed Parameter: $param"] } set strtabs "" for {set x 0} {$x < $ntabs} {incr x} { append strtabs "\t" } set type [lindex $param 0] set val [lindex $param 1] if {$type == "int"} { return "$strtabs$val" } elseif {$type == "i4"} { return "$strtabs$val" } elseif {$type == "boolean"} { return "$strtabs$val" } elseif {$type == "string"} { return "$strtabs$val" } elseif {$type == "double"} { return "$strtabs$val" } elseif {$type == "dateTime.iso8601"} { return "$strtabs$val" } elseif {$type == "base64"} { return "$strtabs$val" } elseif {$type == "struct"} { # get the original caller's scope upvar $distance $val dict # try the global scope if {![array exists dict]} { upvar #0 $val dict } set str "$strtabs\n" append str "$strtabs\t\n" foreach {k v} [array get dict] { append str "$strtabs\t\t\n" append str "$strtabs\t\t\t$k\n" append str [marshall $v [expr $ntabs + 3] [expr $distance + 1]] append str "\n$strtabs\t\t\n" } append str "$strtabs\t\n" append str "$strtabs\n" return $str } elseif {$type == "array"} { set str "$strtabs\n" append str "$strtabs\t\n" append str "$strtabs\t\t\n" foreach el $val { append str [marshall $el [expr $ntabs + 3] [expr $distance + 1]] append str "\n" } append str "$strtabs\t\t\n" append str "$strtabs\t\n" append str "$strtabs\n" return $str } else { return [errReturn "Unknown type: $type"] } } # Given a value param, # return 1 if it a valid parameter # return 0 if not # A valid parameter is a 2 element tuple # proc xmlrpc::validParam {param} { if {[llength $param] != 2} { return 0 } return 1 } # Given a marshalled value, # unmarshall it and return it # proc xmlrpc::unmarshall {str} { set str [string trim $str] if {[string range $str 0 6] != ""} { # check for empty element if {[string range $str 0 7] != ""} { return [errReturn "Bad value tag"] } set rest [string range $str 8 end] set rest [string trim $rest] return [list $rest {}] } set str [string range $str 7 end] set str [string trimleft $str] set RE {<([^>]+)>} if {![regexp $RE $str {} btag]} { return [errReturn "No beginning tag found: $str"] } if {$btag == "int" || $btag == "i4"} { set res [umInt $str] } elseif {$btag== "boolean"} { set res [umBool $str] } elseif {$btag == "string"} { set res [umString $str] } elseif {$btag == "double"} { set res [umDouble $str] } elseif {$btag == "dateTime.iso8601"} { set res [umDateTime $str] } elseif {$btag == "base64"} { set res [umBase64 $str] } elseif {$btag == "array"} { set res [umArray $str] } elseif {$btag == "struct"} { set res [umStruct $str] } else { #check for empty element if {[string range $btag 0 1]=={/}} { set id [string first "]" $str ] if {$id != -1} { set rest [string range $str $id end] set rest [string trim $rest] return [list $rest {}] } } # return [errReturn "Unknown type: $str"] # assume string set id [string first "<" $str ] if {$id != -1} { set vv [string range $str 0 [expr $id-1]] set rr [string range $str $id end] set str "${vv}${rr}" set res [umString $str] } else { return [errReturn "Unknown type: $str"] } } set rest [lindex $res 0] set val [lindex $res 1] if {[string range $rest 0 7] != ""} { return [errReturn "Invalid close of value tag"] } set rest [string range $rest 8 end] set rest [string trim $rest] return [list $rest $val] } proc xmlrpc::umInt {str} { variable WS variable DIGIT set RE "<(int|i4)>$WS*"; # int tag append RE "(-*)($DIGIT+)$WS*"; # int value append RE "$WS*"; # end int tag append RE "(.*)"; # leftover if {![regexp $RE $str {} tag negp digits engtag rest]} { return [errReturn "Invalid Integer"] } if {$negp != ""} { set digits [expr -1 * $digits] } else { set digits [expr 1 * $digits] } set rest [string trim $rest] return [list $rest $digits] } proc xmlrpc::umBool {str} { variable WS set RE "$WS*"; # boolean tag append RE "(0|1)$WS*"; # boolean value append RE "$WS*"; # end boolean tag append RE "(.*)"; # leftover if {![regexp $RE $str {} bool rest]} { return [errReturn "Invalid Boolean"] } set rest [string trim $rest] return [list $rest $bool] } proc xmlrpc::umString {str} { variable WS set RE ""; # string tag append RE "(\[^<\]*)"; # string value append RE "$WS*"; # end string tag append RE "(.*)"; # leftover if {![regexp $RE $str {} s rest]} { return [errReturn "Invalid String"] } set rest [string trim $rest] return [list $rest $s] } proc xmlrpc::umDouble {str} { variable WS variable DIGIT set RE "$WS*"; # double tag append RE "(-*)($DIGIT*\.?$DIGIT*)$WS*"; # double value append RE "$WS*"; # end double tag append RE "(.*)"; # leftover if {![regexp $RE $str {} negp d rest]} { return [errReturn "Invalid Double"] } if {$negp != ""} { set d [expr -1 * $d] } else { set d [expr 1 * $d] } set rest [string trim $rest] return [list $rest $d] } proc xmlrpc::umDateTime {str} { variable WS variable DIGIT set RE "$WS*"; # dateTime tag append RE "($DIGIT+T$DIGIT+:$DIGIT+:$DIGIT+)$WS*"; # dateTime value append RE "$WS*"; # end string tag append RE "(.*)"; # leftover if {![regexp $RE $str {} dateTime rest]} { return [errReturn "Invalid DateTime"] } set rest [string trim $rest] return [list $rest $dateTime] } proc xmlrpc::umBase64 {str} { variable WS set RE ""; # string tag append RE "(\[^<\]*)"; # string value append RE "$WS*"; # end string tag append RE "(.*)"; # leftover if {![regexp $RE $str {} s rest]} { return [errReturn "Invalid Base64"] } set rest [string trim $rest] return [list $rest $s] } proc xmlrpc::umArray {str} { variable WS set RE "$WS*"; # array tag append RE "$WS*"; # data tag append RE "(.*)"; # leftover if {![regexp $RE $str {} rest]} { return [errReturn "Invalid Array"] } set l {} while {[string range $rest 0 6] == ""} { set res [unmarshall $rest] set rest [lindex $res 0] set el [lindex $res 1] lappend l $el } set REAREND "$WS*"; # end data tag append REAREND "$WS*"; # end array tag append REAREND "(.*)"; # leftover if {![regexp $REAREND $rest {} leftover]} { return [errReturn "Invalid End Array"] } return [list $leftover $l] } proc xmlrpc::umStruct {str} { variable WS variable W if {[string range $str 0 7] != ""} { return [errReturn "Invalid Struct"] } set RE "$WS*"; # name tag append RE "($W+?)$WS*"; # key append RE "$WS*"; # end name tag append RE "(.*)"; # value tag set l {} set str [string range $str 8 end] set str [string trim $str] while {[string range $str 0 7] == ""} { set str [string range $str 8 end] set str [string trim $str] if {![regexp $RE $str {} key val]} { return [errReturn "Invalid Struct Member"] } set res [unmarshall $val] set str [lindex $res 0] set el [lindex $res 1] lappend l [list $key $el] if {[string range $str 0 8] != ""} { return [errReturn "Invalid End Struct Member"] } set str [string range $str 9 end] set str [string trim $str] } if {[string range $str 0 8] != ""} { return [errReturn "Invalid End Struct"] } set str [string range $str 9 end] set str [string trim $str] return [list $str $l] } # Given a key, and a list of elements where each element is of the form: # {key, datum}, return {key, datum} if the requested key matches # a key in the list. # Returns the first match found in the list. # Return {} on failure # proc xmlrpc::assoc {key list} { foreach {cons} $list { set tkey [lindex $cons 0] if {[string tolower $key] == [string tolower $tkey]} { return $cons } } return {} } proc xmlrpc::warn {msg} { puts stderr $msg } proc xmlrpc::debug {msg} { variable DEBUG if {$DEBUG} { puts "$msg" } } proc xmlrpc::errReturn {msg} { warn $msg return -code error } proc xmlrpc::test {} { set person(first) {string "eric m"} set person(last) {string yeh} set employed(programmer) {struct person} #set xml [marshall {struct employed}] #set w [list {int 1}] #set q [list "array \{$w\}" {int 2} {string eric}] #puts [marshall "array \{$q\}"] #set xml [marshall {array {{int 1} {string {hello everybody}}}}] set xml [marshall {struct person}] debug "xml:\n$xml" set data [unmarshall $xml] debug "data: $data" set data [lindex $data 1] debug "data: $data" puts [assoc "first" $data] } #proc bgerror {error} { # global xmlcall # if {$xmlcall} { # global readdone # set readdone -1 # set xmlcall 0 # } #} #xmlrpc::test