From b246224b5a26343d3e3f3e97b7636968db6f3c3f Mon Sep 17 00:00:00 2001 From: Pat Thoyts Date: Thu, 19 Jun 2008 01:21:39 +0100 Subject: [PATCH 1/1] imported the vfs tree info a git repository Note that the lib/ tree contains copies of files from 3rd party products like the tcllib cvs and a few binaries for win32-ix86 (tls, udp and tdom). --- .gitignore | 4 + bin/bf_irc.tcl | 331 ++ bin/bf_xmpp.tcl | 1394 +++++++++ bin/bullfrog.tcl | 369 +++ bin/fscrolled.tcl | 96 + bin/history.tcl | 141 + bin/httpredir.tcl | 66 + bin/images/bullfrog48.gif | Bin 0 -> 1840 bytes bin/images/chat.gif | Bin 0 -> 855 bytes bin/images/dhd.gif | Bin 0 -> 346 bytes bin/images/dhn.gif | Bin 0 -> 91 bytes bin/images/dhu.gif | Bin 0 -> 337 bytes bin/images/mail.gif | Bin 0 -> 847 bytes bin/images/xhd.gif | Bin 0 -> 334 bytes bin/images/xhn.gif | Bin 0 -> 93 bytes bin/images/xhu.gif | Bin 0 -> 329 bytes bin/message.tcl | 193 ++ bin/tab.tcl | 251 ++ bin/test/demo.tcl | 281 ++ bin/test/irc.tcl | 347 +++ bin/test/muc-form.xml | 10 + bin/test/test.tcl | 55 + bin/test/z_irc.tcl | 227 ++ bullfrog.ico | Bin 0 -> 11502 bytes lib/autoproxy/autoproxy.tcl | 527 ++++ lib/autoproxy/pkgIndex.tcl | 2 + lib/autosocks/autosocks.tcl | 209 ++ lib/autosocks/pkgIndex.tcl | 2 + lib/base64/base64.tcl | 325 ++ lib/base64/pkgIndex.tcl | 14 + lib/chatwidget/ChangeLog | 26 + lib/chatwidget/chatwidget.man | 116 + lib/chatwidget/chatwidget.tcl | 737 +++++ lib/chatwidget/pkgIndex.tcl | 1 + lib/dns/dns.tcl | 1422 +++++++++ lib/dns/ip.tcl | 369 +++ lib/dns/ipMore.tcl | 1217 ++++++++ lib/dns/ipMoreC.tcl | 242 ++ lib/dns/msgs/en.msg | 8 + lib/dns/pkgIndex.tcl | 9 + lib/dns/resolv.tcl | 254 ++ lib/dns/spf.tcl | 533 ++++ lib/irc/irc.tcl | 521 ++++ lib/irc/picoirc.tcl | 261 ++ lib/irc/pkgIndex.tcl | 8 + lib/jabberlib/XMLFormat.tcl | 41 + lib/jabberlib/avatar.tcl | 807 +++++ lib/jabberlib/bind.tcl | 72 + lib/jabberlib/bytestreams.tcl | 1962 ++++++++++++ lib/jabberlib/caps.tcl | 530 ++++ lib/jabberlib/compress.tcl | 231 ++ lib/jabberlib/connect.tcl | 1109 +++++++ lib/jabberlib/data.tcl | 105 + lib/jabberlib/disco.tcl | 978 ++++++ lib/jabberlib/ftrans.tcl | 728 +++++ lib/jabberlib/groupchat.tcl | 161 + lib/jabberlib/ibb.tcl | 491 +++ lib/jabberlib/jabberlib.tcl | 4319 ++++++++++++++++++++++++++ lib/jabberlib/jingle.tcl | 719 +++++ lib/jabberlib/jlibdns.tcl | 187 ++ lib/jabberlib/jlibhttp.tcl | 688 ++++ lib/jabberlib/jlibsasl.tcl | 506 +++ lib/jabberlib/jlibtls.tcl | 217 ++ lib/jabberlib/muc.tcl | 655 ++++ lib/jabberlib/pep.tcl | 389 +++ lib/jabberlib/pkgIndex.tcl | 41 + lib/jabberlib/pubsub.tcl | 709 +++++ lib/jabberlib/readme | 12 + lib/jabberlib/roster.tcl | 1659 ++++++++++ lib/jabberlib/saslmd5.tcl | 484 +++ lib/jabberlib/scripts/README-scripts | 11 + lib/jabberlib/scripts/message.tcl | 104 + lib/jabberlib/scripts/password.tcl | 105 + lib/jabberlib/scripts/pkgIndex.tcl | 13 + lib/jabberlib/scripts/register.tcl | 118 + lib/jabberlib/scripts/unregister.tcl | 98 + lib/jabberlib/service.tcl | 326 ++ lib/jabberlib/si.tcl | 729 +++++ lib/jabberlib/sipub.tcl | 307 ++ lib/jabberlib/stanzaerror.tcl | 64 + lib/jabberlib/streamerror.tcl | 75 + lib/jabberlib/tinydom.tcl | 159 + lib/jabberlib/util.tcl | 66 + lib/jabberlib/vcard.tcl | 449 +++ lib/jabberlib/wrapper.tcl | 1062 +++++++ lib/log/log.tcl | 851 +++++ lib/log/logger.tcl | 1206 +++++++ lib/log/loggerAppender.tcl | 449 +++ lib/log/loggerUtils.tcl | 544 ++++ lib/log/loggerperformance | 79 + lib/log/msgs/en.msg | 7 + lib/log/pkgIndex.tcl | 9 + lib/md5/md5.tcl | 454 +++ lib/md5/md5x.tcl | 714 +++++ lib/md5/pkgIndex.tcl | 3 + lib/sha1/pkgIndex.tcl | 14 + lib/sha1/sha1.tcl | 818 +++++ lib/sha1/sha1v1.tcl | 713 +++++ lib/tclxml3.1/pkgIndex.tcl | 97 + lib/tclxml3.1/sgml-8.1.tcl | 143 + lib/tclxml3.1/sgmlparser.tcl | 2816 +++++++++++++++++ lib/tclxml3.1/tclparser-8.1.tcl | 612 ++++ lib/tclxml3.1/xml-8.1.tcl | 133 + lib/tclxml3.1/xml__tcl.tcl | 270 ++ lib/tclxml3.1/xmldep.tcl | 179 ++ lib/tclxml3.1/xpath.tcl | 362 +++ lib/tdom/pkgIndex.tcl | 6 + lib/tdom/tdom.tcl | 911 ++++++ lib/tdom/win32-ix86/tdom083.dll | Bin 0 -> 611328 bytes lib/tls/pkgIndex.tcl | 6 + lib/tls/tls.tcl | 250 ++ lib/tls/win32-ix86/tls16.dll | Bin 0 -> 715776 bytes lib/tooltip/pkgIndex.tcl | 4 + lib/tooltip/tipstack.tcl | 169 + lib/tooltip/tooltip.tcl | 414 +++ lib/udp1.0.9/pkgIndex.tcl | 4 + lib/udp1.0.9/win32-ix86/udp109.dll | Bin 0 -> 15360 bytes lib/uri/pkgIndex.tcl | 6 + lib/uri/uri.tcl | 1034 ++++++ lib/uri/urn-scheme.tcl | 136 + lib/uuid/pkgIndex.tcl | 8 + lib/uuid/uuid.tcl | 216 ++ main.tcl | 13 + nokit.tcl | 7 + tclkit.ico | Bin 0 -> 10134 bytes tclkit.inf | 5 + 126 files changed, 45716 insertions(+) create mode 100644 .gitignore create mode 100644 bin/bf_irc.tcl create mode 100644 bin/bf_xmpp.tcl create mode 100644 bin/bullfrog.tcl create mode 100644 bin/fscrolled.tcl create mode 100644 bin/history.tcl create mode 100644 bin/httpredir.tcl create mode 100644 bin/images/bullfrog48.gif create mode 100644 bin/images/chat.gif create mode 100644 bin/images/dhd.gif create mode 100644 bin/images/dhn.gif create mode 100644 bin/images/dhu.gif create mode 100644 bin/images/mail.gif create mode 100644 bin/images/xhd.gif create mode 100644 bin/images/xhn.gif create mode 100644 bin/images/xhu.gif create mode 100644 bin/message.tcl create mode 100644 bin/tab.tcl create mode 100644 bin/test/demo.tcl create mode 100644 bin/test/irc.tcl create mode 100644 bin/test/muc-form.xml create mode 100644 bin/test/test.tcl create mode 100644 bin/test/z_irc.tcl create mode 100644 bullfrog.ico create mode 100644 lib/autoproxy/autoproxy.tcl create mode 100644 lib/autoproxy/pkgIndex.tcl create mode 100644 lib/autosocks/autosocks.tcl create mode 100644 lib/autosocks/pkgIndex.tcl create mode 100644 lib/base64/base64.tcl create mode 100644 lib/base64/pkgIndex.tcl create mode 100644 lib/chatwidget/ChangeLog create mode 100644 lib/chatwidget/chatwidget.man create mode 100644 lib/chatwidget/chatwidget.tcl create mode 100644 lib/chatwidget/pkgIndex.tcl create mode 100644 lib/dns/dns.tcl create mode 100644 lib/dns/ip.tcl create mode 100644 lib/dns/ipMore.tcl create mode 100644 lib/dns/ipMoreC.tcl create mode 100644 lib/dns/msgs/en.msg create mode 100644 lib/dns/pkgIndex.tcl create mode 100644 lib/dns/resolv.tcl create mode 100644 lib/dns/spf.tcl create mode 100644 lib/irc/irc.tcl create mode 100644 lib/irc/picoirc.tcl create mode 100644 lib/irc/pkgIndex.tcl create mode 100644 lib/jabberlib/XMLFormat.tcl create mode 100644 lib/jabberlib/avatar.tcl create mode 100644 lib/jabberlib/bind.tcl create mode 100644 lib/jabberlib/bytestreams.tcl create mode 100644 lib/jabberlib/caps.tcl create mode 100644 lib/jabberlib/compress.tcl create mode 100644 lib/jabberlib/connect.tcl create mode 100644 lib/jabberlib/data.tcl create mode 100644 lib/jabberlib/disco.tcl create mode 100644 lib/jabberlib/ftrans.tcl create mode 100644 lib/jabberlib/groupchat.tcl create mode 100644 lib/jabberlib/ibb.tcl create mode 100644 lib/jabberlib/jabberlib.tcl create mode 100644 lib/jabberlib/jingle.tcl create mode 100644 lib/jabberlib/jlibdns.tcl create mode 100644 lib/jabberlib/jlibhttp.tcl create mode 100644 lib/jabberlib/jlibsasl.tcl create mode 100644 lib/jabberlib/jlibtls.tcl create mode 100644 lib/jabberlib/muc.tcl create mode 100644 lib/jabberlib/pep.tcl create mode 100644 lib/jabberlib/pkgIndex.tcl create mode 100644 lib/jabberlib/pubsub.tcl create mode 100644 lib/jabberlib/readme create mode 100644 lib/jabberlib/roster.tcl create mode 100644 lib/jabberlib/saslmd5.tcl create mode 100644 lib/jabberlib/scripts/README-scripts create mode 100644 lib/jabberlib/scripts/message.tcl create mode 100644 lib/jabberlib/scripts/password.tcl create mode 100644 lib/jabberlib/scripts/pkgIndex.tcl create mode 100644 lib/jabberlib/scripts/register.tcl create mode 100644 lib/jabberlib/scripts/unregister.tcl create mode 100644 lib/jabberlib/service.tcl create mode 100644 lib/jabberlib/si.tcl create mode 100644 lib/jabberlib/sipub.tcl create mode 100644 lib/jabberlib/stanzaerror.tcl create mode 100644 lib/jabberlib/streamerror.tcl create mode 100644 lib/jabberlib/tinydom.tcl create mode 100644 lib/jabberlib/util.tcl create mode 100644 lib/jabberlib/vcard.tcl create mode 100644 lib/jabberlib/wrapper.tcl create mode 100644 lib/log/log.tcl create mode 100644 lib/log/logger.tcl create mode 100644 lib/log/loggerAppender.tcl create mode 100644 lib/log/loggerUtils.tcl create mode 100644 lib/log/loggerperformance create mode 100644 lib/log/msgs/en.msg create mode 100644 lib/log/pkgIndex.tcl create mode 100644 lib/md5/md5.tcl create mode 100644 lib/md5/md5x.tcl create mode 100644 lib/md5/pkgIndex.tcl create mode 100644 lib/sha1/pkgIndex.tcl create mode 100644 lib/sha1/sha1.tcl create mode 100644 lib/sha1/sha1v1.tcl create mode 100644 lib/tclxml3.1/pkgIndex.tcl create mode 100644 lib/tclxml3.1/sgml-8.1.tcl create mode 100644 lib/tclxml3.1/sgmlparser.tcl create mode 100644 lib/tclxml3.1/tclparser-8.1.tcl create mode 100644 lib/tclxml3.1/xml-8.1.tcl create mode 100644 lib/tclxml3.1/xml__tcl.tcl create mode 100644 lib/tclxml3.1/xmldep.tcl create mode 100644 lib/tclxml3.1/xpath.tcl create mode 100644 lib/tdom/pkgIndex.tcl create mode 100644 lib/tdom/tdom.tcl create mode 100644 lib/tdom/win32-ix86/tdom083.dll create mode 100644 lib/tls/pkgIndex.tcl create mode 100644 lib/tls/tls.tcl create mode 100644 lib/tls/win32-ix86/tls16.dll create mode 100644 lib/tooltip/pkgIndex.tcl create mode 100644 lib/tooltip/tipstack.tcl create mode 100644 lib/tooltip/tooltip.tcl create mode 100644 lib/udp1.0.9/pkgIndex.tcl create mode 100644 lib/udp1.0.9/win32-ix86/udp109.dll create mode 100644 lib/uri/pkgIndex.tcl create mode 100644 lib/uri/uri.tcl create mode 100644 lib/uri/urn-scheme.tcl create mode 100644 lib/uuid/pkgIndex.tcl create mode 100644 lib/uuid/uuid.tcl create mode 100644 main.tcl create mode 100644 nokit.tcl create mode 100644 tclkit.ico create mode 100644 tclkit.inf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29763f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Ignore all CVS dirs and emacs temp copies. +CVS +.#* +*~ diff --git a/bin/bf_irc.tcl b/bin/bf_irc.tcl new file mode 100644 index 0000000..4be8638 --- /dev/null +++ b/bin/bf_irc.tcl @@ -0,0 +1,331 @@ +# bf_irc.tcl -- Copyright (C) 2008 Pat Thoyts +# +# Handle the IRC transport (using picoirc) +# +# + +package require picoirc 0.5; # tcllib + +variable ircuid +if {![info exists ircuid]} { set ircuid -1 } + +proc IrcLogin {app} { + set dlg $app.irclogin + variable $dlg {} + variable irc + if {![info exists irc]} { + array set irc {server irc.freenode.net port 6667 channel "" passwd ""} + } + if {![winfo exists $dlg]} { + set dlg [toplevel $dlg -class Dialog] + wm withdraw $dlg + wm transient $dlg $app + wm title $dlg "IRC Login" + + set f [ttk::frame $dlg.f] + set g [ttk::frame $f.g] + ttk::label $f.sl -text Server -anchor w + ttk::entry $f.se -textvariable [namespace which -variable irc](server) + ttk::entry $f.sp -textvariable \ + [namespace which -variable irc](port) -width 5 + ttk::label $f.nl -text Username -anchor w + ttk::entry $f.nn -textvariable [namespace which -variable irc](nick) + ttk::label $f.pl -text Password -anchor w + ttk::entry $f.pw -show * -textvariable [namespace which -variable irc](passwd) + ttk::label $f.cl -text Channel -anchor w + ttk::entry $f.cn -textvariable [namespace which -variable irc](channel) + ttk::button $f.ok -text Login -default active \ + -command [list set [namespace which -variable $dlg] "ok"] + ttk::button $f.cancel -text Cancel \ + -command [list set [namespace which -variable $dlg] "cancel"] + + bind $dlg [list $f.ok invoke] + bind $dlg [list $f.cancel invoke] + wm protocol $dlg WM_DELETE_WINDOW [list $f.cancel invoke] + + grid $f.sl $f.se $f.sp -in $g -sticky new -padx 1 -pady 1 + grid $f.nl $f.nn - -in $g -sticky new -padx 1 -pady 1 + grid $f.pl $f.pw - -in $g -sticky new -padx 1 -pady 1 + grid $f.cl $f.cn - -in $g -sticky new -padx 1 -pady 1 + grid columnconfigure $g 1 -weight 1 + + grid $g - -sticky news + grid $f.ok $f.cancel -sticky e -padx 1 -pady 1 + grid rowconfigure $f 0 -weight 1 + grid columnconfigure $f 0 -weight 1 + + grid $f -sticky news + grid rowconfigure $dlg 0 -weight 1 + grid columnconfigure $dlg 0 -weight 1 + + wm resizable $dlg 0 0 + raise $dlg + } + + catch {::tk::PlaceWindow $dlg widget $app} + wm deiconify $dlg + tkwait visibility $dlg + focus -force $dlg.f.ok + grab $dlg + vwait [namespace which -variable $dlg] + grab release $dlg + wm withdraw $dlg + + if {[set $dlg] eq "ok"} { + after idle [list [namespace origin IrcConnect] $app \ + -server $irc(server) -port $irc(port) \ + -channel $irc(channel) \ + -nick $irc(nick) -passwd $irc(passwd)] + } +} + +proc IrcConnect {app args} { + variable ircuid + set id irc[incr ircuid] + set Chat [namespace current]::$id + upvar #0 $Chat chat + array set chat [list app $app type irc passwd "" nick ""] + while {[string match -* [set option [lindex $args 0]]]} { + switch -exact -- $option { + -server { set chat(server) [Pop args 1] } + -port { set chat(port) [Pop args 1] } + -channel { set chat(channel) [Pop args 1] } + -nick { set chat(nick) [Pop args 1] } + -passwd { set chat(passwd) [Pop args 1] } + default { + return -code error "invalid option \"$option\"" + } + } + Pop args + } + set chat(window) [chatwidget::chatwidget $app.nb.$id] + $chat(window) names hide + set chat(targets) [list] + set url irc://$chat(server):$chat(port) + if {[info exists chat(channel)] && $chat(channel) ne ""} { + append url /$chat(channel) + } + set chat(irc) [picoirc::connect \ + [list [namespace origin IrcCallback] $Chat] \ + $chat(nick) $chat(passwd) $url] + $chat(window) hook add post [list ::picoirc::post $chat(irc) ""] + bind $chat(window) "+unset -nocomplain $Chat" + $app.nb add $chat(window) -text $chat(server) + after idle [list $app.nb select $chat(window)] + return $Chat +} + +proc IrcJoinChannel {Chat args} { + variable ircuid + # FIX ME: +} + +proc IrcAddChannel {Chat channel} { + upvar #0 $Chat chat + set Channel "${Chat}/$channel" + upvar #0 $Channel chan + array set chan [array get chat] + set chan(channel) $channel + set chan(window) [chatwidget::chatwidget $chat(window)$channel] + lappend chat(targets) [list $channel $chan(window)] + set m0 [font measure ChatwidgetFont {[00:00]m}] + set m1 [font measure ChatwidgetFont [string repeat m 10]] + set mm [expr {$m0 + $m1}] + $chan(window) chat configure -tabs [list $m0 $mm] + $chan(window) chat tag configure MSG -lmargin1 $mm -lmargin2 $mm + $chan(window) chat tag configure NICK -font ChatwidgetBoldFont + $chan(window) chat tag configure TYPE-system -font ChatwidgetItalicFont + $chan(window) chat tag bind URL [list UrlEnter %W] + $chan(window) chat tag bind URL [list UrlLeave %W] + $chan(window) chat tag bind URL [list UrlClick %W %x %y] + $chan(window) names tag bind NICK \ + [list [namespace origin IrcChannelNickMenu] $Channel %W %x %y] + $chan(window) names tag bind NICK \ + [list [namespace origin IrcNickTooltip] $Chat enter %W %x %y] + $chan(window) names tag bind NICK \ + [list [namespace origin IrcNickTooltip] $Chat leave %W %x %y] + $chan(window) hook add post [list ::picoirc::post $chan(irc) $channel] + bind $chan(window) "+unset -nocomplain $Channel" + $chat(app).nb add $chan(window) -text $channel + after idle [list $chat(app).nb select $chan(window)] + return +} + +proc IrcRemoveChannel {Chat target} { + upvar #0 $Chat chat + Status $Chat "Left channel $target" + set w [IrcFindWindow $Chat $target] + if {[winfo exists $w]} { destroy $w } + if {[set ndx [lsearch -index 0 $chat(targets) $target]] != -1} { + set chat(targets) [lreplace $chat(targets) $ndx $ndx] + } +} + +proc IrcChannelNickMenu {Channel w x y} { + set nick [string trim [$w get "@$x,$y linestart" "@$x,$y lineend"]] + if {$nick eq ""} { return } + destroy $w.popup + set m [menu $w.popup -tearoff 0] + $m add command -label "$nick" -state disabled + $m add separator + $m add command -label "Whois" -underline 0 \ + -command [list [namespace origin IrcChannelNickCommand] $Channel whois $nick] + $m add command -label "Version" \ + -command [list [namespace origin IrcChannelNickCommand] $Channel version $nick] + tk_popup $m [winfo pointerx $w] [winfo pointery $w] +} + +proc IrcChannelNickCommand {Channel cmd nick} { + upvar #0 $Channel chan + switch -exact -- $cmd { + whois { picoirc::send $chan(irc) "WHOIS $nick" } + version { picoirc::send $chan(irc) "PRIVMSG $nick :\001VERSION\001" } + default {} + } +} + +proc IrcNickTooltip {Chat type w x y} { + if {[package provide tooltip] eq {}} { return } + set nick [string trim [$w get "@$x,$y linestart" "@$x,$y lineend"]] + if {$nick eq ""} { return } + puts stderr "Tooltip $type $nick" + return +} + +proc IrcFindWindow {Chat target} { + upvar #0 $Chat chat + set w $chat(window) + if {[set ndx [lsearch -nocase -index 0 $chat(targets) $target]] != -1} { + set w [lindex [lindex $chat(targets) $ndx] 1] + } + return $w +} + +proc IrcCallback {Chat context state args} { + upvar #0 $Chat chat + upvar #0 $context irc + switch -exact -- $state { + init { + Status $Chat "Attempting to connect to $irc(server)" + } + connect { + $chat(window) message "Logging into $irc(server) as $irc(nick)" -type system + Status $Chat "Connection to IRC server established." + State $Chat connected + } + close { + if {[llength $args] != 0} { + $chat(window) message "Failed to connect: [lindex $args 0]" -type system + Status $Chat [lindex $args 0] + } else { + $chat(window) message "Disconnected from server" -type system + Status $Chat "Disconnected." + } + State $Chat !connected + } + userlist { + foreach {target users} $args break + set colors {black SteelBlue4 tomato chocolate SeaGreen4 red4 + green4 blue4 pink4} + set w [IrcFindWindow $Chat $target] + set current [$w name list -full] + foreach nick $users { + set opts [list -status online] + if {[string match @* $nick]} { + set nick [string range $nick 1 end] + lappend opts -group operators + } else { lappend opts -group users } + if {[lsearch -index 0 $current $nick] == -1} { + lappend opts -color \ + [lindex $colors [expr {int(rand() * [llength $colors])}]] + } + eval [list $w name add $nick] $opts + } + } + userinfo { + foreach {nick userinfo} $args break + array set info {name {} host {} channels {} userinfo {}} + array set info $userinfo + set chat(userinfo,$nick) [array get info] + } + chat { + foreach {target nick msg type} $args break + if {$type eq ""} {set type normal} + set w [IrcFindWindow $Chat $target] + if {$nick eq "tcl@tach.tclers.tk"} { + set action ""; set jnick "" ; set jnew "" + if {[regexp {^\s*([^ ]+) is now known as (.*)} $msg -> jnick jnew]} { + set action nickchange + } elseif {[regexp {^\s*([^ ]+) has left} $msg -> jnick]} { + set action left + } elseif {[regexp {^\s*([^ ]+) has become available} $msg -> jnick]} { + set action entered + } + if {$action ne ""} { + IrcCallbackNick $w $action $target $jnick $jnew jabber + return + } + } + $w message $msg -nick $nick -type $type + } + system { + foreach {target msg} $args break + [IrcFindWindow $Chat $target] message $msg -type system + } + topic { + foreach {target topic} $args break + set w [IrcFindWindow $Chat $target] + $w topic show + $w topic set $topic + } + traffic { + foreach {action target nick new} $args break + if {$nick eq $irc(nick)} { + switch -exact -- $action { + left { IrcRemoveChannel $Chat $target } + entered { IrcAddChannel $Chat $target} + nickchange { set irc(nick) $new } + } + } + if {$target ne {}} { + set w [IrcFindWindow $Chat $target] + IrcCallbackNick $w $action $target $nick $new + } else { + foreach window_target $chat(targets) { + foreach {window_channel w} $window_target break + set current [$w name list -full] + if {[lsearch -index 0 $current $nick] != -1} { + IrcCallbackNick $w $action $target $nick $new + } + } + } + } + debug { + foreach {type line} $args break + Debug $Chat $line $type + + # You can log raw IRC to file by uncommenting the following lines: + #if {![info exists chat(log)]} {set chat(log) [open irc.log a]} + #puts $chat(log) "[string toupper [string range $type 0 0]] $line" + } + version { return "" } + default { + $chat(window) message "unknown irc callback \"$state\": $args" -type error + } + } +} + +proc IrcCallbackNick {w action target nick new {group users}} { + #puts stderr "process traffic $w $nick $action $new $target" + if {$action eq "nickchange"} { + $w name delete $nick + $w name add $new -group $group + $w message "$nick changed to $new" -type system + } else { + switch -exact -- $action { + left { $w name delete $nick } + entered { $w name add $nick -group $group } + } + $w message "$nick $action" -type system + } +} diff --git a/bin/bf_xmpp.tcl b/bin/bf_xmpp.tcl new file mode 100644 index 0000000..d9a984d --- /dev/null +++ b/bin/bf_xmpp.tcl @@ -0,0 +1,1394 @@ +# Present a callback interface akin to the picoirc callback. +# The idea is to have the picoirc application be able to use multiple transports +# with only the callback being the comms interface. +# +# +# TODO: +# +# One-to-one chats should show the nick name - either from the +# additional elements in the message stanza, or the resource if from a +# groupchat source or just the node. +# They also don't need a names window. +# We must echo our own messages in such a window. +# +# :) +# +# +# +package require jlib +package require jlib::connect +package require jlib::disco +package require jlib::roster +package require jlib::muc +package require jlib::caps +package require jlib::vcard + +package require uuid +package require messagewidget + +namespace eval ::xmppplugin { + variable version 0.2 + variable uid; if {![info exists uid]} { set uid 0 } + variable defaults { + -server "all.tclers.tk" + -resource "Bullfrog" + -port 5222 + -channel "tcl@tach.tclers.tk" + callback "" + motd {} + users {} + } + namespace export connect send post splituri +} + +proc ::xmppplugin::connect {callback args} { + variable defaults + variable uid + set context [namespace current]::xmpp[incr uid] + upvar #0 $context xmpp + array set xmpp $defaults + array set xmpp $args ;# see XmppLogin for the list of pairs + + set xmpp(callback) $callback + set xmpp(jlib) [jlib::new [namespace origin OnNetwork] \ + -messagecommand [namespace origin OnMessage] \ + -presencecommand [namespace origin OnPresence] \ + -iqcommand [namespace origin OnIq] \ + -keepalivesecs 90 \ + -autodiscocaps 1] + + # IQ handlers + $xmpp(jlib) iq_register get jabber:iq:version \ + [namespace code [list OnIqVersion $context]] 40 + $xmpp(jlib) iq_register get jabber:iq:last \ + [namespace code [list OnIqLast $context]] 40 + $xmpp(jlib) iq_register result jabber:iq:version \ + [namespace code [list OnIqVersionResult $context]] 40 + + # Presence handlers + $xmpp(jlib) roster register_cmd [list [namespace origin OnRosterChange] $context] + $xmpp(jlib) presence_register available \ + [namespace code [list OnPresenceChange $context]] + $xmpp(jlib) presence_register unavailable \ + [namespace code [list OnPresenceChange $context]] + + # Discovery support + $xmpp(jlib) disco registeridentity client pc Bullfrog + foreach feature {jabber:client jabber:iq:last jabber:iq:time \ + jabber:iq:version jabber:x:event} { + jlib::disco::registerfeature $feature + } + + #$xmpp(jlib) caps register ? ? ? + set [set xmpp(jlib)]::AppContext $context + Callback $context init + set xmpp(jid) [jlib::joinjid $xmpp(-username) $xmpp(-server) $xmpp(-resource)] + $xmpp(jlib) connect init + $xmpp(jlib) connect configure -defaultresource $xmpp(-resource) + + if {$xmpp(-useproxy)} { + # this one traverses an http proxy: + if {$xmpp(-connect) eq "plain"} {set method sasl} else {set method ssl} + $xmpp(jlib) connect connect $xmpp(jid) $xmpp(-passwd) \ + -command [list [namespace origin OnConnect] $context] \ + -secure 1 -method $method -transport tunnel\ + -ip $xmpp(-server) -port $xmpp(-port) + } else { + $xmpp(jlib) connect connect $xmpp(jid) $xmpp(-passwd) \ + -command [list [namespace origin OnConnect] $context] \ + -secure 1 -method tlssasl \ + -ip $xmpp(-server) -port $xmpp(-port) + } + return $context +} + +proc ::xmppplugin::post {ctx channel msg} { + upvar #0 $ctx xmpp + if {[string match "/*" $msg]} { + switch -glob -- $msg { + "/join *" { + set target [string trim [string range $msg 6 end]] + JoinMUC $ctx $target $xmpp(jlib) $xmpp(-nick) + return + } + "/part*" - "/close*" { + set target [string trim [string range $msg 6 end]] + if {$target eq ""} {set target $channel} + if {[$xmpp(jlib) muc isroom $target]} { + $xmpp(jlib) muc exit $target ;# no -command it seems + } else { + #$xmpp(jlib) send_presence -type unavailable -to who + } + Callback $ctx close $target + return + } + "/nick *" { + set nick [string trim [string range $msg 6 end]] + $xmpp(jlib) muc setnick $channel $nick + set xmpp(-nick) $nick ;# should be ony if it works. + Callback $ctx userlist muc $target + return + } + "/users*" - "/userlist*" { + set target $channel + regexp {^/users?(?:list)?(?:\s+(.*))?$} $msg -> target + if {[$xmpp(jlib) muc isroom $target]} { + Callback $ctx userlist muc $target + } else { + Callback $ctx userlist roster + } + return + } + "/create *" { + set target [string trim [string range $msg 8 end]] + $xmpp(jlib) muc create $target $xmpp(-nick) \ + [namespace code [list OnMucCreate $ctx $target]] + return + } + "/invite *" { + if {[regexp {^/invite\s+(\S+)\s+(.*)$} $msg -> who reason]} { + $xmpp(jlib) muc invite $channel $who -reason $reason + } else { + Callback $ctx system $channel "usage: /invite jid ?reason?" + } + return + } + "/me *" { } + default { + Callback $ctx system $channel "unrecognised chat command '$msg'" + return + } + } + } + + # Check the type of channel and for one-to-one chat re-use the thread + # or create a thread if this is a new conversation. + set type groupchat + set thread "" + if {![$xmpp(jlib) muc isroom $channel]} { + set type chat + catch {set thread [dict get $xmpp(opts) $channel -thread]} + if {$thread eq ""} {set thread [uuid::uuid generate] } + } + + if {[catch {dict get $xmll(opts) $channel -chatstate} chatstate]} { + set chatstate active + } + + lappend xlist [wrapper::createtag x \ + -attrlist {xmlns urn:tkchat:chat color 387070}] + if {$chatstate ne {}} { + lappend xlist [wrapper::createtag $chatstate \ + -attrlist {xmlns http://jabber.org/protocol/chatstates}] + } + set margs [list -type $type -body $msg -xlist $xlist] + if {$thread ne ""} { lappend margs -thread $thread } + eval [linsert $margs 0 $xmpp(jlib) send_message $channel] + if {$type eq "chat"} { + set mtype normal + if {[string match "/me *" $msg]} { set mtype action } + Callback $ctx chat $channel $xmpp(-nick) $msg $mtype + } +} + +proc ::xmppplugin::Callback {ctx state args} { + upvar #0 $ctx xmpp + if {[llength $xmpp(callback)] > 0 + && [llength [info commands [lindex $xmpp(callback) 0]]] == 1} { + if {[catch {eval $xmpp(callback) [list $ctx $state] $args} err]} { + puts stderr "callback error \"$state\": $err" + } + } +} + +proc ::xmppplugin::Version {ctx} { + global tcl_platform + if {[catch {Callback $ctx version} ver]} { set ver {} } + if {$ver eq {}} { + set os $tcl_platform(os) + if {[info exists tcl_platform(osVersion)]} { + append os " $tcl_platform(osVersion)" + } + append os "/Tcl [info patchlevel]" + set os [string map {":" ";"} $os] + set ver "Bullfrog:[package provide xmppplugin]:$os" + } + return $ver +} + +proc ::xmppplugin::get_caps {ctx} { + foreach {name ver os} [split [Version $ctx] :] break + set caps [wrapper::createtag c -attrlist \ + [list xmlns "http://jabber.org/protocol/caps" \ + node "http://tkchat.tclers.tk/$name/caps" \ + ver $ver ext {color time}]] + return $caps +} + +proc ::xmppplugin::Log {ctx msg {type {}}} { + Callback $ctx debug system $msg +} + +proc ::xmppplugin::OnAnything {ctx args} { + Log $ctx "Anything: $args" +} + +proc ::xmppplugin::OnNetwork {jlib cmd args} { + puts stderr "-- OnNetwork $jlib $cmd $args" + set ctx [set [set jlib]::AppContext] + if {[catch { + array set a [linsert $args 0 -body {} -errormsg {}] + switch -glob -- $cmd { + connect { Log $ctx "* connected" } + disconnect { + Log $ctx "* disconnected" + Callback $ctx disconnect "disconnected" + } + networkerror { + Log $ctx "* Network error: $a(-body)" + Callback $ctx disconnect "network error: $a(-body)" + } + xmpp-streams-error-* - streamerror { + Log $ctx "* Stream error: $a(-errormsg)" + Callback $ctx disconnect "stream error: $a(-errormsg)" + } + xmlerror { + Log $ctx "* XML parse error: $a(-errormsg)" + Callback $ctx disconnect "xml error: $a(-errormsg)" + } + default { Log $ctx "* Default: $cmd $args" } + } + } err]} { Log $ctx "OnNetwork: $err" error } +} + +proc ::xmppplugin::OnConnect {ctx jlib type args} { + Log $ctx "OnConnect $ctx $jlib $type $args" + upvar #0 $ctx xmpp + switch -exact -- $type { + initnetwork { Log $ctx "$type $args" } + initstream { Log $ctx "$type $args" } + authenticate { Log $ctx "$type $args" } + ok { + Callback $ctx connect + $jlib send_presence + $jlib send_presence -priority 2 -extras [list [get_caps $ctx]] + $jlib roster send_get -command [list [namespace origin OnRosterGet] $ctx] + if {$xmpp(-autoconnect) && $xmpp(-channel) ne {}} { + Log $ctx "Attempting to join $xmpp(-channel)" + JoinMUC $ctx $xmpp(-channel) $jlib $xmpp(-nick) + } + } + error { + Log $ctx "network error: $args" + Callback $ctx close + Callback $ctx disconnect $args + } + } +} + +proc ::xmppplugin::JoinMUC {ctx channel jlib nick} { + #set t 1202713200 + #set since [list since [clock format $t -format {%Y-%m-%dT%T}]] + set since [list maxstanzas 100] + set x [wrapper::createtag x \ + -attrlist {xmlns http://jabber.org/protocol/muc} \ + -subtags [list [wrapper::createtag history \ + -attrlist $since]]] + Callback $ctx addchat $channel groupchat + $jlib muc enter $channel $nick -extras [list $x] \ + -command [list [namespace origin OnMucEnter] $ctx $channel] +} + +proc ::xmppplugin::OnMucEnter {ctx channel jlib xmldata} { + if {[catch { + #puts stderr "MucEnter: $xmldata" + switch -exact -- [wrapper::getattribute $xmldata type] { + "" - available { + Log $ctx "Joined $channel" + Callback $ctx traffic joining $channel + after idle [list [namespace origin Callback] \ + $ctx userlist muc $channel] + # FIX ME: cause history loading + # FIX ME: send custom presence to the conference for auto-away? + } + error { + set e [wrapper::getfirstchildwithtag $xmldata error] + set code [wrapper::getattribute $e code] + set msg {} + switch -exact -- $code { + 401 { set msg "This conference is password protected." } + 403 { set msg "You have been banned from this conference." } + 404 { set msg "The requested server does not exist." } + 405 { set msg "The maximum number of participants has been reached."} + 407 { set msg "You must be a member to enter this conference." } + 409 { + # nick conflict + upvar #0 $ctx xmpp + set n 0 ; set nick $xmpp(-nick) + regexp {^(.*)/(\d+)$} $nick -> nick n + set xmpp(-nick) $nick/[incr $n] + JoinMUC $ctx $channel $jlib $xmpp(-nick) + } + default { + set msg "An unknown error was returned on attempting to join the\ + conference." + } + } + if {$msg ne {}} { + tk_messageBox -icon error -title "Failed to join conference" \ + -message $msg + } + } + } + } err]} { + puts stderr "OnMucEnter: $err {$ctx $channel}" + } +} +proc ::xmppplugin::OnMucCreate {ctx channel jlib xmldata} { + if {[catch { + Log $ctx "MucCreate $xmldata" + set x [wrapper::getchildswithtagandxmlns $xmldata x \ + "http://jabber.org/protocol/muc#user"] + if {[llength $x] > 0} { + set status [wrapper::getchildswithtag [lindex $x 0] status] + array set s [linsert [wrapper::getattrlist [lindex $status 0]] 0 code 0] + switch -exact -- $s(code) { + 201 { + $jlib muc getroom $channel \ + [namespace code [list OnMucConfigure $ctx $channel]] + } + default { + Callback $ctx system $channel "muc create code $s(code)!" + } + } + } + } err]} { puts stderr "OnMucCreate $err" } +} +proc ::xmppplugin::OnMucConfigure {ctx channel jlib type subiq} { + if {[catch { + set form [lindex [wrapper::getchildswithtagandxmlns $subiq x jabber:x:data] 0] + #set r [ShowForm $form] + $jlib muc setroom $channel submit -form [list $form] \ + -command [namespace code [list OnMucConfigured $ctx $channel]] + } err]} { puts stderr "OnMucConfigure $err" } +} +proc ::xmppplugin::OnMucConfigured {ctx channel jlib type subiq} { + upvar #0 $ctx xmpp + if {[catch { + $jlib muc enter $channel $xmpp(-nick) \ + -command [list [namespace origin OnMucEnter] $ctx $channel] + } err]} { puts stderr "OnMucConfigured $err" } +} + +proc ::xmppplugin::ShowForm {ctx form} { + set dlg [toplevel .xmppform -class Dialog] + wm title $dlg "Configure room" + wm withdraw $dlg + set f [ttk::frame $dlg.f] + set wid 0 + foreach field [wrapper::getchildren $form] { + set ftag [wrapper::gettag $field] + puts "$ftag" + switch -exact -- [wrapper::gettag $field] { + title { wm title $dlg [wrapper::getcdata $field] } + instructions { + set w [ttk::label $f.w[incr wid] -text [wrapper::getcdata $field]] + grid $w - -sticky news + } + field { + array set a [linsert [wrapper::getattrlist $field] 0 type {}] + switch -exact -- $a(type) { + hidden {} + text-single {} + text-multi {} + fixed { + set txt {} + foreach node [wrapper::getchildswithtag $field "value"] { + lappend txt [wrapper::getcdata $node] + } + set w [ttk::label $f.w[incr wid] -text [join $txt "\n"] + grid $w - -sticky news + } + boolean { + set w [ttk::checkbutton $f.w[incr wid] -text $a(label)] + grid $w - -sticky news + } + list-single {} + + } + } + } + } + set b0 [ttk::button $f.ok -text OK -default active \ + -command [list set [namespace current]::$dlg ok]] + set b1 [ttk::button $f.cn -text Cancel -default normal \ + -command [list set [namespace current]::$dlg cancel]] + + grid $b0 $b1 -sticky e + grid rowconfigure $f [incr n] -weight 1 + grid columnconfigure $f 2 -weight 1 + grid $f -sticky news + grid rowconfigure $dlg 0 -weight 1 + grid columnconfigure $dlg 0 -weight 1 + + bind $dlg [list $b0 invoke] + bind $dlg [list $b1 invoke] + wm deiconify $dlg + set [namespace current]::$dlg waiting + tkwait variable [namespace current]::$dlg + set r [set [namespace current]::$dlg] + unset [namespace current]::$dlg + destroy $dlg + return $r +} + + +proc ::xmppplugin::OnIqVersion {ctx jlib from subiq args} { + array set a [linsert $args 0 -id {}] + set opts [list -to $from] + if {$a(-id) ne {}} {lappend opts -id $a(-id)} + foreach {cname cver cos} [split [Version $ctx] :] break + set subtags [list \ + [wrapper::createtag name -chdata $cname] \ + [wrapper::createtag version -chdata $cver] \ + [wrapper::createtag os -chdata $cos]] + set xmllist [wrapper::createtag query -subtags $subtags \ + -attrlist {xmlns jabber:iq:version}] + eval [linsert $opts 0 $jlib send_iq result [list $xmllist]] + return 1 ;# handled +} + +proc ::xmppplugin::OnIqLast {ctx jlib from subiq args} { + if {[catch {tk inactive} last]} { + return 0 ;# not handled + } + set last [expr {int($last / 1000.0)}] + array set a [linsert $args 0 -id {}] + set opts [list -to $from] + if {$a(-id) ne {}} { lappend opts -id $a(-id) } + set xml [wrapper::createtag query \ + -attrlist [list xmlns jabber:id:last seconds $last]] + eval [linsert $opts 0 $jlib send_iq result [list $xml]] + return 1 ;# handled +} + +proc ::xmppplugin::OnIqVersionResult {ctx jlib from subiq args} { + upvar #0 $ctx xmpp + if {[catch { + array set a [linsert $args 0 -id {}] + jlib::splitjid $from jid nick + puts stderr "result: $jid $nick => $xmpp(-channel)" + if {[jlib::jidequal $jid $xmpp(-channel)]} { + array set data {} + foreach sub [wrapper::getchildren $subiq] { + set data([wrapper::gettag $sub]) [wrapper::getcdata $sub] + } + set ver "" + if {[info exists data(name)]} { append ver $data(name) } + if {[info exists data(version)]} { append ver " " $data(version) } + if {[info exists data(os)]} { append ver " : $data(os)" } + set xmpp(userversion,$nick) $ver + Callback $ctx userinfo $xmpp(-channel) $nick -version $ver + Log $ctx "$nick: $ver" + } + } err]} { + tk_messageBox -icon error -message $err \ + -title "Error handling version result" + } + return 1 ;# handled +} + +# initiate from $jlib vcard get_async $jid [namespace code OnVCard] +# once got - try get_cache if getcache is {} then do above. +proc ::xmppplugin::OnVCard {jlib type xmldata} { + switch -exact -- $type { + result { + foreach kid [wrapper::getchildren $xmldata] { + # tags are FN, NICKNAME, URL etc + } + } + error { + foreach {code text} $xmldata break + + } + } +} + +proc ::xmppplugin::OnRosterGet {ctx args} { + if {[catch { + Log $ctx "Recieved roster" + after idle [list [namespace origin Callback] $ctx userlist roster] + } err]} { puts stderr "OnRosterGet: $err" } + return 0; +} + +proc ::xmppplugin::OnRosterChange {ctx jlib what {jid {}} args} { + #Log $ctx "Roster '$what' '$jid' '$args'" + #enterroster | exitroster | set jid args | remove jid | + #switch -exact -- $what {} + return 0 +} + +# Look for MUC presence changes +proc ::xmppplugin::OnPresenceChange {ctx jlib xmldata} { + if {[catch { + set x [lindex [wrapper::getchildswithtagandxmlns $xmldata x \ + "http://jabber.org/protocol/muc#user"] 0] + if {[llength $x] > 0} { + array set a [linsert [wrapper::getattrlist $xmldata] 0 type available] + jlib::splitjid $a(from) room nick + # avoid people just becoming active/inactive + if {$a(type) eq "available"} { + if {[set present [lsearch [$jlib muc participants $room] $a(from)]] == -1} { + set status [wrapper::getcdata \ + [wrapper::getfirstchildwithtag $xmldata status]] + Callback $ctx traffic entered $room $nick -status $status + } else { + Log $ctx "presence available for $nick who is present index $present" + } + + # update the userlist + set details {} + lappend details -show [wrapper::getcdata [wrapper::getfirstchildwithtag $xmldata show]] + lappend details -status [wrapper::getcdata [wrapper::getfirstchildwithtag $xmldata status]] + lappend details -priority [wrapper::getcdata [wrapper::getfirstchildwithtag $xmldata priority]] + # x/item contains attrs jid (full jid), affiliation and role + if {[set item [wrapper::getfirstchildwithtag $x item]] ne {}} { + lappend details -jid [wrapper::getattribute $item jid] + lappend details -role [wrapper::getattribute $item role] + lappend details -affiliation [wrapper::getattribute $item affiliation] + } + eval [linsert $details 0 Callback $ctx userinfo $room $nick] + } elseif {$a(type) eq "unavailable"} { + set status [wrapper::getcdata \ + [wrapper::getfirstchildwithtag $xmldata status]] + Callback $ctx traffic left $room $nick -status $status + } + } + } err]} { puts stderr "OnPresenceChange: $err" } + return 0 +} + +proc ::xmppplugin::OnPresence {jlib xmldata} { + set ctx [set [set jlib]::AppContext] + if {[catch {OnPresence2 $ctx $jlib $xmldata} res]} { + Log $ctx "OnPresence: $err" error + return 0 + } + return $res +} +proc ::xmppplugin::OnPresence2 {ctx jlib xmldata} { + #Log $ctx "P: $xmldata" + return 0 +} + +proc ::xmppplugin::OnIq {jlib xmldata} { + set ctx [set [set jlib]::AppContext] + if {[catch {OnIq2 $ctx $jlib $xmldata} res]} { + Log $ctx "OnIq: $err" error + return 0 + } + return $res +} +proc ::xmppplugin::OnIq2 {ctx jlib xmldata} { + Log $ctx "IQ: $xmldata" + return 0 +} + +proc ::xmppplugin::OnMessage {jlib xmldata} { + set ctx [set [set jlib]::AppContext] + if {[catch {OnMessage2 $ctx $jlib $xmldata} err]} { + Log $ctx "OnMessage: $err" error + } + return 0 +} +proc ::xmppplugin::OnMessage2 {ctx jlib xmldata} { + upvar #0 $ctx xmpp + array set a [linsert [wrapper::getattrlist $xmldata] 0 from {} to {} type normal] + jlib::splitjid $a(from) fromjid fromres + jlib::splitjid $a(to) tojid tores + set body [wrapper::getchildswithtag $xmldata body] + set subject [string trim [wrapper::getcdata \ + [wrapper::getfirstchildwithtag $xmldata subject]]] + set thread [string trim [wrapper::getcdata \ + [wrapper::getfirstchildwithtag $xmldata thread]]] + set chatstate [wrapper::gettag \ + [wrapper::getfirstchildwithxmlns $xmldata http://jabber.org/protocol/chatstates]] + + foreach x [wrapper::getchildswithtag $xmldata x] { + switch -exact -- [wrapper::getattribute $x xmlns] { + "jabber:x:delay" {} + "jabber:x:event" {} + "urn:tkchat:chat" { + set color [wrapper::getattribute $x color] + if {[regexp {^[[:xdigit:]]{6}$} $color]} { + Callback $ctx userlist update $fromjid $fromres -color "#$color" + } + } + "urn:tkchat:changenick" {} + "urn:tkchat:whiteboard" {} + "coccinella:whiteboard" {} + } + } + + switch -exact -- $a(type) { + groupchat { + #Log $ctx "groupchat: $a(from)->$a(to)" + if {$subject ne {}} { + Callback $ctx topic $fromjid $subject + } + set msg [wrapper::getcdata [lindex $body 0]] + set what normal + if {$fromres eq "ijchain"} { + if {![regexp {^(<.*?>)\s(.*)$} $msg -> fromres msg]} { + # *** x left | *** x entered | *** x is now known as y + if {[regexp {\*{3} ([^\s]+)\s(.*)$} $msg -> fromres msg]} { + set newnick {} ; set what unknown + switch -glob -- $msg { + joins* - + entered* { set what entered } + leaves* - + left* { set what left } + is* { + set what nickchange + set newnick [lindex [split $msg] 4] + } + } + Callback $ctx traffic $what $fromjid $fromres $newnick -group irc + return 0 + } elseif {[regexp {^\* (\S+) (.*)$} $msg -> fromres msg]} { + set what action + } + } + } + if {[string match "/me *" $msg]} { + set what action + set msg [string range $msg 3 end] + } + if {[string length $msg] > 0} { + Callback $ctx chat $fromjid $fromres $msg $what + } + } + chat { + # subject? could show the topic if we ever get such an element. + + # record the current conversation thread or create one + if {$thread eq {}} { set thread [uuid::uuid generate] } + # maintain per chat state in dicts. Note: we should receive an active + # from the remote client in response to our initial message which enables + # chatstate support. + dict set xmpp(opts) $a(from) \ + [dict create -thread $thread -chatstate $chatstate] + + set nick [string trim [wrapper::getcdata \ + [wrapper::getfirstchildwithtag $xmldata nick]]] + if {$nick eq {}} { + jlib::splitjid $a(from) node resource + if {[$jlib muc isroom $node]} { + set nick $resource + } else { + set nick $node + } + } + # if chatstate stuff -- display somehow + if {[llength $body] > 0} { + Callback $ctx chat $a(from) $nick \ + [wrapper::getcdata [lindex $body 0]] normal + } + } + normal { + set nicktag [wrapper::getchildswithtag $xmldata nick] + jlib::splitjid $a(from) from res + if {[llength $nicktag] >0} { + set from "[wrapper::getcdata [lindex $nicktag 0]] <$from>" + } + set time [clock seconds] + set delay [lindex [wrapper::getchildswithtag $xmldata delay] 0] + if {$delay ne {}} { + set stamp [wrapper::getattribute $delay stamp] + catch {set time [clock scan $da(stamp) \ + -format {%Y-%m-%dT%H:%M:%S%Z}]} + } + set p [list -date $time -subject $subject] + if {$thread ne {}} {lappend p -thread $thread} + lappend p -body [wrapper::getcdata [lindex $body 0]] + eval [linsert $p 0 Callback $ctx message $a(to) $from] + } + headline { + Callback $ctx chat $a(to) $a(from) \ + "header: [wrapper::etcdata [lindex $body 0]]" normal + Log $ctx "$a(from)->$a(to) headline $xmldata" + } + error { + # If we receive a error from a chat partner we must stop the thread and + # avoid sending anything else. + # Had thread, gone(chatstate) and Not Found + Log $ctx "Message error: $xmldata" error + set err [wrapper::getchildswithtag $xmldata error] + set emsg [wrapper::getcdata [lindex $err 0]] + Callback $ctx system $a(from) "error from $a(from): $emsg" + } + default { + set body [wrapper::getcdata [lindex $body 0]] + Log $ctx "message: $a(type) $a(from)->$a(to): $subject\n$body" + } + } +} + + +proc ::xmppplugin::query_user {Chat user what} { + upvar #0 $Chat ctx + upvar #0 $ctx(xmpp) xmpp + + array set q { + version "jabber:iq:version" + last "jabber:iq:last" + time "jabber:iq:time" + discover "http://jabber.org/protocol/disco#info" + } + if {![info exists q($what)]} { + return -code error "invalid query \"$what\": must be one of\ + [join [array names q] {, }]" + } + + if {[string first @ $user] == -1} { + set jid $xmpp(-channel)/$user + } else { + set jid $user + } + set xmllist [wrapper::createtag query -attrlist [list xmlns $q($what)]] + $xmpp(jlib) send_iq get [list $xmllist] -to $jid + return +} + +package provide xmppplugin $::xmppplugin::version + +# ------------------------------------------------------------------------- +# APPLICATION LEVEL CODE +# ------------------------------------------------------------------------- +variable xmppuid +if {![info exists xmppuid]} { set xmppuid 0 } + +proc Grid {w junk row junk column} { + grid rowconfigure $w $row -weight 1 + grid columnconfigure $w $column -weight 1 +} +proc Var {arrayname key} { + set n [uplevel 1 namespace which -variable $arrayname] + if {$n eq {}} { return -code error "invalid variable name \"$arrayname\"" } + return $n\($key\) +} +proc EnableChildren { parent varname } { + upvar #0 $varname var + set state [expr {$var ? "normal" : "disabled"}] + foreach child [winfo children $parent] { + catch {EnableChildren $child $varname} + catch {$child configure -state $state} + } +} + +proc XmppLogin {app} { + set dlg $app.xmpplogin + variable $dlg {} + variable xmpp + if {![info exists xmpp(-connect)]} { + array set xmpp { + -useproxy 0 -proxyhost "" -proxyport "" -proxyuser "" -proxypass "" + -server all.tclers.tk -port 5222 -username "" -passwd "" + -connect tlssasl -resource Bullfrog + -autoconnect 0 -channel "tcl@tach.tclers.tk" -nick "" + } + set xmpp(-proxyhost) [autoproxy::cget -proxy_host] + set xmpp(-proxyport) [autoproxy::cget -proxy_port] + puts stderr "proxy: $xmpp(-proxyhost) $xmpp(-proxyport)" + } + if {![winfo exists $dlg]} { + set dlg [toplevel $dlg -class Dialog] + wm withdraw $dlg + wm transient $dlg $app + wm title $dlg "Login" + + set f [ttk::frame $dlg.f] + set g [ttk::frame $f.g] + + ttk::checkbutton $f.prx -text "Use proxy" \ + -command [list EnableChildren $f.fp [Var xmpp -useproxy]] \ + -variable [Var xmpp -useproxy] -underline 7 + set fp [ttk::labelframe $f.fp -labelwidget $f.prx] + ttk::label $fp.lph -text "Proxy host:port" -underline 0 + set fpx [ttk::frame $fp.fpx] + ttk::entry $fpx.eph -textvariable [Var xmpp -proxyhost] + ttk::entry $fpx.epp -textvariable [Var xmpp -proxyport] -width 5 + ttk::label $fp.lpan -text "Proxy username" -underline 11 + ttk::entry $fp.epan -textvariable [Var xmpp -proxyuser] + ttk::label $fp.lpap -text "Proxy password" -underline 13 + ttk::entry $fp.epap -textvariable [Var xmpp -proxypass] -show {*} + grid $fpx.eph $fpx.epp -sticky news -padx 1 -pady 1 + Grid $fpx row 0 column 0 + grid $fp.lph $fpx -sticky new -padx 1 -pady 1 + grid $fp.lpan $fp.epan -sticky new -padx 1 -pady 1 + grid $fp.lpap $fp.epap -sticky new -padx 1 -pady 1 + EnableChildren $f.fp [Var xmpp -useproxy] + + ttk::label $f.sl -text Server -anchor w + ttk::entry $f.se -textvariable [Var xmpp -server] + ttk::entry $f.sp -textvariable [Var xmpp -port] -width 5 + ttk::label $f.nl -text Username -anchor w + ttk::entry $f.nn -textvariable [Var xmpp -username] + ttk::label $f.pl -text Password -anchor w + ttk::entry $f.pw -textvariable [Var xmpp -passwd] -show {*} + ttk::label $f.rl -text Resource -anchor w + ttk::entry $f.re -textvariable [Var xmpp -resource] + + set fo [ttk::labelframe $f.fo -text "Connection options"] + ttk::radiobutton $fo.ssl0 -text Normal -underline 0 \ + -variable [Var xmpp -connect] -value tlssasl + ttk::radiobutton $fo.ssl1 -text SSL -underline 0 \ + -variable [Var xmpp -connect] -value tls + ttk::radiobutton $fo.ssl2 -text Plain -underline 0 \ + -variable [Var xmpp -connect] -value sasl + grid $fo.ssl0 $fo.ssl1 $fo.ssl2 -sticky ew -padx 1 -pady 1 + Grid $fo row 1 column 3 + + ttk::checkbutton $f.acx -text "Auto-connect" \ + -command [list EnableChildren $f.ac [Var xmpp -autoconnect]]\ + -variable [Var xmpp -autoconnect] -underline 0 + set fa [ttk::labelframe $f.ac -labelwidget $f.acx] + ttk::label $fa.cl -text Channel -anchor w + ttk::entry $fa.cn -textvariable [Var xmpp -channel] + ttk::label $fa.kl -text Nick -anchor w + ttk::entry $fa.kn -textvariable [Var xmpp -nick] + grid $fa.cl $fa.cn -sticky news -padx 1 -pady 1 + grid $fa.kl $fa.kn -sticky news -padx 1 -pady 1 + Grid $fa row 2 column 1 + EnableChildren $f.ac [Var xmpp -autoconnect] + + ttk::button $f.ok -text Login -default active \ + -command [list set [namespace which -variable $dlg] "ok"] + ttk::button $f.cancel -text Cancel \ + -command [list set [namespace which -variable $dlg] "cancel"] + + + bind $dlg [list $f.ok invoke] + bind $dlg [list $f.cancel invoke] + wm protocol $dlg WM_DELETE_WINDOW [list $f.cancel invoke] + + grid $f.fp - - -in $g -sticky new -padx 1 -pady 1 + grid $f.sl $f.se $f.sp -in $g -sticky new -padx 1 -pady 1 + grid $f.rl $f.re - -in $g -sticky new -padx 1 -pady 1 + grid $f.nl $f.nn - -in $g -sticky new -padx 1 -pady 1 + grid $f.pl $f.pw - -in $g -sticky new -padx 1 -pady 1 + grid $fo - - -in $g -sticky new -padx 1 -pady 1 + grid $fa - - -in $g -sticky new -padx 1 -pady 1 + grid columnconfigure $g 1 -weight 1 + + grid $g - -sticky news + grid $f.ok $f.cancel -sticky e -padx 1 -pady 1 + grid rowconfigure $f 0 -weight 1 + grid columnconfigure $f 0 -weight 1 + + grid $f -sticky news + grid rowconfigure $dlg 0 -weight 1 + grid columnconfigure $dlg 0 -weight 1 + + wm resizable $dlg 0 0 + raise $dlg + } + + catch {::tk::PlaceWindow $dlg widget $app} + wm deiconify $dlg + tkwait visibility $dlg + focus -force $dlg.f.ok + grab $dlg + vwait [namespace which -variable $dlg] + grab release $dlg + wm withdraw $dlg + + puts stderr [array get xmpp] + if {[set $dlg] eq "ok"} { + after idle [linsert [array get xmpp] 0 \ + [namespace origin XmppConnect] $app] + } +} + +proc Grid {w junk row junk column} { + grid rowconfigure $w $row -weight 1 + grid columnconfigure $w $column -weight 1 +} + +proc XmppAddXmlConsole {app} { + if {[winfo exists $app.nb.xmlconsole]} { return } + set w [ttk::frame $app.nb.xmlconsole -style ChatwidgetFrame] + text $w.text -relief flat -borderwidth 0 -wrap char -state disabled \ + -font DebugFont -yscrollcommand [list $w.vs set] + set m0 [font measure DebugFont {00:00:00mm}] + $w.text configure -tabs [list $m0 [expr {$m0 * 2}]] + $w.text tag configure read -foreground blue3 + $w.text tag configure write -foreground red3 + $w.text tag configure time -foreground "#202020" + $w.text tag configure msg -lmargin1 $m0 -lmargin2 $m0 -spacing3 2 + $w.text tag configure message -foreground "#000080" + $w.text tag configure presence -foreground "#800080" + $w.text tag configure iq -foreground "#008080" + ttk::scrollbar $w.vs -command [list $w.text yview] + grid $w.text $w.vs -sticky news -padx 1 -pady 1 + grid rowconfigure $w 0 -weight 1 + grid columnconfigure $w 0 -weight 1 + $app.nb add $w -text XML + jlib::setdebug 2 + if {[info commands ::jlib::_Debug] eq {}} { + rename ::jlib::Debug ::jlib::_Debug + interp alias {} ::jlib::Debug {} ::XmppDebugXml $w.text + } + set ::jlib::disco::debug 2 + if {[info commands ::jlib::disco::_Debug] eq {}} { + rename ::jlib::disco::Debug ::jlib::disco::_Debug + interp alias {} ::jlib::disco::Debug {} ::XmppDebugJlib $app + } +} + +# Divert generic jabberlib debug stuff into our debug pane +proc XmppDebugJlib {app num str} { + set w $app.nb.debug.text + if {[winfo exists $w]} { + set t [clock format [clock seconds] -format "%T"] + $w configure -state normal + $w insert end "$t\t$str\n" debug + $w configure -state disabled + } +} + +# Divert the jabberlib debug function into our tab window. +proc XmppDebugXml {w num str} { + if {[jlib::setdebug] >= $num} { + set autoscroll [expr {[lindex [$w yview] 1] == 1.0}] + set t [clock format [clock seconds] -format {%H:%M:%S}] + set tags {} + if {[string match "RECV:*" $str]} { + set str [string range $str 6 end] + lappend tags read + switch -glob -- $str { + " "+unset -nocomplain $Session" + bind $session(app).nb <> \ + [namespace code "XmppUnalert $Session \[%W select\]"] + return $Session +} + +# Add a new chatroom into the gui. +proc XmppAddChannel {Session channel "-type" type} { + upvar #0 $Session session + set Channel "${Session}/$channel" + upvar #0 $Channel chan + array set chan [array get session] + Debug $Session "XmppAddChannel $channel -type $type" + set chan(channel) $channel + set chan(type) $type + set chan(session) $Session + unset -nocomplain chan(targets) + set chan(window) [chatwidget::chatwidget \ + $session(window)[string map {. _} $channel]] + lappend session(targets) [list $channel $chan(window)] + set m0 [font measure ChatwidgetFont {[00:00]m}] + set m1 [font measure ChatwidgetFont [string repeat m 10]] + set mx [expr {$m0 + $m1}] + $chan(window) chat configure -tabs [list $m0 $mx] + $chan(window) chat tag configure MSG -lmargin1 $mx -lmargin2 $mx + $chan(window) chat tag configure NICK -font ChatwidgetBoldFont + $chan(window) chat tag configure TYPE-system -font ChatwidgetItalicFont + $chan(window) chat tag bind URL [list UrlEnter %W] + $chan(window) chat tag bind URL [list UrlLeave %W] + $chan(window) chat tag bind URL [list UrlClick %W %x %y] + if {$type eq "chat"} { + $chan(window) names hide + $chan(window) hook add chatstate [list XmppChatstate $Channel] + } else { + $chan(window) names tag bind NICK \ + [namespace code [list XmppChannelNickMenu $Channel %W %x %y]] + $chan(window) names tag bind NICK \ + [list [namespace origin XmppNickTooltip] $Channel enter %W %x %y] + $chan(window) names tag bind NICK \ + [list [namespace origin XmppNickTooltip] $Channel leave %W %x %y] + $chan(window) hook add names_nick \ + [namespace code [list XmppNamesHook $Channel]] + } + $chan(window) hook add post [list ::xmppplugin::post $chan(xmpp) $channel] + bind $chan(window) "+unset -nocomplain $Channel" + + # If the channel domain is a muc then use the resource. + upvar #0 $chan(xmpp) xmpp + jlib::splitjidex $channel node domain resource + if {[$xmpp(jlib) muc isroom $node@$domain]} { + set title $resource + } elseif {$node ne {}} { + set title $node + } else { set title $domain } + $session(app).nb add $chan(window) -text $title + after idle [list $session(app).nb select $chan(window)] + return $chan(window) +} + +proc XmppChatstate {Chat chatstate} { + upvar #0 $Chat chat + upvar #0 $chat(xmpp) xmpp + if {![catch {dict get $xmpp(opts) $chat(channel) -chatstate} use]} { + if {$use ne {}} { + lappend xlist [wrapper::createtag $chatstate \ + -attrlist {xmlns http://jabber.org/protocol/chatstates}] + set margs [list -type $chat(type) -xlist $xlist] + if {![catch {set thread [dict get $xmpp(opts) $chat(channel) -thread]}]} { + lappend margs -thread $thread + } + eval [linsert $margs 0 $xmpp(jlib) send_message $chat(channel)] + } + } +} + +# Hook called each time the names part of the chatwidget is updated. +# args are the options for the nick +proc XmppNamesHook {Chat nick args} { + upvar #0 $Chat chat + set wclass [winfo class $chat(window)] + #puts stderr "names hook: $Chat $nick $args class:$wclass" + if {[set ndx [lsearch $args -version]] != -1} { + if {$wclass eq "Chatwidget"} { + after idle [list ::tooltip::tooltip \ + [$chat(window) names] -tag NICK-$nick \ + [lindex $args [incr ndx]]] + } + } +} + +proc XmppChannelNickMenu {Chat w x y} { + set nick [string trim [$w get "@$x,$y linestart" "@$x,$y lineend"]] + if {$nick eq ""} { return } + destroy $w.nickmenupopup + set m [menu $w.nickmenupopup -tearoff 0] + $m add command -label "$nick" -state disabled + $m add separator + $m add command -label "Chat" -underline 0 \ + -command [namespace code [list XmppChannelNickCommand $Chat chat $nick]] + $m add command -label "Whois" -underline 0 -state disabled \ + -command [namespace code [list XmppChannelNickCommand $Chat whois $nick]] + $m add command -label "Version" -state normal \ + -command [namespace code [list XmppChannelNickCommand $Chat version $nick]] + tk_popup $m [winfo pointerx $w] [winfo pointery $w] +} + +proc XmppChannelNickCommand {Chat cmd nick} { + upvar #0 $Chat ctx + upvar #0 $ctx(xmpp) xmpp + switch -exact -- $cmd { + version { xmppplugin::query_user $Chat $nick version } + last { xmppplugin::query_user $Chat $nick last } + chat { + # open a private chat to a MUC user. + set w [XmppCreateWindow $ctx(session) $ctx(channel)/$nick -type chat] + $ctx(app).nb tab $w -text $nick + $ctx(app).nb select $w + } + send { + XmppCreateMessage $ctx(session) $ctx(channel)/$nick + } + } +} + +proc XmppNickTooltip {Chat type w x y} { + return + if {[package provide tooltip] eq {}} { return } + set nick [string trim [$w get "@$x,$y linestart" "@$x,$y lineend"]] + if {$nick eq ""} { return } + upvar #0 $Chat chat + puts stderr "tooltip: $w name $nick" + #return + set version [$chat(window) name get $nick -version] + if {$version ne {}} { + ::tooltip::tooltip $w -tag NICK-$nick $version + } + return +} + +proc XmppCreateWindow {Session target "-type" type} { + upvar #0 $Session session + set w [XmppFindWindow $Session $target] + if {$w eq $session(window)} { + set w [XmppAddChannel $Session $target -type $type] + } + return $w +} + +proc XmppFindWindow {Session target} { + upvar #0 $Session session + set result $session(window) + if {[info exists session(targets)]} { + foreach pair $session(targets) { + foreach {name wid} $pair break + if {$name eq $target} { + # check for detached window + if {[lsearch -exact [$session(app).nb tabs] $wid] == -1} { + if {[wm state $wid] eq "withdrawn"} { wm deiconify $wid } + } else { + if {[$session(app).nb tab $wid -state] eq "hidden"} { + $session(app).nb tab $wid -state normal + } + } + set result $wid + break + } + } + } + return $result +} + +proc XmppRemoveWindow {Session target} { + upvar #0 $Session session + if {[info exists session(targets)]} { + foreach pair $session(targets) { + foreach {name wid} $pair break + if {$name eq $target} { + $session(app).nb hide $wid + break + } + } + } +} + +proc XmppAlert {Session target} { + set alert 0 + set w [XmppFindWindow $Session $target] + set top [winfo toplevel $w] + set focus [focus -displayof $top] + set txt [WindowTitle $Session $w] + set count 0 + regexp {^(\d+) - (.*)$} $txt -> count txt + if {[string match "${w}*" $focus]} { + WindowTitle $Session $w $txt + } else { + WindowTitle $Session $w "[incr count] - $txt" + puts stderr "Alert focus:'$focus' '[focus]' state:[wm state $top]" + if {[llength $focus] == 0} { + # the focus is in some other app - check its not the console + if {[llength [console eval focus]] == 0} { + puts stderr "raising and so on" + wm deiconify $top + raise $top + } + } + set alert 1 + } + return $alert +} + +proc XmppUnalert {Session w} { + set title [WindowTitle $Session $w] + if {[regexp {^(\d+) - (.*)$} $title -> count tail]} { + WindowTitle $Session $w $tail + } +} + +proc XmppCallback {Session context state args} { + upvar #0 $Session session + upvar #0 $context xmpp + #puts stderr [list $Session $context $state $args] + switch -exact -- $state { + init { + Status $Session "Attempting to connect to $xmpp(-server)" + } + connect { + XmppCallback $Session $context debug \ + "Logging into $xmpp(-server) as $xmpp(-username)" + Status $Session "Connection to XMPP server established." + State $Session connected + } + disconnect { + foreach {reason} $args break + if {$reason ne {}} { set reason ": $reason" } + Status $Session "Disconnected$reason" + State $Session disconnected + } + close { + foreach {target} $args break + Debug $Session "closing $target" + XmppRemoveWindow $Session $target + } + addchat { + foreach {target type} $args break + set w [XmppCreateWindow $Session $target -type $type] + } + userlist { + foreach {type target} $args break + switch -exact -- $type { + roster { + foreach jid [$xmpp(jlib) roster getusers] { + array set item [linsert [$xmpp(jlib) roster \ + getrosteritem $jid] 0 -groups {}] + if {![info exists item(-name)]} { set item(-name) $jid } + #$session(window) name add $item(-name) -jid $jid -group $item(-groups) + } + } + muc { + set colors {black tomato chocolate blue4 green4 pink4 SteelBlue4 SeaGreen4} + set w [XmppFindWindow $Session $target] + puts stderr "userlist: $target $w\ + [$xmpp(jlib) muc participants $target]" + set current [$w name list -full] + foreach jid [$xmpp(jlib) muc participants $target] { + jlib::splitjid $jid room nick + set opts [list -status online] + if {[lsearch -index 0 $current $nick] == -1} { + lappend opts -color \ + [lindex $colors [expr {int(rand() * [llength $colors])}]] + } + eval [list $w name add $nick] $opts + } + } + update { + #update tcl@tach.tclers.tk nick -color x -affiliation y -group users -status + set w [XmppFindWindow $Session $target] + if {[winfo class $w] eq "Chatwidget"} { + set nick [lindex $args 2] + eval [list $w name add $nick] [lrange $args 3 end] + } + } + } + } + userinfo { + foreach {target nick} $args break + set w [XmppFindWindow $Session $target] + if {[winfo class $w] eq "Chatwidget"} { + foreach {what value} [lrange $args 2 end] { + switch -exact -- $what { + -affiliation { set group $value } + -show {} + -status {} + -jid {} + -role { } + -version { + Status $Session "$nick using $value" + } + } + } + eval [linsert [lrange $args 2 end] 0 $w name add $nick] + } + } + chat { + foreach {target nick msg type} $args break + if {$type eq ""} {set type normal} + set w [XmppCreateWindow $Session $target -type chat] + XmppAlert $Session $target + switch -exact -- [winfo class $w] { + Chatwidget {$w message $msg -nick $nick -type $type} + Messagewidget { + $w add -from $nick -to Me -body $msg -date [clock seconds] + } + default {puts stderr "invalid chat target \"$target\""} + } + } + message { + foreach {target from} $args break + jlib::splitjidex $target node domain resource + set w $session(window) + XmppAlert $Session $domain + eval [list $w add -to $target -from $from] [lrange $args 2 end] + } + system { + foreach {target msg} $args break + set w [XmppFindWindow $Session $target] + XmppAlert $Session $target + switch -exact -- [winfo class $w] { + Chatwidget {$w message $msg -type system} + Messagewidget { + $w add -from SYSTEM -to Me -body $msg -date [clock seconds] + } + default { + Debug $Session "invalid system target \"$target\"" debug + } + } + } + topic { + foreach {target topic} $args break + set w [XmppFindWindow $Session $target] + if {[winfo class $w] eq "Chatwidget"} { + $w topic show + $w topic set $topic + } + } + traffic { + foreach {action target nick new} $args break + set w [XmppFindWindow $Session $target] + if {[winfo class $w] ne "Chatwidget"} {return} + switch -exact -- $action { + joining { XmppCreateWindow $Session $target -type groupchat} + entered { + eval [linsert $args 0 $w name add $nick] + $w message "$nick $action" -nick $nick -type system + } + left { + $w name delete $nick + $w message "$nick $action" -nick $nick -type system + } + nickchange { + $w name delete $nick + eval [linsert $args 0 $w name add $new] + $w message "$nick is now known as $new" -nick $nick -type system + } + default { + $w message "$nick $action" -nick $nick -type system + } + } + } + debug { + foreach {type line} $args break + Debug $Session $line $type + } + version { return "" } + default { + puts stderr "*** unknown xmpp callback \"$state\": $args" + } + } +} diff --git a/bin/bullfrog.tcl b/bin/bullfrog.tcl new file mode 100644 index 0000000..43d8262 --- /dev/null +++ b/bin/bullfrog.tcl @@ -0,0 +1,369 @@ +# bullfrog.tcl - +# +# This is a multi-transport chat application with support for +# IRC (using the picoirc package) and Jabber (using the current +# jabberlib from the coccinella project). +# It makes use of the chatwidget from tklib +# +# Copyright (C) 2007 Pat Thoyts +# +# See the file "license.terms" for information on usage and redistribution of +# this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# $Id: picoirc.tcl,v 1.2 2007/10/24 10:35:25 patthoyts Exp $ + +package require Tk 8.5 +package require chatwidget 1.1; # tklib +package require tooltip 1.4; # tklib + +if {![catch {package require autoproxy}]} { + autoproxy::init +} + +set root [file dirname [info script]] +source [file join $root message.tcl] +source [file join $root tab.tcl] + +# Load the transport specific files... +source [file join $root bf_irc.tcl] +source [file join $root bf_xmpp.tcl] + +# ------------------------------------------------------------------------- + +proc Main {args} { + global env + array set opts {-debug 1 -nick "" -name ""} + if {[info exists env(IRCNICK)]} {set opts(-nick) $env(IRCNICK)} + if {[info exists env(IRCNAME)]} {set opts(-nick) $env(IRCNAME)} + while {[string match -* [set option [lindex $args 0]]]} { + switch -exact -- $option { + -nick { set opts(-nick) [Pop args 1] } + -name { set opts(-name) [Pop args 1] } + -debug { set opts(-debug) 1 } + -- { Pop args ; break } + default { break } + } + Pop args + } + + ConfigureFonts + + set app [toplevel .chat -class Bullfrog] + wm withdraw $app + wm title $app "Bullfrog" + + set imgfile [file join [file dirname [info script]] bullfrog48.gif] + if {[file exists $imgfile]} { + image create photo bullfrogImage -file $imgfile + wm iconphoto $app bullfrogImage + } + + set menu [menu $app.menu -tearoff 0] + # File menu + $menu add cascade -label Network -menu [menu $menu.file -tearoff 0] + $menu.file add command -label "IRC Login..." -underline 0 \ + -command [namespace code [list IrcLogin $app]] + if {[llength [info commands XmppLogin]] != 0} { + $menu.file add command -label "Jabber Login..." -underline 0 \ + -command [namespace code [list XmppLogin $app]] + } + $menu.file add separator + $menu.file add command -label Exit \ + -command [namespace code [list Exit $app]] + # Windows menu + $menu add cascade -label Window \ + -menu [menu $menu.window -tearoff 0 \ + -postcommand [namespace code [list OnPostWindow $app $menu.window]]] + + $app configure -menu $menu + + ttk::notebook $app.nb -style ButtonNotebook + + if {$opts(-debug)} { + set debugf [frame $app.nb.debugf -borderwidth 0 -highlightthickness 0] + set debug [ttk::frame $app.nb.debugf.debug -style ChatwidgetFrame] + text $debug.text -relief flat -borderwidth 0 -wrap word \ + -state disabled -font DebugFont \ + -yscrollcommand [list $debug.vs set] + $debug.text tag configure read -foreground blue3 + $debug.text tag configure write -foreground red3 + ttk::scrollbar $debug.vs -command [list $debug.text yview] + grid $debug.text $debug.vs -sticky news -padx 1 -pady 1 + grid rowconfigure $debug 0 -weight 1 + grid columnconfigure $debug 0 -weight 1 + grid $debug -sticky news + grid rowconfigure $debugf 0 -weight 1 + grid columnconfigure $debugf 0 -weight 1 + $app.nb add $debugf -text Debug + } + + set status [ttk::frame $app.status] + ttk::label $status.pane0 -anchor w + ttk::separator $status.sep0 -orient vertical + ttk::label $status.pane1 -anchor w + ttk::separator $status.sep1 -orient vertical + ttk::sizegrip $status.sizegrip + grid $status.pane0 $status.sep0 $status.pane1\ + $status.sep1 $status.sizegrip -sticky news + grid columnconfigure $status 0 -weight 1 + grid rowconfigure $status 0 -weight 1 + + grid $app.nb -sticky news + grid $status -sticky sew + grid rowconfigure $app 0 -weight 1 + grid columnconfigure $app 0 -weight 1 + + ttk::notebook::enableTraversal $app.nb + bind $app {console show} + + wm geometry .chat 600x400 + wm deiconify $app + + set uri [lindex $args 0] + if {$opts(-nick) ne "" && $uri ne ""} { + foreach {server port channel} [picoirc::splituri $uri] break + after idle [list [namespace origin IrcConnect] $app \ + -server $server -port $port \ + -channel $channel -nick $opts(-nick)] + } + + tkwait window $app + return +} + +# Configure the fixed fonts for the debug windows +proc ConfigureFonts {} { + if {[lsearch -exact [font names] DebugFont] == -1} { + set base [font actual TkDefaultFont] + eval font create DebugFont [font actual TkFixedFont] + } + + set families [font families] + switch -exact -- [tk windowingsystem] { + aqua { set preferred {Monaco 10} } + win32 { set preferred {ProFontWindows 8 Consolas 8} } + default { set preferred {} } + } + foreach {family size} $preferred { + if {[lsearch -exact $families $family] != -1} { + font configure DebugFont -family $family -size $size + break + } + } +} + +proc Pop {varname {nth 0}} { + upvar $varname args + set r [lindex $args $nth] + set args [lreplace $args $nth $nth] + return $r +} + +proc Exit {app} { + destroy $app + exit +} + +proc Status {Chat message} { + upvar #0 $Chat chat + $chat(app).status.pane0 configure -text $message +} + +proc State {Chat message} { + upvar #0 $Chat chat + $chat(app).status.pane1 configure -text $message +} + +proc Debug {Chat message {type debug}} { + upvar #0 $Chat chat + set w $chat(app).nb.debugf.debug.text + if {[winfo exists $w]} { + set t [clock format [clock seconds] -format "%H:%M:%S"] + $w configure -state normal + $w insert end "$t\t$message\n" $type + $w configure -state disabled + } +} + +proc bgerror {args} { + tk_messageBox -icon error -title "Error" -message $::errorInfo +} + +proc OnPostWindow {app menu} { + set ndx 0 + $menu delete 0 end + set mod [expr {[tk windowingsystem] eq "aqua" ? "Cmd" : "Ctrl"}] + $menu add command -label "Close tab" -underline 0 \ + -command [namespace code [list CloseWindow $app]] + $menu add command -label "Detach tab" -underline 0 \ + -command [namespace code [list DetachWindow $app]] + $menu add separator + set tabs [$app.nb tabs] + foreach w [winfo children $app.nb] { + set ndx [incr ndx] + # children that are forgotten raise a 'not managed' error but we can ignore this + catch { + if {[winfo toplevel $w] eq $w} { + set title [wm title $w] + } else { + set title [$app.nb tab $w -text] + } + $menu add command -label "$ndx $title" -underline 0 \ + -accel "$mod-$ndx" -command [namespace code [list SelectWindow $app $w]] + } + } +} +proc SelectWindow {app w} { + if {[lsearch -exact [$app.nb tabs] $w] != -1} { + $app.nb select $w + } else { + wm deiconify $w + } +} +proc CloseWindow {app} { + set tab [$app.nb select] + if {[winfo exists $tab]} { + $app.nb forget $tab + event generate $app.nb <> + } +} +proc DetachWindow {app} { + set tab [$app.nb select] + if {[winfo exists $tab]} { + set title [$app.nb tab $tab -text] + set index [$app.nb index $tab] + $app.nb forget $tab + wm manage $tab + wm title $tab $title + wm protocol $tab WM_DELETE_WINDOW \ + [namespace code [list AttachWindow $app $tab $index]] + } +} +proc AttachWindow {app w {index end}} { + set title [wm title $w] + wm forget $w + if {[catch { + if {[catch {$app.nb insert $index $w -text $title} err]} { + puts stderr "AttachWindow: ($index) $err" + $app.nb add $w -text $title + } + $app.nb select $w + } err]} { + puts stderr "AttachWindow: $err" + wm manage $w + wm title $w $title + } +} +proc WindowTitle {Session w {title {}}} { + upvar #0 $Session session + if {[lsearch -exact [$session(app).nb tabs] $w] == -1} { + if {$title eq {}} { + return [wm title $w] + } else { + wm title $w $title + } + } else { + if {$title eq {}} { + return [$session(app).nb tab $w -text] + } else { + $session(app).nb tab $w -text $title + } + } +} + + +proc UrlEnter {w} { + variable cursor:$w + set cursor:$w [$w cget -cursor] + $w configure -cursor hand2 +} + +proc UrlLeave {w} { + variable cursor:$w + if {![info exists cursor:$w]} {set cursor:$w {}} + $w configure -cursor [set cursor:$w] +} + +proc UrlClick {w x y} { + set tags [$w tag names @$x,$y] + if {[set ndx [lsearch -glob $tags URL-*]] != -1} { + set url "" + foreach {b e} [$w tag ranges [lindex $tags $ndx]] { + append url [$w get $b $e] + } + if {[string length $url] > 0} { + if {[catch {GotoURL $w $url} err]} { + tk_messageBox -icon error -type ok -title "An error occurred"\ + -message $err + } + } + } +} + +proc GotoURL {w url} { + global tcl_platform + set dlg [winfo toplevel $w] + $dlg configure -cursor watch + clipboard clear + clipboard append $url + switch -- $tcl_platform(platform) { + "windows" { + # Try using DDE. Escape commas + package require dde + set url [string map {, %2c} $url] + set handled 0 + foreach app {Firefox Mozilla Netscape Opera IExplore} { + if {[set srv [dde services $app WWW_OpenURL]] != {}} { + # We cant actually check for success here. + catch {dde execute $app WWW_OpenURL $url} + set handled 1 + break + } + } + # Try the shell exec (quote the & chars) + if {!$handled} { + if {$tcl_platform(os) eq "Windows NT"} { + set url [string map {& ^&} $url] + } + if {[catch { + eval exec [auto_execok start] [list $url] & + } err]} then { + tk_messageBox -icon error -type ok \ + -title "Failed top open url" \ + -message "Error displaying \"$url\" in browser\n$err" + } + } + } + "unix" { + # darwin: open -a $env(BROWSER) $url + # gnome-open + # kde? + # find executable, then exec. + } + default { + tk_messageBox -icon error -type ok \ + -title "Unsupported platform" \ + -message "Your platform \"$tcl_platform(platform)\"\ + is not supported. Contact the developers." + } + } + $dlg configure -cursor {} +} + + + +# ------------------------------------------------------------------------- + +if {![info exists initialized] && !$tcl_interactive} { + set initialized 1 + wm withdraw . + set r [catch [linsert $argv 0 Main] err] + if {$r} {tk_messageBox -icon error -type ok -message $::errorInfo} + exit $r +} + +# ------------------------------------------------------------------------- +# Local variables: +# mode: tcl +# indent-tabs-mode: nil +# End: diff --git a/bin/fscrolled.tcl b/bin/fscrolled.tcl new file mode 100644 index 0000000..26cafc8 --- /dev/null +++ b/bin/fscrolled.tcl @@ -0,0 +1,96 @@ +namespace eval ::scrolledframe {} + +proc ::scrolledframe::scrolledframe {w args} { + eval [linsert $args 0 Create $w] + interp hide {} $w + interp alias {} $w {} [namespace origin WidgetProc] $w + return $w +} + +proc ::scrolledframe::WidgetProc {w args} { +} + +proc ::scrolledframe::Create {w} { + set outer [ttk::frame $w\#f] + ttk::frame $w + set vs [ttk::scrollbar $w\#vs] + $vs configure -command [namespace code [list Scroll $vs $w]] + + place $w -in $outer -anchor nw -x 0 -y 0 + place $vs -in $outer -anchor nw -y 0 -rely 0 -relheight 1.0 \ + -relx 1.0 -x -17 ;#-[winfo width $vs] + + bind $w [namespace code [list Update $vs $w %w %h]] + + return $w +} + +proc ::scrolledframe::Update {scrollbar frame width height} { + puts stderr "Update $width $height" + array set pinfo [place info $frame] + set parent [winfo parent $frame] + set ratio [expr {1.0 / [winfo height $frame]}] + set start [expr {(-$pinfo(-y)) * $ratio}] + set end [expr {$start + ($ratio * [winfo height $parent])}] + + if {$start < 0.0} { + set start 0.0 + } + + if {$end > 1.0} { + set end 1.0 + } + $scrollbar set $start $end +} + +proc ::scrolledframe::Scroll {scrollbar frame type ratio args} { + puts stderr "Scroll $type $ratio $args" + switch -exact -- $type { + moveto { + #Don't allow the frame to scroll beyond the very top + if {$ratio < 0.0} { + set ratio 0.0 + } + + # Don't allow the frame to scroll beyond the frame boundary. + set yratio [expr {1.0 / [winfo height $frame]}] + set parent [winfo parent $frame] + set yratiopeak [expr {$yratio * ([winfo height $frame] - [winfo height $parent])}] + if {$ratio > $yratiopeak} { + set ratio $yratiopeak + } + array set pinfo [place info $frame] + set pixel [expr {-round([winfo height $frame] * $ratio)}] + place $frame -y $pixel + } + scroll { + foreach {count what} $args break + # FIX ME + } + + default { + puts TYPE:$type + } + } +} + +proc Main {} { + set f [scrolledframe::scrolledframe .f] + for {set n 0} {$n < 20} {incr n} { + set w [ttk::label $f.l$n -text "Label $n"] + grid $w -sticky ew + } + + grid $f\#f -sticky news + grid rowconfigure . 0 -weight 1 + grid columnconfigure . 0 -weight 1 + + bind . {console show} + tkwait window . +} + +if {!$tcl_interactive} { + set r [catch [linsert $argv 0 Main] err] + if {$r} {tk_messageBox -message $::errorInfo} + exit $r +} \ No newline at end of file diff --git a/bin/history.tcl b/bin/history.tcl new file mode 100644 index 0000000..d80edae --- /dev/null +++ b/bin/history.tcl @@ -0,0 +1,141 @@ +# history.tcl - Copyright (C) 2008 Pat Thoyts +# +# Get history from tclers.tk for a conference +# +# + +package require Tcl 8.5 ;# uses dict and {*} +source [file join [file dirname [info script]] httpredir.tcl] +if {![catch {package require autoproxy}]} { + autoproxy::init +} + +namespace eval ::tclers.tk { + #variable url_base http://tclers.tk/conferences + variable url_base http://localhost/conferences +} + +proc ::tclers.tk::gethistory {room args} { + # -messagecommand + # -progress +} + +proc ::tclers.tk::Progress {tok total current} { + .htest.f.status.progress configure -value $current -maximum $total +} + +proc ::tclers.tk::GetIndex {room} { + variable url_base + set url ${url_base}/$room + set headers [list Accept-Charset utf-8 Cache-control no-cache] + ::http::geturl2 $url -headers $headers \ + -timeout 120000 \ + -progress [namespace code [list Progress]] \ + -command [namespace code [list GotIndex $url]] +} + +proc ::tclers.tk::GotIndex {url tok} { + if {[catch { + set ncode [::http::ncode $tok] + set status [string tolower [::http::status $tok]] + array set ::TOK [array get $tok] + if {$status eq "ok" && $ncode < 300} { + after idle [namespace code [list ProcessIndex $url [::http::data $tok]]] + } else { + puts stderr "GotIndex Failure: [http::status $tok] [http::code $tok]" + if {$status eq "error"} { puts stderr [http::error $tok] } + } + ::http::cleanup $tok + } err]} { puts stderr $err } +} + +proc ::tclers.tk::ProcessIndex {url data} { + set RE {.*\s([0-9]+) bytes} + foreach line [split $data \n] { + if { [regexp -- $RE $line -> logname size] } { + set logname [string map {"%2d" -} $logname] + set size [expr { $size / 1024 }]k + lappend loglist $logname $size + } + } + + ## Only show 7 days worth. + set loglist [lrange $loglist end-13 end] + #after idle [list after 0 ::tkchat::LoadHistoryFromIndex $loglist] + #foreach {name size} $loglist {} + GetLog $url/[lindex $loglist end-1] +} + +proc ::tclers.tk::GetLog {url} { + set headers [list Accept-Charset utf-8 Cache-control no-cache] + set tok [::http::geturl2 $url -headers $headers -timeout 120000 \ + -progress [namespace code Progress] \ + -command [namespace code GotLog]] +} + +proc ::tclers.tk::GotLog {tok} { + upvar #0 $tok state + if {[catch { + set ncode [::http::ncode $tok] + set status [string tolower [::http::status $tok]] + if {$status eq "ok" && $ncode < 300} { + if {$state(charset) eq "iso8859-1"} { + set data [encoding convertfrom utf-8 [::http::data $tok]] + } else { + set data [::http::data $tok] + } + after idle [namespace code [list ProcessLog $data]] + } else { + puts stderr "GotIndex Failure: [http::status $tok] [http::code $tok]" + if {$status eq "error"} { puts stderr [http::error $tok] } + } + ::http::cleanup $tok + } err]} { puts stderr $err } +} + +proc ::tclers.tk::ProcessLog {data} { + if {[catch { + #.htest.f.txt delete 1.0 end + set interp [interp create -safe] + interp alias $interp m {} [namespace origin Message] + interp eval $interp $data + interp delete $interp + } err]} { + puts stderr "error processing log file: $err" + } +} + +proc ::tclers.tk::Message {when nick msg {opts ""} args} { + if {[catch {clock scan $when -format "%Y-%m-%dT%H:%M:%S%Z" -gmt 1} s]} { + set s [clock scan $when -format "%Y%m%dT%H:%M:%S" -gmt 1] + } + set ts [clock format $s -format "%H:%M"] + if {$opts ne ""} {puts stderr "OPTS: '$opts'"} + .htest.f.txt insert history "$ts " TIMESTAMP "$nick\t$msg\n" [list NICK-$nick MSG] +} + +# testing +proc ::tclers.tk::TestGUI {} { + set dlg [toplevel .htest -class Dialog] + wm withdraw $dlg + wm title $dlg "Test history fetch" + set f [ttk::frame $dlg.f] + text $f.txt -background white -height 8 -width 30 \ + -yscrollcommand [list $f.vs set] -font TkDefaultFont + ttk::scrollbar $f.vs -command [list $f.txt yview] + set status [ttk::frame $f.status] + ttk::label $status.pane0 + ttk::progressbar $status.progress + ttk::sizegrip $status.sg + grid $status.pane0 $status.progress $status.sg -sticky news + grid rowconfigure $status 0 -weight 1 + grid columnconfigure $status 0 -weight 1 + grid $f.txt $f.vs -sticky news + grid $status - -sticky ew + grid rowconfigure $f 0 -weight 1 + grid columnconfigure $f 0 -weight 1 + grid $f -sticky news + grid rowconfigure $dlg 0 -weight 1 + grid columnconfigure $dlg 0 -weight 1 + wm deiconify $dlg +} diff --git a/bin/httpredir.tcl b/bin/httpredir.tcl new file mode 100644 index 0000000..621ee92 --- /dev/null +++ b/bin/httpredir.tcl @@ -0,0 +1,66 @@ +# httpredir.tcl - Copyright (C) 2008 Pat Thoyts +# +# This is a wrapper for the http package that handles redirects. If too +# many redirections are encountered then it is converted into an error +# response and the -command procedure called. +# + +package require Tcl 8.5 ;# uses dict and {*} +package require http 2.5 + +namespace eval ::http { + # This is the set of additional options we take. They are removed before we + # call the http::geturl command but are maintained in this package. + variable extrafields {-redirects -maxredirects} +} + +proc ::http::Redirect {opts tok} { + upvar #0 $tok state + variable extrafields + if {[set ndx [lsearch -nocase $state(meta) location]] != -1} { + if {[dict incr opts -redirects] > [dict get $opts -maxredirects]} { + set state(status) error + set state(error) "Too many redirections. Loop detected." + uplevel #0 [dict get $opts -command] [list $tok] + } else { + # RFC 2626:14.30 specifies the location to be absolute url + set url [lindex $state(meta) [incr ndx]] + # RFC 2616:14.36 if not human generated, include Referer header + dict set opts -headers Referer $state(url) + set args [dict remove $opts {*}$extrafields] + dict set args -command [namespace code [list RedirectCheck $opts]] + after idle [list ::http::geturl $url {*}$args] + } + } else { + set state(status) error + set state(error) "Received [http::code $tok] but no Location header." + uplevel #0 [dict get $opts -command] [list $tok] + } + return +} +proc ::http::RedirectCheck {opts tok} { + set ncode [::http::ncode $tok] + set status [string tolower [::http::status $tok]] + if {$status eq "ok" && $ncode < 400 && $ncode >= 300} { + Redirect $opts $tok + } else { + set state(-command) [dict get $opts -command] + uplevel #0 [dict get $opts -command] [list $tok] + } + ::http::cleanup $tok +} + +proc ::http::geturl2 {url args} { + variable extrafields + set opts [dict create {*}$args] + if {![dict exists $opts -command]} { + return -code error "missing -command argument" + } + if {![dict exists $opts -maxredirects]} { + dict set opts -maxredirects 10 + } + # call the http package with _only_ http package options + set args [dict remove $opts {*}$extrafields] + dict set args -command [namespace code [list RedirectCheck $opts]] + return [http::geturl $url {*}$args] +} diff --git a/bin/images/bullfrog48.gif b/bin/images/bullfrog48.gif new file mode 100644 index 0000000000000000000000000000000000000000..5dcb16bfb3518290029081132ee326ca8c0c1dec GIT binary patch literal 1840 zcmV-02haFNNk%w1VK4wN0O$Vz1P22Q3;+}i0~HPh5)ud!6$2L&0}>Pw78U>-69g6* z2^<#!8W#y86$Ba@1tAy(8W|HE904jA03{m(A{_)E9t|iR03#p(ARZJbA_giT3_2SC zBqR+bB?m1b2OlOBCnfYSF$Xs^ z4mL3%IxrkLG8jBF4mmanOD_OEGzmL26*xB>N-+#MI2Tke2R}RjH$M|dItfHN6-GJ` zOF9ESJRd?m5KTA{Nj(BNK^;9m9X>!9LOvKiJTFE(9XvoISvUYeJ}5{+4^}+|Nfm#BwRrNL`Nh?M;%y207pbNN=qU}O(RK2HdaarPf8z1OfFSR z5?o0QO-n3FO)6kZ0AxuBT1y*OOC(ZBKTb_HR!WV*JM%D7?6wR6h3Zp^xA%eaBZxroZUkkPq{&%J=ky{qBMhuX@Y+|In) z*R}NLtpET2A^8LW3IP8AEC2us05AX?000R70RIUbNU)&6g9sBUT*$DY!-o(fB7C;c z+OA>}9lBv4A`7lqYgQB@rAtqi7Yq~>%BV2g0U#hwxUi7mf{!8#IB@8~0FH+WdRQLJ z7wgX&95{w}Awxz81tCL({Ma*!hlUwK3$DlkAd4m(ESz}o&?ANk5>hI}(R3gL4g){3 z3e-htPmUZI@(dsWLkA2PLOf8Y@nQvvMkn|rS&#^Emxd5A2$6ycI2=*M5nIrKh9ia; z5=R#m1i?W87(lSnL8OR~1PeS!qDdlc)Pad3j#N?!8)diKmji(5DY=q002S+03-ka8MoZ>NDV%S5s53m4AaRIw-iB3Gq-p$h!{mQvBP6+6=ReHrHGu zjTdj+!37Z;AmKp)4z$78Kx;4of{-3a&;S>iozlxK;-K@5G1%zrjW@}NBaSxbh$GD~ zsLWwUAS1Uxgbo>W(1rp872*H~3b)1n&I^!5!jycuj zV#gVqD3XaEuiQaH{sbUg003$N;iAF?2uzTG4w-B+g&M6?6OB3Spwmt~^{kD}G{b;H z4mjW(bIB#AoYHM7lzah&0S*LVz$^oWa|8$=kdQ(G4M^|=8DIPoO*7`WbB;XpRIZLZ z?zA(HINMxvj5CiIqYpmRe4;_Y5p+O6HUtTy*&;4XU_uKvXyCyu4wqv-Ir7Le&pht9 z15Y~OFf7e2<5X3_01YhQ!2&ykF-Adz$iM=`aEnfm;SrzE!Y9IEkA2)@9n|oKF?hiX zYJ3A7>OhA)zCn#}2t*I2;KU)I@B}>I0S0nN20@;&0$T)Q7Me(gDG1>Yxd>zxpP&RR eAOQp*i~t%DHiQT=ps|m4@S`8H0ER#U0suSF8G-@; literal 0 HcmV?d00001 diff --git a/bin/images/chat.gif b/bin/images/chat.gif new file mode 100644 index 0000000000000000000000000000000000000000..facea22f38f127c63ffcc9f754cdb8a6086b3aa3 GIT binary patch literal 855 zcmb`GF^ZIN48K$C2d@rwO@HOsuFrNowq#q!ZnqnLAI*C3gO3{jOF+-WkQnqs3TcJu-saiE2?NFz>)U7sCO=wb6nufN44Gk8< zbdUEqGNK|nV!W0tGqNH(a?FBaWmH9V)R+{#ozWHD(E~?DV=|^U$ zLRbR^X(pis4;F(B^!P;Xrc6!@7sE*Ov6igYOJ-l!y$ap@~&@{5Jc)p#3vl-#>Zs?aQOLI=Zsi-2e6dnMj5!IKq;5BlyYlpD-hbyP%H+J2yt~YF(w8FWn;pT)x<#^ zd<8Vo!59me^M;;8qLNtmPk9Zt{05J(sL^_R37TF9W z39<}1hr9wc4^>4$MX`ud31y}XO@pRGXABrRwoRBOEXG2$iW*ZxovEW?!)6>fHkwT| zUAQh<9y|}ekB*PM4!T`*d+7DBKfqvs;Si${{!RE@rE8=?l;z3ti3BcQZ_nCFWZs&N zKMyu3rC)c)u6#594u^u=`p5elJ-wPb=hn_T4cLccX{G-YPteQ0$|q7JHz6BjE6fWi viR5P-nImcJMf>^1b4c|MOFNN( literal 0 HcmV?d00001 diff --git a/bin/images/dhn.gif b/bin/images/dhn.gif new file mode 100644 index 0000000000000000000000000000000000000000..a7ac96dd3ed727a6b545d95c10793485c7d24acc GIT binary patch literal 91 zcmZ?wbhEHb6krfwSjfl#1pi?`@h1x-7XuT64oDOv&%mTJrGMq>hy2VAjWa7(9bMMM kU&o|gamr;Dr&sUt*#|kK+ZV<}?Kn2!oBt(FT~-Eb0H-G*FaQ7m literal 0 HcmV?d00001 diff --git a/bin/images/dhu.gif b/bin/images/dhu.gif new file mode 100644 index 0000000000000000000000000000000000000000..2d2647517bccac66a3f97057c8c72fff246244e5 GIT binary patch literal 337 zcmZ?wbhEHb6krfwSgOhZ1plR_|I5q&S62G3to&b1{lB`}e=V*5TH62hbpPw={nyv~ zZ)o`62#AdSo0$GL1tPQmW~TowE&f|t{iVe z|M&L#@8kX7&;P%_|No$%|6yVOsSOl=vM_Qn*fZ#Wd;;`=Yr!SboZMBM9%Wsl$L>uulz literal 0 HcmV?d00001 diff --git a/bin/images/mail.gif b/bin/images/mail.gif new file mode 100644 index 0000000000000000000000000000000000000000..1bc63580e48732acd45c2a8c03dd7a23a5c417ec GIT binary patch literal 847 zcmb`GziQNR5X2X34lInsMhGa1MZ_YoQ6d&81pibvDQ&da+9E|Nxh8eM%I(A_@DUEN zu$Y(d5v&B8^)ugRaN+pf-tNxM%>M4>>lY_a-|fZq_!Exp`fS%@OSX0FcDv#CiCGVx zd?Wbn|3yYrL`TS*$TA}=cWR7WXl+}jyl(H*Vm@u)IXp$;izrZE{)F&!gl z8`#ibF--S(kE1d>Q<>wnWK~(6rK~Xvie1^At?V%=dR3`TDK&6pG^cVpM>%YTHDHj2 zCA6rq7;K=yVt6dbInh%S$DOn$8upoQC(7LP!B13Yq~}?$iok4GL0SRhXe{? z4H%?hNh>^93^veUjh%CuoKW}JNc6FmOll3jA{Q1Ed(?xEIE?}Dq8+1Ai=fPav6uARv`OIt@{RBtn*< zC{QxcRA>}b1Et7P=sI#aXG*M)VC|R(WGRhVz6;y24HXIi&ZDdqbCwN`FuR})TKM;yfxULhy2VAjWa8?4wM%y m%$@7rEB!6>*~Cri(Ol29r+>NKl{YtXkLB#2Z7zOw*T!N{yPAX<9|no|IRM|U0nXVyZ?9h z`0wrY-`o4YkN1B+|Ns8}|AT`5hlTy8Hc yakI14OEncSu(8yOrSo#8Gu4a4^Kr4W#xqV85|}ZInQ5*|*YZ-2RjZvH8LR +# +# Implementation of a composite widget that displays a set of +# messages from some storage. This could be e-mail or instant-messaging +# or some other source. The upper part shows a summary of each +# message and clicking a message triggers the display in the lower +# section +# +# ------------------------------------------------------------------------- +# TODO: +# - delete +# - images for type/state +# +# - Should abstract the data storage out so that we could use +# sqlite if we have it. Persistence will be simpler as a db. +# Data interface is: add, select, delete so it all looks like sql. +# +# ------------------------------------------------------------------------- + +package require Tk 8.5 + +namespace eval messagewidget { + variable version 1.0.0 + + namespace export messagewidget + + if {[lsearch -exact [font names] MessagewidgetFont] == -1} { + eval [list font create MessagewidgetFont] [font actual TkTextFont] + eval [list font create MessagewidgetBoldFont] \ + [font actual TkTextFont] -weight bold + eval [list font create MessagewidgetItalicFont] \ + [font actual TkTextFont] -slant italic + } + namespace eval ::img {} + set imgdir [file join [file dirname [info script]] images] + image create photo ::img::msgnorm -file [file join $imgdir mail.gif] + image create photo ::img::msgchat -file [file join $imgdir chat.gif] +} + +proc messagewidget::messagewidget {w args} { + Create $w + interp hide {} $w + interp alias {} $w {} [namespace origin WidgetProc] $w + return $w +} + +proc messagewidget::WidgetProc {self cmd args} { + upvar #0 [namespace current]::$self state + switch -exact -- $cmd { + add { + return [uplevel 1 [list [namespace origin Add] $self] $args] + } + summary { + return [uplevel 1 [list [namespace origin Summary] $self] $args] + } + body - + default { + return [uplevel 1 [list [namespace origin Body] $self] $args] + } + } +} + +proc messagewidget::Summary {self args} { + upvar #0 [namespace current]::$self state + if {[llength $args] == 0} { + return $state(mlist) + } + return [uplevel 1 [list $state(mlist)] $args] +} + +proc messagewidget::Body {self args} { + upvar #0 [namespace current]::$self state + if {[llength $args] == 0} { + return $state(body) + } + return [uplevel 1 [list $state(body)] $args] +} + +proc messagewidget::Create {self} { + upvar #0 [set State [namespace current]::$self] state + + set self [ttk::frame $self -class Messagewidget] + set inner [ttk::panedwindow $self.inner -orient vertical] + + # top part shows subject lines, from, date etc + set mlist [ttk::frame $inner.mlist] + set state(mcols) {date from subject} + set state(summary) [ttk::treeview $mlist.tree -height 4 \ + -columns $state(mcols)] + ttk::scrollbar $mlist.vs -command [list $mlist.tree yview] + $mlist.tree configure -yscrollcommand [list $mlist.vs set] + $mlist.tree heading date -anchor w -text Date + $mlist.tree heading from -anchor w -text From + $mlist.tree heading subject -anchor w -text Subject + $mlist.tree column #0 -width 5 + $mlist.tree column date -width \ + [font measure TkHeadingFont "31/12 00:00"] + $mlist.tree column from -width \ + [font measure TkHeadingFont "somenames@xyzzy.xyzzy.com"] + $mlist.tree column subject -anchor w -stretch 1 + $mlist.tree tag bind item \ + [namespace code [list OnSummaryClick $self %W %x %y]] + grid $mlist.tree $mlist.vs -sticky news + Grid $mlist row 0 column 0 + + # lower part displays message + set view [ttk::frame $inner.view] + set state(body) [text $view.body -borderwidth 0 -relief flat \ + -font MessagewidgetFont] + ttk::scrollbar $view.vs -command [list $view.body yview] + $view.body configure -yscrollcommand [list $view.vs set] + grid $view.body $view.vs -sticky news -padx 1 -pady 1 + Grid $view row 0 column 0 + $view.body tag configure header -background LightSteelBlue + $view.body tag configure subject -font MessagewidgetBoldFont + + bind $self "+unset -nocomplain $State" + + $inner add $mlist + $inner add $view -weight 1 + grid $inner -sticky news + Grid $self row 0 column 0 + return $self +} + +proc messagewidget::Grid {w junk row junk column} { + grid rowconfigure $w $row -weight 1 + grid columnconfigure $w $column -weight 1 +} + +proc messagewidget::DisplayTime {time} { + set r $time + catch { + set delta [expr {[clock seconds] - $time}] + if {$delta < 86400} { + set format {%H:%M:%S} + } else { + set format {%a %d %b} + } + set r [clock format $time -format $format] + } + return $r +} + +proc messagewidget::Add {self args} { + upvar #0 [namespace current]::$self state + set msg [eval [linsert $args 0 dict create -state U \ + -from "" -to "" -subject "" -body ""]] + lappend state(messages) $msg + lappend values [DisplayTime [dict get $msg -date]] + lappend values [dict get $msg -from] + lappend values [dict get $msg -subject] + set img ::img::msgnorm + set item [$state(summary) insert {} end -image $img -tags item -values $values] +} + +proc messagewidget::OnSummaryClick {self w x y} { + upvar #0 [namespace current]::$self state + set item [$w identify row $x $y] + set M [lindex $state(messages) [$w index $item]] + # ? dict set $M -state R + $state(summary) item $item -text " " + $state(body) delete 1.0 end + $state(body) insert end \ + "From:\t[dict get $M -from]\n" header \ + "To:\t[dict get $M -to]\n" header \ + "Date:\t[clock format [dict get $M -date]]\n" header \ + "Subject:\t[dict get $M -subject]\n\n" header \ + "[dict get $M -body]\n" body +} + +# ------------------------------------------------------------------------- + +package provide messagewidget $messagewidget::version + +# ------------------------------------------------------------------------- + +# testing + +proc messagewidget::Test {} { + destroy .t + toplevel .t + pack [messagewidget::messagewidget .t.t] -fill both -expand 1 + .t.t add -from rmax@all.tclers.tk -to patthoyts@all.tclers.tk \ + -date 1202540181 -subject "Testing message one" \ + -body [info body [lindex [info procs] [expr {int(rand() * 10)}]]] + .t.t add -from kostix@007sp.ru -to patthoyts@all.tclers.tk \ + -date 1202544181 -subject "Russian testing message" \ + -body [info body [lindex [info procs] [expr {int(rand() * 10)}]]] + .t.t add -from pennythoyts@googlemail.com -to patthoyts@all.tclers.tk \ + -date 1202550181 -subject "Another test" \ + -body [info body [lindex [info procs] [expr {int(rand() * 10)}]]] +} \ No newline at end of file diff --git a/bin/tab.tcl b/bin/tab.tcl new file mode 100644 index 0000000..a4c37aa --- /dev/null +++ b/bin/tab.tcl @@ -0,0 +1,251 @@ +# Replace the standard notebook tab with one that includes a close +# button. +# In future versions of ttk this will be supported more directly when +# the identify command will be able to identify parts of the tab. + +namespace eval ::ButtonNotebook { +} + +# Tk 8.6 has the Visual Styles element engine on windows. If this is +# available we use it to get proper windows close buttons. +# +proc ::ButtonNotebook::CreateElements {} { + if {[lsearch -exact [ttk::style element names] close] == -1} { + if {[catch { + # WINDOW WP_SMALLCLOSEBUTTON (19) + # WINDOW WP_MDICLOSEBUTTON (20) + # WINDOW WP_MDIRESTOREBUTTON (22) + #ttk::style element create close vsapi \ + # WINDOW 20 {disabled 4 {active pressed} 3 active 2 {} 1} + ttk::style element create close vsapi \ + EXPLORERBAR 2 {disabled 4 {active pressed} 3 active 2 {} 1} + ttk::style element create detach vsapi \ + WINDOW 22 {disabled 4 {active pressed} 3 active 2 {} 1} + }]} then { + # No XP element engine - use images... + CreateImageElements + } + } +} + +proc ::ButtonNotebook::CreateImageElements {} { + # Create two image based elements to provide buttons to close the + # tabs or to detach a tab and turn it into a toplevel. + namespace eval ::img {} + set imgdir [file join [file dirname [info script]] images] + image create photo ::img::close -file [file join $imgdir xhn.gif] + image create photo ::img::closepressed -file [file join $imgdir xhd.gif] + image create photo ::img::closeactive -file [file join $imgdir xhu.gif] + image create photo ::img::detach -file [file join $imgdir dhn.gif] + image create photo ::img::detachup -file [file join $imgdir dhu.gif] + image create photo ::img::detachdown -file [file join $imgdir dhd.gif] + if {[lsearch -exact [ttk::style element names] close] == -1} { + if {[catch { + ttk::style element create close image \ + [list ::img::close \ + {active pressed !disabled} ::img::closepressed \ + {active !disabled} ::img::closeactive] \ + -border 3 -sticky {} + ttk::style element create detach image \ + [list ::img::detach \ + {active pressed !disabled} ::img::detachdown \ + {active !disabled} ::img::detachup] \ + -border 3 -sticky {} + } err]} { puts stderr $err } + } +} + +proc ::ButtonNotebook::Init {{pertab 0}} { + CreateElements + + # This places the buttons on the right end of the tab area -- but in + # Tk 8.5 we cannot identify these elements. + if {!$pertab} { + ttk::style layout ButtonNotebook { + ButtonNotebook.client -sticky nswe + ButtonNotebook.close -side right -sticky ne + ButtonNotebook.detach -side right -sticky ne + } + } + + # This places the button elements on each tab which uses quite a + # lot of space but we can identify the elements. Changes to the + # widget state affect all the button elements though. + if {$pertab} { + ttk::style layout ButtonNotebook { + ButtonNotebook.client -sticky nswe + } + ttk::style layout ButtonNotebook.Tab { + ButtonNotebook.tab -sticky nswe -children { + ButtonNotebook.focus -side top -sticky nswe -children { + ButtonNotebook.padding -side right -sticky nswe -children { + ButtonNotebook.close -side right -sticky {} + } + ButtonNotebook.label -side left -sticky {} + } + } + } + if {$::ttk::currentTheme eq "xpnative"} { + ttk::style configure ButtonNotebook.Tab -width -8 + ttk::style configure ButtonNotebook.Tab -padding {8 0 0 0} + } + } + + bind TNotebook {+::ButtonNotebook::Press %W %x %y} + bind TNotebook {+::ButtonNotebook::Drag %W %x %y %X %Y} + bind TNotebook {+::ButtonNotebook::Release %W %x %y %X %Y} + bind TNotebook <> [namespace code [list Init $pertab]] +} + +# Hook in some event extras: +# set the state to pressed if button down over a button element. +proc ::ButtonNotebook::Press {w x y} { + set e [$w identify $x $y] + if {[string match "*close" $e] || [string match "*detach" $e]} { + $w state pressed + } else { + upvar #0 [namespace current]::$w state + if {![info exists state]} { + set state(drag) 1 + set state(drag_index) [$w index @$x,$y] + set state(drag_under) $state(drag_index) + set state(drag_from_x) $x + set state(draw_from_y) $y + set state(drag_indic) [ttk::label $w._indic -text v] + } + } +} + +proc ::ButtonNotebook::Drag {w x y rootX rootY} { + upvar #0 [namespace current]::$w state + if {[info exists state]} { + if {[winfo containing $rootX $rootY] eq $w} { + set index [$w index @$x,$y] + if {$index != $state(drag_under)} { + puts "moved to $index" + place $state(drag_indic) -anchor nw -x $x -y 0 + set state(drag_under) $index + } + } + } +} + +# On release, do the button action if any. +proc ::ButtonNotebook::Release {w x y rootX rootY} { + $w state !pressed + set e [$w identify $x $y] + set index [$w index @$x,$y] + if {[string match "*close" $e]} { + $w forget $index + event generate $w <> + } elseif {[string match "*detach" $e]} { + Detach $w $index + } else { + upvar #0 [namespace current]::$w state + if {[info exists state]} { + set dropwin [winfo containing $rootX $rootY] + if {$dropwin eq {}} { + Detach $w $state(drag_index) + } elseif {$dropwin eq $w && $index != $state(drag_index)} { + Move $w $state(drag_index) $index + } + destroy $state(drag_indic) + unset state + } + } +} + +# Move a tab from old index to new index position. +proc ::ButtonNotebook::Move {notebook old_index new_index} { + set tab [lindex [$notebook tabs] $old_index] + set title [$notebook tab $old_index -text] + $notebook forget $old_index + if {[string is integer -strict $new_index]} { + incr new_index -1 + if {$new_index < 0} {set new_index 0} + if {$new_index > [llength [$notebook tabs]]} { set new_index end } + } else { + set new_index end + } + $notebook insert $new_index $tab -text $title +} + +# Turn a tab into a toplevel (must be a tk::frame) +proc ::ButtonNotebook::Detach {notebook index} { + set tab [lindex [$notebook tabs] $index] + set title [$notebook tab $index -text] + $notebook forget $index + wm manage $tab + wm title $tab $title + wm protocol $tab WM_DELETE_WINDOW \ + [namespace code [list Attach $notebook $tab $index]] + bind $tab \ + [namespace code [list Debug $notebook "Configure %wx%h %x,%y"]] + bind $tab [namespace code [list Debug $notebook "Expose"]] + bind $tab [namespace code [list Debug $notebook "Activate"]] + bind $tab [namespace code [list Debug $notebook "Deactivate"]] + bind $tab [namespace code [list Debug $notebook "Button"]] + bind $tab \ + [namespace code [list Debug $notebook "Visibility %s"]] + + event generate $tab <> +} +proc ::ButtonNotebook::Debug {notebook msg} { + $notebook.page0.text insert end $msg\n {} + $notebook.page0.text see end +} + +# Attach a toplevel to the notebook +proc ::ButtonNotebook::Attach {notebook tab {index end}} { + set title [wm title $tab] + wm forget $tab + if {[catch { + if {[catch {$notebook insert $index $tab -text $title} err]} { + $notebook add $tab -text $title + } + $notebook select $tab + } err]} { + puts stderr "AttachWindow: $err" + wm manage $w + wm title $w $title + } +} +proc ::ButtonNotebook::Test {} { + variable tabtest + set dlg [toplevel .test[incr tabtest]] + wm title $dlg "Notebook test" + wm withdraw $dlg + set nb [ttk::notebook $dlg.nb -style ButtonNotebook] + frame $nb.page0 -background red -width 100 -height 100 + frame $nb.page1 -background blue -width 100 -height 100 + frame $nb.page2 -background green -width 100 -height 100 + frame $nb.page3 -background tomato -width 100 -height 100 + $nb add $nb.page0 -text One + $nb add $nb.page1 -text Two + $nb add $nb.page2 -text Three + $nb add $nb.page3 -text "Some really long label." + + set txt [text $nb.page0.text -height 10 -width 10] + set vs [scrollbar $nb.page0.vs -command [list $txt yview]] + $txt configure -yscrollcommand [list $vs set] + grid $txt $vs -sticky news + grid rowconfigure $nb.page0 0 -weight 1 + grid columnconfigure $nb.page0 0 -weight 1 + + grid $dlg.nb -sticky news + grid rowconfigure $dlg 0 -weight 1 + grid columnconfigure $dlg 0 -weight 1 + + bind TNotebook [string map [list %txt $txt] { + %txt insert end [%W identify %x %y] {} "\n" {} + %txt see end + }] + bind $dlg {console show} + wm withdraw . + wm protocol $dlg WM_DELETE_WINDOW {exit} + wm geometry $dlg 320x240 + wm deiconify $dlg +} + +::ButtonNotebook::Init 1 +if {[winfo class .] eq "Tab"} {::ButtonNotebook::Test; tkwait window .} \ No newline at end of file diff --git a/bin/test/demo.tcl b/bin/test/demo.tcl new file mode 100644 index 0000000..2d992c6 --- /dev/null +++ b/bin/test/demo.tcl @@ -0,0 +1,281 @@ +package require Tk 8.4 +package require http +package require uri +package require autoproxy +package require chatwidget + +package require jlib +package require jlib::connect +package require jlib::disco +package require jlib::roster +package require jlib::muc +package require jlib::vcard + +# Enable proxy-aware TLS sockets. +if {![catch {package require tls}]} { + http::register https 443 ::autoproxy::tls_socket +} + +# Maybe support ipv6 and more efficient sockets on win32 +if {0 && ![catch {package require Iocpsock}]} { + http::register http 80 [info command socket2] +} + +# Use either tile 0.8 or the ttk commands in 8.5a6. +if {[llength [info commands ttk::*]] == 0} { + package require tile 0.8 +} + +# ------------------------------------------------------------------------- + +namespace eval Link { } + +proc OnNetwork {tok cmd args} { + if {[catch { + array set a {-body {} -errormsg {}} + array set a $args + switch -exact -- $cmd { + connect { Log "* connected" } + disconnect { Log "* disconnected" } + networkerror { Log "* Network error: $a(-body)" } + xmpp-streams-error-* - + streamerror { Log "* Stream error: $a(-errormsg)" } + xmlerror { Log "* XML parse error: $a(-errormsg)" } + default { Log "* $cmd $args" } + } + } err]} { Log "OnNetwork: $err" error } +} + +proc OnPresence {tok type args} { + if {[catch [linsert $args 0 OnPresence2 $tok $type] err]} { + Log "OnPresence: $err" error + } + return 0 +} +proc OnPresence2 {tok type args} { + array set a {-from {} -to {} -status {}} + array set a $args + Log "< presence $type $a(-from) $a(-to) $a(-status)" +} + +proc OnIq {tok type args} { + if {[catch [linsert $args 0 OnIq2 $tok $type] err]} { + Log "OnIq: $err" error + } + return 0 +} + +proc OnIq2 {tok type args} { + array set a {-from {} -to {}} + array set a $args + Log "< iq $type $a(-from) $a(-to)" +} + +proc OnMessage {tok type args} { + if {[catch [linsert $args 0 OnMessage2 $tok $type] err]} { + Log "OnMessage: $err" error + } + return 0 +} + +proc OnMessage2 {tok type args} { + array set a {-from {} -to {} -subject {} -body {}} + array set a $args + switch -exact -- $type { + groupchat - + chat { + Print "$a(-from) $a(-body)" + } + headline { + Print "$a(-from) \"$a(-subject)\"\n $a(-body)" + } + error { + Log "Message error: $args" error + } + normal - + default { + Print "$a(-from) $a(-body)" + } + } +} + +proc OnMucEnter {app jlib type args} { + if {[catch { + array set a {-from {} -to {}} + array set a $args + set room [jid !resource $a(-from)] + + if {1} { + variable chatuid ; if {![info exists chatuid]} { set chatuid -1 } + set id chat[incr chatuid] + upvar #0 [set Chat [namespace current]::$id] chat + set chat(app) $app + set chat(type) jabber + set chat(room) $room + set chat(nick) [$jlib muc mynick $room] + set chat(window) [chatwidget::chatwidget $app.$id] + $app.nb add $chat(window) -text $room + } + + Log "< MUC $type $a(-from) $a(-to)" + } err]} { + Log "OnMucEnter $err" + } +} + +proc OnConnect {tok type args} { + Log "OnConnect $tok $type $args" + + switch -exact -- $type { + initnetwork { } + initstream { } + authenticate { } + ok { + $tok send_presence -type available + $tok roster send_get + } + error { } + } +} + +# ------------------------------------------------------------------------- + +# tkjabber::jid -- +# +# A helper function for splitting out parts of Jabber IDs. +# +proc jid {part jid} { + set r {} + if {[regexp {^(?:([^@]*)@)?([^/]+)(?:/(.+))?} $jid \ + -> node domain resource]} { + switch -exact -- $part { + node { set r $node } + domain { set r $domain } + resource { set r $resource } + !resource { set r ${node}@${domain} } + jid { set r $jid } + default { + return -code error "invalid part \"$part\":\ + must be one of node, domain, resource or jid." + } + } + } + return $r +} + +# ------------------------------------------------------------------------- +proc Print {str} {.chat.main insert end $str {} "\n" {}} +proc Log {str {tag {}}} { + .chat.main insert end $str\n [list log $tag] + .chat.main see end +} +proc Exit {app} { destroy $app } + +proc Main {} { + autoproxy::init + set app [toplevel .chat -class Chat] + wm withdraw $app + wm title $app "Chat test app" + + set menu [menu $app.menu -tearoff 0] + $menu add cascade -label File -menu [menu $menu.file -tearoff 0] + $menu.file add command -label "Connect" \ + -command [list [namespace origin Connect] $app] + $menu.file add command -label "Join tcl chat" \ + -command [list [namespace origin JoinRoom] $app tcl@tach.tclers.tk] + $menu.file add command -label "Join test chat" \ + -command [list [namespace origin JoinRoom] $app test@tach.tclers.tk] + $menu.file add separator + $menu.file add command -label Exit \ + -command [list [namespace origin Exit] $app] + $app configure -menu $menu + + ttk::notebook $app.nb + + ttk::frame $app.mainf -style ChatwidgetFrame + text $app.main -yscrollcommand [list $app.mainvs set] -borderwidth 0 -relief flat + ttk::scrollbar $app.mainvs -command [list $app.main yview] + grid $app.main $app.mainvs -in $app.mainf -sticky news -padx 1 -pady 1 + grid rowconfigure $app.mainf 0 -weight 1 + grid columnconfigure $app.mainf 0 -weight 1 + + $app.main tag configure log -foreground black -background grey80 + $app.main tag configure error -foreground red -background grey80 + + $app.nb add $app.mainf -text "Jabber" + + set status [ttk::frame $app.status] + ttk::label $status.pane0 -anchor w + ttk::separator $status.sep0 + ttk::label $status.pane1 -anchor w + ttk::separator $status.sep1 + ttk::sizegrip $status.sizegrip + grid $status.pane0 $status.sep0 $status.pane1 $status.sep1 $status.sizegrip -sticky ew + grid columnconfigure $status 0 -weight 1 + grid rowconfigure $status 0 -weight 1 + + grid $app.nb -sticky news + grid $status -sticky sew + grid rowconfigure $app 0 -weight 1 + grid columnconfigure $app 0 -weight 1 + + bind $app {console show} + + wm geometry .chat 600x400 + wm deiconify $app + + tkwait window . +} + +proc Connect {app} { + set user $::tcl_platform(user) + set server patthoyts.tk + set password SEKRET + set resource JDemo + set jid [jlib::joinjid $user $server $resource] + + variable conn + set conn [jlib::new OnNetwork \ + -iqcommand OnIq \ + -messagecommand OnMessage \ + -presencecommand OnPresence \ + -keepalivesecs 0 \ + -autodiscocaps 1] + + + #$conn roster register_cmd RosterProc + #$conn iq_register get jabber:iq:version OnGetVersion + #$conn presence_register subscribe OnSubscribe + #$conn presence_register subscribed OnSubscribed + #$conn presence_register unsubscribe OnUnsubscribe + #$conn presence_register unsubscribed OnUnsubscribed + $conn connect init + $conn connect configure -defaultresource "Chatdemo" + # -defaultport 5222 -defaultsslport 5223 + + $conn connect connect $jid $password -command OnConnect \ + -secure 1 -method sasl \ + -ip localhost -port 3128 + #$conn send_message $jid -type chat -subject Subject -body $text +} + +proc JoinRoom {app room {nick testing}} { + variable conn + $conn muc enter $room $nick -command [list OnMucEnter $app] +} + +# ------------------------------------------------------------------------- + +if {![info exists initialized] && !$tcl_interactive} { + set initialized 1 + wm withdraw . + set r [catch [linsert $argv 0 Main] err] + if {$r} {puts $::errorInfo} else {puts $err} + exit $r +} + +# ------------------------------------------------------------------------- +# Local variables: +# mode: tcl +# indent-tabs-mode: nil +# End: \ No newline at end of file diff --git a/bin/test/irc.tcl b/bin/test/irc.tcl new file mode 100644 index 0000000..f3a91c6 --- /dev/null +++ b/bin/test/irc.tcl @@ -0,0 +1,347 @@ +# irc.tcl - Copyright (C) 2007 Pat Thoyts +# +# This is a Tk GUI for the picoirc package that provides a simple IRC client +# + +package require Tk 8.5 +package require chatwidget +package require picoirc + +variable ircuid +if {![info exists ircuid]} { set ircuid -1 } + +# ------------------------------------------------------------------------- + +proc Main {} { + set app [toplevel .chat -class Chat] + wm withdraw $app + wm title $app "Chat test app" + + set menu [menu $app.menu -tearoff 0] + $menu add cascade -label Network -menu [menu $menu.file -tearoff 0] + $menu.file add command -label "Login..." -underline 0 \ + -command [list [namespace origin LoginIRC] $app] + $menu.file add separator + $menu.file add command -label Exit \ + -command [list [namespace origin Exit] $app] + $app configure -menu $menu + + ttk::notebook $app.nb + + set status [ttk::frame $app.status] + ttk::label $status.pane0 -anchor w + ttk::separator $status.sep0 -orient vertical + ttk::label $status.pane1 -anchor w + ttk::separator $status.sep1 -orient vertical + ttk::sizegrip $status.sizegrip + grid $status.pane0 $status.sep0 $status.pane1\ + $status.sep1 $status.sizegrip -sticky news + grid columnconfigure $status 0 -weight 1 + grid rowconfigure $status 0 -weight 1 + + grid $app.nb -sticky news + grid $status -sticky sew + grid rowconfigure $app 0 -weight 1 + grid columnconfigure $app 0 -weight 1 + + bind $app {console show} + + wm geometry .chat 600x400 + wm deiconify $app + + tkwait window $app + return +} + +proc LoginIRC {app} { + set dlg $app.irclogin + variable $dlg {} + variable irc + if {![info exists irc]} { + array set irc {server irc.freenode.net port 6667 channel ""} + } + if {![winfo exists $dlg]} { + set dlg [toplevel $dlg -class Dialog] + wm withdraw $dlg + wm transient $dlg $app + wm title $dlg "IRC Login" + + set f [ttk::frame $dlg.f] + set g [ttk::frame $f.g] + ttk::label $f.sl -text Server + ttk::entry $f.se -textvariable [namespace which -variable irc](server) + ttk::entry $f.sp -textvariable \ + [namespace which -variable irc](port) -width 5 + ttk::label $f.cl -text Channel + ttk::entry $f.cn -textvariable [namespace which -variable irc](channel) + ttk::label $f.nl -text Username + ttk::entry $f.nn -textvariable [namespace which -variable irc](nick) + ttk::button $f.ok -text Login -default active \ + -command [list set [namespace which -variable $dlg] "ok"] + ttk::button $f.cancel -text Cancel \ + -command [list set [namespace which -variable $dlg] "cancel"] + + bind $dlg [list $f.ok invoke] + bind $dlg [list $f.cancel invoke] + wm protocol $dlg WM_DELETE_WINDOW [list $f.cancel invoke] + + grid $f.sl $f.se $f.sp -in $g -sticky new -padx 1 -pady 1 + grid $f.cl $f.cn - -in $g -sticky new -padx 1 -pady 1 + grid $f.nl $f.nn - -in $g -sticky new -padx 1 -pady 1 + grid columnconfigure $g 1 -weight 1 + + grid $g - -sticky news + grid $f.ok $f.cancel -sticky e -padx 1 -pady 1 + grid rowconfigure $f 0 -weight 1 + grid columnconfigure $f 0 -weight 1 + + grid $f -sticky news + grid rowconfigure $dlg 0 -weight 1 + grid columnconfigure $dlg 0 -weight 1 + + wm resizable $dlg 0 0 + raise $dlg + } + + catch {::tk::PlaceWindow $dlg widget $app} + wm deiconify $dlg + tkwait visibility $dlg + focus -force $dlg.f.ok + grab $dlg + vwait [namespace which -variable $dlg] + grab release $dlg + wm withdraw $dlg + + if {[set $dlg] eq "ok"} { + after idle [list [namespace origin IrcConnect] $app \ + -server $irc(server) \ + -port $irc(port) \ + -channel $irc(channel) \ + -nick $irc(nick)] + } +} + +proc Exit {app} { + destroy $app + exit +} + +proc Status {Chat message} { + upvar #0 $Chat chat + $chat(app).status.pane0 configure -text $message +} + +proc State {Chat message} { + upvar #0 $Chat chat + $chat(app).status.pane1 configure -text $message +} + +proc bgerror {args} { + tk_messageBox -icon error -title "Error" -message $::errorInfo +} + +proc Pop {varname {nth 0}} { + upvar $varname args + set r [lindex $args $nth] + set args [lreplace $args $nth $nth] + return $r +} + +# ------------------------------------------------------------------------- +# Handle the IRC transport (using picoirc) + +proc IrcConnect {app args} { + variable ircuid + set id irc[incr ircuid] + set Chat [namespace current]::$id + upvar #0 $Chat chat + set chat(app) $app + set chat(type) irc + + while {[string match -* [set option [lindex $args 0]]]} { + switch -exact -- $option { + -server { set chat(server) [Pop args 1] } + -port { set chat(port) [Pop args 1] } + -channel { set chat(channel) [Pop args 1] } + -nick { set chat(nick) [Pop args 1] } + default { + return -code error "invalid option \"$option\"" + } + } + Pop args + } + set chat(window) [chatwidget::chatwidget $app.$id] + $chat(window) names hide + set chat(targets) [list] + $app.nb add $chat(window) -text $chat(server) + set url irc://$chat(server):$chat(port) + set chat(irc) [picoirc::connect \ + [list [namespace origin IrcCallback] $Chat] \ + $chat(nick) $url] + $chat(window) hook add post [list ::picoirc::post $chat(irc) ""] + bind $chat(window) "+unset -nocomplain $Chat" + return $Chat +} + +proc IrcJoinChannel {Chat args} { + variable ircuid +} + +proc IrcAddChannel {Chat channel} { + upvar #0 $Chat chat + set Channel "${Chat}/$channel" + upvar #0 $Channel chan + array set chan [array get chat] + set chan(channel) $channel + set chan(window) [chatwidget::chatwidget $chat(window)$channel] + lappend chat(targets) [list $channel $chan(window)] + $chat(app).nb add $chan(window) -text $channel + $chat(app).nb select $chan(window) + $chan(window) hook add post [list ::picoirc::post $chan(irc) $channel] + bind $chan(window) "+unset -nocomplain $Channel" + return +} + +proc IrcRemoveChannel {Chat target} { + upvar #0 $Chat chat + Status $Chat "Left channel $target" + set w [IrcFindWindow $Chat $target] + if {[winfo exists $w]} { destroy $w } + if {[set ndx [lsearch -index 0 $chat(targets) $target]] != -1} { + set chat(targets) [lreplace $chat(targets) $ndx $ndx] + } +} + +proc IrcFindWindow {Chat target} { + upvar #0 $Chat chat + set w $chat(window) + if {[set ndx [lsearch -index 0 $chat(targets) $target]] != -1} { + set w [lindex [lindex $chat(targets) $ndx] 1] + } + return $w +} + +proc IrcCallback {Chat context state args} { + upvar #0 $Chat chat + upvar #0 $context irc + switch -exact -- $state { + init { + Status $Chat "Attempting to connect to $irc(server)" + } + connect { + $chat(window) message "Logging into $irc(server) as $irc(nick)" -type system + Status $Chat "Connection to IRC server established." + State $Chat connected + } + close { + if {[llength $args] != 0} { + $chat(window) message "Failed to connect: [lindex $args 0]" -type system + Status $Chat [lindex $args 0] + } else { + $chat(window) message "Disconnected from server" -type system + Status $Chat "Disconnected." + } + State $Chat !connected + } + userlist { + foreach {target users} $args break + set colors {black SteelBlue4 tomato chocolate SeaGreen4 red4 + green4 blue4 pink4} + set w [IrcFindWindow $Chat $target] + set current [$w name list -full] + foreach nick $users { + set opts [list -status online] + if {[string match @* $nick]} { + set nick [string range $nick 1 end] + lappend opts -group operators + } else { lappend opts -group users } + if {[lsearch -index 0 $current $nick] == -1} { + lappend opts -color \ + [lindex $colors [expr {int(rand() * [llength $colors])}]] + } + eval [list $w name add $nick] $opts + } + } + userinfo { + foreach {nick userinfo} $args break + array set info $userinfo + $chat(window) message "$nick $userinfo" -type system + } + chat { + foreach {target nick msg type} $args break + if {$type eq ""} {set type normal} + set w [IrcFindWindow $Chat $target] + $w message $msg -nick $nick -type $type + } + system { + foreach {target msg} $args break + [IrcFindWindow $Chat $target] message $msg -type system + } + topic { + foreach {target topic} $args break + set w [IrcFindWindow $Chat $target] + $w topic show + $w topic set $topic + } + traffic { + foreach {action target nick new} $args break + if {$nick eq $irc(nick)} { + switch -exact -- $action { + left { IrcRemoveChannel $Chat $target } + entered { IrcAddChannel $Chat $target} + } + } + if {$target ne {}} { + set w [IrcFindWindow $Chat $target] + IrcCallbackNick $w $action $target $nick $new + } else { + foreach window_target $chat(targets) { + foreach {window_channel w} $window_target break + set current [$w name list -full] + if {[lsearch -index 0 $current $nick] != -1} { + IrcCallbackNick $w $action $target $nick $new + } + } + } + } + debug { + foreach {type line} $args break + if {![info exists chat(log)]} {set chat(log) [open irc.log a]} + puts $chat(log) "[string toupper [string range $type 0 0]] $line" + } + version { return "" } + default { + $chat(window) message "unknown irc callback \"$state\": $args" -type error + } + } +} + +proc IrcCallbackNick {w action target nick new} { + if {$action eq "nickchange"} { + $w name delete $nick + $w name add $new -group users + $w message "$nick changed to $new" -type system + } else { + switch -exact -- $action { + left { $w name delete $nick } + entered { $w name add $nick -group users } + } + $w message "$nick $action" -type system + } +} + +# ------------------------------------------------------------------------- + +if {![info exists initialized] && !$tcl_interactive} { + set initialized 1 + wm withdraw . + set r [catch [linsert $argv 0 Main] err] + if {$r} {tk_messageBox -icon error -type ok -message $::errorInfo} + exit $r +} + +# ------------------------------------------------------------------------- +# Local variables: +# mode: tcl +# indent-tabs-mode: nil +# End: diff --git a/bin/test/muc-form.xml b/bin/test/muc-form.xml new file mode 100644 index 0000000..05c47ed --- /dev/null +++ b/bin/test/muc-form.xml @@ -0,0 +1,10 @@ + +Room configurationYour room "pat0" has been created! The default configuration is as follows: +- No logging +- No moderation +- Up to 30 participants +- No password required +- No invitation required +- Room is not persistent +- Only admins may change the subject +To accept the default configuration, click OK. To select a different configuration, please complete this formconfigpat0pat0The following messages are sent to legacy clients.has lefthas become availableis now known as03011000By default, new users entering a moderated room are only visitors000By default, only admins can send invites in an invite-only room00If a password is required to enter this room, you must specify the password below.admins0text \ No newline at end of file diff --git a/bin/test/test.tcl b/bin/test/test.tcl new file mode 100644 index 0000000..75e8500 --- /dev/null +++ b/bin/test/test.tcl @@ -0,0 +1,55 @@ +proc Grid {w {row 0} {column 0}} { + grid rowconfigure $w $row -weight 1 + grid columnconfigure $w $column -weight 1 +} + +proc Test {} { + set dlg [toplevel .dlg -class Dialog] + wm withdraw $dlg + wm title $dlg "Testing sashplacement" + set pw [ttk::panedwindow $dlg.pw -orient vertical] + + set lower [ttk::frame $pw.lower] + set text [text $lower.text -relief flat -height 10] + set textvs [scrollbar $lower.vs -command [list $text yview]] + $text configure -yscrollcommand [list scroll_set $textvs $pw] + grid $text $textvs -sticky news + Grid $lower 0 0 + + set upper [ttk::frame $pw.upper] + set peer [$text peer create $upper.text -relief flat -height 1] + set peervs [scrollbar $upper.vs -command [list $peer yview]] + $peer configure -yscrollcommand [list scroll_set $peervs $pw] + grid $peer $peervs -sticky news + Grid $upper 0 0 + + $pw add $upper + $pw add $lower -weight 10 + bind $peer [list map_pane %W $pw 0] + + grid $pw -sticky news + Grid $dlg 0 0 + wm deiconify $dlg +} + +proc map_pane {w pw pos} { + bind $w {} + if {[llength [$pw panes]] > 1} { + after idle [list $pw sashpos 0 $pos] + } +} + +proc scroll_set {scrollbar pw f1 f2} { + $scrollbar set $f1 $f2 + if {($f1 == 0) && ($f2 == 1)} { + grid remove $scrollbar + } else { + if {[llength [$pw panes]] > 1} { + set pos [$pw sashpos 0] + grid $scrollbar + after idle [list $pw sashpos 0 $pos] + } else { + grid $scrollbar + } + } +} diff --git a/bin/test/z_irc.tcl b/bin/test/z_irc.tcl new file mode 100644 index 0000000..91ea61e --- /dev/null +++ b/bin/test/z_irc.tcl @@ -0,0 +1,227 @@ +# ------------------------------------------------------------------------- +# Handle the IRC transport (using picoirc) + +proc IrcConnect {app args} { + variable ircuid + set id irc[incr ircuid] + set Chat [namespace current]::$id + upvar #0 $Chat chat + array set chat [list app $app type irc passwd "" nick ""] + while {[string match -* [set option [lindex $args 0]]]} { + switch -exact -- $option { + -server { set chat(server) [Pop args 1] } + -port { set chat(port) [Pop args 1] } + -channel { set chat(channel) [Pop args 1] } + -nick { set chat(nick) [Pop args 1] } + -passwd { set chat(passwd) [Pop args 1] } + default { + return -code error "invalid option \"$option\"" + } + } + Pop args + } + set chat(window) [chatwidget::chatwidget $app.$id] + $chat(window) names hide + set chat(targets) [list] + $app.nb add $chat(window) -text $chat(server) + $app.nb select $chat(window) + set url irc://$chat(server):$chat(port) + if {[info exists chat(channel)] && $chat(channel) ne ""} { + append url /$chat(channel) + } + set chat(irc) [picoirc::connect \ + [list [namespace origin IrcCallback] $Chat] \ + $chat(nick) $chat(passwd) $url] + $chat(window) hook add post [list ::picoirc::post $chat(irc) ""] + bind $chat(window) "+unset -nocomplain $Chat" + return $Chat +} + +proc IrcJoinChannel {Chat args} { + variable ircuid +} + +proc IrcAddChannel {Chat channel} { + upvar #0 $Chat chat + set Channel "${Chat}/$channel" + upvar #0 $Channel chan + array set chan [array get chat] + set chan(channel) $channel + set chan(window) [chatwidget::chatwidget $chat(window)$channel] + lappend chat(targets) [list $channel $chan(window)] + $chat(app).nb add $chan(window) -text $channel + $chat(app).nb select $chan(window) + set m0 [font measure ChatwidgetFont {[00:00]m}] + set m1 [font measure ChatwidgetFont [string repeat m 10]] + set mm [expr {$m0 + $m1}] + $chan(window) chat configure -tabs [list $m0 $mm] + $chan(window) chat tag configure MSG -lmargin1 $mm -lmargin2 $mm + $chan(window) chat tag configure NICK -font ChatwidgetBoldFont + $chan(window) chat tag configure TYPE-system -font ChatwidgetItalicFont + $chan(window) names tag bind NICK \ + [list [namespace origin ChannelNickMenu] $Channel %W %x %y] + $chan(window) names tag bind NICK \ + [list [namespace origin IrcNickTooltip] $Chat enter %W %x %y] + $chan(window) names tag bind NICK \ + [list [namespace origin IrcNickTooltip] $Chat leave %W %x %y] + $chan(window) hook add post [list ::picoirc::post $chan(irc) $channel] + bind $chan(window) "+unset -nocomplain $Channel" + return +} + +proc IrcRemoveChannel {Chat target} { + upvar #0 $Chat chat + Status $Chat "Left channel $target" + set w [IrcFindWindow $Chat $target] + if {[winfo exists $w]} { destroy $w } + if {[set ndx [lsearch -index 0 $chat(targets) $target]] != -1} { + set chat(targets) [lreplace $chat(targets) $ndx $ndx] + } +} + +proc IrcNickTooltip {Chat type w x y} { + if {[package provide tooltip] eq {}} { return } + set nick [string trim [$w get "@$x,$y linestart" "@$x,$y lineend"]] + if {$nick eq ""} { return } + puts stderr "Tooltip $type $nick" + return +} + +proc IrcFindWindow {Chat target} { + upvar #0 $Chat chat + set w $chat(window) + if {[set ndx [lsearch -nocase -index 0 $chat(targets) $target]] != -1} { + set w [lindex [lindex $chat(targets) $ndx] 1] + } + return $w +} + +proc IrcCallback {Chat context state args} { + upvar #0 $Chat chat + upvar #0 $context irc + switch -exact -- $state { + init { + Status $Chat "Attempting to connect to $irc(server)" + } + connect { + $chat(window) message "Logging into $irc(server) as $irc(nick)" -type system + Status $Chat "Connection to IRC server established." + State $Chat connected + } + close { + if {[llength $args] != 0} { + $chat(window) message "Failed to connect: [lindex $args 0]" -type system + Status $Chat [lindex $args 0] + } else { + $chat(window) message "Disconnected from server" -type system + Status $Chat "Disconnected." + } + State $Chat !connected + } + userlist { + foreach {target users} $args break + set colors {black SteelBlue4 tomato chocolate SeaGreen4 red4 + green4 blue4 pink4} + set w [IrcFindWindow $Chat $target] + set current [$w name list -full] + foreach nick $users { + set opts [list -status online] + if {[string match @* $nick]} { + set nick [string range $nick 1 end] + lappend opts -group operators + } else { lappend opts -group users } + if {[lsearch -index 0 $current $nick] == -1} { + lappend opts -color \ + [lindex $colors [expr {int(rand() * [llength $colors])}]] + } + eval [list $w name add $nick] $opts + } + } + userinfo { + foreach {nick userinfo} $args break + array set info {name {} host {} channels {} userinfo {}} + array set info $userinfo + set chat(userinfo,$nick) [array get info] + } + chat { + foreach {target nick msg type} $args break + if {$type eq ""} {set type normal} + set w [IrcFindWindow $Chat $target] + if {$nick eq "tcl@tach.tclers.tk"} { + set action ""; set jnick "" ; set jnew "" + if {[regexp {^\s*([^ ]+) is now known as (.*)} $msg -> jnick jnew]} { + set action nickchange + } elseif {[regexp {^\s*([^ ]+) has left} $msg -> jnick]} { + set action left + } elseif {[regexp {^\s*([^ ]+) has become available} $msg -> jnick]} { + set action entered + } + if {$action ne ""} { + IrcCallbackNick $w $action $target $jnick $jnew jabber + return + } + } + $w message $msg -nick $nick -type $type + } + system { + foreach {target msg} $args break + [IrcFindWindow $Chat $target] message $msg -type system + } + topic { + foreach {target topic} $args break + set w [IrcFindWindow $Chat $target] + $w topic show + $w topic set $topic + } + traffic { + foreach {action target nick new} $args break + if {$nick eq $irc(nick)} { + switch -exact -- $action { + left { IrcRemoveChannel $Chat $target } + entered { IrcAddChannel $Chat $target} + nickchange { set irc(nick) $new } + } + } + if {$target ne {}} { + set w [IrcFindWindow $Chat $target] + IrcCallbackNick $w $action $target $nick $new + } else { + foreach window_target $chat(targets) { + foreach {window_channel w} $window_target break + set current [$w name list -full] + if {[lsearch -index 0 $current $nick] != -1} { + IrcCallbackNick $w $action $target $nick $new + } + } + } + } + debug { + foreach {type line} $args break + if {[winfo exists $chat(app).nb.debug.text]} { + $chat(app).nb.debug.text insert end "$line\n" $type + } + # You can log raw IRC to file by uncommenting the following lines: + #if {![info exists chat(log)]} {set chat(log) [open irc.log a]} + #puts $chat(log) "[string toupper [string range $type 0 0]] $line" + } + version { return "" } + default { + $chat(window) message "unknown irc callback \"$state\": $args" -type error + } + } +} + +proc IrcCallbackNick {w action target nick new {group users}} { + #puts stderr "process traffic $w $nick $action $new $target" + if {$action eq "nickchange"} { + $w name delete $nick + $w name add $new -group $group + $w message "$nick changed to $new" -type system + } else { + switch -exact -- $action { + left { $w name delete $nick } + entered { $w name add $nick -group $group } + } + $w message "$nick $action" -type system + } +} diff --git a/bullfrog.ico b/bullfrog.ico new file mode 100644 index 0000000000000000000000000000000000000000..75f66f823fca637117d91dac1d752bcf57e49500 GIT binary patch literal 11502 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 SV82(uZxpxS_lfDcnEb!?M$5AR literal 0 HcmV?d00001 diff --git a/lib/autoproxy/autoproxy.tcl b/lib/autoproxy/autoproxy.tcl new file mode 100644 index 0000000..a433e7b --- /dev/null +++ b/lib/autoproxy/autoproxy.tcl @@ -0,0 +1,527 @@ +# autoproxy.tcl - Copyright (C) 2002-2008 Pat Thoyts +# +# On Unix the standard for identifying the local HTTP proxy server +# seems to be to use the environment variable http_proxy or ftp_proxy and +# no_proxy to list those domains to be excluded from proxying. +# +# On Windows we can retrieve the Internet Settings values from the registry +# to obtain pretty much the same information. +# +# With this information we can setup a suitable filter procedure for the +# Tcl http package and arrange for automatic use of the proxy. +# +# Example: +# package require autoproxy +# autoproxy::init +# set tok [http::geturl http://wiki.tcl.tk/] +# http::data $tok +# +# To support https add: +# package require tls +# http::register https 443 ::autoproxy::tls_socket +# +# @(#)$Id: autoproxy.tcl,v 1.13 2008/03/01 00:41:35 andreas_kupries Exp $ + +package require http; # tcl +package require uri; # tcllib +package require base64; # tcllib + +namespace eval ::autoproxy { + variable rcsid {$Id: autoproxy.tcl,v 1.13 2008/03/01 00:41:35 andreas_kupries Exp $} + variable version 1.5.1 + variable options + + if {! [info exists options]} { + array set options { + proxy_host "" + proxy_port 80 + no_proxy {} + basic {} + authProc {} + } + } + + variable uid + if {![info exists uid]} { set uid 0 } + + variable winregkey + set winregkey [join { + HKEY_CURRENT_USER + Software Microsoft Windows + CurrentVersion "Internet Settings" + } \\] +} + +# ------------------------------------------------------------------------- +# Description: +# Obtain configuration options for the server. +# +proc ::autoproxy::cget {option} { + variable options + switch -glob -- $option { + -host - + -proxy_h* { set options(proxy_host) } + -port - + -proxy_p* { set options(proxy_port) } + -no* { set options(no_proxy) } + -basic { set options(basic) } + -authProc { set options(authProc) } + default { + set err [join [lsort [array names options]] ", -"] + return -code error "bad option \"$option\":\ + must be one of -$err" + } + } +} + +# ------------------------------------------------------------------------- +# Description: +# Configure the autoproxy package settings. +# You may only configure one type of authorisation at a time as once we hit +# -basic, -digest or -ntlm - all further args are passed to the protocol +# specific script. +# +# Of course, most of the point of this package is to fill as many of these +# fields as possible automatically. You should call autoproxy::init to +# do automatic configuration and then call this method to refine the details. +# +proc ::autoproxy::configure {args} { + variable options + + if {[llength $args] == 0} { + foreach {opt value} [array get options] { + lappend r -$opt $value + } + return $r + } + + while {[string match "-*" [set option [lindex $args 0]]]} { + switch -glob -- $option { + -host - + -proxy_h* { set options(proxy_host) [Pop args 1]} + -port - + -proxy_p* { set options(proxy_port) [Pop args 1]} + -no* { set options(no_proxy) [Pop args 1] } + -basic { Pop args; configure:basic $args ; break } + -authProc { set options(authProc) [Pop args] } + -- { Pop args; break } + default { + set opts [join [lsort [array names options]] ", -"] + return -code error "bad option \"$option\":\ + must be one of -$opts" + } + } + Pop args + } +} + +# ------------------------------------------------------------------------- +# Description: +# Initialise the http proxy information from the environment or the +# registry (Win32) +# +# This procedure will load the http package and re-writes the +# http::geturl method to add in the authorisation header. +# +# A better solution will be to arrange for the http package to request the +# authorisation key on receiving an authorisation reqest. +# +proc ::autoproxy::init {{httpproxy {}} {no_proxy {}}} { + global tcl_platform + global env + variable winregkey + variable options + + # Look for standard environment variables. + if {[string length $httpproxy] > 0} { + + # nothing to do + + } elseif {[info exists env(http_proxy)]} { + set httpproxy $env(http_proxy) + if {[info exists env(no_proxy)]} { + set no_proxy $env(no_proxy) + } + } else { + if {$tcl_platform(platform) == "windows"} { + #checker -scope block exclude nonPortCmd + package require registry 1.0 + array set reg {ProxyEnable 0 ProxyServer "" ProxyOverride {}} + catch { + # IE5 changed ProxyEnable from a binary to a dword value. + switch -exact -- [registry type $winregkey "ProxyEnable"] { + dword { + set reg(ProxyEnable) [registry get $winregkey "ProxyEnable"] + } + binary { + set v [registry get $winregkey "ProxyEnable"] + binary scan $v i reg(ProxyEnable) + } + default { + return -code error "unexpected type found for\ + ProxyEnable registry item" + } + } + set reg(ProxyServer) [GetWin32Proxy http] + set reg(ProxyOverride) [registry get $winregkey "ProxyOverride"] + } + if {![string is bool $reg(ProxyEnable)]} { + set reg(ProxyEnable) 0 + } + if {$reg(ProxyEnable)} { + set httpproxy $reg(ProxyServer) + set no_proxy $reg(ProxyOverride) + } + } + } + + # If we found something ... + if {[string length $httpproxy] > 0} { + # The http_proxy is supposed to be a URL - lets make sure. + if {![regexp {\w://.*} $httpproxy]} { + set httpproxy "http://$httpproxy" + } + + # decompose the string. + array set proxy [uri::split $httpproxy] + + # turn the no_proxy value into a tcl list + set no_proxy [string map {; " " , " "} $no_proxy] + + # configure ourselves + configure -proxy_host $proxy(host) \ + -proxy_port $proxy(port) \ + -no_proxy $no_proxy + + # Lift the authentication details from the environment if present. + if {[string length $proxy(user)] < 1 \ + && [info exists env(http_proxy_user)] \ + && [info exists env(http_proxy_pass)]} { + set proxy(user) $env(http_proxy_user) + set proxy(pwd) $env(http_proxy_pass) + } + + # Maybe the proxy url has authentication parameters? + # At this time, only Basic is supported. + if {[string length $proxy(user)] > 0} { + configure -basic -username $proxy(user) -password $proxy(pwd) + } + + # setup and configure the http package to use our proxy info. + http::config -proxyfilter [namespace origin filter] + } + return $httpproxy +} + +# autoproxy::GetWin32Proxy -- +# +# Parse the Windows Internet Settings registry key and return the +# protocol proxy requested. If the same proxy is in use for all +# protocols, then that will be returned. Otherwise the string is +# parsed. Example: +# ftp=proxy:80;http=proxy:80;https=proxy:80 +# +proc ::autoproxy::GetWin32Proxy {protocol} { + variable winregkey + #checker exclude nonPortCmd + set proxies [split [registry get $winregkey "ProxyServer"] ";"] + foreach proxy $proxies { + if {[string first = $proxy] == -1} { + return $proxy + } else { + foreach {prot host} [split $proxy =] break + if {[string compare $protocol $prot] == 0} { + return $host + } + } + } + return -code error "failed to identify an '$protocol' proxy" +} + +# ------------------------------------------------------------------------- +# Description: +# Pop the nth element off a list. Used in options processing. +proc ::autoproxy::Pop {varname {nth 0}} { + upvar $varname args + set r [lindex $args $nth] + set args [lreplace $args $nth $nth] + return $r +} + +# ------------------------------------------------------------------------- +# Description +# An example user authentication procedure. +# Returns: +# A two element list consisting of the users authentication id and +# password. +proc ::autoproxy::defAuthProc {{user {}} {passwd {}} {realm {}}} { + if {[string length $realm] > 0} { + set title "Realm: $realm" + } else { + set title {} + } + + # If you are using BWidgets then the following will do: + # + # package require BWidget + # return [PasswdDlg .defAuthDlg -parent {} -transient 0 \ + # -title $title -logintext $user -passwdtext $passwd] + # + # if you just have Tk and no BWidgets -- + + set dlg [toplevel .autoproxy_defAuthProc -class Dialog] + wm title $dlg $title + wm withdraw $dlg + label $dlg.ll -text Login -underline 0 -anchor w + entry $dlg.le -textvariable [namespace current]::${dlg}:l + label $dlg.pl -text Password -underline 0 -anchor w + entry $dlg.pe -show * -textvariable [namespace current]::${dlg}:p + button $dlg.ok -text OK -default active -width -11 \ + -command [list set [namespace current]::${dlg}:ok 1] + grid $dlg.ll $dlg.le -sticky news + grid $dlg.pl $dlg.pe -sticky news + grid $dlg.ok - -sticky e + grid columnconfigure $dlg 1 -weight 1 + bind $dlg [list $dlg.ok invoke] + bind $dlg [list focus $dlg.le] + bind $dlg [list focus $dlg.pe] + variable ${dlg}:l $user; variable ${dlg}:p $passwd + variable ${dlg}:ok 0 + wm deiconify $dlg; focus $dlg.pe; update idletasks + set old [::grab current]; grab $dlg + tkwait variable [namespace current]::${dlg}:ok + grab release $dlg ; if {[llength $old] > 0} {::grab $old} + set r [list [set ${dlg}:l] [set ${dlg}:p]] + unset ${dlg}:l; unset ${dlg}:p; unset ${dlg}:ok + destroy $dlg + return $r +} + +# ------------------------------------------------------------------------- + +# Description: +# Implement support for the Basic authentication scheme (RFC 1945,2617). +# Options: +# -user userid - pass in the user ID (May require Windows NT domain +# as DOMAIN\\username) +# -password pwd - pass in the user's password. +# -realm realm - pass in the http realm. +# +proc ::autoproxy::configure:basic {arglist} { + variable options + array set opts {user {} passwd {} realm {}} + foreach {opt value} $arglist { + switch -glob -- $opt { + -u* { set opts(user) $value} + -p* { set opts(passwd) $value} + -r* { set opts(realm) $value} + default { + return -code error "invalid option \"$opt\": must be one of\ + -username or -password or -realm" + } + } + } + + # If nothing was provided, try calling the authProc + if {$options(authProc) != {} \ + && ($opts(user) == {} || $opts(passwd) == {})} { + set r [$options(authProc) $opts(user) $opts(passwd) $opts(realm)] + set opts(user) [lindex $r 0] + set opts(passwd) [lindex $r 1] + } + + # Store the encoded string to avoid re-encoding all the time. + set options(basic) [list "Proxy-Authorization" \ + [concat "Basic" \ + [base64::encode $opts(user):$opts(passwd)]]] + return +} + +# ------------------------------------------------------------------------- +# Description: +# An http package proxy filter. This attempts to work out if a request +# should go via the configured proxy using a glob comparison against the +# no_proxy list items. A typical no_proxy list might be +# [list localhost *.my.domain.com 127.0.0.1] +# +# If we are going to use the proxy - then insert the proxy authorization +# header. +# +proc ::autoproxy::filter {host} { + variable options + + if {$options(proxy_host) == {}} { + return {} + } + + foreach domain $options(no_proxy) { + if {[string match $domain $host]} { + return {} + } + } + + # Add authorisation header to the request (by Anders Ramdahl) + catch { + upvar state State + if {$options(basic) != {}} { + set State(-headers) [concat $options(basic) $State(-headers)] + } + } + return [list $options(proxy_host) $options(proxy_port)] +} + +# ------------------------------------------------------------------------- +# autoproxy::tls_connect -- +# +# Create a connection to a remote machine through a proxy +# if necessary. This is used by the tls_socket command for +# use with the http package but can also be used more generally +# provided your proxy will permit CONNECT attempts to ports +# other than port 443 (many will not). +# This command defers to 'tunnel_connect' to link to the target +# host and then upgrades the link to SSL/TLS +# +proc ::autoproxy::tls_connect {args} { + variable options + if {[string length $options(proxy_host)] > 0} { + set s [eval [linsert $args 0 tunnel_connect]] + fconfigure $s -blocking 1 -buffering none -translation binary + if {[string equal "-async" [lindex $args end-2]]} { + eval [linsert [lrange $args 0 end-3] 0 ::tls::import $s] + } else { + eval [linsert [lrange $args 0 end-2] 0 ::tls::import $s] + } + } else { + set s [eval [linsert $args 0 ::tls::socket]] + } + return $s +} + +# autoproxy::tunnel_connect -- +# +# Create a connection to a remote machine through a proxy +# if necessary. This is used by the tls_socket command for +# use with the http package but can also be used more generally +# provided your proxy will permit CONNECT attempts to ports +# other than port 443 (many will not). +# Note: this command just opens the socket through the proxy to +# the target machine -- no SSL/TLS negotiation is done yet. +# +proc ::autoproxy::tunnel_connect {args} { + variable options + variable uid + set code ok + if {[string length $options(proxy_host)] > 0} { + set token [namespace current]::[incr uid] + upvar #0 $token state + set state(endpoint) [lrange $args end-1 end] + set state(state) connect + set state(data) "" + set state(useragent) [http::config -useragent] + set state(sock) [::socket $options(proxy_host) $options(proxy_port)] + fileevent $state(sock) writable [namespace code [list tunnel_write $token]] + vwait [set token](state) + + if {[string length $state(error)] > 0} { + set result $state(error) + close $state(sock) + unset state + set code error + } elseif {$state(code) >= 300 || $state(code) < 200} { + set result [lindex $state(headers) 0] + regexp {HTTP/\d.\d\s+\d+\s+(.*)} $result -> result + close $state(sock) + set code error + } else { + set result $state(sock) + } + unset state + } else { + set result [eval [linsert $args 0 ::socket]] + } + return -code $code $result +} + +proc ::autoproxy::tunnel_write {token} { + upvar #0 $token state + variable options + fileevent $state(sock) writable {} + if {[catch {set state(error) [fconfigure $state(sock) -error]} err]} { + set state(error) $err + } + if {[string length $state(error)] > 0} { + set state(state) error + return + } + fconfigure $state(sock) -blocking 0 -buffering line -translation crlf + foreach {host port} $state(endpoint) break + puts $state(sock) "CONNECT $host:$port HTTP/1.1" + puts $state(sock) "Host: $host" + if {[string length $state(useragent)] > 0} { + puts $state(sock) "User-Agent: $state(useragent)" + } + puts $state(sock) "Proxy-Connection: keep-alive" + puts $state(sock) "Connection: keep-alive" + if {[string length $options(basic)] > 0} { + puts $state(sock) [join $options(basic) ": "] + } + puts $state(sock) "" + + fileevent $state(sock) readable [namespace code [list tunnel_read $token]] + return +} + +proc ::autoproxy::tunnel_read {token} { + upvar #0 $token state + set len [gets $state(sock) line] + if {[eof $state(sock)]} { + fileevent $state(sock) readable {} + set state(state) eof + } elseif {$len == 0} { + set state(code) [lindex [split [lindex $state(headers) 0] { }] 1] + fileevent $state(sock) readable {} + set state(state) ok + } else { + lappend state(headers) $line + } +} + +# autoproxy::tls_socket -- +# +# This can be used to handle TLS connections independently of +# proxy presence. It can only be used with the Tcl http package +# and to use it you must do: +# http::register https 443 ::autoproxy::tls_socket +# After that you can use the http::geturl command to access +# secure web pages and any proxy details will be handled for you. +# +proc ::autoproxy::tls_socket {args} { + variable options + + # Look into the http package for the actual target. If a proxy is in use then + # The function appends the proxy host and port and not the target. + + upvar host uhost port uport + set args [lrange $args 0 end-2] + lappend args $uhost $uport + + set s [eval [linsert $args 0 tls_connect]] + + # record the tls connection status in the http state array. + upvar state state + tls::handshake $s + set state(tls_status) [tls::status $s] + + return $s +} + +# ------------------------------------------------------------------------- + +package provide autoproxy $::autoproxy::version + +# ------------------------------------------------------------------------- +# +# Local variables: +# mode: tcl +# indent-tabs-mode: nil +# End: diff --git a/lib/autoproxy/pkgIndex.tcl b/lib/autoproxy/pkgIndex.tcl new file mode 100644 index 0000000..6d1c622 --- /dev/null +++ b/lib/autoproxy/pkgIndex.tcl @@ -0,0 +1,2 @@ +if {![package vsatisfies [package provide Tcl] 8.2]} {return} +package ifneeded autoproxy 1.5.1 [list source [file join $dir autoproxy.tcl]] diff --git a/lib/autosocks/autosocks.tcl b/lib/autosocks/autosocks.tcl new file mode 100644 index 0000000..22b7416 --- /dev/null +++ b/lib/autosocks/autosocks.tcl @@ -0,0 +1,209 @@ +# autosocks.tcl --- +# +# Interface to socks4/5 to make usage of 'socket' transparent. +# Can also be used as a wrapper for the 'socket' command without any +# proxy configured. +# +# (c) 2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# This source file is distributed under the BSD license. +# +# $Id: autosocks.tcl,v 1.9 2007/09/21 09:42:48 matben Exp $ + +package provide autosocks 0.1 + +namespace eval autosocks { + variable options + array set options { + -proxy "" + -proxyhost "" + -proxyport "" + -proxyusername "" + -proxypassword "" + -proxyno "" + -proxyfilter autosocks::filter + } + + variable packs + foreach name {socks4 socks5} { + if {![catch {package require $name}]} { + set packs($name) 1 + } + } +} + +# autosocks::config -- +# +# Get or set configuration options for the SOCKS proxy. +# +# Arguments: +# args: +# -proxy ""|socks4|socks5 +# -proxyhost hostname +# -proxyport port number +# -proxyusername user ID +# -proxypassword (socks5) password +# -proxyno glob list of hosts to not use proxy +# -proxyfilter tclProc {host} +# +# Results: +# one or many option values depending on arguments. + +proc autosocks::config {args} { + variable options + variable packs + if {[llength $args] == 0} { + return [array get options] + } elseif {[llength $args] == 1} { + return $options($args) + } else { + set idx [lsearch $args -proxy] + if {$idx >= 0} { + set proxy [lindex $args [incr idx]] + if {[string length $proxy] && ![info exists packs($proxy)]} { + return -code error "unsupported proxy \"$proxy\"" + } + } + array set options $args + } +} + +proc autosocks::init {} { + # @@@ Here we should get default settings from some system API. +} + +# autosocks::socket -- +# +# Subclassing the 'socket' command. Only client side. +# We use -command tclProc instead of -async + fileevent writable. +# +# Arguments: +# host: the peer address, not SOCKS server +# port: the peer's port number +# args: +# -command tclProc {token status} +# the 'status' is any of: +# ok, error, timeout, network-failure, +# rsp_*, err_* (see socks4/5) + +proc autosocks::socket {host port args} { + variable options + + array set argsA $args + array set optsA $args + unset -nocomplain optsA(-command) + set proxy $options(-proxy) + + set hostport [$options(-proxyfilter) $host] + if {[llength $hostport]} { + set ahost [lindex $hostport 0] + set aport [lindex $hostport 1] + } else { + set ahost $host + set aport $port + } + + # Connect ahost + aport. + if {[info exists argsA(-command)]} { + set sock [eval ::socket -async [array get optsA] {$ahost $aport}] + + # Take some precautions here since WiFi behaves odd. + if {[catch {eof $sock} iseof] || $iseof} { + return -code error eof + } + set err [fconfigure $sock -error] + if {$err ne ""} { + return -code error $err + } + + set token [namespace current]::$sock + variable $token + upvar 0 $token state + + set state(host) $host + set state(port) $port + set state(sock) $sock + set state(cmd) $argsA(-command) + fconfigure $sock -blocking 0 + + # There is a potential problem if the socket becomes writable in + # this call before we return! Therefore 'after idle'. + after idle [list \ + fileevent $sock writable [namespace code [list writable $token]]] + } else { + set sock [eval {::socket $ahost $aport} [array get optsA]] + if {[string length $options(-proxy)]} { + eval {${proxy}::init $sock $host $port} [get_opts] + } + } + return $sock +} + +proc autosocks::get_opts {} { + variable options + + set opts [list] + if {[string length $options(-proxyusername)]} { + lappend opts -username $options(-proxyusername) + } + if {[string length $options(-proxypassword)]} { + lappend opts -password $options(-proxypassword) + } + return $opts +} + +proc autosocks::writable {token} { + variable $token + upvar 0 $token state + variable options + + set proxy $options(-proxy) + set sock $state(sock) + fileevent $sock writable {} + + if {[catch {eof $sock} iseof] || $iseof} { + uplevel #0 $state(cmd) network-failure + unset -nocomplain state + } else { + if {[string length $proxy]} { + if {[catch { + eval { + $options(-proxy)::init $sock $state(host) $state(port) \ + -command [namespace code [list socks_cb $token]] + } [get_opts] + } err]} { + uplevel #0 $state(cmd) $err + unset -nocomplain state + } + } else { + uplevel #0 $state(cmd) ok + unset -nocomplain state + } + } +} + +proc autosocks::socks_cb {token stok status} { + variable $token + upvar 0 $token state + variable options + + uplevel #0 $state(cmd) $status + $options(-proxy)::free $stok + unset -nocomplain state +} + +proc autosocks::filter {host} { + variable options + if {[llength $options(-proxy)]} { + foreach domain $options(-proxyno) { + if {[string match $domain $host]} { + return {} + } + } + return [list $options(-proxyhost) $options(-proxyport)] + } else { + return [list] + } +} diff --git a/lib/autosocks/pkgIndex.tcl b/lib/autosocks/pkgIndex.tcl new file mode 100644 index 0000000..9893df4 --- /dev/null +++ b/lib/autosocks/pkgIndex.tcl @@ -0,0 +1,2 @@ +if {![package vsatisfies [package provide Tcl] 8.4]} {return} +package ifneeded autosocks 0.1 [list source [file join $dir autosocks.tcl]] diff --git a/lib/base64/base64.tcl b/lib/base64/base64.tcl new file mode 100644 index 0000000..3edfd48 --- /dev/null +++ b/lib/base64/base64.tcl @@ -0,0 +1,325 @@ +# base64.tcl -- +# +# Encode/Decode base64 for a string +# Stephen Uhler / Brent Welch (c) 1997 Sun Microsystems +# The decoder was done for exmh by Chris Garrigues +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# RCS: @(#) $Id: base64.tcl,v 1.23 2004/10/03 23:06:55 andreas_kupries Exp $ + +# Version 1.0 implemented Base64_Encode, Base64_Decode +# Version 2.0 uses the base64 namespace +# Version 2.1 fixes various decode bugs and adds options to encode +# Version 2.2 is much faster, Tcl8.0 compatible +# Version 2.2.1 bugfixes +# Version 2.2.2 bugfixes +# Version 2.3 bugfixes and extended to support Trf + +package require Tcl 8.2 +namespace eval ::base64 { + namespace export encode decode +} + +if {![catch {package require Trf 2.0}]} { + # Trf is available, so implement the functionality provided here + # in terms of calls to Trf for speed. + + # ::base64::encode -- + # + # Base64 encode a given string. + # + # Arguments: + # args ?-maxlen maxlen? ?-wrapchar wrapchar? string + # + # If maxlen is 0, the output is not wrapped. + # + # Results: + # A Base64 encoded version of $string, wrapped at $maxlen characters + # by $wrapchar. + + proc ::base64::encode {args} { + # Set the default wrapchar and maximum line length to match the output + # of GNU uuencode 4.2. Various RFCs allow for different wrapping + # characters and wraplengths, so these may be overridden by command line + # options. + set wrapchar "\n" + set maxlen 60 + + if { [llength $args] == 0 } { + error "wrong # args: should be \"[lindex [info level 0] 0]\ + ?-maxlen maxlen? ?-wrapchar wrapchar? string\"" + } + + set optionStrings [list "-maxlen" "-wrapchar"] + for {set i 0} {$i < [llength $args] - 1} {incr i} { + set arg [lindex $args $i] + set index [lsearch -glob $optionStrings "${arg}*"] + if { $index == -1 } { + error "unknown option \"$arg\": must be -maxlen or -wrapchar" + } + incr i + if { $i >= [llength $args] - 1 } { + error "value for \"$arg\" missing" + } + set val [lindex $args $i] + + # The name of the variable to assign the value to is extracted + # from the list of known options, all of which have an + # associated variable of the same name as the option without + # a leading "-". The [string range] command is used to strip + # of the leading "-" from the name of the option. + # + # FRINK: nocheck + set [string range [lindex $optionStrings $index] 1 end] $val + } + + # [string is] requires Tcl8.2; this works with 8.0 too + if {[catch {expr {$maxlen % 2}}]} { + error "expected integer but got \"$maxlen\"" + } + + set string [lindex $args end] + set result [::base64 -mode encode -- $string] + set result [string map [list \n ""] $result] + + if {$maxlen > 0} { + set res "" + set edge [expr {$maxlen - 1}] + while {[string length $result] > $maxlen} { + append res [string range $result 0 $edge]$wrapchar + set result [string range $result $maxlen end] + } + if {[string length $result] > 0} { + append res $result + } + set result $res + } + + return $result + } + + # ::base64::decode -- + # + # Base64 decode a given string. + # + # Arguments: + # string The string to decode. Characters not in the base64 + # alphabet are ignored (e.g., newlines) + # + # Results: + # The decoded value. + + proc ::base64::decode {string} { + regsub -all {\s} $string {} string + ::base64 -mode decode -- $string + } + +} else { + # Without Trf use a pure tcl implementation + + namespace eval base64 { + variable base64 {} + variable base64_en {} + + # We create the auxiliary array base64_tmp, it will be unset later. + + set i 0 + foreach char {A B C D E F G H I J K L M N O P Q R S T U V W X Y Z \ + a b c d e f g h i j k l m n o p q r s t u v w x y z \ + 0 1 2 3 4 5 6 7 8 9 + /} { + set base64_tmp($char) $i + lappend base64_en $char + incr i + } + + # + # Create base64 as list: to code for instance C<->3, specify + # that [lindex $base64 67] be 3 (C is 67 in ascii); non-coded + # ascii chars get a {}. we later use the fact that lindex on a + # non-existing index returns {}, and that [expr {} < 0] is true + # + + # the last ascii char is 'z' + scan z %c len + for {set i 0} {$i <= $len} {incr i} { + set char [format %c $i] + set val {} + if {[info exists base64_tmp($char)]} { + set val $base64_tmp($char) + } else { + set val {} + } + lappend base64 $val + } + + # code the character "=" as -1; used to signal end of message + scan = %c i + set base64 [lreplace $base64 $i $i -1] + + # remove unneeded variables + unset base64_tmp i char len val + + namespace export encode decode + } + + # ::base64::encode -- + # + # Base64 encode a given string. + # + # Arguments: + # args ?-maxlen maxlen? ?-wrapchar wrapchar? string + # + # If maxlen is 0, the output is not wrapped. + # + # Results: + # A Base64 encoded version of $string, wrapped at $maxlen characters + # by $wrapchar. + + proc ::base64::encode {args} { + set base64_en $::base64::base64_en + + # Set the default wrapchar and maximum line length to match the output + # of GNU uuencode 4.2. Various RFCs allow for different wrapping + # characters and wraplengths, so these may be overridden by command line + # options. + set wrapchar "\n" + set maxlen 60 + + if { [llength $args] == 0 } { + error "wrong # args: should be \"[lindex [info level 0] 0]\ + ?-maxlen maxlen? ?-wrapchar wrapchar? string\"" + } + + set optionStrings [list "-maxlen" "-wrapchar"] + for {set i 0} {$i < [llength $args] - 1} {incr i} { + set arg [lindex $args $i] + set index [lsearch -glob $optionStrings "${arg}*"] + if { $index == -1 } { + error "unknown option \"$arg\": must be -maxlen or -wrapchar" + } + incr i + if { $i >= [llength $args] - 1 } { + error "value for \"$arg\" missing" + } + set val [lindex $args $i] + + # The name of the variable to assign the value to is extracted + # from the list of known options, all of which have an + # associated variable of the same name as the option without + # a leading "-". The [string range] command is used to strip + # of the leading "-" from the name of the option. + # + # FRINK: nocheck + set [string range [lindex $optionStrings $index] 1 end] $val + } + + # [string is] requires Tcl8.2; this works with 8.0 too + if {[catch {expr {$maxlen % 2}}]} { + error "expected integer but got \"$maxlen\"" + } + + set string [lindex $args end] + + set result {} + set state 0 + set length 0 + + + # Process the input bytes 3-by-3 + + binary scan $string c* X + foreach {x y z} $X { + # Do the line length check before appending so that we don't get an + # extra newline if the output is a multiple of $maxlen chars long. + if {$maxlen && $length >= $maxlen} { + append result $wrapchar + set length 0 + } + + append result [lindex $base64_en [expr {($x >>2) & 0x3F}]] + if {$y != {}} { + append result [lindex $base64_en [expr {(($x << 4) & 0x30) | (($y >> 4) & 0xF)}]] + if {$z != {}} { + append result \ + [lindex $base64_en [expr {(($y << 2) & 0x3C) | (($z >> 6) & 0x3)}]] + append result [lindex $base64_en [expr {($z & 0x3F)}]] + } else { + set state 2 + break + } + } else { + set state 1 + break + } + incr length 4 + } + if {$state == 1} { + append result [lindex $base64_en [expr {(($x << 4) & 0x30)}]]== + } elseif {$state == 2} { + append result [lindex $base64_en [expr {(($y << 2) & 0x3C)}]]= + } + return $result + } + + # ::base64::decode -- + # + # Base64 decode a given string. + # + # Arguments: + # string The string to decode. Characters not in the base64 + # alphabet are ignored (e.g., newlines) + # + # Results: + # The decoded value. + + proc ::base64::decode {string} { + if {[string length $string] == 0} {return ""} + + set base64 $::base64::base64 + set output "" ; # Fix for [Bug 821126] + + binary scan $string c* X + foreach x $X { + set bits [lindex $base64 $x] + if {$bits >= 0} { + if {[llength [lappend nums $bits]] == 4} { + foreach {v w z y} $nums break + set a [expr {($v << 2) | ($w >> 4)}] + set b [expr {(($w & 0xF) << 4) | ($z >> 2)}] + set c [expr {(($z & 0x3) << 6) | $y}] + append output [binary format ccc $a $b $c] + set nums {} + } + } elseif {$bits == -1} { + # = indicates end of data. Output whatever chars are left. + # The encoding algorithm dictates that we can only have 1 or 2 + # padding characters. If x=={}, we have 12 bits of input + # (enough for 1 8-bit output). If x!={}, we have 18 bits of + # input (enough for 2 8-bit outputs). + + foreach {v w z} $nums break + set a [expr {($v << 2) | (($w & 0x30) >> 4)}] + + if {$z == {}} { + append output [binary format c $a ] + } else { + set b [expr {(($w & 0xF) << 4) | (($z & 0x3C) >> 2)}] + append output [binary format cc $a $b] + } + break + } else { + # RFC 2045 says that line breaks and other characters not part + # of the Base64 alphabet must be ignored, and that the decoder + # can optionally emit a warning or reject the message. We opt + # not to do so, but to just ignore the character. + continue + } + } + return $output + } +} + +package provide base64 2.3.1 diff --git a/lib/base64/pkgIndex.tcl b/lib/base64/pkgIndex.tcl new file mode 100644 index 0000000..0c6384c --- /dev/null +++ b/lib/base64/pkgIndex.tcl @@ -0,0 +1,14 @@ +# Tcl package index file, version 1.1 +# This file is generated by the "pkg_mkIndex" command +# and sourced either when an application starts up or +# by a "package unknown" script. It invokes the +# "package ifneeded" command to set up package-related +# information so that packages will be loaded automatically +# in response to "package require" commands. When this +# script is sourced, the variable $dir must contain the +# full path name of this file's directory. + +if {![package vsatisfies [package provide Tcl] 8.2]} {return} +package ifneeded base64 2.3.1 [list source [file join $dir base64.tcl]] +#package ifneeded uuencode 1.1.2 [list source [file join $dir uuencode.tcl]] +#package ifneeded yencode 1.1.1 [list source [file join $dir yencode.tcl]] diff --git a/lib/chatwidget/ChangeLog b/lib/chatwidget/ChangeLog new file mode 100644 index 0000000..50b83e1 --- /dev/null +++ b/lib/chatwidget/ChangeLog @@ -0,0 +1,26 @@ +2008-02-16 Pat Thoyts + + * pkgIndex.tcl: Incremented to 1.1.0 + * chatwidget.tcl: Added support for chatstate notifications. This + is a Jabber (XEP-0085) concept that will likely be useful in many + chat implementations. + Also simpler support for changing the font with a cget/configure + override command and access the chatstate via cget -chatstate. + +2007-10-23 Andreas Kupries + + * chatwidget.man: Fixed syntax error in documentation. + +2007-10-19 Pat Thoyts + + * chatwidget.tcl: Reorganized the widget tree to fix some problems + when adding scrollbars to the panes. Added + accessors for all the components. + +2007-10-19 Pat Thoyts + + * chatwidget.tcl: Initial checkin of a composite widget for use + * chatwidget.man: in chat applications (eg: jabber or irc) + * pkgIndex.tcl: + + diff --git a/lib/chatwidget/chatwidget.man b/lib/chatwidget/chatwidget.man new file mode 100644 index 0000000..4b5db2b --- /dev/null +++ b/lib/chatwidget/chatwidget.man @@ -0,0 +1,116 @@ +[comment {-*- tcl -*- doctools manpage}] +[manpage_begin chatwidget n 1.0.0] +[moddesc {Composite widget for chat applications}] +[titledesc {Provides a multi-paned view suitable for display of chat room or irc channel information}] +[require Tk 8.5] +[require chatwidget [opt 1.0.0]] +[description] + +This is a composite widget designed to simplify the construction of +chat applications. The widget contains display areas for chat +messages, user names and topic and an entry area. It automatically +handles colourization of messages per nick and manages nick +completion. A system of hooks permit the application author to adjust +display features. The main chat display area may be split for use +displaying history or for searching. + +[para] + +The widget is made up of a number of text widget and panedwindow +widgets so that the size of each part of the display may be adjusted +by the user. All the text widgets may be accessed via widget +passthrough commands if fine adjustment is required. The topic and +names sections can also be hidden if desired. + +[section COMMANDS] + +[list_begin definitions] + +[call [cmd ::chatwidget::chatwidget] [arg path] [opt [arg options]]] + +Create a new chatwidget using the Tk window id [arg path]. Any options +provided are currently passed directly to the main chat text widget. + +[list_end] + +[section {WIDGET COMMANDS}] + +[list_begin definitions] + +[call [cmd \$widget] topic [arg command] [arg args]] + +The chat widget can display a topic string, for instance the topic or +name given to a multi-user chatroom or irc channel. +[list_begin commands] +[cmd_def show] +Enable display of the topic. +[cmd_def hide] +Disable display of the topic +[cmd_def "set [arg topic]"] +Set the topic text to [arg topic]. +[list_end] + +[call [cmd \$widget] name [arg nick] [arg args]] + +Control the names and tags associated with names. +[list_begin commands] +[cmd_def "list [opt [arg -full]]"] +Returns a list of all the user names from the names view. If [opt \ +-full] is given then the list returned is a list of lists where each +sublist is made up of the nick followed by any options that have been +set on this nick entry. This may be used to examine any application +specific options that may be applied to a nick when using the +[cmd add] command. +[cmd_def "add [arg nick] [opt [arg options]]"] +[cmd_def "delete [arg nick]"] +[list_end] + +[call [cmd \$widget] message [arg text] [arg args]] + +Add messages to the display. options are -nick, -time, -type, -mark +-tags + +[call [cmd \$widget] hook [arg command] [arg args]] + +Manage hooks. add (message, post names_group, names_nick, chatstate), remove, run + +[call [cmd \$widget] names [arg args]] + +Passthrough to the name display text widget. See the [cmd text] widget manual +for all available commands. The chatwidget provides two additional +commands [cmd show] and [cmd hide] which are used to control the +display of this element in the widget. + +[call [cmd \$widget] entry [arg args]] + +Passthrough to the entry text widget. See the [cmd text] widget manual +for all available commands. + +[list_end] + + +[section EXAMPLE] + +[example { +chatwidget::chatwidget .chat +proc speak {w msg} {$w message $msg -nick user} +.chat hook add post [list speak .chat] +pack .chat -side top -fill both -expand 1 +.chat topic show +.chat topic set "Chat widget demo" +.chat name add "admin" -group admin +.chat name add "user" -group users -color tomato +.chat message "Chatwidget ready" -type system +.chat message "Hello, user" -nick admin +.chat message "Hello, admin" -nick user +}] + +[para] + +A more extensive example is available by examining the code for the picoirc +program in the tclapps repository which ties the tcllib [package picoirc] package to this +[package chatwidget] package to create a simple irc client. + +[see_also text(n)] +[keywords widget {mega-widget} {composite widget} chat irc chatwidget] +[manpage_end] diff --git a/lib/chatwidget/chatwidget.tcl b/lib/chatwidget/chatwidget.tcl new file mode 100644 index 0000000..3fe1bef --- /dev/null +++ b/lib/chatwidget/chatwidget.tcl @@ -0,0 +1,737 @@ +# chatwidget.tcl -- +# +# This package provides a composite widget suitable for use in chat +# applications. A number of panes managed by panedwidgets are available +# for displaying user names, chat text and for entering new comments. +# The main display area makes use of text widget peers to enable a split +# view for history or searching. +# +# Copyright (C) 2007 Pat Thoyts +# +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# RCS: @(#) $Id: chatwidget.tcl,v 1.3 2007/10/24 10:32:19 patthoyts Exp $ + +package require Tk 8.5 + +namespace eval chatwidget { + variable version 1.1.0 + + namespace export chatwidget + + ttk::style layout ChatwidgetFrame { + Entry.field -sticky news -border 1 -children { + ChatwidgetFrame.padding -sticky news + } + } + if {[lsearch -exact [font names] ChatwidgetFont] == -1} { + eval [list font create ChatwidgetFont] [font configure TkTextFont] + eval [list font create ChatwidgetBoldFont] \ + [font configure ChatwidgetFont] -weight bold + eval [list font create ChatwidgetItalicFont] \ + [font configure ChatwidgetFont] -slant italic + } +} + +proc chatwidget::chatwidget {w args} { + Create $w + interp hide {} $w + interp alias {} $w {} [namespace origin WidgetProc] $w + return $w +} + +proc chatwidget::WidgetProc {self cmd args} { + upvar #0 [namespace current]::$self state + switch -- $cmd { + hook { + if {[llength $args] < 2} { + return -code error "wrong \# args: should be\ + \"\$widget hook add|remove|list hook_type ?script? ?priority?\"" + } + return [uplevel 1 [list [namespace origin Hook] $self] $args] + } + cget { + return [uplevel 1 [list [namespace origin Cget] $self] $args] + } + configure { + return [uplevel 1 [list [namespace origin Configure] $self] $args] + } + insert { + return [uplevel 1 [list [namespace origin Insert] $self] $args] + } + message { + return [uplevel 1 [list [namespace origin Message] $self] $args] + } + name { + return [uplevel 1 [list [namespace origin Name] $self] $args] + } + topic { + return [uplevel 1 [list [namespace origin Topic] $self] $args] + } + names { + return [uplevel 1 [list [namespace origin Names] $self] $args] + } + entry { + return [uplevel 1 [list [namespace origin Entry] $self] $args] + } + peer { + return [uplevel 1 [list [namespace origin Peer] $self] $args] + } + chat - + default { + return [uplevel 1 [list [namespace origin Chat] $self] $args] + } + } + return +} + +proc chatwidget::Chat {self args} { + upvar #0 [namespace current]::$self state + if {[llength $args] == 0} { + return $state(chat_widget) + } + return [uplevel 1 [list $state(chat_widget)] $args] +} + +proc chatwidget::Cget {self args} { + upvar #0 [namespace current]::$self state + switch -exact -- [set what [lindex $args 0]] { + -chatstate { return $state(chatstate) } + -history { return $state(history) } + default { + return [uplevel 1 [list $state(chat_widget) cget] $args] + } + } +} + +proc chatwidget::Configure {self args} { + upvar #0 [namespace current]::$self state + switch -exact -- [set option [lindex $args 0]] { + -chatstate { + if {[llength $args] > 1} { set state(chatstate) [Pop args 1] } + else { return $state(chatstate) } + } + -history { + if {[llength $args] > 1} { set state(history) [Pop args 1] } + else { return $state(history) } + } + -font { + if {[llength $args] > 1} { + set font [Pop args 1] + set family [font actual $font -family] + set size [font actual $font -size] + font configure ChatwidgetFont -family $family -size $size + font configure ChatwidgetBoldFont -family $family -size $size + font configure ChatwidgetItalicFont -family $family -size $size + } else { return [$state(chat_widget) cget -font] } + } + default { + return [uplevel 1 [list $state(chat_widget) configure] $args] + } + } +} + +proc chatwidget::Peer {self args} { + upvar #0 [namespace current]::$self state + if {[llength $args] == 0} { + return $state(chat_peer_widget) + } + return [uplevel 1 [list $state(chat_peer_widget)] $args] +} + +proc chatwidget::Topic {self cmd args} { + upvar #0 [namespace current]::$self state + switch -exact -- $cmd { + show { grid $self.topic -row 0 -column 0 -sticky new } + hide { grid forget $self.topic } + set { set state(topic) [lindex $args 0] } + default { + return -code error "bad option \"$cmd\":\ + must be show, hide or set" + } + } +} + +proc chatwidget::Names {self args} { + upvar #0 [namespace current]::$self state + set frame [winfo parent $state(names_widget)] + set pane [winfo parent $frame] + if {[llength $args] == 0} { + return $state(names_widget) + } + if {[llength $args] == 1 && [lindex $args 0] eq "hide"} { + return [$pane forget $frame] + } + if {[llength $args] == 1 && [lindex $args 0] eq "show"} { + return [$pane add $frame] + } + return [uplevel 1 [list $state(names_widget)] $args] +} + +proc chatwidget::Entry {self args} { + upvar #0 [namespace current]::$self state + if {[llength $args] == 0} { + return $state(entry_widget) + } + if {[llength $args] == 1 && [lindex $args 0] eq "text"} { + return [$state(entry_widget) get 1.0 end-1c] + } + return [uplevel 1 [list $state(entry_widget)] $args] +} + +proc chatwidget::Message {self text args} { + upvar #0 [namespace current]::$self state + set chat $state(chat_widget) + + set mark end + set type normal + set nick Unknown + set time [clock seconds] + set tags {} + + while {[string match -* [set option [lindex $args 0]]]} { + switch -exact -- $option { + -nick { set nick [Pop args 1] } + -time { set time [Pop args 1] } + -type { set type [Pop args 1] } + -mark { set type [Pop args 1] } + -tags { set tags [Pop args 1] } + default { + return -code error "unknown option \"$option\"" + } + } + Pop args + } + + if {[catch {Hook $self run message $text \ + -mark $mark -type $type -nick $nick \ + -time $time -tags $tags}] == 3} then { + return + } + + if {$type ne "system"} { lappend tags NICK-$nick } + lappend tags TYPE-$type + $chat configure -state normal + set ts [clock format $time -format "\[%H:%M\]\t"] + $chat insert $mark $ts [concat BOOKMARK STAMP $tags] + if {$type eq "action"} { + $chat insert $mark " * $nick " [concat BOOKMARK NICK $tags] + lappend tags ACTION + } elseif {$type eq "system"} { + } else { + $chat insert $mark "$nick\t" [concat BOOKMARK NICK $tags] + } + if {$type ne "system"} { lappend tags MSG NICK-$nick } + #$chat insert $mark $text $tags + Insert $self $mark $text $tags + $chat insert $mark "\n" $tags + $chat configure -state disabled + if {$state(autoscroll)} { + $chat see end + } + return +} + +proc chatwidget::Insert {self mark args} { + upvar #0 [namespace current]::$self state + if {![info exists state(urluid)]} {set state(urluid) 0} + set w $state(chat_widget) + set parts {} + foreach {s t} $args { + while {[regexp -indices {\m(https?://[^\s]+)} $s -> ndx]} { + foreach {fr bk} $ndx break + lappend parts [string range $s 0 [expr {$fr - 1}]] $t + lappend parts [string range $s $fr $bk] \ + [linsert $t end URL URL-[incr state(urluid)]] + set s [string range $s [incr bk] end] + } + lappend parts $s $t + } + set ws [$w cget -state] + $w configure -state normal + eval [list $w insert $mark] $parts + $w configure -state $ws +} + +# $w name add ericthered -group admin -color red +# state(names) {{pat -color red -group admin -thing wilf} {eric ....}} +proc chatwidget::Name {self cmd args} { + upvar #0 [namespace current]::$self state + switch -exact -- $cmd { + list { + switch -exact -- [lindex $args 0] { + -full { + return $state(names) + } + default { + foreach item $state(names) { lappend r [lindex $item 0] } + return $r + } + } + } + add { + if {[llength $args] < 1 || ([llength $args] % 2) != 1} { + return -code error "wrong # args: should be\ + \"add nick ?-group group ...?\"" + } + set nick [lindex $args 0] + if {[set ndx [lsearch -exact -index 0 $state(names) $nick]] == -1} { + array set opts {-group {} -colour black} + array set opts [lrange $args 1 end] + lappend state(names) [linsert [array get opts] 0 $nick] + } else { + array set opts [lrange [lindex $state(names) $ndx] 1 end] + array set opts [lrange $args 1 end] + lset state(names) $ndx [linsert [array get opts] 0 $nick] + } + UpdateNames $self + } + delete { + if {[llength $args] != 1} { + return -code error "wrong # args: should be \"delete nick\"" + } + set nick [lindex $args 0] + if {[set ndx [lsearch -exact -index 0 $state(names) $nick]] != -1} { + set state(names) [lreplace $state(names) $ndx $ndx] + UpdateNames $self + } + } + get { + if {[llength $args] < 1} { + return -code error "wrong # args:\ + should be \"get nick\" ?option?" + } + set result {} + set nick [lindex $args 0] + if {[set ndx [lsearch -exact -index 0 $state(names) $nick]] != -1} { + set result [lindex $state(names) $ndx] + if {[llength $args] > 1} { + if {[set ndx [lsearch $result [lindex $args 1]]] != -1} { + set result [lindex $result [incr ndx]] + } else { + set result {} + } + } + } + return $result + } + default { + return -code error "bad name option \"$cmd\":\ + must be list, names, add or delete" + } + } +} + +proc chatwidget::UpdateNames {self} { + upvar #0 [namespace current]::$self state + if {[info exists state(updatenames)]} { + after cancel $state(updatenames) + } + set state(updatenames) [after idle [list [namespace origin UpdateNamesExec] $self]] +} + +proc chatwidget::UpdateNamesExec {self} { + upvar #0 [namespace current]::$self state + unset state(updatenames) + set names $state(names_widget) + set chat $state(chat_widget) + + foreach tagname [lsearch -all -inline [$names tag names] NICK-*] { + $names tag delete $tagname + } + foreach tagname [lsearch -all -inline [$names tag names] GROUP-*] { + $names tag delete $tagname + } + + $names configure -state normal + $names delete 1.0 end + array set groups {} + foreach item $state(names) { + set group {} + if {[set ndx [lsearch $item -group]] != -1} { + set group [lindex $item [incr ndx]] + } + lappend groups($group) [lindex $item 0] + } + + foreach group [lsort [array names groups]] { + Hook $self run names_group $group + $names insert end "$group\n" [list SUBTITLE GROUP-$group] + foreach nick [lsort -dictionary $groups($group)] { + $names tag configure NICK-$nick + unset -nocomplain opts ; array set opts {} + if {[set ndx [lsearch -exact -index 0 $state(names) $nick]] != -1} { + array set opts [lrange [lindex $state(names) $ndx] 1 end] + if {[info exists opts(-color)]} { + $names tag configure NICK-$nick -foreground $opts(-color) + $chat tag configure NICK-$nick -foreground $opts(-color) + } + eval [linsert [lindex $state(names) $ndx] 0 \ + Hook $self run names_nick] + } + $names insert end $nick\n [list NICK NICK-$nick GROUP-$group] + } + } + $names insert end "[llength $state(names)] nicks\n" [list SUBTITLE] + + $names configure -state disabled +} + +proc chatwidget::Pop {varname {nth 0}} { + upvar $varname args + set r [lindex $args $nth] + set args [lreplace $args $nth $nth] + return $r +} + +proc chatwidget::Hook {self do type args} { + upvar #0 [namespace current]::$self state + set valid {message post names_group names_nick chatstate url} + if {[lsearch -exact $valid $type] == -1} { + return -code error "unknown hook type \"$type\":\ + must be one of [join $valid ,]" + } + switch -exact -- $do { + add { + if {[llength $args] < 1 || [llength $args] > 2} { + return -code error "wrong # args: should be \"add hook cmd ?priority?\"" + } + foreach {cmd pri} $args break + if {$pri eq {}} { set pri 50 } + lappend state(hook,$type) [list $cmd $pri] + set state(hook,$type) [lsort -real -index 1 [lsort -unique $state(hook,$type)]] + } + remove { + if {[llength $args] != 1} { + return -code error "wrong # args: should be \"remove hook cmd\"" + } + if {![info exists state(hook,$type)]} { return } + for {set ndx 0} {$ndx < [llength $state(hook,$type)]} {incr ndx} { + set item [lindex $state(hook,$type) $ndx] + if {[lindex $item 0] eq [lindex $args 0]} { + set state(hook,$type) [lreplace $state(hook,$type) $ndx $ndx] + break + } + } + set state(hook,$type) + } + run { + if {![info exists state(hook,$type)]} { return } + set res "" + foreach item $state(hook,$type) { + foreach {cmd pri} $item break + set code [catch {eval $cmd $args} err] + if {$code} { + ::bgerror "error running \"$type\" hook: $err" + break + } else { + lappend res $err + } + } + return $res + } + list { + if {[info exists state(hook,$type)]} { + return $state(hook,$type) + } + } + default { + return -code error "unknown hook action \"$do\":\ + must be add, remove, list or run" + } + } +} + +proc chatwidget::Grid {w {row 0} {column 0}} { + grid rowconfigure $w $row -weight 1 + grid columnconfigure $w $column -weight 1 +} + +proc chatwidget::Create {self} { + upvar #0 [set State [namespace current]::$self] state + set state(history) {} + set state(current) 0 + set state(autoscroll) 1 + set state(names) {} + set state(chatstatetimer) {} + set state(chatstate) active + + # NOTE: By using a non-ttk frame as the outermost part we are able + # to be [wm manage]d. The outermost frame should be invisible at all times. + set self [frame $self -class Chatwidget \ + -borderwidth 0 -highlightthickness 0 -relief flat] + set outer [ttk::panedwindow $self.outer -orient vertical] + set inner [ttk::panedwindow $outer.inner -orient horizontal] + + # Create a topic/subject header + set topic [ttk::frame $self.topic] + ttk::label $topic.label -anchor w -text Topic + ttk::entry $topic.text -state disabled -textvariable [set State](topic) + grid $topic.label $topic.text -sticky new -pady {2 0} -padx 1 + Grid $topic 0 1 + + # Create the usernames scrolled text + set names [ttk::frame $inner.names -style ChatwidgetFrame] + text $names.text -borderwidth 0 -relief flat -font ChatwidgetFont + ttk::scrollbar $names.vs -command [list $names.text yview] + $names.text configure -width 10 -height 10 -state disabled \ + -yscrollcommand [list [namespace origin scroll_set] $names.vs $inner 0] + bindtags $names.text [linsert [bindtags $names.text] 1 ChatwidgetNames] + grid $names.text $names.vs -sticky news -padx 1 -pady 1 + Grid $names 0 0 + set state(names_widget) $names.text + + # Create the chat display + set chatf [ttk::frame $inner.chat -style ChatwidgetFrame] + set peers [ttk::panedwindow $chatf.peers -orient vertical] + set upper [ttk::frame $peers.upper] + set lower [ttk::frame $peers.lower] + + set chat [text $lower.text -borderwidth 0 -relief flat -wrap word \ + -state disabled -font ChatwidgetFont] + set chatvs [ttk::scrollbar $lower.vs -command [list $chat yview]] + $chat configure -height 10 -state disabled \ + -yscrollcommand [list [namespace origin scroll_set] $chatvs $peers 1] + grid $chat $chatvs -sticky news + Grid $lower 0 0 + set peer [$chat peer create $upper.text -borderwidth 0 -relief flat \ + -wrap word -state disabled -font ChatwidgetFont] + set peervs [ttk::scrollbar $upper.vs -command [list $peer yview]] + $peer configure -height 0 \ + -yscrollcommand [list [namespace origin scroll_set] $peervs $peers 0] + grid $peer $peervs -sticky news + Grid $upper 0 0 + $peers add $upper + $peers add $lower -weight 1 + grid $peers -sticky news -padx 1 -pady 1 + Grid $chatf 0 0 + bindtags $chat [linsert [bindtags $chat] 1 ChatwidgetText] + set state(chat_widget) $chat + set state(chat_peer_widget) $peer + + # Create the entry widget + set entry [ttk::frame $outer.entry -style ChatwidgetFrame] + text $entry.text -borderwidth 0 -relief flat -font ChatwidgetFont + ttk::scrollbar $entry.vs -command [list $entry.text yview] + $entry.text configure -height 1 \ + -yscrollcommand [list [namespace origin scroll_set] $entry.vs $outer 0] + bindtags $entry.text [linsert [bindtags $entry.text] 1 ChatwidgetEntry] + grid $entry.text $entry.vs -sticky news -padx 1 -pady 1 + Grid $entry 0 0 + set state(entry_widget) $entry.text + + bind ChatwidgetEntry "[namespace origin Post] \[[namespace origin Self] %W\]" + bind ChatwidgetEntry "[namespace origin Post] \[[namespace origin Self] %W\]" + bind ChatwidgetEntry "#" + bind ChatwidgetEntry "#" + bind ChatwidgetEntry "[namespace origin History] \[[namespace origin Self] %W\] prev" + bind ChatwidgetEntry "[namespace origin History] \[[namespace origin Self] %W\] next" + bind ChatwidgetEntry "[namespace origin Nickcomplete] \[[namespace origin Self] %W\]" + bind ChatwidgetEntry "\[[namespace origin Self] %W\] chat yview scroll -1 pages" + bind ChatwidgetEntry "\[[namespace origin Self] %W\] chat yview scroll 1 pages" + bind ChatwidgetEntry "+[namespace origin Chatstate] \[[namespace origin Self] %W\] composing" + bind ChatwidgetEntry "+[namespace origin Chatstate] \[[namespace origin Self] %W\] active" + bind $self "+unset -nocomplain [namespace current]::%W" + bind $peer [list [namespace origin PaneMap] %W $peers 0] + bind $names.text [list [namespace origin PaneMap] %W $inner -90] + bind $entry.text [list [namespace origin PaneMap] %W $outer -28] + + bind ChatwidgetText <> { + ttk::style layout ChatwidgetFrame { + Entry.field -sticky news -border 1 -children { + ChatwidgetFrame.padding -sticky news + } + } + } + + $names.text tag configure SUBTITLE \ + -background grey80 -font ChatwidgetBoldFont + $chat tag configure NICK -font ChatwidgetBoldFont + $chat tag configure TYPE-system -font ChatwidgetItalicFont + $chat tag configure URL -underline 1 + + $inner add $chatf -weight 1 + $inner add $names + $outer add $inner -weight 1 + $outer add $entry + + grid $outer -row 1 -column 0 -sticky news -padx 1 -pady 1 + Grid $self 1 0 + return $self +} + +proc chatwidget::Self {widget} { + set class [winfo class [set w $widget]] + while {[winfo exists $w] && [winfo class $w] ne "Chatwidget"} { + set w [winfo parent $w] + } + if {![winfo exists $w]} { + return -code error "invalid window $widget" + } + return $w +} + +# Set initial position of sash +proc chatwidget::PaneMap {w pane offset} { + bind $pane {} + if {[llength [$pane panes]] > 1} { + if {$offset < 0} { + if {[$pane cget -orient] eq "horizontal"} { + set axis width + } else { + set axis height + } + #after idle [list $pane sashpos 0 [expr {[winfo $axis $pane] + $offset}]] + after idle [namespace code [list PaneMapImpl $pane $axis $offset]] + } else { + #after idle [list $pane sashpos 0 $offset] + after idle [namespace code [list PaneMapImpl $pane {} $offset]] + } + } +} + +proc chatwidget::PaneMapImpl {pane axis offset} { + if {$axis eq {}} { + set size 0 + } else { + set size [winfo $axis $pane] + } + set sashpos [expr {$size + $offset}] + puts stderr "PaneMapImpl $pane $axis $offset : size:$size sashpos:$sashpos" + after 0 [list $pane sashpos 0 $sashpos] +} + +# Handle auto-scroll smarts. This will cause the scrollbar to be removed if +# not required and to disable autoscroll for the text widget if we are not +# tracking the bottom line. +proc chatwidget::scroll_set {scrollbar pw set f1 f2} { + $scrollbar set $f1 $f2 + if {($f1 == 0) && ($f2 == 1)} { + grid remove $scrollbar + } else { + if {[winfo manager $scrollbar] eq {}} {} + if {[llength [$pw panes]] > 1} { + set pos [$pw sashpos 0] + grid $scrollbar + after idle [list $pw sashpos 0 $pos] + } else { + grid $scrollbar + } + + } + if {$set} { + upvar #0 [namespace current]::[Self $scrollbar] state + set state(autoscroll) [expr {(1.0 - $f2) < 1.0e-6 }] + } +} + +proc chatwidget::Post {self} { + set msg [$self entry get 1.0 end-1c] + if {$msg eq ""} { return -code break "" } + if {[catch {Hook $self run post $msg}] != 3} { + $self entry delete 1.0 end + upvar #0 [namespace current]::$self state + set state(history) [lrange [lappend state(history) $msg] end-50 end] + set state(current) [llength $state(history)] + } + return -code break "" +} + +proc chatwidget::History {self dir} { + upvar #0 [namespace current]::$self state + switch -exact -- $dir { + prev { + if {$state(current) == 0} { return } + if {$state(current) == [llength $state(history)]} { + set state(temp) [$self entry get 1.0 end-1c] + } + if {$state(current)} { incr state(current) -1 } + $self entry delete 1.0 end + $self entry insert 1.0 [lindex $state(history) $state(current)] + return + } + next { + if {$state(current) == [llength $state(history)]} { return } + if {[incr state(current)] == [llength $state(history)] && [info exists state(temp)]} { + set msg $state(temp) + } else { + set msg [lindex $state(history) $state(current)] + } + $self entry delete 1.0 end + $self entry insert 1.0 $msg + } + default { + return -code error "invalid direction \"$dir\": + must be either prev or next" + } + } +} + +proc chatwidget::Nickcomplete {self} { + upvar #0 [namespace current]::$self state + if {[info exists state(nickcompletion)]} { + foreach {index matches after} $state(nickcompletion) break + after cancel $after + incr index + if {$index > [llength $matches]} { set index 0 } + set delta 2c + } else { + set delta 1c + set partial [$self entry get "insert - $delta wordstart" "insert - $delta wordend"] + set matches [lsearch -all -inline -glob -index 0 $state(names) $partial*] + set index 0 + } + switch -exact -- [llength $matches] { + 0 { bell ; return -code break ""} + 1 { set match [lindex [lindex $matches 0] 0]} + default { + set match [lindex [lindex $matches $index] 0] + set state(nickcompletion) [list $index $matches \ + [after 2000 [list [namespace origin NickcompleteCleanup] $self]]] + } + } + $self entry delete "insert - $delta wordstart" "insert - $delta wordend" + $self entry insert insert "$match " + return -code break "" +} + +proc chatwidget::NickcompleteCleanup {self} { + upvar #0 [namespace current]::$self state + if {[info exists state(nickcompletion)]} { + unset state(nickcompletion) + } +} + +# Update the widget chatstate (one of active, composing, paused, inactive, gone) +# These are from XEP-0085 but seem likey useful in many chat-type environments. +# Note: this state is _per-widget_. This is not the same as [tk inactive] +# active = got focus and recently active +# composing = typing +# paused = 5 secs non typing +# inactive = no activity for 30 seconds +# gone = no activity for 2 minutes or closed the window +proc chatwidget::Chatstate {self what} { + upvar #0 [namespace current]::$self state + after cancel $state(chatstatetimer) + switch -exact -- $what { + composing - active { + set state(chatstatetimer) [after 5000 [namespace code [list Chatstate $self paused]]] + } + paused { + set state(chatstatetimer) [after 25000 [namespace code [list Chatstate $self inactive]]] + } + inactive { + set state(chatstatetimer) [after 120000 [namespace code [list Chatstate $self gone]]] + } + gone {} + } + set fire [expr {$state(chatstate) eq $what ? 0 : 1}] + set state(chatstate) $what + if {$fire} { + catch {Hook $self run chatstate $what} + event generate $self <> + } +} + +package provide chatwidget $chatwidget::version diff --git a/lib/chatwidget/pkgIndex.tcl b/lib/chatwidget/pkgIndex.tcl new file mode 100644 index 0000000..3685a0e --- /dev/null +++ b/lib/chatwidget/pkgIndex.tcl @@ -0,0 +1 @@ +package ifneeded chatwidget 1.1.0 [list source [file join $dir chatwidget.tcl]] diff --git a/lib/dns/dns.tcl b/lib/dns/dns.tcl new file mode 100644 index 0000000..2d742e6 --- /dev/null +++ b/lib/dns/dns.tcl @@ -0,0 +1,1422 @@ +# dns.tcl - Copyright (C) 2002 Pat Thoyts +# +# Provide a Tcl only Domain Name Service client. See RFC 1034 and RFC 1035 +# for information about the DNS protocol. This should insulate Tcl scripts +# from problems with using the system library resolver for slow name servers. +# +# This implementation uses TCP only for DNS queries. The protocol reccommends +# that UDP be used in these cases but Tcl does not include UDP sockets by +# default. The package should be simple to extend to use a TclUDP extension +# in the future. +# +# Support for SPF (http://spf.pobox.com/rfcs.html) will need updating +# if or when the proposed draft becomes accepted. +# +# Support added for RFC1886 - DNS Extensions to support IP version 6 +# Support added for RFC2782 - DNS RR for specifying the location of services +# Support added for RFC1995 - Incremental Zone Transfer in DNS +# +# TODO: +# - When using tcp we should make better use of the open connection and +# send multiple queries along the same connection. +# +# - We must switch to using TCP for truncated UDP packets. +# +# - Read RFC 2136 - dynamic updating of DNS +# +# ------------------------------------------------------------------------- +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# ------------------------------------------------------------------------- +# +# $Id: dns.tcl,v 1.35 2007/08/26 00:44:34 patthoyts Exp $ + +package require Tcl 8.2; # tcl minimum version +package require logger; # tcllib 1.3 +package require uri; # tcllib 1.1 +package require uri::urn; # tcllib 1.2 +package require ip; # tcllib 1.7 + +namespace eval ::dns { + variable version 1.3.2 + variable rcsid {$Id: dns.tcl,v 1.35 2007/08/26 00:44:34 patthoyts Exp $} + + namespace export configure resolve name address cname \ + status reset wait cleanup errorcode + + variable options + if {![info exists options]} { + array set options { + port 53 + timeout 30000 + protocol tcp + search {} + nameserver {localhost} + loglevel warn + } + variable log [logger::init dns] + ${log}::setlevel $options(loglevel) + } + + # We can use either ceptcl or tcludp for UDP support. + if {![catch {package require udp 1.0.4} msg]} { ;# tcludp 1.0.4+ + # If TclUDP 1.0.4 or better is available, use it. + set options(protocol) udp + } else { + if {![catch {package require ceptcl} msg]} { + set options(protocol) udp + } + } + + variable types + array set types { + A 1 NS 2 MD 3 MF 4 CNAME 5 SOA 6 MB 7 MG 8 MR 9 + NULL 10 WKS 11 PTR 12 HINFO 13 MINFO 14 MX 15 TXT 16 + SPF 16 AAAA 28 SRV 33 IXFR 251 AXFR 252 MAILB 253 MAILA 254 + ANY 255 * 255 + } + + variable classes + array set classes { IN 1 CS 2 CH 3 HS 4 * 255} + + variable uid + if {![info exists uid]} { + set uid 0 + } +} + +# ------------------------------------------------------------------------- + +# Description: +# Configure the DNS package. In particular the local nameserver will need +# to be set. With no options, returns a list of all current settings. +# +proc ::dns::configure {args} { + variable options + variable log + + if {[llength $args] < 1} { + set r {} + foreach opt [lsort [array names options]] { + lappend r -$opt $options($opt) + } + return $r + } + + set cget 0 + if {[llength $args] == 1} { + set cget 1 + } + + while {[string match -* [lindex $args 0]]} { + switch -glob -- [lindex $args 0] { + -n* - + -ser* { + if {$cget} { + return $options(nameserver) + } else { + set options(nameserver) [Pop args 1] + } + } + -po* { + if {$cget} { + return $options(port) + } else { + set options(port) [Pop args 1] + } + } + -ti* { + if {$cget} { + return $options(timeout) + } else { + set options(timeout) [Pop args 1] + } + } + -pr* { + if {$cget} { + return $options(protocol) + } else { + set proto [string tolower [Pop args 1]] + if {[string compare udp $proto] == 0 \ + && [string compare tcp $proto] == 0} { + return -code error "invalid protocol \"$proto\":\ + protocol must be either \"udp\" or \"tcp\"" + } + set options(protocol) $proto + } + } + -sea* { + if {$cget} { + return $options(search) + } else { + set options(search) [Pop args 1] + } + } + -log* { + if {$cget} { + return $options(loglevel) + } else { + set options(loglevel) [Pop args 1] + ${log}::setlevel $options(loglevel) + } + } + -- { Pop args ; break } + default { + set opts [join [lsort [array names options]] ", -"] + return -code error "bad option [lindex $args 0]:\ + must be one of -$opts" + } + } + Pop args + } + + return +} + +# ------------------------------------------------------------------------- + +# Description: +# Create a DNS query and send to the specified name server. Returns a token +# to be used to obtain any further information about this query. +# +proc ::dns::resolve {query args} { + variable uid + variable options + variable log + + # get a guaranteed unique and non-present token id. + set id [incr uid] + while {[info exists [set token [namespace current]::$id]]} { + set id [incr uid] + } + # FRINK: nocheck + variable $token + upvar 0 $token state + + # Setup token/state defaults. + set state(id) $id + set state(query) $query + set state(qdata) "" + set state(opcode) 0; # 0 = query, 1 = inverse query. + set state(-type) A; # DNS record type (A address) + set state(-class) IN; # IN (internet address space) + set state(-recurse) 1; # Recursion Desired + set state(-command) {}; # asynchronous handler + set state(-timeout) $options(timeout); # connection timeout default. + set state(-nameserver) $options(nameserver);# default nameserver + set state(-port) $options(port); # default namerservers port + set state(-search) $options(search); # domain search list + set state(-protocol) $options(protocol); # which protocol udp/tcp + + # Handle DNS URL's + if {[string match "dns:*" $query]} { + array set URI [uri::split $query] + foreach {opt value} [uri::split $query] { + if {$value != {} && [info exists state(-$opt)]} { + set state(-$opt) $value + } + } + set state(query) $URI(query) + ${log}::debug "parsed query: $query" + } + + while {[string match -* [lindex $args 0]]} { + switch -glob -- [lindex $args 0] { + -n* - ns - + -ser* { set state(-nameserver) [Pop args 1] } + -po* { set state(-port) [Pop args 1] } + -ti* { set state(-timeout) [Pop args 1] } + -co* { set state(-command) [Pop args 1] } + -cl* { set state(-class) [Pop args 1] } + -ty* { set state(-type) [Pop args 1] } + -pr* { set state(-protocol) [Pop args 1] } + -sea* { set state(-search) [Pop args 1] } + -re* { set state(-recurse) [Pop args 1] } + -inv* { set state(opcode) 1 } + -status {set state(opcode) 2} + -data { set state(qdata) [Pop args 1] } + default { + set opts [join [lsort [array names state -*]] ", "] + return -code error "bad option [lindex $args 0]: \ + must be $opts" + } + } + Pop args + } + + if {$state(-nameserver) == {}} { + return -code error "no nameserver specified" + } + + if {$state(-protocol) == "udp"} { + if {[llength [package provide ceptcl]] == 0 \ + && [llength [package provide udp]] == 0} { + return -code error "udp support is not available,\ + get ceptcl or tcludp" + } + } + + # Check for reverse lookups + if {[regexp {^(?:\d{0,3}\.){3}\d{0,3}$} $state(query)]} { + set addr [lreverse [split $state(query) .]] + lappend addr in-addr arpa + set state(query) [join $addr .] + set state(-type) PTR + } + + BuildMessage $token + + if {$state(-protocol) == "tcp"} { + TcpTransmit $token + if {$state(-command) == {}} { + wait $token + } + } else { + UdpTransmit $token + } + + return $token +} + +# ------------------------------------------------------------------------- + +# Description: +# Return a list of domain names returned as results for the last query. +# +proc ::dns::name {token} { + set r {} + Flags $token flags + array set reply [Decode $token] + + switch -exact -- $flags(opcode) { + 0 { + # QUERY + foreach answer $reply(AN) { + array set AN $answer + if {![info exists AN(type)]} {set AN(type) {}} + switch -exact -- $AN(type) { + MX - NS - PTR { + if {[info exists AN(rdata)]} {lappend r $AN(rdata)} + } + default { + if {[info exists AN(name)]} { + lappend r $AN(name) + } + } + } + } + } + + 1 { + # IQUERY + foreach answer $reply(QD) { + array set QD $answer + lappend r $QD(name) + } + } + default { + return -code error "not supported for this query type" + } + } + return $r +} + +# Description: +# Return a list of the IP addresses returned for this query. +# +proc ::dns::address {token} { + set r {} + array set reply [Decode $token] + foreach answer $reply(AN) { + array set AN $answer + + if {[info exists AN(type)]} { + switch -exact -- $AN(type) { + "A" { + lappend r $AN(rdata) + } + "AAAA" { + lappend r $AN(rdata) + } + } + } + } + return $r +} + +# Description: +# Return a list of all CNAME results returned for this query. +# +proc ::dns::cname {token} { + set r {} + array set reply [Decode $token] + foreach answer $reply(AN) { + array set AN $answer + + if {[info exists AN(type)]} { + if {$AN(type) == "CNAME"} { + lappend r $AN(rdata) + } + } + } + return $r +} + +# Description: +# Return the decoded answer records. This can be used for more complex +# queries where the answer isn't supported byb cname/address/name. +proc ::dns::result {token args} { + array set reply [eval [linsert $args 0 Decode $token]] + return $reply(AN) +} + +# ------------------------------------------------------------------------- + +# Description: +# Get the status of the request. +# +proc ::dns::status {token} { + upvar #0 $token state + return $state(status) +} + +# Description: +# Get the error message. Empty if no error. +# +proc ::dns::error {token} { + upvar #0 $token state + if {[info exists state(error)]} { + return $state(error) + } + return "" +} + +# Description +# Get the error code. This is 0 for a successful transaction. +# +proc ::dns::errorcode {token} { + upvar #0 $token state + set flags [Flags $token] + set ndx [lsearch -exact $flags errorcode] + incr ndx + return [lindex $flags $ndx] +} + +# Description: +# Reset a connection with optional reason. +# +proc ::dns::reset {token {why reset} {errormsg {}}} { + upvar #0 $token state + set state(status) $why + if {[string length $errormsg] > 0 && ![info exists state(error)]} { + set state(error) $errormsg + } + catch {fileevent $state(sock) readable {}} + Finish $token +} + +# Description: +# Wait for a request to complete and return the status. +# +proc ::dns::wait {token} { + upvar #0 $token state + + if {$state(status) == "connect"} { + vwait [subst $token](status) + } + + return $state(status) +} + +# Description: +# Remove any state associated with this token. +# +proc ::dns::cleanup {token} { + upvar #0 $token state + if {[info exists state]} { + catch {close $state(sock)} + catch {after cancel $state(after)} + unset state + } +} + +# ------------------------------------------------------------------------- + +# Description: +# Dump the raw data of the request and reply packets. +# +proc ::dns::dump {args} { + if {[llength $args] == 1} { + set type -reply + set token [lindex $args 0] + } elseif { [llength $args] == 2 } { + set type [lindex $args 0] + set token [lindex $args 1] + } else { + return -code error "wrong # args:\ + should be \"dump ?option? methodName\"" + } + + # FRINK: nocheck + variable $token + upvar 0 $token state + + set result {} + switch -glob -- $type { + -qu* - + -req* { + set result [DumpMessage $state(request)] + } + -rep* { + set result [DumpMessage $state(reply)] + } + default { + error "unrecognised option: must be one of \ + \"-query\", \"-request\" or \"-reply\"" + } + } + + return $result +} + +# Description: +# Perform a hex dump of binary data. +# +proc ::dns::DumpMessage {data} { + set result {} + binary scan $data c* r + foreach c $r { + append result [format "%02x " [expr {$c & 0xff}]] + } + return $result +} + +# ------------------------------------------------------------------------- + +# Description: +# Contruct a DNS query packet. +# +proc ::dns::BuildMessage {token} { + # FRINK: nocheck + variable $token + upvar 0 $token state + variable types + variable classes + variable options + + if {! [info exists types($state(-type))] } { + return -code error "invalid DNS query type" + } + + if {! [info exists classes($state(-class))] } { + return -code error "invalid DNS query class" + } + + set qdcount 0 + set qsection {} + set nscount 0 + set nsdata {} + + # In theory we can send multiple queries. In practice, named doesn't + # appear to like that much. If it did work we'd do this: + # foreach domain [linsert $options(search) 0 {}] ... + + + # Pack the query: QNAME QTYPE QCLASS + set qsection [PackName $state(query)] + append qsection [binary format SS \ + $types($state(-type))\ + $classes($state(-class))] + incr qdcount + + if {[string length $state(qdata)] > 0} { + set nsdata [eval [linsert $state(qdata) 0 PackRecord]] + incr nscount + } + + switch -exact -- $state(opcode) { + 0 { + # QUERY + set state(request) [binary format SSSSSS $state(id) \ + [expr {($state(opcode) << 11) | ($state(-recurse) << 8)}] \ + $qdcount 0 $nscount 0] + append state(request) $qsection $nsdata + } + 1 { + # IQUERY + set state(request) [binary format SSSSSS $state(id) \ + [expr {($state(opcode) << 11) | ($state(-recurse) << 8)}] \ + 0 $qdcount 0 0 0] + append state(request) \ + [binary format cSSI 0 \ + $types($state(-type)) $classes($state(-class)) 0] + switch -exact -- $state(-type) { + A { + append state(request) \ + [binary format Sc4 4 [split $state(query) .]] + } + PTR { + append state(request) \ + [binary format Sc4 4 [split $state(query) .]] + } + default { + return -code error "inverse query not supported for this type" + } + } + } + default { + return -code error "operation not supported" + } + } + + return +} + +# Pack a human readable dns name into a DNS resource record format. +proc ::dns::PackName {name} { + set data "" + foreach part [split [string trim $name .] .] { + set len [string length $part] + append data [binary format ca$len $len $part] + } + append data \x00 + return $data +} + +# Pack a character string - byte length prefixed +proc ::dns::PackString {text} { + set len [string length $text] + set data [binary format ca$len $len $text] + return $data +} + +# Pack up a single DNS resource record. See RFC1035: 3.2 for the format +# of each type. +# eg: PackRecord name wiki.tcl.tk type MX class IN rdata {10 mail.example.com} +# +proc ::dns::PackRecord {args} { + variable types + variable classes + array set rr {name "" type A class IN ttl 0 rdlength 0 rdata ""} + array set rr $args + set data [PackName $rr(name)] + + switch -exact -- $rr(type) { + CNAME - MB - MD - MF - MG - MR - NS - PTR { + set rr(rdata) [PackName $rr(rdata)] + } + HINFO { + array set r {CPU {} OS {}} + array set r $rr(rdata) + set rr(rdata) [PackString $r(CPU)] + append rr(rdata) [PackString $r(OS)] + } + MINFO { + array set r {RMAILBX {} EMAILBX {}} + array set r $rr(rdata) + set rr(rdata) [PackString $r(RMAILBX)] + append rr(rdata) [PackString $r(EMAILBX)] + } + MX { + foreach {pref exch} $rr(rdata) break + set rr(rdata) [binary format S $pref] + append rr(rdata) [PackName $exch] + } + TXT { + set str $rr(rdata) + set len [string length [set str $rr(rdata)]] + set rr(rdata) "" + for {set n 0} {$n < $len} {incr n} { + set s [string range $str $n [incr n 253]] + append rr(rdata) [PackString $s] + } + } + NULL {} + SOA { + array set r {MNAME {} RNAME {} + SERIAL 0 REFRESH 0 RETRY 0 EXPIRE 0 MINIMUM 0} + array set r $rr(rdata) + set rr(rdata) [PackName $r(MNAME)] + append rr(rdata) [PackName $r(RNAME)] + append rr(rdata) [binary format IIIII $r(SERIAL) \ + $r(REFRESH) $r(RETRY) $r(EXPIRE) $r(MINIMUM)] + } + } + + # append the root label and the type flag and query class. + append data [binary format SSIS $types($rr(type)) \ + $classes($rr(class)) $rr(ttl) [string length $rr(rdata)]] + append data $rr(rdata) + return $data +} + +# ------------------------------------------------------------------------- + +# Description: +# Transmit a DNS request over a tcp connection. +# +proc ::dns::TcpTransmit {token} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + # setup the timeout + if {$state(-timeout) > 0} { + set state(after) [after $state(-timeout) \ + [list [namespace origin reset] \ + $token timeout\ + "operation timed out"]] + } + + # Sometimes DNS servers drop TCP requests. So it's better to + # use asynchronous connect + set s [socket -async $state(-nameserver) $state(-port)] + fileevent $s writable [list [namespace origin TcpConnected] $token $s] + set state(sock) $s + set state(status) connect + + return $token +} + +proc ::dns::TcpConnected {token s} { + variable $token + upvar 0 $token state + + fileevent $s writable {} + if {[catch {fconfigure $s -peername}]} { + # TCP connection failed + Finish $token "can't connect to server" + return + } + + fconfigure $s -blocking 0 -translation binary -buffering none + + # For TCP the message must be prefixed with a 16bit length field. + set req [binary format S [string length $state(request)]] + append req $state(request) + + puts -nonewline $s $req + + fileevent $s readable [list [namespace current]::TcpEvent $token] +} + +# ------------------------------------------------------------------------- +# Description: +# Transmit a DNS request using UDP datagrams +# +# Note: +# This requires a UDP implementation that can transmit binary data. +# As yet I have been unable to test this myself and the tcludp package +# cannot do this. +# +proc ::dns::UdpTransmit {token} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + # setup the timeout + if {$state(-timeout) > 0} { + set state(after) [after $state(-timeout) \ + [list [namespace origin reset] \ + $token timeout\ + "operation timed out"]] + } + + if {[llength [package provide ceptcl]] > 0} { + # using ceptcl + set state(sock) [cep -type datagram $state(-nameserver) $state(-port)] + fconfigure $state(sock) -blocking 0 + } else { + # using tcludp + set state(sock) [udp_open] + udp_conf $state(sock) $state(-nameserver) $state(-port) + } + fconfigure $state(sock) -translation binary -buffering none + set state(status) connect + puts -nonewline $state(sock) $state(request) + + fileevent $state(sock) readable [list [namespace current]::UdpEvent $token] + + return $token +} + +# ------------------------------------------------------------------------- + +# Description: +# Tidy up after a tcp transaction. +# +proc ::dns::Finish {token {errormsg ""}} { + # FRINK: nocheck + variable $token + upvar 0 $token state + global errorInfo errorCode + + if {[string length $errormsg] != 0} { + set state(error) $errormsg + set state(status) error + } + catch {close $state(sock)} + catch {after cancel $state(after)} + if {[info exists state(-command)] && $state(-command) != {}} { + if {[catch {eval $state(-command) {$token}} err]} { + if {[string length $errormsg] == 0} { + set state(error) [list $err $errorInfo $errorCode] + set state(status) error + } + } + if {[info exists state(-command)]} { + unset state(-command) + } + } +} + +# ------------------------------------------------------------------------- + +# Description: +# Handle end-of-file on a tcp connection. +# +proc ::dns::Eof {token} { + # FRINK: nocheck + variable $token + upvar 0 $token state + set state(status) eof + Finish $token +} + +# ------------------------------------------------------------------------- + +# Description: +# Process a DNS reply packet (protocol independent) +# +proc ::dns::Receive {token} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + binary scan $state(reply) SS id flags + set status [expr {$flags & 0x000F}] + + switch -- $status { + 0 { + set state(status) ok + Finish $token + } + 1 { Finish $token "Format error - unable to interpret the query." } + 2 { Finish $token "Server failure - internal server error." } + 3 { Finish $token "Name Error - domain does not exist" } + 4 { Finish $token "Not implemented - the query type is not available." } + 5 { Finish $token "Refused - your request has been refused by the server." } + default { + Finish $token "unrecognised error code: $err" + } + } +} + +# ------------------------------------------------------------------------- + +# Description: +# file event handler for tcp socket. Wait for the reply data. +# +proc ::dns::TcpEvent {token} { + variable log + # FRINK: nocheck + variable $token + upvar 0 $token state + set s $state(sock) + + if {[eof $s]} { + Eof $token + return + } + + set status [catch {read $state(sock)} result] + if {$status != 0} { + ${log}::debug "Event error: $result" + Finish $token "error reading data: $result" + } elseif { [string length $result] >= 0 } { + if {[catch { + # Handle incomplete reads - check the size and keep reading. + if {![info exists state(size)]} { + binary scan $result S state(size) + set result [string range $result 2 end] + } + append state(reply) $result + + # check the length and flags and chop off the tcp length prefix. + if {[string length $state(reply)] >= $state(size)} { + binary scan $result S id + set id [expr {$id & 0xFFFF}] + if {$id != [expr {$state(id) & 0xFFFF}]} { + ${log}::error "received packed with incorrect id" + } + # bug #1158037 - doing this causes problems > 65535 requests! + #Receive [namespace current]::$id + Receive $token + } else { + ${log}::debug "Incomplete tcp read:\ + [string length $state(reply)] should be $state(size)" + } + } err]} { + Finish $token "Event error: $err" + } + } elseif { [eof $state(sock)] } { + Eof $token + } elseif { [fblocked $state(sock)] } { + ${log}::debug "Event blocked" + } else { + ${log}::critical "Event error: this can't happen!" + Finish $token "Event error: this can't happen!" + } +} + +# ------------------------------------------------------------------------- + +# Description: +# file event handler for udp sockets. +proc ::dns::UdpEvent {token} { + # FRINK: nocheck + variable $token + upvar 0 $token state + set s $state(sock) + + set payload [read $state(sock)] + append state(reply) $payload + + binary scan $payload S id + set id [expr {$id & 0xFFFF}] + if {$id != [expr {$state(id) & 0xFFFF}]} { + ${log}::error "received packed with incorrect id" + } + # bug #1158037 - doing this causes problems > 65535 requests! + #Receive [namespace current]::$id + Receive $token +} + +# ------------------------------------------------------------------------- + +proc ::dns::Flags {token {varname {}}} { + # FRINK: nocheck + variable $token + upvar 0 $token state + + if {$varname != {}} { + upvar $varname flags + } + + array set flags {query 0 opcode 0 authoritative 0 errorcode 0 + truncated 0 recursion_desired 0 recursion_allowed 0} + + binary scan $state(reply) SSSSSS mid hdr nQD nAN nNS nAR + + set flags(response) [expr {($hdr & 0x8000) >> 15}] + set flags(opcode) [expr {($hdr & 0x7800) >> 11}] + set flags(authoritative) [expr {($hdr & 0x0400) >> 10}] + set flags(truncated) [expr {($hdr & 0x0200) >> 9}] + set flags(recursion_desired) [expr {($hdr & 0x0100) >> 8}] + set flafs(recursion_allowed) [expr {($hdr & 0x0080) >> 7}] + set flags(errorcode) [expr {($hdr & 0x000F)}] + + return [array get flags] +} + +# ------------------------------------------------------------------------- + +# Description: +# Decode a DNS packet (either query or response). +# +proc ::dns::Decode {token args} { + variable log + # FRINK: nocheck + variable $token + upvar 0 $token state + + array set opts {-rdata 0 -query 0} + while {[string match -* [set option [lindex $args 0]]]} { + switch -exact -- $option { + -rdata { set opts(-rdata) 1 } + -query { set opts(-query) 1 } + default { + return -code error "bad option \"$option\":\ + must be -rdata" + } + } + Pop args + } + + if {$opts(-query)} { + binary scan $state(request) SSSSSSc* mid hdr nQD nAN nNS nAR data + } else { + binary scan $state(reply) SSSSSSc* mid hdr nQD nAN nNS nAR data + } + + set fResponse [expr {($hdr & 0x8000) >> 15}] + set fOpcode [expr {($hdr & 0x7800) >> 11}] + set fAuthoritative [expr {($hdr & 0x0400) >> 10}] + set fTrunc [expr {($hdr & 0x0200) >> 9}] + set fRecurse [expr {($hdr & 0x0100) >> 8}] + set fCanRecurse [expr {($hdr & 0x0080) >> 7}] + set fRCode [expr {($hdr & 0x000F)}] + set flags "" + + if {$fResponse} {set flags "QR"} else {set flags "Q"} + set opcodes [list QUERY IQUERY STATUS] + lappend flags [lindex $opcodes $fOpcode] + if {$fAuthoritative} {lappend flags "AA"} + if {$fTrunc} {lappend flags "TC"} + if {$fRecurse} {lappend flags "RD"} + if {$fCanRecurse} {lappend flags "RA"} + + set info "ID: $mid\ + Fl: [format 0x%02X [expr {$hdr & 0xFFFF}]] ($flags)\ + NQ: $nQD\ + NA: $nAN\ + NS: $nNS\ + AR: $nAR" + ${log}::debug $info + + set ndx 12 + set r {} + set QD [ReadQuestion $nQD $state(reply) ndx] + lappend r QD $QD + set AN [ReadAnswer $nAN $state(reply) ndx $opts(-rdata)] + lappend r AN $AN + set NS [ReadAnswer $nNS $state(reply) ndx $opts(-rdata)] + lappend r NS $NS + set AR [ReadAnswer $nAR $state(reply) ndx $opts(-rdata)] + lappend r AR $AR + return $r +} + +# ------------------------------------------------------------------------- + +proc ::dns::Expand {data} { + set r {} + binary scan $data c* d + foreach c $d { + lappend r [expr {$c & 0xFF}] + } + return $r +} + + +# ------------------------------------------------------------------------- +# Description: +# Pop the nth element off a list. Used in options processing. +# +proc ::dns::Pop {varname {nth 0}} { + upvar $varname args + set r [lindex $args $nth] + set args [lreplace $args $nth $nth] + return $r +} + +# ------------------------------------------------------------------------- +# Description: +# Reverse a list. Code from http://wiki.tcl.tk/tcl/43 +# +proc ::dns::lreverse {lst} { + set res {} + set i [llength $lst] + while {$i} {lappend res [lindex $lst [incr i -1]]} + return $res +} + +# ------------------------------------------------------------------------- + +proc ::dns::KeyOf {arrayname value {default {}}} { + upvar $arrayname array + set lst [array get array] + set ndx [lsearch -exact $lst $value] + if {$ndx != -1} { + incr ndx -1 + set r [lindex $lst $ndx] + } else { + set r $default + } + return $r +} + + +# ------------------------------------------------------------------------- +# Read the question section from a DNS message. This always starts at index +# 12 of a message but may be of variable length. +# +proc ::dns::ReadQuestion {nitems data indexvar} { + variable types + variable classes + upvar $indexvar index + set result {} + + for {set cn 0} {$cn < $nitems} {incr cn} { + set r {} + lappend r name [ReadName data $index offset] + incr index $offset + + # Read off QTYPE and QCLASS for this query. + set ndx $index + incr index 3 + binary scan [string range $data $ndx $index] SS qtype qclass + set qtype [expr {$qtype & 0xFFFF}] + set qclass [expr {$qclass & 0xFFFF}] + incr index + lappend r type [KeyOf types $qtype $qtype] \ + class [KeyOf classes $qclass $qclass] + lappend result $r + } + return $result +} + +# ------------------------------------------------------------------------- + +# Read an answer section from a DNS message. +# +proc ::dns::ReadAnswer {nitems data indexvar {raw 0}} { + variable types + variable classes + upvar $indexvar index + set result {} + + for {set cn 0} {$cn < $nitems} {incr cn} { + set r {} + lappend r name [ReadName data $index offset] + incr index $offset + + # Read off TYPE, CLASS, TTL and RDLENGTH + binary scan [string range $data $index end] SSIS type class ttl rdlength + + set type [expr {$type & 0xFFFF}] + set type [KeyOf types $type $type] + + set class [expr {$class & 0xFFFF}] + set class [KeyOf classes $class $class] + + set ttl [expr {$ttl & 0xFFFFFFFF}] + set rdlength [expr {$rdlength & 0xFFFF}] + incr index 10 + set rdata [string range $data $index [expr {$index + $rdlength - 1}]] + + if {! $raw} { + switch -- $type { + A { + set rdata [join [Expand $rdata] .] + } + AAAA { + set rdata [ip::contract [ip::ToString $rdata]] + } + NS - CNAME - PTR { + set rdata [ReadName data $index off] + } + MX { + binary scan $rdata S preference + set exchange [ReadName data [expr {$index + 2}] off] + set rdata [list $preference $exchange] + } + SRV { + set x $index + set rdata [list priority [ReadUShort data $x off]] + incr x $off + lappend rdata weight [ReadUShort data $x off] + incr x $off + lappend rdata port [ReadUShort data $x off] + incr x $off + lappend rdata target [ReadName data $x off] + incr x $off + } + TXT { + set rdata [ReadString data $index $rdlength] + } + SOA { + set x $index + set rdata [list MNAME [ReadName data $x off]] + incr x $off + lappend rdata RNAME [ReadName data $x off] + incr x $off + lappend rdata SERIAL [ReadULong data $x off] + incr x $off + lappend rdata REFRESH [ReadLong data $x off] + incr x $off + lappend rdata RETRY [ReadLong data $x off] + incr x $off + lappend rdata EXPIRE [ReadLong data $x off] + incr x $off + lappend rdata MINIMUM [ReadULong data $x off] + incr x $off + } + } + } + + incr index $rdlength + lappend r type $type class $class ttl $ttl rdlength $rdlength rdata $rdata + lappend result $r + } + return $result +} + + +# Read a 32bit integer from a DNS packet. These are compatible with +# the ReadName proc. Additionally - ReadULong takes measures to ensure +# the unsignedness of the value obtained. +# +proc ::dns::ReadLong {datavar index usedvar} { + upvar $datavar data + upvar $usedvar used + set r {} + set used 0 + if {[binary scan $data @${index}I r]} { + set used 4 + } + return $r +} + +proc ::dns::ReadULong {datavar index usedvar} { + upvar $datavar data + upvar $usedvar used + set r {} + set used 0 + if {[binary scan $data @${index}cccc b1 b2 b3 b4]} { + set used 4 + # This gets us an unsigned value. + set r [expr {($b4 & 0xFF) + (($b3 & 0xFF) << 8) + + (($b2 & 0xFF) << 16) + ($b1 << 24)}] + } + return $r +} + +proc ::dns::ReadUShort {datavar index usedvar} { + upvar $datavar data + upvar $usedvar used + set r {} + set used 0 + if {[binary scan [string range $data $index end] cc b1 b2]} { + set used 2 + # This gets us an unsigned value. + set r [expr {(($b2 & 0xff) + (($b1 & 0xff) << 8)) & 0xffff}] + } + return $r +} + +# Read off the NAME or QNAME element. This reads off each label in turn, +# dereferencing pointer labels until we have finished. The length of data +# used is passed back using the usedvar variable. +# +proc ::dns::ReadName {datavar index usedvar} { + upvar $datavar data + upvar $usedvar used + set startindex $index + + set r {} + set len 1 + set max [string length $data] + + while {$len != 0 && $index < $max} { + # Read the label length (and preread the pointer offset) + binary scan [string range $data $index end] cc len lenb + set len [expr {$len & 0xFF}] + incr index + + if {$len != 0} { + if {[expr {$len & 0xc0}]} { + binary scan [binary format cc [expr {$len & 0x3f}] [expr {$lenb & 0xff}]] S offset + incr index + lappend r [ReadName data $offset junk] + set len 0 + } else { + lappend r [string range $data $index [expr {$index + $len - 1}]] + incr index $len + } + } + } + set used [expr {$index - $startindex}] + return [join $r .] +} + +proc ::dns::ReadString {datavar index length} { + upvar $datavar data + set startindex $index + + set r {} + set max [expr {$index + $length}] + + while {$index < $max} { + binary scan [string range $data $index end] c len + set len [expr {$len & 0xFF}] + incr index + + if {$len != 0} { + append r [string range $data $index [expr {$index + $len - 1}]] + incr index $len + } + } + return $r +} + +# ------------------------------------------------------------------------- + +# Support for finding the local nameservers +# +# For unix we can just parse the /etc/resolv.conf if it exists. +# Of course, some unices use /etc/resolver and other things (NIS for instance) +# On Windows, we can examine the Internet Explorer settings from the registry. +# +switch -exact $::tcl_platform(platform) { + windows { + proc ::dns::nameservers {} { + package require registry + set base {HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services} + set param "$base\\Tcpip\\Parameters" + set interfaces "$param\\Interfaces" + set nameservers {} + if {[string equal $::tcl_platform(os) "Windows NT"]} { + AppendRegistryValue $param NameServer nameservers + AppendRegistryValue $param DhcpNameServer nameservers + foreach i [registry keys $interfaces] { + AppendRegistryValue "$interfaces\\$i" NameServer nameservers + AppendRegistryValue "$interfaces\\$i" DhcpNameServer nameservers + } + } else { + set param "$base\\VxD\\MSTCP" + AppendRegistryValue $param NameServer nameservers + } + return $nameservers + } + proc ::dns::AppendRegistryValue {key val listName} { + upvar $listName lst + if {![catch {registry get $key $val} v]} { + foreach ns [split $v ", "] { + if {[lsearch -exact $lst $ns] == -1} { + lappend lst $ns + } + } + } + } + } + unix { + proc ::dns::nameservers {} { + set nameservers {} + if {[file readable /etc/resolv.conf]} { + set f [open /etc/resolv.conf r] + while {![eof $f]} { + gets $f line + if {[regexp {^\s*nameserver\s+(.*)$} $line -> ns]} { + lappend nameservers $ns + } + } + close $f + } + if {[llength $nameservers] < 1} { + lappend nameservers 127.0.0.1 + } + return $nameservers + } + } + default { + proc ::dns::nameservers {} { + return -code error "command not supported for this platform." + } + } +} + +# ------------------------------------------------------------------------- +# Possible support for the DNS URL scheme. +# Ref: http://www.ietf.org/internet-drafts/draft-josefsson-dns-url-04.txt +# eg: dns:target?class=IN;type=A +# dns://nameserver/target?type=A +# +# URI quoting to be accounted for. +# + +catch { + uri::register {dns} { + set escape [set [namespace parent [namespace current]]::basic::escape] + set host [set [namespace parent [namespace current]]::basic::host] + set hostOrPort [set [namespace parent [namespace current]]::basic::hostOrPort] + + set class [string map {* \\\\*} \ + "class=([join [array names ::dns::classes] {|}])"] + set type [string map {* \\\\*} \ + "type=([join [array names ::dns::types] {|}])"] + set classOrType "(?:${class}|${type})" + set classOrTypeSpec "(?:${class}|${type})(?:;(?:${class}|${type}))?" + + set query "${host}(${classOrTypeSpec})?" + variable schemepart "(//${hostOrPort}/)?(${query})" + variable url "dns:$schemepart" + } +} + +namespace eval ::uri {} ;# needed for pkg_mkIndex. + +proc ::uri::SplitDns {uri} { + upvar \#0 [namespace current]::dns::schemepart schemepart + upvar \#0 [namespace current]::dns::class classOrType + upvar \#0 [namespace current]::dns::class classRE + upvar \#0 [namespace current]::dns::type typeRE + upvar \#0 [namespace current]::dns::classOrTypeSpec classOrTypeSpec + + array set parts {nameserver {} query {} class {} type {} port {}} + + # validate the uri + if {[regexp -- $dns::schemepart $uri r] == 1} { + + # deal with the optional class and type specifiers + if {[regexp -indices -- "${classOrTypeSpec}$" $uri range]} { + set spec [string range $uri [lindex $range 0] [lindex $range 1]] + set uri [string range $uri 0 [expr {[lindex $range 0] - 2}]] + + if {[regexp -- "$classRE" $spec -> class]} { + set parts(class) $class + } + if {[regexp -- "$typeRE" $spec -> type]} { + set parts(type) $type + } + } + + # Handle the nameserver specification + if {[string match "//*" $uri]} { + set uri [string range $uri 2 end] + array set tmp [GetHostPort uri] + set parts(nameserver) $tmp(host) + set parts(port) $tmp(port) + } + + # what's left is the query domain name. + set parts(query) [string trimleft $uri /] + } + + return [array get parts] +} + +proc ::uri::JoinDns {args} { + array set parts {nameserver {} port {} query {} class {} type {}} + array set parts $args + set query [::uri::urn::quote $parts(query)] + if {$parts(type) != {}} { + append query "?type=$parts(type)" + } + if {$parts(class) != {}} { + if {$parts(type) == {}} { + append query "?class=$parts(class)" + } else { + append query ";class=$parts(class)" + } + } + if {$parts(nameserver) != {}} { + set ns "$parts(nameserver)" + if {$parts(port) != {}} { + append ns ":$parts(port)" + } + set query "//${ns}/${query}" + } + return "dns:$query" +} + +# ------------------------------------------------------------------------- + +catch {dns::configure -nameserver [lindex [dns::nameservers] 0]} + +package provide dns $dns::version + +# ------------------------------------------------------------------------- +# Local Variables: +# indent-tabs-mode: nil +# End: diff --git a/lib/dns/ip.tcl b/lib/dns/ip.tcl new file mode 100644 index 0000000..1efb4d2 --- /dev/null +++ b/lib/dns/ip.tcl @@ -0,0 +1,369 @@ +# ip.tcl - Copyright (C) 2004 Pat Thoyts +# +# Internet address manipulation. +# +# RFC 3513: IPv6 addressing. +# +# ------------------------------------------------------------------------- +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# ------------------------------------------------------------------------- +# +# $Id: ip.tcl,v 1.12 2007/08/20 20:03:04 andreas_kupries Exp $ + +# @mdgen EXCLUDE: ipMoreC.tcl + +package require Tcl 8.2; # tcl minimum version + +namespace eval ip { + variable version 1.1.2 + variable rcsid {$Id: ip.tcl,v 1.12 2007/08/20 20:03:04 andreas_kupries Exp $} + + namespace export is version normalize equal type contract mask + #catch {namespace ensemble create} + + variable IPv4Ranges + if {![info exists IPv4Ranges]} { + array set IPv4Ranges { + 0/8 private + 10/8 private + 127/8 private + 172.16/12 private + 192.168/16 private + 223/8 reserved + 224/3 reserved + } + } + + variable IPv6Ranges + if {![info exists IPv6Ranges]} { + # RFC 3513: 2.4 + # RFC 3056: 2 + array set IPv6Ranges { + 2002::/16 "6to4 unicast" + fe80::/10 "link local" + fec0::/10 "site local" + ff00::/8 "multicast" + ::/128 "unspecified" + ::1/128 "localhost" + } + } +} + +proc ::ip::is {class ip} { + foreach {ip mask} [split $ip /] break + switch -exact -- $class { + ipv4 - IPv4 - 4 { + return [IPv4? $ip] + } + ipv6 - IPv6 - 6 { + return [IPv6? $ip] + } + default { + return -code error "bad class \"$class\": must be ipv4 or ipv6" + } + } +} + +proc ::ip::version {ip} { + set version -1 + foreach {addr mask} [split $ip /] break + if {[string first $addr :] < 0 && [IPv4? $addr]} { + set version 4 + } elseif {[IPv6? $addr]} { + set version 6 + } + return $version +} + +proc ::ip::equal {lhs rhs} { + foreach {LHS LM} [SplitIp $lhs] break + foreach {RHS RM} [SplitIp $rhs] break + if {[set version [version $LHS]] != [version $RHS]} { + return -code error "type mismatch:\ + cannot compare different address types" + } + if {$version == 4} {set fmt I} else {set fmt I4} + set LHS [Mask$version [Normalize $LHS $version] $LM] + set RHS [Mask$version [Normalize $RHS $version] $RM] + binary scan $LHS $fmt LLL + binary scan $RHS $fmt RRR + foreach L $LLL R $RRR { + if {$L != $R} {return 0} + } + return 1 +} + +proc ::ip::normalize {ip {Ip4inIp6 0}} { + foreach {ip mask} [SplitIp $ip] break + set version [version $ip] + set s [ToString [Normalize $ip $version] $Ip4inIp6] + if {($version == 6 && $mask != 128) || ($version == 4 && $mask != 32)} { + append s /$mask + } + return $s +} + +proc ::ip::contract {ip} { + foreach {ip mask} [SplitIp $ip] break + set version [version $ip] + set s [ToString [Normalize $ip $version]] + if {$version == 6} { + set r "" + foreach o [split $s :] { + append r [format %x: 0x$o] + } + set r [string trimright $r :] + regsub {(?:^|:)0(?::0)+(?::|$)} $r {::} r + } else { + set r [string trimright $s .0] + } + return $r +} + +# Returns an IP address prefix. +# For instance: +# prefix 192.168.1.4/16 => 192.168.0.0 +# prefix fec0::4/16 => fec0:0:0:0:0:0:0:0 +# prefix fec0::4/ffff:: => fec0:0:0:0:0:0:0:0 +# +proc ::ip::prefix {ip} { + foreach {addr mask} [SplitIp $ip] break + set version [version $addr] + set addr [Normalize $addr $version] + return [ToString [Mask$version $addr $mask]] +} + +# Return the address type. For IPv4 this is one of private, reserved +# or normal +# For IPv6 it is one of site local, link local, multicast, unicast, +# unspecified or loopback. +proc ::ip::type {ip} { + set version [version $ip] + upvar [namespace current]::IPv${version}Ranges types + set ip [prefix $ip] + foreach prefix [array names types] { + set mask [mask $prefix] + if {[equal $ip/$mask $prefix]} { + return $types($prefix) + } + } + if {$version == 4} { + return "normal" + } else { + return "unicast" + } +} + +proc ::ip::mask {ip} { + foreach {addr mask} [split $ip /] break + return $mask +} + +# ------------------------------------------------------------------------- + +# Returns true is the argument can be converted into an IPv4 address. +# +proc ::ip::IPv4? {ip} { + if {[catch {Normalize4 $ip}]} { + return 0 + } + return 1 +} + +proc ::ip::IPv6? {ip} { + set octets [split $ip :] + if {[llength $octets] < 3 || [llength $octets] > 8} { + return 0 + } + set ndx 0 + foreach octet $octets { + incr ndx + if {[string length $octet] < 1} continue + if {[regexp {^[a-fA-F\d]{1,4}$} $octet]} continue + if {$ndx >= [llength $octets] && [IPv4? $octet]} continue + if {$ndx == 2 && [lindex $octets 0] == 2002 && [IPv4? $octet]} continue + #"Invalid IPv6 address \"$ip\"" + return 0 + } + if {[regexp {^:[^:]} $ip]} { + #"Invalid ipv6 address \"$ip\" (starts with :)" + return 0 + } + if {[regexp {[^:]:$} $ip]} { + # "Invalid IPv6 address \"$ip\" (ends with :)" + return 0 + } + if {[regsub -all :: $ip "|" junk] > 1} { + # "Invalid IPv6 address \"$ip\" (more than one :: pattern)" + return 0 + } + return 1 +} + +proc ::ip::Mask4 {ip {bits {}}} { + if {[string length $bits] < 1} { set bits 32 } + binary scan $ip I ipx + if {[string is integer $bits]} { + set mask [expr {(0xFFFFFFFF << (32 - $bits)) & 0xFFFFFFFF}] + } else { + binary scan [Normalize4 $bits] I mask + } + return [binary format I [expr {$ipx & $mask}]] +} + +proc ::ip::Mask6 {ip {bits {}}} { + if {[string length $bits] < 1} { set bits 128 } + if {[string is integer $bits]} { + set mask [binary format B128 [string repeat 1 $bits]] + } else { + binary scan [Normalize6 $bits] I4 mask + } + binary scan $ip I4 Addr + binary scan $mask I4 Mask + foreach A $Addr M $Mask { + lappend r [expr {$A & $M}] + } + return [binary format I4 $r] +} + + + +# A network address specification is an IPv4 address with an optional bitmask +# Split an address specification into a IPv4 address and a network bitmask. +# This doesn't validate the address portion. +# If a spec with no mask is provided then the mask will be 32 +# (all bits significant). +# Masks may be either integer number of significant bits or dotted-quad +# notation. +# +proc ::ip::SplitIp {spec} { + set slash [string last / $spec] + if {$slash != -1} { + incr slash -1 + set ip [string range $spec 0 $slash] + incr slash 2 + set bits [string range $spec $slash end] + } else { + set ip $spec + if {[string length $ip] > 0 && [version $ip] == 6} { + set bits 128 + } else { + set bits 32 + } + } + return [list $ip $bits] +} + +# Given an IP string from the user, convert to a normalized internal rep. +# For IPv4 this is currently a hex string (0xHHHHHHHH). +# For IPv6 this is a binary string or 16 chars. +proc ::ip::Normalize {ip {version 0}} { + if {$version < 0} { + set version [version $ip] + if {$version < 0} { + return -code error "invalid address \"$ip\":\ + value must be a valid IPv4 or IPv6 address" + } + } + return [Normalize$version $ip] +} + +proc ::ip::Normalize4 {ip} { + set octets [split $ip .] + if {[llength $octets] > 4} { + return -code error "invalid ip address \"$ip\"" + } elseif {[llength $octets] < 4} { + set octets [lrange [concat $octets 0 0 0] 0 3] + } + foreach oct $octets { + if {$oct < 0 || $oct > 255} { + return -code error "invalid ip address" + } + } + return [binary format c4 $octets] +} + +proc ::ip::Normalize6 {ip} { + set octets [split $ip :] + set ip4embed [string first . $ip] + set len [llength $octets] + if {$len < 0 || $len > 8} { + return -code error "invalid address: this is not an IPv6 address" + } + set result "" + for {set n 0} {$n < $len} {incr n} { + set octet [lindex $octets $n] + if {$octet == {}} { + if {$n == 0 || $n == ($len - 1)} { + set octet \0\0 + } else { + set missing [expr {9 - $len}] + if {$ip4embed != -1} {incr missing -1} + set octet [string repeat \0\0 $missing] + } + } elseif {[string first . $octet] != -1} { + set octet [Normalize4 $octet] + } else { + set m [expr {4 - [string length $octet]}] + if {$m != 0} { + set octet [string repeat 0 $m]$octet + } + set octet [binary format H4 $octet] + } + append result $octet + } + if {[string length $result] != 16} { + return -code error "invalid address: \"$ip\" is not an IPv6 address" + } + return $result +} + + +# This will convert a full ipv4/ipv6 in binary format into a normal +# expanded string rep. +proc ::ip::ToString {bin {Ip4inIp6 0}} { + set len [string length $bin] + set r "" + if {$len == 4} { + binary scan $bin c4 octets + foreach octet $octets { + lappend r [expr {$octet & 0xff}] + } + return [join $r .] + } elseif {$len == 16} { + if {$Ip4inIp6 == 0} { + binary scan $bin H32 hex + for {set n 0} {$n < 32} {incr n} { + append r [string range $hex $n [incr n 3]]: + } + return [string trimright $r :] + } else { + binary scan $bin H24c4 hex octets + for {set n 0} {$n < 24} {incr n} { + append r [string range $hex $n [incr n 3]]: + } + foreach octet $octets { + append r [expr {$octet & 0xff}]. + } + return [string trimright $r .] + } + } else { + return -code error "invalid binary address:\ + argument is neither an IPv4 nor an IPv6 address" + } +} + +# ------------------------------------------------------------------------- +# Load extended command set. + +source [file join [file dirname [info script]] ipMore.tcl] + +# ------------------------------------------------------------------------- + +package provide ip $::ip::version + +# ------------------------------------------------------------------------- +# Local Variables: +# indent-tabs-mode: nil +# End: diff --git a/lib/dns/ipMore.tcl b/lib/dns/ipMore.tcl new file mode 100644 index 0000000..2532656 --- /dev/null +++ b/lib/dns/ipMore.tcl @@ -0,0 +1,1217 @@ +#temporary home until this gets cleaned up for export to tcllib ip module +# $Id: ipMore.tcl,v 1.4 2006/01/22 00:27:22 andreas_kupries Exp $ + + +##Library Header +# +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ipMore +# +# Purpose: +# Additional commands for the tcllib ip package. +# +# Author: +# Aamer Akhter / aakhter@cisco.com +# +# Support Alias: +# aakhter@cisco.com +# +# Usage: +# package require ip +# (The command are loaded from the regular package). +# +# Description: +# A detailed description of the functionality provided by the library. +# +# Requirements: +# +# Variables: +# namespace ::ip +# +# Notes: +# 1. +# +# Keywords: +# +# +# Category: +# +# +# End of Header + +package require msgcat + +# Try to load various C based accelerato packages for two of the +# commands. + +if {[catch {package require ipMorec}]} { + catch {package require tcllibc} +} + +if {[llength [info commands ::ip::prefixToNativec]]} { + # An accelerator is present, providing the C variants + interp alias {} ::ip::prefixToNative {} ::ip::prefixToNativec + interp alias {} ::ip::isOverlapNative {} ::ip::isOverlapNativec +} else { + # Link API to the Tcl variants, no accelerators are available. + interp alias {} ::ip::prefixToNative {} ::ip::prefixToNativeTcl + interp alias {} ::ip::isOverlapNative {} ::ip::isOverlapNativeTcl +} + +namespace eval ::ip { + ::msgcat::mcload [file join [file dirname [info script]] msgs] +} + +if {![llength [info commands lassign]]} { + # Either an older tcl version, or tclx not loaded; have to use our + # internal lassign from http://wiki.tcl.tk/1530 by Schelte Bron + + proc ::ip::lassign {values args} { + uplevel 1 [list foreach $args $values break] + lrange $values [llength $args] end + } +} +if {![llength [info commands lvarpop]]} { + # Define an emulation of Tclx's lvarpop if the command + # is not present already. + + proc ::ip::lvarpop {upVar {index 0}} { + upvar $upVar list; + set top [lindex $list $index]; + set list [concat [lrange $list 0 [expr $index - 1]] \ + [lrange $list [expr $index +1] end]]; + return $top; + } +} + +# Some additional aliases for backward compatability. Not +# documented. The old names ar from previous versions while at Cisco. +# +# Old command name --> Documented command name +interp alias {} ::ip::ToInteger {} ::ip::toInteger +interp alias {} ::ip::ToHex {} ::ip::toHex +interp alias {} ::ip::MaskToInt {} ::ip::maskToInt +interp alias {} ::ip::MaskToLength {} ::ip::maskToLength +interp alias {} ::ip::LengthToMask {} ::ip::lengthToMask +interp alias {} ::ip::IpToLayer2Multicast {} ::ip::ipToLayer2Multicast +interp alias {} ::ip::IpHostFromPrefix {} ::ip::ipHostFromPrefix + + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::prefixToNative +# +# Purpose: +# convert from dotted from to native (hex) form +# +# Synopsis: +# prefixToNative +# +# Arguments: +# +# string in the / format +# +# Return Values: +# in native format { } +# +# Description: +# +# Examples: +# % ip::prefixToNative 1.1.1.0/24 +# 0x01010100 0xffffff00 +# +# Sample Input: +# +# Sample Output: +# Notes: +# fixed bug in C extension that modified +# calling context variable +# See Also: +# +# End of Header + +proc ip::prefixToNativeTcl {prefix} { + set plist {} + foreach p $prefix { + set newPrefix [ip::toHex [ip::prefix $p]] + if {[string equal [set mask [ip::mask $p]] ""]} { + set newMask 0xffffffff + } else { + set newMask [format "0x%08x" [ip::maskToInt $mask]] + } + lappend plist [list $newPrefix $newMask] + } + if {[llength $plist]==1} {return [lindex $plist 0]} + return $plist +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::nativeToPrefix +# +# Purpose: +# convert from native (hex) form to dotted form +# +# Synopsis: +# nativeToPrefix | [-ipv4] +# +# Arguments: +# +# list of native form ip addresses native form is: +# +# tcllist in format { } +# -ipv4 +# the provided native format addresses are in ipv4 format (default) +# +# Return Values: +# if nativeToPrefix is called with a single (non-listified) address +# is returned +# if nativeToPrefix is called with a address list, then +# a list of addresses is returned +# +# return form is: / +# +# Description: +# +# Examples: +# % ip::nativeToPrefix {0x01010100 0xffffff00} -ipv4 +# 1.1.1.0/24 +# +# Sample Input: +# +# Sample Output: + +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::nativeToPrefix {nativeList args} { + set pList 1 + set ipv4 1 + while {[llength $args]} { + switch -- [lindex $args 0] { + -ipv4 {set args [lrange $args 1 end]} + default { + return -code error [msgcat::mc "option %s not supported" [lindex $args 0]] + } + } + } + + # if a single native element is passed eg {0x01010100 0xffffff00} + # instead of {{0x01010100 0xffffff00} {0x01010100 0xffffff00}...} + # then return a (non-list) single entry + if {[llength [lindex $nativeList 0]]==1} {set pList 0; set nativeList [list $nativeList]} + foreach native $nativeList { + lassign $native ip mask + if {[string equal $mask ""]} {set mask 32} + set pString "" + append pString [ip::ToString [binary format I [expr {$ip}]]] + append pString "/" + append pString [ip::maskToLength $mask] + lappend rList $pString + } + # a multi (listified) entry was given + # return the listified entry + if {$pList} { return $rList } + return $pString +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::intToString +# +# Purpose: +# convert from an integer/hex to dotted form +# +# Synopsis: +# intToString [-ipv4] +# +# Arguments: +# +# ip address in integer form +# -ipv4 +# the provided integer addresses is ipv4 (default) +# +# Return Values: +# ip address in dotted form +# +# Description: +# +# Examples: +# ip::intToString 4294967295 +# 255.255.255.255 +# +# Sample Input: +# +# Sample Output: + +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::intToString {int args} { + set ipv4 1 + while {[llength $args]} { + switch -- [lindex $args 0] { + -ipv4 {set args [lrange $args 1 end]} + default { + return -code error [msgcat::mc "option %s not supported" [lindex $args 0]] + } + } + } + return [ip::ToString [binary format I [expr {$int}]]] +} + + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::toInteger +# +# Purpose: +# convert dotted form ip to integer +# +# Synopsis: +# toInteger +# +# Arguments: +# +# decimal dotted from ip address +# +# Return Values: +# integer form of +# +# Description: +# +# Examples: +# % ::ip::toInteger 1.1.1.0 +# 16843008 +# +# Sample Input: +# +# Sample Output: + +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::toInteger {ip} { + binary scan [ip::Normalize4 $ip] I out + return $out +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::toHex +# +# Purpose: +# convert dotted form ip to hex +# +# Synopsis: +# toHex +# +# Arguments: +# +# decimal dotted from ip address +# +# Return Values: +# hex form of +# +# Description: +# +# Examples: +# % ::ip::toHex 1.1.1.0 +# 0x01010100 +# +# Sample Input: +# +# Sample Output: + +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::toHex {ip} { + binary scan [ip::Normalize4 $ip] H8 out + return "0x$out" +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::maskToInt +# +# Purpose: +# convert mask to integer +# +# Synopsis: +# maskToInt +# +# Arguments: +# +# mask in either dotted form or mask length form (255.255.255.0 or 24) +# +# Return Values: +# integer form of mask +# +# Description: +# +# Examples: +# ::ip::maskToInt 24 +# 4294967040 +# +# +# Sample Input: +# +# Sample Output: + +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::maskToInt {mask} { + if {[string is integer -strict $mask]} { + set maskInt [expr {(0xFFFFFFFF << (32 - $mask))}] + } else { + binary scan [Normalize4 $mask] I maskInt + } + set maskInt [expr {$maskInt & 0xFFFFFFFF}] + return [format %u $maskInt] +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::broadcastAddress +# +# Purpose: +# return broadcast address given prefix +# +# Synopsis: +# broadcastAddress [-ipv4] +# +# Arguments: +# +# route in the form of / or native form { } +# -ipv4 +# the provided native format addresses are in ipv4 format (default) +# note: broadcast addresses are not valid in ipv6 +# +# +# Return Values: +# ipaddress of broadcast +# +# Description: +# +# Examples: +# ::ip::broadcastAddress 1.1.1.0/24 +# 1.1.1.255 +# +# ::ip::broadcastAddress {0x01010100 0xffffff00} +# 0x010101ff +# +# Sample Input: +# +# Sample Output: + +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::broadcastAddress {prefix args} { + set ipv4 1 + while {[llength $args]} { + switch -- [lindex $args 0] { + -ipv4 {set args [lrange $args 1 end]} + default { + return -code error [msgcat::mc "option %s not supported" [lindex $args 0]] + } + } + } + if {[llength $prefix] == 2} { + lassign $prefix net mask + } else { + set net [maskToInt [ip::prefix $prefix]] + set mask [maskToInt [ip::mask $prefix]] + } + set ba [expr {$net | ((~$mask)&0xffffffff)}] + + if {[llength $prefix]==2} { + return [format "0x%08x" $ba] + } + return [ToString [binary format I $ba]] +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::maskToLength +# +# Purpose: +# converts dotted or integer form of mask to length +# +# Synopsis: +# maskToLength || [-ipv4] +# +# Arguments: +# +# +# +# mask to convert to prefix length format (eg /24) +# -ipv4 +# the provided integer/hex format masks are ipv4 (default) +# +# Return Values: +# prefix length +# +# Description: +# +# Examples: +# ::ip::maskToLength 0xffffff00 -ipv4 +# 24 +# +# % ::ip::maskToLength 255.255.255.0 +# 24 +# +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::maskToLength {mask args} { + set ipv4 1 + while {[llength $args]} { + switch -- [lindex $args 0] { + -ipv4 {set args [lrange $args 1 end]} + default { + return -code error [msgcat::mc "option %s not supported" [lindex $args 0]] + } + } + } + #pick the fastest method for either format + if {[string is integer -strict $mask]} { + binary scan [binary format I [expr {$mask}]] B32 maskB + if {[regexp -all {^1+} $maskB ones]} { + return [string length $ones] + } else { + return 0 + } + } else { + regexp {\/(.+)} $mask dumb mask + set prefix 0 + foreach ipByte [split $mask {.}] { + switch $ipByte { + 255 {incr prefix 8; continue} + 254 {incr prefix 7} + 252 {incr prefix 6} + 248 {incr prefix 5} + 240 {incr prefix 4} + 224 {incr prefix 3} + 192 {incr prefix 2} + 128 {incr prefix 1} + 0 {} + default { + return -code error [msgcat::mc "not an ip mask: %s" $mask] + } + } + break + } + return $prefix + } +} + + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::lengthToMask +# +# Purpose: +# converts mask length to dotted mask form +# +# Synopsis: +# lengthToMask [-ipv4] +# +# Arguments: +# +# mask length +# -ipv4 +# the provided mask length is ipv4 (default) +# +# Return Values: +# mask in dotted form +# +# Description: +# +# Examples: +# ::ip::lengthToMask 24 +# 255.255.255.0 +# +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::lengthToMask {masklen args} { + while {[llength $args]} { + switch -- [lindex $args 0] { + -ipv4 {set args [lrange $args 1 end]} + default { + return -code error [msgcat::mc "option %s not supported" [lindex $args 0]] + } + } + } + # the fastest method is just to look + # thru an array + return $::ip::maskLenToDotted($masklen) +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::nextNet +# +# Purpose: +# returns next an ipaddress in same position in next network +# +# Synopsis: +# nextNet [] [-ipv4] +# +# Arguments: +# +# in hex/integer/dotted format +# +# mask in hex/integer/dotted/maskLen format +# +# number of nets to skip over (default is 1) +# -ipv4 +# the provided hex/integer addresses are in ipv4 format (default) +# +# Return Values: +# ipaddress in same position in next network in hex +# +# Description: +# +# Examples: +# +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::nextNet {prefix mask args} { + set count 1 + while {[llength $args]} { + switch -- [lindex $args 0] { + -ipv4 {set args [lrange $args 1 end]} + default { + set count [lindex $args 0] + set args [lrange $args 1 end] + } + } + } + if {![string is integer -strict $prefix]} { + set prefix [toInteger $prefix] + } + if {![string is integer -strict $mask] || ($mask < 33 && $mask > 0)} { + set mask [maskToInt $mask] + } + + set prefix [expr $prefix + ($mask ^ 0xFFffFFff) + $count ] + return [format "0x%08x" $prefix] +} + + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::isOverlap +# +# Purpose: +# checks to see if prefixes overlap +# +# Synopsis: +# isOverlap ... +# +# Arguments: +# +# in form / prefix to compare against +# +# in form / prefixes to compare against +# +# Return Values: +# 1 if there is an overlap +# +# Description: +# +# Examples: +# % ::ip::isOverlap 1.1.1.0/24 2.1.0.1/32 +# 0 +# +# ::ip::isOverlap 1.1.1.0/24 2.1.0.1/32 1.1.1.1/32 +# 1 +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::isOverlap {ip args} { + lassign [SplitIp $ip] ip1 mask1 + set ip1int [toInteger $ip1] + set mask1int [maskToInt $mask1] + + set overLap 0 + foreach prefix $args { + lassign [SplitIp $prefix] ip2 mask2 + set ip2int [toInteger $ip2] + set mask2int [maskToInt $mask2] + set mask1mask2 [expr {$mask1int & $mask2int}] + if {[expr {$ip1int & $mask1mask2}] == [expr {$ip2int & $mask1mask2}]} { + set overLap 1 + break + } + } + return $overLap +} + + +#optimized overlap, that accepts native format + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::isOverlapNative +# +# Purpose: +# checks to see if prefixes overlap (optimized native form) +# +# Synopsis: +# isOverlap {{ } { ...} +# +# Arguments: +# -all +# return all overlaps rather than the first one +# -inline +# rather than returning index values, return the actual overlap prefixes +# +# ipaddress in hex/integer form +# +# mask in hex/integer form +# -ipv4 +# the provided native format addresses are in ipv4 format (default) +# +# Return Values: +# non-zero if there is an overlap, value is element # in list with overlap +# +# Description: +# isOverlapNative is avaliabel both as a C extension and in a native tcl form +# if the extension is loaded (tried automatically), isOverlapNative will be +# linked to isOverlapNativeC. If an extension is not loaded, then isOverlapNative +# will be linked to the native tcl proc: ipOverlapNativeTcl. +# +# Examples: +# % ::ip::isOverlapNative 0x01010100 0xffffff00 {{0x02010001 0xffffffff}} +# 0 +# +# %::ip::isOverlapNative 0x01010100 0xffffff00 {{0x02010001 0xffffffff} {0x01010101 0xffffffff}} +# 2 +# +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::isOverlapNativeTcl {args} { + set all 0 + set inline 0 + set notOverlap 0 + set ipv4 1 + foreach sw [lrange $args 0 end-3] { + switch -exact -- $sw { + -all { + set all 1 + set allList [list] + } + -inline {set inline 1} + -ipv4 {} + } + } + set args [lassign [lrange $args end-2 end] ip1int mask1int prefixList] + if {$inline} { + set overLap [list] + } else { + set overLap 0 + } + set count 0 + foreach prefix $prefixList { + incr count + lassign $prefix ip2int mask2int + set mask1mask2 [expr {$mask1int & $mask2int}] + if {[expr {$ip1int & $mask1mask2}] == [expr {$ip2int & $mask1mask2}]} { + if {$inline} { + set overLap [list $prefix] + } else { + set overLap $count + } + if {$all} { + if {$inline} { + lappend allList $prefix + } else { + lappend allList $count + } + } else { + break + } + } + } + if {$all} {return $allList} + return $overLap +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::ipToLayer2Multicast +# +# Purpose: +# converts ipv4 address to a layer 2 multicast address +# +# Synopsis: +# ipToLayer2Multicast +# +# Arguments: +# +# ipaddress in dotted form +# +# Return Values: +# mac address in xx.xx.xx.xx.xx.xx form +# +# Description: +# +# Examples: +# % ::ip::ipToLayer2Multicast 224.0.0.2 +# 01.00.5e.00.00.02 +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::ipToLayer2Multicast { ipaddr } { + regexp "\[0-9\]+\.(\[0-9\]+)\.(\[0-9\]+)\.(\[0-9\]+)" $ipaddr junk ip2 ip3 ip4 + #remove MSB of 2nd octet of IP address for mcast L2 addr + set mac2 [expr {$ip2 & 127}] + return [format "01.00.5e.%02x.%02x.%02x" $mac2 $ip3 $ip4] +} + + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::ipHostFromPrefix +# +# Purpose: +# gives back a host address from a prefix +# +# Synopsis: +# ::ip::ipHostFromPrefix [-exclude ] +# +# Arguments: +# +# prefix is / +# -exclude +# list if ipprefixes that host should not be in +# Return Values: +# ip address +# +# Description: +# +# Examples: +# %::ip::ipHostFromPrefix 1.1.1.5/24 +# 1.1.1.1 +# +# %::ip::ipHostFromPrefix 1.1.1.1/32 +# 1.1.1.1 +# +# +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::ipHostFromPrefix { prefix args } { + set mask [mask $prefix] + set ipaddr [prefix $prefix] + if {[llength $args]} { + array set opts $args + } else { + if {$mask==32} { + return $ipaddr + } else { + return [intToString [expr {[toHex $ipaddr] + 1} ]] + } + } + set format {-ipv4} + # if we got here, then options were set + if {[info exists opts(-exclude)]} { + #basic algo is: + # 1. throw away prefixes that are less specific that $prefix + # 2. of remaining pfx, throw away prefixes that do not overlap + # 3. run reducetoAggregates on specific nets + # 4. + + # 1. convert to hex format + set currHex [prefixToNative $prefix ] + set exclHex [prefixToNative $opts(-exclude) ] + # sort the prefixes by their mask, include the $prefix as a marker + # so we know from where to throw away prefixes + set sortedPfx [lsort -integer -index 1 [concat [list $currHex] $exclHex]] + # throw away prefixes that are less specific than $prefix + set specPfx [lrange $sortedPfx [expr {[lsearch -exact $sortedPfx $currHex] +1} ] end] + + #2. throw away non-overlapping prefixes + set specPfx [isOverlapNative -all -inline \ + [lindex $currHex 0 ] \ + [lindex $currHex 1 ] \ + $specPfx ] + #3. run reduce aggregates + set specPfx [reduceToAggregates $specPfx] + + #4 now have to pick an address that overlaps with $currHex but not with + # $specPfx + # 4.1 find the largest prefix w/ most specific mask and go to the next net + + + # current ats tcl does not allow this in one command, so + # for now just going to grab the last prefix (list is already sorted) + set sPfx [lindex $specPfx end] + set startPfx $sPfx + # add currHex to specPfx + set oChkPfx [concat $specPfx [list $currHex]] + + + set notcomplete 1 + set overflow 0 + while {$notcomplete} { + #::ipMore::log::debug "doing nextnet on $sPfx" + set nextNet [nextNet [lindex $sPfx 0] [lindex $sPfx 1]] + #::ipMore::log::debug "trying $nextNet" + if {$overflow && ($nextNet > $startPfx)} { + #we've gone thru the entire net and didn't find anything. + return -code error [msgcat::mc "ip host could not be found in %s" $prefix] + break + } + set oPfx [isOverlapNative -all -inline \ + $nextNet -1 \ + $oChkPfx + ] + switch -exact [llength $oPfx] { + 0 { + # no overlap at all. meaning we have gone beyond the bounds of + # $currHex. need to overlap and try again + #::ipMore::log::debug {ipHostFromPrefix: overlap done} + set overflow 1 + } + 1 { + #we've found what we're looking for. pick this address and exit + return [intToString $nextNet] + } + default { + # 2 or more overlaps, need to increment again + set sPfx [lindex $oPfx 0] + } + } + } + } +} + + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::reduceToAggregates +# +# Purpose: +# finds nets that overlap and filters out the more specifc nets +# +# Synopsis: +# ::ip::reduceToAggregates +# +# Arguments: +# +# prefixList a list in the from of +# is / or native format +# +# Return Values: +# non-overlapping ip prefixes +# +# Description: +# +# Examples: +# +# % ::ip::reduceToAggregates {1.1.1.0/24 1.1.0.0/8 2.1.1.0/24 1.1.1.1/32 } +# 1.0.0.0/8 2.1.1.0/24 +# +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::reduceToAggregates { prefixList } { + #find out format of $prefixeList + set dotConv 0 + if {[llength [lindex $prefixList 0]]==1} { + #format is dotted form convert all prefixes to native form + set prefixList [ip::prefixToNative $prefixList] + set dotConv 1 + } + + set nonOverLapping $prefixList + while {1==1} { + set overlapFound 0 + set remaining $nonOverLapping + set nonOverLapping {} + while {[llength $remaining]} { + set current [lvarpop remaining] + set overLap [ip::isOverlapNative [lindex $current 0] [lindex $current 1] $remaining] + if {$overLap} { + #there was a overlap find out which prefix has a the smaller mask, and keep that one + if {[lindex $current 1] > [lindex [lindex $remaining [expr {$overLap -1}]] 1]} { + #current has more restrictive mask, throw that prefix away + # keep other prefix + lappend nonOverLapping [lindex $remaining [expr {$overLap -1}]] + } else { + lappend nonOverLapping $current + } + lvarpop remaining [expr {$overLap -1}] + set overlapFound 1 + } else { + #no overlap, keep all prefixes, don't touch the stuff in + # remaining, it is needed for other overlap checking + lappend nonOverLapping $current + } + } + if {$overlapFound==0} {break} + } + if {$dotConv} {return [nativeToPrefix $nonOverLapping]} + return $nonOverLapping +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::longestPrefixMatch +# +# Purpose: +# given host IP finds longest prefix match from set of prefixes +# +# Synopsis: +# ::ip::longestPrefixMatch [-ipv4] +# +# Arguments: +# +# is list of in native or dotted form +# +# ip address in format, dotted form, or integer form +# -ipv4 +# the provided integer format addresses are in ipv4 format (default) +# +# Return Values: +# that is the most specific match to +# +# Description: +# +# Examples: +# % ::ip::longestPrefixMatch 1.1.1.1 {1.1.1.0/24 1.0.0.0/8 2.1.1.0/24 1.1.1.0/28 } +# 1.1.1.0/28 +# +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header + +proc ::ip::longestPrefixMatch { ipaddr prefixList args} { + set ipv4 1 + while {[llength $args]} { + switch -- [lindex $args 0] { + -ipv4 {set args [lrange $args 1 end]} + default { + return -code error [msgcat::mc "option %s not supported" [lindex $args 0]] + } + } + } + #find out format of prefixes + set dotConv 0 + if {[llength [lindex $prefixList 0]]==1} { + #format is dotted form convert all prefixes to native form + set prefixList [ip::prefixToNative $prefixList] + set dotConv 1 + } + #sort so that most specific prefix is in the front + if {[llength [lindex [lindex $prefixList 0] 1]]} { + set prefixList [lsort -decreasing -integer -index 1 $prefixList] + } else { + set prefixList [list $prefixList] + } + if {![string is integer -strict $ipaddr]} { + set ipaddr [prefixToNative $ipaddr] + } + set best [ip::isOverlapNative -inline \ + [lindex $ipaddr 0] [lindex $ipaddr 1] $prefixList] + if {$dotConv && [llength $best]} { + return [nativeToPrefix $best] + } + return $best +} + +##Procedure Header +# Copyright (c) 2004 Cisco Systems, Inc. +# +# Name: +# ::ip::cmpDotIP +# +# Purpose: +# helper function for dotted ip address for use in lsort +# +# Synopsis: +# ::ip::cmpDotIP +# +# Arguments: +# +# prefix is in dotted ip address format +# +# Return Values: +# -1 if ipaddr1 is less that ipaddr2 +# 1 if ipaddr1 is more that ipaddr2 +# 0 if ipaddr1 and ipaddr2 are equal +# +# Description: +# +# Examples: +# % lsort -command ip::cmpDotIP {1.0.0.0 2.2.0.0 128.0.0.0 3.3.3.3} +# 1.0.0.0 2.2.0.0 3.3.3.3 128.0.0.0 +# +# Sample Input: +# +# Sample Output: +# Notes: +# +# See Also: +# +# End of Header +# ip address in format, dotted form, or integer form + +if {![package vsatisfies [package provide Tcl] 8.4]} { + # 8.3+ + proc ip::cmpDotIP {ipaddr1 ipaddr2} { + # convert dotted to list of integers + set ipaddr1 [split $ipaddr1 .] + set ipaddr2 [split $ipaddr2 .] + foreach a $ipaddr1 b $ipaddr2 { + #ipMore::log::debug "$ipInt1 $ipInt2" + if { $a < $b} { + return -1 + } elseif {$a >$b} { + return 1 + } + } + return 0 + } +} else { + # 8.4+ + proc ip::cmpDotIP {ipaddr1 ipaddr2} { + # convert dotted to decimal + set ipInt1 [::ip::toHex $ipaddr1] + set ipInt2 [::ip::toHex $ipaddr2] + #ipMore::log::debug "$ipInt1 $ipInt2" + if { $ipInt1 < $ipInt2} { + return -1 + } elseif {$ipInt1 >$ipInt2 } { + return 1 + } else { + return 0 + } + } +} + +# Populate the array "maskLenToDotted" for fast lookups of mask to +# dotted form. + +namespace eval ::ip { + variable maskLenToDotted + variable x + + for {set x 0} {$x <33} {incr x} { + set maskLenToDotted($x) [intToString [maskToInt $x]] + } + unset x +} diff --git a/lib/dns/ipMoreC.tcl b/lib/dns/ipMoreC.tcl new file mode 100644 index 0000000..b28903e --- /dev/null +++ b/lib/dns/ipMoreC.tcl @@ -0,0 +1,242 @@ +# Skip this for window and a specific version of Solaris +# +# This could do with an explanation -- why are we avoiding these platforms +# and perhaps using critcl's platform::platform command might be better? +# +if {[string equal $::tcl_platform(platform) windows] || + ([string equal $::tcl_platform(os) SunOS] && + [string equal $::tcl_platform(osVersion) 5.6]) +} { + # avoid warnings about nothing to compile + critcl::ccode { + /* nothing to do */ + } + return +} + +package require critcl; + +namespace eval ::ip { + +critcl::ccode { +#include +#include +#include +#include +#include +#include +#include +} + +critcl::ccommand prefixToNativec {clientData interp objc objv} { + int elemLen, maskLen, ipLen, mask; + int rval,convertListc,i; + Tcl_Obj **convertListv; + Tcl_Obj *listPtr,*returnPtr, *addrList; + char *stringIP, *slashPos, *stringMask; + char v4HEX[11]; + + uint32_t inaddr; + listPtr = NULL; + + /* printf ("\n in prefixToNativeC"); */ + /* printf ("\n objc = %d",objc); */ + + if (objc != 2) { + Tcl_WrongNumArgs(interp, 1, objv, "/"); + return TCL_ERROR; + } + + + if (Tcl_ListObjGetElements (interp, objv[1], + &convertListc, &convertListv) != TCL_OK) { + return TCL_ERROR; + } + returnPtr = Tcl_NewListObj(0, (Tcl_Obj **) NULL); + for (i = 0; i < convertListc; i++) { + /* need to create a duplicate here because when we modify */ + /* the stringIP it'll mess up the original in the calling */ + /* context */ + addrList = Tcl_DuplicateObj(convertListv[i]); + stringIP = Tcl_GetStringFromObj(addrList, &elemLen); + listPtr = Tcl_NewListObj(0, (Tcl_Obj **) NULL); + /* printf ("\n ### %s ### string \n", stringIP); */ + /* split the ip address and mask */ + slashPos = strchr(stringIP, (int) '/'); + if (slashPos == NULL) { + /* straight ip address without mask */ + mask = 0xffffffff; + ipLen = strlen(stringIP); + } else { + /* ipaddress has the mask, handle the mask and seperate out the */ + /* ip address */ + /* printf ("\n ** %d ",(uintptr_t)slashPos); */ + stringMask = slashPos +1; + maskLen =strlen(stringMask); + /* put mask in hex form */ + if (maskLen < 3) { + mask = atoi(stringMask); + mask = (0xFFFFFFFF << (32 - mask)) & 0xFFFFFFFF; + } else { + /* mask is in dotted form */ + if ((rval = inet_pton(AF_INET,stringMask,&mask)) < 1 ) { + Tcl_AddErrorInfo(interp, "\n bad format encountered in mask conversion"); + return TCL_ERROR; + } + mask = htonl(mask); + } + ipLen = (uintptr_t)slashPos - (uintptr_t)stringIP; + /* divide the string into ip and mask portion */ + *slashPos = '\0'; + /* printf("\n %d %d %d %d", (uintptr_t)stringMask, maskLen, (uintptr_t)stringIP, ipLen); */ + } + if ( (rval = inet_pton(AF_INET,stringIP,&inaddr)) < 1) { + Tcl_AddErrorInfo(interp, + "\n bad format encountered in ip conversion"); + return TCL_ERROR; + }; + inaddr = htonl(inaddr); + /* apply the mask the to the ip portion, just to make sure */ + /* what we return is cleaned up */ + inaddr = inaddr & mask; + sprintf(v4HEX,"0x%08X",inaddr); + /* printf ("\n\n ### %s",v4HEX); */ + Tcl_ListObjAppendElement(interp, listPtr, + Tcl_NewStringObj(v4HEX,-1)); + sprintf(v4HEX,"0x%08X",mask); + Tcl_ListObjAppendElement(interp, listPtr, + Tcl_NewStringObj(v4HEX,-1)); + Tcl_ListObjAppendElement(interp, returnPtr, listPtr); + Tcl_DecrRefCount(addrList); + } + + if (convertListc==1) { + Tcl_SetObjResult(interp,listPtr); + } else { + Tcl_SetObjResult(interp,returnPtr); + } + + return TCL_OK; +} + +critcl::ccommand isOverlapNativec {clientData interp objc objv} { + int i; + unsigned int ipaddr,ipMask, mask1mask2; + unsigned int ipaddr2,ipMask2; + int compareListc,comparePrefixMaskc; + int allSet,inlineSet,index; + Tcl_Obj **compareListv,**comparePrefixMaskv, *listPtr; + Tcl_Obj *result; + static CONST char *options[] = { + "-all", "-inline", "-ipv4", NULL + }; + enum options { + OVERLAP_ALL, OVERLAP_INLINE, OVERLAP_IPV4 + }; + + allSet = 0; + inlineSet = 0; + listPtr = NULL; + + /* printf ("\n objc = %d",objc); */ + if (objc < 3) { + Tcl_WrongNumArgs(interp, 1, objv, "?options? "); + return TCL_ERROR; + } + for (i = 1; i < objc-3; i++) { + if (Tcl_GetIndexFromObj(interp, objv[i], options, "option", 0, &index) + != TCL_OK) { + return TCL_ERROR; + } + switch (index) { + case OVERLAP_ALL: + allSet = 1; + /* printf ("\n all selected"); */ + break; + case OVERLAP_INLINE: + inlineSet = 1; + /* printf ("\n inline selected"); */ + break; + case OVERLAP_IPV4: + break; + } + } + /* options are parsed */ + + /* create return obj */ + result = Tcl_GetObjResult (interp); + + /* set ipaddr and ipmask */ + Tcl_GetIntFromObj(interp,objv[objc-3],&ipaddr); + Tcl_GetIntFromObj(interp,objv[objc-2],&ipMask); + + /* split the 3rd argument into pairs */ + if (Tcl_ListObjGetElements (interp, objv[objc-1], &compareListc, &compareListv) != TCL_OK) { + return TCL_ERROR; + } +/* printf("comparing %x/%x \n",ipaddr,ipMask); */ + + if (allSet || inlineSet) { + listPtr = Tcl_NewListObj(0, (Tcl_Obj **) NULL); + } + + for (i = 0; i < compareListc; i++) { + /* split the ipaddr2 and ipmask2 */ + if (Tcl_ListObjGetElements (interp, + compareListv[i], + &comparePrefixMaskc, + &comparePrefixMaskv) != TCL_OK) { + return TCL_ERROR; + } + if (comparePrefixMaskc != 2) { + Tcl_AddErrorInfo(interp,"need format {{ } { +# +# Original Author -- Emmanuel Frecon - emmanuel@sics.se +# Modified by Pat Thoyts +# +# A super module on top of the dns module for host name resolution. +# There are two services provided on top of the regular Tcl library: +# Firstly, this module attempts to automatically discover the default +# DNS server that is setup on the machine that it is run on. This +# server will be used in all further host resolutions. Secondly, this +# module offers a rudimentary cache. The cache is rudimentary since it +# has no expiration on host name resolutions, but this is probably +# enough for short lived applications. +# +# ------------------------------------------------------------------------- +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# ------------------------------------------------------------------------- +# +# $Id: resolv.tcl,v 1.9 2004/01/25 07:29:39 andreas_kupries Exp $ + +package require dns 1.0; # tcllib 1.3 + +namespace eval ::resolv { + variable version 1.0.3 + variable rcsid {$Id: resolv.tcl,v 1.9 2004/01/25 07:29:39 andreas_kupries Exp $} + + namespace export resolve init ignore hostname + + variable R + if {![info exists R]} { + array set R { + initdone 0 + dns "" + dnsdefault "" + ourhost "" + search {} + } + } +} + +# ------------------------------------------------------------------------- +# Command Name -- ignore +# Original Author -- Emmanuel Frecon - emmanuel@sics.se +# +# Remove a host name resolution from the cache, if present, so that the +# next resolution will query the DNS server again. +# +# Arguments: +# hostname - Name of host to remove from the cache. +# +proc ::resolv::ignore { hostname } { + variable Cache + catch {unset Cache($hostname)} + return +} + +# ------------------------------------------------------------------------- +# Command Name -- init +# Original Author -- Emmanuel Frecon - emmanuel@sics.se +# +# Initialise this module with a known host name. This host (not mandatory) +# will become the default if the library was not able to find a DNS server. +# This command can be called several times, its effect is double: actively +# looking for the default DNS server setup on the running machine; and +# emptying the host name resolution cache. +# +# Arguments: +# defaultdns - Default DNS server +# +proc ::resolv::init { {defaultdns ""} {search {}}} { + variable R + variable Cache + + # Clean the resolver cache + catch {unset Cache} + + # Record the default DNS server and search list. + set R(dnsdefault) $defaultdns + set R(search) $search + + # Now do some intelligent lookup. We do this on the current + # hostname to get a chance to get back some (full) information on + # ourselves. A previous version was using 127.0.0.1, not sure + # what is best. + set res [catch [list exec nslookup [info hostname]] lkup] + if { $res == 0 } { + set l [split $lkup] + set nl "" + foreach e $l { + if { [string length $e] > 0 } { + lappend nl $e + } + } + + # Now, a lot of mixture to arrange so that hostname points at the + # DNS server that we should use for any further request. This + # code is complex, but was actually tested behind a firewall + # during the SITI Winter Conference 2003. There, strangly, + # nslookup returned an error but a DNS server was actually setup + # correctly... + set hostname "" + set len [llength $nl] + for { set i 0 } { $i < $len } { incr i } { + set e [lindex $nl $i] + if { [string match -nocase "*server*" $e] } { + set hostname [lindex $nl [expr {$i + 1}]] + if { [string match -nocase "UnKnown" $hostname] } { + set hostname "" + } + break + } + } + + if { $hostname != "" } { + set R(dns) $hostname + } else { + for { set i 0 } { $i < $len } { incr i } { + set e [lindex $nl $i] + if { [string match -nocase "*address*" $e] } { + set hostname [lindex $nl [expr {$i + 1}]] + break + } + } + if { $hostname != "" } { + set R(dns) $hostname + } + } + } + + if {$R(dns) == ""} { + set R(dns) $R(dnsdefault) + } + + + # Start again to find our full name + set ourhost "" + if {$res == 0} { + set dot [string first "." [info hostname]] + if { $dot < 0 } { + for { set i 0 } { $i < $len } { incr i } { + set e [lindex $nl $i] + if { [string match -nocase "*name*" $e] } { + set ourhost [lindex $nl [expr {$i + 1}]] + break + } + } + if { $ourhost == "" } { + if { ! [regexp {\d+\.\d+\.\d+\.\d+} $hostname] } { + set dot [string first "." $hostname] + set ourhost [format "%s%s" [info hostname] \ + [string range $hostname $dot end]] + } + } + } else { + set ourhost [info hostname] + } + } + + if {$ourhost == ""} { + set R(ourhost) [info hostname] + } else { + set R(ourhost) $ourhost + } + + + set R(initdone) 1 + + return $R(dns) +} + +# ------------------------------------------------------------------------- +# Command Name -- resolve +# Original Author -- Emmanuel Frecon - emmanuel@sics.se +# +# Resolve a host name to an IP address. This is a wrapping procedure around +# the basic services of the dns library. +# +# Arguments: +# hostname - Name of host +# +proc ::resolv::resolve { hostname } { + variable R + variable Cache + + # Initialise if not already done. Auto initialisation cannot take + # any known DNS server (known to the caller) + if { ! $R(initdone) } { init } + + # Check whether this is not simply a raw IP address. What about + # IPv6 ?? + # - We don't have sockets in Tcl for IPv6 protocols - [PT] + # + if { [regexp {\d+\.\d+\.\d+\.\d+} $hostname] } { + return $hostname + } + + # Look for hostname in the cache, if found return. + if { [array names ::resolv::Cache $hostname] != "" } { + return $::resolv::Cache($hostname) + } + + # Scream if we don't have any DNS server setup, since we cannot do + # anything in that case. + if { $R(dns) == "" } { + return -code error "No dns server provided" + } + + set R(retries) 0 + set ip [Resolve $hostname] + + # And store the result of resolution in our cache for further use. + set Cache($hostname) $ip + + return $ip +} + +# Description: +# Attempt to resolve hostname via DNS. If the name cannot be resolved then +# iterate through the search list appending each domain in turn until we +# get one that succeeds. +# +proc ::resolv::Resolve {hostname} { + variable R + set t [::dns::resolve $hostname -server $R(dns)] + ::dns::wait $t; # wait with event processing + set status [dns::status $t] + if {$status == "ok"} { + set ip [lindex [::dns::address $t] 0] + ::dns::cleanup $t + } elseif {$status == "error" + && [::dns::errorcode $t] == 3 + && $R(retries) < [llength $R(search)]} { + ::dns::cleanup $t + set suffix [lindex $R(search) $R(retries)] + incr R(retries) + set new [lindex [split $hostname .] 0].[string trim $suffix .] + set ip [Resolve $new] + } else { + set err [dns::error $t] + ::dns::cleanup $t + return -code error "dns error: $err" + } + return $ip +} + +# ------------------------------------------------------------------------- + +package provide resolv $::resolv::version + +# ------------------------------------------------------------------------- +# Local Variables: +# indent-tabs-mode: nil +# End: diff --git a/lib/dns/spf.tcl b/lib/dns/spf.tcl new file mode 100644 index 0000000..cf5e10a --- /dev/null +++ b/lib/dns/spf.tcl @@ -0,0 +1,533 @@ +# spf.tcl - Copyright (C) 2004 Pat Thoyts +# +# Sender Policy Framework +# +# http://www.ietf.org/internet-drafts/draft-ietf-marid-protocol-00.txt +# http://spf.pobox.com/ +# +# Some domains using SPF: +# pobox.org - mx, a, ptr +# oxford.ac.uk - include +# gnu.org - ip4 +# aol.com - ip4, ptr +# sourceforge.net - mx, a +# altavista.com - exists, multiple TXT replies. +# oreilly.com - mx, ptr, include +# motleyfool.com - include (looping includes) +# +# ------------------------------------------------------------------------- +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# ------------------------------------------------------------------------- +# +# $Id: spf.tcl,v 1.5 2008/03/14 21:21:12 andreas_kupries Exp $ + +package require Tcl 8.2; # tcl minimum version +package require dns; # tcllib 1.3 +package require logger; # tcllib 1.3 +package require ip; # tcllib 1.7 +package require struct::list; # tcllib 1.7 +package require uri::urn; # tcllib 1.3 + +namespace eval spf { + variable version 1.1.1 + variable rcsid {$Id: spf.tcl,v 1.5 2008/03/14 21:21:12 andreas_kupries Exp $} + + namespace export spf + + variable uid + if {![info exists uid]} {set uid 0} + + variable log + if {![info exists log]} { + set log [logger::init spf] + ${log}::setlevel warn + proc ${log}::stdoutcmd {level text} { + variable service + puts "\[[clock format [clock seconds] -format {%H:%M:%S}]\ + $service $level\] $text" + } + } +} + +# ------------------------------------------------------------------------- +# ip : ip address of the connecting host +# domain : the domain to match +# sender : full sender email address +# +proc ::spf::spf {ip domain sender} { + variable log + + # 3.3: Initial processing + # If the sender address has no local part, set it to postmaster + set addr [split $sender @] + if {[set len [llength $addr]] == 0} { + return -code error -errorcode permanent "invalid sender address" + } elseif {$len == 1} { + set sender "postmaster@$sender" + } + + # 3.4: Record lookup + set spf [SPF $domain] + if {[string equal $spf none]} { + return $spf + } + + return [Spf $ip $domain $sender $spf] +} + +proc ::spf::Spf {ip domain sender spf} { + variable log + + # 3.4.1: Matching Version + if {![regexp {^v=spf(\d)\s+} $spf -> version]} { + return none + } + + ${log}::debug "$spf" + + if {$version != 1} { + return -code error -errorcode permanent \ + "version mismatch: we only understand SPF 1\ + this domain has provided version \"$version\"" + } + + set result ? + set seen_domains $domain + set explanation {denied} + + set directives [lrange [split $spf { }] 1 end] + foreach directive $directives { + set prefix [string range $directive 0 0] + if {[string equal $prefix "+"] || [string equal $prefix "-"] + || [string equal $prefix "?"] || [string equal $prefix "~"]} { + set directive [string range $directive 1 end] + } else { + set prefix "+" + } + + set cmd [string tolower [lindex [split $directive {:/=}] 0]] + set param [string range $directive [string length $cmd] end] + + if {[info command ::spf::_$cmd] == {}} { + # 6.1 Unrecognised directives terminate processing + # but unknown modifiers are ignored. + if {[string match "=*" $param]} { + continue + } else { + set result unknown + break + } + } else { + set r [catch {::spf::_$cmd $ip $domain $sender $param} res] + if {$r} { + if {$r == 2} {return $res};# deal with return -code return + if {[string equal $res "none"] + || [string equal $res "error"] + || [string equal $res "unknown"]} { + return $res + } + return -code error "error in \"$cmd\": $res" + } + if {$res} { set result $prefix } + } + + ${log}::debug "$prefix $cmd\($param) -> $result" + if {[string equal $result "+"]} break + } + + return $result +} + +proc ::spf::loglevel {level} { + variable log + ${log}::setlevel $level +} + +# get a guaranteed unique and non-present token id. +proc ::spf::create_token {} { + variable uid + set id [incr uid] + while {[info exists [set token [namespace current]::$id]]} { + set id [incr uid] + } + return $token +} + +# ------------------------------------------------------------------------- +# +# SPF MECHANISM HANDLERS +# +# ------------------------------------------------------------------------- + +# 4.1: The "all" mechanism is a test that always matches. It is used as the +# rightmost mechanism in an SPF record to provide an explicit default +# +proc ::spf::_all {ip domain sender param} { + return 1 +} + +# 4.2: The "include" mechanism triggers a recursive SPF query. +# The domain-spec is expanded as per section 8. +proc ::spf::_include {ip domain sender param} { + variable log + upvar seen_domains Seen + + if {![string equal [string range $param 0 0] ":"]} { + return -code error "dubious parameters for \"include\"" + } + set r ? + set new_domain [Expand [string range $param 1 end] $ip $domain $sender] + if {[lsearch $Seen $new_domain] == -1} { + lappend Seen $new_domain + set spf [SPF $new_domain] + if {[string equal $spf none]} { + return $spf + } + set r [Spf $ip $new_domain $sender $spf] + } + return [string equal $r "+"] +} + +# 4.4: This mechanism matches if is one of the target's +# IP addresses. +# e.g: a:smtp.example.com a:mail.%{d} a +# +proc ::spf::_a {ip domain sender param} { + variable log + foreach {testdomain bits} [ip::SplitIp [string trimleft $param :]] {} + if {[string length $testdomain] < 1} { + set testdomain $domain + } else { + set testdomain [Expand $testdomain $ip $domain $sender] + } + ${log}::debug " fetching A for $testdomain" + set dips [A $testdomain]; # get the IPs for the testdomain + foreach dip $dips { + ${log}::debug " compare: ${ip}/${bits} with ${dip}/${bits}" + if {[ip::equal $ip/$bits $dip/$bits]} { + return 1 + } + } + return 0 +} + +# 4.5: This mechanism matches if the is one of the MX hosts +# for a domain name. +# +proc ::spf::_mx {ip domain sender param} { + variable log + foreach {testdomain bits} [ip::SplitIp [string trimleft $param :]] {} + if {[string length $testdomain] < 1} { + set testdomain $domain + } else { + set testdomain [Expand $testdomain $ip $domain $sender] + } + ${log}::debug " fetching MX for $testdomain" + set mxs [MX $testdomain] + + foreach mx $mxs { + set mx [lindex $mx 1] + set mxips [A $mx] + foreach mxip $mxips { + ${log}::debug " compare: ${ip}/${bits} with ${mxip}/${bits}" + if {[ip::equal $ip/$bits $mxip/$bits]} { + return 1 + } + } + } + return 0 +} + +# 4.6: This mechanism tests if the 's name is within a +# particular domain. +# +proc ::spf::_ptr {ip domain sender param} { + variable log + set validnames {} + if {[catch { set names [PTR $ip] } msg]} { + ${log}::debug " \"$ip\" $msg" + return 0 + } + foreach name $names { + set addrs [A $name] + foreach addr $addrs { + if {[ip::equal $ip $addr]} { + lappend validnames $name + continue + } + } + } + + ${log}::debug " validnames: $validnames" + set testdomain [Expand [string trimleft $param :] $ip $domain $sender] + if {$testdomain == {}} { + set testdomain $domain + } + foreach name $validnames { + if {[string match "*$testdomain" $name]} { + return 1 + } + } + + return 0 +} + +# 4.7: These mechanisms test if the falls into a given IP +# network. +# +proc ::spf::_ip4 {ip domain sender param} { + variable log + foreach {network bits} [ip::SplitIp [string range $param 1 end]] {} + ${log}::debug " compare ${ip}/${bits} to ${network}/${bits}" + if {[ip::equal $ip/$bits $network/$bits]} { + return 1 + } + return 0 +} + +# 4.6: These mechanisms test if the falls into a given IP +# network. +# +proc ::spf::_ip6 {ip domain sender param} { + variable log + foreach {network bits} [ip::SplitIp [string range $param 1 end]] {} + ${log}::debug " compare ${ip}/${bits} to ${network}/${bits}" + if {[ip::equal $ip/$bits $network/$bits]} { + return 1 + } + return 0 +} + +# 4.7: This mechanism is used to construct an arbitrary host name that is +# used for a DNS A record query. It allows for complicated schemes +# involving arbitrary parts of the mail envelope to determine what is +# legal. +# +proc ::spf::_exists {ip domain sender param} { + variable log + set testdomain [Expand [string range $param 1 end] $ip $domain $sender] + ${log}::debug " checking existence of '$testdomain'" + if {[catch {A $testdomain}]} { + return 0 + } + return 1 +} + +# 5.1: Redirected query +# +proc ::spf::_redirect {ip domain sender param} { + variable log + set new_domain [Expand [string range $param 1 end] $ip $domain $sender] + ${log}::debug ">> redirect to '$new_domain'" + set spf [SPF $new_domain] + if {![string equal $spf none]} { + set spf [Spf $ip $new_domain $sender $spf] + } + ${log}::debug "<< redirect returning '$spf'" + return -code return $spf +} + +# 5.2: Explanation +# +proc ::spf::_exp {ip domain sender param} { + variable log + set new_domain [string range $param 1 end] + set exp [TXT $new_domain] + set exp [Expand $exp $ip $domain $sender] + ${log}::debug "exp expanded to \"$exp\"" + # FIX ME: need to store this somehow. +} + +# 5.3: Sender accreditation +# +proc ::spf::_accredit {ip domain sender param} { + variable log + set accredit [Expand [string range $param 1 end] $ip $domain $sender] + ${log}::debug " accreditation '$accredit'" + # We are not using this at the moment. + return 0 +} + + +# 7: Macro expansion +# +proc ::spf::Expand {txt ip domain sender} { + variable log + set re {%\{[[:alpha:]](?:\d+)?r?[\+\-\.,/_=]*\}} + set txt [string map {\[ \\\[ \] \\\]} $txt] + regsub -all $re $txt {[ExpandMacro & $ip $domain $sender]} cmd + set cmd [string map {%% % %_ \ %- %20} $cmd] + return [subst -novariables $cmd] +} + +proc ::spf::ExpandMacro {macro ip domain sender} { + variable log + set re {%\{([[:alpha:]])(\d+)?(r)?([\+\-\.,/_=]*)\}} + set C {} ; set T {} ; set R {}; set D {} + set r [regexp $re $macro -> C T R D] + if {$R == {}} {set R 0} else {set R 1} + set res $macro + if {$r} { + set enc [string is upper $C] + switch -exact -- [string tolower $C] { + s { set res $sender } + l { + set addr [split $sender @] + if {[llength $addr] < 2} { + set res postmaster + } else { + set res [lindex $addr 0] + } + } + o { + set addr [split $sender @] + if {[llength $addr] < 2} { + set res $sender + } else { + set res [lindex $addr 1] + } + } + h - d { set res $domain } + i { + set res [ip::normalize $ip] + if {[ip::is ipv6 $res]} { + # Convert 0000:0001 to 0.1 + set t {} + binary scan [ip::Normalize $ip 6] c* octets + foreach octet $octets { + set hi [expr {($octet & 0xF0) >> 4}] + set lo [expr {$octet & 0x0F}] + lappend t [format %x $hi] [format %x $lo] + } + set res [join $t .] + } + } + v { + if {[ip::is ipv6 $ip]} { + set res ip6 + } else { + set res "in-addr" + } + } + c { + set res [ip::normalize $ip] + if {[ip::is ipv6 $res]} { + set res [ip::contract $res] + } + } + r { + set s [socket -server {} -myaddr [info host] 0] + set res [lindex [fconfigure $s -sockname] 1] + close $s + } + t { set res [clock seconds] } + } + if {$T != {} || $R || $D != {}} { + if {$D == {}} {set D .} + set res [split $res $D] + if {$R} { + set res [struct::list::Lreverse $res] + } + if {$T != {}} { + incr T -1 + set res [join [lrange $res end-$T end] $D] + } + set res [join $res .] + } + if {$enc} { + # URI encode the result. + set res [uri::urn::quote $res] + } + } + return $res +} + +# ------------------------------------------------------------------------- +# +# DNS helper procedures. +# +# ------------------------------------------------------------------------- + +proc ::spf::Resolve {domain type resultproc} { + if {[info command $resultproc] == {}} { + return -code error "invalid arg: \"$resultproc\" must be a command" + } + set tok [dns::resolve $domain -type $type] + dns::wait $tok + set errorcode NONE + if {[string equal [dns::status $tok] "ok"]} { + set result [$resultproc $tok] + set code ok + } else { + set result [dns::error $tok] + set errorcode [dns::errorcode $tok] + set code error + } + dns::cleanup $tok + return -code $code -errorcode $errorcode $result +} + +# 3.4: Record lookup +proc ::spf::SPF {domain} { + set txt "" + if {[catch {Resolve $domain SPF ::dns::result} spf]} { + set code $::errorCode + ${log}::debug "error fetching SPF record: $r" + switch -exact -- $code { + 3 { return -code return [list - "Domain Does Not Exist"] } + 2 { return -code error -errorcode temporary $spf } + } + set txt none + } else { + foreach res $spf { + set ndx [lsearch $res rdata] + incr ndx + if {$ndx != 0} { + append txt [string range [lindex $res $ndx] 1 end] + } + } + } + return $txt +} + +proc ::spf::TXT {domain} { + set r [Resolve $domain TXT ::dns::result] + set txt "" + foreach res $r { + set ndx [lsearch $res rdata] + incr ndx + if {$ndx != 0} { + append txt [string range [lindex $res $ndx] 1 end] + } + } + return $txt +} + +proc ::spf::A {name} { + return [Resolve $name A ::dns::address] +} + + +proc ::spf::AAAA {name} { + return [Resolve $name AAAA ::dns::address] +} + +proc ::spf::PTR {addr} { + return [Resolve $addr A ::dns::name] +} + +proc ::spf::MX {domain} { + set r [Resolve $domain MX ::dns::name] + return [lsort -index 0 $r] +} + + +# ------------------------------------------------------------------------- + +package provide spf $::spf::version + +# ------------------------------------------------------------------------- +# Local Variables: +# indent-tabs-mode: nil +# End: diff --git a/lib/irc/irc.tcl b/lib/irc/irc.tcl new file mode 100644 index 0000000..f3ee41d --- /dev/null +++ b/lib/irc/irc.tcl @@ -0,0 +1,521 @@ +# irc.tcl -- +# +# irc implementation for Tcl. +# +# Copyright (c) 2001-2003 by David N. Welton . +# This code may be distributed under the same terms as Tcl. +# +# $Id: irc.tcl,v 1.26 2006/04/23 22:35:57 patthoyts Exp $ + +package require Tcl 8.3 + +namespace eval ::irc { + variable version 0.6 + + # counter used to differentiate connections + variable conn 0 + variable config + variable irctclfile [info script] + array set config { + debug 0 + logger 0 + } +} + +# ::irc::config -- +# +# Set global configuration options. +# +# Arguments: +# +# key name of the configuration option to change. +# +# value value of the configuration option. + +proc ::irc::config { args } { + variable config + if { [llength $args] == 0 } { + return [array get config] + } elseif { [llength $args] == 1 } { + return $config($key) + } elseif { [llength $args] > 2 } { + error "wrong # args: should be \"config key ?val?\"" + } + set key [lindex $args 0] + set value [lindex $args 1] + foreach ns [namespace children] { + if { [info exists config($key)] && [info exists ${ns}::config($key)] \ + && [set ${ns}::config($key)] == $config($key)} { + ${ns}::cmd-config $key $value + } + } + set config($key) $value +} + + +# ::irc::connections -- +# +# Return a list of handles to all existing connections + +proc ::irc::connections { } { + set r {} + foreach ns [namespace children] { + lappend r ${ns}::network + } + return $r +} + +# ::irc::reload -- +# +# Reload this file, and merge the current connections into +# the new one. + +proc ::irc::reload { } { + variable conn + set oldconn $conn + namespace eval :: { + source [set ::irc::irctclfile] + } + foreach ns [namespace children] { + foreach var {sock logger host port} { + set $var [set ${ns}::$var] + } + array set dispatch [array get ${ns}::dispatch] + array set config [array get ${ns}::config] + # make sure our new connection uses the same namespace + set conn [string range $ns 10 end] + ::irc::connection + foreach var {sock logger host port} { + set ${ns}::$var [set $var] + } + array set ${ns}::dispatch [array get dispatch] + array set ${ns}::config [array get config] + } + set conn $oldconn +} + +# ::irc::connection -- +# +# Create an IRC connection namespace and associated commands. + +proc ::irc::connection { args } { + variable conn + variable config + + # Create a unique namespace of the form irc$conn::$host + + set name [format "%s::irc%s" [namespace current] $conn] + + namespace eval $name { + set sock {} + array set dispatch {} + array set linedata {} + array set config [array get ::irc::config] + if { $config(logger) || $config(debug)} { + package require logger + set logger [logger::init [namespace tail [namespace current]]] + if { !$config(debug) } { ${logger}::disable debug } + } + + + # ircsend -- + # send text to the IRC server + + proc ircsend { msg } { + variable sock + variable dispatch + if { $sock == "" } { return } + cmd-log debug "ircsend: '$msg'" + if { [catch {puts $sock $msg} err] } { + catch { close $sock } + set sock {} + if { [info exists dispatch(EOF)] } { + eval $dispatch(EOF) + } + cmd-log error "Error in ircsend: $err" + } + } + + + ######################################################### + # Implemented user-side commands, meaning that these commands + # cause the calling user to perform the given action. + ######################################################### + + + # cmd-config -- + # + # Set or return per-connection configuration options. + # + # Arguments: + # + # key name of the configuration option to change. + # + # value value (optional) of the configuration option. + + proc cmd-config { args } { + variable config + variable logger + + if { [llength $args] == 0 } { + return [array get config] + } elseif { [llength $args] == 1 } { + return $config($key) + } elseif { [llength $args] > 2 } { + error "wrong # args: should be \"config key ?val?\"" + } + set key [lindex $args 0] + set value [lindex $args 1] + if { $key == "debug" } { + if {$value} { + if { !$config(logger) } { cmd-config logger 1 } + ${logger}::enable debug + } elseif { [info exists logger] } { + ${logger}::disable debug + } + } + if { $key == "logger" } { + if { $value && !$config(logger)} { + package require logger + set logger [logger::init [namespace tail [namespace current]]] + } elseif { [info exists logger] } { + ${logger}::delete + unset logger + } + } + set config($key) $value + } + + proc cmd-log {level text} { + variable logger + if { ![info exists logger] } return + ${logger}::$level $text + } + + proc cmd-logname { } { + variable logger + if { ![info exists logger] } return + return $logger + } + + # cmd-destroy -- + # + # destroys the current connection and its namespace + + proc cmd-destroy { } { + variable logger + variable sock + if { [info exists logger] } { ${logger}::delete } + catch {close $sock} + namespace delete [namespace current] + } + + proc cmd-connected { } { + variable sock + if { $sock == "" } { return 0 } + return 1 + } + + proc cmd-user { username hostname servername {userinfo ""} } { + if { $userinfo == "" } { + ircsend "USER $username $hostname server :$servername" + } else { + ircsend "USER $username $hostname $servername :$userinfo" + } + } + + proc cmd-nick { nk } { + ircsend "NICK $nk" + } + + proc cmd-ping { target } { + ircsend "PRIVMSG $target :\001PING [clock seconds]\001" + } + + proc cmd-serverping { } { + ircsend "PING [clock seconds]" + } + + proc cmd-ctcp { target line } { + ircsend "PRIVMSG $target :\001$line\001" + } + + proc cmd-join { chan {key {}} } { + ircsend "JOIN $chan $key" + } + + proc cmd-part { chan {msg ""} } { + if { $msg == "" } { + ircsend "PART $chan" + } else { + ircsend "PART $chan :$msg" + } + } + + proc cmd-quit { {msg {tcllib irc module - http://tcllib.sourceforge.net/}} } { + ircsend "QUIT :$msg" + } + + proc cmd-privmsg { target msg } { + ircsend "PRIVMSG $target :$msg" + } + + proc cmd-notice { target msg } { + ircsend "NOTICE $target :$msg" + } + + proc cmd-kick { chan target {msg {}} } { + ircsend "KICK $chan $target :$msg" + } + + proc cmd-mode { target args } { + ircsend "MODE $target [join $args]" + } + + proc cmd-topic { chan msg } { + ircsend "TOPIC $chan :$msg" + } + + proc cmd-invite { chan target } { + ircsend "INVITE $target $chan" + } + + proc cmd-send { line } { + ircsend $line + } + + proc cmd-peername { } { + variable sock + if { $sock == "" } { return {} } + return [fconfigure $sock -peername] + } + + proc cmd-sockname { } { + variable sock + if { $sock == "" } { return {} } + return [fconfigure $sock -sockname] + } + + proc cmd-socket { } { + variable sock + return $sock + } + + proc cmd-disconnect { } { + variable sock + if { $sock == "" } { return -1 } + catch { close $sock } + set sock {} + return 0 + } + + # Connect -- + # Create the actual tcp connection. + + proc cmd-connect { h {p 6667} } { + variable sock + variable host + variable port + + set host $h + set port $p + + if { $sock == "" } { + set sock [socket $host $port] + fconfigure $sock -translation crlf -buffering line + fileevent $sock readable [namespace current]::GetEvent + } + return 0 + } + + # Callback API: + + # These are all available from within callbacks, so as to + # provide an interface to provide some information on what is + # coming out of the server. + + # action -- + + # Action returns the action performed, such as KICK, PRIVMSG, + # MODE etc, including numeric actions such as 001, 252, 353, + # and so forth. + + proc action { } { + variable linedata + return $linedata(action) + } + + # msg -- + + # The last argument of the line, after the last ':'. + + proc msg { } { + variable linedata + return $linedata(msg) + } + + # who -- + + # Who performed the action. If the command is called as [who address], + # it returns the information in the form + # nick!ident@host.domain.net + + proc who { {address 0} } { + variable linedata + if { $address == 0 } { + return [lindex [split $linedata(who) !] 0] + } else { + return $linedata(who) + } + } + + # target -- + + # To whom was this action done. + + proc target { } { + variable linedata + return $linedata(target) + } + + # additional -- + + # Returns any additional header elements beyond the target as a list. + + proc additional { } { + variable linedata + return $linedata(additional) + } + + # header -- + + # Returns the entire header in list format. + + proc header { } { + variable linedata + return [concat [list $linedata(who) $linedata(action) \ + $linedata(target)] $linedata(additional)] + } + + # GetEvent -- + + # Get a line from the server and dispatch it. + + proc GetEvent { } { + variable linedata + variable sock + variable dispatch + array set linedata {} + set line "eof" + if { [eof $sock] || [catch {gets $sock} line] } { + close $sock + set sock {} + cmd-log error "Error receiving from network: $line" + if { [info exists dispatch(EOF)] } { + eval $dispatch(EOF) + } + return + } + cmd-log debug "Recieved: $line" + if { [set pos [string first " :" $line]] > -1 } { + set header [string range $line 0 [expr {$pos - 1}]] + set linedata(msg) [string range $line [expr {$pos + 2}] end] + } else { + set header [string trim $line] + set linedata(msg) {} + } + + if { [string match :* $header] } { + set header [split [string trimleft $header :]] + } else { + set header [linsert [split $header] 0 {}] + } + set linedata(who) [lindex $header 0] + set linedata(action) [lindex $header 1] + set linedata(target) [lindex $header 2] + set linedata(additional) [lrange $header 3 end] + if { [info exists dispatch($linedata(action))] } { + eval $dispatch($linedata(action)) + } elseif { [string match {[0-9]??} $linedata(action)] } { + eval $dispatch(defaultnumeric) + } elseif { $linedata(who) == "" } { + eval $dispatch(defaultcmd) + } else { + eval $dispatch(defaultevent) + } + } + + # registerevent -- + + # Register an event in the dispatch table. + + # Arguments: + # evnt: name of event as sent by IRC server. + # cmd: proc to register as the event handler + + proc cmd-registerevent { evnt cmd } { + variable dispatch + set dispatch($evnt) $cmd + if { $cmd == "" } { + unset dispatch($evnt) + } + } + + # getevent -- + + # Return the currently registered handler for the event. + + # Arguments: + # evnt: name of event as sent by IRC server. + + proc cmd-getevent { evnt } { + variable dispatch + if { [info exists dispatch($evnt)] } { + return $dispatch($evnt) + } + return {} + } + + # eventexists -- + + # Return a boolean value indicating if there is a handler + # registered for the event. + + # Arguments: + # evnt: name of event as sent by IRC server. + + proc cmd-eventexists { evnt } { + variable dispatch + return [info exists dispatch($evnt)] + } + + # network -- + + # Accepts user commands and dispatches them. + + # Arguments: + # cmd: command to invoke + # args: arguments to the command + + proc network { cmd args } { + eval [linsert $args 0 [namespace current]::cmd-$cmd] + } + + # Create default handlers. + + set dispatch(PING) {network send "PONG :[msg]"} + set dispatch(defaultevent) # + set dispatch(defaultcmd) # + set dispatch(defaultnumeric) # + } + + set returncommand [format "%s::irc%s::network" [namespace current] $conn] + incr conn + return $returncommand +} + +# ------------------------------------------------------------------------- + +package provide irc $::irc::version + +# ------------------------------------------------------------------------- diff --git a/lib/irc/picoirc.tcl b/lib/irc/picoirc.tcl new file mode 100644 index 0000000..75c1d05 --- /dev/null +++ b/lib/irc/picoirc.tcl @@ -0,0 +1,261 @@ +# Based upon the picoirc code by Salvatore Sanfillipo and Richard Suchenwirth +# See http://wiki.tcl.tk/13134 for the original standalone version. +# +# This package provides a general purpose minimal IRC client suitable for +# embedding in other applications. All communication with the parent +# application is done via an application provided callback procedure. +# +# Copyright (c) 2004 Salvatore Sanfillipo +# Copyright (c) 2004 Richard Suchenwirth +# Copyright (c) 2007 Patrick Thoyts +# +# ------------------------------------------------------------------------- +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# ------------------------------------------------------------------------- +# +# $Id: picoirc.tcl,v 1.3 2007/10/24 10:38:57 patthoyts Exp $ + +namespace eval ::picoirc { + variable version 0.5 + variable uid; if {![info exists uid]} { set uid 0 } + variable defaults { + server "irc.freenode.net" + port 6667 + channel "" + callback "" + motd {} + users {} + } + namespace export connect send post splituri +} + +proc ::picoirc::splituri {uri} { + foreach {server port channel} {{} {} {}} break + if {![regexp {^irc://([^:/]+)(?::([^/]+))?(?:/([^,]+))?} $uri -> server port channel]} { + regexp {^(?:([^@]+)@)?([^:]+)(?::(\d+))?} $uri -> channel server port + } + if {$port eq {}} { set port 6667 } + return [list $server $port $channel] +} + +proc ::picoirc::connect {callback nick args} { + if {[llength $args] > 2} { + return -code error "wrong # args: must be \"callback nick ?passwd? url\"" + } elseif {[llength $args] == 1} { + set url [lindex $args 0] + } else { + foreach {passwd url} $args break + } + variable defaults + variable uid + set context [namespace current]::irc[incr uid] + upvar #0 $context irc + array set irc $defaults + foreach {server port channel} [splituri $url] break + if {[info exists channel] && $channel ne ""} {set irc(channel) $channel} + if {[info exists server] && $server ne ""} {set irc(server) $server} + if {[info exists port] && $port ne ""} {set irc(port) $port} + if {[info exists passwd] && $passwd ne ""} {set irc(passwd) $passwd} + set irc(callback) $callback + set irc(nick) $nick + Callback $context init + set irc(socket) [socket -async $irc(server) $irc(port)] + fileevent $irc(socket) readable [list [namespace origin Read] $context] + fileevent $irc(socket) writable [list [namespace origin Write] $context] + return $context +} + +proc ::picoirc::Callback {context state args} { + upvar #0 $context irc + if {[llength $irc(callback)] > 0 + && [llength [info commands [lindex $irc(callback) 0]]] == 1} { + if {[catch {eval $irc(callback) [list $context $state] $args} err]} { + puts stderr "callback error: $err" + } + } +} + +proc ::picoirc::Version {context} { + if {[catch {Callback $context version} ver]} { set ver {} } + if {$ver eq {}} { + set ver "PicoIRC:[package provide picoirc]:Tcl [info patchlevel]" + } + return $ver +} + +proc ::picoirc::Write {context} { + upvar #0 $context irc + fileevent $irc(socket) writable {} + if {[set err [fconfigure $irc(socket) -error]] ne ""} { + Callback $context close $err + close $irc(socket) + unset irc + return + } + fconfigure $irc(socket) -blocking 0 -buffering line -translation crlf -encoding utf-8 + Callback $context connect + if {[info exists irc(passwd)]} { + send $context "PASS $irc(passwd)" + } + set ver [join [lrange [split [Version $context] :] 0 1] " "] + send $context "NICK $irc(nick)" + send $context "USER $::tcl_platform(user) 0 * :$ver user" + if {$irc(channel) ne {}} { + after idle [list [namespace origin send] $context "JOIN $irc(channel)"] + } + return +} + +proc ::picoirc::Splitirc {s} { + foreach v {nick flags user host} {set $v {}} + regexp {^([^!]*)!([^=]*)=([^@]+)@(.*)} $s -> nick flags user host + return [list $nick $flags $user $host] +} + +proc ::picoirc::Read {context} { + upvar #0 $context irc + if {[eof $irc(socket)]} { + fileevent $irc(socket) readable {} + Callback $context close + close $irc(socket) + unset irc + return + } + if {[gets $irc(socket) line] != -1} { + if {[string match "PING*" $line]} { + send $context "PONG [info hostname] [lindex [split $line] 1]" + return + } + # the callback can return -code break to prevent processing the read + if {[catch {Callback $context debug read $line}] == 3} { + return + } + if {[regexp {:([^!]*)![^ ].* +PRIVMSG ([^ :]+) +:(.*)} $line -> \ + nick target msg]} { + set type "" + if {[regexp {\001(\S+)(.*)?\001} $msg -> ctcp data]} { + switch -- $ctcp { + ACTION { set type ACTION ; set msg $data } + VERSION { + send $context "PRIVMSG $nick :\001VERSION [Version $context]\001" + return + } + default { + send $context "PRIVMSG $nick :\001ERRMSG $msg : unknown query" + return + } + } + } + if {[lsearch -exact {azbridge ijchain} $nick] != -1} { + if {$type eq "ACTION"} { + regexp {(\S+) (.+)} $msg -> nick msg + } else { + regexp {<([^>]+)> (.+)} $msg -> nick msg + } + } + Callback $context chat $target $nick $msg $type + } elseif {[regexp {^:((?:([^ ]+) +){1,}?):(.*)$} $line -> parts junk rest]} { + foreach {server code target fourth fifth} [split $parts] break + switch -- $code { + 001 - 002 - 003 - 004 - 005 - 250 - 251 - 252 - + 254 - 255 - 265 - 266 { return } + 433 { + variable nickid ; if {![info exists nickid]} {set nickid 0} + set seqlen [string length [incr nickid]] + set irc(nick) [string range $irc(nick) 0 [expr 8-$seqlen]]$nickid + send $context "NICK $irc(nick)" + } + 353 { set irc(users) [concat $irc(users) $rest]; return } + 366 { + Callback $context userlist $fourth $irc(users) + set irc(users) {} + return + } + 332 { Callback $context topic $fourth $rest; return } + 333 { return } + 375 { set irc(motd) {} ; return } + 372 { append irc(motd) $rest ; return} + 376 { return } + 311 { + foreach {server code target nick name host x} [split $parts] break + set irc(whois,$fourth) [list name $name host $host userinfo $rest] + return + } + 301 - 312 - 317 - 320 { return } + 319 { lappend irc(whois,$fourth) channels $rest; return } + 318 { + if {[info exists irc(whois,$fourth)]} { + Callback $context userinfo $fourth $irc(whois,$fourth) + unset irc(whois,$fourth) + } + return + } + JOIN { + foreach {n f u h} [Splitirc $server] break + Callback $context traffic entered $rest $n + return + } + NICK { + foreach {n f u h} [Splitirc $server] break + Callback $context traffic nickchange {} $n $rest + return + } + QUIT - PART { + foreach {n f u h} [Splitirc $server] break + Callback $context traffic left $target $n + return + } + } + Callback $context system "" "[lrange [split $parts] 1 end] $rest" + } else { + Callback $context system "" $line + } + } +} + +proc ::picoirc::post {context channel msg} { + upvar #0 $context irc + set type "" + if [regexp {^/([^ ]+) *(.*)} $msg -> cmd msg] { + regexp {^([^ ]+)?(?: +(.*))?} $msg -> first rest + switch -- $cmd { + me {set msg "\001ACTION $msg\001";set type ACTION} + nick {send $context "NICK $msg"; set $irc(nick) $msg} + quit {send $context "QUIT" } + part {send $context "PART $channel" } + names {send $context "NAMES $channel"} + whois {send $context "WHOIS $channel $msg"} + kick {send $context "KICK $channel $first :$rest"} + mode {send $context "MODE $msg"} + topic {send $context "TOPIC $channel :$msg" } + quote {send $context $msg} + join {send $context "JOIN $msg" } + version {send $context "PRIVMSG $first :\001VERSION\001"} + msg { + if {[regexp {([^ ]+) +(.*)} $msg -> target querymsg]} { + send $context "PRIVMSG $target :$msg" + Callback $context chat $target $target $querymsg "" + } + } + default {Callback $context system $channel "unknown command /$cmd"} + } + if {$cmd ne {me} || $cmd eq {msg}} return + } + foreach line [split $msg \n] {send $context "PRIVMSG $channel :$line"} + Callback $context chat $channel $irc(nick) $msg $type +} + +proc ::picoirc::send {context line} { + upvar #0 $context irc + # the callback can return -code break to prevent writing to socket + if {[catch {Callback $context debug write $line}] != 3} { + puts $irc(socket) $line + } +} + +# ------------------------------------------------------------------------- + +package provide picoirc $::picoirc::version + +# ------------------------------------------------------------------------- diff --git a/lib/irc/pkgIndex.tcl b/lib/irc/pkgIndex.tcl new file mode 100644 index 0000000..117eb32 --- /dev/null +++ b/lib/irc/pkgIndex.tcl @@ -0,0 +1,8 @@ +# pkgIndex.tcl -*- tcl -*- +# $Id: pkgIndex.tcl,v 1.8 2007/10/19 21:17:13 patthoyts Exp $ +if { ![package vsatisfies [package provide Tcl] 8.3] } { + # PRAGMA: returnok + return +} +package ifneeded irc 0.6 [list source [file join $dir irc.tcl]] +package ifneeded picoirc 0.5 [list source [file join $dir picoirc.tcl]] diff --git a/lib/jabberlib/XMLFormat.tcl b/lib/jabberlib/XMLFormat.tcl new file mode 100644 index 0000000..feea267 --- /dev/null +++ b/lib/jabberlib/XMLFormat.tcl @@ -0,0 +1,41 @@ +package require xml + +set _wspc {[ \t\n\r]*} +proc CData {data args} { + global indent _wspc + + if {![regexp "^${_wspc}$" $data]} { + puts "$indent $data" + } +} +proc EStart {name attlist args} { + global indent + + set attrs {} + foreach {key value} $attlist { + lappend attrs "$key='$value'" + } + puts "${indent}<$name $attrs>" + append indent { } +} +proc EEnd {name args} { + global indent + + set indent [string range $indent 0 end-4] + puts "${indent}" +} + +set indent {} +set parser [::xml::parser -characterdatacommand CData -elementstartcommand EStart \ + -elementendcommand EEnd] + +proc Format {} { + global parser + + set fileName [tk_getOpenFile] + set fd [open $fileName] + $parser parse [read $fd] + close $fd +} +Format + diff --git a/lib/jabberlib/avatar.tcl b/lib/jabberlib/avatar.tcl new file mode 100644 index 0000000..cfc88f7 --- /dev/null +++ b/lib/jabberlib/avatar.tcl @@ -0,0 +1,807 @@ +# avatar.tcl -- +# +# This file is part of the jabberlib. +# It provides support for avatars (XEP-0008: IQ-Based Avatars) +# and vCard based avatars as XEP-0153. +# Note that this XEP is "historical" only but is easy to adapt to +# a future pub-sub method. +# +# Copyright (c) 2005-2006 Mats Bengtsson +# Copyright (c) 2006 Antonio Cano Damas +# +# This file is distributed under BSD style license. +# +# $Id: avatar.tcl,v 1.27 2007/11/10 15:44:59 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# avatar - convenience command library for avatars. +# +# SYNOPSIS +# jlib::avatar::init jlibname +# +# OPTIONS +# -announce 0|1 +# -share 0|1 +# -command tclProc invoked when hash changed +# -cache 0|1 +# +# INSTANCE COMMANDS +# jlibName avatar configure ?-key value...? +# jlibName avatar set_data data mime +# jlibName avatar unset_data +# jlibName avatar store command +# jlibName avatar store_remove command +# jlibName avatar get_async jid command +# jlibName avatar send_get jid command +# jlibName avatar send_get_storage jid command +# jlibName avatar get_data jid2 +# jlibName avatar get_hash jid2 +# jlibName avatar get_mime jid2 +# jlibName avatar have_data jid2 +# jlibName avatar have_hash jid2 +# +# Note that all internal storage refers to bare (2-tier) JIDs! +# @@@ It is unclear if this is correct. Perhaps the full JIDs shall be used. +# The problem is with XEP-0008 mixing JID2 with JID3. +# Note that all vCards are defined per JID2, bare JID. +# +# @@@ And what happens for groupchat members? +# +# No automatic presence or server storage is made when reconfiguring or +# changing own avatar. This is up to the client layer to do. +# It is callback based which means that the -command is only invoked when +# getting hashes and not else. +# +################################################################################ +# TODO: +# 1) Update to XEP-0084: User Avatar 1.0, 2007-11-07, using PEP + +package require base64 ; # tcllib +package require sha1 ; # tcllib +package require jlib +package require jlib::disco +package require jlib::vcard + +package provide jlib::avatar 0.1 + +namespace eval jlib::avatar { + variable inited 0 + variable xmlns + set xmlns(x-avatar) "jabber:x:avatar" + set xmlns(iq-avatar) "jabber:iq:avatar" + set xmlns(storage) "storage:client:avatar" + set xmlns(vcard-temp) "vcard-temp:x:update" + + jlib::ensamble_register avatar \ + [namespace current]::init \ + [namespace current]::cmdproc + + jlib::disco::registerfeature $xmlns(iq-avatar) + + # Note: jlib::ensamble_register is last in this file! +} + +proc jlib::avatar::init {jlibname args} { + + variable xmlns + + # Instance specific arrays: + # avatar stores our own avatar + # state stores other avatars + namespace eval ${jlibname}::avatar { + variable avatar + variable state + variable options + } + upvar ${jlibname}::avatar::avatar avatar + upvar ${jlibname}::avatar::state state + upvar ${jlibname}::avatar::options options + + array set options { + -announce 0 + -share 0 + -cache 1 + -command "" + } + eval {configure $jlibname} $args + + # Register some standard iq handlers that are handled internally. + $jlibname iq_register get $xmlns(iq-avatar) [namespace current]::iq_handler + $jlibname presence_register_int available \ + [namespace current]::presence_handler + + $jlibname register_reset [namespace current]::reset + + return +} + +proc jlib::avatar::reset {jlibname} { + upvar ${jlibname}::avatar::state state + upvar ${jlibname}::avatar::options options + + # Do not unset our own avatar. + if {!$options(-cache)} { + unset -nocomplain state + } +} + +# jlib::avatar::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: +# args: all args to the cmd procedure. +# +# Results: +# none. + +proc jlib::avatar::cmdproc {jlibname cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +proc jlib::avatar::configure {jlibname args} { + + upvar ${jlibname}::avatar::options options + + set opts [lsort [array names options -*]] + set usage [join $opts ", "] + if {[llength $args] == 0} { + set result {} + foreach name $opts { + lappend result $name $options($name) + } + return $result + } + regsub -all -- - $opts {} opts + set pat ^-([join $opts |])$ + if {[llength $args] == 1} { + set flag [lindex $args 0] + if {[regexp -- $pat $flag]} { + return $options($flag) + } else { + return -code error "Unknown option $flag, must be: $usage" + } + } else { + array set oldopts [array get options] + foreach {flag value} $args { + if {[regexp -- $pat $flag]} { + set options($flag) $value + } else { + return -code error "Unknown option $flag, must be: $usage" + } + } + if {$options(-announce) != $oldopts(-announce)} { + if {$options(-announce)} { + # @@@ ??? + } else { + $jlibname deregister_presence_stanza x $xmlns(x-avatar) + $jlibname deregister_presence_stanza x $xmlns(vcard-temp) + } + } + } +} + +#+++ Two sections: First part deals with our own avatar ------------------------ + +# jlib::avatar::set_data -- +# +# Sets our own avatar data and shares it by default. +# Registers new hashes but does not send updated presence. +# You have to send presence yourself. +# +# Arguments: +# jlibname: the instance of this jlib. +# data: raw binary image data. +# mime: the mime type: image/gif or image/png +# +# Results: +# none. + +proc jlib::avatar::set_data {jlibname data mime} { + variable xmlns + upvar ${jlibname}::avatar::avatar avatar + upvar ${jlibname}::avatar::options options + + set options(-announce) 1 + set options(-share) 1 + + if {[info exists avatar(hash)]} { + set oldHash $avatar(hash) + } else { + set oldHash "" + } + set avatar(data) $data + set avatar(mime) $mime + set avatar(hash) [::sha1::sha1 $data] + set avatar(base64) [::base64::encode $data] + + set hashElem [wrapper::createtag hash -chdata $avatar(hash)] + set xElem [wrapper::createtag x \ + -attrlist [list xmlns $xmlns(x-avatar)] \ + -subtags [list $hashElem]] + + $jlibname deregister_presence_stanza x $xmlns(x-avatar) + $jlibname register_presence_stanza $xElem -type available + + #-- vCard-temp presence stanza -- + set photoElem [wrapper::createtag photo -chdata $avatar(hash)] + set xVCardElem [wrapper::createtag x \ + -attrlist [list xmlns $xmlns(vcard-temp)] \ + -subtags [list $photoElem]] + + $jlibname deregister_presence_stanza x $xmlns(vcard-temp) + $jlibname register_presence_stanza $xVCardElem -type available + + return +} + +proc jlib::avatar::get_my_data {jlibname what} { + upvar ${jlibname}::avatar::avatar avatar + + return $avatar($what) +} + +# jlib::avatar::unset_data -- +# +# Unsets our avatar and does not share it anymore. +# You have to send presence yourself with empty hashes. + +proc jlib::avatar::unset_data {jlibname} { + variable xmlns + upvar ${jlibname}::avatar::avatar avatar + upvar ${jlibname}::avatar::options options + + unset -nocomplain avatar + set options(-announce) 0 + set options(-share) 0 + + $jlibname deregister_presence_stanza x $xmlns(x-avatar) + $jlibname deregister_presence_stanza x $xmlns(vcard-temp) + + return +} + +# jlib::avatar::store -- +# +# Stores our avatar at the server. +# Must store as bare jid. + +proc jlib::avatar::store {jlibname cmd} { + variable xmlns + upvar ${jlibname}::avatar::avatar avatar + + if {![array exists avatar]} { + return -code error "no avatar set" + } + set dataElem [wrapper::createtag data \ + -attrlist [list mimetype $avatar(mime)] \ + -chdata $avatar(base64)] + + set jid2 [$jlibname getthis myjid2] + $jlibname iq_set $xmlns(storage) \ + -to $jid2 -command $cmd -sublists [list $dataElem] +} + +proc jlib::avatar::store_remove {jlibname cmd} { + variable xmlns + + set jid2 [$jlibname getthis myjid2] + $jlibname iq_set $xmlns(storage) -to $jid2 -command $cmd +} + +# jlib::avatar::iq_handler -- +# +# Handles incoming iq requests for our avatar. + +proc jlib::avatar::iq_handler {jlibname from queryElem args} { + variable xmlns + upvar ${jlibname}::avatar::options options + upvar ${jlibname}::avatar::avatar avatar + + array set argsArr $args + if {[info exists argsArr(-xmldata)]} { + set xmldata $argsArr(-xmldata) + set from [wrapper::getattribute $xmldata from] + set id [wrapper::getattribute $xmldata id] + } else { + return 0 + } + + if {$options(-share)} { + set dataElem [wrapper::createtag data \ + -attrlist [list mimetype $avatar(mime)] \ + -chdata $avatar(base64)] + set qElem [wrapper::createtag query \ + -attrlist [list xmlns $xmlns(iq-avatar)] \ + -subtags [list $dataElem]] + $jlibname send_iq result [list $qElem] -to $from -id $id + return 1 + } else { + $jlibname send_iq_error $from $id 404 cancel service-unavailable + return 1 + } +} + +#+++ Second part deals with getting other avatars ------------------------------ + +proc jlib::avatar::get_data {jlibname jid2} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + if {[info exists state($mjid2,data)]} { + return $state($mjid2,data) + } else { + return "" + } +} + +proc jlib::avatar::get_mime {jlibname jid2} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + if {[info exists state($mjid2,mime)]} { + return $state($mjid2,mime) + } else { + return "" + } +} + +proc jlib::avatar::have_data {jlibname jid2} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + return [info exists state($mjid2,data)] +} + +proc jlib::avatar::get_hash {jlibname jid2} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + if {[info exists state($mjid2,hash)]} { + return $state($mjid2,hash) + } else { + return "" + } +} + +proc jlib::avatar::have_hash {jlibname jid2} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + return [info exists state($mjid2,hash)] +} + +proc jlib::avatar::have_hash_protocol {jlibname jid2 protocol} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + return [info exists state($mjid2,protocol,$protocol)] +} + +proc jlib::avatar::get_protocols {jlibname jid2} { + upvar ${jlibname}::avatar::state state + + set protocols {} + set mjid2 [jlib::jidmap $jid2] + foreach p {avatar vcard} { + if {[info exists state($mjid2,protocol,$p)]} { + lappend protocols $p + } + } + return $protocols +} + +# jlib::avatar::get_full_jid -- +# +# This is the jid3 associated with 'avatar' or jid2 if 'vcard', +# else we just return the jid2. + +proc jlib::avatar::get_full_jid {jlibname jid2} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + if {[info exists state($mjid2,jid3)]} { + return $state($mjid2,jid3) + } else { + return $jid2 + } +} + +# jlib::avatar::get_all_avatar_jids -- +# +# Gets a list of all jids with avatar support. +# Actually, everyone that has sent us a presence jabber:x:avatar element. + +proc jlib::avatar::get_all_avatar_jids {jlibname} { + upvar ${jlibname}::avatar::state state + + debug "jlib::avatar::get_all_avatar_jids" + + set jids {} + set len [string length ",hash"] + foreach {key hash} [array get state *,hash] { + if {$hash ne ""} { + set jid2 [string range $key 0 end-$len] + lappend jids $jid2 + } + } + return $jids +} + +proc jlib::avatar::uptodate {jlibname jid2} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + if {[info exists state($mjid2,uptodate)]} { + return $state($mjid2,uptodate) + } else { + return 0 + } +} + +# jlib::avatar::presence_handler -- +# +# We must handle both 'avatar' and 'vcard' from one place +# since we don't want separate callbacks if both are supplied. +# It is assumed that hash from any are identical. +# Invokes any -command if hash changed. + +proc jlib::avatar::presence_handler {jlibname xmldata} { + upvar ${jlibname}::avatar::options options + upvar ${jlibname}::avatar::state state + + set from [wrapper::getattribute $xmldata from] + set mjid [jlib::jidmap $from] + set mjid2 [jlib::barejid $mjid] + + if {[info exists state($mjid2,hash)]} { + set new 0 + set oldhash $state($mjid2,hash) + } else { + set new 1 + } + set gotAvaHash [PresenceAvatar $jlibname $xmldata] + set gotVcardHash [PresenceVCard $jlibname $xmldata] + + if {($gotAvaHash || $gotVcardHash)} { + + # 'uptodate' tells us if we need to request new avatar. + # If new, or not identical to previous, unless empty. + if {$new || ($state($mjid2,hash) ne $oldhash)} { + set hash $state($mjid2,hash) + + # hash can be empty. + if {$hash eq ""} { + set state($mjid2,uptodate) 1 + unset -nocomplain state($mjid2,data) + } else { + set state($mjid2,uptodate) 0 + } + if {[string length $options(-command)]} { + uplevel #0 $options(-command) [list $from] + } + } + } else { + + # Must be sure that nothing there. + if {[info exists state($mjid2,hash)]} { + array unset state [jlib::ESC $mjid2],* + } + } +} + +# jlib::avatar::PresenceAvatar -- +# +# Caches incoming presence elements. +# "To disable the avatar, the avatar-generating user's client will send +# a presence packet with the jabber:x:avatar namespace but with no hash +# information" + +proc jlib::avatar::PresenceAvatar {jlibname xmldata} { + variable xmlns + upvar ${jlibname}::avatar::state state + + set gotHash 0 + set elems [wrapper::getchildswithtagandxmlns $xmldata x $xmlns(x-avatar)] + if {[llength $elems]} { + set hashElem [wrapper::getfirstchildwithtag [lindex $elems 0] hash] + set hash [wrapper::getcdata $hashElem] + set from [wrapper::getattribute $xmldata from] + set mjid2 [jlib::jidmap [jlib::barejid $from]] + + # hash can be empty. + set state($mjid2,hash) $hash + set state($mjid2,jid3) $from + set state($mjid2,protocol,avatar) 1 + set gotHash 1 + } + return $gotHash +} + +proc jlib::avatar::PresenceVCard {jlibname xmldata} { + variable xmlns + upvar ${jlibname}::avatar::state state + + set gotHash 0 + set elems [wrapper::getchildswithtagandxmlns $xmldata x $xmlns(vcard-temp)] + if {[llength $elems]} { + set hashElem [wrapper::getfirstchildwithtag [lindex $elems 0] photo] + set hash [wrapper::getcdata $hashElem] + set from [wrapper::getattribute $xmldata from] + set mjid2 [jlib::jidmap [jlib::barejid $from]] + + # Note that all vCards are defined per jid2, bare JID. + set state($mjid2,hash) $hash + set state($mjid2,jid3) $from + set state($mjid2,protocol,vcard) 1 + set gotHash 1 + } + return $gotHash +} + +# jlib::avatar::get_async -- +# +# The economical way of obtaining a users avatar. +# If uptodate no query made, else it sends at most one query per user +# to get the avatar. + +proc jlib::avatar::get_async {jlibname jid cmd} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap [jlib::barejid $jid]] + if {[uptodate $jlibname $mjid2]} { + uplevel #0 $cmd [list result $mjid2] + } elseif {[info exists state($mjid2,pending)]} { + lappend state($mjid2,invoke) $cmd + } else { + send_get $jlibname $jid \ + [list [namespace current]::get_async_cb $jlibname $mjid2 $cmd] + } +} + +proc jlib::avatar::get_async_cb {jlibname jid2 cmd type subiq args} { + upvar ${jlibname}::avatar::state state + + uplevel #0 $cmd [list $type $jid2] +} + +# jlib::avatar::send_get -- +# +# Initiates a request for avatar to the full jid. +# If fails we try to get avatar from server storage of the bare jid. + +proc jlib::avatar::send_get {jlibname jid cmd} { + variable xmlns + upvar ${jlibname}::avatar::state state + + debug "jlib::avatar::send_get jid=$jid" + + set mjid2 [jlib::jidmap [jlib::barejid $jid]] + set state($mjid2,pending) 1 + $jlibname iq_get $xmlns(iq-avatar) -to $jid \ + -command [list [namespace current]::send_get_cb $jid $cmd] +} + +proc jlib::avatar::send_get_cb {jid cmd jlibname type subiq args} { + variable xmlns + upvar ${jlibname}::avatar::state state + + debug "jlib::avatar::send_get_cb jid=$jid" + + set jid2 [jlib::barejid $jid] + set mjid2 [jlib::jidmap $jid2] + unset -nocomplain state($mjid2,pending) + + if {$type eq "error"} { + + # XEP-0008: "If the first method fails, the second method that should + # be attempted by sending a request to the server..." + send_get_storage $jlibname $mjid2 $cmd + } elseif {$type eq "result"} { + set ok [SetDataFromQueryElem $jlibname $mjid2 $subiq $xmlns(iq-avatar)] + InvokeStacked $jlibname $type $jid2 + uplevel #0 $cmd [list $type $subiq] $args + } +} + +# jlib::avatar::SetDataFromQueryElem -- +# +# Extracts and sets internal avtar storage for the BARE jid +# from a query element. +# +# Results: +# 1 if there was data to store, 0 else. + +proc jlib::avatar::SetDataFromQueryElem {jlibname mjid2 queryElem ns} { + upvar ${jlibname}::avatar::state state + + # Data may be empty from xmlns='storage:client:avatar' ! + + set ans 0 + if {[wrapper::getattribute $queryElem xmlns] eq $ns} { + set dataElem [wrapper::getfirstchildwithtag $queryElem data] + if {$dataElem ne {}} { + + # Mime type can be empty. + set state($mjid2,mime) [wrapper::getattribute $dataElem mimetype] + + # We keep data in base64 format. This seems to be ok for image + # handlers. + set data [wrapper::getcdata $dataElem] + if {[string length $data]} { + set state($mjid2,data) $data + set state($mjid2,uptodate) 1 + set ans 1 + } + } + } + return $ans +} + +proc jlib::avatar::send_get_storage {jlibname jid2 cmd} { + variable xmlns + upvar ${jlibname}::avatar::state state + + debug "jlib::avatar::send_get_storage jid2=$jid2" + + set mjid2 [jlib::jidmap $jid2] + set state($mjid2,pending) 1 + $jlibname iq_get $xmlns(storage) -to $jid2 \ + -command [list [namespace current]::send_get_storage_cb $jid2 $cmd] +} + +proc jlib::avatar::send_get_storage_cb {jid2 cmd jlibname type subiq args} { + variable xmlns + upvar ${jlibname}::avatar::state state + + debug "jlib::avatar::send_get_storage_cb type=$type" + + set mjid2 [jlib::jidmap $jid2] + unset -nocomplain state($mjid2,pending) + if {$type eq "result"} { + set ok [SetDataFromQueryElem $jlibname $mjid2 $subiq $xmlns(storage)] + } + InvokeStacked $jlibname $type $jid2 + uplevel #0 $cmd [list $type $subiq] $args +} + +proc jlib::avatar::InvokeStacked {jlibname type jid2} { + upvar ${jlibname}::avatar::state state + + set mjid2 [jlib::jidmap $jid2] + if {[info exists state($jid2,invoke)]} { + foreach cmd $state($jid2,invoke) { + uplevel #0 $cmd [list $type $jid2] + } + unset -nocomplain state($jid2,invoke) + } +} + +#--- vCard support ------------------------------------------------------------- + +proc jlib::avatar::get_vcard_async {jlibname jid2 cmd} { + upvar ${jlibname}::avatar::state state + + debug "jlib::avatar::get_vcard_async jid=$jid2" + + set mjid2 [jlib::jidmap $jid2] + if {[uptodate $jlibname $mjid2]} { + uplevel #0 $cmd [list result $jid2] + } else { + + # Need to clear vcard cache to trigger sending a request. + # The photo is anyway not up-to-date. + $jlibname vcard clear $jid2 + $jlibname vcard get_async $jid2 \ + [list [namespace current]::get_vcard_async_cb $jid2 $cmd] + } +} + +proc jlib::avatar::get_vcard_async_cb {jid2 cmd jlibname type subiq args} { + + debug "jlib::avatar::get_vcard_async_cb jid=$jid2" + + if {$type eq "result"} { + set mjid2 [jlib::jidmap $jid2] + SetDataFromVCardElem $jlibname $mjid2 $subiq + } + uplevel #0 $cmd [list $type $jid2] +} + +# jlib::avatar::send_get_vcard -- +# +# Support for vCard based avatars as XEP-0153. +# We must get vcard avatars from here since the result shall be cached. +# Note that all vCards are defined per jid2, bare JID. +# This method is more sane compared to iq-based avatars since it is +# based on bare jids and thus not client instance specific. +# Therefore it also handles offline users. + +proc jlib::avatar::send_get_vcard {jlibname jid2 cmd} { + + debug "jlib::avatar::send_get_vcard jid2=$jid2" + + $jlibname vcard send_get $jid2 \ + -command [list [namespace current]::send_get_vcard_cb $jid2 $cmd] +} + +proc jlib::avatar::send_get_vcard_cb {jid2 cmd jlibname type subiq args} { + + debug "jlib::avatar::send_get_vcard_cb" + + if { $type eq "result" } { + set mjid2 [jlib::jidmap $jid2] + SetDataFromVCardElem $jlibname $mjid2 $subiq + uplevel #0 $cmd [list $type $subiq] $args + } +} + +# jlib::avatar::SetDataFromVCardElem -- +# +# Extracts and sets internal avtar storage for the BARE jid +# from a vcard element. +# +# Results: +# 1 if there was data to store, 0 else. + +proc jlib::avatar::SetDataFromVCardElem {jlibname mjid2 subiq} { + upvar ${jlibname}::avatar::state state + + set ans 0 + set photoElem [wrapper::getfirstchildwithtag $subiq PHOTO] + if {$photoElem ne {}} { + set dataElem [wrapper::getfirstchildwithtag $photoElem BINVAL] + set mimeElem [wrapper::getfirstchildwithtag $photoElem TYPE] + if {$dataElem ne {}} { + + # We keep data in base64 format. This seems to be ok for image + # handlers. + set state($mjid2,data) [wrapper::getcdata $dataElem] + set state($mjid2,mime) [wrapper::getcdata $mimeElem] + set state($mjid2,uptodate) 1 + set ans 1 + } + } + return $ans +} + +proc jlib::avatar::debug {msg} { + if {0} { + puts "\t $msg" + } +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::avatar { + + jlib::ensamble_register avatar \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +if {0} { + # Test. + set f "/Users/matben/Desktop/glaze/32x32/apps/clanbomber.png" + set fd [open $f] + fconfigure $fd -translation binary + set data [read $fd] + close $fd + + set data "0123456789" + + set jlib jlib::jlib1 + proc cb {args} {puts "--- cb"} + $jlib avatar set_data $data image/png + $jlib avatar store cb + $jlib avatar send_get [$jlib getthis myjid] cb + $jlib avatar send_get_storage [$jlib getthis myjid2] cb +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/bind.tcl b/lib/jabberlib/bind.tcl new file mode 100644 index 0000000..ba9c51c --- /dev/null +++ b/lib/jabberlib/bind.tcl @@ -0,0 +1,72 @@ +# bind.tcl -- +# +# This file is part of the jabberlib. +# It implements the bind resource mechanism and establish a session. +# +# Copyright (c) 2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: bind.tcl,v 1.1 2007/07/23 15:11:43 matben Exp $ + +package require jlib + +package provide jlib::bind 0.1 + +namespace eval jlib::bind {} + +proc jlib::bind::resource {jlibname resource cmd} { + upvar ${jlibname}::state state + + set state(resource) $resource + set state(cmd) $cmd + + if {[$jlibname have_feature bind]} { + $jlibname bind_resource $state(resource) [namespace code resource_bind_cb] + } else { + $jlibname trace_stream_features [namespace code features] + } +} + +proc jlib::bind::features {jlibname} { + upvar ${jlibname}::state state + + if {[$jlibname have_feature bind]} { + $jlibname bind_resource $state(resource) [namespace code resource_bind_cb] + } else { + establish_session $jlibname + } +} + +proc jlib::bind::resource_bind_cb {jlibname type subiq} { + + if {$type eq "error"} { + final $jlibname error $subiq + } else { + establish_session $jlibname + } +} + +proc jlib::bind::establish_session {jlibname} { + upvar jlib::xmppxmlns xmppxmlns + + # Establish the session. + set xmllist [wrapper::createtag session \ + -attrlist [list xmlns $xmppxmlns(session)]] + $jlibname send_iq set [list $xmllist] \ + -command [namespace code [list send_session_cb $jlibname]] +} + +proc jlib::bind::send_session_cb {jlibname type subiq args} { + final $jlibname $type $subiq +} + +proc jlib::bind::final {jlibname type subiq} { + upvar ${jlibname}::state state + + uplevel #0 $state(cmd) [list $jlibname $type $subiq] + unset -nocomplain state +} + + + diff --git a/lib/jabberlib/bytestreams.tcl b/lib/jabberlib/bytestreams.tcl new file mode 100644 index 0000000..13e6eef --- /dev/null +++ b/lib/jabberlib/bytestreams.tcl @@ -0,0 +1,1962 @@ +# bytestreams.tcl -- +# +# This file is part of the jabberlib. +# It provides support for the bytestreams protocol (XEP-0065). +# +# Copyright (c) 2005-2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: bytestreams.tcl,v 1.33 2007/11/30 14:38:34 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# bytestreams - implements the socks5 bytestream stream protocol. +# +# SYNOPSIS +# +# +# OPTIONS +# +# +# INSTANCE COMMANDS +# +# jlibName bytestream configure ?-address -port -timeout ms -proxyhost? +# jlibName bytestream send_initiate to sid cmd ?-streamhosts -fastmode? +# +# +############################# CHANGES ########################################## +# +# 0.1 first version +# 0.2 timeouts + fast mode +# 0.3 connector object +# 0.4 proxy support +# +# FAST MODE: +# Some details: +# "This is done by sending an additional [CR] character across the +# bytestream to indicate its selection." +# The character is sent after socks5 authorization, and even after +# iq-streamhost-used, since the initiator must pick only a working stream. +# If the desired stream is offered by the initiator, it would send the +# character only after receiving iq-streamhost-used from the target. +# If the desired stream is offered by the target, then the initiator +# would send the character after it sends iq-streamhost-used to the target. +# +# When do we know to use the fast mode protocol? +# (initiator): received streamhost with sid we have initiated +# (target): receiving streamhost+fast and sent streamhost +# +# initiator target +# --------- ------ +# +# streamhosts + fast +# ---------------------------> +# +# streamhosts (fastmode) +# <--------------------------- +# +# connector (s5) +# <--------------------------- +# +# connector (s5) (fastmode) +# ---------------------------> +# +# streamhost-used +# sock <--------------------------- +# +# streamhost-used (fastmode) +# sock_fast ---------------------------> +# +# Initiator picks one of 0-2 sockets and fastmode sends a CR. +# +# HASH: +# SHA1(SID + Initiator JID + Target JID) +# The JIDs provided MUST be the JIDs used for the IQ exchange; +# furthermore, in order to ensure proper results, the appropriate +# stringprep profiles. +# +# INITIATOR FLOW: +# There are two different flows: +# (1) iq query/response +# (2) socks5 connections and negotiations, denoted s5 here +# They interact and depend on each other. (f) means fast mode only. +# As seen from the initiator: +# +# (a) iq-stream initiate (send) +# (f) (b) iq-stream target provides streamhosts to initiator (recv) +# (1) s5 socket to initiators server +# (f) (2) s5 fast socket to targets streamhost +# (3) s5 socket initiator to proxy +# +# iq-stream (a) controls (1) and (3) +# iq-stream (b) controls (2) +# +# There are three possible s5 streams: +# +# (A) s5 (server) initiator <--- s5 (client) target +# (f) (B) s5 (client) initiator ---> s5 (server) target +# (C) s5 (client) initiator ---> s5 (server) proxy +# +# The first succesful stream wins and kills the other. +# +# TARGET: +# The target handles the (intiators) proxy like any other streamhost +# and proxies are therefore transparent to the target. +# +# +# NOTES: +# o If yoy are trying to follow this code, focus on one side alone, +# initiator or target, else you are likely to get insane. + +package require sha1 +package require jlib +package require jlib::disco +package require jlib::si + +package provide jlib::bytestreams 0.4 + +#--- generic bytestreams ------------------------------------------------------- + +namespace eval jlib::bytestreams { + + variable xmlns + set xmlns(bs) "http://jabber.org/protocol/bytestreams" + set xmlns(fast) "http://affinix.com/jabber/stream" + + jlib::si::registertransport $xmlns(bs) $xmlns(bs) 40 \ + [namespace current]::si_open \ + [namespace current]::si_close + + jlib::disco::registerfeature $xmlns(bs) + + # Support for http://affinix.com/jabber/stream. + variable fastmode 1 + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::bytestreams::init -- +# +# Instance init procedure. + +proc jlib::bytestreams::init {jlibname args} { + variable xmlns + + # Keep different state arrays for initiator (i) and receiver (t). + namespace eval ${jlibname}::bytestreams { + variable istate + variable tstate + + # Mapper from SOCKS5 hash to sid. + variable hash2sid + + # Independent of sid variables. + variable static + + # Server port 0 says that arbitrary port can be chosen. + set static(-address) "" + set static(-block-size) 4096 + set static(-port) 0 + set static(-s5timeoutms) 8000 ;# TODO + set static(-timeoutms) 30000 + set static(-proxyhost) [list] + set static(-targetproxy) 0 ;# Not implemented + } + + # Register standard iq handler that is handled internally. + $jlibname iq_register set $xmlns(bs) [namespace current]::handle_set + eval {configure $jlibname} $args + + return +} + +proc jlib::bytestreams::cmdproc {jlibname cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +proc jlib::bytestreams::configure {jlibname args} { + + upvar ${jlibname}::bytestreams::static static + + if {![llength $args]} { + return [array get static -*] + } else { + foreach {key value} $args { + + switch -- $key { + -address { + set static($key) $value + } + -port - -timeoutms { + if {![string is integer -strict $value]} { + return -code error "$key must be integer number" + } + set static($key) $value + } + -proxyhost { + if {[llength $value]} { + if {[llength $value] != 3} { + return -code error "$key must be a list {jid ip port}" + } + if {![string is integer -strict [lindex $value 2]]} { + return -code error "port must be an integer number" + } + } + set static($key) $value + } + -targetproxy { + if {![string is boolean -strict $value]} { + return -code error "$key must be integer number" + } + set static($key) $value + } + default { + return -code error "unknown option \"$key\"" + } + } + } + } + return +} + +# Common code for both initiator and target. + +# jlib::bytestreams::i_or_t -- +# +# In some situations we must know if we are the initiator or target +# using just the sid. + +proc jlib::bytestreams::i_or_t {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + upvar ${jlibname}::bytestreams::tstate tstate + debug "jlib::bytestreams::i_or_t" + + if {[info exists istate($sid,state)]} { + return "i" + } elseif {[info exists tstate($sid,state)]} { + return "t" + } else { + return "" + } +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# These are all functions to use by a initiator (sender). + +# si_open, si_close -- +# +# Bindings for si. + +# jlib::bytestreams::si_open -- +# +# Constructor for an initiator object. + +proc jlib::bytestreams::si_open {jlibname jid sid args} { + + variable fastmode + upvar ${jlibname}::bytestreams::istate istate + upvar ${jlibname}::bytestreams::static static + upvar ${jlibname}::bytestreams::hash2sid hash2sid + debug "jlib::bytestreams::si_open (i)" + + set jid [jlib::jidmap $jid] + set istate($sid,jid) $jid + + if {![info exists static(sock)]} { + + # Protect against server failure. + if {[catch {s5i_server $jlibname}]} { + si_open_report $jlibname $sid error \ + {error "Failed starting our streamhost"} + return + } + } + + # Provide our streamhosts. + # First, the local one. + if {$static(-address) ne ""} { + set ip $static(-address) + } else { + set ip [jlib::getip $jlibname] + } + set myjid [jlib::myjid $jlibname] + set hash [::sha1::sha1 $sid$myjid$jid] + + set istate($sid,ip) $ip + set istate($sid,state) open + set istate($sid,fast) 0 + set istate($sid,hash) $hash + set istate($sid,used-proxy) 0 ;# Set if target picks our proxy host + set istate($sid,proxy,state) "" + + set hash2sid($hash) $sid + set host [list $myjid -host $ip -port $static(port)] + set streamhosts [list $host] + set opts [list] + lappend opts -fastmode $fastmode + + # Second, the proxy host if any. + if {[llength $static(-proxyhost)]} { + lassign $static(-proxyhost) pjid pip pport + set proxyhost [list $pjid -host $pip -port $pport] + lappend streamhosts $proxyhost + lappend opts -proxyjid $pjid + } + lappend opts -streamhosts $streamhosts + + # Schedule a timeout until we get a streamhost-used returned. + set istate($sid,timeoutid) [after $static(-timeoutms) \ + [list [namespace current]::si_timeout_cb $jlibname $sid]] + + # Initiate the stream to the target. + set si_open_cb [list [namespace current]::si_open_cb $jlibname $sid] + eval {send_initiate $jlibname $jid $sid $si_open_cb} $opts + + return +} + +proc jlib::bytestreams::si_timeout_cb {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::si_timeout_cb (i)" + + si_open_report $jlibname $sid "error" {timeout "Timeout"} + ifinish $jlibname $sid +} + +# jlib::bytestreams::si_open_cb -- +# +# This is the iq-response we get as an initiator when sent our streamhosts +# to the target. We expect that it either returns a 'streamhost-used' +# or an error. +# We shall not return any iq as a response to this response. +# +# The target either returns an error if it failed to connect any +# streamhost, else it replies eith a 'streamhost-used' element. +# +# This is the main event handler for the initiator where it manages +# both open iq-streams as well as all sockets. +# +# See also 'i_connect_cb' for the fastmode side. + +proc jlib::bytestreams::si_open_cb {jlibname sid type subiq args} { + + variable xmlns + upvar ${jlibname}::bytestreams::istate istate + upvar ${jlibname}::bytestreams::static static + debug "jlib::bytestreams::si_open_cb (i) type=$type" + + # In fast mode we may get this callback after we have finished. + # Or after a timeout or something. + if {![info exists istate($sid,state)]} { + return + } + + # 'result' is normally the iq type but we add more error checking. + # Try to catch possible error situations. + set result $type + set istate($sid,type) $type + set istate($sid,subiq) $subiq + + # Collect streamhost used. If this fails we need to catch it below. + if {$type eq "result"} { + if {[wrapper::gettag $subiq] eq "query" \ + && [wrapper::getattribute $subiq xmlns] eq $xmlns(bs)} { + set usedE [wrapper::getfirstchildwithtag $subiq "streamhost-used"] + if {[llength $usedE]} { + set jidused [wrapper::getattribute $usedE "jid"] + set istate($sid,streamhost-used) $jidused + + # Need to know if target picked our proxy streamhost. + set jidproxy [lindex $static(-proxyhost) 0] + if {[jlib::jidequal $jidused $jidproxy]} { + set istate($sid,used-proxy) 1 + } + } + } + } + debug "\t used-proxy=$istate($sid,used-proxy)" + + # Must end the normal path if the target sent us weird response. + if {![info exists istate($sid,streamhost-used)]} { + set istate($sid,state) error + set istate($sid,subiq) {error "missing streamhost-used"} + } + if {$result eq "error"} { + set istate($sid,state) error + } + + # NB1: We may already have picked fast mode and istate($sid,state) = error + # Even if the normal path succeded! + # NB2: We can never pick fast mode from this proc! + + # Fastmode only: + if {$istate($sid,fast)} { + if {$istate($sid,state) eq "error"} { + ifast_error_normal $jlibname $sid + } else { + if {$istate($sid,used-proxy)} { + + # Now its time to start up and activate our proxy host. + iproxy_connect $jlibname $sid + } else { + ifast_select_normal $jlibname $sid + ifast_end_fast $jlibname $sid + } + } + } else { + + # Normal non-fastmode execution path. + if {$result eq "error"} { + if {[info exists istate($sid,sock)]} { + debug_sock "close $istate($sid,sock)" + catch {close $istate($sid,sock)} + unset istate($sid,sock) + } + si_open_report $jlibname $sid $type $istate($sid,subiq) + } else { + + if {$istate($sid,used-proxy)} { + + # Now its time to start up and activate our proxy host. + iproxy_connect $jlibname $sid + } else { + + # One last check that we actually got a socket connection. + # Try to catch possible error situations. + if {![info exists istate($sid,sock)]} { + set istate($sid,state) error + si_open_report $jlibname $sid error {error "Network Error"} + } else { + + # Everything is fine. + set istate($sid,state) streamhost-used + set istate($sid,active,sock) $istate($sid,sock) + si_open_report $jlibname $sid $type $subiq + } + } + } + } +} + +# jlib::bytestreams::ifast_* -- +# +# A number of methods to handle execution paths for the fast mode. +# They are normally called for iq-responses, but for the proxy they are +# called after activate response. +# Selects the first succesful stream and kills the others. +# If all streams have failed we report the error to si. +# +# NB1: ifast_* means that we are in fast mode; the suffix normally +# indicates which stream we are dealing with. +# NB2: we do not send any iq response here, which should only be done when +# calling 'ifast_select_fast'. + +proc jlib::bytestreams::ifast_error_normal {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::ifast_error_normal (i)" + + # The target failed the 'normal' s5 connection. + # Be sure to close normal and proxy sockets, (1) and (3) above. + set istate($sid,state) error + if {[info exists istate($sid,sock)]} { + debug_sock "close $istate($sid,sock)" + catch {close $istate($sid,sock)} + unset istate($sid,sock) + } + if {$istate($sid,used-proxy)} { + connector_reset $jlibname $sid p + if {[info exists istate($sid,proxy,sock)]} { + debug_sock "close $istate($sid,proxy,sock)" + catch {close $istate($sid,proxy,sock)} + unset istate($sid,proxy,sock) + } + } + + # If also the 'fast' way failed we are done. + if {$istate($sid,fast,state) eq "error"} { + si_open_report $jlibname $sid error $istate($sid,subiq) + } + + # At this stage we may already have activated the fast stream. +} + +proc jlib::bytestreams::ifast_select_normal {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::ifast_select_normal (i)" + + # Activate the 'normal' stream: + # Protect us from failed socks5 connections. + # This can be our own streamhost or the proxy host. Handle both here! + # Normally the target picks the one it wants. + + debug "\t used-proxy=$istate($sid,used-proxy), proxy,state=$istate($sid,proxy,state)" + set have_s5 0 + if {$istate($sid,used-proxy) && $istate($sid,proxy,state) eq "result"} { + set sock $istate($sid,proxy,sock) + set have_s5 1 + } elseif {[info exists istate($sid,sock)]} { + set sock $istate($sid,sock) + set have_s5 1 + } + if {$have_s5} { + set istate($sid,state) activated + debug "\t select normal, send CR" + if {[catch { + puts -nonewline $sock "\r" + flush $sock + }]} { + set have_s5 0 + } + } + if {$have_s5} { + set istate($sid,active,sock) $sock + si_open_report $jlibname $sid result $istate($sid,subiq) + } else { + debug "\t error missing s5 stream or failed send CR" + set istate($sid,state) error + si_open_report $jlibname $sid error {error "Network Error"} + } +} + +proc jlib::bytestreams::ifast_end_fast {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::ifast_end_fast (i)" + + # Put an end to any 'fast' stream. Both socket and iq-stream. + set istate($sid,fast,state) error + connector_reset $jlibname $sid f + if {[info exists istate($sid,fast,sock)]} { + debug_sock "close $istate($sid,fast,sock)" + catch {close $istate($sid,fast,sock)} + unset istate($sid,fast,sock) + } + + # This just informs the target that our 'fast' stream is shut down. + if {[info exists istate($sid,fast,id)]} { + isend_error $jlibname $sid 404 cancel item-not-found + } +} + +proc jlib::bytestreams::ifast_select_fast {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::ifast_select_fast (i)" + + + # Activate the fast stream. Set normal stream to error so we wont use it. + debug "\t select fast, send CR" + set sock $istate($sid,fast,sock) + set istate($sid,active,sock) $sock + set istate($sid,fast,state) activated + if {[catch { + puts -nonewline $sock "\r" + flush $sock + }]} { + debug "\t failed sending CR" + si_open_report $jlibname $sid error {error "Network Failure"} + } else { + + # Shut down the 'normal' stream: + # Must close down any connections to our own streamhost. + set istate($sid,state) error + if {[info exists istate($sid,sock)]} { + debug_sock "close $istate($sid,sock)" + catch {close $istate($sid,sock)} + unset istate($sid,sock) + } + si_open_report $jlibname $sid result {ok OK} + } +} + +#............................................................................... + +# jlib::bytestreams::si_open_report -- +# +# This prepares the callback to 'si' as a response to 'si_open. + +proc jlib::bytestreams::si_open_report {jlibname sid type subiq} { + + upvar ${jlibname}::bytestreams::istate istate + upvar ${jlibname}::bytestreams::static static + debug "jlib::bytestreams::si_open_report (i)" + + if {[info exists istate($sid,timeoutid)]} { + after cancel $istate($sid,timeoutid) + unset istate($sid,timeoutid) + } + jlib::si::transport_open_cb $jlibname $sid $type $subiq + + # If all went well this far we initiate the read/write data process. + if {$type eq "result"} { + + # Tell the profile to prepare to read data (open file). + jlib::si::open_data $jlibname $sid + + # Initiate the transport when socket is ready for writing. + set sock $istate($sid,active,sock) + setwritable $jlibname $sid $sock + } +} + +# jlib::bytestreams::si_read -- +# +# Read data from the profile via 'si' using its registered reader. + +proc jlib::bytestreams::si_read {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::si_read (i)" + + # NB: This should be safe to do since if we have been reset also + # the fileevent handler is removed when socket is closed. + set s $istate($sid,active,sock) + + fileevent $s writable {} + if {[catch {eof $s} iseof] || $iseof} { + jlib::si::close_data $jlibname $sid error + return + } + set data [jlib::si::read_data $jlibname $sid] + set len [string length $data] + + if {$len > 0} { + if {[catch {puts -nonewline $s $data}]} { + debug "\t failed" + jlib::si::close_data $jlibname $sid error + return + } + + # Trick to avoid UI blocking. + after idle [list after 0 [list \ + [namespace current]::setwritable $jlibname $sid $s]] + } else { + + # Empty data from the reader means that we are done. + jlib::si::close_data $jlibname $sid + } +} + +proc jlib::bytestreams::setwritable {jlibname sid sock} { + + # We could have been closed since this event comes async. + if {[lsearch [file channels] $sock] >= 0} { + fileevent $sock writable \ + [list [namespace current]::si_read $jlibname $sid] + } +} + +# jlib::bytestreams::si_close -- +# +# Destroys an initiator object. + +proc jlib::bytestreams::si_close {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::si_close (i)" + + # We don't have any particular to do here as 'ibb' has. + jlib::si::transport_close_cb $jlibname $sid result {} + ifinish $jlibname $sid +} + +proc jlib::bytestreams::is_initiator {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::is_initiator [info exists istate($sid,state)]" + + return [info exists istate($sid,state)] +} + +#--- Generic initiator code ---------------------------------------------------- + +# jlib::bytestreams::send_initiate -- +# +# -streamhosts {{jid (-host -port | -zeroconf)} {...} ...} +# -fastmode +# +# Stateless code that never access the istate array. + +proc jlib::bytestreams::send_initiate {jlibname to sid cmd args} { + variable xmlns + debug "jlib::bytestreams::initiate" + + set attrlist [list xmlns $xmlns(bs) sid $sid mode tcp] + set sublist [list] + set opts [list] + set proxyjid "" + foreach {key value} $args { + + switch -- $key { + -streamhosts { + set streamhosts $value + } + -fastmode { + if {$value} { + + # + lappend sublist [wrapper::createtag "fast" \ + -attrlist [list xmlns $xmlns(fast)]] + } + } + -proxyjid { + # Mark proxy: + set proxyjid $value + } + default { + return -code error "unknown option \"$key\"" + } + } + } + + # Need to do it here in order to handle any proxy element. + if {[info exists streamhosts]} { + foreach hostspec $streamhosts { + set jid [lindex $hostspec 0] + set hostattr [list jid $jid] + foreach {hkey hvalue} [lrange $hostspec 1 end] { + lappend hostattr [string trimleft $hkey -] $hvalue + } + set ssub [list] + if {[jlib::jidequal $proxyjid $jid]} { + set ssub [list [wrapper::createtag proxy \ + -attrlist [list xmlns $xmlns(fast)]]] + } + lappend sublist [wrapper::createtag "streamhost" \ + -attrlist $hostattr -subtags $ssub] + } + } + + set xmllist [wrapper::createtag "query" \ + -attrlist $attrlist -subtags $sublist] + eval {$jlibname send_iq "set" [list $xmllist] -to $to -command $cmd} $opts + return +} + +proc jlib::bytestreams::get_proxy {jlibname to cmd} { + variable xmlns + debug "jlib::bytestreams::get_proxy (i)" + + $jlibname iq_get $xmlns(bs) -to $to -command $cmd +} + +# jlib::bytestreams::activate -- +# +# Initiator requests activation of bytestream. +# This is only necessary for proxy streamhosts. + +proc jlib::bytestreams::activate {jlibname sid to targetjid args} { + variable xmlns + debug "jlib::bytestreams::activate (i)" + + set opts [list] + foreach {key value} $args { + switch -- $key { + -command { + set opts [list -command $value] + } + default { + return -code error "unknown option \"$key\"" + } + } + } + set activateE [wrapper::createtag "activate" -chdata $targetjid] + set xmllist [wrapper::createtag "query" \ + -attrlist [list xmlns $xmlns(bs) sid $sid] \ + -subtags [list $activateE]] + + eval {$jlibname send_iq "set" [list $xmllist] -to $to} $opts +} + +#--- Fastmode: handle targets streamhosts -------------------------------------- + +# jlib::bytestreams::i_handle_set -- +# +# This is the initiators handler when provided streamhosts by the +# target which only happens in fastmode. +# Fastmode only! + +proc jlib::bytestreams::i_handle_set {jlibname sid id jid hosts queryE} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::i_handle_set (i)" + + # We have already initiated this sid and must have fastmode. + # At this stage we run in the fast mode! + set istate($sid,fast) 1 + set istate($sid,fast,id) $id + set istate($sid,fast,jid) $jid + set istate($sid,fast,state) inited + set istate($sid,fast,hosts) $hosts + set istate($sid,fast,queryE) $queryE + + set myjid [$jlibname myjid] + set hash [::sha1::sha1 $sid$jid$myjid] + + # Try connecting the host(s) in turn. + set cb [list [namespace current]::i_connect_cb $jlibname $sid] + connector $jlibname $sid f $hash $hosts $cb +} + +# jlib::bytestreams::i_connect_cb -- +# +# The 'connector' callback when tried to connect to the targets streamhosts. +# We shall return an iq response to the targets iq streamhost offer. +# Fastmode only! + +proc jlib::bytestreams::i_connect_cb {jlibname sid result args} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::i_connect_cb $result (i)" + + array set argsA $args + + if {$result eq "error"} { + set istate($sid,fast,state) error + + # Deliver error to target. + isend_error $jlibname $sid 404 cancel item-not-found + + # In fastmode we are not done until the target also fails connecting. + if {!$istate($sid,fast) || ($istate($sid,state) eq "error")} { + + # Deliver error to target profile. + si_open_report $jlibname $sid error {error "Network Failure"} + } + } elseif {$istate($sid,fast,state) ne "error"} { + + # Must be sure that the normal stream hasn't already put a stop at fast. + # Shouldn't be needed since it should do connector_reset. + set sock $argsA(-socket) + set host $argsA(-streamhost) + set hostjid [lindex $host 0] + + # Deliver 'streamhost-used' to the target. + set id $istate($sid,fast,id) + set jid $istate($sid,fast,jid) + send_used $jlibname $jid $id $hostjid + + set istate($sid,fast,sock) $sock + set istate($sid,fast,host) $host + set istate($sid,fast,hostjid) $hostjid + + ifast_select_fast $jlibname $sid + } +} + +# Proxy handling --------------------------------------------------------------- + +# This is done as a response that the target has selected the proxy streamhost. +# There are two steps here: +# 1) initiator make a complete socks5 connection to the proxy +# 2) the stream is activated by the initiator + +proc jlib::bytestreams::iproxy_connect {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + upvar ${jlibname}::bytestreams::static static + debug "jlib::bytestreams::iproxy_connect (i)" + + set istate($sid,state) connecting + set myjid [$jlibname myjid] + set jid $istate($sid,jid) + set hash [::sha1::sha1 $sid$myjid$jid] + set hosts [list $static(-proxyhost)] + + set cb [list [namespace current]::iproxy_s5_cb $jlibname $sid] + connector $jlibname $sid p $hash $hosts $cb +} + +proc jlib::bytestreams::iproxy_s5_cb {jlibname sid result args} { + + upvar ${jlibname}::bytestreams::istate istate + upvar ${jlibname}::bytestreams::static static + debug "jlib::bytestreams::iproxy_s5_cb (i) $result $args" + + array set argsA $args + + if {$result eq "error"} { + if {$istate($sid,fast)} { + ifast_error_normal $jlibname $sid + } else { + + # If not fastmode we are finito. + set istate($sid,state) error + if {[info exists istate($sid,sock)]} { + debug_sock "close $istate($sid,sock)" + catch {close $istate($sid,sock)} + unset istate($sid,sock) + } + si_open_report $jlibname $sid error {error "Network Error"} + } + } else { + + # Allright so far, cache socket. + # Note that we need a specific variable for this since the target can + # connect our server: istate($sid,sock). + set istate($sid,proxy,sock) $argsA(-socket) + set proxyjid [lindex $static(-proxyhost) 0] + set jid $istate($sid,jid) + set cb [list [namespace current]::iproxy_activate_cb $jlibname $sid] + activate $jlibname $sid $proxyjid $jid -command $cb + } +} + +proc jlib::bytestreams::iproxy_activate_cb {jlibname sid type subiq args} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::iproxy_activate_cb (i) type=$type" + + set istate($sid,proxy,state) $type + set istate($sid,type) $type + set istate($sid,subiq) $subiq + + if {$istate($sid,fast)} { + + # When we get this response the fast mode may already have succeded. + if {$istate($sid,state) eq "error"} { + ifast_error_normal $jlibname $sid + } else { + ifast_select_normal $jlibname $sid + ifast_end_fast $jlibname $sid + } + } else { + if {$type eq "error"} { + + # If not fastmode we are finito. + set istate($sid,state) error + } else { + + # Everything is fine. + set istate($sid,state) streamhost-used + set istate($sid,active,sock) $istate($sid,proxy,sock) + } + si_open_report $jlibname $sid $type $subiq + } +} + +# Server side socks5 functions ------------------------------------------------- +# +# Normally used by the initiator except in fastmode where it is also used by +# the target. +# This is stateless code that never directly access the istate array. +# Think of it like an object: +# [in]: sock, addr, port +# [out]: sid, sock +# +# NB: We don't return any errors on the server side; this is up to the client. + +# jlib::bytestreams::s5i_server -- +# +# Start socks5 server. We use the server for the streams and keep it +# running for the lifetime of the application. + +proc jlib::bytestreams::s5i_server {jlibname} { + + upvar ${jlibname}::bytestreams::static static + debug "jlib::bytestreams::s5i_server (i)" + + # Note the difference between static(-port) and static(port) ! + set connectProc [list [namespace current]::s5i_accept $jlibname] + set sock [socket -server $connectProc $static(-port)] + set static(sock) $sock + set static(port) [lindex [fconfigure $sock -sockname] 2] + + # Test fast mode or proxy host... + #close $sock + return $static(port) +} + +# jlib::bytestreams::s5i_accept -- +# +# The server socket callback when connected. +# We keep a single server socket for all transfers and distinguish +# them when they do the SOCKS5 authentication using the mapping +# hash (sha1 sid+jid+myjid) -> sid + +proc jlib::bytestreams::s5i_accept {jlibname sock addr port} { + + debug "jlib::bytestreams::s5i_accept (i)" + debug_sock "open $sock" + + fconfigure $sock -translation binary -blocking 0 + fileevent $sock readable \ + [list [namespace current]::s5i_read_methods $jlibname $sock] +} + +proc jlib::bytestreams::s5i_read_methods {jlibname sock} { + + debug "jlib::bytestreams::s5i_read_methods (i)" + # For testing... + #after 50 + + fileevent $sock readable {} + if {[catch {read $sock} data] || [eof $sock]} { + debug_sock "close $sock" + catch {close $sock} + return + } + debug "\t read [string length $data]" + + # Pick method. Must be \x00 + binary scan $data ccc* ver nmethods methods + if {($ver != 5) || ([lsearch -exact $methods 0] < 0)} { + catch { + debug_sock "close $sock" + puts -nonewline $sock "\x05\xff" + close $sock + } + return + } + if {[catch { + puts -nonewline $sock "\x05\x00" + flush $sock + debug "\t wrote 2: 'x05x00'" + }]} { + return + } + fileevent $sock readable \ + [list [namespace current]::s5i_read_auth $jlibname $sock] +} + +proc jlib::bytestreams::s5i_read_auth {jlibname sock} { + + upvar ${jlibname}::bytestreams::hash2sid hash2sid + debug "jlib::bytestreams::s5i_read_auth (i)" + + fileevent $sock readable {} + if {[catch {read $sock} data] || [eof $sock]} { + debug_sock "close $sock" + catch {close $sock} + return + } + debug "\t read [string length $data]" + + binary scan $data ccccc ver cmd rsv atyp len + if {$ver != 5 || $cmd != 1 || $atyp != 3} { + set reply [string replace $data 1 1 \x07] + catch { + debug_sock "close $sock" + puts -nonewline $sock $reply + close $sock + } + return + } + + binary scan $data @5a${len} hash + + # At this stage we are in a position to find the sid. + if {[info exists hash2sid($hash)]} { + set sid $hash2sid($hash) + + # This is the way the initiator knows the socket. + s5i_register_socket $jlibname $sid $sock + + set reply [string replace $data 1 1 \x00] + catch { + puts -nonewline $sock $reply + flush $sock + } + debug "\t wrote [string length $reply]" + } else { + debug "\t missing sid" + set reply [string replace $data 1 1 \x02] + catch { + debug_sock "close $sock" + puts -nonewline $sock $reply + close $sock + } + return + } +} + +# jlib::bytestreams::s5i_register_socket -- +# +# This is a callback when a client has connected and authentized +# with our server. Normally we are the initiator but in fastmode +# we may also be the target. +# Since the server handles connections async it needs this method to +# communicate. + +proc jlib::bytestreams::s5i_register_socket {jlibname sid sock} { + + variable fastmode + upvar ${jlibname}::bytestreams::istate istate + upvar ${jlibname}::bytestreams::tstate tstate + debug "jlib::bytestreams::s5i_register_socket" + + if {$fastmode && [info exists tstate($sid,fast,state)]} { + debug "\t (t)" + if {$tstate($sid,fast,state) ne "error"} { + set tstate($sid,fast,sock) $sock + set tstate($sid,fast,state) connected + } + } elseif {[info exists istate($sid,state)]} { + debug "\t (i)" + if {$istate($sid,state) ne "error"} { + set istate($sid,sock) $sock + set istate($sid,state) connected + } + } else { + debug "\t empty" + # We may have been reset (timeout) or something. + } +} + +# End s5i ---------------------------------------------------------------------- + +# jlib::bytestreams::isend_error -- +# +# Deliver iq error to target as a response to the targets streamhosts. +# Fastmode only! + +proc jlib::bytestreams::isend_error {jlibname sid errcode errtype stanza} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::isend_error (i)" + + set id $istate($sid,fast,id) + set jid $istate($sid,fast,jid) + set qE $istate($sid,fast,queryE) + jlib::send_iq_error $jlibname $jid $id $errcode $errtype $stanza $qE +} + +# jlib::bytestreams::ifinish -- +# +# Close all sockets and make sure to free all memory. + +proc jlib::bytestreams::ifinish {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + debug "jlib::bytestreams::ifinish (i)" + + # Skip any ongoing socks5 connections. + if {$istate($sid,used-proxy)} { + connector_reset $jlibname $sid p + } + if {$istate($sid,fast)} { + connector_reset $jlibname $sid f + } + + # Close socket. + if {[info exists istate($sid,sock)]} { + debug_sock "close $istate($sid,sock)" + catch {close $istate($sid,sock)} + } + if {[info exists istate($sid,fast,sock)]} { + debug_sock "close $istate($sid,fast,sock)" + catch {close $istate($sid,fast,sock)} + } + if {[info exists istate($sid,proxy,sock)]} { + debug_sock "close $istate($sid,proxy,sock)" + catch {close $istate($sid,proxy,sock)} + } + ifree $jlibname $sid +} + +# jlib::bytestreams::ifree -- +# +# Releases all memory for an initiator object. + +proc jlib::bytestreams::ifree {jlibname sid} { + + upvar ${jlibname}::bytestreams::istate istate + upvar ${jlibname}::bytestreams::hash2sid hash2sid + debug "jlib::bytestreams::ifree (i)" + + if {[info exists istate($sid,hash)]} { + set hash $istate($sid,hash) + unset -nocomplain hash2sid($hash) + } + array unset istate $sid,* +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# These are all functions to use by a target (receiver) of a stream. + +# jlib::bytestreams::handle_set -- +# +# Handler for incoming iq-set element with xmlns +# "http://jabber.org/protocol/bytestreams". +# +# Initiator sends IQ-set to Target specifying the full JID and network +# address of StreamHost/Initiator as well as the StreamID (SID) of the +# proposed bytestream. +# +# For fastmode this can be either initiator or target. +# It is stateless and only dispatches the iq to the target normally, +# but can also be the initiator in case of fastmode. +# +# Result: +# MUST return 0 or 1! + +proc jlib::bytestreams::handle_set {jlibname from queryE args} { + variable xmlns + variable fastmode + + debug "jlib::bytestreams::handle_set (t+i)" + + array set argsA $args + array set attr [wrapper::getattrlist $queryE] + if {![info exists argsA(-id)]} { + # We cannot handle this since missing id-attribute. + return 0 + } + if {![info exists attr(sid)]} { + eval {return_error $jlibname $queryE 400 modify bad-request} $args + return 1 + } + set id $argsA(-id) + set sid $attr(sid) + set jid $from + + # We make sure that we have already got a si with this sid. + if {![jlib::si::havesi $jlibname $sid]} { + eval {return_error $jlibname $queryE 406 cancel not-acceptable} $args + return 1 + } + + # Get streamhosts keeping their order. + set hosts [list] + foreach elem [wrapper::getchildswithtag $queryE "streamhost"] { + array unset sattr + array set sattr [wrapper::getattrlist $elem] + if {[info exists sattr(jid)] \ + && [info exists sattr(host)] \ + && [info exists sattr(port)]} { + lappend hosts [list $sattr(jid) $sattr(host) $sattr(port)] + } + } + debug "\t hosts=$hosts" + if {![llength $hosts]} { + eval {return_error $jlibname $queryE 400 modify bad-request} $args + return 1 + } + + # In fastmode we may get a streamhosts offer for reversed socks5 connections. + if {[is_initiator $jlibname $sid]} { + if {$fastmode} { + i_handle_set $jlibname $sid $id $jid $hosts $queryE + } else { + # @@@ inconsistency! + return 0 + } + } else { + + # This is the normal execution path. + t_handle_set $jlibname $sid $id $jid $hosts $queryE + } + return 1 +} + +# jlib::bytestreams::t_handle_set -- +# +# This is like the constructor of a target sid object. + +proc jlib::bytestreams::t_handle_set {jlibname sid id jid hosts queryE} { + variable fastmode + variable xmlns + + upvar ${jlibname}::bytestreams::tstate tstate + upvar ${jlibname}::bytestreams::static static + upvar ${jlibname}::bytestreams::hash2sid hash2sid + debug "jlib::bytestreams::t_handle_set (t)" + + set tstate($sid,id) $id + set tstate($sid,jid) $jid + set tstate($sid,fast) 0 + set tstate($sid,state) open + set tstate($sid,hosts) $hosts + set tstate($sid,queryE) $queryE + + if {$fastmode} { + set fastE [wrapper::getchildswithtagandxmlns $queryE "fast" $xmlns(fast)] + if {[llength $fastE]} { + + set haveserver 1 + if {![info exists static(sock)]} { + + # Protect against server failure. + if {[catch {s5i_server $jlibname}]} { + set haveserver 0 + } + } + + # At this stage we switch to use the fast mode protocol. + if {$haveserver} { + set tstate($sid,fast) 1 + set tstate($sid,fast,state) initiate + + # Provide our streamhosts. + # First, the local one. + if {$static(-address) ne ""} { + set ip $static(-address) + } else { + set ip [jlib::getip $jlibname] + } + set myjid [jlib::myjid $jlibname] + set hash [::sha1::sha1 $sid$myjid$jid] + set tstate($sid,hash) $hash + set hash2sid($hash) $sid + + # @@@ Is there a point that also the target provides a + # proxy streamhost? + # If the clients are using different servers, one may have a + # proxy while the other has not. + # Keep it optional (-targetproxy). + set host [list $myjid -host $ip -port $static(port)] + set streamhosts [list $host] + + # Second, the proxy host if any. + if {$static(-targetproxy) && [llength $static(-proxyhost)]} { + lassign $static(-proxyhost) pjid pip pport + set proxyhost [list $pjid -host $pip -port $pport] + lappend streamhosts $proxyhost + } + + set t_initiate_cb \ + [list [namespace current]::t_initiate_cb $jlibname $sid] + send_initiate $jlibname $jid $sid $t_initiate_cb \ + -streamhosts $streamhosts + } + } + } + + # Try connecting the host(s) in turn. + set tstate($sid,state) connecting + set myjid [$jlibname myjid] + set hash [::sha1::sha1 $sid$jid$myjid] + + set cb [list [namespace current]::connect_cb $jlibname $sid] + connector $jlibname $sid t $hash $hosts $cb +} + +# jlib::bytestreams::connect_cb -- +# +# Callback command from 'connector' object when tried socks5 connections +# to initiators streamhosts. + +proc jlib::bytestreams::connect_cb {jlibname sid result args} { + + upvar ${jlibname}::bytestreams::tstate tstate + debug "jlib::bytestreams::connect_cb (t)" + + array set argsA $args + + if {$result eq "error"} { + set tstate($sid,state) error + + # Deliver error to initiator. + tsend_error $jlibname $sid 404 cancel item-not-found + + # In fastmode we are not done until the fast mode also fails. + if {!$tstate($sid,fast) || ($tstate($sid,fast,state) eq "error")} { + + # Deliver error to target profile. + jlib::si::stream_error $jlibname $sid item-not-found + tfinish $jlibname $sid + } + } else { + set sock $argsA(-socket) + set host $argsA(-streamhost) + set hostjid [lindex $host 0] + + set tstate($sid,sock) $sock + set tstate($sid,host) $host + set tstate($sid,hostjid) $hostjid + + set jid $tstate($sid,jid) + set id $tstate($sid,id) + send_used $jlibname $jid $id $hostjid + + # If fast mode we must wait for a CR before start reading. + if {$tstate($sid,fast)} { + + # Wait for initiator send a CR for selection or just close it. + set tstate($sid,state) waiting-cr + set cmd_cr [list [namespace current]::read_CR_cb $jlibname $sid] + fileevent $sock readable \ + [list [namespace current]::read_CR $sock $cmd_cr] + + } else { + start_read_data $jlibname $sid $sock + } + } +} + +proc jlib::bytestreams::t_initiate_cb {jlibname sid type subiq args} { + + upvar ${jlibname}::bytestreams::tstate tstate + debug "jlib::bytestreams::t_initiate_cb (t) type=$type" + + # In fast mode we may get this callback after we have finished. + # Or after a timeout or something. + if {![info exists tstate($sid,state)]} { + return + } + + if {$type eq "error"} { + + # Cleanup and close any fast socks5 connection. + set tstate($sid,fast,state) error + if {[info exists tstate($sid,fast,sock)]} { + debug_sock "close $tstate($sid,fast,sock)" + catch {close $tstate($sid,fast,sock)} + unset tstate($sid,fast,sock) + } + + # If also the standard way failed we are done. + if {$tstate($sid,state) eq "error"} { + jlib::si::stream_error $jlibname $sid item-not-found + tfinish $jlibname $sid + } + } else { + + # Wait for initiator send a CR for selction or just close it. + debug "\t waiting CR" + set tstate($sid,fast,state) waiting-cr + set sock $tstate($sid,fast,sock) + set cmd_cr [list [namespace current]::fast_read_CR_cb $jlibname $sid] + fileevent $sock readable \ + [list [namespace current]::read_CR $sock $cmd_cr] + } +} + +proc jlib::bytestreams::read_CR {sock cmd} { + + debug "jlib::bytestreams::read_CR (t)" + + fileevent $sock readable {} + if {[catch {read $sock 1} data] || [eof $sock]} { + debug "\t eof" + catch {close $sock} + eval $cmd error + } elseif {$data ne "\r"} { + debug "\t not CR" + catch {close $sock} + eval $cmd error + } else { + debug "\t got CR" + eval $cmd + } +} + +proc jlib::bytestreams::fast_read_CR_cb {jlibname sid {error ""}} { + + upvar ${jlibname}::bytestreams::tstate tstate + debug "jlib::bytestreams::fast_read_CR_cb (t) error=$error" + + if {$error ne ""} { + set tstate($sid,fast,state) error + unset -nocomplain tstate($sid,fast,sock) + + # If also the standard way failed we are done. + if {$tstate($sid,state) eq "error"} { + jlib::si::stream_error $jlibname $sid item-not-found + tfinish $jlibname $sid + } + } else { + + # At this stage we are using reversed transport (fast mode). + # We are using the targets (our own) streamhost. + connector_reset $jlibname $sid t + if {[info exists tstate($sid,sock)]} { + debug_sock "close $tstate($sid,sock)" + catch {close $tstate($sid,sock)} + unset tstate($sid,sock) + } + + # Deliver error to initiator unless not done so. + if {$tstate($sid,state) ne "error"} { + tsend_error $jlibname $sid 404 cancel item-not-found + } + set tstate($sid,state) error + set tstate($sid,fast,selected) fast + set tstate($sid,fast,state) read + + start_read_data $jlibname $sid $tstate($sid,fast,sock) + } +} + +#............................................................................... + +proc jlib::bytestreams::read_CR_cb {jlibname sid {error ""}} { + + upvar ${jlibname}::bytestreams::tstate tstate + debug "jlib::bytestreams::read_CR_cb (t) error=$error" + + if {$error ne ""} { + set tstate($sid,state) error + unset -nocomplain tstate($sid,sock) + + # If also the fast mode failed this is The End. + if {$tstate($sid,fast) && ($tstate($sid,fast,state) eq "error")} { + jlib::si::stream_error $jlibname $sid item-not-found + tfinish $jlibname $sid + } + } else { + if {[info exists tstate($sid,fast,sock)]} { + debug_sock "close $tstate($sid,fast,sock)" + catch {close $tstate($sid,fast,sock)} + unset tstate($sid,fast,sock) + } + set sock $tstate($sid,sock) + + set tstate($sid,fast,selected) normal + set tstate($sid,state) read + + start_read_data $jlibname $sid $sock + } +} + +proc jlib::bytestreams::start_read_data {jlibname sid sock} { + + upvar ${jlibname}::bytestreams::static static + + fconfigure $sock -buffersize $static(-block-size) -buffering full + fileevent $sock readable \ + [list [namespace current]::readable $jlibname $sid $sock] +} + +# End connect_socks ------------------------------------------------------------ + +# jlib::bytestreams::readable -- +# +# Reads channel and delivers data up to si. + +proc jlib::bytestreams::readable {jlibname sid sock} { + + upvar ${jlibname}::bytestreams::tstate tstate + upvar ${jlibname}::bytestreams::static static + debug "jlib::bytestreams::readable (t)" + + fileevent $sock readable {} + + # We may have been reset or something. + if {![jlib::si::havesi $jlibname $sid]} { + tfinish $jlibname $sid + return + } + + if {[catch {eof $sock} iseof] || $iseof} { + debug "\t eof" + # @@@ Perhaps we should check number of bytes reveived or something??? + # If the initiator closes socket before transfer is complete + # we wont notice this otherwise. + jlib::si::stream_closed $jlibname $sid + tfinish $jlibname $sid + } else { + + # @@@ Keep tranck of number bytes read? + set data [read $sock $static(-block-size)] + set len [string length $data] + debug "\t len=$len" + + # Deliver to si for further processing. + jlib::si::stream_recv $jlibname $sid $data + + # This is a trick to put this event at the back of the queue to + # avoid using any 'update'. + after idle [list after 0 [list \ + [namespace current]::setreadable $jlibname $sid $sock]] + } +} + +proc jlib::bytestreams::setreadable {jlibname sid sock} { + + # We could have been closed since this event comes async. + if {[lsearch [file channels] $sock] >= 0} { + fileevent $sock readable \ + [list [namespace current]::readable $jlibname $sid $sock] + } +} + +# jlib::bytestreams::send_used -- +# +# Target (also initiator in fast mode) notifies initiator of connection. + +proc jlib::bytestreams::send_used {jlibname to id hostjid} { + variable xmlns + + set usedE [wrapper::createtag "streamhost-used" \ + -attrlist [list jid $hostjid]] + set xmllist [wrapper::createtag "query" \ + -attrlist [list xmlns $xmlns(bs)] \ + -subtags [list $usedE]] + + $jlibname send_iq "result" [list $xmllist] -to $to -id $id +} + +# The client socks5 functions -------------------------------------------------- +# +# Normally used by the target but in fastmode also used by the initiator. +# +# This object handles everything to make a single socks5 connection + +# authentication. +# [in]: addr, port, hash, cmd +# [out]: sock, result + +# jlib::bytestreams::socks5 -- +# +# Open a client socket to the specified host and port and announce method. +# This must be kept stateless. + +proc jlib::bytestreams::socks5 {addr port hash cmd} { + + debug "jlib::bytestreams::socks5 (t)" + + if {[catch { + set sock [socket -async $addr $port] + } err]} { + return -code error $err + } + debug_sock "open $sock" + fconfigure $sock -translation binary -blocking 0 + fileevent $sock writable \ + [list [namespace current]::s5t_write_method $hash $sock $cmd] + return $sock +} + +proc jlib::bytestreams::s5t_write_method {hash sock cmd} { + + debug "jlib::bytestreams::s5t_write_method (t)" + fileevent $sock writable {} + + # Announce method (\x00). + if {[catch { + puts -nonewline $sock "\x05\x01\x00" + flush $sock + debug "\t wrote 3: 'x05x01x00'" + } err]} { + catch {close $sock} + eval $cmd error-network-write + return + } + fileevent $sock readable \ + [list [namespace current]::s5t_method_result $hash $sock $cmd] +} + +proc jlib::bytestreams::s5t_method_result {hash sock cmd} { + + debug "jlib::bytestreams::s5t_method_result (t)" + + fileevent $sock readable {} + if {[catch {read $sock} data] || [eof $sock]} { + catch {close $sock} + eval $cmd error-network-read + return + } + debug "\t read [string length $data]" + binary scan $data cc ver method + if {($ver != 5) || ($method != 0)} { + catch {close $sock} + eval $cmd error-socks5 + return + } + set len [binary format c [string length $hash]] + if {[catch { + puts -nonewline $sock "\x05\x01\x00\x03$len$hash\x00\x00" + flush $sock + debug "\t wrote [string length "\x05\x01\x00\x03$len$hash\x00\x00"]: 'x05x01x00x03${len}${hash}x00x00'" + } err]} { + catch {close $sock} + eval $cmd error-network-write + return + } + fileevent $sock readable \ + [list [namespace current]::s5t_auth_result $sock $cmd] +} + +proc jlib::bytestreams::s5t_auth_result {sock cmd} { + + debug "jlib::bytestreams::s5t_auth_result (t)" + + fileevent $sock readable {} + if {[catch {read $sock} data] || [eof $sock]} { + catch {close $sock} + eval $cmd error-network-read + return + } + debug "\t read [string length $data]" + binary scan $data cc ver method + if {($ver != 5) || ($method != 0)} { + catch {close $sock} + eval $cmd error-socks5 + return + } + + # Here we should be finished. + eval $cmd +} + +# End s5t ---------------------------------------------------------------------- + +# jlib::bytestreams::return_error, tsend_error -- +# +# Various helper functions to return errors. + +proc jlib::bytestreams::return_error {jlibname qElem errcode errtype stanza args} { + + array set attr $args + set id $attr(-id) + set jid $attr(-from) + jlib::send_iq_error $jlibname $jid $id $errcode $errtype $stanza $qElem +} + +proc jlib::bytestreams::tsend_error {jlibname sid errcode errtype stanza} { + + upvar ${jlibname}::bytestreams::tstate tstate + debug "jlib::bytestreams::tsend_error (t)" + + set id $tstate($sid,id) + set jid $tstate($sid,jid) + set qE $tstate($sid,queryE) + jlib::send_iq_error $jlibname $jid $id $errcode $errtype $stanza $qE +} + +proc jlib::bytestreams::tfinish {jlibname sid} { + + upvar ${jlibname}::bytestreams::tstate tstate + debug "jlib::bytestreams::tfinish (t)" + + # Close socket. + if {[info exists tstate($sid,sock)]} { + debug_sock "close $tstate($sid,sock)" + catch {close $tstate($sid,sock)} + } + if {[info exists tstate($sid,fast,sock)]} { + debug_sock "close $tstate($sid,fast,sock)" + catch {close $tstate($sid,fast,sock)} + } + if {[info exists tstate($sid,timeoutid)]} { + after cancel $tstate($sid,timeoutid) + } + tfree $jlibname $sid +} + +proc jlib::bytestreams::tfree {jlibname sid} { + + upvar ${jlibname}::bytestreams::tstate tstate + upvar ${jlibname}::bytestreams::hash2sid hash2sid + debug "jlib::bytestreams::tfree (t)" + + if {[info exists tstate($sid,hash)]} { + set hash $tstate($sid,hash) + unset -nocomplain hash2sid($hash) + } + array unset tstate $sid,* +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::bytestreams { + + jlib::ensamble_register bytestreams \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +# connector -------------------------------------------------------------------- + +# jlib::bytestreams::connector -- +# +# Standalone object which is target and initiator agnostic that tries +# to make socks5 connections to the hosts in turn. Invokes the callback +# for the first succesful connection or an error if none worked. +# The 'sid' is the characteristic identifier of an object. +# It sets its own timeouts. Needs also a unique 'key' if using multiple +# connectors for one sid. +# +# NB1: SHA1(SID + Initiator JID + Target JID) +# NB2: the initiator may have two connector objects if fast + proxy. +# +# [in]: sid, key, hash, hosts, cmd +# [out]: result (-error | -host -socket) + +proc jlib::bytestreams::connector {jlibname sid key hash hosts cmd} { + + upvar ${jlibname}::bytestreams::conn conn + debug "jlib::bytestreams::connector $key" + + set x $sid,$key + set conn($x,hosts) $hosts + set conn($x,cmd) $cmd + set conn($x,hash) $hash + set conn($x,idx) [expr {[llength $hosts]-1}] + + connector_sock $jlibname $sid $key + return +} + +# jlib::bytestreams::connector_sock -- +# +# Tries to make a socks5 connection to streamhost with 'idx' index. +# If 'idx' goes negative we report an error. + +proc jlib::bytestreams::connector_sock {jlibname sid key} { + + upvar ${jlibname}::bytestreams::conn conn + upvar ${jlibname}::bytestreams::static static + debug "jlib::bytestreams::connector_sock $key" + + set x $sid,$key + if {[info exists conn($x,timeoutid)]} { + after cancel $conn($x,timeoutid) + unset conn($x,timeoutid) + } + if {$conn($x,idx) < 0} { + connector_final $jlibname $sid $key "error" + return + } + set conn($x,timeoutid) [after $static(-s5timeoutms) \ + [list [namespace current]::connector_timeout_cb $jlibname $sid $key]] + + set host [lindex $conn($x,hosts) $conn($x,idx)] + lassign $host hostjid addr port + debug "\t host=$host" + set s5_cb [list [namespace current]::connector_s5_cb $jlibname $sid $key] + if {[catch { + set conn($x,sock) [socks5 $addr $port $conn($x,hash) $s5_cb] + }]} { + + # Retry with next streamhost if any. + incr conn($x,idx) -1 + connector_sock $jlibname $sid $key + } +} + +proc jlib::bytestreams::connector_s5_cb {jlibname sid key {err ""}} { + + upvar ${jlibname}::bytestreams::conn conn + debug "jlib::bytestreams::connector_s5_cb $key err=$err" + + set x $sid,$key + if {$err eq ""} { + connector_final $jlibname $sid $key + } else { + incr conn($x,idx) -1 + connector_sock $jlibname $sid $key + } +} + +proc jlib::bytestreams::connector_timeout_cb {jlibname sid key} { + + upvar ${jlibname}::bytestreams::conn conn + debug "jlib::bytestreams::connector_timeout_cb $key" + + # On timeouts we are responsible for closing the socket. + set x $sid,$key + unset conn($x,timeoutid) + if {[info exists conn($x,sock)]} { + debug_sock "close $conn($x,sock)" + catch {close $conn($x,sock)} + unset conn($x,sock) + } + incr conn($x,idx) -1 + connector_sock $jlibname $sid $key +} + +proc jlib::bytestreams::connector_reset {jlibname sid key} { + + upvar ${jlibname}::bytestreams::conn conn + debug "jlib::bytestreams::connector_reset $key" + + # Protect for nonexisting connector object. + set x $sid,$key + if {![info exists conn($x,cmd)]} { + return + } + if {[info exists conn($x,timeoutid)]} { + after cancel $conn($x,timeoutid) + unset conn($x,timeoutid) + } + if {[info exists conn($x,sock)]} { + debug_sock "close $conn($x,sock)" + catch {close $conn($x,sock)} + unset conn($x,sock) + } + connector_final $jlibname $sid $key "reset" +} + +proc jlib::bytestreams::connector_final {jlibname sid key {err ""}} { + + upvar ${jlibname}::bytestreams::conn conn + debug "jlib::bytestreams::connector_final err=$err" + + set x $sid,$key + if {[info exists conn($x,timeoutid)]} { + after cancel $conn($x,timeoutid) + unset conn($x,timeoutid) + } + set cmd $conn($x,cmd) + if {$err eq ""} { + set host [lindex $conn($x,hosts) $conn($x,idx)] + eval $cmd ok -streamhost $host -socket $conn($x,sock) + } else { + + # Skip callback when we have reset. ? + if {$err ne "reset"} { + eval $cmd error -error $err + } + } + array unset conn $x,* +} + +proc jlib::bytestreams::debug {msg} {if {0} {puts $msg}} + +proc jlib::bytestreams::debug_sock {msg} {if {0} {puts $msg}} + +#------------------------------------------------------------------------------- + +if {0} { + # Testing the 'connector' + set jlib ::jlib::jlib1 + set port [$jlib bytestreams s5i_server] + set hosts [list \ + [list proxy.localhost junk.se 8237] \ + [list matben@localhost 127.0.0.1 $port]] + proc cb {args} {puts "---> $args"} + set sid [jlib::generateuuid] + set myjid [$jlib myjid] + set jid killer@localhost/coccinella + set hash [::sha1::sha1 $sid$myjid$jid] + $jlib bytestreams connector $sid $hash $hosts cb + + # Testing proxy: + # 1) get proxy + set jlib ::jlib::jlib1 + proc pcb {jlib type queryE} { + puts "---> $jlib $type $queryE" + set hostE [wrapper::getfirstchildwithtag $queryE "streamhost"] + array set attr [wrapper::getattrlist $hostE] + set ::proxyHost $attr(host) + set ::proxyPort $attr(port) + } + set proxy proxy.jabber.se + $jlib bytestreams get_proxy $proxy pcb + $jlib bytestreams configure -proxyhost [list $proxy $proxyHost $proxyPort] + + # 2) socks5 connection + set sid [jlib::generateuuid] + set myjid [$jlib myjid] + set jid killer@jabber.se/coccinella + set hash [::sha1::sha1 $sid$myjid$jid] + set hosts [list [list $proxy $proxyHost $proxyPort]] + $jlib bytestreams connector $sid $hash $hosts cb + + # 3) activate + $jlib bytestreams activate $sid $proxy $jid + +} + + diff --git a/lib/jabberlib/caps.tcl b/lib/jabberlib/caps.tcl new file mode 100644 index 0000000..ccb6860 --- /dev/null +++ b/lib/jabberlib/caps.tcl @@ -0,0 +1,530 @@ +# caps.tcl -- +# +# This file is part of the jabberlib. It handles the internal cache +# for caps (xmlns='http://jabber.org/protocol/caps') XEP-0115. +# It is updated to version 1.3 of XEP-0115. +# +# A typical caps element looks like: +# +# +# +# +# +# The core function of caps is a mapping: +# +# jid -> node+ver -> disco info +# jid -> node+ext -> disco info +# +# NB: The ext must be consistent over all versions (ver). +# +# UPDATE version 1.4: --------------------------------------------------------- +# +# +# +# +# +# The 'ver' map to a unique combination of disco identities+features. +# +# ----------------------------------------------------------------------------- +# +# Copyright (c) 2005-2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: caps.tcl,v 1.25 2007/10/04 14:01:07 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# caps - convenience command library for caps: Entity Capabilities +# +# INSTANCE COMMANDS +# jlibname caps register name xmllist features +# jlibname caps configure ?-autodisco 0|1? -command tclProc +# jlibname caps getexts +# jlibname caps getxmllist name +# jlibname caps getallfeatures +# jlibname caps getfeatures name +# +# The 'name' is here the ext token. + +# TODO: make a static cache (variable cache) which maps the hashed ver attribute +# to a list of disco identities and features. + +package require base64 ; # tcllib +package require sha1 ; # tcllib +package require jlib::disco +package require jlib::roster + +package provide jlib::caps 0.3 + +namespace eval jlib::caps { + + variable xmlns + set xmlns(caps) "http://jabber.org/protocol/caps" + + # Note: jlib::ensamble_register is last in this file! +} + +proc jlib::caps::init {jlibname args} { + + # Instance specific arrays. + namespace eval ${jlibname}::caps { + variable ext + variable options + } + + upvar ${jlibname}::caps::options options + array set options { + -autodisco 0 + -command {} + } + eval {configure $jlibname} $args + + # Since the caps element from a JID is globally defined there is no need + # to keep its state instance specific (per jlibname). + + # The cache for disco results. Must not be instance specific. + variable caps + + # This collects various mappings and states: + # o It keeps track of mapping jid -> node+ver+exts + # o + variable state + + jlib::presence_register_int $jlibname available \ + [namespace current]::avail_cb + jlib::presence_register_int $jlibname unavailable \ + [namespace current]::unavail_cb + + jlib::register_reset $jlibname [namespace current]::reset +} + +proc jlib::caps::configure {jlibname args} { + upvar ${jlibname}::caps::options options + + if {[llength $args]} { + foreach {key value} $args { + switch -- $key { + -autodisco { + if {[string is boolean -strict $value]} { + set options(-autodisco) $value + } else { + return -code error "expected boolean for -autodisco" + } + } + -command { + set options(-command) $value + } + default { + return -code error "unrecognized option \"$key\"" + } + } + } + } else { + return [array get options] + } +} + +proc jlib::caps::cmdproc {jlibname cmd args} { + + # Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +#--- First, handle our own caps stuff ------------------------------------------ + +# jlib::caps::register -- +# +# Register an 'ext' token and associated disco#info element. +# The 'name' is the ext token. +# The 'features' must be the 'var' attributes in 'xmllist'. +# + +proc jlib::caps::register {jlibname name xmllist features} { + upvar ${jlibname}::caps::ext ext + + set ext(name,$name) $name + set ext(xmllist,$name) $xmllist + set ext(features,$name) $features +} + +proc jlib::caps::getallidentities {jlibname} { + upvar ${jlibname}::caps::ext ext + + return $ext(identities) +} + +proc jlib::caps::getexts {jlibname} { + upvar ${jlibname}::caps::ext ext + + set exts [list] + foreach {key name} [array get ext name,*] { + lappend exts $name + } + return [lsort $exts] +} + +proc jlib::caps::getxmllist {jlibname name} { + upvar ${jlibname}::caps::ext ext + + if {[info exists ext(xmllist,$name)]} { + return $ext(xmllist,$name) + } else { + return + } +} + +proc jlib::caps::getfeatures {jlibname name} { + upvar ${jlibname}::caps::ext ext + + if {[info exists ext(features,$name)]} { + return $ext(features,$name) + } else { + return + } +} + +proc jlib::caps::getallfeatures {jlibname} { + upvar ${jlibname}::caps::ext ext + + set featureL [list] + foreach {key features} [array get ext features,*] { + set featureL [concat $featureL $features] + } + return [lsort -unique $featureL] +} + +# jlib::caps::generate_ver -- +# +# This just takes the internal identities and features into account. +# NB: A client MUST synchronize the disco identity amd feature elements +# here else we respond with a false ver attribute! + +proc jlib::caps::generate_ver {jlibname} { + + set identities [jlib::disco::getidentities $jlibname] + set features [concat [getallfeatures $jlibname] \ + [jlib::disco::getregisteredfeatures]] + return [create_ver $identities $features] +} + +proc jlib::caps::create_ver {identityL featureL} { + + set ver "" + append ver [join [lsort -unique $identityL] <] + append ver < + append ver [join [lsort -unique $featureL] <] + append ver < + set hex [::sha1::sha1 $ver] + + # Inverse to: [format %0.8x%0.8x%0.8x%0.8x%0.8x $H0 $H1 $H2 $H3 $H4] + set parts "" + for {set i 0} {$i < 5} {incr i} { + append parts "0x" + append parts [string range $hex [expr {8*$i}] [expr {8*$i + 7}]] + append parts " " + } + # Works independent on machine Endian order! + set bin [eval binary format IIIII $parts] + return [::base64::encode $bin] +} + +# Test case: +if {0} { + set S "client/pc +# +# +# +# We MUST have got a presence caps element for this user. +# +# The client that received the annotated presence sends a disco#info +# request to exactly one of the users that sent a particular presenece +# element caps combination of node and ver. + +proc jlib::caps::disco_ver {jlibname jid} { + + set ver [$jlibname roster getcapsattr $jid ver] + disco $jlibname $jid ver $ver +} + +# jlib::caps::disco_ext -- +# +# Disco the 'ext' via the caps node+ext cache. +# +# We MUST have got a presence caps element for this user with the +# corresponding 'ext' token. + +proc jlib::caps::disco_ext {jlibname jid ext} { + + disco $jlibname $jid ext $ext +} + +# jlib::caps::disco -- +# +# Internal use only. See disco_ver and disco_ext. +# +# Arguments: +# what: "ver" or "ext" +# value: value for 'ver' or the name of the 'ext'. + +proc jlib::caps::disco {jlibname jid what value} { + variable state + variable caps + + set node [$jlibname roster getcapsattr $jid node] + set key $what,$node,$value + + # Mark that we have a pending node+ver or node+ext request. + set state(pending,$key) 1 + + # It should be safe to use 'disco get_async' here. + # Need to provide node+ver for error recovery. + set cb [list [namespace current]::disco_cb $node $what $value] + $jlibname disco get_async info $jid $cb -node ${node}#${value} +} + +# jlib::caps::disco_cb -- +# +# Callback for 'disco get_async'. +# We must take care of a situation where the jid went unavailable, +# or otherwise returns an error, and try to use another jid. + +proc jlib::caps::disco_cb {node what value jlibname type from queryE args} { + upvar ${jlibname}::caps::options options + variable state + variable caps + + set key $what,$node,$value + unset -nocomplain state(pending,$key) + + if {$type eq "error"} { + + # If one client with a certain 'key' fails it is likely all will + # fail since they are assumed to be identical, unless it failed + # because it went offline. + # @@@ Risk for infinite loop? + if {$options(-autodisco) && ![$jlibname roster isavailable $from]} { + set rjid [get_random_jid $what $node $value] + if {$rjid ne ""} { + disco $jlibname $rjid $what $value + } + } + } else { + set jid [jlib::jidmap $from] + + # Cache the returned element to be reused for all node+ver combinations. + set caps(queryE,$key) $queryE + if {[llength $options(-command)]} { + uplevel #0 $options(-command) [list $jlibname $from $queryE] + } + } +} + +# OBSOLETE IN 1.4 + +# jlib::caps::avail_cb -- +# +# Registered available presence callback. +# Keeps track of all jid <-> node+ver combinations. +# The exts may be different for identical node+ver and must be +# obtained for individual jids using 'roster getcapsattr'. + +proc jlib::caps::avail_cb {jlibname xmldata} { + upvar ${jlibname}::caps::options options + variable state + variable caps + + set jid [wrapper::getattribute $xmldata from] + set jid [jlib::jidmap $jid] + + set node [$jlibname roster getcapsattr $jid node] + + # Skip if the client doesn't have a caps presence element. + if {$node eq ""} { + return + } + set ver [$jlibname roster getcapsattr $jid ver] + set ext [$jlibname roster getcapsattr $jid ext] + + # Map jid -> node+ver+ext. Note that 'ext' may be empty. + set state(jid,node,$jid) $node + set state(jid,ver,$jid) $ver + set state(jid,ext,$jid) $ext + + # For each combinations node+ver and node+ext we must be able to collect + # a list of JIDs where we shall pick a random one to disco. + # Avoid a linear search. Better to use the array hash mechanism. + + set state(jids,ver,$ver,$node,$jid) $jid + foreach e $ext { + set state(jids,ext,$e,$node,$jid) $jid + } + + # If auto disco then try to disco all node+ver and node+exts which we + # don't have and aren't waiting for. + if {$options(-autodisco)} { + set key ver,$node,$ver + if {![info exists caps(queryE,$key)]} { + if {![info exists state(pending,$key)]} { + set rjid [get_random_jid ver $node $ver] + if {$rjid ne ""} { + disco $jlibname $rjid ver $ver + } + } + } + foreach e $ext { + set key ext,$node,$e + if {![info exists caps(queryE,$key)]} { + if {![info exists state(pending,$key)]} { + set rjid [get_random_jid ext $node $e] + if {$rjid ne ""} { + disco $jlibname $rjid ext $e + } + } + } + } + } + return 0 +} + +# OBSOLETE IN 1.4 + +# jlib::caps::get_random_jid_ver, get_random_jid_ext -- +# +# Methods to pick a random JID from node+ver or node+ext. + +proc jlib::caps::get_random_jid {what node value} { + get_random_jid_$what $node $value +} + +proc jlib::caps::get_random_jid_ver {node ver} { + variable state + + set keys [array names state jids,ver,$ver,$node,*] + if {[llength $keys]} { + set idx [expr {int(rand()*[llength $keys])}] + return $state([lindex $keys $idx]) + } else { + return + } +} + +proc jlib::caps::get_random_jid_ext {node ext} { + variable state + + set keys [array names state jids,ext,$ext,$node,*] + if {[llength $keys]} { + set idx [expr {int(rand()*[llength $keys])}] + return $state([lindex $keys $idx]) + } else { + return + } +} + +# OBSOLETE IN 1.4 + +# jlib::caps::unavail_cb -- +# +# Registered unavailable presence callback. +# Frees internal cache related to this jid. + +proc jlib::caps::unavail_cb {jlibname xmldata} { + variable state + + set jid [wrapper::getattribute $xmldata from] + set jid [jlib::jidmap $jid] + + # JID may not have caps. + if {![info exists state(jid,node,$jid)]} { + return + } + set node $state(jid,node,$jid) + set ver $state(jid,ver,$jid) + set ext $state(jid,ext,$jid) + + set jidESC [jlib::ESC $jid] + array unset state jid,node,$jidESC + array unset state jid,ver,$jidESC + array unset state jid,ext,$jidESC + array unset state jids,*,$jidESC + + return 0 +} + +proc jlib::caps::reset {jlibname} { + variable state + + unset -nocomplain state +} + +# OBSOLETE IN 1.4 + +proc jlib::caps::writecache {fileName} { + variable caps + + set fd [open $fileName w] + fconfigure $fd -encoding utf-8 + foreach {key value} [array get caps] { + puts $fd [list set caps($key) $value] + } + close $fd +} + +proc jlib::caps::readcache {fileName} { + variable caps + + source $fileName +} + +proc jlib::caps::freecache {} { + variable caps + + unset -nocomplain caps +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::caps { + + jlib::ensamble_register caps \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +# Tests +if {0} { + + proc cb {args} {} + set jlib ::jlib::jlib1 + set jid matben@localhost/coccinella + set caps "http://coccinella.sourceforge.net/protocol/caps" + set ver 0.95.17 + $jlib disco send_get info $jid cb -node $caps#$ver + $jlib disco send_get info $jid cb -node $caps#whiteboard + $jlib disco send_get info $jid cb -node $caps#iax +} diff --git a/lib/jabberlib/compress.tcl b/lib/jabberlib/compress.tcl new file mode 100644 index 0000000..fcdff04 --- /dev/null +++ b/lib/jabberlib/compress.tcl @@ -0,0 +1,231 @@ +# compress.tcl -- +# +# This file is part of jabberlib. +# It implements stream compression as defined in XEP-0138: +# Stream Compression +# +# Copyright (c) 2006-2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# NB: There are several zlib packages floating around the net with the same +# name!. But we must have the one implemented for TIP 234, see +# http://www.tcl.tk/cgi-bin/tct/tip/234.html. +# This is currently of version 2.0.1 so we rely on this when doing +# package require. Beware! +# +# $Id: compress.tcl,v 1.9 2008/01/04 13:41:32 matben Exp $ + +package require jlib +package require -exact zlib 2.0.1 + +package provide jlib::compress 0.1 + +namespace eval jlib::compress { + + variable methods {zlib} + + # NB: There are two namespaces: + # 'http://jabber.org/features/compress' + # 'http://jabber.org/protocol/compress' + variable xmlns + array set xmlns { + features/compress "http://jabber.org/features/compress" + protocol/compress "http://jabber.org/protocol/compress" + } + jlib::register_instance [namespace code instance] +} + +proc jlib::compress::instance {jlibname} { + $jlibname register_reset [namespace code reset] +} + +proc jlib::compress::start {jlibname cmd} { + + variable xmlns + variable methods + + # puts "jlib::compress::start" + + # Instance specific namespace. + namespace eval ${jlibname}::compress { + variable state + } + upvar ${jlibname}::compress::state state + + set state(cmd) $cmd + set state(-method) [lindex $methods 0] + + # Set up the streams for zlib. + set state(compress) [zlib stream compress] + set state(decompress) [zlib stream decompress] + + # Set up callback for the xmlns that is of interest to us. + $jlibname element_register $xmlns(protocol/compress) [namespace code parse] + + if {[$jlibname have_feature]} { + compress $jlibname + } else { + $jlibname trace_stream_features [namespace code features_write] + } +} + +proc jlib::compress::features_write {jlibname} { + + # puts "jlib::compress::features_write" + + $jlibname trace_stream_features {} + compress $jlibname +} + +# jlib::compress::compress -- +# +# Initiating Entity Requests Stream Compression. + +proc jlib::compress::compress {jlibname} { + + variable methods + variable xmlns + upvar ${jlibname}::compress::state state + + # puts "jlib::compress::compress" + + # Note: If the initiating entity did not understand any of the advertised + # compression methods, it SHOULD ignore the compression option and + # proceed as if no compression methods were advertised. + + set have_method [$jlibname have_feature compression $state(-method)] + if {!$have_method} { + finish $jlibname + return + } + + # @@@ MUST match methods!!! + # A compliant implementation MUST implement the ZLIB compression method... + + set methodE [wrapper::createtag method -chdata $state(-method)] + + set xmllist [wrapper::createtag compress \ + -attrlist [list xmlns $xmlns(protocol/compress)] -subtags [list $methodE]] + $jlibname send $xmllist + + # Wait for 'compressed' or 'failure' element. +} + +proc jlib::compress::parse {jlibname xmldata} { + + # puts "jlib::compress::parse" + + set tag [wrapper::gettag $xmldata] + + switch -- $tag { + compressed { + compressed $jlibname $xmldata + } + failure { + failure $jlibname $xmldata + } + default { + finish $jlibname compress-protocol-error + } + } + return +} + +proc jlib::compress::compressed {jlibname xmldata} { + + # puts "jlib::compress::compressed" + + # Example 5. Receiving Entity Acknowledges Stream Compression + # + # Both entities MUST now consider the previous stream to be null and void, + # just as with TLS negotiation and SASL negotiation + # Therefore the initiating entity MUST initiate a new stream to the + # receiving entity: + + $jlibname wrapper_reset + + # We must clear out any server info we've received so far. + $jlibname stream_reset + + $jlibname set_socket_filter [namespace code out] [namespace code in] + + if {[catch { + $jlibname sendstream -version 1.0 + } err]} { + finish $jlibname network-failure $err + return + } + finish $jlibname +} + +# jlib::compress::out, in -- +# +# Actual compression takes place here. +# XEP says: +# When using ZLIB for compression, the sending application SHOULD +# complete a partial flush of ZLIB when its current send is complete. + +proc jlib::compress::out {jlibname data} { + upvar ${jlibname}::compress::state state + + $state(compress) put -flush $data + return [$state(compress) get] +} + +proc jlib::compress::in {jlibname cdata} { + upvar ${jlibname}::compress::state state + + $state(decompress) put $cdata + #$state(decompress) flush + return [$state(decompress) get] +} + +proc jlib::compress::failure {jlibname xmldata} { + + # puts "jlib::compress::failure" + + set c [wrapper::getchildren $xmldata] + if {[llength $c]} { + set errcode [wrapper::gettag [lindex $c 0]] + } else { + set errcode unknown-failure + } + finish $jlibname $errcode +} + +proc jlib::compress::finish {jlibname {errcode ""} {errmsg ""}} { + + upvar ${jlibname}::compress::state state + variable xmlns + + # puts "jlib::compress:finish errcode=$errcode, errmsg=$errmsg" + + # NB: We must keep our state array for the lifetime of the stream. + $jlibname trace_stream_features {} + $jlibname element_deregister $xmlns(protocol/compress) [namespace code parse] + + if {$errcode ne ""} { + uplevel #0 $state(cmd) $jlibname [list $errcode $errmsg] + } else { + uplevel #0 $state(cmd) $jlibname + } +} + +proc jlib::compress::reset {jlibname} { + + upvar ${jlibname}::compress::state state + + # puts "jlib::compress::reset" + + if {[info exists state(compress)]} { + $state(compress) close + unset state(compress) + } + if {[info exists state(decompress)]} { + $state(decompress) close + unset state(decompress) + } + unset -nocomplain state +} + diff --git a/lib/jabberlib/connect.tcl b/lib/jabberlib/connect.tcl new file mode 100644 index 0000000..241a743 --- /dev/null +++ b/lib/jabberlib/connect.tcl @@ -0,0 +1,1109 @@ +# connect.tcl -- +# +# This file is part of the jabberlib. +# It provides a high level method to handle all the things to establish +# a connection with a jabber server and do TLS, SASL, and authentication. +# +# Copyright (c) 2006-2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: connect.tcl,v 1.39 2008/03/27 15:15:26 matben Exp $ +# +############################# USAGE ############################################ +# +# jlib::connect::configure ?options? +# jlibname connect connect jid password ?options? (constructor) +# jlibname connect reset +# jlibname connect register jid password +# jlibname connect auth +# jlibname connect free (destructor) +# jlibname connect feature name +# +#### EXECUTION PATHS ########################################################### +# +# sections: callback status: +# +# o dns lookup (optional) dnsresolve +# o transport initnetwork +# o initialize xmpp stream initstream +# o start tls (optional) starttls +# o stream compression (untested) startcompress +# o sasl authentication (or digest or plain) authenticate +# o final ok | error +# +# error tokens: +# +# no-stream-id +# no-stream-version-1 +# network-failure +# tls-failure +# starttls-nofeature +# starttls-failure +# starttls-protocol-error +# sasl-no-mechanisms +# sasl-protocol-error +# +# All SASL error elements according to RFC 3920 (XMPP Core) +# not-authorized being the most common +# +# xmpp-streams-error +# +# And all stream error tags as defined in "4.7.3. Defined Conditions" +# in RFC 3920 (XMPP Core) as: +# xmpp-streams-error-TheTagName +# +### From: XEP-0170: Recommended Order of Stream Feature Negotiation ############ +# +# The XMPP RFCs define an ordering for the features defined therein, namely: +# 0. TLS +# 1. SASL +# 2. Resource binding +# 3. IM session establishment +# +# Using Stream Compression: +# 0. TLS +# 1. SASL +# 2. Stream compression +# 3. Resource binding +# 4. IM session establishment +# +################################################################################ +# +# @@@ Note to myself: maybe it would be a good idea to make this more OO +# like. jlib::connect returns a 'connector' object that is used as +# an instance for invoking the methods. We make sure that each jlib +# instance can make at most a single connector object at a time. +# Make sure that any connector object gets deleted from the jlib +# instance destructor. + +package require jlib +package require sha1 +package require autosocks ;# wrapper for the 'socket' command. +package require autoproxy ;# another wrapper for 'socket' + +package provide jlib::connect 0.1 + +namespace eval jlib::connect { + + variable inited 0 + variable have + variable debug 0 +} + +proc jlib::connect::init {jlibname} { + variable inited + + if {!$inited} { + init_static + } +} + +proc jlib::connect::cmdproc {jlibname cmd args} { + + # Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +proc jlib::connect::init_static {} { + variable inited + variable have + + debug "jlib::connect::init_static" + + # Loop through all packages we may need. + foreach name { + tls jlibsasl jlibtls + jlib::dns jlib::compress jlib::http + jlib::bind + } { + set have($name) 0 + if {![catch {package require $name}]} { + set have($name) 1 + } + } + + autoproxy::init + + # -method: ssl | tlssasl | sasl + # -transport tcp | http | tunnel + + # Default options. + variable options + array set options { + -command "" + -compress 0 + -defaulthttpurl http://%h:5280/http-poll/ + -defaultport 5222 + -defaultresource "default" + -defaultsslport 5223 + -digest 1 + -dnsprotocol udp + -dnssrv 1 + -dnstxthttp 1 + -dnstimeout 3000 + -http 0 + -httpurl "" + -ip "" + -method sasl + -minpollsecs 4 + -noauth 0 + -port "" + -saslthencomp 1 + -secure 0 + -timeout 30000 + -transport tcp + } + + # todo: + # -anonymous + set inited 1 +} + +# jlib::connect::filteroptions -- +# +# Filter an arbitrary -key value list to receive options that can +# typically be used by a client. + +proc jlib::connect::filteroptions {args} { + variable options + + set opts [list] + foreach {key value} $args { + if {$key eq "-command"} { continue } + if {[info exists options($key)]} { + lappend opts $key $value + } + } + return $opts +} + +# jlib::connect::configure -- +# +# + +proc jlib::connect::configure {args} { + variable have + variable options + + debug "jlib::connect::configure args=$args" + + if {[llength $args] == 0} { + return [array get options] + } else { + foreach {key value} $args { + switch -- $key { + -compress { + if {!$have(jlib::compress)} { + return -code error "missing jlib::compress package" + } + } + -http { + if {!$have(jlib::http)} { + return -code error "missing jlib::http package" + } + } + -method { + if {($value eq "ssl") && !$have(tls)} { + return -code error "missing tls package" + } elseif {($value eq "tlssasl") \ + && (!$have(jlibtls) || !$have(jlibsasl))} { + return -code error "missing jlibtls or jlibsasl package" + } elseif {($value eq "sasl") && !$have(jlibsasl)} { + return -code error "missing jlibsasl package" + } + } + -port { + if {![string is integer $state(-port)]} { + return -code error "the -port must be an integer" + } + } + } + set options($key) $value + } + } +} + +proc jlib::connect::get_state {jlibname {name ""}} { + upvar ${jlibname}::connect::state state + + if {$name eq ""} { + return [array get state] + } else { + if {[info exists state($name)]} { + return $state($name) + } else { + return "" + } + } +} + +# jlib::connect::connect -- +# +# Initiate the login process. +# +# Arguments: +# jid +# password +# cmd callback command +# args: +# -command tclProc +# -compress 0|1 +# -defaulthttpurl url +# -defaultport 5222 +# -defaultresource +# -defaultsslport 5223 +# -digest 0|1 +# -dnsprotocol tcp[udp +# -dnssrv 0|1 +# -dnstxthttp 0|1 +# -dnstimeout millisecs +# -http 0|1 +# -httpurl url +# -ip +# -secure 0|1 @@@ Change this to -xmpp ? +# -method ssl|tlssasl|sasl +# -noauth 0|1 +# -port +# -saslthencomp 0|1 This is the normal order for compression +# -timeout millisecs +# -transport tcp|http|tunnel +# +# o Note the naming convention for -method! +# ssl using direct tls socket connection +# it corresponds to the original jabber method +# tlssasl in stream tls negotiation + sasl, xmpp compliant +# XMPP requires sasl after starttls! +# sasl only sasl authentication +# +# o @@@ Perhaps a better way is to use a -xmpp switch that sets +# the main mode of operation, and then use whatever as sub switches. +# +# o The http proxy is configured from the http package. +# o The SOCKS proxy is configured from the autosocks package. +# +# Port priorites: +# 1) -port +# 2) DNS SRV resource record +# 3) -defaultport +# +# Results: +# jlibname + +proc jlib::connect::connect {jlibname jid password args} { + variable have + variable options + + debug "jlib::connect::connect jid=$jid, args=$args" + + # Instance specific namespace. + # 'state' only lives until connection finalized + # 'feature' lives until stream is closed + + namespace eval ${jlibname}::connect { + variable state + variable feature + } + upvar ${jlibname}::connect::state state + upvar ${jlibname}::connect::feature feature + + $jlibname register_reset [namespace code stream_reset] + + jlib::splitjidex $jid username server resource + + # Notes: + # o use "coccinella" as default resource + # o state(host) is the DNS SRV record or server if DNS failed + # o set one timeout on the complete sequence + + set state(jid) $jid + set state(username) $username + set state(server) $server + set state(host) $server + set state(resource) $resource + set state(password) $password + set state(args) $args + set state(error) "" + set state(state) "" + set state(httpurl) "" + set state(dns_srv) [list] ; # list of {host port} DNS TXT records + set state(dns_srv_idx) 0 ; # index of dns_srv currently tried + + foreach name {ssl tls sasl compress} { + set state(use$name) 0 + set feature($name) 0 + } + + # Default options. + array set state [array get options] + array set state $args + + if {$resource eq ""} { + set state(resource) $state(-defaultresource) + } + + # Verify that we have the necessary packages. + if {[catch {verify $jlibname} err]} { + return -code error $err + } + + if {$state(-http)} { + set state(-transport) http + } + if {$state(-secure)} { + switch -- $state(-method) { + sasl { + set state(usesasl) 1 + } + tlssasl { + set state(usesasl) 1 + set state(usetls) 1 + } + ssl { + set state(usessl) 1 + } + } + if {$state(-compress)} { + set state(usecompress) 1 + } + } + if {$state(-compress) && ($state(usetls) || $state(usessl))} { + #return -code error "connot have -compress and tls at the same time" + } + + # Any stream version. XMPP requires 1.0. + if {$state(usesasl) || $state(usetls) || $state(usecompress)} { + set state(version) 1.0 + } + + if {$state(-ip) ne ""} { + set state(host) $state(-ip) + } + + # Actual port to connect to (tcp). + # May be changed by DNS lookup unless -port set. + if {[string is integer -strict $state(-port)]} { + set state(port) $state(-port) + } else { + if {$state(usessl)} { + set state(port) $state(-defaultsslport) + } else { + set state(port) $state(-defaultport) + } + } + + # Schedule a timeout. + if {$state(-timeout) > 0} { + set state(after) [after $state(-timeout) \ + [list jlib::connect::timeout $jlibname]] + } + + # Start by doing a DNS lookup. + if {$state(-transport) eq "tcp" || $state(-transport) eq "tunnel"} { + + # Do not do a DNS SRV lookup if we have an explicit ip address. + if {!$state(-dnssrv) || ($state(-ip) ne "")} { + tcp_connect $jlibname + } else { + set state(state) dnsresolve + set cb [list jlib::connect::dns_srv_cb $jlibname] + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname dnsresolve + } + if {[catch { + set state(dnstoken) [jlib::dns::get_addr_port $server $cb \ + -protocol $state(-dnsprotocol) -timeout $state(-dnstimeout)] + } err]} { + # @@@ We should reset the jlib::dns here but it's buggy! + unset -nocomplain state(dnstoken) + tcp_connect $jlibname + } + } + } elseif {$state(-transport) eq "http"} { + + # Do not do a DNS TXT lookup if we have an explicit url address. + if {!$state(-dnstxthttp) || ($state(-httpurl) ne "")} { + set state(httpurl) $state(-httpurl) + http_init $jlibname + } else { + set state(state) dnsresolve + set cb [list jlib::connect::dns_http_cb $jlibname] + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname dnsresolve + } + if {[catch { + set state(dnstoken) [jlib::dns::get_http_poll_url $server $cb] + } err]} { + # @@@ We should reset the jlib::dns here but it's buggy! + unset -nocomplain state(dnstoken) + http_init $jlibname + } + } + } + jlib::set_async_error_handler $jlibname [namespace code async_error] + + return $jlibname +} + +proc jlib::connect::verify {jlibname} { + variable have + upvar ${jlibname}::connect::state state + + if {$state(-secure)} { + if {($state(-method) eq "sasl") && !$have(jlibsasl)} { + return -code error "missing jlibsasl package" + } + if {($state(-method) eq "ssl") && !$have(tls)} { + return -code error "missing tls package" + } + if {($state(-method) eq "tlssasl") \ + && (!$have(jlibtls) || !$have(jlibsasl))} { + return -code error "missing jlibtls or jlibsasl package" + } + } + if {$state(-compress) && !$have(jlib::compress)} { + return -code error "missing jlib::compress package" + } +} + +proc jlib::connect::async_error {jlibname err {msg ""}} { + upvar ${jlibname}::connect::state state + + finish $jlibname $err $msg +} + +# jlib::connect::dns_srv_cb -- +# +# This is our callback from the jlib::dns call. +# +# addrPort: {{soumar.jabbim.cz 5222} {nezmar.jabbim.cz 5222} ...} + +proc jlib::connect::dns_srv_cb {jlibname addrPort {err ""}} { + upvar ${jlibname}::connect::state state + + debug "jlib::connect::dns_srv_cb addrPort=$addrPort, err=$err" + + if {![info exists state(state)]} { + # We do not exist. dns::reset seems to be buggy! + return + } + + # dns doesn't seem to use the 'err' argument in this case. + set status [::dns::status $state(dnstoken)] + if {$status eq "reset"} { + return + } + + # We never let a failure stop us here. Use host as fallback. + if {$err eq ""} { + set state(host) [lindex $addrPort 0 0] + set state(port) [lindex $addrPort 0 1] + + # Collect multiple DNS TXT record responses so we may try them in order. + set state(dns_srv) $addrPort + set state(dns_srv_idx) 0 + + # Try ad-hoc method for port number for ssl connections (5223). + if {$state(usessl)} { + incr state(port) + } + } + + # If -port set this always takes precedence. + if {[string is integer -strict $state(-port)]} { + set state(port) $state(-port) + } + unset -nocomplain state(dnstoken) + tcp_connect $jlibname +} + +proc jlib::connect::dns_http_cb {jlibname url {err ""}} { + upvar ${jlibname}::connect::state state + + debug "jlib::connect::dns_http_cb url=$url, err=$err" + + if {![info exists state(state)]} { + # We do not exist. dns::reset seems to be buggy! + return + } + + # dns doesn't seem to use the 'err' argument in this case. + set status [::dns::status $state(dnstoken)] + if {$status eq "reset"} { + return + } + unset -nocomplain state(dnstoken) + if {$err eq ""} { + set state(httpurl) $url + } + + # If -httpurl set this always takes precedence. + if {$state(-httpurl) ne ""} { + set state(httpurl) $state(-httpurl) + } + http_init $jlibname +} + +proc jlib::connect::http_init {jlibname} { + upvar ${jlibname}::connect::state state + + debug "jlib::connect::http_init" + + if {$state(httpurl) eq ""} { + set state(httpurl) \ + [string map [list "%h" $state(server)] $state(-defaulthttpurl)] + } + jlib::http::new $jlibname $state(httpurl) + init_stream $jlibname +} + +proc jlib::connect::tunnel_connect {jlibname} { + upvar ${jlibname}::connect::state state + + debug "jlib::connect::tunnel_connect $state(host) $state(port)" + + set state(state) initnetwork + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname initnetwork + } + if {[catch { + set state(sock) [autoproxy::tunnel_connect $state(host) $state(port)] + tcp_writable $jlibname + } err]} { + puts stderr $::errorInfo + finish $jlibname network-failure $err + } +} + +# jlib::connect::tcp_connect -- +# +# Try make a TCP connection to state(host/port). + +proc jlib::connect::tcp_connect {jlibname} { + upvar ${jlibname}::connect::state state + + if {$state(-transport) eq "tunnel"} { + return [tunnel_connect $jlibname] + } + + debug "jlib::connect::tcp_connect $state(host) $state(port)" + + set state(state) initnetwork + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname initnetwork + } + if {[catch { + set state(sock) [autosocks::socket $state(host) $state(port) \ + -command [list jlib::connect::tcp_cb $jlibname]] + } err]} { + tcp_cb $jlibname network-failure + } +} + +proc jlib::connect::tcp_cb {jlibname status} { + upvar ${jlibname}::connect::state state + + debug "jlib::connect::tcp_cb status=$status" + + # If we have multiple DNS TXT records try them in order. + if {$status eq "ok"} { + tcp_writable $jlibname + } else { + set len [llength $state(dns_srv)] + set idx $state(dns_srv_idx) + if {$len && ($idx < [expr {$len-1}])} { + incr idx + set state(dns_srv_idx) $idx + set state(host) [lindex $state(dns_srv) $idx 0] + set state(port) [lindex $state(dns_srv) $idx 1] + + # If -port set this always takes precedence. + if {[string is integer -strict $state(-port)]} { + set state(port) $state(-port) + } + tcp_connect $jlibname + } else { + finish $jlibname network-failure + } + } +} + +proc jlib::connect::socks_cb {jlibname status} { + + debug "jlib::connect::socks_cb status=$status" + + if {$status eq "ok"} { + tcp_writable $jlibname + } else { + finish $jlibname proxy-failure $status + } +} + +proc jlib::connect::tcp_writable {jlibname} { + upvar ${jlibname}::connect::state state + + debug "jlib::connect::tcp_writable" + + if {![info exists state(sock)]} { + return + } + set sock $state(sock) + fileevent $sock writable {} + + if {[catch {eof $sock} iseof] || $iseof} { + finish $jlibname network-failure "connection eof" + return + } + + # Check if something went wrong first. + if {[catch {fconfigure $sock -sockname} sockname]} { + finish $jlibname network-failure $sockname + return + } + + # Configure socket. + fconfigure $sock -buffering line -blocking 0 + catch {fconfigure $sock -encoding utf-8} + + $jlibname setsockettransport $sock + + # Do SSL handshake. See jlib::tls_handshake for a better way! + if {$state(usessl)} { + + # Make it a SSL connection. + if {[catch { + tls::import $sock -cafile "" -certfile "" -keyfile "" \ + -request 1 -server 0 -require 0 -ssl2 no -ssl3 yes -tls1 yes + } err]} { + close $sock + finish $jlibname tls-failure $err + return + } + set retry 0 + + # Do SSL handshake. + while {1} { + if {$retry > 100} { + close $sock + set err "too long retry to setup SSL connection" + finish $jlibname tls-failure $err + return + } + if {[catch {tls::handshake $sock} err]} { + if {[string match "*resource temporarily unavailable*" $err]} { + after 50 + incr retry + } else { + close $sock + finish $jlibname tls-failure $err + return + } + } else { + break + } + } + fconfigure $sock -blocking 0 -encoding utf-8 + } + + # Send the init stream xml command. + init_stream $jlibname +} + +proc jlib::connect::init_stream {jlibname} { + upvar ${jlibname}::connect::state state + + debug "jlib::connect::init_stream" + + set state(state) initstream + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname initstream + } + + set opts [list] + if {[info exists state(version)]} { + lappend opts -version $state(version) + } + + # Initiate a new stream. We should wait for the server . + # openstream may throw error. + if {[catch { + eval {$jlibname openstream $state(server) \ + -cmd [list jlib::connect::init_stream_cb]} $opts + } err]} { + finish $jlibname network-failure $err + return + } +} + +proc jlib::connect::init_stream_cb {jlibname args} { + upvar ${jlibname}::connect::state state + + if {![info exists state]} return + + debug "jlib::connect::init_stream_cb args=$args" + + array set argsA $args + + # We require an 'id' attribute. + if {![info exists argsA(id)]} { + finish $jlibname no-stream-id + return + } + set state(streamid) $argsA(id) + + # If we are trying to use sasl or tls indicated by version='1.0' + # we must also be sure to receive a version attribute larger or + # equal to 1.0. + set version1 0 + if {[info exists argsA(version)]} { + set state(streamversion) $argsA(version) + if {[package vcompare $argsA(version) 1.0] >= 0} { + set version1 1 + } + } + if {$state(usesasl) || $state(usetls)} { + if {!$version1} { + finish $jlibname no-stream-version-1 + return + } + } + + # This XEP is superseeded by XEP-0170 + # XEP-0138: Stream Compression: + # If both TLS (whether including TLS compression or not) and stream + # compression are used, then TLS MUST be negotiated first, followed by + # negotiation of stream compression. + + if {$state(usetls)} { + set state(state) starttls + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname starttls + } + $jlibname starttls jlib::connect::starttls_cb + + # This is the order ejabberd expects, compression before sasl. + } elseif {!$state(-saslthencomp) && $state(usecompress)} { + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname startcompress + } + jlib::compress::start $jlibname [namespace code compress_cb] + } elseif {$state(-noauth)} { + finish $jlibname + } else { + auth $jlibname + } +} + +proc jlib::connect::starttls_cb {jlibname type args} { + upvar ${jlibname}::connect::state state + + if {![info exists state]} return + + debug "jlib::connect::starttls_cb type=$type, args=$args" + + if {$type eq "error"} { + foreach {errcode errmsg} [lindex $args 0] break + finish $jlibname $errcode $errmsg + } else { + + # We have a new stream. XMPP Core: + # 12. If the TLS negotiation is successful, the initiating entity + # MUST continue with SASL negotiation. + set state(streamid) [$jlibname getstreamattr id] + if {$state(-noauth)} { + finish $jlibname + } else { + auth $jlibname + } + } +} + +# jlib::connect::register -- +# +# Typically used after registered a new account since JID and password +# not known until registration succesful. + +proc jlib::connect::register {jlibname jid password} { + upvar ${jlibname}::connect::state state + + jlib::splitjidex $jid username server resource + + set state(jid) $jid + set state(username) $username + set state(password) $password + if {$resource eq ""} { + set state(resource) $state(-defaultresource) + } +} + +# jlib::connect::auth -- +# +# Initiates the authentication process using an existing connect instance, +# typically when started using -noauth. +# The user can modify the options from the initial ones. + +proc jlib::connect::auth {jlibname args} { + upvar ${jlibname}::connect::state state + + debug "jlib::connect::auth" + + array set state $args + + if {[catch {verify $jlibname} err]} { + return -code error $err + } + set state(state) authenticate + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname authenticate + } + + set username $state(username) + set password $state(password) + set resource $state(resource) + + if {$state(usesasl)} { + $jlibname auth_sasl $username $resource $password \ + [namespace code auth_cb] + } elseif {$state(-digest)} { + set digested [::sha1::sha1 $state(streamid)$password] + $jlibname send_auth $username $resource \ + [namespace code auth_cb] -digest $digested + } else { + + # Plain password authentication. + $jlibname send_auth $username $resource \ + [namespace code auth_cb] -password $password + } +} + +proc jlib::connect::auth_cb {jlibname type queryE} { + upvar ${jlibname}::connect::state state + + if {![info exists state]} return + + debug "jlib::connect::auth_cb type=$type, queryE=$queryE" + + if {$type eq "error"} { + lassign $queryE errcode errmsg + finish $jlibname $errcode $errmsg + } else { + + # We have a new stream. + set state(streamid) [$jlibname getstreamattr id] + if {$state(-saslthencomp) && $state(usecompress)} { + if {$state(-command) ne {}} { + uplevel #0 $state(-command) $jlibname startcompress + } + jlib::compress::start $jlibname [namespace code compress_cb] + } elseif {$state(usesasl)} { + jlib::bind::resource $jlibname $state(resource) [namespace code bind_cb] + } else { + finish $jlibname + } + } +} + +proc jlib::connect::compress_cb {jlibname {errcode ""} {errmsg ""}} { + upvar ${jlibname}::connect::state state + + if {![info exists state]} return + + debug "jlib::connect::compress_cb" + + # Note: Failure of compression setup SHOULD NOT be treated as an + # unrecoverable error and therefore SHOULD NOT result in a stream error. + if {$errcode ne ""} { + finish $jlibname $errcode $errmsg + return + } + + # We have a new stream. + set state(streamid) [$jlibname getstreamattr id] + if {$state(-saslthencomp)} { + jlib::bind::resource $jlibname $state(resource) [namespace code bind_cb] + } else { + + # If we have taken compression before SASL then go back. + if {$state(-noauth)} { + finish $jlibname + } else { + auth $jlibname + } + } +} + +proc jlib::connect::bind_cb {jlibname type queryE} { + + debug "jlib::connect::bind_cb" + + if {$type eq "error"} { + lassign $queryE errcode errmsg + finish $jlibname $errcode $errmsg + } else { + finish $jlibname + } +} + +# jlib::connect::reset -- +# +# This is kills any ongoing or nonexisting connect object. + +proc jlib::connect::reset {jlibname} { + + debug "jlib::connect::reset" + + if {[jlib::havesasl]} { + $jlibname sasl_reset + } + if {[jlib::havetls]} { + $jlibname tls_reset + } + if {[namespace exists ${jlibname}::connect]} { + finish $jlibname reset + } +} + +proc jlib::connect::timeout {jlibname} { + + if {[jlib::havesasl]} { + $jlibname sasl_reset + } + if {[jlib::havetls]} { + $jlibname tls_reset + } + finish $jlibname timeout +} + +# jlib::connect::finish -- +# +# Finalize the complete sequence, with or without any errors. +# +# Arguments: +# errcode: one word error code, empty if ok +# errmsg: an additional arbitrary error message with details that +# typically gets reported by some component +# +# Results: +# Callback made. + +proc jlib::connect::finish {jlibname {errcode ""} {errmsg ""}} { + upvar ${jlibname}::connect::state state + upvar ${jlibname}::connect::feature feature + + debug "jlib::connect::finish errcode=$errcode, errmsg=$errmsg" + + jlib::set_async_error_handler $jlibname + + if {![info exists state(state)]} { + # We do not exist. + return + } + if {[info exists state(after)]} { + after cancel $state(after) + } + if {[info exists state(dnstoken)]} { + jlib::dns::reset $state(dnstoken) + } + if {$state(error) ne ""} { + set errcode $state(error) + } + if {$errcode ne ""} { + set status error + + # We can be called before the socket has been registered with jlib. + if {[info exists state(sock)]} { + catch {close $state(sock)} + } + + # This 'kills' the connection. Needed for both tcp and http! + # after idle seems necessary when resetting xml parser from callback + #after idle [list $jlibname closestream] + $jlibname kill + } else { + set status ok + + # Copy the state(use*) to feature(*) + foreach name {ssl tls sasl compress} { + set feature($name) $state(use$name) + } + + } + + # Here status must be either 'ok' or 'error'. + if {$state(-command) ne {}} { + if {$errcode eq ""} { + uplevel #0 $state(-command) [list $jlibname $status] + } else { + uplevel #0 $state(-command) [list $jlibname $status $errcode $errmsg] + } + } +} + +proc jlib::connect::feature {jlibname name} { + upvar ${jlibname}::connect::feature feature + + if {[info exists feature($name)]} { + return $feature($name) + } else { + return 0 + } +} + +proc jlib::connect::free {jlibname} { + + debug "jlib::connect::free" + if {[namespace exists ${jlibname}::connect]} { + upvar ${jlibname}::connect::state state + unset -nocomplain state + } +} + +proc jlib::connect::stream_reset {jlibname} { + upvar ${jlibname}::connect::feature feature + debug "jlib::connect::stream_reset" + unset -nocomplain feature +} + +proc jlib::connect::debug {str} { + variable debug + + if {$debug} { + puts $str + } +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::connect { + + jlib::ensamble_register connect \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +# Tests +if {0} { + package require jlib::connect + proc cb {args} { + puts "---> $args" + #puts [jlib::connect::get_state ::jlib::jlib1] + } + set pw xxx + ::jlib::jlib1 connect connect matben@localhost $pw -command cb + ::jlib::jlib1 connect connect matben@devrieze.dyndns.org $pw \ + -command cb -secure 1 -method tlssasl + + ::jlib::jlib1 connect connect matben@sgi.se xxx -command cb \ + -http 1 -httpurl http://sgi.se:5280/http-poll/ + + ::jlib::jlib1 connect connect openfire.matben@sgi.se $pw \ + -command cb -compress 1 -secure 1 -method sasl + + ::jlib::jlib1 connect connect matben@jabber.ru $pw \ + -command cb -compress 1 -secure 1 -method sasl + + jlib::jlib1 closestream +} + +#------------------------------------------------------------------------------- + diff --git a/lib/jabberlib/data.tcl b/lib/jabberlib/data.tcl new file mode 100644 index 0000000..f1acb14 --- /dev/null +++ b/lib/jabberlib/data.tcl @@ -0,0 +1,105 @@ +# data.tcl -- +# +# This file is part of the jabberlib. It contains support code +# for XEP-0231: Data Element +# +# Copyright (c) 2008 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: data.tcl,v 1.1 2008/05/30 14:21:02 matben Exp $ +# +############################# USAGE ############################################ +# +# INSTANCE COMMANDS +# jlibName data create +# +################################################################################ + +package require jlib +package require base64 ; # tcllib + +package provide jlib::data 0.1 + +namespace eval jlib::data { + + # Common xml namespaces. + variable xmlns + array set xmlns { + data "urn:xmpp:tmp:data-element" + } +} + +# jlib::data::init -- +# +# Creates a new instance of the data object. + +proc jlib::data::init {jlibname} { + variable xmlns + + # Instance specifics arrays. + namespace eval ${jlibname}::data { + variable cache + } + + # Register some standard iq handlers that are handled internally. + $jlibname iq_register get $xmlns(data) [namespace code iq_handler] +} + +proc jlib::data::cmdproc {jlibname cmd args} { + return [eval {$cmd $jlibname} $args] +} + +proc jlib::data::element {type data args} { + variable xmlns + upvar ${jlibname}::data::cache cache + + set attrL [list xmlns $xmlns(data)] + foreach {key value} $args { + -alt - -cid { + set name [string trimleft $key -] + set $name $value + lappend attrL $name $value + } + } + set dataE [wrapper::createtag data \ + -attrlist $attrL -chdata [::base64::encode $data]] + if {[info exists cid]} { + set cache($cid) $dataE + } + return $dataE +} + +proc jlib::data::iq_handler {jlibname from dataE args} { + upvar ${jlibname}::data::cache cache + + array set argsA $args + if {![info exists argsA(id)]} { + return 0 + } + set cid [wrapper::getattribute $dataE cid] + if {![info exists cache($cid)]} { + # Should be + return 0 + } + + $jlibname send_iq result $cache($cid) -to $from -id $id + return 1 +} + +# We have to do it here since need the initProc before doing this. +namespace eval jlib::data { + + jlib::ensamble_register data \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +# Test: +if {0} { + package require jlib::data + set jlibname ::jlib::jlib1 + + +} + diff --git a/lib/jabberlib/disco.tcl b/lib/jabberlib/disco.tcl new file mode 100644 index 0000000..c7ed5b1 --- /dev/null +++ b/lib/jabberlib/disco.tcl @@ -0,0 +1,978 @@ +# disco.tcl -- +# +# This file is part of the jabberlib. +# +# Copyright (c) 2004-2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: disco.tcl,v 1.57 2008/06/11 08:12:05 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# disco - convenience command library for the disco part of XMPP. +# +# SYNOPSIS +# jlib::disco::init jlibName ?-opt value ...? +# +# OPTIONS +# -command tclProc +# +# INSTANCE COMMANDS +# jlibname disco children jid +# jlibname disco childs jid ?node? +# jlibname disco send_get discotype jid cmd ?-opt value ...? +# jlibname disco isdiscoed discotype jid ?node? +# jlibname disco get discotype key jid ?node? +# jlibname disco getallcategories pattern +# jlibname disco get_async discotype jid cmd ?-node node? +# jlibname disco getconferences +# jlibname disco getjidsforcategory pattern +# jlibname disco getjidsforfeature feature +# jlibname disco getxml jid ?node? +# jlibname disco features jid ?node? +# jlibname disco hasfeature feature jid ?node? +# jlibname disco isroom jid +# jlibname disco iscategorytype category/type jid ?node? +# jlibname disco name jid ?node? +# jlibname disco nodes jid ?node? +# jlibname disco types jid ?node? +# jlibname disco reset ?jid ?node?? +# +# where discotype = (items|info) +# +################################################################################ +# +# Structures: +# items(jid,node,children) list of any children JIDs +# items(jid,node,childs) list of {JID node} +# +# jid must always be nonempty while node may be empty. +# +# rooms(jid,node) exists if children of 'conference' + +# NEW: In order to manage the complex jid/node structure it is best to +# keep an internal structure always using a pair JID+node. +# As array index: ($jid,$node,..) or list of childs: +# {{JID1 node1} {JID2 node2} ..} where any of JID or node can be +# empty but not both. +# +# This reflects the disco xml structure (node can be empty): +# +# JID node +# JID node +# JID node +# ... +# +# @@@ While 'parent -> child' is uniquely defined 'parent <- child' is NOT! +# A certain JID+node can appear in more than one place in the disco tree! +# It is better to use another data structure to store this. + +package require jlib + +package provide jlib::disco 0.1 + +namespace eval jlib::disco { + + # Globals same for all instances of this jlib. + variable debug 0 + if {[info exists ::debugLevel] && ($::debugLevel > 1) && ($debug == 0)} { + set debug 2 + } + + variable version 0.1 + + # Common xml namespaces. + variable xmlns + array set xmlns { + disco "http://jabber.org/protocol/disco" + items "http://jabber.org/protocol/disco#items" + info "http://jabber.org/protocol/disco#info" + muc "http://jabber.org/protocol/muc" + } + + # Components register their feature elements for disco/info. + variable features [list] + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::disco::init -- +# +# Creates a new instance of the disco object. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# args: +# +# Results: +# namespaced instance command + +proc jlib::disco::init {jlibname args} { + + variable xmlns + + # Instance specific arrays. + namespace eval ${jlibname}::disco { + variable items + variable info + variable rooms + variable handler + variable state + variable identities [list] + } + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + upvar ${jlibname}::disco::rooms rooms + + # Register service. + $jlibname service register disco disco + + # Register some standard iq handlers that is handled internally. + $jlibname iq_register get $xmlns(items) \ + [list [namespace current]::handle_get items] + $jlibname iq_register get $xmlns(info) \ + [list [namespace current]::handle_get info] + + # Clear any cache info we may have collected since likely invalid offline. + $jlibname presence_register_int unavailable [namespace current]::unavail_cb + + # Register our own features. + registerfeature $xmlns(disco) + registerfeature $xmlns(items) + registerfeature $xmlns(info) + + set info(conferences) [list] + + return +} + +# jlib::disco::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# cmd: +# args: all args to the cmd procedure. +# +# Results: +# none. + +proc jlib::disco::cmdproc {jlibname cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +# jlib::disco::registerfeature -- +# +# @@@ Make instance specific instead! +# +# Components register their feature elements for disco#info. +# Clients must handle this using the disco handler. +# NB1: This is only for 'basic' features not associated with a caps ext +# token. Those are handled by jlib::caps::register. +# NB2: We consider everything inside jlib to be 'basic' but also client +# level features can be basic. +# NB3: Features registered here MUST NEVER change within a certain version. + +proc jlib::disco::registerfeature {feature} { + variable features + + lappend features $feature + set features [lsort -unique $features] +} + +proc jlib::disco::getregisteredfeatures {} { + variable features + + return $features +} + +# jlib::disco::registeridentity -- +# +# +# as 'category type ?name?' + +proc jlib::disco::registeridentity {jlibname category type {name ""}} { + upvar ${jlibname}::identities identities + + lappend identities [list $category $type $name] +} + +proc jlib::disco::getidentities {jlibname} { + upvar ${jlibname}::identities identities + + return $identities +} + +# jlib::disco::registerhandler -- +# +# Register handler to deliver incoming disco queries. + +proc jlib::disco::registerhandler {jlibname cmdProc} { + + upvar ${jlibname}::disco::handler handler + + set handler $cmdProc +} + +# jlib::disco::send_get -- +# +# Sends a get request within the disco namespace. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# type: items|info +# jid: to jid +# cmd: callback tcl proc +# args: -node chdata +# +# Results: +# none. + +proc jlib::disco::send_get {jlibname type jid cmd args} { + + variable xmlns + upvar ${jlibname}::disco::state state + + set jid [jlib::jidmap $jid] + set node "" + set opts [list] + if {[set idx [lsearch -exact $args -node]] >= 0} { + set node [lindex $args [incr idx]] + set opts [list -node $node] + } + set state(pending,$type,$jid,$node) 1 + + eval {$jlibname iq_get $xmlns($type) -to $jid \ + -command [list [namespace current]::send_get_cb $type $jid $cmd]} $opts +} + +# jlib::disco::get_async -- +# +# Do disco async using 'cmd' callback. +# If cached it is returned directly using 'cmd', if pending the cmd +# is invoked when getting result, else we do a send_get. + +proc jlib::disco::get_async {jlibname type jid cmd args} { + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + upvar ${jlibname}::disco::state state + + set jid [jlib::jidmap $jid] + set node "" + set opts [list] + if {[set idx [lsearch -exact $args -node]] >= 0} { + set node [lindex $args [incr idx]] + set opts [list -node $node] + } + set var ${type}($jid,$node,xml) + if {[info exists $var]} { + set xml [set $var] + set etype [wrapper::getattribute $xml type] + + # Errors are reported specially! + # @@@ BAD!!! + if {$etype eq "error"} { + set xml [lindex [wrapper::getchildren $xml] 0] + } + uplevel #0 $cmd [list $jlibname $etype $jid $xml] + } elseif {[info exists state(pending,$type,$jid,$node)]} { + lappend state(invoke,$type,$jid,$node) $cmd + } else { + eval {send_get $jlibname $type $jid $cmd} $opts + } + return +} + +# jlib::disco::send_get_cb -- +# +# Fills in the internal state arrays, and invokes any callback. + +proc jlib::disco::send_get_cb {ditype from cmd jlibname type queryE args} { + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + upvar ${jlibname}::disco::state state + + # We need to use both jid and any node for addressing since + # each item may have identical jid's but different node's. + + # Do STRINGPREP. + set from [jlib::jidmap $from] + set node [wrapper::getattribute $queryE "node"] + + unset -nocomplain state(pending,$ditype,$from,$node) + + if {[string equal $type "error"]} { + + # Cache xml for later retrieval. + set var ${ditype}($from,$node,xml) + set $var [eval {getfulliq $type $queryE} $args] + } else { + switch -- $ditype { + items { + parse_get_items $jlibname $from $queryE + } + info { + parse_get_info $jlibname $from $queryE + } + } + } + invoke_stacked $jlibname $ditype $type $from $queryE + + # Invoke callback for this get. + uplevel #0 $cmd [list $jlibname $type $from $queryE] $args +} + +proc jlib::disco::invoke_stacked {jlibname ditype type jid queryE} { + + upvar ${jlibname}::disco::state state + + set node [wrapper::getattribute $queryE "node"] + if {[info exists state(invoke,$ditype,$jid,$node)]} { + foreach cmd $state(invoke,$ditype,$jid,$node) { + uplevel #0 $cmd [list $jlibname $type $jid $queryE] + } + unset -nocomplain state(invoke,$ditype,$jid,$node) + } +} + +proc jlib::disco::getfulliq {type queryE args} { + + # Errors are reported specially! + # @@@ BAD!!! + # If error queryE is just a two element list {errtag text} + set attr [list type $type] + foreach {key value} $args { + lappend attr [string trimleft $key "-"] $value + } + return [wrapper::createtag iq -attrlist $attr -subtags [list $queryE]] +} + +# jlib::disco::parse_get_items -- +# +# Fills the internal records with this disco items query result. +# There are four parent-childs combinations: +# +# (0) JID1 +# JID JID1 != JID +# +# (1) JID1 +# JID1+node JID equal +# +# (2) JID1+node1 +# JID JID1 != JID +# +# (3) JID1+node1 +# JID+node JID1 != JID +# +# Typical xml: +# +# +# +# ... +# +# Any of the following scenarios is perfectly acceptable: +# +# (0) Upon querying an entity (JID1) for items, one receives a list of items +# that can be addressed as JIDs; each associated item has its own JID, +# but no such JID equals JID1. +# +# (1) Upon querying an entity (JID1) for items, one receives a list of items +# that cannot be addressed as JIDs; each associated item has its own +# JID+node, where each JID equals JID1 and each NodeID is unique. +# +# (2) Upon querying an entity (JID1+NodeID1) for items, one receives a list +# of items that can be addressed as JIDs; each associated item has its +# own JID, but no such JID equals JID1. +# +# (3) Upon querying an entity (JID1+NodeID1) for items, one receives a list +# of items that cannot be addressed as JIDs; each associated item has +# its own JID+node, but no such JID equals JID1 and each NodeID is +# unique in the context of the associated JID. +# +# In addition, the results MAY also be mixed, so that a query to a JID or a +# JID+node could yield both (1) items that are addressed as JIDs and (2) +# items that are addressed as JID+node combinations. + +proc jlib::disco::parse_get_items {jlibname from queryE} { + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + upvar ${jlibname}::disco::rooms rooms + + # Parents node if any. + set pnode [wrapper::getattribute $queryE "node"] + set pitem [list $from $pnode] + + set items($from,$pnode,xml) [getfulliq result $queryE -from $from] + unset -nocomplain items($from,$pnode,children) items($from,$pnode,nodes) + unset -nocomplain items($from,$pnode,childs) + + # This is perhaps not a robust way. + if {0} { + if {![info exists items($from,parent)]} { + set items($from,parent) [list] + set items($from,parents) [list] + } + if {![info exists items($from,$pnode,parent2)]} { + set items($from,$pnode,parent2) [list] + set items($from,$pnode,parents2) [list] + } + } + if {![info exists items($from,$pnode,paL)]} { + set items($from,$pnode,paL) [list] + } + + # Cache children of category='conference' as rooms. + if {[lsearch -exact $info(conferences) $from] >= 0} { + set isrooms 1 + } else { + set isrooms 0 + } + + foreach c [wrapper::getchildren $queryE] { + if {![string equal [wrapper::gettag $c] "item"]} { + continue + } + unset -nocomplain attr + array set attr [wrapper::getattrlist $c] + + # jid is a required attribute! + set jid [jlib::jidmap $attr(jid)] + set node "" + + # Children---> + # Only 'childs' gives the full picture. + if {$jid ne $from} { + lappend items($from,$pnode,children) $jid + } + if {[info exists attr(node)]} { + + # Not two nodes of a jid may be identical. Beware for infinite loops! + # We only do some rudimentary check. + set node $attr(node) + if {[string equal $pnode $node]} { + continue + } + lappend items($from,$pnode,nodes) $node + } + lappend items($from,$pnode,childs) [list $jid $node] + + # Parents---> + + # Keep list of parents since not unique. + lappend items($jid,$node,paL) $pitem + + # Cache the optional attributes. + # Any {jid node} must have identical attributes and childrens. + foreach key {name action} { + if {[info exists attr($key)]} { + set items($jid,$node,$key) $attr($key) + } + } + if {$isrooms} { + set rooms($jid,$node) 1 + } + } +} + +# jlib::disco::parse_get_info -- +# +# Fills the internal records with this disco info query result. + +proc jlib::disco::parse_get_info {jlibname from queryE} { + variable xmlns + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + upvar ${jlibname}::disco::rooms rooms + + set node [wrapper::getattribute $queryE "node"] + + array unset info [jlib::ESC $from],[jlib::ESC $node],* + set info($from,$node,xml) [getfulliq result $queryE -from $from] + set isconference 0 + + foreach c [wrapper::getchildren $queryE] { + unset -nocomplain attr + array set attr [wrapper::getattrlist $c] + + # There can be one or many of each 'identity' and 'feature'. + switch -- [wrapper::gettag $c] { + identity { + + # Each element MUST possess 'category' and + # 'type' attributes. (category/type) + # Each identity element SHOULD have the same name value. + # + # XEP 0030: + # If the hierarchy category is used, every node in the + # hierarchy MUST be identified as either a branch or a leaf; + # however, since a node MAY have multiple identities, any given + # node MAY also possess an identity other than + # "hierarchy/branch" or "hierarchy/leaf". + + # Protect for entities which don't follow the rules. + if {![info exists attr(category)] || ![info exists attr(type)]} { + continue + } + set category [string tolower $attr(category)] + set ctype [string tolower $attr(type)] + set name "" + if {[info exists attr(name)]} { + set name $attr(name) + } + set info($from,$node,name) $name + set cattype $category/$ctype + lappend info($from,$node,cattypes) $cattype + lappend info($cattype,typelist) $from + set info($cattype,typelist) \ + [lsort -unique $info($cattype,typelist)] + + if {![string match *@* $from]} { + + switch -- $category { + conference { + lappend info(conferences) $from + set isconference 1 + } + } + } + } + feature { + set feature $attr(var) + lappend info($from,$node,features) $feature + lappend info($feature,featurelist) $from + + # Register any groupchat protocol with jlib. + # Note that each room also returns gc features; skip! + if {![string match *@* $from]} { + + switch -- $feature { + "http://jabber.org/protocol/muc" { + $jlibname service registergcprotocol $from "muc" + } + "gc-1.0" { + $jlibname service registergcprotocol $from "gc-1.0" + } + } + } + } + } + } + + # If this is a conference be sure to cache any children as rooms. + if {$isconference && [info exists items($from,,children)]} { + foreach c $items($from,,children) { + set rooms($c,) 1 + } + } +} + +proc jlib::disco::isdiscoed {jlibname discotype jid {node ""}} { + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + + set jid [jlib::jidmap $jid] + + switch -- $discotype { + items { + return [info exists items($jid,$node,xml)] + } + info { + return [info exists info($jid,$node,xml)] + } + } +} + +proc jlib::disco::getxml {jlibname discotype jid {node ""}} { + return [get $jlibname $discotype xml $jid $node] +} + +proc jlib::disco::get {jlibname discotype key jid {node ""}} { + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + + set jid [jlib::jidmap $jid] + + switch -- $discotype { + items { + if {[info exists items($jid,$node,$key)]} { + return $items($jid,$node,$key) + } + } + info { + if {[info exists info($jid,$node,$key)]} { + return $info($jid,$node,$key) + } + } + } + return +} + +# Both the items and the info elements may have name attributes! Related??? + +# The login servers jid name attribute is not returned via any items +# element; only via info/identity element. +# + +proc jlib::disco::name {jlibname jid {node ""}} { + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + + set jid [jlib::jidmap $jid] + if {[info exists items($jid,$node,name)]} { + return $items($jid,$node,name) + } elseif {[info exists info($jid,$node,name)]} { + return $info($jid,$node,name) + } else { + return + } +} + +# jlib::disco::features -- +# +# Returns the var attributes of all feature elements for this jid/node. + +proc jlib::disco::features {jlibname jid {node ""}} { + + upvar ${jlibname}::disco::info info + + set jid [jlib::jidmap $jid] + if {[info exists info($jid,$node,features)]} { + return $info($jid,$node,features) + } else { + return + } +} + +# jlib::disco::hasfeature -- +# +# Returns 1 if the jid/node has the specified feature var. + +proc jlib::disco::hasfeature {jlibname feature jid {node ""}} { + + upvar ${jlibname}::disco::info info + + set jid [jlib::jidmap $jid] + if {[info exists info($jid,$node,features)]} { + set features $info($jid,$node,features) + return [expr [lsearch -exact $features $feature] < 0 ? 0 : 1] + } else { + return 0 + } +} + +# jlib::disco::types -- +# +# Returns a list of all category/types of this jid/node. + +proc jlib::disco::types {jlibname jid {node ""}} { + + upvar ${jlibname}::disco::info info + + set jid [jlib::jidmap $jid] + if {[info exists info($jid,$node,cattypes)]} { + return $info($jid,$node,cattypes) + } else { + return + } +} + +# jlib::disco::iscategorytype -- +# +# Search for any matching feature var glob pattern. + +proc jlib::disco::iscategorytype {jlibname cattype jid {node ""}} { + + upvar ${jlibname}::disco::info info + + set jid [jlib::jidmap $jid] + if {[info exists info($jid,$node,cattypes)]} { + set types $info($jid,$node,cattypes) + return [expr [lsearch -glob $types $cattype] < 0 ? 0 : 1] + } else { + return 0 + } +} + +# jlib::disco::getjidsforfeature -- +# +# Returns a list of all jids that support the specified feature. + +proc jlib::disco::getjidsforfeature {jlibname feature} { + + upvar ${jlibname}::disco::info info + + if {[info exists info($feature,featurelist)]} { + set info($feature,featurelist) [lsort -unique $info($feature,featurelist)] + return $info($feature,featurelist) + } else { + return + } +} + +# jlib::disco::getjidsforcategory -- +# +# Returns all jids that match the glob pattern category/type. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# pattern: a global pattern of jid type/subtype (gateway/*). +# +# Results: +# List of jid's matching the type pattern. nodes??? + +proc jlib::disco::getjidsforcategory {jlibname pattern} { + + upvar ${jlibname}::disco::info info + + set jidL [list] + foreach {key jids} [array get info "$pattern,typelist"] { + set jidL [concat $jidL $jids] + } + return $jidL +} + +# jlib::disco::getallcategories -- +# +# Returns all categories that match the glob pattern catpattern. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# pattern: a global pattern of jid type/subtype (gateway/*). +# +# Results: +# List of types matching the category/type pattern. + +proc jlib::disco::getallcategories {jlibname pattern} { + + upvar ${jlibname}::disco::info info + + set cattypes [list] + foreach {key jids} [array get info "$pattern,typelist"] { + lappend cattypes [string map {,typelist ""} $key] + } + return [lsort -unique $cattypes] +} + +proc jlib::disco::getconferences {jlibname} { + + upvar ${jlibname}::disco::info info + + return [lsort -unique $info(conferences)] +} + +# jlib::disco::isroom -- +# +# Room or not? The problem is that some components, notably some +# msn gateways, have multiple categories, gateway and conference. BAD! +# We therefore use a specific 'rooms' array. + +proc jlib::disco::isroom {jlibname jid} { + + upvar ${jlibname}::disco::rooms rooms + + if {[info exists rooms($jid,)]} { + return 1 + } else { + return 0 + } +} + +# jlib::disco::children -- +# +# Returns a list of all child jids of this jid. + +proc jlib::disco::children {jlibname jid} { + + upvar ${jlibname}::disco::items items + + set jid [jlib::jidmap $jid] + if {[info exists items($jid,,children)]} { + return $items($jid,,children) + } else { + return + } +} + +proc jlib::disco::childs {jlibname jid {node ""}} { + + upvar ${jlibname}::disco::items items + + set jid [jlib::jidmap $jid] + if {[info exists items($jid,$node,childs)]} { + return $items($jid,$node,childs) + } else { + return + } +} + +# jlib::disco::nodes -- +# +# Returns a list of child nodes of this jid|node. + +proc jlib::disco::nodes {jlibname jid {node ""}} { + + upvar ${jlibname}::disco::items items + + set jid [jlib::jidmap $jid] + if {[info exists items($jid,$node,nodes)]} { + return $items($jid,$node,nodes) + } else { + return + } +} + +proc jlib::disco::handle_get {discotype jlibname from queryE args} { + + upvar ${jlibname}::disco::handler handler + + set ishandled 0 + if {[info exists handler]} { + set ishandled [uplevel #0 $handler \ + [list $jlibname $discotype $from $queryE] $args] + } + return $ishandled +} + +# jlib::disco::unavail_cb -- +# +# Registered unavailable presence callback. +# Frees internal cache related to this jid. + +proc jlib::disco::unavail_cb {jlibname xmldata} { + + # This screws up gateway handling completely since a gateway is still + # a gateway even if unavailable! + # @@@ Perhaps we shall make a distinction here between ordinary users + # and services? + #set jid [wrapper::getattribute $xmldata from] + #reset $jlibname $jid +} + +# jlib::disco::reset -- +# +# Clear this particular jid and all its children. + +proc jlib::disco::reset {jlibname {jid ""} {node ""}} { + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + upvar ${jlibname}::disco::rooms rooms + + if {($jid eq "") && ($node eq "")} { + array unset items + array unset info + array unset rooms + + set info(conferences) [list] + } else { + set jid [jlib::jidmap $jid] + + # Can be problems with this (ICQ) ??? + if {[info exists items($jid,,children)]} { + foreach child $items($jid,,children) { + ResetJid $jlibname $child + } + } + ResetJid $jlibname $jid + } +} + +# jlib::disco::ResetJid -- +# +# Clear only this particular jid. + +proc jlib::disco::ResetJid {jlibname jid} { + + upvar ${jlibname}::disco::items items + upvar ${jlibname}::disco::info info + upvar ${jlibname}::disco::rooms rooms + + if {$jid eq ""} { + unset -nocomplain items info rooms + set info(conferences) [list] + } else { + + if {0} { + + # Keep parents! + + if {[info exists items($jid,parent)]} { + set parent $items($jid,parent) + } + if {[info exists items($jid,parents)]} { + set parents $items($jid,parents) + } + + if {[info exists items($jid,,parent2)]} { + set parent2 $items($jid,,parent2) + } + if {[info exists items($jid,,parents2)]} { + set parents2 $items($jid,,parents2) + } + + } + + array unset items [jlib::ESC $jid],* + array unset info [jlib::ESC $jid],* + array unset rooms [jlib::ESC $jid],* + + if {0} { + + # Add back parent(s). + if {[info exists parent]} { + set items($jid,parent) $parent + } + if {[info exists parents]} { + set items($jid,parents) $parents + } + + if {[info exists parent2]} { + set items($jid,,parent2) $parent2 + } + if {[info exists parents2]} { + set items($jid,,parents2) $parents2 + } + + } + + # Rest. + foreach {key value} [array get info "*,typelist"] { + set info($key) [lsearch -all -not -inline -exact $value $jid] + } + foreach {key value} [array get info "*,featurelist"] { + set info($key) [lsearch -all -not -inline -exact $value $jid] + } + } +} + +proc jlib::disco::Debug {num str} { + variable debug + if {$num <= $debug} { + puts $str + } +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::disco { + + jlib::ensamble_register disco \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/ftrans.tcl b/lib/jabberlib/ftrans.tcl new file mode 100644 index 0000000..d1a4ee0 --- /dev/null +++ b/lib/jabberlib/ftrans.tcl @@ -0,0 +1,728 @@ +# ftrans.tcl -- +# +# This file is part of the jabberlib. +# It provides support for the file-transfer profile (XEP-0096). +# +# Copyright (c) 2005-2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: ftrans.tcl,v 1.31 2008/02/10 09:43:22 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# filetransfer - convenience library for the file-transfer profile of si. +# +# SYNOPSIS +# +# +# OPTIONS +# +# +# INSTANCE COMMANDS +# jlibName filetransfer send jid tclProc \ +# -progress, -description, -date, -hash, -block-size, -mime +# jlibName filetransfer reset sid +# jlibName filetransfer ifree sid +# +############################# CHANGES ########################################## + +package require jlib +package require jlib::si +package require jlib::disco + +package provide jlib::ftrans 0.1 + +namespace eval jlib::ftrans { + + variable xmlns + set xmlns(ftrans) "http://jabber.org/protocol/si/profile/file-transfer" + + # Our target handlers. + jlib::si::registerprofile $xmlns(ftrans) \ + [namespace current]::open_handler \ + [namespace current]::recv \ + [namespace current]::close_handler + + # This is our reader commands when the transport sends off data + # on the network. + jlib::si::registerreader $xmlns(ftrans) \ + [namespace current]::open_data \ + [namespace current]::read_data \ + [namespace current]::close_data + + jlib::disco::registerfeature $xmlns(ftrans) + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::ftrans::registerhandler -- +# +# An application using file-transfer must register here to get a call +# when we receive a file-transfer query. + +proc jlib::ftrans::registerhandler {clientProc} { + variable handler + set handler $clientProc +} + +proc jlib::ftrans::init {jlibname args} { + + # Keep different state arrays for initiator (i) and receiver (r). + namespace eval ${jlibname}::ftrans { + variable istate + variable tstate + } + + # Register this feature with disco. +} + +# jlib::ftrans::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: +# args: all args to the cmd procedure. +# +# Results: +# none. + +proc jlib::ftrans::cmdproc {jlibname cmd args} { + return [eval {$cmd $jlibname} $args] +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# These are all functions used by the initiator (sender). + +# jlib::ftrans::send -- +# +# High level interface to the file-transfer profile for si. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: +# args: +# +# Results: +# sid to identify this transaction. + +proc jlib::ftrans::send {jlibname jid cmd args} { + variable xmlns + upvar ${jlibname}::ftrans::istate istate + #puts "jlib::ftrans::send $args" + + set sid [jlib::util::from args -sid [jlib::generateuuid]] + set fileE [eval {i_constructor $jlibname $sid $jid $cmd} $args] + + # The 'block-size' is crucial here; must tell the stream in question. + set cmd [namespace current]::open_cb + jlib::si::send_set $jlibname $jid $sid $istate($sid,-mime) $xmlns(ftrans) \ + $fileE $cmd -block-size $istate($sid,-block-size) + + return $sid +} + +# jlib::ftrans::i_constructor -- +# +# This is the initiator constructor of a file transfer object. +# Makes a new ftrans instance but doesn't do any networking. +# +# Results: +# The file element. + +proc jlib::ftrans::i_constructor {jlibname sid jid cmd args} { + variable xmlns + upvar ${jlibname}::ftrans::istate istate + + # 4096 is the recommended block-size + array set opts { + -progress "" + -block-size 4096 + -mime application/octet-stream + } + array set opts $args + if {![info exists opts(-data)] \ + && ![info exists opts(-file)] \ + && ![info exists opts(-base64)]} { + return -code error "must have any of -data, -file, or -base64" + } + #puts "jlib::ftrans::i_constructor (i) $args" + + # @@@ TODO + if {![info exists opts(-file)]} {return -code error "todo"} + + switch -- [info exists opts(-base64)],[info exists opts(-data)],[info exists opts(-file)] { + 1,0,0 { + set dtype base64 + set size [string length $opts(-base64)] + } + 0,1,0 { + set dtype data + set size [string length $opts(-data)] + } + 0,0,1 { + set dtype file + set fileName $opts(-file) + if {![file readable $fileName]} { + return -code error "file \"$fileName\" is not readable" + } + + # File open is not done until we get the 'open_cb'. + set size [file size $fileName] + set name [file tail $fileName] + } + default { + return -code error "must have exactly one of -data, -file, or -base64" + } + } + set istate($sid,sid) $sid + set istate($sid,jid) $jid + set istate($sid,cmd) $cmd + set istate($sid,dtype) $dtype + set istate($sid,size) $size + set istate($sid,status) "" + set istate($sid,bytes) 0 + foreach {key value} [array get opts] { + set istate($sid,$key) $value + } + switch -- $dtype { + file { + set istate($sid,name) $name + set istate($sid,fileName) $fileName + } + } + + return [eval {element $name $size} $args] +} + +# jlib::ftrans::uri -- +# +# Create a sipub uri that references a local file. +# XEP-0096 File Transfer, sect. 6.2.2 recvfile: +# xmpp:romeo@montague.net/orchard?recvfile;sid=pub234;mime-type=text%2Fplain&name=reply.txt&size=2048 + +proc jlib::ftrans::uri {jid fileName mime} { + + # NB: The JID must be uri encoded as a path to preserver the "/" + # while the query part must be encoded as is. + set spid [jlib::sipub::newcache $fileName $mime] + set tail [file tail $fileName] + set size [file size $fileName] + set jid [uriencode::quotepath $jid] + set uri "xmpp:$jid?recvfile" + set uri2 "" + append uri2 ";" "sid=$spid" + append uri2 ";" "mime-type=$mime" + append uri2 ";" "name=$tail" + append uri2 ";" "size=$size" + set uri2 [::uri::urn::quote $uri2] + + return $uri$uri2 +} + +# jlib::ftrans::element -- +# +# Just create the file element. Nothing cached. Stateless. +# +# +# All Shakespearean characters must sign and return this NDA ASAP +# +# +# Arguments: +# name +# size +# args: -description -date -hash +# +# Result: +# The file element. + +proc jlib::ftrans::element {name size args} { + variable xmlns + + array set argsA $args + + set subEL [list] + if {[info exists argsA(-description)]} { + set descE [wrapper::createtag "desc" -chdata $argsA(-description)] + set subEL [list $descE] + } + set attrs [list xmlns $xmlns(ftrans) name $name size $size] + if {[info exists argsA(-date)]} { + lappend attrs date $argsA(-date) + } + if {[info exists argsA(-hash)]} { + lappend attrs hash $argsA(-hash) + } + set fileE [wrapper::createtag "file" -attrlist $attrs -subtags $subEL] + + return $fileE +} + +# jlib::ftrans::sipub_element -- +# +# This creates a new sipub instance. Typically only used for normal +# messages. For groupchats, pubsub etc. you must not use this one. + +proc jlib::ftrans::sipub_element {jlibname name size fileName mime args} { + variable xmlns + + set fileE [element $name $size] + set sipubE [jlib::sipub::element [$jlibname myjid] $xmlns(ftrans) \ + $fileE $fileName $mime] + + return $sipubE +} + +# jlib::ftrans::open_cb -- +# +# This is a transports way of reporting result from it's 'open' method. + +proc jlib::ftrans::open_cb {jlibname type sid subiq} { + variable xmlns + upvar ${jlibname}::ftrans::istate istate + + #puts "jlib::ftrans::open_cb (i)" + + if {[string equal $type "error"]} { + set istate($sid,status) "error" + uplevel #0 $istate($sid,cmd) [list $jlibname error $sid $subiq] + ifree $jlibname $sid + } +} + +# jlib::ftrans::open_data, read_data, close_data -- +# +# These are all used by the streams (transports) to handle the data +# stream it needs when transmitting. + +proc jlib::ftrans::open_data {jlibname sid} { + upvar ${jlibname}::ftrans::istate istate + #puts "jlib::ftrans::open_data (i) sid=$sid" + + # @@@ assuming -file type + # This must never fail since tested if 'readable' before. + set fd [open $istate($sid,fileName) r] + fconfigure $fd -translation binary + set istate($sid,fd) $fd + return +} + +proc jlib::ftrans::read_data {jlibname sid} { + upvar ${jlibname}::ftrans::istate istate + #puts "jlib::ftrans::read_data (i) sid=$sid" + + # If we have reached eof we receive empty. + set data [read $istate($sid,fd) $istate($sid,-block-size)] + set len [string length $data] + #puts "\t len=$len" + incr istate($sid,bytes) $len + + if {[string length $istate($sid,-progress)]} { + uplevel #0 $istate($sid,-progress) \ + [list $jlibname $sid $istate($sid,size) $istate($sid,bytes)] + } + return $data +} + +# This is called by the stream when either all data have been sent or if +# there is any network error. + +proc jlib::ftrans::close_data {jlibname sid {err ""}} { + upvar ${jlibname}::ftrans::istate istate + #puts "jlib::ftrans::close_data (i) sid=$sid, err=$err" + + # Empty -> eof. + catch {close $istate($sid,fd)} + + if {$err eq ""} { + set istate($sid,status) "ok" + } else { + set istate($sid,status) "error" + set istate($sid,error) "networkerror" + } + + # Close stream. + # Shall we wait for a result from this query before reporting? + set cmd [namespace current]::close_cb + jlib::si::send_close $jlibname $sid $cmd +} + +# jlib::ftrans::close_cb -- +# +# This is the callback to 'jlib::si::send_close'. +# It is our destructor. + +proc jlib::ftrans::close_cb {jlibname type sid subiq} { + upvar ${jlibname}::ftrans::istate istate + #puts "jlib::ftrans::close_cb (i)" + + # We may have an error status. + set status $istate($sid,status) + if {$status eq "error"} { + set err $istate($sid,error) + uplevel #0 $istate($sid,cmd) [list $jlibname error $sid [list $err ""]] + } elseif {$status eq "reset"} { + uplevel #0 $istate($sid,cmd) [list $jlibname reset $sid {}] + } else { + uplevel #0 $istate($sid,cmd) [list $jlibname $type $sid $subiq] + } + + # There could be situations, a transfer manager, where we want to keep + # this information. + ifree $jlibname $sid +} + +# jlib::ftrans::ireset -- +# +# Reset an initiated transaction. + +proc jlib::ftrans::ireset {jlibname sid} { + upvar ${jlibname}::ftrans::istate istate + + if {[info exists istate($sid,aid)]} { + after cancel $istate($sid,aid) + } + set istate($sid,status) "reset" + set cmd [namespace current]::close_cb + jlib::si::send_close $jlibname $sid $cmd +} + +proc jlib::ftrans::iresetall {jlibname} { + + foreach spec [initiatorinfo $jlibname] { + set sid [lindex $spec 0] + ireset $jlibname $sid + } +} + +# jlib::ftrans::initiatorinfo -- +# +# Returns current open transfers we have initiated. + +proc jlib::ftrans::initiatorinfo {jlibname} { + upvar ${jlibname}::ftrans::istate istate + + set iList [list] + foreach skey [array names istate *,sid] { + set sid $istate($skey) + set opts $sid + foreach {key value} [array get istate $sid,*] { + set name [string map [list $sid, ""] $key] + lappend opts $name $value + } + lappend iList $opts + } + return $iList +} + +proc jlib::ftrans::getinitiatorstate {jlibname sid} { + upvar ${jlibname}::ftrans::istate istate + + set opts [list] + foreach {key value} [array get istate $sid,*] { + set name [string map [list $sid, ""] $key] + lappend opts $name $value + } + return $opts +} + +proc jlib::ftrans::ifree {jlibname sid} { + upvar ${jlibname}::ftrans::istate istate + #puts "jlib::ftrans::ifree (i) sid=$sid" + + array unset istate $sid,* +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# These are all functions to use by a target (receiver) of a stream. + +# jlib::ftrans::open_handler -- +# +# Callback when si receives this specific profile (file-transfer). +# It is called as an iq-set/si handler. +# +# There are two ways this can work: +# 1) Using the global handler registered by 'registerhandler' +# 2) Or the for a specific sid, 'register_sid_handler', which is typically +# used for sipub. + +proc jlib::ftrans::open_handler {jlibname sid jid siE respCmd args} { + variable handler + variable xmlns + upvar ${jlibname}::ftrans::tstate tstate + upvar ${jlibname}::ftrans::sid_handler sid_handler + #puts "jlib::ftrans::open_handler (t)" + + if {![info exists handler]} { + return -code break + } + eval {t_constructor $jlibname $sid $jid $siE} $args + + set tstate($sid,cmd) $respCmd + + set opts [list] + foreach key {mime desc hash date} { + if {[string length $tstate($sid,$key)]} { + lappend opts -$key $tstate($sid,$key) + } + } + lappend opts -queryE $siE + + # Make a call up to application level to pick destination file. + # This is an idle call in order not to block. + set cb [list [namespace current]::accept $jlibname $sid] + + # For sipub we have a registered handler for this sid. + if {[info exists sid_handler($sid)]} { + set cmd $sid_handler($sid) + unset sid_handler($sid) + } else { + set cmd $handler + } + after idle [list eval $cmd \ + [list $jlibname $jid $tstate($sid,name) $tstate($sid,size) $cb] $opts] + + return +} + +proc jlib::ftrans::t_constructor {jlibname sid jid siE args} { + variable handler + variable xmlns + upvar ${jlibname}::ftrans::tstate tstate + #puts "jlib::ftrans::t_constructor (t)" + + array set opts { + -channel "" + -command "" + -progress "" + } + array set opts $args + set fileE [wrapper::getfirstchild $siE "file" $xmlns(ftrans)] + if {![llength $fileE]} { + # Exception + return + } + set tstate($sid,sid) $sid + set tstate($sid,jid) $jid + set tstate($sid,mime) [wrapper::getattribute $siE "mime-type"] + foreach {key value} [array get opts] { + set tstate($sid,$key) $value + } + if {[string length $opts(-channel)]} { + fconfigure $opts(-channel) -translation binary + } + + # File element attributes 'name' and 'size' are required! + array set attr { + name "" + size 0 + date "" + hash "" + } + array set attr [wrapper::getattrlist $fileE] + foreach {name value} [array get attr] { + set tstate($sid,$name) $value + } + set tstate($sid,desc) "" + set descE [wrapper::getfirstchildwithtag $fileE "desc"] + if {[llength $descE]} { + set tstate($sid,desc) [wrapper::getcdata $descE] + } + set tstate($sid,bytes) 0 + set tstate($sid,data) "" + + return +} + +# jlib::ftrans::register_sid_handler -- +# +# Used by sipub to take over from the client handler for this sid. + +proc jlib::ftrans::register_sid_handler {jlibname sid cmd} { + upvar ${jlibname}::ftrans::sid_handler sid_handler + #puts "jlib::ftrans::register_sid_handler (t)" + set sid_handler($sid) $cmd +} + +# jlib::ftrans::accept -- +# +# Used by profile handler to accept/reject file transfer. +# +# Arguments: +# jlibname: the instance of this jlib. +# args: -channel +# -command +# -progress + +proc jlib::ftrans::accept {jlibname sid accepted args} { + upvar ${jlibname}::ftrans::tstate tstate + + array set opts { + -channel "" + -command "" + -progress "" + } + array set opts $args + foreach {key value} [array get opts] { + set tstate($sid,$key) $value + } + if {$accepted} { + set type ok + if {[string length $opts(-channel)]} { + fconfigure $opts(-channel) -translation binary + # -buffersize 4096 + } + } else { + set type error + } + set respCmd $tstate($sid,cmd) + eval $respCmd [list $type {}] + if {!$accepted} { + tfree $jlibname $sid + } +} + +# jlib::ftrans::recv -- +# +# Registered handler when receiving data. Called indirectly from stream. + +proc jlib::ftrans::recv {jlibname sid data} { + upvar ${jlibname}::ftrans::tstate tstate + #puts "jlib::ftrans::recv (t)" + + set len [string length $data] + #puts "\t len=$len" + incr tstate($sid,bytes) $len + if {[string length $tstate($sid,-channel)]} { + if {[catch {puts -nonewline $tstate($sid,-channel) $data} err]} { + terror $jlibname $sid $err + return + } + } else { + #puts "\t append" + append tstate($sid,data) $data + } + if {$len && [string length $tstate($sid,-progress)]} { + uplevel #0 $tstate($sid,-progress) [list $jlibname $sid \ + $tstate($sid,size) $tstate($sid,bytes)] + } +} + +# jlib::ftrans::close_handler -- +# +# Registered handler when closing the stream. +# This is called both for normal close and when an error occured +# in the stream to close prematurely. + +proc jlib::ftrans::close_handler {jlibname sid {errmsg ""}} { + upvar ${jlibname}::ftrans::tstate tstate + #puts "jlib::ftrans::close_handler (t)" + + # Be sure to close the file before doing the callback, else md5 bail out! + if {[string length $tstate($sid,-channel)]} { + close $tstate($sid,-channel) + } + if {[string length $tstate($sid,-command)]} { + if {[string length $errmsg]} { + uplevel #0 $tstate($sid,-command) [list $jlibname $sid error $errmsg] + } else { + uplevel #0 $tstate($sid,-command) [list $jlibname $sid ok] + } + } + tfree $jlibname $sid +} + +proc jlib::ftrans::data {jlibname sid} { + return $tstate($sid,data) +} + +# jlib::ftrans::treset -- +# +# Resets are closes down target side file-transfer during transport. + +proc jlib::ftrans::treset {jlibname sid} { + upvar ${jlibname}::ftrans::tstate tstate + #puts "jlib::ftrans::treset (t)" + + # Tell transport we are resetting. + jlib::si::reset $jlibname $sid + + set tstate($sid,status) "reset" + if {[string length $tstate($sid,-channel)]} { + close $tstate($sid,-channel) + } + if {[string length $tstate($sid,-command)]} { + uplevel #0 $tstate($sid,-command) [list $jlibname $sid reset] + } + tfree $jlibname $sid +} + +# jlib::ftrans::targetinfo -- +# +# Returns current target transfers. + +proc jlib::ftrans::targetinfo {jlibname} { + upvar ${jlibname}::ftrans::tstate tstate + + set tList [list] + foreach skey [array names tstate *,sid] { + set sid $tstate($skey) + set opts [list] + foreach {key value} [array get tstate $sid,*] { + set name [string map [list $sid, ""] $key] + lappend opts $name $value + } + lappend tList $opts + } + return $tList +} + +proc jlib::ftrans::gettargetstate {jlibname sid} { + upvar ${jlibname}::ftrans::tstate tstate + + set opts [list] + foreach {key value} [array get tstate $sid,*] { + set name [string map [list $sid, ""] $key] + lappend opts $name $value + } + return $opts +} + +proc jlib::ftrans::terror {jlibname sid {errormsg ""}} { + upvar ${jlibname}::ftrans::tstate tstate + #puts "jlib::ftrans::terror (t) errormsg=$errormsg" + + if {[string length $tstate($sid,-channel)]} { + close $tstate($sid,-channel) + } + if {[string length $tstate($sid,-command)]} { + uplevel #0 $tstate($sid,-command) [list $jlibname $sid error $errormsg] + } + tfree $jlibname $sid +} + +proc jlib::ftrans::tfree {jlibname sid} { + upvar ${jlibname}::ftrans::tstate tstate + #puts "jlib::ftrans::tfree (t) sid=$sid" + + array unset tstate $sid,* +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::ftrans { + + jlib::ensamble_register filetransfer \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/groupchat.tcl b/lib/jabberlib/groupchat.tcl new file mode 100644 index 0000000..dceff5e --- /dev/null +++ b/lib/jabberlib/groupchat.tcl @@ -0,0 +1,161 @@ +# groupchat.tcl-- +# +# Support for the old gc-1.0 groupchat protocol. +# +# Copyright (c) 2002-2005 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: groupchat.tcl,v 1.10 2008/02/06 13:57:25 matben Exp $ +# +############################# USAGE ############################################ +# +# INSTANCE COMMANDS +# jlibName groupchat enter room nick +# jlibName groupchat exit room +# jlibName groupchat mynick room +# jlibName groupchat setnick room nick ?-command tclProc? +# jlibName groupchat status room +# jlibName groupchat participants room +# jlibName groupchat allroomsin +# +################################################################################ + +package provide groupchat 1.0 +package provide jlib::groupchat 1.0 + +namespace eval jlib {} + +namespace eval jlib::groupchat {} + +# jlib::groupchat -- +# +# Provides API's for the old-style groupchat protocol, 'groupchat 1.0'. + +proc jlib::groupchat {jlibname cmd args} { + return [eval {[namespace current]::groupchat::${cmd} $jlibname} $args] +} + +proc jlib::groupchat::init {jlibname} { + upvar ${jlibname}::gchat gchat + + namespace eval ${jlibname}::groupchat { + variable rooms + } + set gchat(allroomsin) [list] +} + +# jlib::groupchat::enter -- +# +# Enter room using the 'gc-1.0' protocol by sending . +# +# args: -command callback + +proc jlib::groupchat::enter {jlibname room nick args} { + upvar ${jlibname}::gchat gchat + upvar ${jlibname}::groupchat::rooms rooms + + set room [jlib::jidmap $room] + set jid $room/$nick + eval {$jlibname send_presence -to $jid} $args + set gchat($room,mynick) $nick + + # This is not foolproof since it may not always success. + lappend gchat(allroomsin) $room + set rooms($room) 1 + $jlibname service setroomprotocol $room "gc-1.0" + set gchat(allroomsin) [lsort -unique $gchat(allroomsin)] + return +} + +proc jlib::groupchat::exit {jlibname room} { + upvar ${jlibname}::gchat gchat + + set room [jlib::jidmap $room] + if {[info exists gchat($room,mynick)]} { + set nick $gchat($room,mynick) + set jid $room/$nick + $jlibname send_presence -to $jid -type "unavailable" + unset -nocomplain gchat($room,mynick) + } + set ind [lsearch -exact $gchat(allroomsin) $room] + if {$ind >= 0} { + set gchat(allroomsin) [lreplace $gchat(allroomsin) $ind $ind] + } + $jlibname roster clearpresence "${room}*" + return +} + +proc jlib::groupchat::mynick {jlibname room} { + upvar ${jlibname}::gchat gchat + + set room [jlib::jidmap $room] + return $gchat($room,mynick) +} + +proc jlib::groupchat::setnick {jlibname room nick args} { + upvar ${jlibname}::gchat gchat + + set room [jlib::jidmap $room] + set jid $room/$nick + eval {$jlibname send_presence -to $jid} $args + set gchat($room,mynick) $nick +} + +proc jlib::groupchat::status {jlibname room args} { + upvar ${jlibname}::gchat gchat + + set room [jlib::jidmap $room] + if {[info exists gchat($room,mynick)]} { + set nick $gchat($room,mynick) + } else { + return -code error "Unknown nick name for room \"$room\"" + } + set jid ${room}/${nick} + eval {$jlibname send_presence -to $jid} $args +} + +proc jlib::groupchat::participants {jlibname room} { + + upvar ${jlibname}::agent agent + upvar ${jlibname}::gchat gchat + + set room [jlib::jidmap $room] + set isroom 0 + if {[regexp {^[^@]+@([^@ ]+)$} $room match domain]} { + if {[info exists agent($domain,groupchat)]} { + set isroom 1 + } + } + if {!$isroom} { + return -code error "Not recognized \"$room\" as a groupchat room" + } + + # The rosters presence elements should give us all info we need. + set everyone {} + foreach userAttr [$jlibname roster getpresence $room -type available] { + unset -nocomplain attrArr + array set attrArr $userAttr + lappend everyone ${room}/$attrArr(-resource) + } + return $everyone +} + +proc jlib::groupchat::isroom {jlibname jid} { + upvar ${jlibname}::groupchat::rooms rooms + + if {[info exists rooms($jid)]} { + return 1 + } else { + return 0 + } +} + +proc jlib::groupchat::allroomsin {jlibname} { + upvar ${jlibname}::gchat gchat + + set gchat(allroomsin) [lsort -unique $gchat(allroomsin)] + return $gchat(allroomsin) +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/ibb.tcl b/lib/jabberlib/ibb.tcl new file mode 100644 index 0000000..0131699 --- /dev/null +++ b/lib/jabberlib/ibb.tcl @@ -0,0 +1,491 @@ +# ibb.tcl -- +# +# This file is part of the jabberlib. +# It provides support for the ibb stuff (In Band Bytestreams). +# +# Copyright (c) 2005 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: ibb.tcl,v 1.22 2007/11/30 14:38:34 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# ibb - convenience command library for the ibb part of XMPP. +# +# SYNOPSIS +# jlib::ibb::init jlibname +# +# OPTIONS +# +# +# INSTANCE COMMANDS +# jlibName ib send_set jid command ?-key value? +# +############################# CHANGES ########################################## +# +# 0.1 first version + +package require jlib +package require base64 ; # tcllib +package require jlib::disco +package require jlib::si + +package provide jlib::ibb 0.1 + +namespace eval jlib::ibb { + + variable inited 0 + variable xmlns + set xmlns(ibb) "http://jabber.org/protocol/ibb" + set xmlns(amp) "http://jabber.org/protocol/amp" + + jlib::si::registertransport $xmlns(ibb) $xmlns(ibb) 80 \ + [namespace current]::si_open \ + [namespace current]::si_close + + jlib::disco::registerfeature $xmlns(ibb) + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::ibb::init -- +# +# Sets up jabberlib handlers and makes a new instance if an ibb object. + +proc jlib::ibb::init {jlibname args} { + + #puts "jlib::ibb::init" + + variable inited + variable xmlns + + if {!$inited} { + InitOnce + } + + # Keep different state arrays for initiator (i) and target (t). + namespace eval ${jlibname}::ibb { + variable priv + variable opts + variable istate + variable tstate + } + upvar ${jlibname}::ibb::priv priv + upvar ${jlibname}::ibb::opts opts + + array set opts { + -block-size 4096 + } + array set opts $args + + # Each base64 byte takes 6 bits; need to translate to binary bytes. + set binblock [expr {(6 * $opts(-block-size))/8}] + set priv(binblock) [expr {6 * ($binblock/6)}] + + # Register some standard iq handlers that is handled internally. + $jlibname iq_register set $xmlns(ibb) [namespace current]::handle_set + $jlibname message_register * $xmlns(ibb) [namespace current]::message_handler + + return +} + +proc jlib::ibb::InitOnce { } { + + variable ampElem + variable inited + variable xmlns + + set rule1 [wrapper::createtag "rule" \ + -attrlist {condition deliver-at value stored action error}] + set rule2 [wrapper::createtag "rule" \ + -attrlist {condition match-resource value exact action error}] + set ampElem [wrapper::createtag "amp" \ + -attrlist [list xmlns $xmlns(amp)] \ + -subtags [list $rule1 $rule2]] + + set inited 1 +} + +# jlib::ibb::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: +# args: all args to the cmd procedure. +# +# Results: +# none. + +proc jlib::ibb::cmdproc {jlibname cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# These are all functions to use by a initiator (sender). + +# jlib::ibb::si_open, si_close -- +# +# Bindings for si. + +proc jlib::ibb::si_open {jlibname jid sid args} { + + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::si_open (i)" + + set istate($sid,sid) $sid + set istate($sid,jid) $jid + set istate($sid,seq) 0 + set istate($sid,status) "" + set si_open_cb [namespace current]::si_open_cb + eval {send_open $jlibname $jid $sid $si_open_cb} $args + return +} + +proc jlib::ibb::si_open_cb {jlibname sid type subiq args} { + + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::si_open_cb (i)" + + # Since this is an async call we may have been reset. + if {![info exists istate($sid,sid)]} { + return + } + jlib::si::transport_open_cb $jlibname $sid $type $subiq + + # If all went well this far we initiate the read/write data process. + if {$type eq "result"} { + + # Tell the profile to prepare to read data (open file). + jlib::si::open_data $jlibname $sid + si_read $jlibname $sid + } +} + +proc jlib::ibb::si_read {jlibname sid} { + + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::si_read (i)" + + # Since this is an async call we may have been reset. + if {![info exists istate($sid,sid)]} { + return + } + + # We have been reset or something. + if {$istate($sid,status) eq "close"} { + return + } + set data [jlib::si::read_data $jlibname $sid] + set len [string length $data] + + if {$len > 0} { + si_send $jlibname $sid $data + } else { + + # Empty data from the reader means that we are done. + jlib::si::close_data $jlibname $sid + } +} + +proc jlib::ibb::si_send {jlibname sid data} { + + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::si_send (i)" + + set jid $istate($sid,jid) + send_data $jlibname $jid $sid $data [namespace current]::si_send_cb + + # Trick to avoid UI blocking. + # @@@ We should have a method to detect if xmpp socket writable. + after idle [list after 0 [list \ + [namespace current]::si_read $jlibname $sid]] +} + +# jlib::ibb::si_send_cb -- +# +# XEP says that we SHOULD track each mesage, in case of error. + +proc jlib::ibb::si_send_cb {jlibname sid type subiq args} { + + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::si_send_cb (i)" + + # We get this async so we may have been reset or something. + if {![info exists istate($sid,sid)]} { + return + } + if {[string equal $type "error"]} { + jlib::si::close_data $jlibname $sid error + ifree $jlibname $sid + } +} + +# jlib::ibb::si_close -- +# +# The profile closes us down. It could be a reset. + +proc jlib::ibb::si_close {jlibname sid} { + + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::si_close (i)" + + # Keep a status so we can stop sending messages right away. + set istate($sid,status) "close" + set jid $istate($sid,jid) + set cmd [namespace current]::si_close_cb + + send_close $jlibname $jid $sid $cmd +} + +# jlib::ibb::si_close_cb -- +# +# This is our destructor that ends it all. + +proc jlib::ibb::si_close_cb {jlibname sid type subiq args} { + + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::si_close_cb (i)" + + set jid $istate($sid,jid) + + jlib::si::transport_close_cb $jlibname $sid $type $subiq + ifree $jlibname $sid +} + +proc jlib::ibb::ifree {jlibname sid} { + + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::ifree (i)" + + array unset istate $sid,* +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +proc jlib::ibb::configure {jlibname args} { + + upvar ${jlibname}::ibb::opts opts + + # @@@ TODO + +} + +# jlib::ibb::send_open -- +# +# Initiates a file transport. We must be able to configure 'block-size' +# from the file-transfer profile. +# +# Arguments: +# + +proc jlib::ibb::send_open {jlibname jid sid cmd args} { + variable xmlns + upvar ${jlibname}::ibb::opts opts + + #puts "jlib::ibb::send_open (i)" + + array set arr [list -block-size $opts(-block-size)] + array set arr $args + + set openElem [wrapper::createtag "open" \ + -attrlist [list sid $sid block-size $arr(-block-size) xmlns $xmlns(ibb)]] + jlib::send_iq $jlibname set [list $openElem] -to $jid \ + -command [concat $cmd [list $jlibname $sid]] + return +} + +# jlib::ibb::send_data -- +# +# + +proc jlib::ibb::send_data {jlibname jid sid data cmd} { + variable xmlns + variable ampElem + upvar ${jlibname}::ibb::istate istate + #puts "jlib::ibb::send_data (i) sid=$sid, cmd=$cmd" + + set jid $istate($sid,jid) + set seq $istate($sid,seq) + set edata [base64::encode $data] + set dataElem [wrapper::createtag "data" \ + -attrlist [list xmlns $xmlns(ibb) sid $sid seq $seq] \ + -chdata $edata] + set istate($sid,seq) [expr {($seq + 1) % 65536}] + + jlib::send_message $jlibname $jid -xlist [list $dataElem $ampElem] \ + -command [concat $cmd [list $jlibname $sid]] +} + +# jlib::ibb::send_close -- +# +# Sends the close tag. +# +# Arguments: +# + +proc jlib::ibb::send_close {jlibname jid sid cmd} { + variable xmlns + #puts "jlib::ibb::send_close (i)" + + set closeElem [wrapper::createtag "close" \ + -attrlist [list sid $sid xmlns $xmlns(ibb)]] + jlib::send_iq $jlibname set [list $closeElem] -to $jid \ + -command [concat $cmd [list $jlibname $sid]] + return +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# These are all functions to use by a target (receiver) of a stream. + +# jlib::ibb::handle_set -- +# +# Parse incoming ibb iq-set open/close element. +# It is being assumed that we already have accepted a stream initiation. + +proc jlib::ibb::handle_set {jlibname from subiq args} { + + variable xmlns + upvar ${jlibname}::ibb::tstate tstate + + #puts "jlib::ibb::handle_set (t)" + + set tag [wrapper::gettag $subiq] + array set attr [wrapper::getattrlist $subiq] + array set argsArr $args + if {![info exists argsArr(-id)] || ![info exists attr(sid)]} { + # We can't do more here. + return 0 + } + set sid $attr(sid) + + # We make sure that we have already got a si with this sid. + if {![jlib::si::havesi $jlibname $sid]} { + send_error $jlibname $from $argsArr(-id) $sid 404 cancel item-not-found + return 1 + } + + switch -- $tag { + open { + if {![info exists attr(block-size)]} { + # @@@ better stanza! + send_error $jlibname $from $argsArr(-id) $sid 501 cancel \ + feature_not_implemented + return + } + set tstate($sid,sid) $sid + set tstate($sid,jid) $from + set tstate($sid,block-size) $attr(block-size) + set tstate($sid,seq) 0 + + # Make a success response on open. + jlib::send_iq $jlibname "result" {} -to $from -id $argsArr(-id) + } + close { + + # Make a success response on close. + jlib::send_iq $jlibname "result" {} -to $from -id $argsArr(-id) + jlib::si::stream_closed $jlibname $sid + tfree $jlibname $sid + } + default { + return 0 + } + } + return 1 +} + +# jlib::ibb::message_handler -- +# +# Message handler for incoming http://jabber.org/protocol/ibb elements. + +proc jlib::ibb::message_handler {jlibname ns msgElem args} { + + variable xmlns + upvar ${jlibname}::ibb::tstate tstate + + array set argsArr $args + #puts "jlib::ibb::message_handler (t) ns=$ns" + + set jid [wrapper::getattribute $msgElem "from"] + + # Pack up the data and deliver to si. + set dataElems [wrapper::getchildswithtagandxmlns $msgElem data $xmlns(ibb)] + foreach dataElem $dataElems { + array set attr [wrapper::getattrlist $dataElem] + set sid $attr(sid) + set seq $attr(seq) + + # We make sure that we have already got a si with this sid. + # Since there can be many of these, reply with error only to first. + if {![jlib::si::havesi $jlibname $sid] \ + || ![info exists tstate($sid,sid)]} { + if {[info exists argsArr(-id)]} { + set id $argsArr(-id) + jlib::send_message_error $jlibname $jid $id 404 cancel \ + item-not-found + } + return 1 + } + + # Check that no packets have been lost. + if {$seq != $tstate($sid,seq)} { + if {[info exists argsArr(-id)]} { + #puts "\t seq=$seq, expectseq=$expectseq" + set id $argsArr(-id) + jlib::send_message_error $jlibname $jid $id 400 cancel \ + bad-request + } + return 1 + } + + set encdata [wrapper::getcdata $dataElem] + if {[catch { + set data [base64::decode $encdata] + }]} { + if {[info exists argsArr(-id)]} { + jlib::send_message_error $jlibname $jid $id 400 cancel bad-request + } + return 1 + } + + # Next expected 'seq'. + set tstate($sid,seq) [expr {($seq + 1) % 65536}] + + # Deliver to si for further processing. + jlib::si::stream_recv $jlibname $sid $data + } + return 1 +} + +proc jlib::ibb::send_error {jlibname jid id sid errcode errtype stanza} { + + jlib::send_iq_error $jlibname $jid $id $errcode $errtype $stanza + tfree $jlibname $sid +} + +proc jlib::ibb::tfree {jlibname sid} { + + upvar ${jlibname}::ibb::tstate tstate + #puts "jlib::ibb::tfree (t)" + + array unset tstate $sid,* +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::ibb { + + jlib::ensamble_register ibb \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/jabberlib.tcl b/lib/jabberlib/jabberlib.tcl new file mode 100644 index 0000000..3285c71 --- /dev/null +++ b/lib/jabberlib/jabberlib.tcl @@ -0,0 +1,4319 @@ +# jabberlib.tcl -- +# +# This is the main part of the jabber lib, a Tcl library for interacting +# with jabber servers. The core parts are known under the name XMPP. +# +# Copyright (c) 2001-2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: jabberlib.tcl,v 1.199 2008/06/09 14:24:46 matben Exp $ +# +# Error checking is minimal, and we assume that all clients are to be trusted. +# +# News: the transport mechanism shall be completely configurable, but where +# the standard mechanism (put directly to socket) is included here. +# +# Variables used in JabberLib: +# +# lib: +# lib(wrap) : Wrap ID +# lib(clientcmd) : Callback proc up to the client +# lib(sock) : socket name +# lib(streamcmd) : Callback command to run when the +# tag is received from the server. +# +# iqcmd: +# iqcmd(uid) : Next iq id-number. Sent in +# "id" attributes of packets. +# iqcmd($id) : Callback command to run when iq result +# packet of $id is received. +# +# locals: +# locals(server) : The servers logical name (streams 'from') +# locals(username) +# locals(myjid) +# locals(myjid2) +# +############################# SCHEMA ########################################### +# +# TclXML <---> wrapper <---> jabberlib <---> client +# | +# jlib::roster +# jlib::disco +# jlib::muc +# ... +# +# Most jlib-packages are self-registered and are invoked using ensamble (sub) +# commands. +# +############################# USAGE ############################################ +# +# NAME +# jabberlib - an interface between Jabber clients and the wrapper +# +# SYNOPSIS +# jlib::new clientCmd ?-opt value ...? +# jlib::havesasl +# jlib::havetls +# +# OPTIONS +# -iqcommand callback for elements not handled explicitly +# -messagecommand callback for elements +# -presencecommand callback for elements +# -streamnamespace initialization namespace (D = "jabber:client") +# -keepalivesecs send a newline character with this interval +# -autoawaymins if > 0 send away message after this many minutes +# -xautoawaymins if > 0 send xaway message after this many minutes +# -awaymsg the away message +# -xawaymsg the xaway message +# -autodiscocaps 0|1 should presence caps elements be auto discoed +# +# INSTANCE COMMANDS +# jlibName config ?args? +# jlibName openstream server ?args? +# jlibName closestream +# jlibName element_deregister xmlns func +# jlibName element_register xmlns func ?seq? +# jlibName getstreamattr name +# jlibName get_feature name +# jlibName get_last to cmd +# jlibName get_time to cmd +# jlibName getserver +# jlibName get_version to cmd +# jlibName getrecipientjid jid +# jlibName get_registered_presence_stanzas ?tag? ?xmlns? +# jlibName iq_get xmlns ?-to, -command, -sublists? +# jlibName iq_set xmlns ?-to, -command, -sublists? +# jlibName iq_register type xmlns cmd +# jlibName message_register xmlns cmd +# jlibName myjid +# jlibName myjid2 +# jlibName myjidmap +# jlibName myjid2map +# jlibName mypresence +# jlibName oob_set to cmd url ?args? +# jlibName presence_register type cmd +# jlibName registertransport name initProc sendProc resetProc ipProc +# jlibName register_set username password cmd ?args? +# jlibName register_get cmd ?args? +# jlibName register_presence_stanza elem +# jlibName register_remove to cmd ?args? +# jlibName resetstream +# jlibName schedule_auto_away +# jlibName search_get to cmd +# jlibName search_set to cmd ?args? +# jlibName send_iq type xmldata ?args? +# jlibName send_message to ?args? +# jlibName send_presence ?args? +# jlibName send_auth username resource ?args? +# jlibName send xmllist +# jlibName setsockettransport socket +# jlibName state +# jlibName transport +# jlibName deregister_presence_stanza tag xmlns +# +# +# The callbacks given for any of the '-iqcommand', '-messagecommand', +# or '-presencecommand' must have the following form: +# +# tclProc {jlibname xmldata} +# +# where 'type' is the type attribute valid for each specific element, and +# 'args' is a list of '-key value' pairs. The '-iqcommand' returns a boolean +# telling if any 'get' is handled or not. If not, then a "Not Implemented" is +# returned automatically. +# +# The clientCmd procedure must have the following form: +# +# clientCmd {jlibName what args} +# +# where 'what' can be any of: connect, disconnect, xmlerror, +# version, networkerror, .... +# 'args' is a list of '-key value' pairs. +# +# @@@ TODO: +# +# 1) Rewrite from scratch and deliver complete iq, message, and presence +# elements to callbacks. Callbacks then get attributes like 'from' etc +# using accessor functions. +# +# 2) Cleanup all the presence code. +# +#------------------------------------------------------------------------------- + +# @@@ TODO: change package names to jlib::* + +package require wrapper +package require service +package require stanzaerror +package require streamerror +package require groupchat +package require jlib::util + +package provide jlib 2.0 + + +namespace eval jlib { + + # Globals same for all instances of this jlib. + # > 1 prints raw xml I/O + # > 2 prints a lot more + variable debug 0 + if {[info exists ::debugLevel] && ($::debugLevel > 1) && ($debug == 0)} { + set debug 2 + } + + variable statics + set statics(inited) 0 + set statics(presenceTypeExp) \ + {(available|unavailable|subscribe|unsubscribe|subscribed|unsubscribed|invisible|probe)} + set statics(instanceCmds) [list] + + variable version 1.0 + + # Running number. + variable uid 0 + + # Let jlib components register themselves for subcommands, ensamble, + # so that they can be invoked by: jlibname subcommand ... + variable ensamble + + # Some common xmpp xml namespaces. + variable xmppxmlns + array set xmppxmlns { + stream "http://etherx.jabber.org/streams" + streams "urn:ietf:params:xml:ns:xmpp-streams" + tls "urn:ietf:params:xml:ns:xmpp-tls" + sasl "urn:ietf:params:xml:ns:xmpp-sasl" + bind "urn:ietf:params:xml:ns:xmpp-bind" + stanzas "urn:ietf:params:xml:ns:xmpp-stanzas" + session "urn:ietf:params:xml:ns:xmpp-session" + } + + variable jxmlns + array set jxmlns { + amp "http://jabber.org/protocol/amp" + caps "http://jabber.org/protocol/caps" + compress "http://jabber.org/features/compress" + disco "http://jabber.org/protocol/disco" + disco,items "http://jabber.org/protocol/disco#items" + disco,info "http://jabber.org/protocol/disco#info" + ibb "http://jabber.org/protocol/ibb" + muc "http://jabber.org/protocol/muc" + muc,user "http://jabber.org/protocol/muc#user" + muc,admin "http://jabber.org/protocol/muc#admin" + muc,owner "http://jabber.org/protocol/muc#owner" + pubsub "http://jabber.org/protocol/pubsub" + } + + # This is likely to change when XEP accepted. + set jxmlns(entitytime) "http://www.xmpp.org/extensions/xep-0202.html#ns" + + # Auto away and extended away are only set when the + # current status has a lower priority than away or xa respectively. + # After an idea by Zbigniew Baniewski. + variable statusPriority + array set statusPriority { + chat 1 + available 2 + away 3 + xa 4 + dnd 5 + invisible 6 + unavailable 7 + } +} + +proc jlib::getxmlns {name} { + variable xmppxmlns + variable jxmlns + + if {[info exists xmppxmlns($name)]} { + return $xmppxmlns($name) + } elseif {[info exists xmppxmlns($name)]} { + return $jxmlns($name) + } else { + return -code error "unknown xmlns for $name" + } +} + +# jlib::register_instance -- +# +# Packages can register here to get notified when a new jlib instance is +# created. + +proc jlib::register_instance {cmd} { + variable statics + + lappend statics(instanceCmds) $cmd +} + +# jlib::new -- +# +# This creates a new instance jlib interpreter. +# +# Arguments: +# clientcmd: callback procedure for the client +# args: +# -iqcommand +# -messagecommand +# -presencecommand +# -streamnamespace +# -keepalivesecs +# -autoawaymins +# -xautoawaymins +# -awaymsg +# -xawaymsg +# -autodiscocaps +# +# Results: +# jlibname which is the namespaced instance command + +proc jlib::new {clientcmd args} { + + variable jxmlns + variable statics + variable objectmap + variable uid + variable ensamble + + # Generate unique command token for this jlib instance. + # Fully qualified! + set jlibname [namespace current]::jlib[incr uid] + + # Instance specific namespace. + namespace eval $jlibname { + variable lib + variable locals + variable iqcmd + variable iqhook + variable msghook + variable preshook + variable genhook + variable opts + variable pres + variable features + } + + # Set simpler variable names. + upvar ${jlibname}::lib lib + upvar ${jlibname}::iqcmd iqcmd + upvar ${jlibname}::prescmd prescmd + upvar ${jlibname}::msgcmd msgcmd + upvar ${jlibname}::opts opts + upvar ${jlibname}::locals locals + upvar ${jlibname}::features features + + array set opts { + -iqcommand "" + -messagecommand "" + -presencecommand "" + -streamnamespace "jabber:client" + -keepalivesecs 60 + -autoawaymins 0 + -xautoawaymins 0 + -awaymsg "" + -xawaymsg "" + -autodiscocaps 0 + } + + # Verify options. + eval verify_options $jlibname $args + + if {!$statics(inited)} { + init + } + + set wrapper [wrapper::new [list [namespace current]::got_stream $jlibname] \ + [list [namespace current]::end_of_parse $jlibname] \ + [list [namespace current]::dispatcher $jlibname] \ + [list [namespace current]::xmlerror $jlibname]] + + set iqcmd(uid) 1001 + set prescmd(uid) 1001 + set msgcmd(uid) 1001 + set lib(clientcmd) $clientcmd + set lib(async_handler) "" + set lib(wrap) $wrapper + set lib(resetCmds) [list] + + set lib(isinstream) 0 + set lib(state) "" + set lib(transport,name) "" + + set lib(socketfilter,out) [list] + set lib(socketfilter,in) [list] + + set lib(tee,send) [list] + set lib(tee,recv) [list] + + init_inst $jlibname + + # Init groupchat state. + groupchat::init $jlibname + + # Register some standard iq handlers that are handled internally. + iq_register $jlibname get jabber:iq:last \ + [namespace current]::handle_get_last + iq_register $jlibname get jabber:iq:time \ + [namespace current]::handle_get_time + # This overrides any client handler which is bad. + #iq_register $jlibname get jabber:iq:version \ + # [namespace current]::handle_get_version + + iq_register $jlibname get $jxmlns(entitytime) \ + [namespace current]::handle_entity_time + + # Create the actual jlib instance procedure. + proc $jlibname {cmd args} \ + "eval jlib::cmdproc {$jlibname} \$cmd \$args" + + # Init the service layer for this jlib instance. + service::init $jlibname + + # Init ensamble commands. + foreach {- name} [array get ensamble *,name] { + uplevel #0 $ensamble($name,init) $jlibname + } + + return $jlibname +} + +# jlib::init -- +# +# Static initializations. + +proc jlib::init {} { + variable statics + + if {[catch {package require jlibsasl}]} { + set statics(sasl) 0 + } else { + set statics(sasl) 1 + sasl_init + } + if {[catch {package require jlibtls}]} { + set statics(tls) 0 + } else { + set statics(tls) 1 + } + + set statics(inited) 1 +} + +# jlib::init_inst -- +# +# Instance specific initializations. + +proc jlib::init_inst {jlibname} { + + upvar ${jlibname}::locals locals + upvar ${jlibname}::features features + + # Any of {available chat away xa dnd invisible unavailable} + set locals(status) "unavailable" + set locals(pres,type) "unavailable" + set locals(myjid) "" + set locals(myjid2) "" + set locals(myjidmap) "" + set locals(myjid2map) "" + set locals(trigAutoAway) 1 + set locals(server) "" + set locals(servermap) "" + + set features(trace) [list] +} + +# jlib::havesasl -- +# +# Cache this info for effectiveness. It is needed at application level. + +proc jlib::havesasl {} { + variable statics + + if {![info exists statics(sasl)]} { + if {[catch {package require jlibsasl}]} { + set statics(sasl) 0 + } else { + set statics(sasl) 1 + } + } + return $statics(sasl) +} + +# jlib::havetls -- +# +# Cache this info for effectiveness. It is needed at application level. + +proc jlib::havetls {} { + variable statics + + if {![info exists statics(tls)]} { + if {[catch {package require jlibtls}]} { + set statics(tls) 0 + } else { + set statics(tls) 1 + } + } + return $statics(tls) +} + +proc jlib::havecompress {} { + variable statics + + if {![info exists statics(compress)]} { + if {[catch {package require jlib::compress}]} { + set statics(compress) 0 + } else { + set statics(compress) 1 + } + } + return $statics(compress) +} + +# jlib::register_package -- +# +# This is supposed to be a method for jlib::* packages to register +# themself just so we know they are there. So far only for the 'roster'. + +proc jlib::register_package {name} { + variable statics + + set statics($name) 1 +} + +# jlib::ensamble_register -- +# +# Register a sub command. +# This is then used as: 'jlibName subCmd ...' + +proc jlib::ensamble_register {name initProc cmdProc} { + variable statics + variable ensamble + + set ensamble($name,name) $name + set ensamble($name,init) $initProc + set ensamble($name,cmd) $cmdProc + + # Must call the initProc for already existing jlib instances. + if {$statics(inited)} { + foreach jlibname [namespace children ::jlib jlib*] { + uplevel #0 $initProc $jlibname + } + } +} + +proc jlib::ensamble_deregister {name} { + variable ensamble + + array unset ensamble ${name},* +} + +# jlib::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: openstream - closestream - send_iq - send_message ... etc. +# args: all args to the cmd procedure. +# +# Results: +# none. + +proc jlib::cmdproc {jlibname cmd args} { + variable ensamble + + # Which command? Just dispatch the command to the right procedure. + if {[info exists ensamble($cmd,cmd)]} { + return [uplevel #0 $ensamble($cmd,cmd) $jlibname $args] + } else { + return [eval {$cmd $jlibname} $args] + } +} + +# jlib::config -- +# +# See documentaion for details. +# +# Arguments: +# args Options parsed by the procedure. +# +# Results: +# depending on args. + +proc jlib::config {jlibname args} { + variable ensamble + upvar ${jlibname}::opts opts + + set options [lsort [array names opts -*]] + set usage [join $options ", "] + if {[llength $args] == 0} { + set result [list] + foreach name $options { + lappend result $name $opts($name) + } + return $result + } + regsub -all -- - $options {} options + set pat ^-([join $options |])$ + if {[llength $args] == 1} { + set flag [lindex $args 0] + if {[regexp -- $pat $flag]} { + return $opts($flag) + } else { + return -code error "Unknown option $flag, must be: $usage" + } + } else { + array set argsA $args + + # Reschedule auto away only if changed. Before setting new opts! + # Better to use 'tk inactive' or 'tkinactive' and handle this on + # application level. + if {[info exists argsA(-autoawaymins)] && \ + ($argsA(-autoawaymins) != $opts(-autoawaymins))} { + schedule_auto_away $jlibname + } + if {[info exists argsA(-xautoawaymins)] && \ + ($argsA(-xautoawaymins) != $opts(-xautoawaymins))} { + schedule_auto_away $jlibname + } + foreach {flag value} $args { + if {[regexp -- $pat $flag]} { + set opts($flag) $value + } else { + return -code error "Unknown option $flag, must be: $usage" + } + } + } + + # Let components configure themselves. + # @@@ It is better to let components handle this??? + foreach ename [array names ensamble] { + set ecmd ${ename}::configure + if {[llength [info commands $ecmd]]} { + #uplevel #0 $ecmd $jlibname $args + } + } + + return +} + +# jlib::verify_options +# +# Check if valid options and set them. +# +# Arguments +# +# args The argument list given on the call. +# +# Side Effects +# Sets error + +proc jlib::verify_options {jlibname args} { + + upvar ${jlibname}::opts opts + + set validopts [array names opts] + set usage [join $validopts ", "] + regsub -all -- - $validopts {} theopts + set pat ^-([join $theopts |])$ + foreach {flag value} $args { + if {[regexp $pat $flag]} { + + # Validate numbers + if {[info exists opts($flag)] && \ + [string is integer -strict $opts($flag)] && \ + ![string is integer -strict $value]} { + return -code error "Bad value for $flag ($value), must be integer" + } + set opts($flag) $value + } else { + return -code error "Unknown option $flag, can be: $usage" + } + } +} + +# jlib::state -- +# +# Accesor for the internal 'state'. + +proc jlib::state {jlibname} { + + upvar ${jlibname}::lib lib + + return $lib(state) +} + +# jlib::register_reset -- +# +# Packages can register here to get notified when the jlib stream is reset. + +proc jlib::register_reset {jlibname cmd} { + + upvar ${jlibname}::lib lib + + lappend lib(resetCmds) $cmd +} + +# jlib::registertransport -- +# +# We must have a transport mechanism for our xml. Socket is standard but +# http is also possible. + +proc jlib::registertransport {jlibname name initProc sendProc resetProc ipProc} { + + upvar ${jlibname}::lib lib + + set lib(transport,name) $name + set lib(transport,init) $initProc + set lib(transport,send) $sendProc + set lib(transport,reset) $resetProc + set lib(transport,ip) $ipProc +} + +proc jlib::transport {jlibname} { + + upvar ${jlibname}::lib lib + + return $lib(transport,name) +} + +# jlib::setsockettransport -- +# +# Sets the standard socket transport and the actual socket to use. + +proc jlib::setsockettransport {jlibname sock} { + + upvar ${jlibname}::lib lib + + # Settings for the raw socket transport layer. + set lib(sock) $sock + set lib(transport,name) "socket" + set lib(transport,init) [namespace current]::initsocket + set lib(transport,send) [namespace current]::putssocket + set lib(transport,reset) [namespace current]::resetsocket + set lib(transport,ip) [namespace current]::ipsocket +} + +# The procedures for the standard socket transport layer ----------------------- + +# jlib::initsocket +# +# Default transport mechanism; init already opened socket. +# +# Arguments: +# +# Side Effects: +# none + +proc jlib::initsocket {jlibname} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::opts opts + + set sock $lib(sock) + if {[catch { + fconfigure $sock -blocking 0 -buffering none -encoding utf-8 + } err]} { + return -code error "The connection failed or dropped later" + } + + # Set up callback on incoming socket. + fileevent $sock readable [list [namespace current]::recvsocket $jlibname] + + # Schedule keep-alives to keep socket open in case anyone want's to close it. + # Be sure to not send any keep-alives before the stream is inited. + if {$opts(-keepalivesecs)} { + after [expr 1000 * $opts(-keepalivesecs)] \ + [list [namespace current]::schedule_keepalive $jlibname] + } +} + +# jlib::putssocket +# +# Default transport mechanism; put directly to socket. +# +# Arguments: +# +# xml The xml that is to be written. +# +# Side Effects: +# none + +proc jlib::putssocket {jlibname xml} { + + upvar ${jlibname}::lib lib + + Debug 2 "SEND: $xml" + + if {$lib(socketfilter,out) ne {}} { + set xml [$lib(socketfilter,out) $jlibname $xml] + } + if {[catch {puts -nonewline $lib(sock) $xml} err]} { + # Error propagated to the caller that calls clientcmd. + return -code error $err + } +} + +# jlib::resetsocket +# +# Default transport mechanism; reset socket. +# +# Arguments: +# +# Side Effects: +# none + +proc jlib::resetsocket {jlibname} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::locals locals + + catch {close $lib(sock)} + catch {after cancel $locals(aliveid)} + + set lib(socketfilter,out) [list] + set lib(socketfilter,in) [list] +} + +# jlib::recvsocket -- +# +# Default transport mechanism; fileevent on socket socket. +# Callback on incoming socket xml data. Feeds our wrapper and XML parser. +# +# Arguments: +# jlibname: the instance of this jlib. +# +# Results: +# none. + +proc jlib::recvsocket {jlibname} { + + upvar ${jlibname}::lib lib + + if {[catch {eof $lib(sock)} iseof] || $iseof} { + kill $jlibname + invoke_async_error $jlibname networkerror + return + } + + # Read what we've got. + if {[catch {read $lib(sock)} data]} { + kill $jlibname + invoke_async_error $jlibname networkerror + return + } + if {$lib(socketfilter,in) ne {}} { + set data [$lib(socketfilter,in) $jlibname $data] + } + Debug 2 "RECV: $data" + + # Feed the XML parser. When the end of a command element tag is reached, + # we get a callback to 'jlib::dispatcher'. + wrapper::parse $lib(wrap) $data +} + +proc jlib::set_socket_filter {jlibname outcmd incmd} { + + upvar ${jlibname}::lib lib + + set lib(socketfilter,out) $outcmd + set lib(socketfilter,in) $incmd + + fconfigure $lib(sock) -translation binary +} + +# jlib::ipsocket -- +# +# Get our own ip address. + +proc jlib::ipsocket {jlibname} { + + upvar ${jlibname}::lib lib + + if {[string length $lib(sock)]} { + return [lindex [fconfigure $lib(sock) -sockname] 0] + } else { + return "" + } +} + +# standard socket transport layer end ------------------------------------------ + +proc jlib::tee_recv {jlibname cmd procName} { + + upvar ${jlibname}::lib lib + + if {$cmd eq "add"} { + lappend lib(tee,recv) $procName + } elseif {$cmd eq "remove"} { + set lib(tee,recv) [lsearch -all -inline -not $lib(tee,recv) $procName] + } else { + return -code error "unknown sub command \"$cmd\"" + } +} + +proc jlib::tee_send {jlibname cmd procName} { + + upvar ${jlibname}::lib lib + + if {$cmd eq "add"} { + lappend lib(tee,send) $procName + } elseif {$cmd eq "remove"} { + set lib(tee,send) [lsearch -all -inline -not $lib(tee,send) $procName] + } else { + return -code error "unknown sub command \"$cmd\"" + } +} + +# jlib::recv -- +# +# Feed the XML parser. When the end of a command element tag is reached, +# we get a callback to 'jlib::dispatcher'. + +proc jlib::recv {jlibname xml} { + + upvar ${jlibname}::lib lib + + wrapper::parse $lib(wrap) $xml +} + +# jlib::openstream -- +# +# Initializes a stream to a jabber server. The socket must already +# be opened. Sets up fileevent on incoming xml stream. +# +# Arguments: +# jlibname: the instance of this jlib. +# server: the domain name or ip number of the server. +# args: +# -cmd callback when we receive the tag from the server. +# -to the receipients jabber id. +# -id +# -version +# +# Results: +# none. + +proc jlib::openstream {jlibname server args} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::locals locals + upvar ${jlibname}::opts opts + variable xmppxmlns + + array set argsA $args + + # The server 'to' attribute is only temporary until we have either a + # confirmation or a redirection (alias) in received streams 'from' attribute. + set locals(server) $server + set locals(servermap) [jidmap $server] + set locals(last) [clock seconds] + + # Make sure we start with a clean state. + wrapper::reset $lib(wrap) + + set optattr "" + foreach {key value} $args { + + switch -- $key { + -cmd { + if {$value ne ""} { + # Register a callback proc. + set lib(streamcmd) $value + } + } + -socket { + # empty + } + default { + set attr [string trimleft $key "-"] + append optattr " $attr='$value'" + } + } + } + set lib(isinstream) 1 + set lib(state) "instream" + + if {[catch { + + # This call to the transport layer shall set up fileevent callbacks etc. + # to handle all incoming xml. + uplevel #0 $lib(transport,init) $jlibname + + # Network errors if failed to open connection properly are likely to show here. + set xml "" + + sendraw $jlibname $xml + } err]} { + + # The socket probably was never connected, + # or the connection dropped later. + #closestream $jlibname + kill $jlibname + return -code error "The connection failed or dropped later: $err" + } + return +} + +# jlib::sendstream -- +# +# Utility for SASL, TLS etc. Sends only the actual stream:stream tag. +# May throw error! + +proc jlib::sendstream {jlibname args} { + + upvar ${jlibname}::locals locals + upvar ${jlibname}::opts opts + variable xmppxmlns + + set attr "" + foreach {key value} $args { + set name [string trimleft $key "-"] + append attr " $name='$value'" + } + set xml "" + + sendraw $jlibname $xml +} + +# jlib::closestream -- +# +# Closes the stream down, closes socket, and resets internal variables. +# It should handle the complete shutdown of our connection and state. +# +# There is a potential problem if called from within a xml parser +# callback which makes the subsequent parsing to fail. (after idle?) +# +# Arguments: +# jlibname: the instance of this jlib. +# +# Results: +# none. + +proc jlib::closestream {jlibname} { + + upvar ${jlibname}::lib lib + + Debug 4 "jlib::closestream" + + if {$lib(isinstream)} { + set xml "" + catch {sendraw $jlibname $xml} + set lib(isinstream) 0 + } + kill $jlibname +} + +# jlib::invoke_async_error -- +# +# Used for reporting async errors, typically network errors. + +proc jlib::invoke_async_error {jlibname err {msg ""}} { + + upvar ${jlibname}::lib lib + Debug 4 "jlib::invoke_async_error err=$err, msg=$msg" + + if {$lib(async_handler) eq ""} { + uplevel #0 $lib(clientcmd) [list $jlibname $err -errormsg $msg] + } else { + uplevel #0 $lib(async_handler) [list $jlibname $err $msg] + } +} + +# jlib::set_async_error_handler -- +# +# This is a way to get all async events directly to a registered handler +# without delivering them to clientcmd. Used in jlib::connect. +proc jlib::set_async_error_handler {jlibname {cmd ""}} { + + upvar ${jlibname}::lib lib + + set lib(async_handler) $cmd +} + +# jlib::reporterror -- +# +# Used for transports to report async, fatal and nonrecoverable errors. + +proc jlib::reporterror {jlibname err {msg ""}} { + + Debug 4 "jlib::reporterror" + + kill $jlibname + invoke_async_error $jlibname $err $msg +} + +# jlib::kill -- +# +# Like closestream but without any network transactions. + +proc jlib::kill {jlibname} { + + upvar ${jlibname}::lib lib + + Debug 4 "jlib::kill" + + # Close socket typically. + catch {uplevel #0 $lib(transport,reset) $jlibname} + reset $jlibname + + # Be sure to reset the wrapper, which implicitly resets the XML parser. + wrapper::reset $lib(wrap) + return +} + +proc jlib::wrapper_reset {jlibname} { + upvar ${jlibname}::lib lib + wrapper::reset $lib(wrap) +} + +# jlib::getip -- +# +# Transport independent way of getting own ip address. + +proc jlib::getip {jlibname} { + upvar ${jlibname}::lib lib + return [$lib(transport,ip) $jlibname] +} + +# jlib::getserver -- +# +# Is the received streams 'from' attribute which is the logical host. +# This is normally identical to the 'to' attribute but not always. + +proc jlib::getserver {jlibname} { + upvar ${jlibname}::locals locals + return $locals(server) +} + +proc jlib::getservermap {jlibname} { + upvar ${jlibname}::locals locals + return $locals(servermap) +} + +# jlib::isinstream -- +# +# Utility to help us closing down a stream. + +proc jlib::isinstream {jlibname} { + upvar ${jlibname}::lib lib + return $lib(isinstream) +} + +# jlib::dispatcher -- +# +# Just dispatches the xml to any of the iq, message, or presence handlers, +# which in turn dispatches further and/or handles internally. +# +# Arguments: +# jlibname: the instance of this jlib. +# xmldata: the complete xml as a hierarchical list. +# +# Results: +# none. + +proc jlib::dispatcher {jlibname xmldata} { + upvar ${jlibname}::lib lib + + # Which method? + set tag [wrapper::gettag $xmldata] + + switch -- $tag { + iq { + iq_handler $jlibname $xmldata + } + message { + message_handler $jlibname $xmldata + } + presence { + presence_handler $jlibname $xmldata + } + features { + features_handler $jlibname $xmldata + } + error { + error_handler $jlibname $xmldata + } + default { + element_run_hook $jlibname $xmldata + } + } + + foreach cmd $lib(tee,recv) { + uplevel #0 $cmd [list $jlibname $xmldata] + } + + # Will have to wait... + #general_run_hook $jlibname $xmldata +} + +# jlib::iq_handler -- +# +# Callback for incoming elements. +# The handling sequence is the following: +# 1) handle all preregistered callbacks via id attributes +# 2) handle callbacks specific for 'type' and 'xmlns' that have been +# registered with 'iq_register' +# 3) if unhandled by 2, use any -iqcommand callback +# 4) if type='get' and still unhandled, return an error element +# +# Arguments: +# jlibname: the instance of this jlib. +# xmldata the xml element as a list structure. +# +# Results: +# roster object set, callbacks invoked. + +proc jlib::iq_handler {jlibname xmldata} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::iqcmd iqcmd + upvar ${jlibname}::opts opts + upvar ${jlibname}::locals locals + variable xmppxmlns + + Debug 4 "jlib::iq_handler: ------------" + + # Extract the command level XML data items. + set tag [wrapper::gettag $xmldata] + array set attrArr [wrapper::getattrlist $xmldata] + + # Make an argument list ('-key value' pairs) suitable for callbacks. + # Make variables of the attributes. + set arglist [list] + foreach {key value} [array get attrArr] { + set $key $value + lappend arglist -$key $value + } + + # This helps callbacks to adapt to using full element as argument. + lappend arglist -xmldata $xmldata + + # The 'type' attribute must exist! Else we return silently. + if {![info exists type]} { + return + } + if {[info exists from]} { + set afrom $from + } else { + set afrom $locals(servermap) + } + + # @@@ Section 9.2.3 of RFC 3920 states in part: + # 6. An IQ stanza of type "result" MUST include zero or one child elements. + # 7. An IQ stanza of type "error" SHOULD include the child element + # contained in the associated "get" or "set" and MUST include an + # child.... + + set childlist [wrapper::getchildren $xmldata] + set subiq [lindex $childlist 0] + set xmlns [wrapper::getattribute $subiq xmlns] + + set ishandled 0 + + # (1) Handle all preregistered callbacks via id attributes. + # Must be type 'result' or 'error'. + # Some components use type='set' instead of 'result'. + # BUT this creates logical errors since we may also receive iq with + # identical id! + + # @@@ It would be better NOT to have separate calls for errors. + + switch -- $type { + result { + + # Protect us from our own 'set' calls when we are awaiting + # 'result' or 'error'. + set setus 0 + if {($type eq "set") && ($afrom eq $locals(myjidmap))} { + set setus 1 + } + + if {!$setus && [info exists id] && [info exists iqcmd($id)]} { + uplevel #0 $iqcmd($id) [list result $subiq] + + # @@@ TODO: + #uplevel #0 $iqcmd($id) [list $jlibname xmldata] + + # The callback my in turn call 'closestream' which unsets + # all iq before returning. + unset -nocomplain iqcmd($id) + set ishandled 1 + } + } + error { + set errspec [getstanzaerrorspec $xmldata] + if {[info exists id] && [info exists iqcmd($id)]} { + + # @@@ Having a separate form of error callbacks is really BAD!!! + uplevel #0 $iqcmd($id) [list error $errspec] + + #uplevel #0 $iqcmd($id) [list $jlibname $xmldata] + + unset -nocomplain iqcmd($id) + set ishandled 1 + } + } + } + + # (2) Handle callbacks specific for 'type' and 'xmlns' that have been + # registered with 'iq_register' + + if {[string equal $ishandled "0"]} { + set ishandled [eval { + iq_run_hook $jlibname $type $xmlns $afrom $subiq} $arglist] + } + + # (3) If unhandled by 2, use any -iqcommand callback. + + if {[string equal $ishandled "0"]} { + if {[string length $opts(-iqcommand)]} { + set ishandled [uplevel #0 $opts(-iqcommand) [list $jlibname $xmldata]] + } + + # (4) If type='get' or 'set', and still unhandled, return an error element. + + if {[string equal $ishandled "0"] && \ + ([string equal $type "get"] || [string equal $type "set"])} { + + # Return a "Not Implemented" to the sender. Just switch to/from, + # type='result', and add an element. + if {[info exists attrArr(from)]} { + return_error $jlibname $xmldata 501 cancel "feature-not-implemented" + } + } + } +} + +# jlib::return_error -- +# +# Returns an iq-error response using complete iq-element. + +proc jlib::return_error {jlibname iqElem errcode errtype errtag} { + variable xmppxmlns + + array set attr [wrapper::getattrlist $iqElem] + set childlist [wrapper::getchildren $iqElem] + + # Switch from -> to, type='error', retain any id. + set attr(to) $attr(from) + set attr(type) "error" + unset attr(from) + + set iqElem [wrapper::setattrlist $iqElem [array get attr]] + set stanzaElem [wrapper::createtag $errtag \ + -attrlist [list xmlns $xmppxmlns(stanzas)]] + set errElem [wrapper::createtag "error" -subtags [list $stanzaElem] \ + -attrlist [list code $errcode type $errtype]] + + lappend childlist $errElem + set iqElem [wrapper::setchildlist $iqElem $childlist] + + send $jlibname $iqElem +} + +# jlib::send_iq_error -- +# +# Sends an iq error element as a response to a iq element. + +proc jlib::send_iq_error {jlibname jid id errcode errtype stanza {extraElem {}}} { + variable xmppxmlns + + set stanzaElem [wrapper::createtag $stanza \ + -attrlist [list xmlns $xmppxmlns(stanzas)]] + set errChilds [list $stanzaElem] + if {[llength $extraElem]} { + lappend errChilds $extraElem + } + set errElem [wrapper::createtag "error" \ + -attrlist [list code $errcode type $errtype] \ + -subtags $errChilds] + set iqElem [wrapper::createtag "iq" \ + -attrlist [list type error to $jid id $id] -subtags [list $errElem]] + + send $jlibname $iqElem +} + +# jlib::message_handler -- +# +# Callback for incoming elements. See 'jlib::dispatcher'. +# +# Arguments: +# jlibname: the instance of this jlib. +# xmldata the xml element as a list structure. +# +# Results: +# callbacks invoked. + +proc jlib::message_handler {jlibname xmldata} { + + upvar ${jlibname}::opts opts + upvar ${jlibname}::lib lib + upvar ${jlibname}::msgcmd msgcmd + + # Extract the command level XML data items. + set attrlist [wrapper::getattrlist $xmldata] + set childlist [wrapper::getchildren $xmldata] + set attrArr(type) "normal" + array set attrArr $attrlist + set type $attrArr(type) + + # Make an argument list ('-key value' pairs) suitable for callbacks. + # Make variables of the attributes. + foreach {key value} [array get attrArr] { + set vopts(-$key) $value + } + + # This helps callbacks to adapt to using full element as argument. + set vopts(-xmldata) $xmldata + set ishandled 0 + + switch -- $type { + error { + set errspec [getstanzaerrorspec $xmldata] + set vopts(-error) $errspec + } + } + + # Extract the message sub-elements. + # @@@ really bad solution... Deliver full element instead + set xmlnsList [list] + foreach child $childlist { + + # Extract the message sub-elements XML data items. + set ctag [wrapper::gettag $child] + set cchdata [wrapper::getcdata $child] + + switch -- $ctag { + body - subject - thread { + set vopts(-$ctag) $cchdata + } + error { + # handled above + } + default { + lappend elem(-$ctag) $child + lappend xmlnsList [wrapper::getattribute $child xmlns] + } + } + } + set xmlnsList [lsort -unique $xmlnsList] + set arglist [array get vopts] + + # Invoke any registered handler for this particular message. + set iscallback 0 + if {[info exists attrArr(id)]} { + set id $attrArr(id) + + # Avoid the weird situation when we send to ourself. + if {[info exists msgcmd($id)] && ![info exists msgcmd($id,self)]} { + uplevel #0 $msgcmd($id) [list $jlibname $type] $arglist + unset -nocomplain msgcmd($id) + set iscallback 1 + } + unset -nocomplain msgcmd($id,self) + } + + # Invoke any registered message handlers for this type and xmlns. + if {[array exists elem]} { + set arglist [concat [array get vopts] [array get elem]] + foreach xmlns $xmlnsList { + set ishandled [eval { + message_run_hook $jlibname $type $xmlns $xmldata} $arglist] + if {$ishandled} { + break + } + } + } + if {!$iscallback && [string equal $ishandled "0"]} { + + # Invoke callback to client. + if {[string length $opts(-messagecommand)]} { + uplevel #0 $opts(-messagecommand) [list $jlibname $xmldata] + } + } +} + +# jlib::send_message_error -- +# +# Sends a message error element as a response to another message. + +proc jlib::send_message_error {jlibname jid id errcode errtype stanza {extraElem {}}} { + variable xmppxmlns + + set stanzaElem [wrapper::createtag $stanza \ + -attrlist [list xmlns $xmppxmlns(stanzas)]] + set errChilds [list $stanzaElem] + if {[llength $extraElem]} { + lappend errChilds $extraElem + } + set errElem [wrapper::createtag "error" \ + -attrlist [list code $errcode type $errtype] \ + -subtags $errChilds] + set msgElem [wrapper::createtag "iq" \ + -attrlist [list type error to $jid id $id] \ + -subtags [list $errElem]] + + send $jlibname $msgElem +} + +# jlib::presence_handler -- +# +# Callback for incoming elements. See 'jlib::dispatcher'. +# +# Arguments: +# jlibname: the instance of this jlib. +# xmldata the xml element as a list structure. +# +# Results: +# roster object set, callbacks invoked. + +proc jlib::presence_handler {jlibname xmldata} { + variable statics + upvar ${jlibname}::lib lib + upvar ${jlibname}::prescmd prescmd + upvar ${jlibname}::opts opts + upvar ${jlibname}::locals locals + + set id [wrapper::getattribute $xmldata id] + + # Handle callbacks specific for 'type' that have been registered with + # 'presence_register(_ex)'. + + # We keep two sets of registered handlers, jlib internal which are + # called first, and then externals which are used by the client. + + # Internals: + presence_run_hook $jlibname 1 $xmldata + presence_ex_run_hook $jlibname 1 $xmldata + + # Externals: + presence_run_hook $jlibname 0 $xmldata + presence_ex_run_hook $jlibname 0 $xmldata + + # Invoke any callback before the rosters callback. + # @@@ Right place ??? + if {[info exists prescmd($id)]} { + uplevel #0 $prescmd($id) [list $jlibname $xmldata] + unset -nocomplain prescmd($id) + } + + # This is the last station. + if {[string length $opts(-presencecommand)]} { + uplevel #0 $opts(-presencecommand) [list $jlibname $xmldata] + } +} + +# jlib::features_handler -- +# +# Callback for the element. + +proc jlib::features_handler {jlibname xmllist} { + + upvar ${jlibname}::features features + variable xmppxmlns + variable jxmlns + + Debug 4 "jlib::features_handler" + + set features(xmllist) $xmllist + + foreach child [wrapper::getchildren $xmllist] { + wrapper::splitxml $child tag attr chdata children + set xmlns [wrapper::getattribute $child xmlns] + + # All feature elements must be namespaced. + if {$xmlns eq ""} { + continue + } + set features(elem,$xmlns) $child + + switch -- $tag { + starttls { + + # TLS + if {$xmlns eq $xmppxmlns(tls)} { + set features(starttls) 1 + set childs [wrapper::getchildswithtag $child required] + if {$childs ne ""} { + set features(starttls,required) 1 + } + } + } + compression { + + # Compress + if {$xmlns eq $jxmlns(compress)} { + set features(compression) 1 + foreach c [wrapper::getchildswithtag $child method] { + set method [wrapper::getcdata $c] + set features(compression,$method) 1 + } + } + } + mechanisms { + + # SASL + set mechanisms [list] + if {$xmlns eq $xmppxmlns(sasl)} { + set features(sasl) 1 + foreach mechelem $children { + wrapper::splitxml $mechelem mtag mattr mchdata mchild + if {$mtag eq "mechanism"} { + lappend mechanisms $mchdata + } + set features(mechanism,$mchdata) 1 + } + } + + # Variable that may trigger a trace event. + set features(mechanisms) $mechanisms + } + bind { + if {$xmlns eq $xmppxmlns(bind)} { + set features(bind) 1 + } + } + session { + if {$xmlns eq $xmppxmlns(session)} { + set features(session) 1 + } + } + default { + + # Have no idea of what this could be. + set features($xmlns) 1 + } + } + } + + if {$features(trace) ne {}} { + uplevel #0 $features(trace) [list $jlibname] + } +} + +# jlib::trace_stream_features -- +# +# Register a callback when getting stream features. +# Only one component at a time. +# +# args: tclProc set callback +# {} unset callback +# empty return callback + +proc jlib::trace_stream_features {jlibname args} { + + upvar ${jlibname}::features features + + switch -- [llength $args] { + 0 { + return $features(trace) + } + 1 { + set features(trace) [lindex $args 0] + } + default { + return -code error "Usage: trace_stream_features ?tclProc?" + } + } +} + +# jlib::get_feature, have_feature -- +# +# Just to get access of the stream features. + +proc jlib::get_feature {jlibname name {name2 ""}} { + + upvar ${jlibname}::features features + + set ans "" + if {$name2 ne ""} { + if {[info exists features($name,$name2)]} { + set ans $features($name,$name2) + } + } else { + if {[info exists features($name)]} { + set ans $features($name) + } + } + return $ans +} + +proc jlib::have_feature {jlibname {name ""} {name2 ""}} { + + upvar ${jlibname}::features features + + set ans 0 + if {$name2 ne ""} { + if {[info exists features($name,$name2)]} { + set ans 1 + } + } elseif {$name ne ""} { + if {[info exists features($name)]} { + set ans 1 + } + } else { + if {[info exists features(xmllist)]} { + set ans 1 + } + } + return $ans +} + +# jlib::got_stream -- +# +# Callback when we have parsed the initial root element. +# +# Arguments: +# jlibname: the instance of this jlib. +# args: attributes +# +# Results: +# none. + +proc jlib::got_stream {jlibname args} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::locals locals + + Debug 4 "jlib::got_stream jlibname=$jlibname, args='$args'" + + # Cache stream attributes. + foreach {name value} $args { + set locals(streamattr,$name) $value + } + + # The streams 'from' attribute has the "last word" on the servers name. + if {[info exists locals(streamattr,from)]} { + set locals(server) $locals(streamattr,from) + set locals(servermap) [jidmap $locals(server)] + } + schedule_auto_away $jlibname + + # If we use we should have a callback command here. + if {[info exists lib(streamcmd)] && [llength $lib(streamcmd)]} { + uplevel #0 $lib(streamcmd) $jlibname $args + unset lib(streamcmd) + } +} + +# jlib::getthis -- +# +# Access function for: server, username, myjid, myjid2... + +proc jlib::getthis {jlibname name} { + + upvar ${jlibname}::locals locals + + if {[info exists locals($name)]} { + return $locals($name) + } else { + return + } +} + +# jlib::getstreamattr -- +# +# Returns the value of any stream attribute, typically 'id'. + +proc jlib::getstreamattr {jlibname name} { + + upvar ${jlibname}::locals locals + + if {[info exists locals(streamattr,$name)]} { + return $locals(streamattr,$name) + } else { + return + } +} + +# jlib::end_of_parse -- +# +# Callback when the ending root element is parsed. +# +# Arguments: +# jlibname: the instance of this jlib. +# +# Results: +# none. + +proc jlib::end_of_parse {jlibname} { + + upvar ${jlibname}::lib lib + + Debug 4 "jlib::end_of_parse jlibname=$jlibname" + + catch {eval $lib(transport,reset) $jlibname} + invoke_async_error $jlibname disconnect + reset $jlibname +} + +# jlib::error_handler -- +# +# Callback when receiving an stream:error element. According to xmpp-core +# this is an unrecoverable error (4.7.1) and the stream MUST be closed +# and the TCP connection also be closed. +# +# jabberd 1.4.3: Disconnected +# jabberd 1.4.4: +# +# +# +# + +proc jlib::error_handler {jlibname xmllist} { + + variable xmppxmlns + + Debug 4 "jlib::error_handler" + + # This should handle all internal stuff. + closestream $jlibname + + if {[llength [wrapper::getchildren $xmllist]]} { + set errspec [getstreamerrorspec $xmllist] + set errcode "xmpp-streams-error-[lindex $errspec 0]" + set errmsg [lindex $errspec 1] + } else { + set errcode xmpp-streams-error + set errmsg [wrapper::getcdata $xmllist] + } + invoke_async_error $jlibname $errcode $errmsg +} + +# jlib::xmlerror -- +# +# Callback when we receive an XML error from the wrapper (parser). +# +# Arguments: +# jlibname: the instance of this jlib. +# +# Results: +# none. + +proc jlib::xmlerror {jlibname args} { + + Debug 4 "jlib::xmlerror jlibname=$jlibname, args='$args'" + + # This should handle all internal stuff. + closestream $jlibname + invoke_async_error $jlibname xmlerror $args +} + +# jlib::reset -- +# +# Unsets all iqcmd($id) callback procedures. +# +# Arguments: +# jlibname: the instance of this jlib. +# +# Results: +# none. + +proc jlib::reset {jlibname} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::iqcmd iqcmd + upvar ${jlibname}::prescmd prescmd + upvar ${jlibname}::locals locals + upvar ${jlibname}::features features + + Debug 4 "jlib::reset" + + cancel_auto_away $jlibname + + set num $iqcmd(uid) + unset -nocomplain iqcmd + set iqcmd(uid) $num + + set num $prescmd(uid) + unset -nocomplain prescmd + set prescmd(uid) $num + + unset -nocomplain locals + unset -nocomplain features + + init_inst $jlibname + + set lib(isinstream) 0 + set lib(state) "reset" + + stream_reset $jlibname + if {[havesasl]} { + sasl_reset $jlibname + } + if {[havetls]} { + tls_reset $jlibname + } + + # Execute any register reset commands. + foreach cmd $lib(resetCmds) { + uplevel #0 $cmd $jlibname + } +} + +# jlib::stream_reset -- +# +# Clears out all variables that are cached for this stream. +# The xmpp specifies that any information obtained during tls,sasl +# must be discarded before opening a new stream. +# Call this before opening a new stream + +proc jlib::stream_reset {jlibname} { + + upvar ${jlibname}::locals locals + upvar ${jlibname}::features features + + array unset locals streamattr,* + + set cmd $features(trace) + unset -nocomplain features + set features(trace) $cmd +} + +# jlib::getstanzaerrorspec -- +# +# Extracts the error code and an error message from an type='error' +# element. We must handle both the original Jabber protocol and the +# XMPP protocol: +# +# The syntax for stanza-related errors is as follows (XMPP): +# +# +# [RECOMMENDED to include sender XML here] +# +# +# +# OPTIONAL descriptive text +# +# [OPTIONAL application-specific condition element] +# +# +# +# Jabber: +# +# +# +# ... +# +# +# +# or: +# +# +# ... +# +# +# or: +# +# ... +# Forbidden +# + +proc jlib::getstanzaerrorspec {stanza} { + + variable xmppxmlns + + set errcode "" + set errmsg "" + + # First search children of stanza ( element) for error element. + foreach child [wrapper::getchildren $stanza] { + set tag [wrapper::gettag $child] + if {[string equal $tag "error"]} { + set errelem $child + } + if {[string equal $tag "query"]} { + set queryelem $child + } + } + if {![info exists errelem] && [info exists queryelem]} { + + # Search children if element (Jabber). + set errlist [wrapper::getchildswithtag $queryelem "error"] + if {[llength $errlist]} { + set errelem [lindex $errlist 0] + } + } + + # Found it! XMPP contains an error stanza and not pure text. + if {[info exists errelem]} { + foreach {errcode errmsg} [geterrspecfromerror $errelem stanzas] {break} + } + return [list $errcode $errmsg] +} + +# jlib::getstreamerrorspec -- +# +# Extracts the error code and an error message from a stream:error +# element. We must handle both the original Jabber protocol and the +# XMPP protocol: +# +# The syntax for stream errors is as follows: +# +# +# +# +# OPTIONAL descriptive text +# +# [OPTIONAL application-specific condition element] +# +# +# Jabber: +# + +proc jlib::getstreamerrorspec {errelem} { + + return [geterrspecfromerror $errelem streams] +} + +# jlib::geterrspecfromerror -- +# +# Get an error specification from an stanza error element. +# +# Arguments: +# errelem: the element +# kind. 'stanzas' or 'streams' +# +# Results: +# {errcode errmsg} + +proc jlib::geterrspecfromerror {errelem kind} { + + variable xmppxmlns + variable errCodeToText + + array set msgproc { + stanzas stanzaerror::getmsg + streams streamerror::getmsg + } + set cchdata [wrapper::getcdata $errelem] + set errcode [wrapper::getattribute $errelem code] + set errmsg "Unknown" + + if {[string is integer -strict $errcode]} { + if {$cchdata ne ""} { + set errmsg $cchdata + } elseif {[info exists errCodeToText($errcode)]} { + set errmsg $errCodeToText($errcode) + } + } elseif {$cchdata ne ""} { + + # Old jabber way. + set errmsg $cchdata + } + + # xmpp way. + foreach c [wrapper::getchildren $errelem] { + set tag [wrapper::gettag $c] + + switch -- $tag { + text { + # Use only as a complement iff our language. ??? + set xmlns [wrapper::getattribute $c xmlns] + set lang [wrapper::getattribute $c xml:lang] + # [string equal $lang [getlang]] + if {[string equal $xmlns $xmppxmlns($kind)]} { + set errstr [wrapper::getcdata $c] + } + } + default { + set xmlns [wrapper::getattribute $c xmlns] + if {[string equal $xmlns $xmppxmlns($kind)]} { + set errcode $tag + set errstr [$msgproc($kind) $tag] + } + } + } + } + if {[info exists errstr]} { + set errmsg $errstr + } + if {$errmsg eq ""} { + set errmsg "Unknown" + } + return [list $errcode $errmsg] +} + +# jlib::bind_resource -- +# +# xmpp requires us to bind a resource to the stream. + +proc jlib::bind_resource {jlibname resource cmd} { + + variable xmppxmlns + + # If resource is an empty string request the server to create it. + set subtags [list] + if {$resource ne ""} { + set subtags [list [wrapper::createtag resource -chdata $resource]] + } + set xmllist [wrapper::createtag bind \ + -attrlist [list xmlns $xmppxmlns(bind)] -subtags $subtags] + send_iq $jlibname set [list $xmllist] \ + -command [list [namespace current]::parse_bind_resource $jlibname $cmd] +} + +proc jlib::parse_bind_resource {jlibname cmd type subiq args} { + + upvar ${jlibname}::locals locals + variable xmppxmlns + + # The server MAY change the 'resource' why we need to check this here. + if {[string equal [wrapper::gettag $subiq] bind] && \ + [string equal [wrapper::getattribute $subiq xmlns] $xmppxmlns(bind)]} { + set jidElem [wrapper::getfirstchildwithtag $subiq jid] + if {[llength $jidElem]} { + + # Server replies with full JID. + set sjid [wrapper::getcdata $jidElem] + splitjid $sjid sjid2 sresource + if {![string equal [resourcemap $locals(resource)] $sresource]} { + set locals(myjid) $sjid + set locals(myjid2) $sjid2 + set locals(resource) $sresource + set locals(myjidmap) [jidmap $sjid] + set locals(myjid2map) [jidmap $sjid2] + } + } + } + uplevel #0 $cmd [list $jlibname $type $subiq] +} + +# jlib::invoke_iq_callback -- +# +# Callback when we get server response on iq set/get. +# This is a generic callback procedure. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: the 'cmd' argument in the calling procedure. +# type: "error" or "ok". +# subiq: if type="error", this is a list {errcode errmsg}, +# else it is the query element as a xml list structure. +# +# Results: +# none. + +proc jlib::invoke_iq_callback {jlibname cmd type subiq} { + + Debug 3 "jlib::invoke_iq_callback cmd=$cmd, type=$type, subiq=$subiq" + + uplevel #0 $cmd [list $jlibname $type $subiq] +} + +# jlib::parse_search_set -- +# +# Callback for 'jabber:iq:search' 'result' and 'set' elements. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: the callback to notify. +# type: "ok", "error", or "set" +# subiq: + +proc jlib::parse_search_set {jlibname cmd type subiq} { + + upvar ${jlibname}::lib lib + + uplevel #0 $cmd [list $type $subiq] +} + +# jlib::iq_register -- +# +# Handler for registered iq callbacks. +# +# @@@ We could think of a more general mechanism here!!!! +# 1) Using -type, -xmlns, -from etc. + +proc jlib::iq_register {jlibname type xmlns func {seq 50}} { + + upvar ${jlibname}::iqhook iqhook + + lappend iqhook($type,$xmlns) [list $func $seq] + set iqhook($type,$xmlns) \ + [lsort -integer -index 1 [lsort -unique $iqhook($type,$xmlns)]] +} + +proc jlib::iq_run_hook {jlibname type xmlns from subiq args} { + + upvar ${jlibname}::iqhook iqhook + + set ishandled 0 + + foreach key [list $type,$xmlns *,$xmlns $type,*] { + if {[info exists iqhook($key)]} { + foreach spec $iqhook($key) { + set func [lindex $spec 0] + set code [catch { + uplevel #0 $func [list $jlibname $from $subiq] $args + } ans] + if {$code} { + bgerror "iqhook $func failed: $code\n$::errorInfo" + } + if {[string equal $ans "1"]} { + set ishandled 1 + break + } + } + } + if {$ishandled} { + break + } + } + return $ishandled +} + +# jlib::message_register -- +# +# Handler for registered message callbacks. +# +# We could think of a more general mechanism here!!!! + +proc jlib::message_register {jlibname type xmlns func {seq 50}} { + + upvar ${jlibname}::msghook msghook + + lappend msghook($type,$xmlns) [list $func $seq] + set msghook($type,$xmlns) \ + [lsort -integer -index 1 [lsort -unique $msghook($type,$xmlns)]] +} + +proc jlib::message_run_hook {jlibname type xmlns xmldata args} { + + upvar ${jlibname}::msghook msghook + + set ishandled 0 + + foreach key [list $type,$xmlns *,$xmlns $type,*] { + if {[info exists msghook($key)]} { + foreach spec $msghook($key) { + set func [lindex $spec 0] + set code [catch { + uplevel #0 $func [list $jlibname $xmlns $xmldata] $args + } ans] + if {$code} { + bgerror "msghook $func failed: $code\n$::errorInfo" + } + if {[string equal $ans "1"]} { + set ishandled 1 + break + } + } + } + if {$ishandled} { + break + } + } + return $ishandled +} + +# @@@ We keep two versions, internal for jlib usage and external for apps. +# Do this for all registered callbacks! + +# jlib::presence_register -- +# +# Handler for registered presence callbacks. Simple version. + +proc jlib::presence_register_int {jlibname type func {seq 50}} { + pres_reg $jlibname 1 $type $func $seq +} + +proc jlib::presence_register {jlibname type func {seq 50}} { + pres_reg $jlibname 0 $type $func $seq +} + +proc jlib::pres_reg {jlibname int type func {seq 50}} { + + upvar ${jlibname}::preshook preshook + + lappend preshook($int,$type) [list $func $seq] + set preshook($int,$type) \ + [lsort -integer -index 1 [lsort -unique $preshook($int,$type)]] +} + +proc jlib::presence_run_hook {jlibname int xmldata} { + + upvar ${jlibname}::preshook preshook + upvar ${jlibname}::locals locals + + set type [wrapper::getattribute $xmldata type] + set from [wrapper::getattribute $xmldata from] + if {$type eq ""} { + set type "available" + } + if {$from eq ""} { + set from $locals(server) + } + set ishandled 0 + + if {[info exists preshook($int,$type)]} { + foreach spec $preshook($int,$type) { + set func [lindex $spec 0] + set code [catch { + uplevel #0 $func [list $jlibname $xmldata] + } ans] + if {$code} { + bgerror "preshook $func failed: $code\n$::errorInfo" + } + if {[string equal $ans "1"]} { + set ishandled 1 + break + } + } + } + return $ishandled +} + +proc jlib::presence_deregister_int {jlibname type func} { + pres_dereg $jlibname 1 $type $func +} + +proc jlib::presence_deregister {jlibname type func} { + pres_dereg $jlibname 0 $type $func +} + +proc jlib::pres_dereg {jlibname int type func} { + + upvar ${jlibname}::preshook preshook + + if {[info exists preshook($int,$type)]} { + set idx [lsearch -glob $preshook($int,$type) "$func *"] + if {$idx >= 0} { + set preshook($int,$type) [lreplace $preshook($int,$type) $idx $idx] + } + } +} + +# jlib::presence_register_ex -- +# +# Set extended presence callbacks which can be triggered for +# various attributes and elements. +# +# The internal storage consists of two parts: +# 1) attributes; stored as array keys using wildcards (*) +# 2) elements : stored as a -tag .. -xmlns .. list +# +# expreshook($type,$from,$from2) {{{-key value ...} tclProc seq} {...} ...} +# +# These are matched separately but not independently. +# +# Arguments: +# jlibname: the instance of this jlib. +# func: tclProc +# args: -type type and from must match the presence element +# -from attributes +# -from2 match the bare from jid +# -tag tag and xmlns must coexist in the same element +# -xmlns for a valid match +# -seq priority 0-100 (D=50) +# +# Results: +# none. + +proc jlib::presence_register_ex_int {jlibname func args} { + eval {pres_reg_ex $jlibname 1 $func} $args +} + +proc jlib::presence_register_ex {jlibname func args} { + eval {pres_reg_ex $jlibname 0 $func} $args +} + +proc jlib::pres_reg_ex {jlibname int func args} { + + upvar ${jlibname}::expreshook expreshook + + set type "*" + set from "*" + set from2 "*" + set seq 50 + + foreach {key value} $args { + switch -- $key { + -from - -from2 { + set name [string trimleft $key "-"] + set $name [ESC $value] + } + -type { + set type $value + } + -tag - -xmlns { + set aopts($key) $value + } + -seq { + set seq $value + } + } + } + set pat "$type,$from,$from2" + + # The 'opts' must be ordered. + set opts [list] + foreach key [array names aopts] { + lappend opts $key $aopts($key) + } + lappend expreshook($int,$pat) [list $opts $func $seq] + set expreshook($int,$pat) \ + [lsort -integer -index 2 [lsort -unique $expreshook($int,$pat)]] +} + +proc jlib::presence_ex_run_hook {jlibname int xmldata} { + + upvar ${jlibname}::expreshook expreshook + upvar ${jlibname}::locals locals + + set type [wrapper::getattribute $xmldata type] + set from [wrapper::getattribute $xmldata from] + if {$type eq ""} { + set type "available" + } + if {$from eq ""} { + set from $locals(server) + } + set from2 [barejid $from] + set pkey "$int,$type,$from,$from2" + + # Make matching in two steps, attributes and elements. + # First the attributes. + set matched [list] + foreach {pat value} [array get expreshook $int,*] { + + if {[string match $pat $pkey]} { + + foreach spec $value { + + # Match attributes only if opts empty. + if {[lindex $spec 0] eq {}} { + set func [lindex $spec 1] + set code [catch { + uplevel #0 $func [list $jlibname $xmldata] + } ans] + if {$code} { + bgerror "preshook $func failed: $code\n$::errorInfo" + } + } else { + + # Collect all callbacks that match the attributes and have + # a nonempty element spec. + lappend matched $spec + } + } + } + } + + # Now try match the elements with the ones that matched the attributes. + if {[llength $matched]} { + + # Start by collecting all tags and xmlns we have in 'xmldata'. + set tagxmlns [list] + foreach c [wrapper::getchildren $xmldata] { + set xmlns [wrapper::getattribute $c xmlns] + lappend tagxmlns [list [wrapper::gettag $c] $xmlns] + } + + foreach spec $matched { + array set opts {-tag * -xmlns *} + array set opts [lindex $spec 0] + + # The 'olist' must be ordered. + set olist [list $opts(-tag) $opts(-xmlns)] + set idx [lsearch -glob $tagxmlns $olist] + if {$idx >= 0} { + set func [lindex $spec 1] + set code [catch { + uplevel #0 $func [list $jlibname $xmldata] + } ans] + if {$code} { + bgerror "preshook $func failed: $code\n$::errorInfo" + } + } + } + } +} + +proc jlib::presence_deregister_ex_int {jlibname func args} { + eval {pres_dereg_ex $jlibname 1 $func} $args +} + +proc jlib::presence_deregister_ex {jlibname func args} { + eval {pres_dereg_ex $jlibname 0 $func} $args +} + +proc jlib::pres_dereg_ex {jlibname int func args} { + + upvar ${jlibname}::expreshook expreshook + + set type "*" + set from "*" + set from2 "*" + set seq "*" + + foreach {key value} $args { + switch -- $key { + -from - -from2 { + set name [string trimleft $key "-"] + set $name [jlib::ESC $value] + } + -type { + set type $value + } + -tag - -xmlns { + set aopts($key) $value + } + -seq { + set seq $value + } + } + } + set pat "$type,$from,$from2" + if {[info exists expreshook($int,$pat)]} { + + # The 'opts' must be ordered. + set opts [list] + foreach key [array names aopts] { + lappend opts $key $aopts($key) + } + set idx [lsearch -glob $expreshook($int,$pat) [list $opts $func $seq]] + if {$idx >= 0} { + set expreshook($int,$pat) [lreplace $expreshook($int,$pat) $idx $idx] + if {$expreshook($int,$pat) eq {}} { + unset expreshook($int,$pat) + } + } + } +} + +# jlib::element_register -- +# +# Used to get callbacks from non stanza elements, like sasl etc. + +proc jlib::element_register {jlibname xmlns func {seq 50}} { + + upvar ${jlibname}::elementhook elementhook + + lappend elementhook($xmlns) [list $func $seq] + set elementhook($xmlns) \ + [lsort -integer -index 1 [lsort -unique $elementhook($xmlns)]] +} + +proc jlib::element_deregister {jlibname xmlns func} { + + upvar ${jlibname}::elementhook elementhook + + if {![info exists elementhook($xmlns)]} { + return + } + set ind -1 + set found 0 + foreach spec $elementhook($xmlns) { + incr ind + if {[string equal $func [lindex $spec 0]]} { + set found 1 + break + } + } + if {$found} { + set elementhook($xmlns) [lreplace $elementhook($xmlns) $ind $ind] + } +} + +proc jlib::element_run_hook {jlibname xmldata} { + + upvar ${jlibname}::elementhook elementhook + + set ishandled 0 + set xmlns [wrapper::getattribute $xmldata xmlns] + + if {[info exists elementhook($xmlns)]} { + foreach spec $elementhook($xmlns) { + set func [lindex $spec 0] + set code [catch { + uplevel #0 $func [list $jlibname $xmldata] + } ans] + if {$code} { + bgerror "preshook $func failed: $code\n$::errorInfo" + } + if {[string equal $ans "1"]} { + set ishandled 1 + break + } + } + } + return $ishandled +} + +# This part is supposed to be a maximal flexible event register mechanism. +# +# Bind: stanza (presence, iq, message,...) +# its attributes (optional) +# any child tag name (optional) +# its attributes (optional) +# +# genhook(stanza) = {{attrspec childspec func seq} ...} +# +# with: attrspec = {name1 value1 name2 value2 ...} +# childspec = {tag attrspec} + +# jlib::general_register -- +# +# A mechanism to register for almost any kind of elements. + +proc jlib::general_register {jlibname tag attrspec childspec func {seq 50}} { + + upvar ${jlibname}::genhook genhook + + lappend genhook($tag) [list $attrspec $childspec $func $seq] + set genhook($tag) \ + [lsort -integer -index 3 [lsort -unique $genhook($tag)]] +} + +proc jlib::general_run_hook {jlibname xmldata} { + + upvar ${jlibname}::genhook genhook + + set ishandled 0 + set tag [wrapper::gettag $xmldata] + if {[info exists genhook($tag)]} { + foreach spec $genhook($tag) { + lassign $spec attrspec childspec func seq + lassign $childspec ctag cattrspec + if {![match_attr $attrspec [wrapper::getattrlist $xmldata]]} { + continue + } + + # Search child elements for matches. + set match 0 + foreach c [wrapper::getchildren $xmldata] { + if {$ctag ne "" && $ctag ne [wrapper::gettag $c]} { + continue + } + if {![match_attr $cattrspec [wrapper::getattrlist $c]]} { + continue + } + set match 1 + break + } + if {!$match} { + continue + } + + # If the spec survived here it matched. + set code [catch { + uplevel #0 $func [list $jlibname $xmldata] + } ans] + if {$code} { + bgerror "genhook $func failed: $code\n$::errorInfo" + } + if {[string equal $ans "1"]} { + set ishandled 1 + break + } + } + } + return $ishandled +} + +proc jlib::match_attr {attrspec attr} { + + array set attrA $attr + foreach {name value} $attrspec { + if {![info exists attrA($name)]} { + return 0 + } elseif {$value ne $attrA($name)} { + return 0 + } + } + return 1 +} + +proc jlib::general_deregister {jlibname tag attrspec childspec func} { + + upvar ${jlibname}::genhook genhook + + if {[info exists genhook($tag)]} { + set idx [lsearch -glob $genhook($tag) [list $attrspec $childspec $func *]] + if {$idx >= 0} { + set genhook($tag) [lreplace $genhook($tag) $idx $idx] + + } + } +} + +# Test code... +if {0} { + proc cb {args} {puts "************** $args"} + set childspec [list query [list xmlns "http://jabber.org/protocol/disco#items"]] + ::jlib::jlib1 general_register iq {} $childspec cb + ::jlib::jlib1 general_deregister iq {} $childspec cb + + +} + +# jlib::send_iq -- +# +# To send an iq (info/query) packet. +# +# Arguments: +# jlibname: the instance of this jlib. +# type: can be "get", "set", "result", or "error". +# "result" and "error" are used when replying an incoming iq. +# xmldata: list of elements as xmllists +# args: +# -to $to : Specify jid to send this packet to. If it +# isn't specified, this part is set to sender's user-id by +# the server. +# +# -id $id : Specify an id to send with the . +# If $type is "get", or "set", then the id will be generated +# by jlib internally, and this switch will not work. +# If $type is "result" or "error", then you may use this +# switch. +# +# -command $cmd : Specify a callback to call when the +# reply-packet is got. This switch will not work if $type +# is "result" or "error". +# +# Results: +# none. + +proc jlib::send_iq {jlibname type xmldata args} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::iqcmd iqcmd + + Debug 3 "jlib::send_iq type='$type', xmldata='$xmldata', args='$args'" + + array set argsA $args + set attrlist [list "type" $type] + + # Need to generate a unique identifier (id) for this packet. + if {[string equal $type "get"] || [string equal $type "set"]} { + lappend attrlist "id" $iqcmd(uid) + + # Record any callback procedure. + if {[info exists argsA(-command)] && ($argsA(-command) ne "")} { + set iqcmd($iqcmd(uid)) $argsA(-command) + } + incr iqcmd(uid) + } elseif {[info exists argsA(-id)]} { + lappend attrlist "id" $argsA(-id) + } + unset -nocomplain argsA(-id) argsA(-command) + foreach {key value} [array get argsA] { + set name [string trimleft $key -] + lappend attrlist $name $value + } + set xmllist [wrapper::createtag "iq" -attrlist $attrlist -subtags $xmldata] + + send $jlibname $xmllist + return +} + +# jlib::iq_get, iq_set -- +# +# Wrapper for 'send_iq' for set/getting namespaced elements. +# +# Arguments: +# jlibname: the instance of this jlib. +# xmlns: +# args: -to recepient jid +# -command procName +# -sublists +# else as attributes +# +# Results: +# none. + +proc jlib::iq_get {jlibname xmlns args} { + + set opts [list] + set sublists [list] + set attrlist [list xmlns $xmlns] + foreach {key value} $args { + + switch -- $key { + -command { + lappend opts -command \ + [list [namespace current]::invoke_iq_callback $jlibname $value] + } + -to { + lappend opts -to $value + } + -sublists { + set sublists $value + } + default { + lappend attrlist [string trimleft $key "-"] $value + } + } + } + set xmllist [wrapper::createtag "query" -attrlist $attrlist \ + -subtags $sublists] + eval {send_iq $jlibname "get" [list $xmllist]} $opts + return +} + +proc jlib::iq_set {jlibname xmlns args} { + + set opts [list] + set sublists [list] + foreach {key value} $args { + + switch -- $key { + -command { + lappend opts -command \ + [list [namespace current]::invoke_iq_callback $jlibname $value] + } + -to { + lappend opts -to $value + } + -sublists { + set sublists $value + } + default { + #lappend subelements [wrapper::createtag \ + # [string trimleft $key -] -chdata $value] + } + } + } + set xmllist [wrapper::createtag "query" -attrlist [list xmlns $xmlns] \ + -subtags $sublists] + eval {send_iq $jlibname "set" [list $xmllist]} $opts + return +} + +# jlib::send_auth -- +# +# Send simple client authentication. +# It implements the 'jabber:iq:auth' set method. +# +# Arguments: +# jlibname: the instance of this jlib. +# username: +# resource: +# cmd: client command to be executed at the iq "result" element. +# args: Any of "-password" or "-digest" must be given. +# -password +# -digest +# -to +# +# Results: +# none. + +proc jlib::send_auth {jlibname username resource cmd args} { + + upvar ${jlibname}::locals locals + + set subelements [list \ + [wrapper::createtag "username" -chdata $username] \ + [wrapper::createtag "resource" -chdata $resource]] + set toopt [list] + + foreach {key value} $args { + switch -- $key { + -password - -digest { + lappend subelements [wrapper::createtag \ + [string trimleft $key -] -chdata $value] + } + -to { + set toopt [list -to $value] + } + } + } + + # Cache our login jid. + set myjid ${username}@$locals(server)/${resource} + set myjid2 ${username}@$locals(server) + + set locals(username) $username + set locals(resource) $resource + set locals(myjid) $myjid + set locals(myjid2) $myjid2 + set locals(myjidmap) [jidmap $myjid] + set locals(myjid2map) [jidmap $myjid2] + + set xmllist [wrapper::createtag "query" -attrlist {xmlns jabber:iq:auth} \ + -subtags $subelements] + eval {send_iq $jlibname "set" [list $xmllist] -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd]} $toopt + + return +} + +# jlib::register_get -- +# +# Sent with a blank query to retrieve registration information. +# Retrieves a key for use on future registration pushes. +# It implements the 'jabber:iq:register' get method. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: client command to be executed at the iq "result" element. +# args: -to : the jid for the service +# +# Results: +# none. + +proc jlib::register_get {jlibname cmd args} { + + array set argsA $args + set xmllist [wrapper::createtag "query" -attrlist {xmlns jabber:iq:register}] + if {[info exists argsA(-to)]} { + set toopt [list -to $argsA(-to)] + } else { + set toopt "" + } + eval {send_iq $jlibname "get" [list $xmllist] -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd]} $toopt + return +} + +# jlib::register_set -- +# +# Create a new account with the server, or to update user information. +# It implements the 'jabber:iq:register' set method. +# +# Arguments: +# jlibname: the instance of this jlib. +# username: +# password: +# cmd: client command to be executed at the iq "result" element. +# args: -to : the jid for the service +# -nick : +# -name : +# -first : +# -last : +# -email : +# -address : +# -city : +# -state : +# -zip : +# -phone : +# -url : +# -date : +# -misc : +# -text : +# -key : +# +# Results: +# none. + +proc jlib::register_set {jlibname username password cmd args} { + + set subelements [list \ + [wrapper::createtag "username" -chdata $username] \ + [wrapper::createtag "password" -chdata $password]] + array set argsA $args + foreach argsswitch [array names argsA] { + if {[string equal $argsswitch "-to"]} { + continue + } + set par [string trimleft $argsswitch {-}] + lappend subelements [wrapper::createtag $par \ + -chdata $argsA($argsswitch)] + } + set xmllist [wrapper::createtag "query" \ + -attrlist {xmlns jabber:iq:register} \ + -subtags $subelements] + + if {[info exists argsA(-to)]} { + set toopt [list -to $argsA(-to)] + } else { + set toopt "" + } + eval {send_iq $jlibname "set" [list $xmllist] -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd]} $toopt + return +} + +# jlib::register_remove -- +# +# It implements the 'jabber:iq:register' set method with a tag. +# +# Arguments: +# jlibname: the instance of this jlib. +# to: +# cmd: client command to be executed at the iq "result" element. +# args -key +# +# Results: +# none. + +proc jlib::register_remove {jlibname to cmd args} { + + set subelements [list [wrapper::createtag "remove"]] + array set argsA $args + if {[info exists argsA(-key)]} { + lappend subelements [wrapper::createtag "key" -chdata $argsA(-key)] + } + set xmllist [wrapper::createtag "query" \ + -attrlist {xmlns jabber:iq:register} -subtags $subelements] + + eval {send_iq $jlibname "set" [list $xmllist] -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd]} -to $to + return +} + +# jlib::search_get -- +# +# Sent with a blank query to retrieve search information. +# Retrieves a key for use on future search pushes. +# It implements the 'jabber:iq:search' get method. +# +# Arguments: +# jlibname: the instance of this jlib. +# to: this must be a searchable jud service, typically +# 'jud.jabber.org'. +# cmd: client command to be executed at the iq "result" element. +# +# Results: +# none. + +proc jlib::search_get {jlibname to cmd} { + + set xmllist [wrapper::createtag "query" -attrlist {xmlns jabber:iq:search}] + send_iq $jlibname "get" [list $xmllist] -to $to -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd] + return +} + +# jlib::search_set -- +# +# Makes an actual search in our roster at the server. +# It implements the 'jabber:iq:search' set method. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: client command to be executed at the iq "result" element. +# to: this must be a searchable jud service, typically +# 'jud.jabber.org'. +# args: -subtags list +# +# Results: +# none. + +proc jlib::search_set {jlibname to cmd args} { + + set argsA(-subtags) [list] + array set argsA $args + + set xmllist [wrapper::createtag "query" \ + -attrlist {xmlns jabber:iq:search} \ + -subtags $argsA(-subtags)] + send_iq $jlibname "set" [list $xmllist] -to $to -command \ + [list [namespace current]::parse_search_set $jlibname $cmd] + + return +} + +# jlib::send_message -- +# +# Sends a message element. +# +# Arguments: +# jlibname: the instance of this jlib. +# to: the jabber id of the receiver. +# args: +# -subject $subject : Set subject of the message to +# $subject. +# +# -thread $thread : Set thread of the message to +# $thread. +# +# -priority $priority : Set priority of the message to +# $priority. +# +# -body text : +# +# -type $type : normal, chat or groupchat +# +# -id token +# +# -from : only for internal use, never send +# +# -xlist $xlist : A list containing *X* xml_data. +# Anything can be put inside an *X*. Please make sure you +# created it with "wrapper::createtag" procedure, +# and also, it has a "xmlns" attribute in its root tag. +# +# -command +# +# Results: +# none. + +proc jlib::send_message {jlibname to args} { + + upvar ${jlibname}::msgcmd msgcmd + upvar ${jlibname}::locals locals + + Debug 3 "jlib::send_message to=$to, args=$args" + + array set argsA $args + if {[info exists argsA(-command)]} { + set uid $msgcmd(uid) + set msgcmd($uid) $argsA(-command) + incr msgcmd(uid) + lappend args -id $uid + unset argsA(-command) + + # There exist a weird situation if we send to ourself. + # Skip this registered command the 1st time we get this, + # and let any handlers take over. Trigger this 2nd time. + if {[string equal $to $locals(myjidmap)]} { + set msgcmd($uid,self) 1 + } + + } + set xmllist [eval {send_message_xmllist $to} [array get argsA]] + send $jlibname $xmllist + return +} + +# jlib::send_message_xmllist -- +# +# Create the xml list for send_message. + +proc jlib::send_message_xmllist {to args} { + + array set argsA $args + set attr [list to $to] + set children [list] + + foreach {name value} $args { + set par [string trimleft $name "-"] + + switch -- $name { + -xlist { + foreach xchild $value { + lappend children $xchild + } + } + -type { + if {![string equal $value "normal"]} { + lappend attr "type" $value + } + } + -id - -from { + lappend attr $par $value + } + default { + lappend children [wrapper::createtag $par -chdata $value] + } + } + } + return [wrapper::createtag "message" -attrlist $attr -subtags $children] +} + +# jlib::send_presence -- +# +# To send your presence. +# +# Arguments: +# +# jlibname: the instance of this jlib. +# args: +# -keep 0|1 (D=0) we may keep the present 'status' and 'show' +# elements for undirected presence +# -to the JID of the recepient. +# -from should never be set by client! +# -type one of 'available', 'unavailable', 'subscribe', +# 'unsubscribe', 'subscribed', 'unsubscribed', 'invisible'. +# -status +# -priority persistant option if undirected presence +# -show +# -xlist +# -extras +# -command Specify a callback to call if we may expect any reply +# package, as entering a room with 'gc-1.0'. +# +# Results: +# none. + +proc jlib::send_presence {jlibname args} { + + variable statics + upvar ${jlibname}::locals locals + upvar ${jlibname}::opts opts + upvar ${jlibname}::prescmd prescmd + upvar ${jlibname}::pres pres + + Debug 3 "jlib::send_presence args='$args'" + + set attrlist [list] + set children [list] + set directed 0 + set keep 0 + set type "available" + array set argsA $args + + foreach {key value} $args { + set par [string trimleft $key -] + + switch -- $key { + -command { + lappend attrlist "id" $prescmd(uid) + set prescmd($prescmd(uid)) $value + incr prescmd(uid) + } + -extras - -xlist { + foreach xchild $value { + lappend children $xchild + } + } + -from { + # Should never happen! + lappend attrlist $par $value + } + -keep { + set keep $value + } + -priority - -show { + lappend children [wrapper::createtag $par -chdata $value] + } + -status { + if {$value ne ""} { + lappend children [wrapper::createtag $par -chdata $value] + } + } + -to { + # Presence to server (undirected) shall not contain a to. + if {$value ne $locals(servermap)} { + lappend attrlist $par $value + set directed 1 + } + } + -type { + set type $value + if {[regexp $statics(presenceTypeExp) $type]} { + lappend attrlist $par $type + } else { + return -code error "Is not valid presence type: \"$type\"" + } + } + default { + return -code error "unrecognized option \"$value\"" + } + } + } + + # Must be destined to login server (by default). + if {!$directed} { + + # Each and every presence stanza MUST contain the complete presence + # state of the client. As a convinience we cache previous states and + # may use them if not set explicitly: + # 1. + # 2. + # 3. Always reused if cached + + foreach name {show status} { + if {[info exists argsA(-$name)]} { + set locals(pres,$name) $argsA(-$name) + } elseif {[info exists locals(pres,$name)]} { + if {$keep} { + lappend children [wrapper::createtag $name \ + -chdata $locals(pres,$name)] + } else { + unset -nocomplain locals(pres,$name) + } + } + } + if {[info exists argsA(-priority)]} { + set locals(pres,priority) $argsA(-priority) + } elseif {[info exists locals(pres,priority)]} { + lappend children [wrapper::createtag "priority" \ + -chdata $locals(pres,priority)] + } + + set locals(pres,type) $type + + set locals(status) $type + if {[info exists argsA(-show)]} { + set locals(status) $argsA(-show) + set locals(pres,show) $argsA(-show) + } + } + + # Assemble our registered presence stanzas. Only for undirected? + foreach {key elem} [array get pres "stanza,*,"] { + lappend children $elem + } + foreach {key elem} [array get pres "stanza,*,$type"] { + lappend children $elem + } + + set xmllist [wrapper::createtag "presence" -attrlist $attrlist \ + -subtags $children] + send $jlibname $xmllist + + return +} + +# jlib::register_presence_stanza, ... -- +# +# Each presence element we send to the server (undirected) must contain +# the complete state. This is a way to add custom presence stanzas +# to our internal presence state to send each time we set our presence +# with the server (undirected presence). +# They are stored by tag, xmlns, and an optional type attribute. +# Any existing presence stanza with identical tag/xmlns/type will +# be replaced. +# +# Arguments: +# jlibname: the instance of this jlib +# elem: xml element +# args -type available | unavailable | ... + +proc jlib::register_presence_stanza {jlibname elem args} { + + upvar ${jlibname}::pres pres + + set argsA(-type) "" + array set argsA $args + set type $argsA(-type) + + set tag [wrapper::gettag $elem] + set xmlns [wrapper::getattribute $elem xmlns] + set pres(stanza,$tag,$xmlns,$type) $elem +} + +proc jlib::deregister_presence_stanza {jlibname tag xmlns} { + + upvar ${jlibname}::pres pres + + array unset pres "stanza,$tag,$xmlns,*" +} + +proc jlib::get_registered_presence_stanzas {jlibname {tag *} {xmlns *}} { + + upvar ${jlibname}::pres pres + + set stanzas [list] + foreach key [array names pres -glob stanza,$tag,$xmlns,*] { + lassign [split $key ,] - t x type + set spec [list $t $x $pres($key)] + if {$type ne ""} { + lappend spec -type $type + } + lappend stanzas $spec + } + return $stanzas +} + +# jlib::send -- +# +# Sends general xml using a xmllist. +# Never throws error. Network errors reported via callback. + +proc jlib::send {jlibname xmllist} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::locals locals + + # For the auto away function. + if {$locals(trigAutoAway)} { + schedule_auto_away $jlibname + } + set locals(last) [clock seconds] + set xml [wrapper::createxml $xmllist] + foreach cmd $lib(tee,send) { + uplevel #0 $cmd [list $jlibname $xmllist] + } + + # We fail only if already in stream. + # The first failure reports the network error, closes the stream, + # which stops multiple errors to be reported to the client. + if {$lib(isinstream)} { + if {[catch { + uplevel #0 $lib(transport,send) [list $jlibname $xml] + } err]} { + kill $jlibname + invoke_async_error $jlibname networkerror + } + } + return +} + +# jlib::sendraw -- +# +# Send raw xml. The caller is responsible for catching errors. + +proc jlib::sendraw {jlibname xml} { + + upvar ${jlibname}::lib lib + + uplevel #0 $lib(transport,send) [list $jlibname $xml] +} + +# jlib::mypresence -- +# +# Returns any of {available away xa chat dnd invisible unavailable} +# for our status with the login server. + +proc jlib::mypresence {jlibname} { + + upvar ${jlibname}::locals locals + + if {[info exists locals(pres,show)]} { + return $locals(pres,show) + } else { + return $locals(pres,type) + } +} + +proc jlib::mypresencestatus {jlibname} { + + upvar ${jlibname}::locals locals + + if {[info exists locals(pres,status)]} { + return $locals(pres,status) + } else { + return "" + } +} + +# jlib::myjid -- +# +# Returns our 3-tier jid as authorized with the login server. + +proc jlib::myjid {jlibname} { + upvar ${jlibname}::locals locals + return $locals(myjid) +} + +proc jlib::myjid2 {jlibname} { + upvar ${jlibname}::locals locals + return $locals(myjid2) +} + +proc jlib::myjidmap {jlibname} { + upvar ${jlibname}::locals locals + return $locals(myjidmap) +} + +proc jlib::myjid2map {jlibname} { + upvar ${jlibname}::locals locals + return $locals(myjid2map) +} + +# jlib::oob_set -- +# +# It implements the 'jabber:iq:oob' set method. +# +# Arguments: +# jlibname: the instance of this jlib. +# to: +# cmd: client command to be executed at the iq "result" element. +# url: +# args: +# -desc +# +# Results: +# none. + +proc jlib::oob_set {jlibname to cmd url args} { + + set attrlist {xmlns jabber:iq:oob} + set children [list [wrapper::createtag "url" -chdata $url]] + array set argsA $args + if {[info exists argsA(-desc)] && [string length $argsA(-desc)]} { + lappend children [wrapper::createtag "desc" -chdata $argsA(-desc)] + } + set xmllist [wrapper::createtag query -attrlist $attrlist \ + -subtags $children] + send_iq $jlibname set [list $xmllist] -to $to -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd] + return +} + +# jlib::get_last -- +# +# Query the 'last' of 'to' using 'jabber:iq:last' get. + +proc jlib::get_last {jlibname to cmd} { + + set xmllist [wrapper::createtag "query" \ + -attrlist {xmlns jabber:iq:last}] + send_iq $jlibname "get" [list $xmllist] -to $to -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd] + return +} + +# jlib::handle_get_last -- +# +# Seconds since last activity. Response to 'jabber:iq:last' get. + +proc jlib::handle_get_last {jlibname from subiq args} { + + upvar ${jlibname}::locals locals + + array set argsA $args + + set secs [expr [clock seconds] - $locals(last)] + set xmllist [wrapper::createtag "query" \ + -attrlist [list xmlns jabber:iq:last seconds $secs]] + + set opts [list] + if {[info exists argsA(-from)]} { + lappend opts -to $argsA(-from) + } + if {[info exists argsA(-id)]} { + lappend opts -id $argsA(-id) + } + eval {send_iq $jlibname "result" [list $xmllist]} $opts + + # Tell jlib's iq-handler that we handled the event. + return 1 +} + +# jlib::get_time -- +# +# Query the 'time' of 'to' using 'jabber:iq:time' get. + +proc jlib::get_time {jlibname to cmd} { + + set xmllist [wrapper::createtag "query" \ + -attrlist {xmlns jabber:iq:time}] + send_iq $jlibname "get" [list $xmllist] -to $to -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd] + return +} + +# jlib::handle_get_time -- +# +# Send our time. Response to 'jabber:iq:time' get. + +proc jlib::handle_get_time {jlibname from subiq args} { + + array set argsA $args + + # Applications using 'jabber:iq:time' SHOULD use the old format, + # not the format defined in XEP-0082. + set secs [clock seconds] + set utc [clock format $secs -format "%Y%m%dT%H:%M:%S" -gmt 1] + set tz "GMT" + set display [clock format $secs] + set subtags [list \ + [wrapper::createtag "utc" -chdata $utc] \ + [wrapper::createtag "tz" -chdata $tz] \ + [wrapper::createtag "display" -chdata $display] ] + set xmllist [wrapper::createtag "query" -subtags $subtags \ + -attrlist {xmlns jabber:iq:time}] + + set opts [list] + if {[info exists argsA(-from)]} { + lappend opts -to $argsA(-from) + } + if {[info exists argsA(-id)]} { + lappend opts -id $argsA(-id) + } + eval {send_iq $jlibname "result" [list $xmllist]} $opts + + # Tell jlib's iq-handler that we handled the event. + return 1 +} + +# Support for XEP-0202 Entity Time. + +proc jlib::get_entity_time {jlibname to cmd} { + variable jxmlns + + set xmllist [wrapper::createtag "time" \ + -attrlist [list xmlns $jxmlns(entitytime)]] + send_iq $jlibname "get" [list $xmllist] -to $to -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd] + return +} + +proc jlib::handle_entity_time {jlibname from subiq args} { + variable jxmlns + + array set argsA $args + + # Figure out our time zone in terms of HH:MM. + # Compare with the GMT time and take the diff. Avoid year wrap around. + set secs [clock seconds] + set day [clock format $secs -format "%j"] + if {$day eq "001"} { + incr secs [expr {24*60*60}] + } elseif {($day eq "365") || ($day eq "366")} { + incr secs [expr {-2*24*60*60}] + } + set format "%S + 60*(%M + 60*(%H + 24*%j))" + set local [clock format $secs -format $format] + set gmt [clock format $secs -format $format -gmt 1] + + # Remove leading zeros since they will be interpreted as octals. + regsub -all {0+([1-9]+)} $local {\1} local + regsub -all {0+([1-9]+)} $gmt {\1} gmt + set local [expr $local] + set gmt [expr $gmt] + set mindiff [expr {($local - $gmt)/60}] + set sign [expr {$mindiff >= 0 ? "" : "-"}] + set zhour [expr {abs($mindiff)/60}] + set zmin [expr {$mindiff % 60}] + set tzo [format "$sign%.2d:%.2d" $zhour $zmin] + + # Time format according to XEP-0082 (XMPP Date and Time Profiles). + # 2006-12-19T17:58:35Z + set utc [clock format $secs -format "%Y-%m-%dT%H:%M:%SZ" -gmt 1] + + set subtags [list \ + [wrapper::createtag "tzo" -chdata $tzo] \ + [wrapper::createtag "utc" -chdata $utc] ] + set xmllist [wrapper::createtag "time" -subtags $subtags \ + -attrlist [list xmlns $jxmlns(entitytime)]] + + set opts [list] + if {[info exists argsA(-from)]} { + lappend opts -to $argsA(-from) + } + if {[info exists argsA(-id)]} { + lappend opts -id $argsA(-id) + } + eval {send_iq $jlibname "result" [list $xmllist]} $opts + return 1 +} + +# jlib::get_version -- +# +# Query the 'version' of 'to' using 'jabber:iq:version' get. + +proc jlib::get_version {jlibname to cmd} { + + set xmllist [wrapper::createtag "query" \ + -attrlist {xmlns jabber:iq:version}] + send_iq $jlibname "get" [list $xmllist] -to $to -command \ + [list [namespace current]::invoke_iq_callback $jlibname $cmd] + return +} + +# jlib::handle_get_version -- +# +# Send our version. Response to 'jabber:iq:version' get. + +proc jlib::handle_get_version {jlibname from subiq args} { + global prefs tcl_platform + variable version + + array set argsA $args + + # Return any id! + set opts [list] + if {[info exists argsA(-id)]} { + set opts [list -id $argsA(-id)] + } + set os $tcl_platform(os) + if {[info exists tcl_platform(osVersion)]} { + append os " " $tcl_platform(osVersion) + } + lappend opts -to $from + set subtags [list \ + [wrapper::createtag name -chdata "JabberLib"] \ + [wrapper::createtag version -chdata $version] \ + [wrapper::createtag os -chdata $os] ] + set xmllist [wrapper::createtag query -subtags $subtags \ + -attrlist {xmlns jabber:iq:version}] + eval {send_iq $jlibname "result" [list $xmllist]} $opts + + # Tell jlib's iq-handler that we handled the event. + return 1 +} + +# jlib::schedule_keepalive -- +# +# Supposed to detect network failures but seems not to work like that. + +proc jlib::schedule_keepalive {jlibname} { + + upvar ${jlibname}::locals locals + upvar ${jlibname}::opts opts + upvar ${jlibname}::lib lib + + if {$opts(-keepalivesecs) && $lib(isinstream)} { + if {[catch { + uplevel #0 $lib(transport,send) [list $jlibname "\n"] + flush $lib(sock) + } err]} { + kill $jlibname + invoke_async_error $jlibname networkerror + } else { + set locals(aliveid) [after [expr 1000 * $opts(-keepalivesecs)] \ + [list [namespace current]::schedule_keepalive $jlibname]] + } + } +} + +# OUTDATED !!!!!!!!!!!!!!!!!!!! + +# jlib::schedule_auto_away, cancel_auto_away, auto_away_cmd +# +# Procedures for auto away things. +# Better to use 'tk inactive' or 'tkinactive' and handle this on +# application level. + +proc jlib::schedule_auto_away {jlibname} { + + upvar ${jlibname}::locals locals + upvar ${jlibname}::opts opts + + cancel_auto_away $jlibname + if {$opts(-autoawaymins) > 0} { + set locals(afterawayid) [after [expr 60000 * $opts(-autoawaymins)] \ + [list [namespace current]::auto_away_cmd $jlibname away]] + } + if {$opts(-xautoawaymins) > 0} { + set locals(afterxawayid) [after [expr 60000 * $opts(-xautoawaymins)] \ + [list [namespace current]::auto_away_cmd $jlibname xaway]] + } +} + +proc jlib::cancel_auto_away {jlibname} { + + upvar ${jlibname}::locals locals + + if {[info exists locals(afterawayid)]} { + after cancel $locals(afterawayid) + unset locals(afterawayid) + } + if {[info exists locals(afterxawayid)]} { + after cancel $locals(afterxawayid) + unset locals(afterxawayid) + } +} + +# jlib::auto_away_cmd -- +# +# what: "away", or "xaway" +# +# @@@ Replaced by idletime and AutoAway + +proc jlib::auto_away_cmd {jlibname what} { + + variable statusPriority + upvar ${jlibname}::locals locals + upvar ${jlibname}::lib lib + upvar ${jlibname}::opts opts + + Debug 3 "jlib::auto_away_cmd what=$what" + + if {$what eq "xaway"} { + set status xa + } else { + set status $what + } + + # Auto away and extended away are only set when the + # current status has a lower priority than away or xa respectively. + if {$statusPriority($locals(status)) >= $statusPriority($status)} { + return + } + + # Be sure not to trig ourselves. + set locals(trigAutoAway) 0 + + switch -- $what { + away { + send_presence $jlibname -show "away" -status $opts(-awaymsg) + } + xaway { + send_presence $jlibname -show "xa" -status $opts(-xawaymsg) + } + } + set locals(trigAutoAway) 1 + uplevel #0 $lib(clientcmd) [list $jlibname $status] +} + +# jlib::getrecipientjid -- +# +# Tries to obtain the correct form of jid to send message to. +# Follows the XMPP spec, section 4.1. +# +# @@@ Perhaps this should go in app code? + +proc jlib::getrecipientjid {jlibname jid} { + variable statics + + set jid2 [barejid $jid] + set isroom [[namespace current]::service::isroom $jlibname $jid2] + if {$isroom} { + return $jid + } elseif {[info exists statics(roster)] && \ + [$jlibname roster isavailable $jid]} { + return $jid + } else { + return $jid2 + } +} + +proc jlib::getlang {} { + + if {[catch {package require msgcat}]} { + return en + } else { + set lang [lindex [::msgcat::mcpreferences] end] + + switch -- $lang { + "" - c - posix { + return en + } + default { + return $lang + } + } + } +} + +namespace eval jlib { + + # We just the http error codes here since may be useful if we only + # get the 'code' attribute in an error element. + # @@@ Add to message catalogs. + variable errCodeToText + array set errCodeToText { + 100 "Continue" + 101 "Switching Protocols" + 200 "OK" + 201 "Created" + 202 "Accepted" + 203 "Non-Authoritative Information" + 204 "No Content" + 205 "Reset Content" + 206 "Partial Content" + 300 "Multiple Choices" + 301 "Moved Permanently" + 302 "Found" + 303 "See Other" + 304 "Not Modified" + 305 "Use Proxy" + 307 "Temporary Redirect" + 400 "Bad Request" + 401 "Unauthorized" + 402 "Payment Required" + 403 "Forbidden" + 404 "Not Found" + 405 "Method Not Allowed" + 406 "Not Acceptable" + 407 "Proxy Authentication Required" + 408 "Request Time-out" + 409 "Conflict" + 410 "Gone" + 411 "Length Required" + 412 "Precondition Failed" + 413 "Request Entity Too Large" + 414 "Request-URI Too Large" + 415 "Unsupported Media Type" + 416 "Requested Range Not Satisfiable" + 417 "Expectation Failed" + 500 "Internal Server Error" + 501 "Not Implemented" + 502 "Bad Gateway" + 503 "Service Unavailable" + 504 "Gateway Time-out" + 505 "HTTP Version not supported" + } +} + +# Various utility procedures to handle jid's.................................... + +# jlib::ESC -- +# +# array get and array unset accepts glob characters. These need to be +# escaped if they occur as part of a JID. +# NB1: 'string match pattern str' MUST have pattern escaped! +# NB2: This also applies to 'lsearch'! + +proc jlib::ESC {s} { + return [string map {* \\* ? \\? [ \\[ ] \\] \\ \\\\} $s] +} + +# STRINGPREPs for the differnt parts of jids. + +proc jlib::UnicodeListToRE {ulist} { + + set str [string map {- -\\u} $ulist] + set str "\\u[join $str \\u]" + return [subst $str] +} + +# jlib::MakeHexHexEscList -- +# +# Takes a list of characters and transforms them to their hexhex form. +# Used by: XEP-0106: JID Escaping + +proc jlib::MakeHexHexEscList {clist} { + + set hexlist [list] + foreach c $clist { + scan $c %c n + lappend hexlist [format %x $n] + } + return $hexlist +} + +proc jlib::MakeHexHexCharMap {clist} { + + set map [list] + foreach c $clist h [MakeHexHexEscList $clist] { + lappend map $c \\$h + } + return $map +} + +proc jlib::MakeHexHexInvCharMap {clist} { + + set map [list] + foreach c $clist h [MakeHexHexEscList $clist] { + lappend map \\$h $c + } + return $map +} + +namespace eval jlib { + + # Characters that need to be escaped since non valid. + # XEP-0106: JID Escaping + variable jidEsc { "\&'/:<>@\\} + variable jidEscMap [MakeHexHexCharMap [split $jidEsc ""]] + variable jidEscInvMap [MakeHexHexInvCharMap [split $jidEsc ""]] + + # Prohibited ASCII characters. + set asciiC12C22 {\x00-\x1f\x80-\x9f\x7f\xa0} + set asciiC11 {\x20} + + # C.1.1 is actually allowed (RFC3491), weird! + set asciiProhibit(domain) $asciiC11 + append asciiProhibit(domain) $asciiC12C22 + append asciiProhibit(domain) /@ + + # The nodeprep prohibits these characters in addition: + # All whitespace characters (which reduce to U+0020, also called SP) + # U+0022 (") + # U+0026 (&) + # U+0027 (') + # U+002F (/) + # U+003A (:) + # U+003C (<) + # U+003E (>) + # U+0040 (@) + set asciiProhibit(node) {"&'/:<>@} + append asciiProhibit(node) $asciiC11 + append asciiProhibit(node) $asciiC12C22 + + set asciiProhibit(resource) $asciiC12C22 + + # RFC 3454 (STRINGPREP); all unicode characters: + # + # Maps to nothing (empty). + set mapB1 { + 00ad 034f 1806 180b 180c 180d 200b 200c + 200d 2060 fe00 fe01 fe02 fe03 fe04 fe05 + fe06 fe07 fe08 fe09 fe0a fe0b fe0c fe0d + fe0e fe0f feff + } + + # ASCII space characters. Just a space. + set prohibitC11 {0020} + + # Non-ASCII space characters + set prohibitC12 { + 00a0 1680 2000 2001 2002 2003 2004 2005 + 2006 2007 2008 2009 200a 200b 202f 205f + 3000 + } + + # C.2.1 ASCII control characters + set prohibitC21 { + 0000-001F 007F + } + + # C.2.2 Non-ASCII control characters + set prohibitC22 { + 0080-009f 06dd 070f 180e 200c 200d 2028 + 2029 2060 2061 2062 2063 206a-206f feff + fff9-fffc 1d173-1d17a + } + + # C.3 Private use + set prohibitC3 { + e000-f8ff f0000-ffffd 100000-10fffd + } + + # C.4 Non-character code points + set prohibitC4 { + fdd0-fdef fffe-ffff 1fffe-1ffff 2fffe-2ffff + 3fffe-3ffff 4fffe-4ffff 5fffe-5ffff 6fffe-6ffff + 7fffe-7ffff 8fffe-8ffff 9fffe-9ffff afffe-affff + bfffe-bffff cfffe-cffff dfffe-dffff efffe-effff + ffffe-fffff 10fffe-10ffff + } + + # C.5 Surrogate codes + set prohibitC5 {d800-dfff} + + # C.6 Inappropriate for plain text + set prohibitC6 { + fff9 fffa fffb fffc fffd + } + + # C.7 Inappropriate for canonical representation + set prohibitC7 {2ff0-2ffb} + + # C.8 Change display properties or are deprecated + set prohibitC8 { + 0340 0341 200e 200f 202a 202b 202c 202d + 202e 206a 206b 206c 206d 206e 206f + } + + # Test: 0, 1, 2, A-Z + set test { + 0030 0031 0032 0041-005a + } + + # And many more... + + variable mapB1RE [UnicodeListToRE $mapB1] + variable prohibitC11RE [UnicodeListToRE $prohibitC11] + variable prohibitC12RE [UnicodeListToRE $prohibitC12] + +} + +# jlib::splitjid -- +# +# Splits a general jid into a jid-2-tier and resource + +proc jlib::splitjid {jid jid2Var resourceVar} { + + set idx [string first / $jid] + if {$idx == -1} { + uplevel 1 [list set $jid2Var $jid] + uplevel 1 [list set $resourceVar {}] + } else { + set jid2 [string range $jid 0 [expr {$idx - 1}]] + set res [string range $jid [expr {$idx + 1}] end] + uplevel 1 [list set $jid2Var $jid2] + uplevel 1 [list set $resourceVar $res] + } +} + +# jlib::splitjidex -- +# +# Split a jid into the parts: jid = [ node "@" ] domain [ "/" resource ] +# Possibly empty. Doesn't check for valid content, only the form. +# +# RFC3920 3.1: +# jid = [ node "@" ] domain [ "/" resource ] + +proc jlib::splitjidex {jid nodeVar domainVar resourceVar} { + + set node "" + set domain "" + set res "" + + # Node part: + set idx [string first @ $jid] + if {$idx > 0} { + set node [string range $jid 0 [expr {$idx-1}]] + set jid [string range $jid [expr {$idx+1}] end] + } + + # Resource part: + set idx [string first / $jid] + if {$idx > 0} { + set res [string range $jid [expr {$idx+1}] end] + set jid [string range $jid 0 [expr {$idx-1}]] + } + + # Domain part is what remains: + set domain $jid + + uplevel 1 [list set $nodeVar $node] + uplevel 1 [list set $domainVar $domain] + uplevel 1 [list set $resourceVar $res] +} + +proc jlib::barejid {jid} { + + set idx [string first / $jid] + if {$idx == -1} { + return $jid + } else { + return [string range $jid 0 [expr {$idx-1}]] + } +} + +proc jlib::resourcejid {jid} { + set idx [string first / $jid] + if {$idx > 0} { + return [string range $jid [expr {$idx+1}] end] + } else { + return "" + } +} + +proc jlib::isbarejid {jid} { + return [expr {([string first / $jid] == -1) ? 1 : 0}] +} + +proc jlib::isfulljid {jid} { + return [expr {([string first / $jid] == -1) ? 0 : 1}] +} + +# jlib::joinjid -- +# +# Joins the, optionally empty, parts into a jid. +# domain must be nonempty though. + +proc jlib::joinjid {node domain resource} { + + set jid $domain + if {$node ne ""} { + set jid ${node}@${jid} + } + if {$resource ne ""} { + append jid "/$resource" + } + return $jid +} + +# jlib::jidequal -- +# +# Checks if two jids are actually equal after mapped. Does not check +# for prohibited characters. + +proc jlib::jidequal {jid1 jid2} { + return [string equal [jidmap $jid1] [jidmap $jid2]] +} + +# jlib::jidvalidate -- +# +# Checks if this is a valid jid interms of form and characters. + +proc jlib::jidvalidate {jid} { + + if {$jid eq ""} { + return 0 + } elseif {[catch {splitjidex $jid node name resource} ans]} { + return 0 + } + foreach what {node name resource} { + if {$what ne ""} { + if {[catch {${what}prep [set $what]} ans]} { + return 0 + } + } + } + return 1 +} + +# String preparation (STRINGPREP) RFC3454: +# +# The steps for preparing strings are: +# +# 1) Map -- For each character in the input, check if it has a mapping +# and, if so, replace it with its mapping. This is described in +# section 3. +# +# 2) Normalize -- Possibly normalize the result of step 1 using Unicode +# normalization. This is described in section 4. +# +# 3) Prohibit -- Check for any characters that are not allowed in the +# output. If any are found, return an error. This is described in +# section 5. +# +# 4) Check bidi -- Possibly check for right-to-left characters, and if +# any are found, make sure that the whole string satisfies the +# requirements for bidirectional strings. If the string does not +# satisfy the requirements for bidirectional strings, return an +# error. This is described in section 6. + +# jlib::*map -- +# +# Does the mapping part. + +proc jlib::nodemap {node} { + + return [string tolower $node] +} + +proc jlib::namemap {domain} { + + return [string tolower $domain] +} + +proc jlib::resourcemap {resource} { + + # Note that resources are case sensitive! + return $resource +} + +# jlib::*prep -- +# +# Does the complete stringprep. + +proc jlib::nodeprep {node} { + variable asciiProhibit + + set node [nodemap $node] + if {[regexp ".*\[${asciiProhibit(node)}\].*" $node]} { + return -code error "node part contains illegal character(s)" + } + return $node +} + +proc jlib::nameprep {domain} { + variable asciiProhibit + + set domain [namemap $domain] + if {[regexp ".*\[${asciiProhibit(domain)}\].*" $domain]} { + return -code error "domain contains illegal character(s)" + } + return $domain +} + +proc jlib::resourceprep {resource} { + variable asciiProhibit + + set resource [resourcemap $resource] + + # Orinary spaces are allowed! + if {[regexp ".*\[${asciiProhibit(resource)}\].*" $resource]} { + return -code error "resource contains illegal character(s)" + } + return $resource +} + +# jlib::jidmap -- +# +# Does the mapping part of STRINGPREP. Does not check for prohibited +# characters. +# +# Results: +# throws an error if form unrecognized, else the mapped jid. + +proc jlib::jidmap {jid} { + + if {$jid eq ""} { + return + } + # Guard against spurious spaces. + set jid [string trim $jid] + splitjidex $jid node domain resource + return [joinjid [nodemap $node] [namemap $domain] [resourcemap $resource]] +} + +# jlib::jidprep -- +# +# Applies STRINGPREP to the individiual and specific parts of the jid. +# +# Results: +# throws an error if prohibited, else the prepared jid. + +proc jlib::jidprep {jid} { + + if {$jid eq ""} { + return + } + splitjidex $jid node domain resource + set node [nodeprep $node] + set domain [nameprep $domain] + set resource [resourceprep $resource] + return [joinjid $node $domain $resource] +} + +proc jlib::MapStr {str } { + + # TODO +} + +# jlib::escapestr, unescapestr, escapejid, unescapejid -- +# +# XEP-0106: JID Escaping +# NB1: 'escapstr' and 'unescapstr' must only be applied to the node +# part of a JID. +# NB2: 'escapstr' must never be applied twice! +# NB3: it is currently unclear if escaping should be allowed on "ordinary" +# user JIDs + +proc jlib::escapestr {str} { + variable jidEscMap + return [string map $jidEscMap $str] +} + +proc jlib::unescapestr {str} { + variable jidEscInvMap + return [string map $jidEscInvMap $str] +} + +proc jlib::escapejid {jid} { + + # Node part: + # @@@ I think there is a protocol flaw here!!! + set idx [string first @ $jid] + if {$idx > 0} { + set node [string range $jid 0 [expr {$idx-1}]] + set rest [string range $jid [expr {$idx+1}] end] + return [escapestr $node]@$rest + } else { + return $jid + } +} + +proc jlib::unescapejid {jid} { + + # Node part: + # @@@ I think there is a protocol flaw here!!! + set idx [string first @ $jid] + if {$idx > 0} { + set node [string range $jid 0 [expr {$idx-1}]] + set rest [string range $jid [expr {$idx+1}] end] + return [unescapestr $node]@$rest + } else { + return $jid + } +} + +proc jlib::setdebug {args} { + variable debug + + if {[llength $args] == 0} { + return $debug + } elseif {[llength $args] == 1} { + set debug $args + } else { + return -code error "Usage: jlib::setdebug ?integer?" + } +} + +# jlib::generateuuid -- +# +# Simplified uuid generator. See the uuid package for a better one. + +proc jlib::generateuuid {} { + set MAX_INT 0x7FFFFFFF + # Bugfix Eric Hassold from Evolane + set hex1 [format {%x} [expr {[clock clicks] & $MAX_INT}]] + set hex2 [format {%x} [expr {int($MAX_INT*rand())}]] + return $hex1-$hex2 +} + +proc jlib::Debug {num str} { + global fdDebug + variable debug + if {$num <= $debug} { + if {[info exists fdDebug]} { + puts $fdDebug $str + flush $fdDebug + } + puts $str + } +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/jingle.tcl b/lib/jabberlib/jingle.tcl new file mode 100644 index 0000000..725819c --- /dev/null +++ b/lib/jabberlib/jingle.tcl @@ -0,0 +1,719 @@ +# jingle.tcl -- +# +# This file is part of the jabberlib. +# It provides support for the jingle stuff XEP-0166, +# and provides pluggable "slots" for media description formats and +# transport methods, which are implemented separately. +# +# Copyright (c) 2006 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: jingle.tcl,v 1.10 2007/07/19 06:28:17 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# jingle - library for Jingle +# +# SYNOPSIS +# jlib::jingle::init jlibname +# jlib::jingle::register name priority mediaElems transportElems tclProc +# +# The 'tclProc' is invoked for all jingle 'set' we get as: +# +# tclProc {jlibname jingleElem args} +# +# where the args are as usual the iq attributes, and the jingleElem is +# guranteed to be a valid jingle element with the required attributes. +# +# OPTIONS +# +# +# INSTANCE COMMANDS +# jlibName jingle initiate name jid mediaElems trptElems cmd +# jlibName jingle getstate session|media|transport sid +# jlibName jingle getvalue session|media|transport sid key +# jlibName jingle send_set sid action cmd ?elems? +# jlibName jingle free sid +# +# The 'cmd' here are invoked just as ordinary send_iq callbacks: +# +# cmd {type subiq args} +# +# o You MUST use either 'initiate' or 'send_set' for anything not a *-info +# call in order to keep the internal state machines inn sync. +# +# o In your registered tclProc you must handle all calls and start by +# acknowledging the receipt by sending a result for session-* actions (?). +# +# o When a session is ended you are required to 'free' it yourself. +# +# o While debugging you may switch off 'verifyState' below. +# +# Each component registers a callback proc which gets called when there is +# a 'set' (async) call aimed for it. +# +# jlib::jingle +# ------------ +# / | | \ +# / | | \ +# / | | \ +# / | | \ +# iax libjingle sip file-transfer +# +# TODO +# Use responder attribute +# +# UNCLEAR +# o When are the state changed, after sending an action or when the response +# is received? +# o Does the media and transport require a result set for every state change? +# +################################################################################ + +package require jlib +package require jlib::disco + +package provide jlib::jingle 0.1 + +namespace eval jlib::jingle { + + variable inited 0 + variable inited_reg 0 + variable jxmlns + set jxmlns(jingle) "http://jabber.org/protocol/jingle" + set jxmlns(media) "http://jabber.org/protocol/jingle/media" + set jxmlns(transport) "http://jabber.org/protocol/jingle/transport" + set jxmlns(errors) "http://jabber.org/protocol/jingle#errors" + + # Storage for registered media and transport. + variable jingle + + # Cache some of our capabilities. + variable have + set have(jingle) 0 + + # By default we verify all state changes. + variable verifyState 1 + + # For each session/media/transport state, make a map of allowed + # state changes: state + action -> new state + # State changes not listed here are not allowed. + # @@@ It is presently unclear if these are independent. + # At least session-initiate and session-terminate control + # the media and transport states. + + # Session state maps: + variable sessionMap + array set sessionMap { + pending,session-accept active + pending,session-redirect ended + pending,session-info pending + pending,session-terminate ended + active,session-redirect ended + active,session-info active + active,session-terminate ended + } + + # Media state maps: + variable mediaMap + array set mediaMap { + pending,media-info pending + pending,media-accept active + active,media-info active + active,media-modify modifying + modifying,media-info modifying + modifying,media-accept active + modifying,media-decline active + } + + # Transport state maps: + variable transportMap + array set transportMap { + pending,transport-info pending + pending,transport-accept active + active,transport-info active + active,transport-modify modifying + modifying,transport-info modifying + modifying,transport-accept active + modifying,transport-decline active + } + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::jingle::first_register -- +# +# This is called for the first component that registers. + +proc jlib::jingle::first_register {} { + variable jxmlns + variable inited_reg + variable have + + # Now we know we have at least one component supporting this. + jlib::disco::registerfeature $jxmlns(jingle) + set have(jingle) 1 + set inited_reg 1 +} + +# jlib::jingle::register -- +# +# A jingle component registers for a number of media and transport +# elements. These are used together with its registered command to +# dispatch incoming requests. +# The 'name' is for internal use only and is not related to the +# Jabber Registrar. +# Disco features are automatically registered but caps are not. +# +# NB: Currently this must be called before any jlib instance created, +# that is, before jlib::jingle::init! +# +# Arguments: +# name: unique name +# priority: number between 0-100 +# mediaElems: list of the media elements. Only the xmlns is necessary. +# The complete elements is supplied when doing an initiate. +# transportElems: same for transport +# cmd: tclProc for callbacks +# +# Result: +# name + +proc jlib::jingle::register {name priority mediaElems transportElems cmd} { + variable jingle + variable inited_reg + + set jingle($name,name) $name + set jingle($name,prio) $priority + set jingle($name,cmd) $cmd + set jingle($name,lmedia) $mediaElems + set jingle($name,ltransport) $transportElems + + # Extract the xmlns for media and transport. + set jingle($name,media,lxmlns) {} + foreach elem $mediaElems { + set xmlns [wrapper::getattribute $elem xmlns] + lappend jingle($name,media,lxmlns) $xmlns + } + set jingle($name,transport,lxmlns) {} + foreach elem $transportElems { + set xmlns [wrapper::getattribute $elem xmlns] + lappend jingle($name,transport,lxmlns) $xmlns + } + + # Register disco xmlns. + if {!$inited_reg} { + first_register + } + foreach xmlns $jingle($name,media,lxmlns) { + jlib::disco::registerfeature $xmlns + } + foreach xmlns $jingle($name,transport,lxmlns) { + jlib::disco::registerfeature $xmlns + } + return $name +} + +# jlib::jingle::init -- +# +# Sets up jabberlib handlers and makes a new instance if a jingle object. + +proc jlib::jingle::init {jlibname args} { + variable inited + variable jxmlns + + if {!$inited} { + InitOnce + } + + # Keep state array for each session as session(sid,...). + namespace eval ${jlibname}::jingle { + variable session + } + upvar ${jlibname}::jingle::session session + + # Register some standard iq handlers that is handled internally. + $jlibname iq_register set $jxmlns(jingle) [namespace current]::set_handler + $jlibname register_reset [namespace current]::reset + + return +} + +proc jlib::jingle::InitOnce { } { + + variable inited + + + set inited 1 +} + +proc jlib::jingle::have {what} { + variable have + + # ??? +} + +# jlib::jingle::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: +# args: all args to the cmd procedure. +# +# Results: +# none. + +proc jlib::jingle::cmdproc {jlibname cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +# jlib::jingle::send_set -- +# +# Utility function for sending jingle set stanzas. +# This MUST be used instead of send_iq since the internal state +# machines must be updated as well. The exception is *-info actions +# which don't affect the state. +# +# Arguments: +# +# Results: +# None. + +proc jlib::jingle::send_set {jlibname sid action cmd {elems {}}} { + variable verifyState + + # Be sure to set the internal state as well. + set state [set_state $jlibname $sid $action] + if {$verifyState && $state eq ""} { + return -code error "the proposed action $action is not allowed" + } + do_send_set $jlibname $sid $action $cmd $elems + return +} + +# jlib::jingle::do_send_set -- +# +# Makes the actual sending. State must be fixed prior to call. +# Internal use only. + +proc jlib::jingle::do_send_set {jlibname sid action cmd {elems {}}} { + variable jxmlns + upvar ${jlibname}::jingle::session session + + set jid $session($sid,jid) + set initiator $session($sid,initiator) + set attr [list xmlns $jxmlns(jingle) action $action \ + initiator $initiator sid $sid] + set jelem [wrapper::createtag jingle -attrlist $attr -subtags $elems] + + jlib::send_iq $jlibname set [list $jelem] -to $jid -command $cmd +} + +# @@@ If the state changes shall only take place *after* received a response, +# we need to intersect all calls here. +proc jlib::jingle::send_set_cb {} { + +} + +# @@@ Same thing here! +proc jlib::jingle::send_result {} { + +} + +# jlib::jingle::set_state -- +# +# Checks to see if the requested action is a valid one. +# Sets the new state if ok. +# +# Arguments: +# +# Results: +# empty if inconsistent, else the new state. + +proc jlib::jingle::set_state {jlibname sid action} { + variable sessionMap + variable mediaMap + variable transportMap + upvar ${jlibname}::jingle::session session + + #puts "jlib::jingle::set_state" + + # Since we are a state machine we must check that the requested state + # change is consistent. + if {$action eq "session-initiate"} { + + # No error checking here! + set session($sid,state,session) "pending" + set session($sid,state,media) "pending" + set session($sid,state,transport) "pending" + #puts "\t action=$action, state=pending" + return "pending" + } elseif {$action eq "session-terminate"} { + + # No error checking here! + set session($sid,state,session) "ended" + set session($sid,state,media) "ended" + set session($sid,state,transport) "ended" + #puts "\t action=$action, state=ended" + return "ended" + + } else { + set actionType [lindex [split $action -] 0] + set state $session($sid,state,$actionType) + + #puts "\t action=$action, state=$state, actionType=$actionType" + if {[info exists ${actionType}Map\($state,$action)]} { + set state [set ${actionType}Map\($state,$action)] + #puts "\t new state=$state" + set session($sid,state,$actionType) $state + return $state + } else { + #puts "\t out-of-sync" + return "" + } + } +} + +# jlib::jingle::initiate -- +# +# A jingle component makes a session-initiate request. +# This must be used instead of send_set for the initiate call. +# +# Arguments: +# jlibname: the instance of this jlib. +# name: +# jid: +# mediaElems: +# trptElems: +# cmd: +# +# Results: +# sid. + +proc jlib::jingle::initiate {jlibname name jid mediaElems trptElems cmd args} { + variable jingle + upvar ${jlibname}::jingle::session session + + #puts "jlib::jingle::initiate" + + # SIP may want to generate its own sid. + set opts(-sid) [jlib::generateuuid] + set opts(-initiator) [jlib::myjid $jlibname] + array set opts $args + set sid $opts(-sid) + + # We keep the internal jingle states for this sid. + set session($sid,sid) $sid + set session($sid,jid) $jid + set session($sid,name) $name + set session($sid,initiator) $opts(-initiator) + set session($sid,cmd) $jingle($name,cmd) + + set subElems [concat $mediaElems $trptElems] + set_state $jlibname $sid "session-initiate" + + do_send_set $jlibname $sid "session-initiate" $cmd $subElems + + return $sid +} + +# jlib::jingle::set_handler -- +# +# Parse incoming jingle set element. + +proc jlib::jingle::set_handler {jlibname from subiq args} { + variable have + variable verifyState + upvar ${jlibname}::jingle::session session + + #puts "jlib::jingle::set_handler" + + array set argsArr $args + if {![info exists argsArr(-id)]} { + return + } + set id $argsArr(-id) + + # There are several reasons why the target entity might return an error + # instead of acknowledging receipt of the initiation request: + # o The initiating entity is unknown to the target entity (e.g., via + # presence subscription). + # o The target entity does not support Jingle. + # o The target entity does not support any of the specified media + # description formats. + # o The target entity does not support any of the specified transport + # methods. + # o The initiation request was malformed. + + set jelem [wrapper::getfirstchildwithtag $argsArr(-xmldata) "jingle"] + if {$jelem eq {}} { + jlib::send_iq_error $jlibname $from $id 404 cancel service-unavailable + return 1 + } + if {!$have(jingle)} { + jlib::send_iq_error $jlibname $from $id 404 cancel service-unavailable + return 1 + } + + # Check required attributes: sid, action, initiator. + foreach aname {sid action initiator} { + set $aname [wrapper::getattribute $jelem $aname] + if {$aname eq ""} { + #puts "\t missing $aname" + jlib::send_iq_error $jlibname $from $id 404 cancel bad-request + return 1 + } + } + #puts "\t $sid $action $initiator" + + # We already have a session for this sid. + if {[info exists session($sid,sid)]} { + if {$verifyState && $session($sid,state,session) eq "ended"} { + send_error $jlibname $from $id unknown-session + return 1 + } + + # The action must not be an initiate. + if {$verifyState && $action eq "session-initiate"} { + send_error $jlibname $from $id out-of-order + return 1 + } + + # Since we are a state machine we must check that the requested state + # change is consistent. + set state [set_state $jlibname $sid $action] + if {$verifyState && $state eq ""} { + #puts "\t $action out-of-order" + send_error $jlibname $from $id out-of-order + return 1 + } + } else { + + # The first action must be an initiate. + if {$verifyState && $action ne "session-initiate"} { + send_error $jlibname $from $id out-of-order + return 1 + } + } + + switch -- $action { + "session-initiate" { + set session($sid,sid) $sid + set session($sid,jid) $from + set session($sid,initiator) $initiator + set session($sid,jelem) $jelem + eval {initiate_handler $jlibname $sid $id $jelem} $args + } + default { + uplevel #0 $session($sid,cmd) $jlibname [list $jelem] $args + } + } + + # Is handled here. + return 1 +} + +# jlib::jingle::initiate_handler -- +# +# We must find the jingle component that matches this initiate. + +proc jlib::jingle::initiate_handler {jlibname sid id jelem args} { + variable jingle + upvar ${jlibname}::jingle::session session + + #puts "jlib::jingle::initiate_handler" + + # Use the 'sid' as the identifier for the state array. + set session($sid,state,session) "pending" + set session($sid,state,media) "pending" + set session($sid,state,transport) "pending" + + set jid $session($sid,jid) + + # Match the media and transport with the ones we have registered, + # and use the best matched registered component. + set nsmedia {} + foreach elem [wrapper::getchildswithtag $jelem "description"] { + lappend nsmedia [wrapper::getattribute $elem xmlns] + } + set nstrpt {} + foreach elem [wrapper::getchildswithtag $jelem "transport"] { + lappend nstrpt [wrapper::getattribute $elem xmlns] + } + + # @@@ This matches only the xmlns which is not enough. + # The details is up to each component to negotiate? + # + # Make a list of candidates that support both media and transport xmlns: + # {{name prio} ...} and order them in decreasing priorities. + set lbest {} + set anymedia 0 + set anytransport 0 + foreach {- name} [array get jingle *,name] { + set mns [jlib::util::lintersect $jingle($name,media,lxmlns) $nsmedia] + set tns [jlib::util::lintersect $jingle($name,transport,lxmlns) $nstrpt] + + # A component must support both media and transport. + if {[llength $mns] && [llength $tns]} { + lappend lbest [list $name $jingle($name,prio)] + } + if {[llength $mns]} { + set anymedia 1 + } + if {[llength $tns]} { + set anytransport 1 + } + } + if {$lbest eq {}} { + if {!$anymedia} { + send_error $jlibname $jid $id unsupported-media + } elseif {!$anytransport} { + send_error $jlibname $jid $id unsupported-transports + } else { + # It is the actual combination media/transport that is unsupported. + send_error $jlibname $jid $id unsupported-media + } + } else { + set lbest [lsort -integer -index 1 -decreasing $lbest] + #puts "\t lbest=$lbest" + + # Delegate to the component. + # It is then up to the component to take the initiatives: + # transport-accept etc. + # @@@ We make a crude shortcut here and pick only the best. + set name [lindex $lbest 0 0] + set cmd $jingle($name,cmd) + set session($sid,name) $name + set session($sid,cmd) $cmd + uplevel #0 $cmd $jlibname [list $jelem] $args + } +} + +proc jlib::jingle::send_error {jlibname jid id stanza} { + variable jxmlns + + #puts "jlib::jingle::send_error" + # @@@ Not sure about the details here. + # We must add an extra error element: + # + set elem [wrapper::createtag $stanza \ + -attrlist [list xmlns $jxmlns(errors)]] + jlib::send_iq_error $jlibname $jid $id 404 cancel bad-request $elem +} + +# A few accessor functions. + +# jlib::jingle::getstate -- +# +# Return the current state for session, media, or transport. + +proc jlib::jingle::getstate {jlibname type sid} { + upvar ${jlibname}::jingle::session session + + return $session($sid,state,$type) +} + +proc jlib::jingle::getvalue {jlibname sid key} { + upvar ${jlibname}::jingle::session session + + return $session($sid,$key) +} + +proc jlib::jingle::havesession {jlibname sid} { + upvar ${jlibname}::jingle::session session + + return [info exists session($sid,sid)] +} + +proc jlib::jingle::reset {jlibname} { + + # Shall we clear out all sessions here? +} + +proc jlib::jingle::free {jlibname sid} { + upvar ${jlibname}::jingle::session session + + array unset session $sid,* +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::jingle { + + jlib::ensamble_register jingle \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +# Primitive test code. +if {0} { + package require jlib::jingle + + set jlibname jlib::jlib1 + set myjid [jlib::myjid $jlibname] + set jid $myjid + set xmlnsTransportIAX "http://jabber.org/protocol/jingle/transport/iax" + set xmlnsMediaAudio "http://jabber.org/protocol/jingle/media/audio" + + # Register: + set transportElem [wrapper::createtag "transport" \ + -attrlist [list xmlns $xmlnsTransportIAX version 2] ] + set mediaElemAudio [wrapper::createtag "description" \ + -attrlist [list xmlns $xmlnsMediaAudio] ] + + proc cmdIAX {jlibname _jelem args} { + #puts "IAX: $args" + array set argsArr $args + set sid [wrapper::getattribute $_jelem sid] + set action [wrapper::getattribute $_jelem action] + #puts "\t action=$action, sid=$sid" + + # Only session actions are acknowledged? + if {[string match "session-*" $action]} { + $jlibname send_iq result {} -to $argsArr(-from) -id $argsArr(-id) + } + + switch -- $action { + "session-initiate" { + set ::jelem $_jelem + + + } + } + } + jlib::jingle::register iax 50 \ + [list $mediaElemAudio] [list $transportElem] cmdIAX + + # Disco: + proc cb {args} {puts "cb: $args"} + $jlibname disco send_get info $jid cb + + # Initiate: + set sid [$jlibname jingle initiate iax $jid \ + [list $mediaElemAudio] [list $transportElem] cb] + + # IAX callbacks: + set media [wrapper::getfirstchildwithtag $jelem "description"] + $jlibname jingle send_set $sid "media-accept" cb [list $media] + + set trpt [wrapper::getfirstchildwithtag $jelem "transport"] + $jlibname jingle send_set $sid "transport-accept" cb [list $trpt] + + $jlibname jingle send_set $sid "session-accept" cb + + # Talk here! + + parray ${jlibname}::jingle::session + + # Shut up! + $jlibname jingle send_set $sid "session-terminate" cb +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/jlibdns.tcl b/lib/jabberlib/jlibdns.tcl new file mode 100644 index 0000000..da2aca5 --- /dev/null +++ b/lib/jabberlib/jlibdns.tcl @@ -0,0 +1,187 @@ +# jlibdns.tcl -- +# +# This file is part of the jabberlib. +# It provides support for XEP-0156: +# A DNS TXT Resource Record Format for XMPP Connection Methods +# and client DNS SRV records (XMPP Core sect. 14.3) +# +# Copyright (c) 2006-2008 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: jlibdns.tcl,v 1.9 2008/03/27 15:15:26 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# jlib::dns - library for DNS lookups +# +# SYNOPSIS +# jlib::dns::get_addr_port domain cmd +# jlib::dns::get_http_bind_url domain cmd OUTDATED +# jlib::dns::get_http_poll_url domain cmd +# jlib::dns::get_http_bosh_url domain cmd +# +############################# 7.1.2 Initial Registration ####################### +# +# +# _xmpp-client-httppoll +# HTTP Polling connection method +# +# The http: or https: URL at which to contact the HTTP Polling connection manager or proxy +# +# XEP-0025 +# +# +# +# _xmpp-client-xbosh +# XMPP Over Bosh connection method +# +# The http: or https: URL at which to contact the HTTP Binding connection manager or proxy +# +# XEP-0206 +# + +package require dns 9.9 ;# Fake version to avoid loding buggy version. +package require jlib + +package provide jlib::dns 0.1 + +namespace eval jlib::dns { + + variable owner + array set owner { + client _xmpp-client._tcp + poll _xmppconnect + } + + variable nameA + array set nameA { + bind _xmpp-client-httpbind + bosh _xmpp-client-xbosh + poll _xmpp-client-httppoll + } +} + +proc jlib::dns::get_addr_port {domain cmd args} { + + # dns::resolve may throw error! + set name _xmpp-client._tcp.$domain + return [eval {dns::resolve $name -type SRV \ + -command [list [namespace current]::addr_cb $cmd]} $args] +} + +proc jlib::dns::addr_cb {cmd token} { + + set addrList {} + if {[dns::status $token] eq "ok"} { + set result [dns::result $token] + foreach reply $result { + array unset rr + array set rr $reply + if {[info exists rr(rdata)]} { + array unset rd + array set rd $rr(rdata) + if {[info exists rd(priority)] && \ + [info exists rd(weight)] && \ + [info exists rd(port)] && \ + [info exists rd(target)] && \ + [isUInt16 $rd(priority)] && \ + [isUInt16 $rd(weight)] && \ + [isUInt16 $rd(port)] && \ + ($rd(target) ne ".")} { + if {$rd(weight) == 0} { + set n 0 + } else { + set n [expr {($rd(weight)+1)*rand()}] + } + set priority [expr {$rd(priority)*65536 - $n}] + lappend addrList [list $priority $rd(target) $rd(port)] + } + } + } + if {[llength $addrList]} { + set addrPort {} + foreach p [lsort -real -index 0 $addrList] { + lappend addrPort [lrange $p 1 2] + } + uplevel #0 $cmd [list $addrPort] + } else { + uplevel #0 $cmd [list {} dns-empty] + } + } else { + uplevel #0 $cmd [list {} [dns::error $token]] + } + + # Weird bug! + #after 2000 [list dns::cleanup $token] +} + +proc jlib::dns::isUInt16 {n} { + return [expr {[string is integer -strict $n] && $n >= 0 && $n < 65536} \ + ? 1 : 0] +} + +proc jlib::dns::get_http_bind_url {domain cmd args} { + set name _xmppconnect.$domain + return [eval {dns::resolve $name -type TXT \ + -command [list [namespace current]::http_cb bind $cmd]} $args] +} + +proc jlib::dns::get_http_bosh_url {domain cmd args} { + set name _xmppconnect.$domain + return [eval {dns::resolve $name -type TXT \ + -command [list [namespace current]::http_cb bosh $cmd]} $args] +} + +proc jlib::dns::get_http_poll_url {domain cmd args} { + set name _xmppconnect.$domain + return [eval {dns::resolve $name -type TXT \ + -command [list [namespace current]::http_cb poll $cmd]} $args] +} + +proc jlib::dns::http_cb {attr cmd token} { + variable nameA + + set found 0 + if {[dns::status $token] eq "ok"} { + set result [dns::result $token] + foreach reply $result { + array unset rr + array set rr $reply + if {[info exists rr(rdata)]} { + if {[regexp "$nameA($attr)=(.*)" $rr(rdata) - url]} { + set found 1 + uplevel #0 $cmd [list $url] + } + } + } + if {!$found} { + uplevel #0 $cmd [list {} dns-no-resource-record] + } + } else { + uplevel #0 $cmd [list {} [dns::error $token]] + } + + # Weird bug! + #after 2000 [list dns::cleanup $token] +} + +proc jlib::dns::reset {token} { + ::dns::reset $token + ::dns::cleanup $token +} + +# Test +if {0} { + proc cb {args} {puts "---> $args"} + jlib::dns::get_addr_port gmail.com cb + jlib::dns::get_addr_port jabber.ru cb + jlib::dns::get_addr_port jabber.com cb + jlib::dns::get_addr_port jabber.cz cb + jlib::dns::get_addr_port tigase.org cb + # Missing + jlib::dns::get_http_poll_url gmail.com cb + jlib::dns::get_http_poll_url jabber.ru cb + jlib::dns::get_http_poll_url ham9.net cb +} diff --git a/lib/jabberlib/jlibhttp.tcl b/lib/jabberlib/jlibhttp.tcl new file mode 100644 index 0000000..a6ba26b --- /dev/null +++ b/lib/jabberlib/jlibhttp.tcl @@ -0,0 +1,688 @@ +# jlibhttp.tcl --- +# +# Provides a http transport mechanism for jabberlib. +# Implements the deprecated XEP-0025: Jabber HTTP Polling protocol. +# +# Copyright (c) 2002-2008 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: jlibhttp.tcl,v 1.21 2008/02/29 12:55:36 matben Exp $ +# +# USAGE ######################################################################## +# +# jlib::http::new jlibname url ?-key value ...? +# url A valid url for the POST method of HTTP. +# +# -keylength sets the length of the key sequence +# -maxpollms ms max interval in ms for post requests +# -minpollms ms min interval in ms for post requests +# -proxyhost domain name of proxu host if any +# -proxyport integer and its port number +# -proxyusername name your username for the proxy +# -proxypasswd secret and your password +# (-resendinterval ms if sending fails, try again after this interval) +# -timeout ms timeout for connecting the server +# -usekeys 0|1 if keys should be used +# +# Although you can use the -proxy* switches here, it is much simpler to let +# the autoproxy package configure them. +# +# Callbacks for the JabberLib: +# jlib::http::transportinit, jlib::http::transportreset, +# jlib::http::send, jlib::http::transportip +# +# STATES ####################################################################### +# +# priv(state): "" inactive and not reset +# "instream" active connection +# "reset" reset by callback +# +# priv(status): "" inactive +# "scheduled" http post is scheduled as timer event +# "pending" http post made, waiting for response +# "error" error status + +package require jlib +package require http 2.4 +package require base64 +package require sha1 + +package provide jlib::http 0.1 + +namespace eval jlib::http { + + # Check for the TLS package so we can use https. + if {![catch {package require tls}]} { + http::register https 443 ::tls::socket + } + + # Inherit jlib's debug level. + variable debug 0 + if {!$debug} { + set debug [namespace parent]::debug + } + variable errcode + array set errcode { + 0 "unknown error" + -1 "server error" + -2 "bad request" + -3 "key sequence error" + } +} + +# jlib::http::new -- +# +# Configures the state of this thing. + +proc jlib::http::new {jlibname url args} { + + namespace eval ${jlibname}::http { + variable priv + variable opts + } + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + + Debug 2 "jlib::http::new url=$url, args=$args" + + array set opts { + -keylength 64 + -maxpollms 16000 + -minpollms 4000 + -proxyhost "" + -proxyport 80 + -proxyusername "" + -proxypasswd "" + -resendinterval 20000 + -timeout 30000 + -usekeys 1 + header "" + port 80 + proxyheader "" + url "" + pollupfactor 0.8 + polldownfactor 1.2 + } + set RE {^(([^:]*)://)?([^/:]+)(:([0-9]+))?(/.*)?$} + if {![regexp -nocase $RE $url - prefix proto host - port filepath]} { + return -code error "the url \"$url\" is not valid" + } + set opts(url) $url + set opts(host) $host + if {$port ne ""} { + set opts(port) $port + } + array set opts $args + + set priv(id) 0 + set priv(afterid) "" + + # Perhaps the autoproxy package can be used here? + if {[string length $opts(-proxyhost)] && [string length $opts(-proxyport)]} { + ::http::config -proxyhost $opts(-proxyhost) -proxyport $opts(-proxyport) + } + if {[string length $opts(-proxyusername)] || \ + [string length $opts(-proxypasswd)]} { + set opts(proxyheader) [BuildProxyHeader \ + $opts(-proxyusername) $opts(-proxypasswd)] + } + set opts(header) $opts(proxyheader) + + # Initialize. + InitState $jlibname + + $jlibname registertransport "http" \ + [namespace current]::transportinit \ + [namespace current]::send \ + [namespace current]::transportreset \ + [namespace current]::transportip + + return +} + +# jlib::http::InitState -- +# +# Sets initial state of 'priv' array. + +proc jlib::http::InitState {jlibname} { + + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + + set ms [clock clicks -milliseconds] + + set priv(state) "" + set priv(status) "" + + set priv(afterid) "" + set priv(xml) "" + set priv(lastxml) "" ; # Last posted xml. + set priv(id) 0 + set priv(first) 1 + set priv(first) 0 + set priv(postms) -1 + set priv(ip) "" + set priv(lastpostms) $ms + set priv(2lastpostms) $ms + set priv(keys) {} + if {$opts(-usekeys)} { + set priv(keys) [NewKeySequence [NewSeed] $opts(-keylength)] + } +} + +# jlib::http::BuildProxyHeader -- +# +# Builds list for the "Proxy-Authorization" header line. + +proc jlib::http::BuildProxyHeader {proxyusername proxypasswd} { + + set str $proxyusername:$proxypasswd + set auth [list "Proxy-Authorization" "Basic [base64::encode $str]"] + return $auth +} + +proc jlib::http::NewSeed { } { + set MAX_INT 0x7FFFFFFF + set num [expr {int($MAX_INT*rand())}] + return [format %0x $num] +} + +proc jlib::http::NewKeySequence {seed len} { + + set keys $seed + set prevkey $seed + + for {set i 1} {$i < $len} {incr i} { + + # It seems that it is expected to have sha1 in binary format; + # get from hex + set hex [::sha1::sha1 $prevkey] + set key [::base64::encode [binary format H* $hex]] + lappend keys $key + set prevkey $key + } + return $keys +} + +# jlib::http::transportinit -- +# +# For the -transportinit command. + +proc jlib::http::transportinit {jlibname} { + + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + + InitState $jlibname +} + +# jlib::http::transportreset -- +# +# For the -transportreset command. + +proc jlib::http::transportreset {jlibname} { + + upvar ${jlibname}::http::priv priv + + Debug 2 "jlib::http::transportreset" + + # Stop polling and resends. + if {$priv(afterid) ne ""} { + catch {after cancel $priv(afterid)} + } + set priv(afterid) "" + set priv(state) "reset" + set priv(ip) "" + + # If we have got cached xml to send must post it now and ignore response. + if {[string length $priv(xml)] > 2} { + Post $jlibname + } + if {[info exists priv(token)]} { + ::http::reset $priv(token) + ::http::cleanup $priv(token) + unset priv(token) + } +} + +# jlib::http::transportip -- +# +# Get our own ip address. +# @@@ If proxy we have the usual firewall problem! + +proc jlib::http::transportip {jlibname} { + + upvar ${jlibname}::http::priv priv + + return $priv(ip) +} + +# jlib::http::send -- +# +# For the -transportsend command. + +proc jlib::http::send {jlibname xml} { + + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + + Debug 2 "jlib::http::send state='$priv(state)' $xml" + + # Cancel if already 'reset'. + if {[string equal $priv(state) "reset"]} { + return + } + set priv(state) "instream" + + append priv(xml) $xml + + # If this is our first post we shall post right away. + if {$priv(status) eq ""} { + Post $jlibname + + # Unless we already have a pending event, post as soon as possible. + } elseif {$priv(status) ne "pending"} { + PostASAP $jlibname + } +} + +# jlib::http::PostASAP -- +# +# Make a post as soon as possible without taking 'minpollms' as the +# constraint. If we have waited longer than 'minpollms' post right away, +# else reschedule if necessary to post at 'minpollms'. + +proc jlib::http::PostASAP {jlibname} { + + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + + Debug 2 "jlib::http::PostASAP" + + if {$priv(afterid) eq ""} { + SchedulePost $jlibname minpollms + } else { + + # now (case A) now (case B) + # | | + # ---------------------------------------------------> time + # | ->| ->| + # last post min max + + # We shall always use '-minpollms' when there is something to send. + set nowms [clock clicks -milliseconds] + set minms [expr {$priv(lastpostms) + $opts(-minpollms)}] + if {$nowms < $minms} { + + # Case A: + # If next post is scheduled after min, then repost at min instead. + if {$priv(nextpostms) > $minms} { + SchedulePost $jlibname minpollms + } + } else { + + # Case B: + # We have already waited longer than '-minpollms'. + after cancel $priv(afterid) + set priv(afterid) "" + Post $jlibname + } + } +} + +# jlib::http::Schedule -- +# +# Computes the time for the next post and calls SchedulePost. + +proc jlib::http::Schedule {jlibname} { + + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + + Debug 2 "jlib::http::Schedule len priv(lastxml)=[string length $priv(lastxml)]" + + # Compute time for next post. + set ms [clock clicks -milliseconds] + if {$priv(lastxml) eq ""} { + set when [expr {$opts(polldownfactor) * ($ms - $priv(2lastpostms))}] + set when [Min $when $opts(-maxpollms)] + set when [Max $when $opts(-minpollms)] + } else { + set when minpollms + } + + # Reschedule next post unless 'reset'. + # Always keep a scheduled post at 'maxpollms' (or something else), + # and let any subsequent events reschedule if at an earlier time. + if {[string equal $priv(state) "instream"]} { + SchedulePost $jlibname $when + } +} + +# jlib::http::SchedulePost -- +# +# Schedule a post as a timer event. + +proc jlib::http::SchedulePost {jlibname when} { + + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + + Debug 2 "jlib::http::SchedulePost when=$when" + + set nowms [clock clicks -milliseconds] + + switch -- $when { + minpollms { + set minms [expr {$priv(lastpostms) + $opts(-minpollms)}] + set afterms [expr {$minms - $nowms}] + } + maxpollms { + set maxms [expr {$priv(lastpostms) + $opts(-maxpollms)}] + set afterms [expr {$maxms - $nowms}] + } + default { + set afterms $when + } + } + if {$afterms < 0} { + set afterms 0 + } + set priv(afterms) [expr int($afterms)] + set priv(nextpostms) [expr {$nowms + $afterms}] + set priv(postms) [expr {$priv(nextpostms) - $priv(lastpostms)}] + + if {$priv(afterid) ne ""} { + after cancel $priv(afterid) + } + set priv(status) "scheduled" + set priv(afterid) [after $priv(afterms) \ + [list [namespace current]::Post $jlibname]] +} + +# jlib::http::Post -- +# +# Just a wrapper for PostXML when sending xml. + +proc jlib::http::Post {jlibname} { + + upvar ${jlibname}::http::priv priv + + Debug 2 "jlib::http::Post" + + # If called directly any timers must have been cancelled before this. + set priv(afterid) "" + set xml $priv(xml) + set priv(xml) "" + PostXML $jlibname $xml +} + +# jlib::http::PostXML -- +# +# Do actual posting with (any) xml to send. +# Always called from 'Post'. + +proc jlib::http::PostXML {jlibname xml} { + + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + + Debug 2 "jlib::http::PostXML" + + set xml [encoding convertto utf-8 $xml] + + if {$opts(-usekeys)} { + + # Administrate the keys. Pick from end until no left. + set key [lindex $priv(keys) end] + set priv(keys) [lrange $priv(keys) 0 end-1] + + # Need new key sequence? + if {[llength $priv(keys)] == 0} { + set priv(keys) [NewKeySequence [NewSeed] $opts(-keylength)] + set newkey [lindex $priv(keys) end] + set priv(keys) [lrange $priv(keys) 0 end-1] + set query "$priv(id);$key;$newkey,$xml" + Debug 4 "\t key change" + } else { + set query "$priv(id);$key,$xml" + } + } else { + set query "$priv(id),$xml" + } + set priv(status) "pending" + if {[string equal $priv(state) "reset"]} { + set cmdProc [namespace current]::NoopResponse + } else { + set cmdProc [list [namespace current]::Response $jlibname] + } + set progProc [list [namespace current]::Progress $jlibname] + + Debug 2 "POST: $query" + + # -query forces a POST request. + # Make sure we send it as text dispite the application/* type.??? + if {[catch { + set token [::http::geturl $opts(url) \ + -timeout $opts(-timeout) \ + -headers $opts(header) \ + -query $query \ + -queryprogress $progProc \ + -command $cmdProc] + } msg]} { + # @@@ We could have a method here to retry a number of times before + # giving up. + Debug 2 "\t post failed: $msg" + Error $jlibname networkerror $msg + } else { + set priv(token) $token + set priv(lastxml) $xml + set priv(2lastpostms) $priv(lastpostms) + set priv(lastpostms) [clock clicks -milliseconds] + } +} + +# jlib::http::Progress -- +# +# Only useful the first post to get socket and our own IP. + +proc jlib::http::Progress {jlibname token args} { + + upvar ${jlibname}::http::priv priv + + if {$priv(ip) eq ""} { + # @@@ When we switch to httpex we will add a method for this. + set s [set $token\(sock)] + set priv(ip) [lindex [fconfigure $s -sockname] 0] + } +} + +# jlib::http::Response -- +# +# The response to our POST request. Parse any indata that should +# be of mime type text/xml + +proc jlib::http::Response {jlibname token} { + + upvar #0 $token state + upvar ${jlibname}::http::priv priv + upvar ${jlibname}::http::opts opts + variable errcode + + Debug 2 "jlib::http::Response priv(state)=$priv(state)" + + # We may have been 'reset' after this post was sent! + if {[string equal $priv(state) "reset"]} { + return + } + set status [::http::status $token] + + Debug 2 "\t status=$status, ::http::ncode=[::http::ncode $token]" + + if {$status eq "ok"} { + if {[::http::ncode $token] != 200} { + Error $jlibname error [::http::ncode $token] + return + } + set haveCookie 0 + set haveContentType 0 + + foreach {key value} $state(meta) { + + if {[string equal -nocase $key "set-cookie"]} { + + # Extract the 'ID' from the Set-Cookie key. + foreach pair [split $value ";"] { + set pair [string trim $pair] + if {[string equal -nocase -length 3 "ID=" $pair]} { + set id [string range $pair 3 end] + break + } + } + + if {![info exists id]} { + Error $jlibname error \ + "Set-Cookie in HTTP header \"$value\" invalid" + return + } + + # Invesitigate the ID: + set ids [split $id :] + if {[llength $ids] == 2} { + + # Any identifier that ends in ':0' indicates an error. + if {[string equal [lindex $ids 1] "0"]} { + + # ID=0:0 Unknown Error. The response body can + # contain a textual error message. + # ID=-1:0 Server Error. + # ID=-2:0 Bad Request. + # ID=-3:0 Key Sequence Error . + set code [lindex $ids 0] + if {[info exists errcode($code)]} { + set errmsg $errcode($code) + } else { + set errmsg "Server error $id" + } + Error $jlibname error $errmsg + return + } + } + set haveCookie 1 + } elseif {[string equal -nocase $key "content-type"]} { + + # Responses from the server have Content-Type: text/xml. + # Both the request and response bodies are UTF-8 + # encoded text, even if an HTTP header to the contrary + # exists. + # ejabberd: Content-Type {text/plain; charset=utf-8} + + set typeOK 0 + if {[string match -nocase "*text/xml*" $value]} { + set typeOK 1 + } elseif {[regexp -nocase { *text/plain; *charset=utf-8} $value]} { + set typeOK 1 + } + + if {!$typeOK} { + # This is an invalid response. + set errmsg "Content-Type in HTTP header is " + append errmsg $value + append errmsg " expected \"text/xml\" or \"text/plain\"" + Error $jlibname error $errmsg + return + } + set haveContentType 1 + } + } + if {!$haveCookie} { + Error $jlibname error "missing Set-Cookie in HTTP header" + return + } + if {!$haveContentType} { + Error $jlibname error "missing Content-Type in HTTP header" + return + } + set priv(id) $id + set priv(lastxml) "" + set body [::http::data $token] + Debug 2 "POLL: $body" + + # Send away to jabberlib for parsing and processing. + if {[string length $body] > 2} { + [namespace parent]::recv $jlibname $body + } + + # Reschedule new POST. + # NB: We always rescedule from the POST callback to avoid queuing + # up requests which can distort the order and make a + # 'key sequence error' + if {[string length $body] > 2} { + SchedulePost $jlibname minpollms + } else { + Schedule $jlibname + } + } else { + + # @@@ We could have a method here to retry a number of times before + # giving up. + Error $jlibname $status [::http::error $token] + return + } + + # And cleanup after each post. + ::http::cleanup $token + unset priv(token) +} + +# jlib::http::NoopResponse -- +# +# This shall be used when we flush out any xml after a 'reset' and +# don't expect any further actions to be taken. + +proc jlib::http::NoopResponse {token} { + + Debug 2 "jlib::http::NoopResponse" + + # Only thing we shall do here. + ::http::cleanup $token +} + +# jlib::http::Error -- +# +# Only network errors and server errors are reported here. + +proc jlib::http::Error {jlibname status {errmsg ""}} { + + upvar ${jlibname}::http::priv priv + + Debug 2 "jlib::http::Error status=$status, errmsg=$errmsg" + + set priv(status) "error" + if {[info exists priv(token)]} { + ::http::cleanup $priv(token) + unset priv(token) + } + + # @@@ We should perhaps be more specific here. + jlib::reporterror $jlibname networkerror $errmsg +} + +proc jlib::http::Min {x y} { + return [expr {$x <= $y ? $x : $y}] +} + +proc jlib::http::Max {x y} { + return [expr {$x >= $y ? $x : $y}] +} + +proc jlib::http::Debug {num str} { + variable debug + if {$num <= $debug} { + puts $str + } +} + +#------------------------------------------------------------------------------- + diff --git a/lib/jabberlib/jlibsasl.tcl b/lib/jabberlib/jlibsasl.tcl new file mode 100644 index 0000000..6118ae1 --- /dev/null +++ b/lib/jabberlib/jlibsasl.tcl @@ -0,0 +1,506 @@ +# jlibsasl.tcl -- +# +# This file is part of the jabberlib. It provides support for the +# sasl authentication layer via the tclsasl package or the saslmd5 +# pure tcl package. +# It also makes the resource binding and session initiation. +# +# o sasl authentication +# skipped: +# X bind resource +# X establish session +# +# Copyright (c) 2004-2006 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: jlibsasl.tcl,v 1.29 2007/09/12 07:20:46 matben Exp $ + +package require jlib +package require saslmd5 +set ::_saslpack saslmd5 + +package provide jlibsasl 1.0 + + +namespace eval jlib { + variable cyrussasl + + if {$::_saslpack eq "cyrussasl"} { + set cyrussasl 1 + } else { + set cyrussasl 0 + } + unset ::_saslpack +} + +proc jlib::sasl_init {} { + variable cyrussasl + + if {$cyrussasl} { + sasl::client_init -callbacks \ + [list [list log [namespace current]::sasl_log]] + } else { + # empty + } +} + +proc jlib::decode64 {str} { + variable cyrussasl + + if {$cyrussasl} { + return [sasl::decode64 $str] + } else { + return [saslmd5::decode64 $str] + } +} + +proc jlib::encode64 {str} { + variable cyrussasl + + if {$cyrussasl} { + return [sasl::encode64 $str] + } else { + return [saslmd5::encode64 $str] + } +} + +# jlib::auth_sasl -- +# +# Create a new SASL object. + +proc jlib::auth_sasl {jlibname username resource password cmd} { + + upvar ${jlibname}::locals locals + variable xmppxmlns + + Debug 4 "jlib::auth_sasl" + + # Cache our login jid. + set locals(username) $username + set locals(resource) $resource + set locals(password) $password + set locals(myjid2) ${username}@$locals(server) + set locals(myjid) ${username}@$locals(server)/${resource} + set locals(sasl,cmd) $cmd + + # Set up callbacks for elements that are of interest to us. + element_register $jlibname $xmppxmlns(sasl) [namespace current]::sasl_parse + + if {[have_feature $jlibname mechanisms]} { + auth_sasl_continue $jlibname + } else { + trace_stream_features $jlibname [namespace current]::sasl_features + } +} + +proc jlib::sasl_features {jlibname} { + + upvar ${jlibname}::locals locals + + Debug 4 "jlib::sasl_features" + + # Verify that sasl is supported before going on. + set features [get_feature $jlibname "mechanisms"] + if {$features eq ""} { + set msg "no sasl mechanisms announced by the server" + sasl_final $jlibname error [list sasl-no-mechanisms $msg] + } else { + auth_sasl_continue $jlibname + } +} + +proc jlib::sasl_parse {jlibname xmldata} { + + set tag [wrapper::gettag $xmldata] + + switch -- $tag { + challenge { + sasl_challenge $jlibname $tag $xmldata + } + failure { + sasl_failure $jlibname $tag $xmldata + } + success { + sasl_success $jlibname $tag $xmldata + } + default { + sasl_final $jlibname error [list sasl-protocol-error {}] + } + } + return +} + +# jlib::auth_sasl_continue -- +# +# We respond to the +# +# +# DIGEST-MD5 +# PLAIN +# ... + +proc jlib::auth_sasl_continue {jlibname} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::locals locals + variable xmppxmlns + variable cyrussasl + + Debug 4 "jlib::auth_sasl_continue" + + if {$cyrussasl} { + + # TclSASL's callback id's seem to be a bit mixed up. + foreach id {authname user pass getrealm} { + lappend callbacks [list $id [list [namespace current]::sasl_callback \ + $jlibname]] + } + set sasltoken [sasl::client_new \ + -service xmpp -serverFQDN $locals(server) -callbacks $callbacks \ + -flags success_data] + } else { + + # The saslmd5 package follow the naming convention in RFC 2831 + foreach id {username authzid pass realm} { + lappend callbacks [list $id [list [namespace current]::saslmd5_callback \ + $jlibname]] + } + set sasltoken [saslmd5::client_new \ + -service xmpp -serverFQDN $locals(server) -callbacks $callbacks \ + -flags success_data] + } + set lib(sasl,token) $sasltoken + + if {$cyrussasl} { + $sasltoken -operation setprop -property sec_props \ + -value {min_ssf 0 max_ssf 0 flags {noplaintext}} + } else { + $sasltoken setprop sec_props {min_ssf 0 max_ssf 0 flags {noplaintext}} + } + + # Returns a serialized array if succesful. + set mechanisms [get_feature $jlibname mechanisms] + if {$cyrussasl} { + set code [catch { + $sasltoken -operation start -mechanisms $mechanisms \ + -interact [list [namespace current]::sasl_interact $jlibname] + } out] + } else { + set ans [$sasltoken start -mechanisms $mechanisms] + set code [lindex $ans 0] + set out [lindex $ans 1] + } + Debug 4 "\t -operation start: code=$code, out=$out" + + switch -- $code { + 0 { + # ok + array set outArr $out + set xmllist [wrapper::createtag auth \ + -attrlist [list xmlns $xmppxmlns(sasl) mechanism $outArr(mechanism)] \ + -chdata [encode64 $outArr(output)]] + send $jlibname $xmllist + } + 4 { + # continue + array set outArr $out + set xmllist [wrapper::createtag auth \ + -attrlist [list xmlns $xmppxmlns(sasl) mechanism $outArr(mechanism)] \ + -chdata [encode64 $outArr(output)]] + send $jlibname $xmllist + } + default { + # This is an error + # We should perhaps send an abort element here. + sasl_final $jlibname error [list sasl-protocol-error $out] + } + } +} + +proc jlib::sasl_interact {jlibname data} { + + # empty +} + +# jlib::sasl_callback -- +# +# TclSASL's callback id's seem to be a bit mixed up. + +proc jlib::sasl_callback {jlibname data} { + + upvar ${jlibname}::locals locals + + array set arr $data + + # @@@ Is 'convertto utf-8' really necessary? + + switch -- $arr(id) { + authname { + # username + set value [encoding convertto utf-8 $locals(username)] + } + user { + # authzid + set value [encoding convertto utf-8 $locals(myjid2)] + } + pass { + set value [encoding convertto utf-8 $locals(password)] + } + getrealm { + set value [encoding convertto utf-8 $locals(server)] + } + default { + set value "" + } + } + return $value +} + +# jlib::saslmd5_callback -- +# +# The saslmd5 package follow the naming convention in RFC 2831. + +proc jlib::saslmd5_callback {jlibname data} { + + upvar ${jlibname}::locals locals + + array set arr $data + + switch -- $arr(id) { + username { + set value [encoding convertto utf-8 $locals(username)] + } + pass { + set value [encoding convertto utf-8 $locals(password)] + } + authzid { + + # xmpp-core sect. 6.1: + # As specified in [SASL], the initiating entity MUST NOT provide an + # authorization identity unless the authorization identity is + # different from the default authorization identity derived from + # the authentication identity as described in [SASL]. + + #set value [encoding convertto utf-8 $locals(myjid2)] + set value "" + } + realm { + set value [encoding convertto utf-8 $locals(server)] + } + default { + set value "" + } + } + Debug 4 "jlib::saslmd5_callback id=$arr(id), value=$value" + + return $value +} + +proc jlib::sasl_challenge {jlibname tag xmllist} { + + Debug 4 "jlib::sasl_challenge" + + sasl_step $jlibname [wrapper::getcdata $xmllist] + return +} + +proc jlib::sasl_step {jlibname serverin64} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::locals locals + variable xmppxmlns + variable cyrussasl + + set serverin [decode64 $serverin64] + Debug 4 "jlib::sasl_step, serverin=$serverin" + + # Note that 'step' returns the output if succesful, not a serialized array! + if {$cyrussasl} { + set code [catch { + $lib(sasl,token) -operation step -input $serverin \ + -interact [list [namespace current]::sasl_interact $jlibname] + } output] + } else { + foreach {code output} [$lib(sasl,token) step -input $serverin] {break} + } + Debug 4 "\t code=$code \n\t output=$output" + + switch -- $code { + 0 { + # ok + set xmllist [wrapper::createtag response \ + -attrlist [list xmlns $xmppxmlns(sasl)] \ + -chdata [encode64 $output]] + send $jlibname $xmllist + } + 4 { + # continue + set xmllist [wrapper::createtag response \ + -attrlist [list xmlns $xmppxmlns(sasl)] \ + -chdata [encode64 $output]] + send $jlibname $xmllist + } + default { + #puts "\t errdetail: [$lib(sasl,token) -operation errdetail]" + sasl_final $jlibname error [list sasl-protocol-error $output] + } + } +} + +proc jlib::sasl_failure {jlibname tag xmllist} { + + upvar ${jlibname}::locals locals + variable xmppxmlns + + Debug 4 "jlib::sasl_failure" + + if {[wrapper::getattribute $xmllist xmlns] eq $xmppxmlns(sasl)} { + set errE [lindex [wrapper::getchildren $xmllist] 0] + if {[llength $errE]} { + set errtag [wrapper::gettag $errE] + set errmsg [sasl_getmsg $errtag] + } else { + set errmsg "not-authorized" + } + sasl_final $jlibname error [list $errtag $errmsg] + } + return +} + +proc jlib::sasl_success {jlibname tag xmllist} { + + upvar ${jlibname}::lib lib + + Debug 4 "jlib::sasl_success" + + # Upon receiving a success indication within the SASL negotiation, the + # client MUST send a new stream header to the server, to which the + # server MUST respond with a stream header as well as a list of + # available stream features. Specifically, if the server requires the + # client to bind a resource to the stream after successful SASL + # negotiation, it MUST include an empty element qualified by + # the 'urn:ietf:params:xml:ns:xmpp-bind' namespace in the stream + # features list it presents to the client upon sending the header for + # the response stream sent after successful SASL negotiation (but not + # before): + + wrapper::reset $lib(wrap) + + # We must clear out any server info we've received so far. + stream_reset $jlibname + + if {[catch { + sendstream $jlibname -version 1.0 + } err]} { + sasl_final $jlibname error [list network-failure $err] + return + } + sasl_final $jlibname result $xmllist + + return + + # Wait for the resource binding feature (optional) or session (mandantory): + trace_stream_features $jlibname \ + [namespace current]::auth_sasl_features_write + return +} + +proc jlib::auth_sasl_features_write {jlibname} { + + upvar ${jlibname}::locals locals + + if {[have_feature $jlibname bind]} { + bind_resource $jlibname $locals(resource) \ + [namespace current]::resource_bind_cb + } else { + establish_session $jlibname + } +} + +proc jlib::resource_bind_cb {jlibname type subiq} { + + if {$type eq "error"} { + sasl_final $jlibname error $subiq + } else { + establish_session $jlibname + } +} + +proc jlib::establish_session {jlibname} { + + variable xmppxmlns + + # Establish the session. + set xmllist [wrapper::createtag session \ + -attrlist [list xmlns $xmppxmlns(session)]] + send_iq $jlibname set [list $xmllist] -command \ + [list [namespace current]::send_session_cb $jlibname] +} + +proc jlib::send_session_cb {jlibname type subiq args} { + + upvar ${jlibname}::locals locals + + sasl_final $jlibname $type $subiq +} + +proc jlib::sasl_final {jlibname type subiq} { + + upvar ${jlibname}::locals locals + variable xmppxmlns + + Debug 4 "jlib::sasl_final" + + # We are no longer interested in these. + element_deregister $jlibname $xmppxmlns(sasl) [namespace current]::sasl_parse + + uplevel #0 $locals(sasl,cmd) [list $jlibname $type $subiq] +} + +proc jlib::sasl_log {args} { + + Debug 2 "SASL: $args" +} + +proc jlib::sasl_reset {jlibname} { + + variable xmppxmlns + + set cmd [trace_stream_features $jlibname] + if {$cmd eq "[namespace current]::sasl_features"} { + trace_stream_features $jlibname {} + } + element_deregister $jlibname $xmppxmlns(sasl) [namespace current]::sasl_parse +} + +namespace eval jlib { + + # This maps Defined Conditions to clear text messages. + # RFC 3920 (XMPP core); 6.4 Defined Conditions + # Added 'bad-auth' which seems to be a ejabberd anachronism. + + variable saslmsg + array set saslmsg { + aborted {The receiving entity acknowledges an abort element sent by the initiating entity.} + incorrect-encoding {The data provided by the initiating entity could not be processed because the [BASE64] encoding is incorrect.} + invalid-authzid {The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID.} + invalid-mechanism {The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity.} + mechanism-too-weak {The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity.} + not-authorized {The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username).} + temporary-auth-failure {The authentication failed because of a temporary error condition within the receiving entity.} + bad-auth {The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username).} + } +} + +proc jlib::sasl_getmsg {condition} { + variable saslmsg + + if {[info exists saslmsg($condition)]} { + return $saslmsg($condition) + } else { + return $condition + } +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/jlibtls.tcl b/lib/jabberlib/jlibtls.tcl new file mode 100644 index 0000000..b5ddb00 --- /dev/null +++ b/lib/jabberlib/jlibtls.tcl @@ -0,0 +1,217 @@ +# jlibtls.tcl -- +# +# This file is part of the jabberlib. It provides support for the +# tls network socket security layer. +# +# Copyright (c) 2004 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: jlibtls.tcl,v 1.19 2007/07/23 15:11:43 matben Exp $ + +package require tls +package require jlib + +package provide jlibtls 1.0 + + +proc jlib::starttls {jlibname cmd args} { + + upvar ${jlibname}::locals locals + variable xmppxmlns + + Debug 2 "jlib::starttls" + + set locals(tls,cmd) $cmd + + # Set up callbacks for the xmlns that is of interest to us. + element_register $jlibname $xmppxmlns(tls) [namespace current]::tls_parse + + if {[have_feature $jlibname]} { + tls_continue $jlibname + } else { + trace_stream_features $jlibname [namespace current]::tls_features_write + } +} + +proc jlib::tls_features_write {jlibname} { + + Debug 2 "jlib::tls_features_write" + + trace_stream_features $jlibname {} + tls_continue $jlibname +} + +proc jlib::tls_continue {jlibname} { + + variable xmppxmlns + + Debug 2 "jlib::tls_continue" + + # Must verify that the server provides a 'starttls' feature. + if {![have_feature $jlibname starttls]} { + tls_finish $jlibname starttls-nofeature + } + set xmllist [wrapper::createtag starttls -attrlist [list xmlns $xmppxmlns(tls)]] + send $jlibname $xmllist + + # Wait for 'failure' or 'proceed' element. +} + +proc jlib::tls_parse {jlibname xmldata} { + + set tag [wrapper::gettag $xmldata] + + switch -- $tag { + proceed { + tls_proceed $jlibname $tag $xmldata + } + failure { + tls_failure $jlibname $tag $xmldata + } + default { + tls_finish $jlibname starttls-protocol-error "unrecognized element" + } + } + return +} + +proc jlib::tls_proceed {jlibname tag xmllist} { + + upvar ${jlibname}::lib lib + upvar ${jlibname}::locals locals + + Debug 2 "jlib::tls_proceed" + + set sock $lib(sock) + + # Make it a SSL connection. + if {[catch { + tls::import $sock -cafile "" -certfile "" -keyfile "" \ + -request 1 -server 0 -require 0 -ssl2 no -ssl3 yes -tls1 yes + } err]} { + close $sock + tls_finish $jlibname starttls-failure $err + } + + # We must initiate the handshake before getting any answers. + set locals(tls,retry) 0 + set locals(tls,fevent) [fileevent $sock readable] + tls_handshake $jlibname +} + +# jlib::tls_handshake -- +# +# Performs the TLS handshake using filevent readable until completed +# or a nonrecoverable error. +# This method of using fileevent readable seems independent of +# speed of network connection (dialup/broadband) which a fixed +# loop with 50ms delay isn't! + +proc jlib::tls_handshake {jlibname} { + global errorCode + upvar ${jlibname}::lib lib + upvar ${jlibname}::locals locals + + set sock $lib(sock) + + # Do SSL handshake. + if {$locals(tls,retry) > 100} { + close $sock + set err "too long retry to setup SSL connection" + tls_finish $jlibname starttls-failure $err + } elseif {[catch {tls::handshake $sock} complete]} { + if {[lindex $errorCode 1] eq "EAGAIN"} { + incr locals(tls,retry) + + # Temporarily hijack these events. + fileevent $sock readable \ + [namespace code [list tls_handshake $jlibname]] + } else { + close $sock + tls_finish $jlibname starttls-failure $err + } + } elseif {$complete} { + Debug 2 "\t number of TLS handshakes=$locals(tls,retry)" + + # Reset the event handler to what it was. + fileevent $sock readable $locals(tls,fevent) + tls_handshake_fin $jlibname + } +} + +proc jlib::tls_handshake_fin {jlibname} { + + upvar ${jlibname}::lib lib + + wrapper::reset $lib(wrap) + + # We must clear out any server info we've received so far. + stream_reset $jlibname + set sock $lib(sock) + + # The tls package resets the encoding to: -encoding binary + if {[catch { + fconfigure $sock -encoding utf-8 + sendstream $jlibname -version 1.0 + } err]} { + tls_finish $jlibname network-failure $err + return + } + + # Wait for the SASL features. Seems to be the only way to detect success. + trace_stream_features $jlibname [namespace current]::tls_features_write_2nd + return +} + +proc jlib::tls_features_write_2nd {jlibname} { + + Debug 2 "jlib::tls_features_write_2nd" + + tls_finish $jlibname +} + +proc jlib::tls_failure {jlibname tag xmllist} { + + Debug 2 "jlib::tls_failure" + + # Seems we don't get any additional error info here. + tls_finish $jlibname starttls-failure "tls failed" +} + +proc jlib::tls_finish {jlibname {errcode ""} {msg ""}} { + + upvar ${jlibname}::locals locals + variable xmppxmlns + + Debug 2 "jlib::tls_finish errcode=$errcode, msg=$msg" + + trace_stream_features $jlibname {} + element_deregister $jlibname $xmppxmlns(tls) [namespace current]::tls_parse + + if {$errcode ne ""} { + uplevel #0 $locals(tls,cmd) $jlibname [list error [list $errcode $msg]] + } else { + uplevel #0 $locals(tls,cmd) $jlibname [list result {}] + } +} + +# jlib::tls_reset -- +# +# + +proc jlib::tls_reset {jlibname} { + + variable xmppxmlns + + element_deregister $jlibname $xmppxmlns(tls) [namespace current]::tls_parse + + set cmd [trace_stream_features $jlibname] + if {$cmd eq "[namespace current]::tls_features_write"} { + trace_stream_features $jlibname {} + } elseif {$cmd eq "[namespace current]::tls_features_write_2nd"} { + trace_stream_features $jlibname {} + } +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/muc.tcl b/lib/jabberlib/muc.tcl new file mode 100644 index 0000000..40d7c00 --- /dev/null +++ b/lib/jabberlib/muc.tcl @@ -0,0 +1,655 @@ +# muc.tcl -- +# +# This file is part of jabberlib. +# It implements the Multi User Chat (MUC) protocol part of the XMPP +# protocol as defined by the 'http://jabber.org/protocol/muc*' +# namespace. +# +# Copyright (c) 2003-2005 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: muc.tcl,v 1.40 2007/10/22 11:51:33 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# muc - convenience command library for MUC +# +# OPTIONS +# see below for instance command options +# +# INSTANCE COMMANDS +# jlibname muc allroomsin +# jlibname muc create roomjid nick callback ?-extras? +# jlibname muc destroy roomjid ?-command, -reason, alternativejid? +# jlibname muc enter roomjid nick ?-command, -extras, -password? +# jlibname muc exit roomjid +# jlibname muc getaffiliation roomjid affiliation callback +# jlibname muc getrole roomjid role callback +# jlibname muc getroom roomjid callback +# jlibname muc invite roomjid jid ?-reason? +# jlibname muc isroom jid +# jlibname muc mynick roomjid +# jlibname muc participants roomjid +# jlibname muc setaffiliation roomjid nick affiliation ?-command, -reason? +# jlibname muc setnick roomjid nick ?-command? +# jlibname muc setrole roomjid nick role ?-command, -reason? +# jlibname muc setroom roomjid type ?-command, -form? +# +############################# CHANGES ########################################## +# +# 0.1 first version +# 0.2 rewritten as a standalone component +# 0.3 ensamble command +# +# 050913 INCOMPATIBLE CHANGE! complete reorganization using ensamble command. + +package require jlib +package require jlib::disco +package require jlib::roster + +package provide jlib::muc 0.3 + +namespace eval jlib::muc { + + # Globals same for all instances of this jlib. + variable debug 0 + + variable xmlns + array set xmlns { + "muc" "http://jabber.org/protocol/muc" + "admin" "http://jabber.org/protocol/muc#admin" + "owner" "http://jabber.org/protocol/muc#owner" + "user" "http://jabber.org/protocol/muc#user" + } + + variable muc + set muc(affiliationExp) {(owner|admin|member|outcast|none)} + set muc(roleExp) {(moderator|participant|visitor|none)} + + jlib::disco::registerfeature $xmlns(muc) + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::muc::init -- +# +# Creates a new instance of a muc object. +# +# Arguments: +# jlibname: name of existing jabberlib instance; fully qualified! +# args: +# +# Results: +# namespaced instance command + +proc jlib::muc::init {jlibname args} { + + Debug 2 "jlib::muc::init jlibname=$jlibname" + + # Instance specific namespace. + namespace eval ${jlibname}::muc { + variable cache + variable rooms + } + upvar ${jlibname}::muc::cache cache + upvar ${jlibname}::muc::rooms rooms + + # Register service. + $jlibname service register muc muc + + $jlibname register_reset [namespace current]::reset + + return +} + +# jlib::muc::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname name of jabberlib instance. +# cmd the method. +# args all args to the cmd method. +# +# Results: +# from the individual command if any. + +proc jlib::muc::cmdproc {jlibname cmd args} { + return [eval {$cmd $jlibname} $args] +} + +# jlib::muc::invoke_callback -- ????????????? +# +# + +proc jlib::muc::invoke_callback {mucname cmd type subiq} { + uplevel #0 $cmd [list $mucname $type $subiq] +} + +# jlib::muc::enter -- +# +# Enter room. +# +# Arguments: +# jlibname name of jabberlib instance. +# roomjiid +# nick nick name +# args ?-command callbackProc? +# ?-extras list of xmllist? +# ?-password str? +# +# Results: +# none. + +proc jlib::muc::enter {jlibname roomjid nick args} { + variable xmlns + upvar ${jlibname}::muc::cache cache + upvar ${jlibname}::muc::rooms rooms + + set xsub [list] + set extras [list] + set cmd "" + foreach {name value} $args { + + switch -- $name { + -command { + set cmd $value + } + -extras { + set extras $value + } + -password { + set xsub [list [wrapper::createtag "password" \ + -chdata $value]] + } + default { + return -code error "Unrecognized option \"$name\"" + } + } + } + set jid $roomjid/$nick + set xelem [wrapper::createtag "x" -subtags $xsub \ + -attrlist [list xmlns $xmlns(muc)]] + $jlibname send_presence -to $jid -xlist [list $xelem] -extras $extras \ + -command [list [namespace current]::parse_enter $cmd] + set cache($roomjid,mynick) $nick + set rooms($roomjid) 1 + $jlibname service setroomprotocol $roomjid "muc" +} + +# jlib::muc::parse_enter -- +# +# Callback when entering room to make sure there are no error. + +proc jlib::muc::parse_enter {cmd jlibname xmldata} { + upvar ${jlibname}::muc::cache cache + + set from [wrapper::getattribute $xmldata from] + set type [wrapper::getattribute $xmldata type] + if {$type eq ""} { + set type "available" + } + set roomjid [jlib::jidmap [jlib::barejid $from]] + if {[string equal $type "error"]} { + unset -nocomplain cache($roomjid,mynick) + } else { + set cache($roomjid,inside) 1 + } + if {$cmd ne ""} { + uplevel #0 $cmd [list $jlibname $xmldata] + } +} + +# jlib::muc::exit -- +# +# Exit room. + +proc jlib::muc::exit {jlibname roomjid} { + upvar ${jlibname}::muc::cache cache + + if {[info exists cache($roomjid,mynick)]} { + set jid $roomjid/$cache($roomjid,mynick) + $jlibname send_presence -to $jid -type "unavailable" + unset -nocomplain cache($roomjid,mynick) + } + unset -nocomplain cache($roomjid,inside) + $jlibname roster clearpresence "${roomjid}*" +} + +# jlib::muc::setnick -- +# +# Set new nick name for room. + +proc jlib::muc::setnick {jlibname roomjid nick args} { + upvar ${jlibname}::muc::cache cache + + set opts [list] + foreach {name value} $args { + switch -- $name { + -command { + lappend opts $name $value + } + default { + return -code error "Unrecognized option \"$name\"" + } + } + } + set jid $roomjid/$nick + eval {$jlibname send_presence -to $jid} $opts + set cache($roomjid,mynick) $nick +} + +# jlib::muc::invite -- +# +# + +proc jlib::muc::invite {jlibname roomjid jid args} { + variable xmlns + + set opts [list] + set children [list] + foreach {name value} $args { + switch -- $name { + -command { + lappend opts $name $value + } + -reason { + lappend children [wrapper::createtag \ + [string trimleft $name "-"] -chdata $value] + } + -continue { + lappend children [wrapper::createtag "continue"] + } + default { + return -code error "Unrecognized option \"$name\"" + } + } + } + set invite [list [wrapper::createtag "invite" \ + -attrlist [list to $jid] -subtags $children]] + + set xelem [wrapper::createtag "x" \ + -attrlist [list xmlns $xmlns(user)] \ + -subtags $invite] + eval {$jlibname send_message $roomjid -xlist [list $xelem]} $opts +} + +# jlib::muc::setrole -- +# +# + +proc jlib::muc::setrole {jlibname roomjid nick role args} { + variable muc + variable xmlns + + if {![regexp $muc(roleExp) $role]} { + return -code error "Unrecognized role \"$role\"" + } + set opts [list] + set subitem [list] + foreach {name value} $args { + switch -- $name { + -command { + lappend opts -command [concat $value $jlibname] + } + -reason { + set subitem [list [wrapper::createtag "reason" -chdata $value]] + } + default { + return -code error "Unrecognized option \"$name\"" + } + } + } + + set subelements [list [wrapper::createtag "item" \ + -attrlist [list nick $nick role $role] \ + -subtags $subitem]] + + set xmllist [wrapper::createtag "query" \ + -attrlist [list xmlns $xmlns(admin)] \ + -subtags $subelements] + eval {$jlibname send_iq "set" [list $xmllist] -to $roomjid} $opts +} + +# jlib::muc::setaffiliation -- +# +# + +proc jlib::muc::setaffiliation {jlibname roomjid nick affiliation args} { + variable muc + variable xmlns + + if {![regexp $muc(affiliationExp) $affiliation]} { + return -code error "Unrecognized affiliation \"$affiliation\"" + } + set opts [list] + set subitem [list] + foreach {name value} $args { + switch -- $name { + -command { + lappend opts -command [concat $value $jlibname] + } + -reason { + set subitem [list [wrapper::createtag "reason" -chdata $value]] + } + default { + return -code error "Unrecognized option \"$name\"" + } + } + } + + switch -- $affiliation { + owner { + set ns $xmlns(owner) + } + default { + set ns $xmlns(admin) + } + } + + set subelements [list [wrapper::createtag "item" \ + -attrlist [list nick $nick affiliation $affiliation] \ + -subtags $subitem]] + + set xmllist [wrapper::createtag "query" \ + -attrlist [list xmlns $ns] -subtags $subelements] + eval {$jlibname send_iq "set" [list $xmllist] -to $roomjid} $opts +} + +# jlib::muc::getrole -- +# +# + +proc jlib::muc::getrole {jlibname roomjid role callback} { + variable muc + variable xmlns + + if {![regexp $muc(roleExp) $role]} { + return -code error "Unrecognized role \"$role\"" + } + set subelements [list [wrapper::createtag "item" \ + -attrlist [list role $role]]] + + set xmllist [wrapper::createtag "query" \ + -attrlist [list xmlns $xmlns(admin)] \ + -subtags $subelements] + $jlibname send_iq "get" [list $xmllist] -to $roomjid \ + -command [concat $callback $jlibname] +} + +# jlib::muc::getaffiliation -- +# +# + +proc jlib::muc::getaffiliation {jlibname roomjid affiliation callback} { + variable muc + variable xmlns + + if {![regexp $muc(affiliationExp) $affiliation]} { + return -code error "Unrecognized role \"$affiliation\"" + } + set subelements [list [wrapper::createtag "item" \ + -attrlist [list affiliation $affiliation]]] + + switch -- $affiliation { + owner - admin { + set ns $xmlns(owner) + } + default { + set ns $xmlns(admin) + } + } + + set xmllist [wrapper::createtag "query" \ + -attrlist [list xmlns $ns] -subtags $subelements] + $jlibname send_iq "get" [list $xmllist] -to $roomjid \ + -command [concat $callback $jlibname] +} + +# jlib::muc::create -- +# +# The first thing to do when creating a room. +# +# Arguments: +# jlibname name of jabberlib instance. +# roomjiid +# nick nick name +# command callbackProc +# args ?-extras list of xmllist? +# +# Results: +# none. + +proc jlib::muc::create {jlibname roomjid nick command args} { + variable xmlns + upvar ${jlibname}::muc::cache cache + upvar ${jlibname}::muc::rooms rooms + + set extras [list] + foreach {name value} $args { + + switch -- $name { + -extras { + set extras $value + } + default { + return -code error "Unrecognized option \"$name\"" + } + } + } + set jid $roomjid/$nick + set xelem [wrapper::createtag "x" -attrlist [list xmlns $xmlns(muc)]] + $jlibname send_presence \ + -to $jid -xlist [list $xelem] -extras $extras \ + -command [list [namespace current]::parse_create $command] + set cache($roomjid,mynick) $nick + set rooms($roomjid) 1 + $jlibname service setroomprotocol $roomjid "muc" +} + +proc jlib::muc::parse_create {cmd jlibname xmldata} { + upvar ${jlibname}::muc::cache cache + + set from [wrapper::getattribute $xmldata from] + set type [wrapper::getattribute $xmldata type] + if {$type eq ""} { + set type "available" + } + set roomjid [jlib::jidmap [jlib::barejid $from]] + if {[string equal $type "error"]} { + unset -nocomplain cache($roomjid,mynick) + } else { + set cache($roomjid,inside) 1 + } + if {$cmd ne ""} { + uplevel #0 $cmd [list $jlibname $xmldata] + } +} + +# jlib::muc::setroom -- +# +# Sends an iq set element to room. If -form the 'type' argument is +# omitted. +# +# Arguments: +# jlibname name of muc instance. +# roomjid the rooms jid. +# type typically 'submit' or 'cancel'. +# args: +# -command +# -form xmllist starting with the x-element +# +# Results: +# None. + +proc jlib::muc::setroom {jlibname roomjid type args} { + variable xmlns + + set opts [list] + set subelements [list] + foreach {name value} $args { + switch -- $name { + -command { + lappend opts -command [concat $value $jlibname] + } + -form { + set xelem $value + } + default { + return -code error "Unrecognized option \"$name\"" + } + } + } + if {[info exists xelem]} { + if {[llength $xelem] == 0} { + set xelem [list [wrapper::createtag "x" \ + -attrlist [list xmlns "jabber:x:data" type $type]]] + } + set xmllist [wrapper::createtag "query" -subtags $xelem \ + -attrlist [list xmlns $xmlns(owner)]] + eval {$jlibname send_iq "set" [list $xmllist] -to $roomjid} $opts + } +} + +# jlib::muc::destroy -- +# +# +# Arguments: +# jlibname name of muc instance. +# roomjid the rooms jid. +# args -command, -reason, alternativejid. +# +# Results: +# None. + +proc jlib::muc::destroy {jlibname roomjid args} { + variable xmlns + + set opts [list] + set subelements [list] + foreach {name value} $args { + + switch -- $name { + -command { + lappend opts -command [concat $value $jlibname] + } + -reason { + lappend subelements [wrapper::createtag "reason" \ + -chdata $value] + } + -alternativejid { + lappend subelements [wrapper::createtag "alt" \ + -attrlist [list jid $value]] + } + default { + return -code error "Unrecognized option \"$name\"" + } + } + } + + set destroyelem [wrapper::createtag "destroy" -subtags $subelements \ + -attrlist [list jid $roomjid]] + + set xmllist [wrapper::createtag "query" -subtags [list $destroyelem] \ + -attrlist [list xmlns $xmlns(owner)]] + eval {$jlibname send_iq "set" [list $xmllist] -to $roomjid} $opts +} + +# jlib::muc::getroom -- +# +# + +proc jlib::muc::getroom {jlibname roomjid callback} { + variable xmlns + + set xmllist [wrapper::createtag "query" \ + -attrlist [list xmlns $xmlns(owner)]] + $jlibname send_iq "get" [list $xmllist] -to $roomjid \ + -command [concat $callback $jlibname] +} + +# jlib::muc::mynick -- +# +# Returns own nick name for room, or empty if not there. + +proc jlib::muc::mynick {jlibname roomjid} { + upvar ${jlibname}::muc::cache cache + + if {[info exists cache($roomjid,mynick)]} { + return $cache($roomjid,mynick) + } else { + return "" + } +} + +# jlib::muc::allroomsin -- +# +# Returns a list of all room jid's we are inside. + +proc jlib::muc::allroomsin {jlibname} { + upvar ${jlibname}::muc::cache cache + + set roomList [list] + foreach key [array names cache "*,inside"] { + regexp {(.+),inside} $key match room + lappend roomList $room + } + return $roomList +} + +proc jlib::muc::isroom {jlibname jid} { + upvar ${jlibname}::muc::rooms rooms + + if {[info exists rooms($jid)]} { + return 1 + } else { + return 0 + } +} + +# jlib::muc::participants -- +# +# + +proc jlib::muc::participants {jlibname roomjid} { + upvar ${jlibname}::muc::cache cache + + set everyone [list] + + # The rosters presence elements should give us all info we need. + foreach userAttr [$jlibname roster getpresence $roomjid -type available] { + unset -nocomplain attr + array set attr $userAttr + lappend everyone $roomjid/$attr(-resource) + } + return $everyone +} + +proc jlib::muc::reset {jlibname} { + upvar ${jlibname}::muc::cache cache + upvar ${jlibname}::muc::rooms rooms + + unset -nocomplain cache + unset -nocomplain rooms +} + +proc jlib::muc::Debug {num str} { + variable debug + if {$num <= $debug} { + puts $str + } +} + +# We have to do it here since need the initProc befor doing this. + +namespace eval jlib::muc { + + jlib::ensamble_register muc \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +#------------------------------------------------------------------------------- + diff --git a/lib/jabberlib/pep.tcl b/lib/jabberlib/pep.tcl new file mode 100644 index 0000000..eece5d6 --- /dev/null +++ b/lib/jabberlib/pep.tcl @@ -0,0 +1,389 @@ +# pep.tcl -- +# +# This file is part of the jabberlib. It contains support code +# for the Personal Eventing PubSub +# (xmlns='http://jabber.org/protocol/pubsub') XEP-0163. +# +# Copyright (c) 2007 Mats Bengtsson +# Copyright (c) 2006 Antonio Cano Damas +# +# This file is distributed under BSD style license. +# +# $Id: pep.tcl,v 1.10 2007/09/06 13:20:47 matben Exp $ +# +############################# USAGE ############################################ +# +# INSTANCE COMMANDS +# jlibName pep create +# jlibName pep have +# jlibName pep publish +# jlibName pep retract +# jlibName pep subscribe +# +################################################################################ +# +# With PEP version 1.0 and mutual presence subscriptions we only need: +# +# jlibName pep have +# jlibName pep publish +# jlibName pep retract ?-notify 0|1? +# +# Typical names and nodes: +# activity 'http://jabber.org/protocol/activity' +# geoloc 'http://jabber.org/protocol/geoloc' +# mood 'http://jabber.org/protocol/mood' +# tune 'http://jabber.org/protocol/tune' +# +# NB: It is currently unclear there should be an id attribute in the item +# element since PEP doesn't use it but pubsub do, and the experimental +# OpenFire PEP implementation. + +package require jlib::disco +package require jlib::pubsub + +package provide jlib::pep 0.3 + +namespace eval jlib::pep { + + # Common xml namespaces. + variable xmlns + array set xmlns { + node_config "http://jabber.org/protocol/pubsub#node_config" + } + + variable state +} + +# jlib::pep::init -- +# +# Creates a new instance of the pep object. + +proc jlib::pep::init {jlibname} { + + # Instance specifics arrays. + namespace eval ${jlibname}::pep { + variable autosub + set autosub(presreg) 0 + } +} + +proc jlib::pep::cmdproc {jlibname cmd args} { + return [eval {$cmd $jlibname} $args] +} + +# Setting own PEP -------------------------------------------------------------- +# +# Disco server for PEP, disco own bare JID, create pubsub node. +# +# 1) Disco server for pubsub/pep support +# 2) Create node if not there (optional) +# 3) Publish item + +# jlib::pep::have -- +# +# Simplified way to know if a JID supports PEP or not. +# Typically only needed for the server JID. +# The command just gets invoked with: jlibname boolean + +proc jlib::pep::have {jlibname jid cmd} { + $jlibname disco get_async info $jid [namespace code [list OnPepDisco $cmd]] +} + +proc jlib::pep::OnPepDisco {cmd jlibname type from subiq args} { + + set havepep 0 + if {$type eq "result"} { + set node [wrapper::getattribute $subiq node] + + # Check if disco returns + if {[$jlibname disco iscategorytype pubsub/pep $from $node]} { + set havepep 1 + } + } + uplevel #0 $cmd [list $jlibname $havepep] +} + +# jlib::pep::create -- +# +# Create a PEP node service. +# This shall not be necessary if we want just the default configuration. +# +# Arguments: +# node typically xmlns +# args: -access_model "presence", "open", "roster", or "whitelist" +# -fields additional list of field elements +# -command tclProc +# +# Results: +# none + +proc jlib::pep::create {jlibname node args} { + variable xmlns + + array set argsA { + -access_model presence + -command {} + -fields {} + } + array set argsA $args + + # Configure setup for PEP node + set valueFormE [wrapper::createtag value -chdata $xmlns(node_config)] + set fieldFormE [wrapper::createtag field \ + -attrlist [list var "FORM_TYPE" type hidden] \ + -subtags [list $valueFormE]] + + # PEP Values for access_model: roster / presence / open or authorize / whitelist + set valueModelE [wrapper::createtag value -chdata $argsA(-access_model)] + set fieldModelE [wrapper::createtag field \ + -attrlist [list var "pubsub#access_model"] \ + -subtags [list $valueModelE]] + + set xattr [list xmlns "jabber:x:data" type submit] + set xsubE [list $fieldFormE $fieldModelE] + set xsubE [concat $xsubE $argsA(-fields)] + set xE [wrapper::createtag x -attrlist $xattr -subtags $xsubE] + + $jlibname pubsub create -node $node -configure $xE -command $argsA(-command) +} + +# jlib::pep::publish -- +# +# Publish a stanza into the PEP node (create an item to a node) +# Typically: +# +# +# +# +# +# curse my nurse! +# +# +# +# +# Arguments: +# node typically xmlns +# itemE XML stanza to publishing +# args for the 'publish subscribe' +# +# Results: +# none + +proc jlib::pep::publish {jlibname node itemE args} { + eval {$jlibname pubsub publish $node -items [list $itemE]} $args +} + +# jlib::pep::retract -- +# +# Retract a PEP item (Delete an item from a node) +# +# Arguments: +# node typically xmlns +# +# Results: +# none + +proc jlib::pep::retract {jlibname node args} { + #set itemE [wrapper::createtag item] + # Se comment above about this one. + set itemE [wrapper::createtag item -attrlist [list id current]] + eval {$jlibname pubsub retract $node [list $itemE]} $args +} + +# Others PEP ------------------------------------------------------------------- +# +# In normal circumstances with mutual presence subscriptions we don't +# need to do pusub subscribe. +# +# 1) disco bare JID (not necessary for 1.0) +# 2) subscribe to node (not necessary for 1.0) +# 3) handle events (pubsub register_event tclProc -node) + +# jlib::pep::subscribe -- +# +# Arguments: +# jid JID which we want to subscribe to. +# node typically xmlns +# args: anything for the pubsub command, like -command. +# +# Results: +# none + +proc jlib::pep::subscribe {jlibname jid node args} { + + # If an entity is not subscribed to the account owner's presence, + # it MUST subscribe to a node using.... + set myjid2 [$jlibname myjid2] + eval {$jlibname pubsub subscribe $jid $myjid2 -node $node} $args +} + +# @@@ OUTDATED; BACKUP !!!!!!!!!!!!!!! + +# jlib::pep::set_auto_subscribe -- +# +# Subscribe all available users automatically. + +proc jlib::pep::set_auto_subscribe {jlibname node args} { + upvar ${jlibname}::pep::autosub autosub + + array set argsA { + -command {} + } + array set argsA $args + set autosub($node,node) $node + set autosub($node,-command) $argsA(-command) + + # For those where we've already got presence. + set jidL [$jlibname roster getusers -type available] + foreach jid $jidL { + + # We may not yet have disco info for this. + if {[$jlibname disco iscategorytype gateway/* $jid]} { + continue + } + + # If Juliet's server supports PEP (thereby making juliet@capulet.com + # a virtual pubsub service), it MUST return an identity of "pubsub/pep" + $jlibname disco get_async items $jid \ + [list [namespace current]::OnDiscoItems $node] + } + + # And register an event handler for any presence. + if {!$autosub(presreg)} { + set autosub(presreg) 1 + $jlibname presence_register_int available \ + [namespace code [list PresenceEvent $node]] + } +} + +proc jlib::pep::list_auto_subscribe {jlibname} { + upvar ${jlibname}::pep::autosub autosub + + set nodes {} + foreach {key node} [array get autosub *,node] { + lappend nodes $node + } + return $nodes +} + +proc jlib::pep::have_auto_subscribe {jlibname node} { + upvar ${jlibname}::pep::autosub autosub + + return [info exists autosub($node,node)] +} + +proc jlib::pep::unset_auto_subscribe {jlibname node} { + upvar ${jlibname}::pep::autosub autosub + + array unset autosub $node,* + if {![llength [array names autosub *,node]]} { + set autosub(presreg) 0 + $jlibname presence_deregister_int available \ + [namespace code [list PresenceEvent $node]] + } +} + +proc jlib::pep::PresenceEvent {jlibname xmldata node} { + upvar ${jlibname}::pep::autosub autosub + variable state + + set type [wrapper::getattribute $xmldata type] + set from [wrapper::getattribute $xmldata from] + if {$type eq ""} { + set type "available" + } + set jid2 [jlib::barejid $from] + if {![$jlibname roster isitem $jid2]} { + return + } + if {[$jlibname disco iscategorytype gateway/* $from]} { + return + } + + # We should be careful not to disco/publish for each presence change. + # @@@ There is a small glitch here if user changes presence before we + # received its disco result. + if {![$jlibname disco isdiscoed info $from]} { + foreach {key node} [array get autosub $node,*] { + $jlibname disco get_async items $jid2 \ + [list [namespace current]::OnDiscoItems $node] + } + } +} + +proc jlib::pep::OnDiscoItems {node jlibname type from subiq args} { + + # Get contact PEP nodes. + if {$type eq "result"} { + set nodes [$jlibname disco nodes $from] + if {[lsearch -exact $nodes $node] >= 0} { + + # NEW PEP: + # If an entity is not subscribed to the account owner's presence, + # it MUST subscribe to a node using.... + set subscribe [$jlibname roster getsubscription $from] + set myjid2 [$jlibname myjid2] + $jlibname pubsub subscribe $from $myjid2 -node $node \ + -command $autosub($node,-command) + } + } +} + +# We have to do it here since need the initProc before doing this. +namespace eval jlib::pep { + + jlib::ensamble_register pep \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +# Test: +if {0} { + package require jlib::pep + set jlibname ::jlib::jlib1 + set moodNode "http://jabber.org/protocol/mood" + set mood "neutral" + proc cb {args} {puts "---> $args"} + set server [$jlibname getserver] + set myjid2 [$jlibname myjid2] + $jlibname pubsub register_event cb -node $moodNode + $jlibname disco send_get info $server cb + + # List items + $jlibname disco send_get items $myjid2 cb + $jlibname pubsub items $myjid2 $moodNode + + # Retract item from node + set pepE [wrapper::createtag mood -attrlist [list xmlns $moodNode]] + set itemE [wrapper::createtag item -subtags [list $pepE]] + $jlibname pubsub retract $moodNode [list $itemE] + + # Delete node + $jlibname pubsub delete $myjid2 $moodNode + + # Publish item to node + set moodChildEs [list [wrapper::createtag mood]] + set moodE [wrapper::createtag mood \ + -attrlist [list xmlns $moodNode] -subtags $moodChildEs] + set itemE [wrapper::createtag item -subtags [list $moodE]] + $jlibname pubsub publish $moodNode -items [list $itemE] -command cb + + # User + set jid matben2@stor.no-ip.org + $jlibname disco send_get info $jid cb + $jlibname disco send_get items $jid cb + $jlibname roster getsubscription $jid + $jlibname pubsub items $jid $moodNode + + # PEP + # Owner + $jlibname pep have $server cb + $jlibname pep create $moodNode + $jlibname pep publish $moodNode $itemE + + # User + $jlibname disco send_get items $jid cb + $jlibname pep subscribe $jid $moodNode + +} + diff --git a/lib/jabberlib/pkgIndex.tcl b/lib/jabberlib/pkgIndex.tcl new file mode 100644 index 0000000..1edaffd --- /dev/null +++ b/lib/jabberlib/pkgIndex.tcl @@ -0,0 +1,41 @@ +# Tcl package index file, version 1.1 +# This file is generated by the "pkg_mkIndex" command +# and sourced either when an application starts up or +# by a "package unknown" script. It invokes the +# "package ifneeded" command to set up package-related +# information so that packages will be loaded automatically +# in response to "package require" commands. When this +# script is sourced, the variable $dir must contain the +# full path name of this file's directory. + +package ifneeded groupchat 1.0 [list source [file join $dir groupchat.tcl]] +package ifneeded jlib 2.0 [list source [file join $dir jabberlib.tcl]] +package ifneeded jlib::http 0.1 [list source [file join $dir jlibhttp.tcl]] +package ifneeded jlibsasl 1.0 [list source [file join $dir jlibsasl.tcl]] +package ifneeded jlibtls 1.0 [list source [file join $dir jlibtls.tcl]] +package ifneeded saslmd5 1.0 [list source [file join $dir saslmd5.tcl]] +package ifneeded service 1.0 [list source [file join $dir service.tcl]] +package ifneeded stanzaerror 1.0 [list source [file join $dir stanzaerror.tcl]] +package ifneeded streamerror 1.0 [list source [file join $dir streamerror.tcl]] +package ifneeded tinydom 0.2 [list source [file join $dir tinydom.tcl]] +package ifneeded wrapper 1.2 [list source [file join $dir wrapper.tcl]] + +package ifneeded jlib::avatar 0.1 [list source [file join $dir avatar.tcl]] +package ifneeded jlib::bind 0.1 [list source [file join $dir bind.tcl]] +package ifneeded jlib::bytestreams 0.4 [list source [file join $dir bytestreams.tcl]] +package ifneeded jlib::caps 0.3 [list source [file join $dir caps.tcl]] +package ifneeded jlib::compress 0.1 [list source [file join $dir compress.tcl]] +package ifneeded jlib::connect 0.1 [list source [file join $dir connect.tcl]] +package ifneeded jlib::disco 0.1 [list source [file join $dir disco.tcl]] +package ifneeded jlib::dns 0.1 [list source [file join $dir jlibdns.tcl]] +package ifneeded jlib::ftrans 0.1 [list source [file join $dir ftrans.tcl]] +package ifneeded jlib::ibb 0.1 [list source [file join $dir ibb.tcl]] +package ifneeded jlib::jingle 0.1 [list source [file join $dir jingle.tcl]] +package ifneeded jlib::muc 0.3 [list source [file join $dir muc.tcl]] +package ifneeded jlib::pep 0.3 [list source [file join $dir pep.tcl]] +package ifneeded jlib::pubsub 0.2 [list source [file join $dir pubsub.tcl]] +package ifneeded jlib::roster 1.0 [list source [file join $dir roster.tcl]] +package ifneeded jlib::si 0.1 [list source [file join $dir si.tcl]] +package ifneeded jlib::sipub 0.2 [list source [file join $dir sipub.tcl]] +package ifneeded jlib::util 0.1 [list source [file join $dir util.tcl]] +package ifneeded jlib::vcard 0.1 [list source [file join $dir vcard.tcl]] diff --git a/lib/jabberlib/pubsub.tcl b/lib/jabberlib/pubsub.tcl new file mode 100644 index 0000000..d4f485b --- /dev/null +++ b/lib/jabberlib/pubsub.tcl @@ -0,0 +1,709 @@ +# pubsub.tcl -- +# +# This file is part of the jabberlib. It contains support code +# for the pub-sub (xmlns='http://jabber.org/protocol/pubsub') XEP-0060. +# +# Copyright (c) 2005-2006 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: pubsub.tcl,v 1.21 2008/01/01 09:05:27 matben Exp $ +# +############################# USAGE ############################################ +# +# INSTANCE COMMANDS +# jlibName pubsub affiliations +# jlibName pubsub create +# jlibName pubsub delete +# jlibName pubsub deregister_event +# jlibName pubsub items +# jlibName pubsub options +# jlibName pubsub publish +# jlibName pubsub purge +# jlibName pubsub register_event +# jlibName pubsub retract +# jlibName pubsub subscribe +# jlibName pubsub unsubscribe +# +################################################################################ +# +# BRIEF: +# +# pubsub-service +# node +# item +# item +# ... +# node +# item +# ... +# ... +# +# Owner use case: +# +# create node +# delete node +# +# publish item to a node +# retract (remove) item from a node +# +# User use case: +# +# register for events +# subscribe to a node +# unsubscribe from a node +# +################################################################################ + +package provide jlib::pubsub 0.2 + +namespace eval jlib::pubsub { + + variable debug 0 + + # Common xml namespaces. + variable xmlns + array set xmlns { + pubsub "http://jabber.org/protocol/pubsub" + errors "http://jabber.org/protocol/pubsub#errors" + event "http://jabber.org/protocol/pubsub#event" + owner "http://jabber.org/protocol/pubsub#owner" + } +} + +# jlib::pubsub::init -- +# +# Creates a new instance of the pubsub object. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# +# Results: +# namespaced instance command + +proc jlib::pubsub::init {jlibname} { + + variable xmlns + + # Instance specific arrays. + namespace eval ${jlibname}::pubsub { + variable items + variable events + } + + # Register event notifier. + $jlibname message_register normal $xmlns(event) [namespace code event] +} + +proc jlib::pubsub::cmdproc {jlibname cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +# jlib::pubsub::create -- +# +# Create a new pubsub node. +# +# Arguments: +# args: +# -to (JID) if not indicated, we are using PEP recomendations +# -command tclProc +# -configure 0 no configure element +# 1 new node with default configuration +# xmldata jabber:x:data element +# -node the nodeID (else we get an instant node) +# +# Results: +# none + +proc jlib::pubsub::create {jlibname args} { + + variable xmlns + + set attr [list] + set opts [list] + set configure 0 + foreach {key value} $args { + set name [string trimleft $key -] + + switch -- $key { + -command { + lappend opts -command $value + } + -configure { + set configure $value + } + -node { + lappend attr $name $value + } + -to { + lappend opts -to $value + } + } + } + set subtags [list [wrapper::createtag create -attrlist $attr]] + if {$configure eq "1"} { + lappend subtags [wrapper::createtag configure] + } elseif {[wrapper::validxmllist $configure]} { + lappend subtags [wrapper::createtag configure -subtags [list $configure]] + } + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(pubsub)] -subtags $subtags]] + eval {jlib::send_iq $jlibname set $xmllist} $opts +} + +# jlib::pubsub::configure -- +# +# Get or set configuration options for a node. +# +# Arguments: +# type: get|set +# to: JID +# +# Results: +# none + +proc jlib::pubsub::configure {jlibname type to node args} { + + variable xmlns + + set opts [list -to $to] + set xE [list] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + -x { + set xE $value + } + } + } + set subtags [list [wrapper::createtag configure \ + -attrlist [list node $node] -subtags [list $xE]]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(owner)] -subtags $subtags]] + eval {jlib::send_iq $jlibname $type $xmllist} $opts +} + +# jlib::pubsub::default -- +# +# Request default configuration options for new nodes. + +proc jlib::pubsub::default {jlibname to args} { + + variable xmlns + + set opts [list -to $to] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + } + } + set subtags [list [wrapper::createtag default]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(owner)] -subtags $subtags]] + eval {jlib::send_iq $jlibname get $xmllist} $opts +} + +# jlib::pubsub::delete -- +# +# Delete a node. + +proc jlib::pubsub::delete {jlibname to node args} { + + variable xmlns + + set opts [list -to $to] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + } + } + set subtags [list [wrapper::createtag delete -attrlist [list node $node]]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(owner)] -subtags $subtags]] + eval {jlib::send_iq $jlibname set $xmllist} $opts +} + +# jlib::pubsub::purge -- +# +# Purge all node items. (Deletes all items of a node.) + +proc jlib::pubsub::purge {jlibname to node args} { + + variable xmlns + + set opts [list -to $to] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + } + } + set subtags [list [wrapper::createtag purge -attrlist [list node $node]]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(owner)] -subtags $subtags]] + eval {jlib::send_iq $jlibname set $xmllist} $opts +} + +# jlib::pubsub::subscriptions -- +# +# Gets or sets subscriptions. +# +# Arguments: +# type: get|set +# to: JID +# node: pubsub nodeID +# args: +# -command tclProc +# -subscriptions list of subscription elements +# Results: +# none + +proc jlib::pubsub::subscriptions {jlibname type to node args} { + + variable xmlns + + set opts [list -to $to] + set subsEs [list] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + -subscriptions { + set subsEs $value + } + } + } + set subtags [list [wrapper::createtag subscriptions \ + -attrlist [list node $node] -subtags $subsEs]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(owner)] -subtags $subtags]] + eval {jlib::send_iq $jlibname $type $xmllist} $opts +} + +# jlib::pubsub::affiliations -- +# +# Gets or sets affiliations. +# +# Arguments: +# type: get|set +# to: JID +# node: pubsub nodeID +# args: +# -command tclProc +# -affiliations list of affiliation elements +# Results: +# none + +proc jlib::pubsub::affiliations {jlibname type to node args} { + + variable xmlns + + set opts [list -to $to] + set affEs [list] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + -affiliations { + set affEs $value + } + } + } + set subtags [list [wrapper::createtag affiliations] \ + -attrlist [list node $node] -subtags $affEs]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(pubsub)] -subtags $subtags]] + eval {jlib::send_iq $jlibname $type $xmllist} $opts +} + +# jlib::pubsub::items -- +# +# Retrieve items from a node. + +proc jlib::pubsub::items {jlibname to node args} { + + variable xmlns + + set opts [list -to $to] + set attr [list node $node] + set itemids [list] + foreach {key value} $args { + set name [string trimleft $key -] + + switch -- $key { + -command { + lappend opts -command $value + } + -itemids { + set itemids $value + } + -max_items - -subid { + lappend attr $name $value + } + } + } + set items [list] + foreach id $itemids { + lappend items [wrapper::createtag item -attrlist [list id $id]] + } + set subtags [list [wrapper::createtag items \ + -attrlist $attr -subtags $items]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(pubsub)] -subtags $subtags]] + eval {jlib::send_iq $jlibname get $xmllist} $opts +} + +# jlib::pubsub::options -- +# +# Gets or sets options for a JID+node +# +# Arguments: +# type: set or get +# to: JID for pubsub service +# jid: the subscribed JID +# args: +# -command tclProc +# -subid subscription ID +# -xdata +# +# Results: +# none + +proc jlib::pubsub::options {jlibname type to jid node args} { + + variable xmlns + + set opts [list -to $to] + set attr [list node $node jid $jid] + set xdata [list] + + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + -subid { + lappend attr subid $value + } + -xdata { + set xdata $value + } + } + } + set optE [list [wrapper::createtag options \ + -attrlist $attr -subtags [list $xdata]]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(pubsub)] -subtags $optE]] + eval {jlib::send_iq $jlibname $type $xmllist} $opts +} + +# jlib::pubsub::publish -- +# +# Publish an item to a node. +# +# Arguments: +# args: +# -to (JID) if not indicated, we are using PEP recomendations +# -command tclProc +# -configure 0 no configure element +# 1 new node with default configuration +# xmldata jabber:x:data element +# -node the nodeID (else we get an instant node) +# -items +# +# Results: +# none + +proc jlib::pubsub::publish {jlibname node args} { + + variable xmlns + + set opts [list] + set itemEs [list] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + -items { + set itemEs $value + } + -to { + lappend opts -to $value + } + } + } + set subtags [list [wrapper::createtag publish \ + -attrlist [list node $node] -subtags $itemEs]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(pubsub)] -subtags $subtags]] + eval {jlib::send_iq $jlibname set $xmllist} $opts +} + +# jlib::pubsub::retract -- +# +# Delete an item from a node. + +proc jlib::pubsub::retract {jlibname node items args} { + + variable xmlns + + set opts [list] + set attr [list node $node] + + foreach {key value} $args { + switch -- $key { + -command { + lappend opts $name $value + } + -notify { + # Must be boolean. + lappend attr notify $value + } + -to { + lappend opts -to $value + } + } + } + set subtags [list [wrapper::createtag retract \ + -attrlist $attr -subtags $items]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(pubsub)] -subtags $subtags]] + eval {jlib::send_iq $jlibname set $xmllist} $opts +} + +# jlib::pubsub::subscribe -- +# +# Subscribe to a JID+nodeID. +# +# Arguments: +# to: JID for pubsub service +# jid: the subscribed JID +# args: +# -command tclProc +# -node pubsub nodeID; MUST be there except for root collection +# node +# +# Results: +# + +proc jlib::pubsub::subscribe {jlibname to jid args} { + + variable xmlns + + set opts [list -to $to] + set attr [list jid $jid] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + -node { + lappend attr node $value + } + } + } + set subEs [list [wrapper::createtag subscribe -attrlist $attr]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(pubsub)] -subtags $subEs]] + eval {jlib::send_iq $jlibname set $xmllist} $opts +} + +# jlib::pubsub::unsubscribe -- +# +# Unsubscribe to a JID+nodeID. + +proc jlib::pubsub::unsubscribe {jlibname to jid node args} { + + variable xmlns + + set opts [list -to $to] + set attr [list node $node jid $jid] + foreach {key value} $args { + switch -- $key { + -command { + lappend opts -command $value + } + -subid { + lappend attr subid $value + } + } + } + set unsubE [list [wrapper::createtag unsubscribe -attrlist $attr]] + set xmllist [list [wrapper::createtag pubsub \ + -attrlist [list xmlns $xmlns(pubsub)] -subtags $unsubE]] + eval {jlib::send_iq $jlibname set $xmllist} $opts +} + +# jlib::pubsub::register_event -- +# +# Register for specific pubsub events. +# +# Arguments: +# jlibname: the instance of this jlib. +# func: tclProc +# args: -from +# -node +# -seq priority 0-100 (D=50) +# +# Results: +# none. + +# @@@ TODO: +# +# +# +# +# + +proc jlib::pubsub::register_event {jlibname func args} { + + upvar ${jlibname}::events events + + # args: -from, -node + set from "*" + set node "*" + set seq 50 + + foreach {key value} $args { + switch -- $key { + -from { + set from [jlib::ESC $value] + } + -node { + + # The pubsub service MUST ensure that the NodeID conforms to + # the Resourceprep profile of Stringprep as described in + # RFC 3920. + # @@@ ??? + set node [jlib::resourceprep $value] + } + -seq { + set seq $value + } + } + } + set pattern "$from,$node" + lappend events($pattern) [list $func $seq] + set events($pattern) \ + [lsort -integer -index 1 [lsort -unique $events($pattern)]] +} + +proc jlib::pubsub::deregister_event {jlibname func args} { + + upvar ${jlibname}::events events + + set from "*" + set node "*" + + foreach {key value} $args { + switch -- $key { + -from { + set from [jlib::ESC $value] + } + -node { + set node [jlib::resourceprep $value] + } + } + } + set pattern "$from,$node" + if {[info exists events($pattern)]} { + set idx [lsearch -glob $events($pattern) [list $func *]] + if {$idx >= 0} { + set events($pattern) [lreplace $events($pattern) $idx $idx] + } + } +} + +# jlib::pubsub::event -- +# +# The event notifier. Dispatches events to the relevant registered +# event handlers. +# +# Normal events: +# +# +# +# ... ENTRY ... +# +# +# + +proc jlib::pubsub::event {jlibname ns msgE args} { + + variable xmlns + upvar ${jlibname}::events events + + array set aargs $args + set xmldata $aargs(-xmldata) + + set from [wrapper::getattribute $xmldata from] + set nodes [list] + + set eventEs [wrapper::getchildswithtagandxmlns $xmldata event $xmlns(event)] + foreach eventE $eventEs { + set itemsEs [wrapper::getchildswithtag $eventE items] + foreach itemsE $itemsEs { + lappend nodes [wrapper::getattribute $itemsE node] + } + } + foreach node $nodes { + set key "$from,$node" + foreach {pattern value} [array get events] { + if {[string match $pattern $key]} { + foreach spec $value { + set func [lindex $spec 0] + set code [catch { + uplevel #0 $func [list $jlibname $xmldata] + } ans] + if {$code} { + bgerror "jlib::pubsub::event $func failed: $code\n$::errorInfo" + } + } + } + } + } +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::pubsub { + + jlib::ensamble_register pubsub \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +if {0} { + # Test code. + set jlib jlib::jlib1 + set psjid pubsub.sgi.se + set psjid pubsub.devrieze.dyndns.org + set myjid [$jlib myjid2] + set server [$jlib getserver] + set itemE [wrapper::createtag item -attrlist [list id 123456789]] + proc cb {args} {puts "---> $args"} + set node mats + set node home/$server/matben/xyz + + $jlib pubsub create -to $psjid -node $node -command cb + $jlib pubsub register_event cb -from $psjid -node $node + $jlib pubsub subscribe $psjid $myjid -node $node -command cb + $jlib pubsub subscriptions get $psjid $node -command cb + $jlib pubsub publish $node -to $psjid -items [list $itemE] + +} + +#------------------------------------------------------------------------------- + diff --git a/lib/jabberlib/readme b/lib/jabberlib/readme new file mode 100644 index 0000000..b437d5a --- /dev/null +++ b/lib/jabberlib/readme @@ -0,0 +1,12 @@ + +All jabberlib sources are distributed under the BSD license. + +README + + - Install TclXML as a proper package. Must be patched version! + + - start wish + + - TODO + + - If you run tclsh instead of wish be sure to start the event loop. diff --git a/lib/jabberlib/roster.tcl b/lib/jabberlib/roster.tcl new file mode 100644 index 0000000..238221c --- /dev/null +++ b/lib/jabberlib/roster.tcl @@ -0,0 +1,1659 @@ +# roster.tcl -- +# +# An object for storing the roster and presence information for a +# jabber client. Is used together with jabberlib. +# +# Copyright (c) 2001-2006 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: roster.tcl,v 1.68 2008/03/29 11:55:06 matben Exp $ +# +# Note that every jid in the rostA is usually (always) without any resource, +# but the jid's in the presA are identical to the 'from' attribute, except +# the presA($jid-2,res) which have any resource stripped off. The 'from' +# attribute are (always) with /resource. +# +# All jid's in internal arrays are STRINGPREPed! +# +# Variables used in roster: +# +# rostA(groups) : List of all groups the exist in roster. +# +# rostA($jid,item) : $jid. +# +# rostA($jid,name) : Name of $jid. +# +# rostA($jid,groups) : Groups $jid is in. Note: PLURAL! +# +# rostA($jid,subscription) : Subscription of $jid (to|from|both|"") +# +# rostA($jid,ask) : "Ask" of $jid +# (subscribe|unsubscribe|"") +# +# presA($jid-2,res) : List of resources for this $jid. +# +# presA($from,type) : One of 'available' or 'unavailable. +# +# presA($from,status) : The presence status element. +# +# presA($from,priority) : The presence priority element. +# +# presA($from,show) : The presence show element. +# +# presA($from,x,xmlns) : Storage for x elements. +# xmlns is a namespace but where any +# http://jabber.org/protocol/ stripped off +# +# oldpresA : As presA but any previous state. +# +# state($jid,*) : Keeps other info not directly related +# to roster or presence elements. +# +############################# USAGE ############################################ +# +# Changes to the state of this object should only be made from jabberlib, +# and never directly by the client! +# +# NAME +# roster - an object for roster and presence information. +# +# SYNOPSIS +# jlibname roster cmd ?? +# +# INSTANCE COMMANDS +# jlibname roster availablesince jid +# jlibname roster clearpresence ?jidpattern? +# jlibname roster getgroups ?jid? +# jlibname roster getask jid +# jlibname roster getcapsattr jid name +# jlibname roster getname jid +# jlibname roster getpresence jid ?-resource, -type? +# jlibname roster getresources jid +# jlibname roster gethighestresource jid +# jlibname roster getrosteritem jid +# jlibname roster getstatus jid +# jlibname roster getsubscription jid +# jlibname roster getusers ?-type available|unavailable? +# jlibname roster getx jid xmlns +# jlibname roster getextras jid xmlns +# jlibname roster isavailable jid +# jlibname roster isitem jid +# jlibname roster haveroster +# jlibname roster reset +# jlibname roster send_get ?-command tclProc? +# jlibname roster send_remove ?-command tclProc? +# jlibname roster send_set ?-command tclProc, -name, -groups? +# jlibname roster wasavailable jid +# +# The 'clientCommand' procedure must have the following form: +# +# clientCommand {jlibname what {jid {}} args} +# +# where 'what' can be any of: enterroster, exitroster, presence, remove, set. +# The args is a list of '-key value' pairs with the following keys for each +# 'what': +# enterroster: no keys +# exitroster: no keys +# presence: -resource (required) +# -type (required) +# -status (optional) +# -priority (optional) +# -show (optional) +# -x (optional) +# -extras (optional) +# remove: no keys +# set: -name (optional) +# -subscription (optional) +# -groups (optional) +# -ask (optional) +# +################################################################################ + +package require jlib + +package provide jlib::roster 1.0 + +namespace eval jlib::roster { + + variable rostGlobals + + # Globals same for all instances of this roster. + set rostGlobals(debug) 0 + + # List of all rostA element sub entries. First the actual roster, + # with 'rostA($jid,...)' + set rostGlobals(tags) {name groups ask subscription} + + # ...and the presence arrays: 'presA($jid/$resource,...)' + # The list of resources is treated separately (presA($jid,res)) + set rostGlobals(presTags) {type status priority show x} + + # Used for sorting resources. + variable statusPrio + array set statusPrio { + chat 1 + available 2 + away 3 + xa 4 + dnd 5 + invisible 6 + unavailable 7 + } + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::roster::roster -- +# +# This creates a new instance of a roster. +# +# Arguments: +# clientCmd: callback procedure when internals of roster or +# presence changes. +# args: +# +# Results: +# + +proc jlib::roster::init {jlibname args} { + + # Instance specific namespace. + namespace eval ${jlibname}::roster { + variable rostA + variable presA + variable options + variable priv + + set priv(haveroster) 0 + } + + # Set simpler variable names. + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::options options + + # Register for roster pushes. + $jlibname iq_register set "jabber:iq:roster" [namespace code set_handler] + + # Register for presence. Be sure they are first in order. + # @@@ We should have a separate internal register API to avoid any conflicts. + $jlibname presence_register_int available \ + [namespace code presence_handler] 10 + $jlibname presence_register_int unavailable \ + [namespace code presence_handler] 10 + + set rostA(groups) [list] + set options(cmd) "" + + jlib::register_package roster +} + +# jlib::roster::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# cmd: +# args: all args to the cmd procedure. +# +# Results: +# none. + +proc jlib::roster::cmdproc {jlibname cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +# jlib::roster::register_cmd -- +# +# This sets a client callback command. + +proc jlib::roster::register_cmd {jlibname cmd} { + upvar ${jlibname}::roster::options options + + set options(cmd) $cmd +} + +proc jlib::roster::haveroster {jlibname} { + upvar ${jlibname}::roster::priv priv + + return $priv(haveroster) +} + +# jlib::roster::send_get -- +# +# Request our complete roster. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# args: -command tclProc +# +# Results: +# none. + +proc jlib::roster::send_get {jlibname args} { + + array set argsA {-command {}} + array set argsA $args + + set queryE [wrapper::createtag "query" \ + -attrlist [list xmlns jabber:iq:roster]] + jlib::send_iq $jlibname "get" [list $queryE] \ + -command [list [namespace current]::send_get_cb $jlibname $argsA(-command)] + return +} + +proc jlib::roster::send_get_cb {jlibname cmd type queryE} { + + if {![string equal $type "error"]} { + enterroster $jlibname + handle_roster $jlibname $queryE + exitroster $jlibname + } + if {$cmd ne {}} { + uplevel #0 $cmd [list $type $queryE] + } +} + +# jlib::roster::set_handler -- +# +# This gets called for roster pushes. + +proc jlib::roster::set_handler {jlibname from queryE args} { + + handle_roster $jlibname $queryE + + # RFC 3921, sect 8.1: + # The 'from' and 'to' addresses are OPTIONAL in roster pushes; ... + # A client MUST acknowledge each roster push with an IQ stanza of + # type "result"... + array set argsA $args + if {[info exists argsA(-id)]} { + $jlibname send_iq "result" {} -id $argsA(-id) + } + return 1 +} + +proc jlib::roster::handle_roster {jlibname queryE} { + + upvar ${jlibname}::roster::itemA itemA + + foreach itemE [wrapper::getchildren $queryE] { + if {[wrapper::gettag $itemE] ne "item"} { + continue + } + set subscription "none" + set opts [list] + set havejid 0 + foreach {aname avalue} [wrapper::getattrlist $itemE] { + set $aname $avalue + if {$aname eq "jid"} { + set havejid 1 + } else { + lappend opts -$aname $avalue + } + } + + # This shall NEVER happen! + if {!$havejid} { + continue + } + set mjid [jlib::jidmap $jid] + if {$subscription eq "remove"} { + unset -nocomplain itemA($mjid) + removeitem $jlibname $jid + } else { + set itemA($mjid) $itemE + set groups [list] + foreach groupE [wrapper::getchildswithtag $itemE group] { + lappend groups [wrapper::getcdata $groupE] + } + if {[llength $groups]} { + lappend opts -groups $groups + } + eval {setitem $jlibname $jid} $opts + } + } +} + +# jlib::roster::send_set -- +# +# To set/add an jid in/to your roster. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: jabber user id to add/set. +# args: +# -command tclProc +# -name $name: A name to show the user-id as on roster to the user. +# -groups $group_list: Groups of user. If you omit this, then the user's +# groups will be set according to the user's options +# stored in the roster object. If user doesn't exist, +# or you haven't got your roster, user's groups will be +# set to "", which means no groups. +# +# Results: +# none. + +proc jlib::roster::send_set {jlibname jid args} { + + upvar ${jlibname}::roster::rostA rostA + + array set argsA {-command {}} + array set argsA $args + + set mjid [jlib::jidmap $jid] + + # Find group(s). + if {[info exists argsA(-groups)]} { + set groups $argsA(-groups) + } elseif {[info exists rostA($mjid,groups)]} { + set groups $rostA($mjid,groups) + } else { + set groups [list] + } + + set attr [list jid $jid] + set name "" + if {[info exists argsA(-name)] && [string length $argsA(-name)]} { + set name $argsA(-name) + lappend attr name $name + } + set groupEs [list] + foreach group $groups { + if {$group ne ""} { + lappend groupEs [wrapper::createtag "group" -chdata $group] + } + } + + # Roster items get pushed to us. Only any errors need to be taken care of. + set itemE [wrapper::createtag "item" -attrlist $attr -subtags $groupEs] + set queryE [wrapper::createtag "query" \ + -attrlist [list xmlns jabber:iq:roster] -subtags [list $itemE]] + jlib::send_iq $jlibname "set" [list $queryE] -command $argsA(-command) + return +} + +proc jlib::roster::send_remove {jlibname jid args} { + + array set argsA {-command {}} + array set argsA $args + + # Roster items get pushed to us. Only any errors need to be taken care of. + set itemE [wrapper::createtag "item" \ + -attrlist [list jid $jid subscription remove]] + set queryE [wrapper::createtag "query" \ + -attrlist [list xmlns jabber:iq:roster] -subtags [list $itemE]] + jlib::send_iq $jlibname "set" [list $queryE] -command $argsA(-command) + return +} + +# jlib::roster::setitem -- +# +# Adds or modifies an existing roster item. +# Features not set are left as they are; features not set will give +# nonexisting array entries, just to differentiate between an empty +# element and a nonexisting one. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: 2-tier jid, with no /resource, usually. +# Some transports keep a resource part in jid. +# args: a list of '-key value' pairs, where '-key' is any of: +# -name value +# -subscription value +# -groups list Note: GROUPS in plural! +# -ask value +# +# Results: +# none. + +proc jlib::roster::setitem {jlibname jid args} { + variable rostGlobals + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::options options + + Debug 2 "roster::setitem jid='$jid', args='$args'" + + set mjid [jlib::jidmap $jid] + + # Clear out the old state since an 'ask' element may still be lurking. + foreach key $rostGlobals(tags) { + unset -nocomplain rostA($mjid,$key) + } + + # This array is better than list to keep track of users. + set rostA($mjid,item) $mjid + + # Old values will be overwritten, nonexisting options will result in + # nonexisting array entries. + foreach {name value} $args { + set par [string trimleft $name "-"] + set rostA($mjid,$par) $value + if {[string equal $par "groups"]} { + foreach gr $value { + if {[lsearch -exact $rostA(groups) $gr] < 0} { + lappend rostA(groups) $gr + } + } + } + } + + # Be sure to evaluate the registered command procedure. + if {[string length $options(cmd)]} { + uplevel #0 $options(cmd) [list $jlibname set $jid] $args + } + return +} + +# jlib::roster::removeitem -- +# +# Removes an existing roster item and all its presence info. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: 2-tier jid with no /resource. +# +# Results: +# none. + +proc jlib::roster::removeitem {jlibname jid} { + variable rostGlobals + + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::presA presA + upvar ${jlibname}::roster::oldpresA oldpresA + upvar ${jlibname}::roster::options options + + Debug 2 "roster::removeitem jid='$jid'" + + set mjid [jlib::jidmap $jid] + + # Be sure to evaluate the registered command procedure. + # Do this BEFORE unsetting the internal state! + if {[string length $options(cmd)]} { + uplevel #0 $options(cmd) [list $jlibname remove $jid] + } + + # First the roster, then presence... + foreach name $rostGlobals(tags) { + unset -nocomplain rostA($mjid,$name) + } + unset -nocomplain rostA($mjid,item) + + # Be sure to unset all, also jid3 entries! + array unset presA [jlib::ESC $mjid]* + array unset oldpresA [jlib::ESC $mjid]* + return +} + +# jlib::roster::ClearRoster -- +# +# Removes all existing roster items but keeps all presence info.(?) +# and list of resources. +# +# Arguments: +# jlibname: the instance of this jlib. +# +# Results: +# none. Callback evaluated. + +proc jlib::roster::ClearRoster {jlibname} { + + variable rostGlobals + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::itemA itemA + upvar ${jlibname}::roster::options options + + Debug 2 "roster::ClearRoster" + + # Remove the roster. + foreach {x mjid} [array get rostA *,item] { + foreach key $rostGlobals(tags) { + unset -nocomplain rostA($mjid,$key) + } + } + array unset rostA *,item + unset -nocomplain itemA + + # Be sure to evaluate the registered command procedure. + if {[string length $options(cmd)]} { + uplevel #0 $options(cmd) [list $jlibname enterroster] + } + return +} + +# jlib::roster::enterroster -- +# +# Is called when new roster coming. +# +# Arguments: +# jlibname: the instance of this jlib. +# +# Results: +# none. + +proc jlib::roster::enterroster {jlibname} { + + ClearRoster $jlibname +} + +# jlib::roster::exitroster -- +# +# Is called when finished receiving a roster get command. +# +# Arguments: +# jlibname: the instance of this jlib. +# +# Results: +# none. Callback evaluated. + +proc jlib::roster::exitroster {jlibname} { + + upvar ${jlibname}::roster::options options + upvar ${jlibname}::roster::priv priv + + set priv(haveroster) 1 + + # Be sure to evaluate the registered command procedure. + if {[string length $options(cmd)]} { + uplevel #0 $options(cmd) [list $jlibname exitroster] + } +} + +# jlib::roster::reset -- +# +# Removes everything stored in the roster object, including all roster +# items and any presence information. + +proc jlib::roster::reset {jlibname} { + + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::presA presA + upvar ${jlibname}::roster::priv priv + + unset -nocomplain rostA presA + set rostA(groups) {} + set priv(haveroster) 0 +} + +# jlib::roster::clearpresence -- +# +# Removes all presence cached internally for jid glob pattern. +# Helpful when exiting a room. +# +# Arguments: +# jlibname: the instance of this jlib. +# jidpattern: glob pattern for items to remove. +# +# Results: +# none. + +proc jlib::roster::clearpresence {jlibname {jidpattern ""}} { + + upvar ${jlibname}::roster::presA presA + upvar ${jlibname}::roster::oldpresA oldpresA + + Debug 2 "roster::clearpresence '$jidpattern'" + + if {$jidpattern eq ""} { + unset -nocomplain presA + } else { + array unset presA $jidpattern + array unset oldpresA $jidpattern + } +} + +proc jlib::roster::presence_handler {jlibname xmldata} { + presence $jlibname $xmldata + return 0 +} + +# jlib::roster::presence -- +# +# Registered internal presence handler for 'available' and 'unavailable' +# that caches all presence info. + +proc jlib::roster::presence {jlibname xmldata} { + + variable rostGlobals + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::presA presA + upvar ${jlibname}::roster::oldpresA oldpresA + upvar ${jlibname}::roster::state state + + Debug 2 "jlib::roster::presence" + + set from [wrapper::getattribute $xmldata from] + set type [wrapper::getattribute $xmldata type] + if {$type eq ""} { + set type "available" + } + + # We don't handle subscription types (remove?). + if {$type ne "available" && $type ne "unavailable"} { + return + } + + set mjid [jlib::jidmap $from] + jlib::splitjid $mjid mjid2 res + + # Set secs only if unavailable before. + if {![info exists presA($mjid,type)] \ + || ($presA($mjid,type) eq "unavailable")} { + set state($mjid,secs) [clock seconds] + } + + # Keep cache of any old state. + # Note special handling of * for array unset - prefix with \\ to quote. + array unset oldpresA [jlib::ESC $mjid],* + array set oldpresA [array get presA [jlib::ESC $mjid],*] + + # Clear out the old presence state since elements may still be lurking. + array unset presA [jlib::ESC $mjid],* + + # Add to list of resources. + set presA($mjid2,res) [lsort -unique [lappend presA($mjid2,res) $res]] + + set presA($mjid,type) $type + + foreach E [wrapper::getchildren $xmldata] { + set tag [wrapper::gettag $E] + set chdata [wrapper::getcdata $E] + + switch -- $tag { + priority { + if {[string is integer -strict $chdata]} { + set presA($mjid,$tag) $chdata + } + } + status { + set presA($mjid,$tag) $chdata + } + show { + if {[regexp {^(away|chat|dnd|xa)$} $chdata]} { + set presA($mjid,$tag) $chdata + } + } + x { + set ns [wrapper::getattribute $E xmlns] + regexp {http://jabber.org/protocol/(.*)$} $ns - ns + set presA($mjid,x,$ns) $E + } + default { + + # This can be anything properly namespaced. + set ns [wrapper::getattribute $E xmlns] + set presA($mjid,extras,$ns) $E + } + } + } +} + + +# Firts attempt to keep the jid's as they are reported, with no separate +# resource part. + +proc jlib::roster::setpresence2 {jlibname xmldata} { + + +} + +# jlib::roster::getrosteritem -- +# +# Returns the state of an existing roster item. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: . +# +# Results: +# a list of '-key value' pairs where key is any of: +# name, groups, subscription, ask. Note GROUPS in plural! + +proc jlib::roster::getrosteritem {jlibname jid} { + + variable rostGlobals + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::options options + + Debug 2 "roster::getrosteritem jid='$jid'" + + set mjid [jlib::jidmap $jid] + if {![info exists rostA($mjid,item)]} { + return {} + } + set result [list] + foreach key $rostGlobals(tags) { + if {[info exists rostA($mjid,$key)]} { + lappend result -$key $rostA($mjid,$key) + } + } + return $result +} + +proc jlib::roster::getitem {jlibname jid} { + + upvar ${jlibname}::roster::itemA itemA + + set mjid [jlib::jidmap $jid] + if {[info exists itemA($mjid)]} { + return $itemA($mjid) + } else { + return {} + } +} + +# jlib::roster::isitem -- +# +# Does the jid exist in the roster? + +proc jlib::roster::isitem {jlibname jid} { + + upvar ${jlibname}::roster::rostA rostA + + set mjid [jlib::jidmap $jid] + return [expr {[info exists rostA($mjid,item)] ? 1 : 0}] +} + +# jlib::roster::getrosterjid -- +# +# Returns the matching jid as reported by a roster item. +# If given a full JID try match this, else bare JID. +# If given a bare JID try match this, else find any matching full JID. +# For ordinary users this is a jid2. +# +# @@@ NB: For the new xmpp lib we shall have a mapping from the roster JID +# to a set of online JID's if any, which shall be completely indpendent +# of bare vs. full JID forms! +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: +# +# Results: +# a jid or empty if no matching roster item. + +proc jlib::roster::getrosterjid {jlibname jid} { + + upvar ${jlibname}::roster::rostA rostA + + set mjid [jlib::jidmap $jid] + if {[info exists rostA($mjid,item)]} { + return $jid + } else { + set mjid2 [jlib::barejid $mjid] + if {[info exists rostA($mjid2,item)]} { + return [jlib::barejid $jid] + } else { + set name [array names rostA [jlib::ESC $mjid2]*,item] + if {[llength $name] == 1} { + # There should only be one. + return [string map {",item" ""} $name] + } + } + } + return +} + +# jlib::roster::getusers -- +# +# Returns a list of jid's of all existing roster items. +# +# Arguments: +# jlibname: the instance of this jlib. +# args: -type available|unavailable +# +# Results: +# list of all 2-tier jid's in roster + +proc jlib::roster::getusers {jlibname args} { + + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::presA presA + + set all {} + foreach {x jid} [array get rostA *,item] { + lappend all $jid + } + array set argsA $args + set jidlist {} + if {$args == {}} { + set jidlist $all + } elseif {[info exists argsA(-type)]} { + set type $argsA(-type) + set jidlist {} + foreach jid2 $all { + set isavailable 0 + + # Be sure to handle empty resources as well: '1234@icq.host' + foreach key [array names presA "[jlib::ESC $jid2]*,type"] { + if {[string equal $presA($key) "available"]} { + set isavailable 1 + break + } + } + if {$isavailable && [string equal $type "available"]} { + lappend jidlist $jid2 + } elseif {!$isavailable && [string equal $type "unavailable"]} { + lappend jidlist $jid2 + } + } + } + return $jidlist +} + +# jlib::roster::getpresence -- +# +# Returns the presence state of an existing roster item. +# This is as reported in presence element. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: username@server, without /resource. +# args ?-resource, -type? +# -resource: return presence for this alone, +# else a list for each resource. +# Allow empty resources!!?? +# -type: return presence for (un)available only. +# +# Results: +# a list of '-key value' pairs where key is any of: +# resource, type, status, priority, show, x. +# If the 'resource' in argument is not given, +# the result contains a sublist for each resource. IMPORTANT! Bad? +# BAD!!!!!!!!!!!!!!!!!!!!!!!! + +proc jlib::roster::getpresence {jlibname jid args} { + + variable rostGlobals + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::presA presA + upvar ${jlibname}::roster::options options + + Debug 2 "roster::getpresence jid=$jid, args='$args'" + + set jid [jlib::jidmap $jid] + array set argsA $args + set haveRes 0 + if {[info exists argsA(-resource)]} { + set haveRes 1 + set resource $argsA(-resource) + } + + # It may happen that there is no roster item for this jid (groupchat). + if {![info exists presA($jid,res)] || ($presA($jid,res) eq "")} { + if {[info exists argsA(-type)] && \ + [string equal $argsA(-type) "available"]} { + return + } else { + if {$haveRes} { + return [list -resource $resource -type unavailable] + } else { + return [list [list -resource "" -type unavailable]] + } + } + } + + set result [list] + if {$haveRes} { + + # Return presence only from the specified resource. + # Be sure to handle empty resources as well: '1234@icq.host' + if {[lsearch -exact $presA($jid,res) $resource] < 0} { + return [list -resource $resource -type unavailable] + } + set result [list -resource $resource] + if {$resource eq ""} { + set jid3 $jid + } else { + set jid3 $jid/$resource + } + if {[info exists argsA(-type)] && \ + ![string equal $argsA(-type) $presA($jid3,type)]} { + return + } + foreach key $rostGlobals(presTags) { + if {[info exists presA($jid3,$key)]} { + lappend result -$key $presA($jid3,$key) + } + } + } else { + + # Get presence for all resources. + # Be sure to handle empty resources as well: '1234@icq.host' + foreach res $presA($jid,res) { + set thisRes [list -resource $res] + if {$res eq ""} { + set jid3 $jid + } else { + set jid3 $jid/$res + } + if {[info exists argsA(-type)] && \ + ![string equal $argsA(-type) $presA($jid3,type)]} { + # Empty. + } else { + foreach key $rostGlobals(presTags) { + if {[info exists presA($jid3,$key)]} { + lappend thisRes -$key $presA($jid3,$key) + } + } + lappend result $thisRes + } + } + } + return $result +} + +# UNFINISHED!!!!!!!!!! +# Return empty list or -type unavailable ??? +# '-key value' or 'key value' ??? +# Returns a list of flat arrays + +proc jlib::roster::getpresence2 {jlibname jid args} { + + variable rostGlobals + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::presA2 presA2 + upvar ${jlibname}::roster::options options + + Debug 2 "roster::getpresence2 jid=$jid, args='$args'" + + array set argsA { + -type * + } + array set argsA $args + + set mjid [jlib::jidmap $jid] + jlib::splitjid $mjid jid2 resource + set result {} + + if {$resource eq ""} { + + # 2-tier jid. Match any resource. + set arrlist [concat [array get presA2 [jlib::ESC $mjid],jid] \ + [array get presA2 [jlib::ESC $mjid]/*,jid]] + foreach {key value} $arrlist { + set thejid $value + set jidresult {} + foreach {akey avalue} [array get presA2 [jlib::ESC $thejid],*] { + set thekey [string map [list $thejid, ""] $akey] + lappend jidresult -$thekey $avalue + } + if {[llength $jidresult]} { + lappend result $jidresult + } + } + } else { + + # 3-tier jid. Only exact match. + if {[info exists presA2($mjid,type)]} { + if {[string match $argsA(-type) $presA2($mjid,type)]} { + set result [list [list -jid $jid -type $presA2($mjid,type)]] + } + } else { + set result [list [list -jid $jid -type unavailable]] + } + } + return $result +} + +# jlib::roster::getoldpresence -- +# +# This makes a simplified assumption and uses the full JID. + +proc jlib::roster::getoldpresence {jlibname jid} { + + variable rostGlobals + upvar ${jlibname}::roster::rostA rostA + upvar ${jlibname}::roster::oldpresA oldpresA + + set jid [jlib::jidmap $jid] + + if {[info exists oldpresA($jid,type)]} { + set result [list] + foreach key $rostGlobals(presTags) { + if {[info exists oldpresA($jid,$key)]} { + lappend result -$key $oldpresA($jid,$key) + } + } + } else { + set result [list -type unavailable] + } + return $result +} + +# jlib::roster::getgroups -- +# +# Returns the list of groups for this jid, or an empty list if not +# exists. If no jid, return a list of all groups existing in this roster. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: (optional). +# +# Results: +# a list of groups or empty. + +proc jlib::roster::getgroups {jlibname {jid {}}} { + + upvar ${jlibname}::roster::rostA rostA + + Debug 2 "roster::getgroups jid='$jid'" + + set jid [jlib::jidmap $jid] + if {[string length $jid]} { + if {[info exists rostA($jid,groups)]} { + return $rostA($jid,groups) + } else { + return + } + } else { + set rostA(groups) [lsort -unique $rostA(groups)] + return $rostA(groups) + } +} + +# jlib::roster::getname -- +# +# Returns the roster name of this jid. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: +# +# Results: +# the roster name or empty. + +proc jlib::roster::getname {jlibname jid} { + + upvar ${jlibname}::roster::rostA rostA + + set jid [jlib::jidmap $jid] + if {[info exists rostA($jid,name)]} { + return $rostA($jid,name) + } else { + return "" + } +} + +# jlib::roster::getsubscription -- +# +# Returns the 'subscription' state of this jid. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: +# +# Results: +# the 'subscription' state or "none" if no 'subscription' state. + +proc jlib::roster::getsubscription {jlibname jid} { + + upvar ${jlibname}::roster::rostA rostA + + set jid [jlib::jidmap $jid] + if {[info exists rostA($jid,subscription)]} { + return $rostA($jid,subscription) + } else { + return none + } +} + +# jlib::roster::getask -- +# +# Returns the 'ask' state of this jid. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: +# +# Results: +# the 'ask' state or empty if no 'ask' state. + +proc jlib::roster::getask {jlibname jid} { + + upvar ${jlibname}::roster::rostA rostA + + Debug 2 "roster::getask jid='$jid'" + + if {[info exists rostA($jid,ask)]} { + return $rostA($jid,ask) + } else { + return "" + } +} + +# jlib::roster::getresources -- +# +# Returns a list of all resources for this JID or empty. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: a JID without any resource (jid2) typically. +# it must be the JID which is reported by roster. +# args ?-type? +# -type: return presence for (un)available only. +# +# Results: +# a list of all resources for this jid or empty. + +proc jlib::roster::getresources {jlibname jid args} { + + upvar ${jlibname}::roster::presA presA + + Debug 2 "roster::getresources jid='$jid'" + array set argsA $args + + set jid [jlib::jidmap $jid] + if {[info exists presA($jid,res)]} { + if {[info exists argsA(-type)]} { + + # Need to loop through all resources for this jid. + set resL [list] + set type $argsA(-type) + foreach res $presA($jid,res) { + + # Be sure to handle empty resources as well: '1234@icq.host' + if {$res eq ""} { + set jid3 $jid + } else { + set jid3 $jid/$res + } + if {[string equal $argsA(-type) $presA($jid3,type)]} { + lappend resL $res + } + } + return $resL + } else { + return $presA($jid,res) + } + } else { + + # If the roster JID is something like: icq.home.se/registered + set jid2 [jlib::barejid $jid] + if {[info exists presA($jid2,res)]} { + if {[info exists argsA(-type)]} { + + # Need to loop through all resources for this jid. + set resL [list] + set type $argsA(-type) + foreach res $presA($jid2,res) { + + # Be sure to handle empty resources as well: '1234@icq.host' + if {$res eq ""} { + set jid3 $jid2 + } else { + set jid3 $jid2/$res + } + if {[string equal $argsA(-type) $presA($jid3,type)]} { + lappend resL $res + } + } + return $resL + } else { + return $presA($jid2,res) + } + } else { + return + } + } +} + +proc jlib::roster::getmatchingjids2 {jlibname jid args} { + + upvar ${jlibname}::roster::presA2 presA2 + + set jidlist {} + set arrlist [concat [array get presA2 [jlib::ESC $mjid],jid] \ + [array get presA2 [jlib::ESC $mjid]/*,jid]] + foreach {key value} $arrlist { + lappend jidlist $value + } + return $jidlist +} + +# jlib::roster::gethighestresource -- +# +# Returns the resource with highest priority for this jid or empty. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: a jid without any resource (jid2). +# +# Results: +# a resource for this jid or empty if unavailable. + +proc jlib::roster::gethighestresource {jlibname jid} { + + upvar ${jlibname}::roster::presA presA + variable statusPrio + + Debug 2 "roster::gethighestresource jid='$jid'" + + set jid [jlib::jidmap $jid] + set maxResL [list] + + # @@@ Perhaps this sorting shall be made when receiving presence instead? + + if {[info exists presA($jid,res)]} { + + # Find the resource corresponding to the highest priority (D=0). + set maxPrio -128 + + foreach res $presA($jid,res) { + + # Be sure to handle empty resources as well: '1234@icq.host' + if {$res eq ""} { + set jid3 $jid + } else { + set jid3 $jid/$res + } + if {[info exists presA($jid3,type)]} { + if {$presA($jid3,type) eq "available"} { + set prio 0 + if {[info exists presA($jid3,priority)]} { + set prio $presA($jid3,priority) + } + if {$prio > $maxPrio} { + set maxPrio $prio + set maxResL [list $res] + } elseif {$prio == $maxPrio} { + lappend maxResL $res + } + } + } + } + } + if {[llength $maxResL] == 1} { + set maxRes [lindex $maxResL 0] + } elseif {[llength $maxResL] > 1} { + + # Sort according to show attributes. + set resIndL [list] + foreach res $maxResL { + if {$res eq ""} { + set jid3 $jid + } else { + set jid3 $jid/$res + } + set show "available" + if {[info exists presA($jid3,show)]} { + set show $presA($jid3,show) + } + lappend resIndL [list $res $statusPrio($show)] + } + set resIndL [lsort -integer -index 1 $resIndL] + set maxRes [lindex $resIndL 0 0] + } else { + set maxRes "" + } + return $maxRes +} + +proc jlib::roster::getmaxpriorityjid2 {jlibname jid} { + + upvar ${jlibname}::roster::presA2 presA2 + + Debug 2 "roster::getmaxpriorityjid2 jid='$jid'" + + # Find the resource corresponding to the highest priority (D=0). + set maxjid "" + set maxpri 0 + foreach jid3 [getmatchingjids2 $jlibname $jid] { + if {[info exists presA2($jid3,priority)]} { + if {$presA2($jid3,priority) > $maxpri} { + set maxjid $jid3 + set maxpri $presA2($jid3,priority) + } + } + } + return $jid3 +} + +# jlib::roster::isavailable -- +# +# Returns boolean 0/1. Returns 1 only if presence is equal to available. +# If 'jid' without resource, return 1 if any is available. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: either 'username$hostname', or 'username$hostname/resource'. +# +# Results: +# 0/1. + +proc jlib::roster::isavailable {jlibname jid} { + + upvar ${jlibname}::roster::presA presA + + Debug 2 "roster::isavailable jid='$jid'" + + set jid [jlib::jidmap $jid] + + # If any resource in jid, we get it here. + jlib::splitjid $jid jid2 resource + + if {[string length $resource] > 0} { + if {[info exists presA($jid2/$resource,type)]} { + if {[string equal $presA($jid2/$resource,type) "available"]} { + return 1 + } else { + return 0 + } + } else { + return 0 + } + } else { + + # Be sure to allow for 'user@domain' with empty resource. + foreach key [array names presA "[jlib::ESC $jid2]*,type"] { + if {[string equal $presA($key) "available"]} { + return 1 + } + } + return 0 + } +} + +proc jlib::roster::isavailable2 {jlibname jid} { + + upvar ${jlibname}::roster::presA2 presA2 + + Debug 2 "roster::isavailable jid='$jid'" + + set jid [jlib::jidmap $jid] + + # If any resource in jid, we get it here. + jlib::splitjid $jid jid2 resource + + if {[string length $resource] > 0} { + if {[info exists presA($jid2/$resource,type)]} { + if {[string equal $presA($jid2/$resource,type) "available"]} { + return 1 + } else { + return 0 + } + } else { + return 0 + } + } else { + + # Be sure to allow for 'user@domain' with empty resource. + foreach key [array names presA "[jlib::ESC $jid2]*,type"] { + if {[string equal $presA($key) "available"]} { + return 1 + } + } + return 0 + } +} + +# jlib::roster::wasavailable -- +# +# As 'isavailable' but for any "old" former presence state. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: either 'username$hostname', or 'username$hostname/resource'. +# +# Results: +# 0/1. + +proc jlib::roster::wasavailable {jlibname jid} { + + upvar ${jlibname}::roster::oldpresA oldpresA + + Debug 2 "roster::wasavailable jid='$jid'" + + set jid [jlib::jidmap $jid] + + # If any resource in jid, we get it here. + jlib::splitjid $jid jid2 resource + + if {[string length $resource] > 0} { + if {[info exists oldpresA($jid2/$resource,type)]} { + if {[string equal $oldpresA($jid2/$resource,type) "available"]} { + return 1 + } else { + return 0 + } + } else { + return 0 + } + } else { + + # Be sure to allow for 'user@domain' with empty resource. + foreach key [array names oldpresA "[jlib::ESC $jid2]*,type"] { + if {[string equal $oldpresA($key) "available"]} { + return 1 + } + } + return 0 + } +} + +# jlib::roster::anychange -- +# +# Returns boolean telling us if any presence attributes as listed +# in 'nameList' has changed. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: the JID as reported in presence +# nameList: type | status | priority | show, D=type +# +# Results: +# 0/1. + +proc jlib::roster::anychange {jlibname jid {nameList type}} { + + upvar ${jlibname}::roster::presA presA + upvar ${jlibname}::roster::oldpresA oldpresA + + set jid [jlib::jidmap $jid] + + foreach name $nameList { + set have1 [info exists presA($jid,$name)] + set have2 [info exists oldpresA($jid,$name)] + if {$have1 && $have2} { + if {$presA($jid,$name) ne $oldpresA($jid,$name)} { + return 1 + } + } elseif {($have1 && !$have2) || (!$have1 && $have2)} { + return 1 + } + } + return 0 +} + +# jlib::roster::gettype -- +# +# Returns "available" or "unavailable". + +proc jlib::roster::gettype {jlibname jid} { + + upvar ${jlibname}::roster::presA presA + + set jid [jlib::jidmap $jid] + if {[info exists presA($jid,type)]} { + return $presA($jid,type) + } else { + return "unavailable" + } +} + +proc jlib::roster::getshow {jlibname jid} { + + upvar ${jlibname}::roster::presA presA + + set jid [jlib::jidmap $jid] + if {[info exists presA($jid,show)]} { + return $presA($jid,show) + } else { + return "" + } +} +proc jlib::roster::getstatus {jlibname jid} { + + upvar ${jlibname}::roster::presA presA + + set jid [jlib::jidmap $jid] + if {[info exists presA($jid,status)]} { + return $presA($jid,status) + } else { + return "" + } +} + +# jlib::roster::getx -- +# +# Returns the xml list for this jid's x element with given xml namespace. +# Returns empty if no matching info. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: any jid +# xmlns: the (mandatory) xmlns specifier. Any prefix +# http://jabber.org/protocol/ must be stripped off. +# @@@ BAD!!!! +# +# Results: +# xml list or empty. + +proc jlib::roster::getx {jlibname jid xmlns} { + + upvar ${jlibname}::roster::presA presA + + set jid [jlib::jidmap $jid] + if {[info exists presA($jid,x,$xmlns)]} { + return $presA($jid,x,$xmlns) + } else { + return + } +} + +# jlib::roster::getextras -- +# +# Returns the xml list for this jid's extras element with given xml namespace. +# Returns empty if no matching info. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: any jid +# xmlns: the (mandatory) full xmlns specifier. +# +# Results: +# xml list or empty. + +proc jlib::roster::getextras {jlibname jid xmlns} { + + upvar ${jlibname}::roster::presA presA + + set jid [jlib::jidmap $jid] + if {[info exists presA($jid,extras,$xmlns)]} { + return $presA($jid,extras,$xmlns) + } else { + return + } +} + +# jlib::roster::getcapsattr -- +# +# Access function for the caps elements attributes: +# +# +# +# +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: any jid +# attrname: +# +# Results: +# the value of the attribute or empty + +proc jlib::roster::getcapsattr {jlibname jid attrname} { + + upvar jlib::jxmlns jxmlns + upvar ${jlibname}::roster::presA presA + + set attr "" + set jid [jlib::jidmap $jid] + set xmlnscaps $jxmlns(caps) + if {[info exists presA($jid,extras,$xmlnscaps)]} { + set cElem $presA($jid,extras,$xmlnscaps) + set attr [wrapper::getattribute $cElem $attrname] + } + return $attr +} + +proc jlib::roster::havecaps {jlibname jid} { + + upvar jlib::jxmlns jxmlns + upvar ${jlibname}::roster::presA presA + + set xmlnscaps $jxmlns(caps) + return [info exists presA($jid,extras,$xmlnscaps)] +} + +# jlib::roster::availablesince -- +# +# Not sure exactly how delay elements are updated when new status set. + +proc jlib::roster::availablesince {jlibname jid} { + + upvar ${jlibname}::roster::presA presA + upvar ${jlibname}::roster::state state + + set jid [jlib::jidmap $jid] + set xmlns "jabber:x:delay" + if {[info exists presA($jid,x,$xmlns)]} { + + # An ISO 8601 point-in-time specification. clock works! + set stamp [wrapper::getattribute $presA($jid,x,$xmlns) stamp] + set time [clock scan $stamp -gmt 1] + } elseif {[info exists state($jid,secs)]} { + set time $state($jid,secs) + } else { + set time "" + } + return $time +} + +proc jlib::roster::getpresencesecs {jlibname jid} { + + upvar ${jlibname}::roster::state state + + set jid [jlib::jidmap $jid] + if {[info exists state($jid,secs)]} { + return $state($jid,secs) + } else { + return "" + } +} + +proc jlib::roster::Debug {num str} { + variable rostGlobals + if {$num <= $rostGlobals(debug)} { + puts "===========$str" + } +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::roster { + + jlib::ensamble_register roster \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/saslmd5.tcl b/lib/jabberlib/saslmd5.tcl new file mode 100644 index 0000000..de02b40 --- /dev/null +++ b/lib/jabberlib/saslmd5.tcl @@ -0,0 +1,484 @@ +# saslmd5.tcl -- +# +# This package provides a rudimentary implementation of the client side +# SASL authentication method using the DIGEST-MD5 mechanism. +# SASL [RFC 2222] +# DIGEST-MD5 [RFC 2831] +# ANONYMOUS [] +# +# It also includes the PLAIN mechanism, so saslmd5 is a misnomer. +# +# Copyright (c) 2004 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: saslmd5.tcl,v 1.13 2008/02/19 07:30:38 matben Exp $ + +package require base64 +package require md5 2.0 + +package provide saslmd5 1.0 + + +namespace eval saslmd5 { + + # These are in order of preference. + variable mechanisms [list "DIGEST-MD5" "PLAIN"] + # @@@ Enable this when testing. Not production code! + #variable mechanisms [list "DIGEST-MD5" "PLAIN" "ANONYMOUS"] + variable needed {username authzid pass realm} + variable uid 0 + +} + +# "static" methods. + +proc saslmd5::mechanisms {} { + variable mechanisms + return $mechanisms +} + +proc saslmd5::info {args} { + + # empty + return {} +} + +proc saslmd5::client_init {args} { + + # empty +} + +proc saslmd5::decode64 {str} { + return [::base64::decode $str] +} + +proc saslmd5::encode64 {str} { + + # important! no whitespace allowed in response! + return [string map [list "\n" ""] [::base64::encode $str]] +} + +# saslmd5::client_new -- +# +# Create a new instance for a session. +# +# Arguments: +# args -callbacks {{id proc} ...} with id any of +# {username authzid pass realm} +# note that everyone must be utf-8 encoded! +# -service name of service (xmpp) +# -serverFQDN servers fully qualified domain name +# -flags not used +# +# Results: +# token. + +proc saslmd5::client_new {args} { + variable uid + + #puts "saslmd5::client_new" + set token [namespace current]::[incr uid] + variable $token + upvar 0 $token state + + set state(step) 0 + set state(service) "" + set state(serverFQDN) "" + set state(flags) {} + + foreach {key value} $args { + switch -- $key { + -callbacks { + set_callbacks $token $value + } + -service - -serverFQDN - -flags { + set state([string trimleft $key -]) $value + } + default { + return -code error "unrocognized option \"$key\"" + } + } + } + + proc $token {cmd args} \ + "eval [namespace current]::cmdproc {$token} \$cmd \$args" + + return $token +} + +proc saslmd5::cmdproc {token cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {method_$cmd $token} $args] +} + +# class methods. + +# saslmd5::method_start -- +# +# Starts negotiating. +# +# Arguments: +# token +# args -mechanisms {list of mechanisms} +# +# Results: +# {returnCode list-or-error}. + +proc saslmd5::method_start {token args} { + variable $token + upvar 0 $token state + variable mechanisms + + #puts "saslmd5::method_start $args" + set state(step) 0 + + foreach {key value} $args { + switch -- $key { + -mechanisms { + set state(inmechanisms) $value + } + default { + # empty + } + } + } + if {![::info exists state(inmechanisms)]} { + return [list 1 "missing a \"-mechanisms\" option"] + } + + # we must have at least on of the servers announced mechanisms + set match 0 + foreach m $mechanisms { + if {[set idx [lsearch -exact $state(inmechanisms) $m]] >= 0} { + set match 1 + set mechanism [lindex $state(inmechanisms) $idx] + break + } + } + if {!$match} { + return [list 1 "the servers mechanisms \"$state(inmechanisms)\"\ + do not match any of the supported mechanisms \"$mechanisms\""] + } + set state(step) 1 + + switch -- $mechanism { + PLAIN { + set output [get_plain_output $token] + } + DIGEST-MD5 { + set output "" + } + ANONYMOUS { + set output [get_anonymous_output $token] + } + } + + # continue + return [list 4 [list mechanism $mechanism output $output]] +} + +proc saslmd5::get_plain_output {token} { + variable $token + upvar 0 $token state + + # SENT: + # somelongstring + # + # where somelongstring is (from Pandion's .js src): + # /* Plaintext algorithm: + # * Base64( UTF8( Addr ) + 0x00 + UTF8( User ) + 0x00 + UTF8( Pass ) ) + # */ + # User is the username, Addr is the full JID, and Pass is the password. + + request_userpars $token + + set username $state(upar,username) + set pass $state(upar,pass) + set realm $state(upar,realm) + + set user_lat1 [encoding convertto iso8859-1 $username] + set pass_lat1 [encoding convertto iso8859-1 $pass] + set realm_lat1 [encoding convertto iso8859-1 $realm] + + set jid [jlib::joinjid $user_lat1 $realm_lat1 ""] + return [binary format a*xa*xa* $jid $user_lat1 $pass_lat1] +} + +proc saslmd5::get_anonymous_output {token} { + + # @@@ Is this correct??? + return [jlib::generateuuid] +} + +# saslmd5::method_step -- +# +# Takes one step when negotiating. +# +# Arguments: +# token +# args -input challenge +# +# Results: +# {returnCode list-or-error}. + +proc saslmd5::method_step {token args} { + variable $token + upvar 0 $token state + + #puts "saslmd5::method_step $token, $args" + foreach {key value} $args { + switch -- $key { + -input { + set challenge $value + } + } + } + if {![::info exists challenge]} { + return [list 1 "must have -input challenge string"] + } + + if {$state(step) == 0} { + return [list 1 "need to call the 'start' procedure first"] + } elseif {$state(step) == 1} { + if {![iscapable $token]} { + return [list 1 "missing one or more callbacks"] + } + array set challarr [parse_challenge $challenge] + if {![::info exists challarr(nonce)]} { + return [list 1 "challenge missing 'nonce' attribute"] + } + if {![::info exists challarr(algorithm)]} { + return [list 1 "challenge missing 'algorithm' attribute"] + } + request_userpars $token + set output [process_challenge $token [array get challarr]] + incr state(step) + + # continue + set code 4 + } else { + incr state(step) + + # success + set output "" + set code 0 + } + return [list $code $output] +} + +proc saslmd5::method_setprop {token property value} { + variable $token + upvar 0 $token state + + # empty +} + +proc saslmd5::method_getprop {token property} { + variable $token + upvar 0 $token state + + # empty + return +} + +proc saslmd5::method_info {args} { + + # empty + return {} +} + +proc saslmd5::set_callbacks {token cblist} { + variable $token + upvar 0 $token state + + # some of tclsasl's id's are different from the spec's! + # note that everyone must be utf-8 encoded! + foreach cbpair $cblist { + foreach {id cbproc} $cbpair { + set state(cb,$id) $cbproc + } + } +} + +proc saslmd5::iscapable {token} { + variable $token + upvar 0 $token state + variable needed + + set capable 1 + foreach id $needed { + if {[::info exists state(cb,$id)] && ($state(cb,$id) != {})} { + # empty + } else { + set capable 0 + break + } + } + return $capable +} + +# saslmd5::request_userpars -- +# +# Invokes the needed callbacks to get user's parameters. + +proc saslmd5::request_userpars {token} { + variable $token + upvar 0 $token state + variable needed + + foreach id $needed { + if {[::info exists state(cb,$id)] && ($state(cb,$id) != {})} { + set plist [list id $id] + set state(upar,$id) [uplevel #0 $state(cb,$id) [list $plist]] + } else { + return -code error "missing one or more callbacks" + } + } +} + +# saslmd5::process_challenge -- +# +# Computes an output from a challenge using user's parameters. +# +# Arguments: +# token +# challenge +# +# Results: +# the output string as clear text. + +proc saslmd5::process_challenge {token challenge} { + variable $token + upvar 0 $token state + + array set charr $challenge + + # users parameters + set username $state(upar,username) + set authzid $state(upar,authzid) + set pass $state(upar,pass) + set realm $state(upar,realm) + + set host $state(serverFQDN) + set service $state(service) + + # make a 'cnonce' + set bytes "" + for {set n 0} {$n < 32} {incr n} { + set r [expr {int(256*rand())}] + append bytes [binary format c $r] + } + set cnonce [encode64 $bytes] + + # other + set realm $host + set nonce $charr(nonce) + set nc "00000001" + set diguri $service/$host + set qop "auth" + + # build 'response' (2.1.2.1 Response-value in RFC 2831) + # try to be a bit general here (from Cyrus SASL) + # + # encoding is a bit unclear. + # from RFC 2831: + # If "charset=UTF-8" is present, and all the characters of either + # "username-value" or "passwd" are in the ISO 8859-1 character set, + # then it must be converted to ISO 8859-1 before being hashed. + # + # from Cyrus SASL: + # if the string is entirely in the 8859-1 subset of UTF-8, then translate + # to 8859-1 prior to MD5 + + set user_lat1 [encoding convertto iso8859-1 $username] + set realm_lat1 [encoding convertto iso8859-1 $realm] + set pass_lat1 [encoding convertto iso8859-1 $pass] + set secret ${user_lat1}:${realm_lat1}:${pass_lat1} + set secretmd5 [::md5::md5 $secret] + set A1 ${secretmd5}:${nonce}:${cnonce} + if {$authzid ne ""} { + append A1 :${authzid} + } + set A2 AUTHENTICATE:${diguri} + if {$qop ne "auth"} { + append A2 ":00000000000000000000000000000000" + } + set HA1 [string tolower [::md5::md5 -hex $A1]] + set HA2 [string tolower [::md5::md5 -hex $A2]] + set KD ${HA1}:${nonce} + if {$qop ne ""} { + append KD :${nc}:${cnonce}:${qop}:${HA2} + } + set response [string tolower [::md5::md5 -hex $KD]] + + # build output + set output "" + append output "username=\"$username\"" + append output ",realm=\"$realm\"" + append output ",nonce=\"$nonce\"" + append output ",cnonce=\"$cnonce\"" + append output ",nc=\"$nc\"" + append output ",serv-type=\"$service\"" + append output ",host=\"$host\"" + append output ",digest-uri=\"$diguri\"" + append output ",qop=\"$qop\"" + append output ",response=\"$response\"" + append output ",charset=\"utf-8\"" + if {$authzid ne ""} { + append output ",authzid=\"$authzid\"" + } + return $output +} + +# saslmd5::parse_challenge -- +# +# Parses a clear text challenge string into a challenge list. + +proc saslmd5::parse_challenge {str} { + # RFC 2831 2.1 + # Char categories as per spec... + # Build up a regexp for splitting the challenge into key value pairs. + + set sep "\\\]\\\[\\\\()<>@,;:\\\"\\\?=\\\{\\\} \t" + set tok {0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\-\|\~\!\#\$\%\&\*\+\.\^\_\`} + set sqot {(?:\'(?:\\.|[^\'\\])*\')} + set dqot {(?:\"(?:\\.|[^\"\\])*\")} + set parameters {} + regsub -all "(\[${tok}\]+)=(${dqot}|(?:\[${tok}\]+))(?:\[${sep}\]+|$)" \ + $str {\1 \2 } parameters + return $parameters +} + +# RFC 2831 2.1 +# Char categories as per spec... +# Build up a regexp for splitting the challenge into key value pairs. + +proc saslmd5::parse_challengePT {str} { + puts "str=$str" + + set sep "\\\]\\\[\\\\()<>@,;:\\\"\\\?= \\\{\\\} \t" + set tok {0123456789ABCDEFGHIJKLMNOPQRS TUVWXYZabcdefghijklmnopqrstuvw xyz\-\|\~\!\#\$\%\&\*\+\.\^\_\ `} + set sqot {(?:\'(?:\\.|[^\'\\])*\')} + set dqot {(?:\"(?:\\.|[^\"\\])*\")} + set parameters {} + regsub -all "(\[${tok}\]+)=(${dqot}|(?:\[$ {tok}\]+))(?:\[${sep}\]+|$)" \ + $str {\1 \2 } parameters + puts "parameters=$parameters" + return $parameters +} + +# Fails when quotes are missing: +# str=nonce="1142339597",qop="auth",charset=utf-8,algorithm=md5-sess +# parameters=nonce "1142339597" qop "auth" charset=utf-8,algorithm=md5-sess + +proc saslmd5::free {token} { + variable $token + upvar 0 $token state + + unset -nocomplain state +} + diff --git a/lib/jabberlib/scripts/README-scripts b/lib/jabberlib/scripts/README-scripts new file mode 100644 index 0000000..92dc7e6 --- /dev/null +++ b/lib/jabberlib/scripts/README-scripts @@ -0,0 +1,11 @@ + +README-scripts +-------------- + +This folder is supposed to contain high level scripts using jabberlib to +perform various actions normally implemented at application level, such as: + + o register account + o remove account + o send message + diff --git a/lib/jabberlib/scripts/message.tcl b/lib/jabberlib/scripts/message.tcl new file mode 100644 index 0000000..5377ac8 --- /dev/null +++ b/lib/jabberlib/scripts/message.tcl @@ -0,0 +1,104 @@ +# message.tcl -- +# +# Simple script that uses jabberlib to send a message. +# +# Copyright (c) 2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: message.tcl,v 1.2 2007/08/06 07:49:54 matben Exp $ + +package require jlib +package require jlib::connect + +package provide jlibs::message 0.1 + +namespace eval jlibs::message { + + variable sendOpts {-subject -thread -body -type -xlist} +} + +interp alias {} jlibs::message {} jlibs::message::message + +# jlibs::message -- +# +# Make a complete new session and send a message. +# The options are passed on to 'connect' except: +# + +proc jlibs::message::message {jid password to cmd args} { + variable sendOpts + + set jlib [jlib::new [namespace code noop]] + + variable $jlib + upvar 0 $jlib state + + set state(jid) $jid + set state(password) $password + set state(to) $to + set state(cmd) $cmd + set state(args) $args + set state(jlib) $jlib + + jlib::util::from args -command + jlib::util::from args -noauth + + # Extract the message options. + foreach name $sendOpts { + set state($name) [jlib::util::from args $name] + } + eval {$jlib connect connect $jid $password \ + -command [namespace code cmdC]} $args + return $jlib +} + +proc jlibs::message::cmdC {jlib status {errcode ""} {errmsg ""}} { + variable sendOpts + variable $jlib + upvar 0 $jlib state + + if {![info exists state]} { + return + } + if {$status eq "ok"} { + set opts [list] + foreach name $sendOpts { + if {$state($name) ne ""} { + lappend opts $name $state($name) + } + } + eval {$jlib send_message $state(to)} $opts + finish $jlib + } elseif {$status eq "error"} { + finish $jlib $errcode + } +} + +proc jlibs::message::reset {jlib} { + finish $jlib reset +} + +proc jlibs::message::finish {jlib {err ""}} { + variable $jlib + upvar 0 $jlib state + + $jlib closestream + + if {$err ne ""} { + uplevel #0 $state(cmd) [list $jlib error $err] + } else { + uplevel #0 $state(cmd) [list $jlib ok] + } + unset -nocomplain state +} + +proc jlibs::message::noop {args} {} + +if {0} { + # Test: + proc cmd {args} {puts "---> $args"} + jlibs::message xyz@localhost xxx matben@localhost cmd -body Hej -subject Hej +} + + diff --git a/lib/jabberlib/scripts/password.tcl b/lib/jabberlib/scripts/password.tcl new file mode 100644 index 0000000..5dde657 --- /dev/null +++ b/lib/jabberlib/scripts/password.tcl @@ -0,0 +1,105 @@ +# password.tcl -- +# +# Simple script that uses jabberlib to change password. +# +# Copyright (c) 2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: password.tcl,v 1.1 2007/08/07 07:51:27 matben Exp $ + +package require jlib +package require jlib::connect + +package provide jlibs::password 0.1 + +namespace eval jlibs::password {} + +interp alias {} jlibs::password {} jlibs::password::password + +# jlibs::password -- +# +# Make a complete new session and change password. +# The options are passed on to 'connect' except: +# + +proc jlibs::password::password {jid password newpassword cmd args} { + + set jlib [jlib::new [namespace code noop]] + + variable $jlib + upvar 0 $jlib state + + set state(jid) $jid + set state(password) $password + set state(newpassword) $newpassword + set state(cmd) $cmd + set state(args) $args + set state(jlib) $jlib + + jlib::util::from args -command + jlib::util::from args -noauth + + eval {$jlib connect connect $jid $password \ + -command [namespace code cmdC]} $args + return $jlib +} + +proc jlibs::password::cmdC {jlib status {errcode ""} {errmsg ""}} { + variable sendOpts + variable $jlib + upvar 0 $jlib state + + if {![info exists state]} { + return + } + if {$status eq "ok"} { + jlib::splitjidex $state(jid) node server - + $jlib register_set $node $state(password) [namespace code cmdS] \ + -to $server + } elseif {$status eq "error"} { + finish $jlib $errcode + } +} + +proc jlibs::password::cmdS {jlib type iqchild args} { + variable $jlib + upvar 0 $jlib state + + if {![info exists state]} { + return + } + if {$type eq "result"} { + finish $jlib + } else { + finish $jlib $iqchild + } +} + +proc jlibs::password::reset {jlib} { + finish $jlib reset +} + +proc jlibs::password::finish {jlib {err ""}} { + variable $jlib + upvar 0 $jlib state + + $jlib closestream + + if {$err ne ""} { + uplevel #0 $state(cmd) [list $jlib error $err] + } else { + uplevel #0 $state(cmd) [list $jlib ok] + } + unset -nocomplain state +} + +proc jlibs::password::noop {args} {} + +if {0} { + # Test: + proc cmd {args} {puts "---> $args"} + jlibs::password xyz@localhost xxx yyy cmd +} + + diff --git a/lib/jabberlib/scripts/pkgIndex.tcl b/lib/jabberlib/scripts/pkgIndex.tcl new file mode 100644 index 0000000..e6ef643 --- /dev/null +++ b/lib/jabberlib/scripts/pkgIndex.tcl @@ -0,0 +1,13 @@ +# Tcl package index file, version 1.1 +# This file is generated by the "pkg_mkIndex" command +# and sourced either when an application starts up or +# by a "package unknown" script. It invokes the +# "package ifneeded" command to set up package-related +# information so that packages will be loaded automatically +# in response to "package require" commands. When this +# script is sourced, the variable $dir must contain the +# full path name of this file's directory. + +package ifneeded jlibs::register 0.1 [list source [file join $dir register.tcl]] +package ifneeded jlibs::unregister 0.1 [list source [file join $dir unregister.tcl]] +package ifneeded jlibs::message 0.1 [list source [file join $dir message.tcl]] diff --git a/lib/jabberlib/scripts/register.tcl b/lib/jabberlib/scripts/register.tcl new file mode 100644 index 0000000..6a1e66b --- /dev/null +++ b/lib/jabberlib/scripts/register.tcl @@ -0,0 +1,118 @@ +# register.tcl -- +# +# Simple script that uses jabberlib to register an account. +# +# Copyright (c) 2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: register.tcl,v 1.4 2007/08/07 07:50:25 matben Exp $ + +package require jlib +package require jlib::connect + +package provide jlibs::register 0.1 + +namespace eval jlibs::register {} + +interp alias {} jlibs::register {} jlibs::register::register + +# jlibs::register -- +# +# Make a complete new session and register an account. +# The options are passed on to 'connect'. + +proc jlibs::register::register {jid password cmd args} { + + set jlib [jlib::new [namespace code noop]] + + variable $jlib + upvar 0 $jlib state + + set state(jid) $jid + set state(password) $password + set state(cmd) $cmd + set state(args) $args + set state(jlib) $jlib + + jlib::util::from args -command + jlib::util::from args -noauth + jlib::splitjidex $jid node server - + + eval {$jlib connect connect $server {} \ + -noauth 1 -command [namespace code cmdC]} $args + return $jlib +} + +proc jlibs::register::cmdC {jlib status {errcode ""} {errmsg ""}} { + variable $jlib + upvar 0 $jlib state + + if {![info exists state]} { + return + } + if {$status eq "ok"} { + $jlib register_get [namespace code cmdG] + } elseif {$status eq "error"} { + finish $jlib $errcode + } +} + +proc jlibs::register::cmdG {jlib type iqchild} { + variable $jlib + upvar 0 $jlib state + + if {![info exists state]} { + return + } + if {$type eq "result"} { + jlib::splitjidex $state(jid) node server - + + # Assuming minimal registration fields. + $jlib register_set $node $state(password) [namespace code cmdS] + } else { + finish $jlib $iqchild + } +} + +proc jlibs::register::cmdS {jlib type iqchild args} { + variable $jlib + upvar 0 $jlib state + + if {![info exists state]} { + return + } + if {$type eq "result"} { + finish $jlib + } else { + finish $jlib $iqchild + } +} + +proc jlibs::register::reset {jlib} { + finish $jlib reset +} + +proc jlibs::register::finish {jlib {err ""}} { + variable $jlib + upvar 0 $jlib state + + $jlib closestream + + if {$err ne ""} { + uplevel #0 $state(cmd) [list $jlib error $err] + } else { + uplevel #0 $state(cmd) [list $jlib ok] + } + unset -nocomplain state +} + +proc jlibs::register::noop {args} {} + +if {0} { + # Test: + proc cmd {args} {puts "---> $args"} + jlibs::register xyz@localhost xxx cmd +} + + diff --git a/lib/jabberlib/scripts/unregister.tcl b/lib/jabberlib/scripts/unregister.tcl new file mode 100644 index 0000000..9ff5c6d --- /dev/null +++ b/lib/jabberlib/scripts/unregister.tcl @@ -0,0 +1,98 @@ +# unregister.tcl -- +# +# Simple script that uses jabberlib to unregister an account. +# +# Copyright (c) 2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: unregister.tcl,v 1.3 2007/08/06 07:49:54 matben Exp $ + +package require jlib +package require jlib::connect + +package provide jlibs::unregister 0.1 + +namespace eval jlibs::unregister {} + +interp alias {} jlibs::unregister {} jlibs::unregister::unregister + +# jlibs::unregister -- +# +# Make a complete new session and unregister an account. +# The options are passed on to 'connect'. + +proc jlibs::unregister::unregister {jid password cmd args} { + + #puts "jlibs::unregister::unregister" + set jlib [jlib::new [namespace code noop]] + + variable $jlib + upvar 0 $jlib state + + set state(jid) $jid + set state(password) $password + set state(cmd) $cmd + set state(args) $args + set state(jlib) $jlib + + jlib::util::from args -command + jlib::util::from args -noauth + + eval {$jlib connect connect $jid $password \ + -command [namespace code cmdC]} $args + return $jlib +} + +proc jlibs::unregister::cmdC {jlib status {errcode ""} {errmsg ""}} { + variable $jlib + upvar 0 $jlib state + + if {![info exists state]} { + return + } + if {$status eq "ok"} { + jlib::splitjidex $state(jid) node server - + $jlib register_remove $server [namespace code cmdR] + } elseif {$status eq "error"} { + finish $jlib $errcode + } +} + +proc jlibs::unregister::cmdR {jlib type subiq} { + + if {$type eq "result"} { + finish $jlib + } else { + finish $jlib $subiq + } +} + +proc jlibs::unregister::reset {jlib} { + finish $jlib reset +} + +proc jlibs::unregister::finish {jlib {err ""}} { + variable $jlib + upvar 0 $jlib state + + #puts "jlibs::unregister::finish" + $jlib closestream + + if {$err ne ""} { + uplevel #0 $state(cmd) [list $jlib error $err] + } else { + uplevel #0 $state(cmd) [list $jlib ok] + } + unset -nocomplain state +} + +proc jlibs::unregister::noop {args} {} + +if {0} { + # Test: + proc cmd {args} {puts "---> $args"} + jlibs::unregister xyz@localhost xxx cmd +} + + diff --git a/lib/jabberlib/service.tcl b/lib/jabberlib/service.tcl new file mode 100644 index 0000000..f312f10 --- /dev/null +++ b/lib/jabberlib/service.tcl @@ -0,0 +1,326 @@ +# service.tcl -- +# +# This is an abstraction layer for groupchat protocols gc-1.0/muc. +# All except disco/muc are EOL! +# +# Copyright (c) 2004-2006 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: service.tcl,v 1.27 2008/02/06 13:57:25 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# service - protocol independent methods for groupchats/muc +# +# SYNOPSIS +# jlib::service::init jlibName +# +# INSTANCE COMMANDS +# jlibName service allroomsin +# jlibName service exitroom room +# jlibName service isroom jid +# jlibName service nick jid +# jlibName service register type name +# jlibName service roomparticipants room +# jlibName service setroomprotocol jid protocol +# jlibName service unregister type name +# +# +# VARIABLES +# +# serv: +# serv(gcProtoPriority) : The groupchat protocol priority list. +# +# serv(gcprot,$jid) : Map a groupchat service jid to protocol: +# (gc-1.0|muc) +# +# serv(prefgcprot,$jid) : Stores preferred groupchat protocol that +# overrides the priority list. +# +############################# CHANGES ########################################## +# +# 0.1 first version + +package provide service 1.0 + +namespace eval ::jlib {} + +namespace eval jlib::service { + + # This is an abstraction layer for the groupchat protocols gc-1.0/muc. + + # Cache the following services in particular. + variable services {search register groupchat conference muc} + + # Maintain a priority list of groupchat protocols in decreasing priority. + # Entries must match: ( gc-1.0 | muc ) + variable groupchatTypeExp {(gc-1.0|muc)} +} + +proc jlib::service {jlibname cmd args} { + set ans [eval {[namespace current]::service::${cmd} $jlibname} $args] + return $ans +} + +proc jlib::service::init {jlibname} { + + upvar ${jlibname}::serv serv + + # Init defaults. + array set serv { + disco 0 + muc 0 + } + + # Maintain a priority list of groupchat protocols in decreasing priority. + # Entries must match: ( gc-1.0 | muc ) + set serv(gcProtoPriority) {muc gc-1.0} +} + +# jlib::service::register -- +# +# Let components (browse/disco/muc etc.) register that their services +# are available. + +proc jlib::service::register {jlibname type name} { + upvar ${jlibname}::serv serv + + set serv($type) 1 + set serv($type,name) $name +} + +proc jlib::service::unregister {jlibname type} { + upvar ${jlibname}::serv serv + + set serv($type) 0 + array unset serv $type,* +} + +proc jlib::service::get {jlibname type} { + upvar ${jlibname}::serv serv + + if {$serv($type)} { + return $serv($type,name) + } else { + return + } +} + +#------------------------------------------------------------------------------- +# +# A couple of routines that handle the selection of groupchat protocol for +# each groupchat service. +# A groupchat service may support more than a single protocol. For instance, +# the MUC component supports both gc-1.0 and MUC. + +# Needs some more verification before using it for a dispatcher. + + +# jlib::service::registergcprotocol -- +# +# Register (sets) a groupchat service jid according to the priorities +# presently set. Only called internally! + +proc jlib::service::registergcprotocol {jlibname jid gcprot} { + upvar ${jlibname}::serv serv + + Debug 2 "jlib::registergcprotocol jid=$jid, gcprot=$gcprot" + set jid [jlib::jidmap $jid] + + # If we already told jlib to use a groupchat protocol then... + if {[info exist serv(prefgcprot,$jid)]} { + return + } + + # Set 'serv(gcprot,$jid)' according to the priority list. + foreach prot $serv(gcProtoPriority) { + + # Do we have registered a groupchat protocol with higher priority? + if {[info exists serv(gcprot,$jid)] && \ + [string equal $serv(gcprot,$jid) $prot]} { + return + } + if {[string equal $prot $gcprot]} { + set serv(gcprot,$jid) $prot + return + } + } +} + +# jlib::service::setroomprotocol -- +# +# Set the groupchat protocol in use for room. This acts only as a +# dispatcher for 'service' commands. +# Only called internally when entering a room! + +proc jlib::service::setroomprotocol {jlibname roomjid protocol} { + variable groupchatTypeExp + upvar ${jlibname}::serv serv + + set roomjid [jlib::jidmap $roomjid] + if {![regexp $groupchatTypeExp $protocol]} { + return -code error "Unrecognized groupchat protocol \"$protocol\"" + } + set serv(roomprot,$roomjid) $protocol +} + +# jlib::service::isroom -- +# +# Try to figure out if the jid is a room. +# If we've browsed it it's been registered in our browse object. +# If using agent(s) method, check the agent for this jid + +proc jlib::service::isroom {jlibname jid} { + upvar ${jlibname}::serv serv + upvar ${jlibname}::locals locals + + # Check if domain name supports the 'groupchat' service. + # disco uses explicit children of conference, and muc cache + set isroom 0 + if {!$isroom && $serv(disco) && [$jlibname disco isdiscoed info $locals(server)]} { + set isroom [$jlibname disco isroom $jid] + } + if {!$isroom && $serv(muc)} { + set isroom [$jlibname muc isroom $jid] + } + if {!$isroom} { + set isroom [jlib::groupchat::isroom $jlibname $jid] + } + return $isroom +} + +# jlib::service::nick -- +# +# Return nick name for ANY room participant, or the rooms name +# if jid is a room. +# Not very useful since old 'conference' protocol has gone but keep +# it as an abstraction anyway. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: 'roomname@conference.jabber.org/nick' typically, +# or just room jid. + +proc jlib::service::nick {jlibname jid} { + return [jlib::resourcejid $jid] +} + +# jlib::service::mynick -- +# +# A way to get our OWN nickname for a given room independent of protocol. +# +# Arguments: +# jlibname: the instance of this jlib. +# room: 'roomname@conference.jabber.org' typically. +# +# Results: +# mynickname + +proc jlib::service::mynick {jlibname room} { + upvar ${jlibname}::serv serv + + set room [jlib::jidmap $room] + + # All kind of conference components seem to support the old 'gc-1.0' + # protocol, and we therefore must query our method for entering the room. + if {![info exists serv(roomprot,$room)]} { + return -code error "Does not know which protocol to use in $room" + } + + switch -- $serv(roomprot,$room) { + gc-1.0 { + set nick [$jlibname groupchat mynick $room] + } + muc { + set nick [$jlibname muc mynick $room] + } + } + return $nick +} + +# jlib::service::setnick -- + +proc jlib::service::setnick {jlibname room nick args} { + upvar ${jlibname}::serv serv + + set room [jlib::jidmap $room] + if {![info exists serv(roomprot,$room)]} { + return -code error "Does not know which protocol to use in $room" + } + + switch -- $serv(roomprot,$room) { + gc-1.0 { + eval {$jlibname groupchat setnick $room $nick} $args + } + muc { + eval {$jlibname muc setnick $room $nick} $args + } + } +} + +# jlib::service::allroomsin -- +# +# + +proc jlib::service::allroomsin {jlibname} { + upvar ${jlibname}::lib lib + upvar ${jlibname}::gchat gchat + upvar ${jlibname}::serv serv + + set roomList [concat $gchat(allroomsin) \ + [[namespace parent]::muc::allroomsin $jlibname]] + if {$serv(muc)} { + set roomList [concat $roomList [$jlibname muc allroomsin]] + } + return [lsort -unique $roomList] +} + +proc jlib::service::roomparticipants {jlibname room} { + upvar ${jlibname}::locals locals + upvar ${jlibname}::serv serv + + set room [jlib::jidmap $room] + if {![info exists serv(roomprot,$room)]} { + return -code error "Does not know which protocol to use in $room" + } + + set everyone {} + if {![[namespace current]::isroom $jlibname $room]} { + return -code error "The jid \"$room\" is not a room" + } + + switch -- $serv(roomprot,$room) { + gc-1.0 { + set everyone [[namespace parent]::groupchat::participants $jlibname $room] + } + muc { + set everyone [$jlibname muc participants $room] + } + } + return $everyone +} + +proc jlib::service::exitroom {jlibname room} { + upvar ${jlibname}::locals locals + upvar ${jlibname}::serv serv + + set room [jlib::jidmap $room] + if {![info exists serv(roomprot,$room)]} { + #return -code error "Does not know which protocol to use in $room" + # Not sure here??? + set serv(roomprot,$room) "gc-1.0" + } + + switch -- $serv(roomprot,$room) { + gc-1.0 { + [namespace parent]::groupchat::exit $jlibname $room + } + muc { + $jlibname muc exit $room + } + } +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/si.tcl b/lib/jabberlib/si.tcl new file mode 100644 index 0000000..f538825 --- /dev/null +++ b/lib/jabberlib/si.tcl @@ -0,0 +1,729 @@ +# si.tcl -- +# +# This file is part of the jabberlib. +# It provides support for the stream initiation protocol (XEP-0095). +# +# Copyright (c) 2005-2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: si.tcl,v 1.26 2007/11/30 14:38:34 matben Exp $ +# +# There are several layers involved when sending/receiving a file for +# instance. Each layer reports only to the nearest layer above using +# callbacks. From top to bottom: +# +# 1) application +# 2) profiles, file-transfer etc. +# 3) stream initiation (si) +# 4) the streams, bytestreams (socks5), ibb, etc. +# 5) jabberlib +# +# Each layer divides into two parts, the initiator and target. +# Keep different state arrays for initiator (i) and target (t). +# The si layer acts as a mediator between the profiles and the streams. +# Each profile registers with si, and each stream registers with si. +# +# profiles ... +# \ | / +# \ | / +# \ | / +# si (stream initiation) +# / | \ +# / | \ +# / | \ +# streams ... +# +# INITIATOR: each transport (stream) registers for open, send & close +# using 'registertransport'. The profiles call these indirectly +# through si. The profile gets feedback from streams using direct +# callbacks. +# +# TARGET: each profile (file-transfer) registers for open, read & close +# using 'registerprofile'. The transports register for element +# handlers for their specific protocol. When activated, the transport +# calls si which in turn calls the profile using its registered +# handlers. +# +# Initiator: Target: +# +# profiles | : : /|\ : : +# | : : | : : +# \|/ : : | : : +# si ============= <--------> ============= +# : | : : /|\ : +# : | : : | : +# streams : \|/ : : | : +# o .......................> o +# +# +############################# USAGE ############################################ +# +# NAME +# si - convenience command library for stream initiation. +# +# SYNOPSIS +# +# +# OPTIONS +# +# +# INSTANCE COMMANDS +# jlibName si registertransport ... +# jlibName si registerprofile ... +# jlibName si send_set ... +# jlibName si send_data ... +# jlibName si send_close ... +# jlibName si getstate sid +# +################################################################################ + +package require jlib +package require jlib::disco + +package provide jlib::si 0.1 + +#--- generic si ---------------------------------------------------------------- + +namespace eval jlib::si { + + variable xmlns + set xmlns(si) "http://jabber.org/protocol/si" + set xmlns(neg) "http://jabber.org/protocol/feature-neg" + set xmlns(xdata) "jabber:x:data" + set xmlns(streams) "urn:ietf:params:xml:ns:xmpp-streams" + + # Storage for registered transports. + variable trpt + set trpt(list) [list] + + jlib::disco::registerfeature $xmlns(si) + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::si::registertransport -- +# +# Register transports on the initiator (sender) side. +# This is used by the streams that do the actual job. +# Typically 'name' and 'ns' are xml namespaces and identical. + +proc jlib::si::registertransport {name ns priority openProc closeProc} { + variable trpt + #puts "jlib::si::registertransport (i)" + + lappend trpt(list) [list $name $priority] + set trpt(list) [lsort -unique -index 1 $trpt(list)] + set trpt($name,ns) $ns + set trpt($name,open) $openProc + set trpt($name,close) $closeProc + + # Keep these in sync. + set trpt(names) [list] + set trpt(streams) [list] + foreach spec $trpt(list) { + set nm [lindex $spec 0] + lappend trpt(names) $nm + lappend trpt(streams) $trpt($nm,ns) + } +} + +# jlib::si::registerreader -- +# +# This lives on the initiator side. +# Each profile must register a reader which is then used by the streams +# (transport) when writing data to the network. +# The streams shall limit its control to the data handling alone, +# and the major control is still with the profile. +# In particular, any close operation is initiated by the profile. +# This is merely a layer to dispatch reading actions from the stream +# to the profile. +# NB: We could do with only a 'read' proc but this is a cleaner interface. + +proc jlib::si::registerreader {profile openProc readProc closeProc} { + variable reader + #puts "jlib::si::registerreader" + + set reader($profile,open) $openProc + set reader($profile,read) $readProc + set reader($profile,close) $closeProc +} + +# jlib::si::registerprofile -- +# +# This is used by profiles to register handler when receiving a si set +# with the specified profile. It contains handlers for 'set', 'read', +# and 'close' streams. These belong to the target side. + +proc jlib::si::registerprofile {profile openProc readProc closeProc} { + variable prof + #puts "jlib::si::registerprofile (t)" + + set prof($profile,open) $openProc + set prof($profile,read) $readProc + set prof($profile,close) $closeProc +} + +# jlib::si::init -- +# +# Instance init procedure. + +proc jlib::si::init {jlibname args} { + variable xmlns + #puts "jlib::si::init" + + # Keep different state arrays for initiator (i) and receiver (r). + namespace eval ${jlibname}::si { + variable istate + variable tstate + } + $jlibname iq_register set $xmlns(si) [namespace current]::handle_set + $jlibname iq_register get $xmlns(si) [namespace current]::handle_get +} + +proc jlib::si::cmdproc {jlibname cmd args} { + + #puts "jlib::si::cmdproc jlibname=$jlibname, cmd='$cmd', args='$args'" + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# These are all functions to use by a initiator (sender). + +# jlib::si::send_set -- +# +# Makes a stream initiation (open). +# It will eventually, if negotiation went ok, invoke the stream +# 'open' method. +# The 'args' ar transparently delivered to the streams 'open' method. + +proc jlib::si::send_set {jlibname jid sid mime profile profileE cmd args} { + + #puts "jlib::si::send_set (i)" + + set siE [i_constructor $jlibname $sid $jid $mime $profile $profileE $cmd] + jlib::send_iq $jlibname set [list $siE] -to $jid \ + -command [list [namespace current]::send_set_cb $jlibname $sid] + return +} + +# jlib::si::i_constructor -- +# +# Makes a new si instance. Does everything except delivering it. +# Returns the si element. + +proc jlib::si::i_constructor {jlibname sid jid mime profile profileE cmd args} { + upvar ${jlibname}::si::istate istate + + set istate($sid,jid) $jid + set istate($sid,mime) $mime + set istate($sid,profile) $profile + set istate($sid,openCmd) $cmd + set istate($sid,args) $args + foreach {key val} $args { + set istate($sid,$key) $val + } + + return [element $sid $mime $profile $profileE] +} + +# jlib::si::element -- +# +# Just create the si element. Nothing cached. Stateless. + +proc jlib::si::element {sid mime profile profileE} { + variable xmlns + variable trpt + + set optionEL [list] + foreach name $trpt(names) { + set valueE [wrapper::createtag "value" -chdata $trpt($name,ns)] + lappend optionEL [wrapper::createtag "option" -subtags [list $valueE]] + } + set fieldE [wrapper::createtag "field" \ + -attrlist {var stream-method type list-single} -subtags $optionEL] + set xE [wrapper::createtag "x" \ + -attrlist {xmlns jabber:x:data type form} -subtags [list $fieldE]] + set featureE [wrapper::createtag "feature" \ + -attrlist [list xmlns $xmlns(neg)] -subtags [list $xE]] + set siE [wrapper::createtag "si" \ + -attrlist [list xmlns $xmlns(si) id $sid mime-type $mime profile $profile] \ + -subtags [list $profileE $featureE]] + + return $siE +} + +# jlib::si::send_set_cb -- +# +# Our internal callback handler when offered stream initiation. + +proc jlib::si::send_set_cb {jlibname sid type iqChild args} { + variable xmlns + variable trpt + upvar ${jlibname}::si::istate istate + + #puts "jlib::si::send_set_cb (i)" + + if {[string equal $type "error"]} { + eval $istate($sid,openCmd) [list $jlibname $type $sid $iqChild] + ifree $jlibname $sid + return + } + eval {i_handler $jlibname $sid $iqChild} $args +} + +# jlib::si::handle_get -- +# +# This handles incoming iq-get/si elements. The 'sid' must already exist +# since this belongs to the initiator side! We obtain this call as a +# response to an si element sent. It should behave as 'send_set_cb'. + +proc jlib::si::handle_get {jlibname from iqChild args} { + upvar ${jlibname}::si::istate istate + #puts "jlib::si::handle_get (i)" + + array set argsA $args + array set attr [wrapper::getattrlist $iqChild] + if {![info exists attr(id)]} { + return 0 + } + set sid $attr(id) + if {![info exists argsA(-id)]} { + return 0 + } + set id $argsA(-id) + + # Verify that we have actually initiated this stream. + if {![info exists istate($sid,jid)]} { + jlib::send_iq_error $jlibname $from $id 403 cancel forbidden + return 1 + } + eval {i_handler $jlibname $sid $iqChild} $args + + # We must respond ourselves. + $jlibname send_iq result {} -to $from -id $id + + return 1 +} + +# jlib::si::i_handler -- +# +# Handles both responses to an iq-set call and an incoming iq-get. + +proc jlib::si::i_handler {jlibname sid iqChild args} { + variable xmlns + variable trpt + upvar ${jlibname}::si::istate istate + #puts "jlib::si::i_handler (i)" + + # Verify that it is consistent. + if {![string equal [wrapper::gettag $iqChild] "si"]} { + + # @@@ errors ? + eval $istate($sid,openCmd) [list $jlibname error $sid {}] + ifree $jlibname $sid + return + } + + set value "" + set valueE [wrapper::getchilddeep $iqChild [list \ + [list "feature" $xmlns(neg)] [list "x" $xmlns(xdata)] "field" "value"]] + if {[llength $valueE]} { + set value [wrapper::getcdata $valueE] + } + + # Find if matching transport. + if {[lsearch -exact $trpt(streams) $value] >= 0} { + + # Open transport. + # We provide a callback for the transport when open is finished. + set istate($sid,stream) $value + set jid $istate($sid,jid) + set cmd [namespace current]::transport_open_cb + eval $trpt($value,open) [list $jlibname $jid $sid] \ + $istate($sid,args) + } else { + eval $istate($sid,openCmd) [list $jlibname error $sid {}] + ifree $jlibname $sid + } +} + +# jlib::si::transport_open_cb -- +# +# This is a transports way of reporting result from it's 'open' method. + +proc jlib::si::transport_open_cb {jlibname sid type iqChild} { + upvar ${jlibname}::si::istate istate + #puts "jlib::si::transport_open_cb (i)" + + # Just report this to the relevant profile. + eval $istate($sid,openCmd) [list $jlibname $type $sid $iqChild] +} + +# jlib::si::getstate -- +# +# Just an access function to the internal state variables. + +proc jlib::si::getstate {jlibname sid} { + upvar ${jlibname}::si::istate istate + + set arr [list] + foreach {key value} [array get istate $sid,*] { + set name [string map [list "$sid," ""] $key] + lappend arr $name $value + } + return $arr +} + +# jlib::si::open_data, read_data, close_data -- +# +# These are all used by the streams (transports) to handle the data +# stream it needs when transmitting. +# This is merely a layer to dispatch reading actions from the stream +# to the profile. + +proc jlib::si::open_data {jlibname sid} { + variable reader + upvar ${jlibname}::si::istate istate + #puts "jlib::si::open_data (i)" + + set profile $istate($sid,profile) + $reader($profile,open) $jlibname $sid +} + +proc jlib::si::read_data {jlibname sid} { + variable reader + upvar ${jlibname}::si::istate istate + #puts "jlib::si::read_data (i)" + + set profile $istate($sid,profile) + return [$reader($profile,read) $jlibname $sid] +} + +# This is also used to report any errors from transport to profile. + +proc jlib::si::close_data {jlibname sid {err ""}} { + variable reader + upvar ${jlibname}::si::istate istate + #puts "jlib::si::close_data (i)" + + set profile $istate($sid,profile) + $reader($profile,close) $jlibname $sid $err +} + +# jlib::si::send_close -- +# +# Used by profile to close down the stream. + +proc jlib::si::send_close {jlibname sid cmd} { + variable trpt + upvar ${jlibname}::si::istate istate + #puts "jlib::si::send_close (i)" + + set istate($sid,closeCmd) $cmd + set stream $istate($sid,stream) + eval $trpt($stream,close) [list $jlibname $sid] +} + +# jlib::si::transport_close_cb -- +# +# Called by tansport when closed operation is completed. +# It is called as a response (callback) to 'send_close'. +# This is our destructor. + +proc jlib::si::transport_close_cb {jlibname sid type iqChild} { + upvar ${jlibname}::si::istate istate + #puts "jlib::si::transport_close_cb (i)" + + # Just report this to the relevant profile. + eval $istate($sid,closeCmd) [list $jlibname $type $sid $iqChild] + + ifree $jlibname $sid +} + +proc jlib::si::ifree {jlibname sid} { + upvar ${jlibname}::si::istate istate + #puts "jlib::si::ifree (i) sid=$sid" + + array unset istate $sid,* +} + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +# +# These are all functions to use by a target (receiver) of a stream. + +# jlib::si::handle_set -- +# +# Parse incoming si set element. Invokes registered callback for the +# profile in question. It is the responsibility of this callback to +# deliver the result via the command in its argument. + +proc jlib::si::handle_set {jlibname from siE args} { + variable xmlns + variable trpt + variable prof + upvar ${jlibname}::si::tstate tstate + + #puts "jlib::si::handle_set (t)" + + array set iqattr $args + if {![info exists iqattr(-id)]} { + return 0 + } + set id $iqattr(-id) + + # Note: there are two different 'id'! + # These are the attributes of the si element. + array set attr { + id "" + mime-type "" + profile "" + } + array set attr [wrapper::getattrlist $siE] + set sid $attr(id) + set profile $attr(profile) + + # This is a profile we don't understand. + if {![info exists prof($profile,open)]} { + set errE [wrapper::createtag "bad-profile" \ + -attrlist [list xmlns $xmlns(si)]] + send_error $jlibname $from $id $sid 400 cancel "bad-request" $errE + return 1 + } + + # Extract all streams and pick one with highest priority. + set stream [pick_stream $siE] + + # No valid stream :-( + if {![string length $stream]} { + set errE [wrapper::createtag "no-valid-streams" \ + -attrlist [list xmlns $xmlns(si)]] + send_error $jlibname $from $id $sid 400 cancel "bad-request" $errE + return 1 + } + + # Get profile element. Can have any tag but xmlns must be $profile. + set profileE [wrapper::getfirstchildwithxmlns $siE $profile] + if {![llength $profileE]} { + send_error $jlibname $from $id $sid 400 cancel "bad-request" + return 1 + } + + set tstate($sid,profile) $profile + set tstate($sid,stream) $stream + set tstate($sid,mime-type) $attr(mime-type) + foreach {key val} $args { + set tstate($sid,$key) $val + } + set jid $tstate($sid,-from) + + # Invoke registered handler for this profile. + set respCmd [list [namespace current]::profile_response $jlibname $sid] + set rc [catch { + eval $prof($profile,open) [list $jlibname $sid $jid $siE $respCmd] + }] + if {$rc == 1} { + # error + return 0 + } elseif {$rc == 3 || $rc == 4} { + # break or continue + return 0 + } + return 1 +} + +# jlib::si::pick_stream -- +# +# Extracts the highest priority stream from an si element. Empty if error. + +proc jlib::si::pick_stream {siE} { + variable xmlns + variable trpt + + # Extract all streams and pick one with highest priority. + set values [list] + set fieldE [wrapper::getchilddeep $siE [list \ + [list "feature" $xmlns(neg)] [list "x" $xmlns(xdata)] "field"]] + if {[llength $fieldE]} { + set optionEL [wrapper::getchildswithtag $fieldE "option"] + foreach c $optionEL { + set firstE [lindex [wrapper::getchildren $c] 0] + lappend values [wrapper::getcdata $firstE] + } + } + + # Pick first matching since priority ordered. + set stream "" + foreach name $values { + if {[lsearch -exact $trpt(streams) $name] >= 0} { + set stream $name + break + } + } + return $stream +} + +# jlib::si::profile_response -- +# +# Invoked by the registered profile callback. +# +# Arguments: +# type 'result' or 'error' if user accepts the stream or not. +# profileE any extra profile element; can be empty. + +proc jlib::si::profile_response {jlibname sid type profileE args} { + variable xmlns + upvar ${jlibname}::si::tstate tstate + + #puts "jlib::si::profile_response (t) type=$type" + + set jid $tstate($sid,-from) + set id $tstate($sid,-id) + + # Rejected stream initiation. + if {[string equal $type "error"]} { + # @@@ We could have a text element here... + send_error $jlibname $jid $id $sid 403 cancel forbidden + } else { + + # Accepted stream initiation. + # Construct si element from selected profile. + set siE [t_element $jlibname $sid $profileE] + jlib::send_iq $jlibname result [list $siE] -to $jid -id $id + } + return +} + +# jlib::si::t_element -- +# +# Construct si element from selected profile. + +proc jlib::si::t_element {jlibname sid profileE} { + variable xmlns + upvar ${jlibname}::si::tstate tstate + + set valueE [wrapper::createtag "value" -chdata $tstate($sid,stream)] + set fieldE [wrapper::createtag "field" \ + -attrlist {var stream-method} -subtags [list $valueE]] + set xE [wrapper::createtag "x" \ + -attrlist [list xmlns $xmlns(xdata) type submit] -subtags [list $fieldE]] + set featureE [wrapper::createtag "feature" \ + -attrlist [list xmlns $xmlns(neg)] -subtags [list $xE]] + + # Include 'profileE' if nonempty. + set subsiEL [list $featureE] + if {[llength $profileE]} { + lappend subsiEL $profileE + } + set siE [wrapper::createtag "si" \ + -attrlist [list xmlns $xmlns(si) id $sid] -subtags $subsiEL] + return $siE +} + +# jlib::si::reset -- +# +# Used by profile when doing reset. + +proc jlib::si::reset {jlibname sid} { + upvar ${jlibname}::si::tstate tstate + #puts "jlib::si::reset (t)" + + # @@@ Tell transport we are resetting??? + # Brute force. + + tfree $jlibname $sid +} + +# jlib::si::havesi -- +# +# The streams may need to know if we have got a si request (set). +# @@@ Perhaps we should have timeout for incoming si requests that +# cancels it all. + +proc jlib::si::havesi {jlibname sid} { + upvar ${jlibname}::si::tstate tstate + upvar ${jlibname}::si::istate istate + + if {[info exists tstate($sid,profile)] || [info exists istate($sid,profile)]} { + return 1 + } else { + return 0 + } +} + +# jlib::si::stream_recv -- +# +# Used by transports (streams) to deliver the actual data. + +proc jlib::si::stream_recv {jlibname sid data} { + variable prof + upvar ${jlibname}::si::tstate tstate + #puts "jlib::si::stream_recv (t)" + + # Each stream should check that we exist before calling us! + set profile $tstate($sid,profile) + eval $prof($profile,read) [list $jlibname $sid $data] +} + +# jlib::si::stream_closed -- +# +# This should be the final stage for a succesful transfer. +# Called by transports (streams). + +proc jlib::si::stream_closed {jlibname sid} { + variable prof + upvar ${jlibname}::si::tstate tstate + #puts "jlib::si::stream_closed (t)" + + # Each stream should check that we exist before calling us! + set profile $tstate($sid,profile) + eval $prof($profile,close) [list $jlibname $sid] + tfree $jlibname $sid +} + +# jlib::si::stream_error -- +# +# Called by transports to report an error. + +proc jlib::si::stream_error {jlibname sid errmsg} { + variable prof + upvar ${jlibname}::si::tstate tstate + #puts "jlib::si::stream_error (t)" + + set profile $tstate($sid,profile) + eval $prof($profile,close) [list $jlibname $sid $errmsg] + tfree $jlibname $sid +} + +# jlib::si::send_error -- +# +# Reply with iq error element. + +proc jlib::si::send_error {jlibname jid id sid errcode errtype stanza {extraElem {}}} { + + #puts "jlib::si::send_error" + + jlib::send_iq_error $jlibname $jid $id $errcode $errtype $stanza $extraElem + tfree $jlibname $sid +} + +proc jlib::si::tfree {jlibname sid} { + upvar ${jlibname}::si::tstate tstate + #puts "jlib::si::tfree (t)" + + array unset tstate $sid,* +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::si { + + jlib::ensamble_register si \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/sipub.tcl b/lib/jabberlib/sipub.tcl new file mode 100644 index 0000000..384442c --- /dev/null +++ b/lib/jabberlib/sipub.tcl @@ -0,0 +1,307 @@ +# sipub.tcl -- +# +# This file is part of the jabberlib. +# It provides support for the sipub prootocol: +# XEP-0137: Publishing Stream Initiation Requests +# +# Copyright (c) 2007 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: sipub.tcl,v 1.7 2007/11/25 15:48:54 matben Exp $ +# +# NB: There are three different id's floating around: +# 1) iq-get/result related +# 2) sipub id (spid) +# 3) si id (stream id, sid) +# +# @@@ TODO: Move some code to the profile instead since we have hardcoded +# the 'filetransfer' profile. + +package require jlib +package require jlib::si +package require jlib::disco + +package provide jlib::sipub 0.2 + +namespace eval jlib::sipub { + + variable xmlns + set xmlns(sipub) "http://jabber.org/protocol/si-pub" + + jlib::disco::registerfeature $xmlns(sipub) + + # We use a static cache array that maps sipub id (spid) to file name and mime. + # This seems more practical since the jlib instances may vary between the + # sessions. + variable cache + + # Note: jlib::ensamble_register is last in this file! +} + +proc jlib::sipub::init {jlibname args} { + variable xmlns + + $jlibname iq_register get $xmlns(sipub) [namespace current]::handle_get +} + +proc jlib::sipub::cmdproc {jlibname cmd args} { + return [eval {$cmd $jlibname} $args] +} + +#--- Initiator side ------------------------------------------------------------ +# +# Initiator and target are dubious names here. With initiator we mean the part +# that has a file to offer, and target the one who gets it. + +# jlib::sipub::set_cache, get_cache -- +# +# Set or get the complete cache. Useful if we store the cache in a file +# between sessions. + +proc jlib::sipub::set_cache {cacheL} { + variable cache + array set cache $cacheL +} + +proc jlib::sipub::get_cache {} { + variable cache + return [array get cache] +} + +# jlib::sipub::newcache -- +# +# This just adds a reference to our cache. Used to construct xmpp uri +# for 'recvfile'. + +proc jlib::sipub::newcache {fileName mime} { + variable cache + + set spid [jlib::generateuuid] + set cache($spid,file) $fileName + set cache($spid,mime) $mime + + return $spid +} + +# jlib::sipub::element -- +# +# Makes a sipub element for a local file and adds the reference to cache. +# This is the constructor for a sipub object. Each object may generate +# any number of file transfers instances, each with its unique 'sid'. +# Once a sipub instance is created it can be made to live as long as +# the cache is kept. +# This shall be called from the profile or application layer. +# +# Results: +# sipub element. + +# @@@ Shall it have jlibname? + +proc jlib::sipub::element {from profile profileE fileName mime} { + variable xmlns + variable cache + + set spid [jlib::generateuuid] + set cache($spid,file) $fileName + set cache($spid,mime) $mime + + set attr [list xmlns $xmlns(sipub) from $from id $spid mime-type $mime \ + profile $profile] + set sipubE [wrapper::createtag "sipub" -attrlist $attr \ + -subtags [list $profileE]] + + return $sipubE +} + +# jlib::sipub::handle_get -- +# +# Handles incoming iq-get/start sipub stanzas. +# There must be a sipub object with matching id (spid). +# This has the corresponding role of the HTTP server side GET request. +# +# NB: We have hardcoded the 'filetransfer' profile. + +proc jlib::sipub::handle_get {jlibname from startE args} { + variable xmlns + variable cache + + array set argsA $args + if {![info exists argsA(-id)]} { + return 0 + } + set id $argsA(-id) + if {[wrapper::gettag $startE] ne "start"} { + return 0 + } + array set attr [wrapper::getattrlist $startE] + if {![info exists attr(id)]} { + return 0 + } + set spid $attr(id) + if {[info exists cache($spid,file)]} { + + # We must pick the 'sid' here since it is also used in 'starting'. + set sid [jlib::generateuuid] + set startingE [wrapper::createtag "starting" \ + -attrlist [list xmlns $xmlns(sipub) sid $sid]] + $jlibname send_iq result [list $startingE] -id $id -to $from + + # This is the constructor of a file stream. + $jlibname filetransfer send $from [namespace code send_cb] -sid $sid \ + -file $cache($spid,file) \ + -mime $cache($spid,mime) + } else { + jlib::send_iq_error $jlibname $from $id 405 modify not-acceptable + } + return 1 +} + +proc jlib::sipub::send_cb {jlibname status sid {subiq ""}} { + + # empty. +} + +#--- Target side --------------------------------------------------------------- + +# jlib::sipub::have_sipub -- +# +# Searches an element recursively to see if there is a sipub element. + +proc jlib::sipub::have_sipub {xmldata} { + variable xmlns + + return [llength [wrapper::getchilddeep $xmldata \ + [list [list sipub $xmlns(sipub)]]]] +} + +proc jlib::sipub::get_element {xmldata} { + variable xmlns + + return [wrapper::getchilddeep $xmldata [list [list sipub $xmlns(sipub)]]] +} + +# NB: We have a separate 'start' command in order to catch the response and +# obtain the 'sid' which is typically needed to control the file transfer. + +# Typical usage: +# +# jlib sipub start ... cb +# proc cb {type startingE} { +# set sid [wrapper::getattribute $startingE sid] +# jlib sipub set_accept_handler $sid \ +# -channel ... -command ... -progress ... +# } + +# jlib::sipub::start -- +# +# Sends a start element. A iq-result/error is expected. +# This 'id' must be a matching spid. + +proc jlib::sipub::start {jlibname jid id cmd} { + variable xmlns + + set startE [wrapper::createtag "start" \ + -attrlist [list xmlns $xmlns(sipub) id $id]] + $jlibname send_iq get [list $startE] -to $jid -command $cmd +} + +# jlib::sipub::set_accept_handler -- +# +# This is normally called as a response to the 'start' command's +# callback when we get a sipub 'starting' element. +# We shall typically provide -command, -progress, and -channel. +# +# Arguments: +# xmldata complete message element or whatever. +# args: -channel +# -command +# -progress +# +# Result: +# none + +proc jlib::sipub::set_accept_handler {jlibname sid args} { + variable state + + # This is just kept until we get the si-callback. + set state($sid,args) $args + + # We shall be prepared to get the si-set request. + $jlibname filetransfer register_sid_handler $sid \ + [namespace code [list si_handler $sid]] + return +} + +proc jlib::sipub::si_handler {sid jlibname jid name size cmd args} { + variable state + + #puts "jlib::sipub::si_handler sid=$sid" + + # We requested this file using 'sipub::get' in the first place so + # therefore accept the stream. + # We also provide all the arguments -channel etc. + uplevel #0 $cmd 1 $state($sid,args) + unset state($sid,args) +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::sipub { + + jlib::ensamble_register sipub \ + [namespace current]::init \ + [namespace current]::cmdproc +} + +# Test: +if {0} { + package require jlib::sipub + set jlib ::jlib::jlib1 + + # Initiator side: + set jid matben@localhost + set fileName /Users/matben/Desktop/splash.svg + set name [file tail $fileName] + set size [file size $fileName] + set fileE [jlib::ftrans::element $name $size] + set sipubE [jlib::sipub::element [$jlib myjid] $jlib::ftrans::xmlns(ftrans) \ + $fileE $fileName image/svg] + + $jlib send_message $jid -xlist [list $sipubE] + + # Target side: + package require jlib::sipub + set jlib ::jlib::jlib1 + proc progress {args} {puts "progress: $args"} + proc command {args} {puts "command: $args"} + proc msg {jlib xmlns xmldata args} { + puts "message: $xmldata" + set ::messageE $xmldata + return 0 + } + $jlib message_register normal * msg + + set fileName /Users/matben/Desktop/splash.svg + set fd [open $fileName.tmp w] + + proc start_cb {type startingE} { + puts "start_cb type=$type" + if {$type eq "result"} { + set sid [wrapper::getattribute $startingE sid] + $::jlib sipub set_accept_handler $sid \ + -channel $::fd -command command -progress progress + } + } + set sipubE [wrapper::getchilddeep $messageE \ + [list [list sipub $jlib::sipub::xmlns(sipub)]]] + set from [wrapper::getattribute $sipubE from] + if {$from eq ""} { + set from [wrapper::getattribute $messageE from] + } + set spid [wrapper::getattribute $sipubE id] + $jlib sipub start $from $spid start_cb + +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/stanzaerror.tcl b/lib/jabberlib/stanzaerror.tcl new file mode 100644 index 0000000..36197e7 --- /dev/null +++ b/lib/jabberlib/stanzaerror.tcl @@ -0,0 +1,64 @@ +# stanzaerror.tcl -- +# +# This file is part of the jabberlib. It provides english clear text +# messages that gives some detail of 'urn:ietf:params:xml:ns:xmpp-stanzas'. +# +# Copyright (c) 2004 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: stanzaerror.tcl,v 1.9 2007/09/12 07:20:46 matben Exp $ +# + +package provide stanzaerror 1.0 + +namespace eval stanzaerror { + + # This maps Defined Conditions to clear text messages. + # Extensible Messaging and Presence Protocol (XMPP): Core (RFC 3920) + # 9.3.3 Defined Conditions + # Applications use the error tag directly for the key into a message catalog. + + variable msg + array set msg { + bad-request {The sender has sent XML that is malformed or that cannot be processed.} + conflict {Access cannot be granted because an existing resource or session exists with the same name or address.} + feature-not-implemented {The feature requested is not implemented by the recipient or server and therefore cannot be processed.} + forbidden {The requesting entity does not possess the required permissions to perform the action.} + gone {The recipient or server can no longer be contacted at this address.} + internal-server-error {The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error.} + item-not-found {The addressed JID or item requested cannot be found.} + jid-malformed {The sending entity has provided or communicated an XMPP address or aspect thereof that does not adhere to the syntax defined in Addressing Scheme.} + not-acceptable {The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server.} + not-allowed {The recipient or server does not allow any entity to perform the action.} + not-authorized {The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials.} + payment-required {The requesting entity is not authorized to access the requested service because payment is required.} + recipient-unavailable {The intended recipient is temporarily unavailable.} + redirect {The recipient or server is redirecting requests for this information to another entity, usually temporarily.} + registration-required {The requesting entity is not authorized to access the requested service because registration is required.} + remote-server-not-found {A remote server or service specified as part or all of the JID of the intended recipient does not exist.} + remote-server-timeout {A remote server or service specified as part or all of the JID of the intended recipient (or required to fulfill a request) could not be contacted within a reasonable amount of time.} + resource-constraint {The server or recipient lacks the system resources necessary to service the request.} + service-unavailable {The server or recipient does not currently provide the requested service.} + subscription-required {The requesting entity is not authorized to access the requested service because a subscription is required.} + undefined-condition {The error condition is not one of those defined by the other conditions in this list.} + unexpected-request {The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).} + } +} + +# stanzaerror::getmsg -- +# +# Return the english clear text message from a defined-condition. + +proc stanzaerror::getmsg {condition} { + variable msg + + if {[info exists msg($condition)]} { + return $msg($condition) + } else { + return + } +} + +#------------------------------------------------------------------------------- + diff --git a/lib/jabberlib/streamerror.tcl b/lib/jabberlib/streamerror.tcl new file mode 100644 index 0000000..9157c7b --- /dev/null +++ b/lib/jabberlib/streamerror.tcl @@ -0,0 +1,75 @@ +# streamerror.tcl -- +# +# This file is part of the jabberlib. It provides english clear text +# messages that gives some detail of 'urn:ietf:params:xml:ns:xmpp-streams'. +# +# Copyright (c) 2004 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: streamerror.tcl,v 1.7 2007/09/12 13:37:55 matben Exp $ +# + +# The syntax for stream errors is as follows: +# +# +# +# +# OPTIONAL descriptive text +# +# [OPTIONAL application-specific condition element] +# + +package provide streamerror 1.0 + +namespace eval streamerror { + + # This maps Defined Conditions to clear text messages. + # draft-ietf-xmpp-core23; 4.7.3 Defined Conditions + # Applications use the error tag directly for the key into a message catalog. + + variable msg + array set msg { + bad-format {The entity has sent XML that cannot be processed.} + bad-namespace-prefix {The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix.} + conflict {The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream.} + connection-timeout {The entity has not generated any traffic over the stream for some period of time.} + host-gone {The value of the 'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server.} + host-unknown {The value of the 'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server.} + improper-addressing {A stanza sent between two servers lacks a 'to' or 'from' attribute.} + internal-server-error {The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream.} + invalid-from {The JID or hostname provided in a 'from' address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource binding.} + invalid-id {The stream ID or dialback ID is invalid or does not match an ID previously provided.} + invalid-namespace {The streams namespace name is something other than "http://etherx.jabber.org/streams" or the dialback namespace name is something other than "jabber:server:dialback".} + invalid-xml {The entity has sent invalid XML over the stream to a server that performs validation.} + not-authorized {The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation; the receiving entity MUST NOT process the offending stanza before sending the stream error.} + policy-violation {The entity has violated some local service policy; the server MAY choose to specify the policy in the element or an application-specific condition element.} + remote-connection-failed {The server is unable to properly connect to a remote entity that is required for authentication or authorization.} + resource-constraint {The server lacks the system resources necessary to service the stream.} + restricted-xml {The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character.} + see-other-host {The server will not provide service to the initiating entity but is redirecting traffic to another host; the server SHOULD specify the alternate hostname or IP address (which MUST be a valid domain identifier) as the XML character data of the element.} + system-shutdown {The server is being shut down and all active streams are being closed.} + undefined-condition {The error condition is not one of those defined by the other conditions in this list; this error condition SHOULD be used only in conjunction with an application-specific condition.} + unsupported-encoding {The initiating entity has encoded the stream in an encoding that is not supported by the server.} + unsupported-stanza-type {The initiating entity has sent a first-level child of the stream that is not supported by the server.} + unsupported-version {The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server.} + xml-not-well-formed {The initiating entity has sent XML that is not well-formed as defined by [XML].} + } +} + +# streamerror::getmsg -- +# +# Return the english clear text message from a defined-condition. + +proc streamerror::getmsg {condition} { + variable msg + + if {[info exists msg($condition)]} { + return $msg($condition) + } else { + return + } +} + +#------------------------------------------------------------------------------- + diff --git a/lib/jabberlib/tinydom.tcl b/lib/jabberlib/tinydom.tcl new file mode 100644 index 0000000..e7a4aa9 --- /dev/null +++ b/lib/jabberlib/tinydom.tcl @@ -0,0 +1,159 @@ +# tinydom.tcl --- +# +# This file is part of The Coccinella application. It implements +# a tiny DOM model which wraps xml into tcl lists. +# +# Copyright (c) 2003 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: tinydom.tcl,v 1.14 2008/03/26 13:11:34 matben Exp $ + +package require xml + +package provide tinydom 0.2 + +# This is an attempt to make a minimal DOM thing to store xml data as +# a hierarchical list which is better suited to Tcl. +# @@@ Try make a common syntax with wrapper. + +namespace eval tinydom { + variable uid 0 + variable cache +} + +proc tinydom::parse {xml args} { + variable uid + variable cache + + array set argsA { + -package xml + } + array set argsA $args + switch -- $argsA(-package) { + xml { + set xmlparser [xml::parser] + } + qdxml { + package require qdxml + set xmlparser [qdxml::create] + } + default { + return -code error "unknown -package \"$argsA(-package)\"" + } + } + + # Store in internal array and return token which is the array index. + set token [namespace current]::[incr uid] + upvar #0 $token state + + set state(1) [list] + set state(level) 0 + + $xmlparser configure -reportempty 1 \ + -elementstartcommand [namespace code [list ElementStart $token]] \ + -elementendcommand [namespace code [list ElementEnd $token]] \ + -characterdatacommand [namespace code [list CHdata $token]] \ + -ignorewhitespace 1 + $xmlparser parse $xml + + set cache($token) $state(1) + unset state + return $token +} + +proc tinydom::ElementStart {token tag attrlist args} { + upvar #0 $token state + + array set argsA $args + if {[info exists argsA(-namespacedecls)]} { + lappend attrlist xmlns [lindex $argsA(-namespacedecls) 0] + } + set state([incr state(level)]) [list $tag $attrlist 0 {} {}] +} + +proc tinydom::ElementEnd {token tagname args} { + upvar #0 $token state + + set level $state(level) + if {$level > 1} { + + # Insert the child tree in the parent tree. + Append $token [expr $level-1] $state($level) + } + incr state(level) -1 +} + +proc tinydom::CHdata {token chdata} { + upvar #0 $token state + + set level $state(level) + set cdata [lindex $state($level) 3] + append cdata [xmldecrypt $chdata] + lset state($level) 3 $cdata +} + +proc tinydom::Append {token plevel childtree} { + upvar #0 $token state + + # Get child list at parent level (level). + set childlist [lindex $state($plevel) 4] + lappend childlist $childtree + + # Build the new parent tree. + lset state($plevel) 4 $childlist +} + +proc tinydom::xmldecrypt {chdata} { + + return [string map { + {&} {&} {<} {<} {>} {>} {"} {"} {'} {'}} $chdata] +} + +proc tinydom::documentElement {token} { + variable cache + return $cache($token) +} + +proc tinydom::tagname {xmllist} { + return [lindex $xmllist 0] +} + +proc tinydom::attrlist {xmllist} { + return [lindex $xmllist 1] +} + +proc tinydom::chdata {xmllist} { + return [lindex $xmllist 3] +} + +proc tinydom::children {xmllist} { + return [lindex $xmllist 4] +} + +proc tinydom::getattribute {xmllist attrname} { + foreach {attr val} [lindex $xmllist 1] { + if {[string equal $attr $attrname]} { + return $val + } + } + return +} + +proc tinydom::getfirstchildwithtag {xmllist tag} { + set c [list] + foreach celem [lindex $xmllist 4] { + if {[string equal [lindex $celem 0] $tag]} { + set c $celem + break + } + } + return $c +} + +proc tinydom::cleanup {token} { + variable cache + unset -nocomplain cache($token) +} + +#------------------------------------------------------------------------------- diff --git a/lib/jabberlib/util.tcl b/lib/jabberlib/util.tcl new file mode 100644 index 0000000..7fbe342 --- /dev/null +++ b/lib/jabberlib/util.tcl @@ -0,0 +1,66 @@ +# util.tcl -- +# +# This file is part of the jabberlib. +# It provides small utility functions. +# +# Copyright (c) 2006 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: util.tcl,v 1.6 2007/09/06 13:20:47 matben Exp $ + +package provide jlib::util 0.1 + +namespace eval jlib::util {} + +# Standin for a 8.5 feature. +if {![llength [info commands lassign]]} { + proc lassign {vals args} {uplevel 1 [list foreach $args $vals break] } +} + +# jlib::util::lintersect -- +# +# Picks out the common list elements from two lists, their intersection. + +proc jlib::util::lintersect {list1 list2} { + set lans [list] + foreach l $list1 { + if {[lsearch -exact $list2 $l] >= 0} { + lappend lans $l + } + } + return $lans +} + +# jlib::util::lprune -- +# +# Removes element from list, silently. + +proc jlib::util::lprune {listName elem} { + upvar $listName listValue + set idx [lsearch -exact $listValue $elem] + if {$idx >= 0} { + uplevel [list set $listName [lreplace $listValue $idx $idx]] + } + return +} + +# jlib::util::from -- +# +# The from command plucks an option value from a list of options and their +# values. If it is found, it and its value are removed from the list, +# and the value is returned. + +proc jlib::util::from {argvName option {defvalue ""}} { + upvar $argvName argv + + set ioption [lsearch -exact $argv $option] + if {$ioption == -1} { + return $defvalue + } else { + set ivalue [expr {$ioption + 1}] + set value [lindex $argv $ivalue] + set argv [lreplace $argv $ioption $ivalue] + return $value + } +} diff --git a/lib/jabberlib/vcard.tcl b/lib/jabberlib/vcard.tcl new file mode 100644 index 0000000..5c57555 --- /dev/null +++ b/lib/jabberlib/vcard.tcl @@ -0,0 +1,449 @@ +# vcard.tcl -- +# +# This file is part of the jabberlib. +# It handles vcard stuff and provides cache for it as well. +# +# Copyright (c) 2005-2006 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: vcard.tcl,v 1.14 2007/11/10 15:44:59 matben Exp $ +# +############################# USAGE ############################################ +# +# NAME +# vcard - convenience command library for the vcard extension. +# +# SYNOPSIS +# jlib::vcard::init jlibName ?-opt value ...? +# +# INSTANCE COMMANDS +# jlibname vcard send_get jid callbackProc +# jlibname vcard send_set jid callbackProc +# jlibname vcard get_async jid callbackProc +# jlibname vcard has_cache jid +# jlibname vcard get_cache jid +# +################################################################################ + +package require jlib + +package provide jlib::vcard 0.1 + +namespace eval jlib::vcard { + + # Note: jlib::ensamble_register is last in this file! +} + +# jlib::vcard::init -- +# +# Creates a new instance of a vcard object. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# args: +# +# Results: +# namespaced instance command + +proc jlib::vcard::init {jlibname args} { + + variable xmlns + set xmlns(vcard) "vcard-temp" + + # Instance specific arrays. + namespace eval ${jlibname}::vcard { + variable state + } + upvar ${jlibname}::vcard::state state + + set state(cache) 1 + + return +} + +# jlib::vcard::cmdproc -- +# +# Just dispatches the command to the right procedure. +# +# Arguments: +# jlibname: name of existing jabberlib instance +# cmd: +# args: all args to the cmd procedure. +# +# Results: +# none. + +proc jlib::vcard::cmdproc {jlibname cmd args} { + + # Which command? Just dispatch the command to the right procedure. + return [eval {$cmd $jlibname} $args] +} + +# jlib::vcard::send_get -- +# +# It implements the 'jabber:iq:vcard-temp' get method. +# +# Arguments: +# jlibname: the instance of this jlib. +# jid: bare JID for other users, full jid for ourself. +# cmd: client command to be executed at the iq "result" element. +# +# Results: +# none. + +proc jlib::vcard::send_get {jlibname jid cmd} { + variable xmlns + upvar ${jlibname}::vcard::state state + + set mjid [jlib::jidmap $jid] + set state(pending,$mjid) 1 + set attrlist [list xmlns $xmlns(vcard)] + set xmllist [wrapper::createtag "vCard" -attrlist $attrlist] + jlib::send_iq $jlibname "get" [list $xmllist] -to $jid -command \ + [list [namespace current]::send_get_cb $jlibname $jid $cmd] + return +} + +# jlib::vcard::send_get_cb -- +# +# Cache vcard info from above and call up. + +proc jlib::vcard::send_get_cb {jlibname jid cmd type subiq} { + upvar ${jlibname}::vcard::state state + + set mjid [jlib::jidmap $jid] + unset -nocomplain state(pending,$mjid) + if {$state(cache)} { + set state(cache,$mjid) $subiq + } + InvokeStacked $jlibname $jid $type $subiq + + uplevel #0 $cmd [list $jlibname $type $subiq] +} + +# jlib::vcard::get_async -- +# +# Get vcard async using 'cmd' callback. +# If cached it is returned directly using 'cmd', if pending the cmd +# is invoked when getting result, else we do a send_get. + +proc jlib::vcard::get_async {jlibname jid cmd} { + upvar ${jlibname}::vcard::state state + + set mjid [jlib::jidmap $jid] + if {[info exists state(cache,$mjid)]} { + uplevel #0 $cmd [list $jlibname result $state(cache,$mjid)] + } elseif {[info exists state(pending,$mjid)]} { + lappend state(invoke,$mjid) $cmd + } else { + send_get $jlibname $jid $cmd + } + return +} + +proc jlib::vcard::InvokeStacked {jlibname jid type subiq} { + upvar ${jlibname}::vcard::state state + + set mjid [jlib::jidmap $jid] + if {[info exists state(invoke,$mjid)]} { + foreach cmd $state(invoke,$mjid) { + uplevel #0 $cmd [list $jlibname $type $subiq] + } + unset -nocomplain state(invoke,$mjid) + } +} + +# jlib::vcard::get_own_async -- +# +# Getting and setting owns vcard is special since lacks to attribute. + +proc jlib::vcard::get_own_async {jlibname cmd} { + upvar ${jlibname}::vcard::state state + + set jid [$jlibname myjid2] + set mjid [jlib::jidmap $jid] + if {[info exists state(cache,$mjid)]} { + uplevel #0 $cmd [list $jlibname result $state(cache,$mjid)] + } elseif {[info exists state(pending,$mjid)]} { + lappend state(invoke,$mjid) $cmd + } else { + send_get_own $jlibname $cmd + } + return +} + +proc jlib::vcard::send_get_own {jlibname cmd} { + variable xmlns + + # A user may retrieve his or her own vCard by sending XML of the + # following form to his or her own JID (the 'to' attribute SHOULD NOT + # be included). + set attrlist [list xmlns $xmlns(vcard)] + set xmllist [wrapper::createtag "vCard" -attrlist $attrlist] + jlib::send_iq $jlibname "get" [list $xmllist] -command \ + [list [namespace current]::send_get_own_cb $jlibname $cmd] +} + +proc jlib::vcard::send_get_own_cb {jlibname cmd type subiq} { + upvar ${jlibname}::vcard::state state + + set jid [$jlibname myjid2] + set mjid [jlib::jidmap $jid] + unset -nocomplain state(pending,$mjid) + if {$state(cache)} { + set state(cache,$mjid) $subiq + } + InvokeStacked $jlibname $jid $type $subiq + + uplevel #0 $cmd [list $jlibname $type $subiq] +} + +# jlib::vcard::set_my_photo -- +# +# A utility to set our vCard photo. +# If photo empty then remove photo from vCard. +# +# @@@ TODO: Perhaps we should use a cached vCard instead of getting it +# each time? The cache would only need one request and then +# set each time we set our usual vCard. + +proc jlib::vcard::set_my_photo {jlibname photo mime cmd} { + + send_get_own $jlibname \ + [list [namespace current]::get_my_photo_cb $photo $mime $cmd] +} + +proc jlib::vcard::get_my_photo_cb {photo mime cmd jlibname type subiq} { + variable xmlns + + # Replace or set an element: + # + # + # image/jpeg + # Base64-encoded-avatar-file-here! + # + + if {$type eq "result"} { + if {[string length $photo]} { + set newphoto 1 + set vcardE $subiq + + # Replace or add photo. But only if different. + set photoE [wrapper::getfirstchildwithtag $vcardE "PHOTO"] + if {[llength $photoE]} { + set binE [wrapper::getfirstchildwithtag $photoE "BINVAL"] + if {[llength $binE]} { + set sphoto [wrapper::getcdata $binE] + + # Base64 code can contain undefined spaces: decode! + set sdata [::base64::decode $sphoto] + set data [::base64::decode $photo] + if {[string equal $sdata $data]} { + set newphoto 0 + } + } + } + if {$newphoto} { + lappend subElems [wrapper::createtag "TYPE" -chdata $mime] + lappend subElems [wrapper::createtag "BINVAL" -chdata $photo] + set photoE [wrapper::createtag "PHOTO" -subtags $subElems] + if {$vcardE eq {}} { + set xmllist [wrapper::createtag "vCard" \ + -attrlist [list xmlns $xmlns(vcard)] \ + -subtags [list $photoE]] + } else { + set xmllist [wrapper::setchildwithtag $vcardE $photoE] + } + jlib::send_iq $jlibname "set" [list $xmllist] -command \ + [list [namespace current]::set_my_photo_cb $jlibname $cmd] + } + } else { + + # Remove any photo. If there is no PHOTO no need to set. + set photoE [wrapper::getfirstchildwithtag $subiq "PHOTO"] + if {[llength $photoE]} { + set xmllist [wrapper::deletechildswithtag $subiq "PHOTO"] + jlib::send_iq $jlibname "set" [list $xmllist] -command \ + [list [namespace current]::set_my_photo_cb $jlibname $cmd] + } + } + } else { + uplevel #0 $cmd [list $jlibname $type $subiq] + } +} + +proc jlib::vcard::set_my_photo_cb {jlibname cmd type subiq} { + + uplevel #0 $cmd [list $jlibname $type $subiq] +} + +proc jlib::vcard::has_cache {jlibname jid} { + upvar ${jlibname}::vcard::state state + + set mjid [jlib::jidmap $jid] + return [info exists state(cache,$mjid)] +} + +proc jlib::vcard::get_cache {jlibname jid} { + upvar ${jlibname}::vcard::state state + + set mjid [jlib::jidmap $jid] + if {[info exists state(cache,$mjid)]} { + return $state(cache,$mjid) + } else { + return + } +} + +# jlib::vcard::send_set, createvcard -- +# +# Sends our vCard to the server. Internally we use all lower case +# but the spec (XEP-0054) says that all tags be all upper case. +# +# Arguments: +# jlibname: the instance of this jlib. +# cmd: client command to be executed at the iq "result" element. +# args: All keys are named so that the element hierarchy becomes +# vcardElement_subElement_subsubElement ... and so on; +# all lower case. +# +# Results: +# none. + +proc jlib::vcard::send_set {jlibname cmd args} { + upvar ${jlibname}::vcard::state state + + set jid [$jlibname myjid2] + set xmllist [eval {create $jlibname} $args] + set state(cache,$jid) $xmllist + jlib::send_iq $jlibname "set" [list $xmllist] -command \ + [list [namespace current]::send_set_cb $jlibname $cmd] + return +} + +proc jlib::vcard::create {jlibname args} { + variable xmlns + + set attrlist [list xmlns $xmlns(vcard)] + + # Form all the sub elements by inspecting the -key. + array set arr $args + set subE [list] + + # All "sub" elements with no children. + foreach tag {fn nickname bday url title role desc} { + if {[info exists arr(-$tag)]} { + lappend subE [wrapper::createtag [string toupper $tag] \ + -chdata $arr(-$tag)] + } + } + if {[info exists arr(-email_internet_pref)]} { + set elem [list] + lappend elem [wrapper::createtag "INTERNET"] + lappend elem [wrapper::createtag "PREF"] + lappend subE [wrapper::createtag "EMAIL" \ + -chdata $arr(-email_internet_pref) -subtags $elem] + } + if {[info exists arr(-email_internet)]} { + foreach email $arr(-email_internet) { + set elem [list] + lappend elem [wrapper::createtag "INTERNET"] + lappend subE [wrapper::createtag "EMAIL" \ + -chdata $email -subtags $elem] + } + } + + # All "subsub" elements. + foreach tag {n org} { + set elem [list] + foreach key [array names arr "-${tag}_*"] { + regexp -- "-${tag}_(.+)" $key match sub + lappend elem [wrapper::createtag [string toupper $sub] \ + -chdata $arr($key)] + } + + # Insert subsub elements where they belong. + if {[llength $elem]} { + lappend subE [wrapper::createtag [string toupper $tag] \ + -subtags $elem] + } + } + + # The , sub elements. + foreach tag {adr_home adr_work} { + regexp -- {([^_]+)_(.+)} $tag match head sub + set elem [list [wrapper::createtag [string toupper $sub]]] + set haveThisTag 0 + foreach key [array names arr "-${tag}_*"] { + set haveThisTag 1 + regexp -- "-${tag}_(.+)" $key match sub + lappend elem [wrapper::createtag [string toupper $sub] \ + -chdata $arr($key)] + } + if {$haveThisTag} { + lappend subE [wrapper::createtag [string toupper $head] \ + -subtags $elem] + } + } + + # The sub elements. + foreach tag [array names arr "-tel_*"] { + if {[regexp -- {-tel_([^_]+)_([^_]+)} $tag match second third]} { + set elem {} + lappend elem [wrapper::createtag [string toupper $second]] + lappend elem [wrapper::createtag [string toupper $third]] + lappend subE [wrapper::createtag "TEL" -chdata $arr($tag) \ + -subtags $elem] + } + } + + # The sub elements. + if {[info exists arr(-photo_binval)]} { + set elem {} + lappend elem [wrapper::createtag "BINVAL" -chdata $arr(-photo_binval)] + if {[info exists arr(-photo_type)]} { + lappend elem [wrapper::createtag "TYPE" -chdata $arr(-photo_type)] + } + lappend subE [wrapper::createtag "PHOTO" -subtags $elem] + } + + return [wrapper::createtag "vCard" -attrlist $attrlist -subtags $subE] +} + +proc jlib::vcard::send_set_cb {jlibname cmd type subiq args} { + + uplevel #0 $cmd [list $jlibname $type $subiq] +} + +proc jlib::vcard::cache {jlibname args} { + upvar ${jlibname}::vcard::state state + + if {[llength $args] == 1} { + set state(cache) [lindex $args 0] + } + return $state(cache) +} + +proc jlib::vcard::clear {jlibname {jid ""}} { + upvar ${jlibname}::vcard::state state + + if {$jid eq ""} { + array unset state "cache,*" + } else { + set mjid [jlib::jidmap $jid] + array unset state "cache,[jlib::ESC $mjid]" + } +} + +# We have to do it here since need the initProc before doing this. + +namespace eval jlib::vcard { + + jlib::ensamble_register vcard \ + [namespace current]::init \ + [namespace current]::cmdproc +} + + + diff --git a/lib/jabberlib/wrapper.tcl b/lib/jabberlib/wrapper.tcl new file mode 100644 index 0000000..179adb7 --- /dev/null +++ b/lib/jabberlib/wrapper.tcl @@ -0,0 +1,1062 @@ +################################################################################ +# +# wrapper.tcl +# +# This file defines wrapper procedures. These +# procedures are called by functions in jabberlib, and +# they in turn call the TclXML library functions. +# +# Copyright (c) 2002-2008 Mats Bengtsson +# +# This file is distributed under BSD style license. +# +# $Id: wrapper.tcl,v 1.41 2008/03/26 15:37:23 matben Exp $ +# +# ########################### INTERNALS ######################################## +# +# The whole parse tree is stored as a hierarchy of lists as: +# +# parent = {tag attrlist isempty cdata {child1 child2 ...}} +# +# where the childs are in turn a list of identical structure: +# +# child1 = {tag attrlist isempty cdata {grandchild1 grandchild2 ...}} +# child2 = {tag attrlist isempty cdata {grandchild1 grandchild2 ...}} +# +# etc. +# +# ########################### USAGE ############################################ +# +# NAME +# wrapper::new - a wrapper for the TclXML parser. +# SYNOPSIS +# wrapper::new streamstartcmd streamendcmd parsecmd errorcmd +# OPTIONS +# none +# COMMANDS +# wrapper::reset wrapID +# wrapper::createxml xmllist +# wrapper::createtag tagname ?args? +# wrapper::getattr attrlist attrname +# wrapper::setattr attrlist attrname value +# wrapper::parse id xml +# wrapper::xmlcrypt chdata +# wrapper::gettag xmllist +# wrapper::getattrlist xmllist +# wrapper::getisempty xmllist +# wrapper::getcdata xmllist +# wrapper::getchildren xmllist +# wrapper::getattribute xmllist attrname +# wrapper::setattrlist xmllist attrlist +# wrapper::setcdata xmllist cdata +# wrapper::splitxml xmllist tagVar attrVar cdataVar childVar +# +# ########################### LIMITATIONS ###################################### +# +# Mixed elements of character data and elements are not working. +# +# ########################### CHANGES ########################################## +# +# 0.* by Kerem HADIMLI and Todd Bradley +# 1.0a1 complete rewrite, and first release by Mats Bengtsson +# 1.0a2 a few fixes +# 1.0a3 wrapper::reset was not right, -ignorewhitespace, +# -defaultexpandinternalentities +# 1.0b1 added wrapper::parse command, configured for expat, +# return break at stream end +# 1.0b2 fix to make parser reentrant +# 030910 added accessor functions to get/set xmllist elements +# 031103 added splitxml command + + +if {[catch {package require tdom}]} { + package require xml 3.1 +} + +namespace eval wrapper { + + # The public interface. + namespace export what + + # Keep all internal data in this array, with 'id' as first index. + variable wrapper + + # Running id that is never reused; start from 0. + set wrapper(uid) 0 + + # Keep all 'id's in this list. + set wrapper(list) [list] + + variable xmldefaults {-isempty 1 -attrlist {} -chdata {} -subtags {}} +} + +# wrapper::new -- +# +# Contains initializations needed for the wrapper. +# Sets up callbacks via the XML parser. +# +# Arguments: +# streamstartcmd: callback when level one start tag received +# streamendcmd: callback when level one end tag received +# parsecmd: callback when level two end tag received +# errorcmd callback when receiving an error from the XML parser. +# Must all be fully qualified names. +# +# Results: +# A unique wrapper id. + +proc wrapper::new {streamstartcmd streamendcmd parsecmd errorcmd} { + variable wrapper + + # Handle id of the wrapper. + set id wrap[incr wrapper(uid)] + lappend wrapper(list) $id + + set wrapper($id,streamstartcmd) $streamstartcmd + set wrapper($id,streamendcmd) $streamendcmd + set wrapper($id,parsecmd) $parsecmd + set wrapper($id,errorcmd) $errorcmd + + # Create the actual XML parser. It is created in our present namespace, + # at least for the tcl parser!!! + + if {[llength [package provide tdom]]} { + #set wrapper($id,parser) [xml::parser -namespace 1] + set wrapper($id,parser) [expat -namespace 1] + set wrapper($id,class) "tdom" + $wrapper($id,parser) configure \ + -final 0 \ + -elementstartcommand [list [namespace current]::elementstart $id] \ + -elementendcommand [list [namespace current]::elementend $id] \ + -characterdatacommand [list [namespace current]::chdata $id] \ + -ignorewhitespace 0 + } else { + set wrapper($id,parser) [xml::parser] + + # Investigate which parser class we've got, and act consequently. + set classes [::xml::parserclass info names] + if {[lsearch $classes "expat"] >= 0} { + set wrapper($id,class) "expat" + $wrapper($id,parser) configure \ + -final 0 \ + -reportempty 1 \ + -elementstartcommand [list [namespace current]::elementstart $id] \ + -elementendcommand [list [namespace current]::elementend $id] \ + -characterdatacommand [list [namespace current]::chdata $id] \ + -ignorewhitespace 1 \ + -defaultexpandinternalentities 0 + } else { + set wrapper($id,class) "tcl" + $wrapper($id,parser) configure \ + -final 0 \ + -reportempty 1 \ + -elementstartcommand [list [namespace current]::elementstart $id] \ + -elementendcommand [list [namespace current]::elementend $id] \ + -characterdatacommand [list [namespace current]::chdata $id] \ + -errorcommand [list [namespace current]::xmlerror $id] \ + -ignorewhitespace 1 \ + -defaultexpandinternalentities 0 + } + } + + # Experiment. + if {0} { + package require qdxml + set token [qdxml::create \ + -elementstartcommand [list [namespace current]::elementstart $id] \ + -elementendcommand [list [namespace current]::elementend $id] \ + -characterdatacommand [list [namespace current]::chdata $id]] + set wrapper($id,parser) $token + } + + # Current level; 0 before root tag; 1 just after root tag, 2 after + # command tag, etc. + set wrapper($id,level) 0 + set wrapper($id,levelonetag) "" + + # Level 1 is the main tag, , and level 2 + # is the command tag, such as . We don't handle level 1 xmldata. + set wrapper($id,tree,2) [list] + + set wrapper($id,refcount) 0 + set wrapper($id,stack) "" + + return $id +} + +# wrapper::parse -- +# +# For parsing xml. +# +# Arguments: +# id: the wrapper id. +# xml: raw xml data to be parsed. +# +# Results: +# none. + +proc wrapper::parse {id xml} { + variable wrapper + + # This is not as innocent as it looks; the 'tcl' parser proc is created in + # the creators namespace (wrapper::), but the 'expat' parser ??? + parsereentrant $id $xml + return +} + +# wrapper::parsereentrant -- +# +# Forces parsing to be serialized in an event driven environment. +# If we read xml from socket and happen to trigger a read (and parse) +# event right from an element callback, everyhting will be out of sync. +# +# Arguments: +# id: the wrapper id +# xml: raw xml data to be parsed. +# +# Results: +# none. + +proc wrapper::parsereentrant {id xml} { + variable wrapper + + set p $wrapper($id,parser) + set refcount [incr wrapper($id,refcount)] + + if {$refcount == 1} { + + # This is the main entry: do parse original xml. + $p parse $xml + + # Parse everything on the stack (until empty?). + while {[string length $wrapper($id,stack)] > 0} { + set tmp $wrapper($id,stack) + set wrapper($id,stack) "" + $p parse $tmp + } + } else { + + # Reentry, put on stack for delayed execution. + append wrapper($id,stack) $xml + } + + # If we was reset from callback 'refcount' can have been reset to 0. + incr wrapper($id,refcount) -1 + if {$wrapper($id,refcount) < 0} { + set wrapper($id,refcount) 0 + } + return +} + +# wrapper::elementstart -- +# +# Callback proc for all element start. +# +# Arguments: +# id: the wrapper id. +# tagname: the element (tag) name. +# attrlist: list of attributes {key value key value ...} +# args: additional arguments given by the parser. +# +# Results: +# none. + +proc wrapper::elementstart {id tagname attrlist args} { + variable wrapper + + # Check args, to see if empty element and/or namespace. + # Put xmlns in attribute list. + array set argsarr $args + set isempty 0 + if {[info exists argsarr(-empty)]} { + set isempty $argsarr(-empty) + } + if {[info exists argsarr(-namespacedecls)]} { + lappend attrlist xmlns [lindex $argsarr(-namespacedecls) 0] + } + + if {$wrapper($id,class) eq "tdom"} { + if {[set ndx [string last : $tagname]] != -1} { + set ns [string range $tagname 0 [expr {$ndx - 1}]] + set tagname [string range $tagname [incr ndx] end] + lappend attrlist xmlns $ns + } + } + + if {$wrapper($id,level) == 0} { + + # We got a root tag, such as + set wrapper($id,level) 1 + set wrapper($id,levelonetag) $tagname + set wrapper($id,tree,1) [list $tagname $attrlist $isempty {} {}] + + # Do the registered callback at the global level. + uplevel #0 $wrapper($id,streamstartcmd) $attrlist + + } else { + + # This is either a level 2 command tag, such as 'presence', 'iq', or 'message', + # or we have got a new tag beyond level 2. + # It is time to start building the parse tree. + set level [incr wrapper($id,level)] + set wrapper($id,tree,$level) [list $tagname $attrlist $isempty {} {}] + } +} + +# wrapper::elementend -- +# +# Callback proc for all element ends. +# +# Arguments: +# id: the wrapper id. +# tagname: the element (tag) name. +# args: additional arguments given by the parser. +# +# Results: +# none. + +proc wrapper::elementend {id tagname args} { + variable wrapper + + # tclxml doesn't do the reset properly but continues to send us endtags. + # qdxml behaves better! + if {!$wrapper($id,level)} { + return + } + + # Check args, to see if empty element + set isempty 0 + set ind [lsearch -exact $args {-empty}] + if {$ind >= 0} { + set isempty [lindex $args [expr {$ind + 1}]] + } + if {$wrapper($id,level) == 1} { + + # End of the root tag (). + # Do the registered callback at the global level. + uplevel #0 $wrapper($id,streamendcmd) + + incr wrapper($id,level) -1 + + # We are in the middle of parsing, need to break. + reset $id + return -code 3 + } else { + + # We are finshed with this child tree. + set childlevel $wrapper($id,level) + + # Insert the child tree in the parent tree. + # Avoid adding to the level 1 else we just consume memory forever [PT] + set level [incr wrapper($id,level) -1] + if {$level > 1} { + append_child $id $level $wrapper($id,tree,$childlevel) + } elseif {$level == 1} { + + # We've got an end tag of a command tag, and it's time to + # deliver our parse tree to the registered callback proc. + uplevel #0 $wrapper($id,parsecmd) [list $wrapper($id,tree,2)] + } + } +} + +# wrapper::append_child -- +# +# Inserts a child element data in level temp data. +# +# Arguments: +# id: the wrapper id. +# level: the parent level, child is level+1. +# childtree: the tree to append. +# +# Results: +# none. + +proc wrapper::append_child {id level childtree} { + variable wrapper + + # Get child list at parent level (level). + set childlist [lindex $wrapper($id,tree,$level) 4] + lappend childlist $childtree + + # Build the new parent tree. + set wrapper($id,tree,$level) [lreplace $wrapper($id,tree,$level) 4 4 \ + $childlist] +} + +# wrapper::chdata -- +# +# Appends character data to the tree level xml chdata. +# It makes also internal entity replacements on character data. +# Callback from the XML parser. +# +# Arguments: +# id: the wrapper id. +# chardata: the character data. +# +# Results: +# none. + +proc wrapper::chdata {id chardata} { + variable wrapper + + set level $wrapper($id,level) + + # If we receive CHDATA before any root element, + # or after the last root element, discard. + if {$level <= 0} { + return + } + set chdata [lindex $wrapper($id,tree,$level) 3] + + # Make standard entity replacements. + append chdata [xmldecrypt $chardata] + set wrapper($id,tree,$level) \ + [lreplace $wrapper($id,tree,$level) 3 3 "$chdata"] +} + +# wrapper::free -- +# +# tdom doesn't permit freeing a parser from within a callback. So +# we keep trying until it works. +# + +proc wrapper::free {id} { + if {[catch {$id free}]} { + after 100 [list [namespace origin free] $id] + } +} + +# wrapper::reset -- +# +# Resets the wrapper and XML parser to be prepared for a fresh new +# document. +# If done while parsing be sure to return a break (3) from callback. +# +# Arguments: +# id: the wrapper id. +# +# Results: +# none. + +proc wrapper::reset {id} { + variable wrapper + + if {$wrapper($id,class) eq "tdom"} { + + # We cannot reset a tdom expat parser from within a callback. However, + # we can always replace it with a new one. + set old $wrapper($id,parser) + after idle [list [namespace origin free] $old] + #set wrapper($id,parser) [xml::parser -namespace 1] + set wrapper($id,parser) [expat -namespace 1] + + $wrapper($id,parser) configure \ + -final 0 \ + -elementstartcommand [list [namespace current]::elementstart $id] \ + -elementendcommand [list [namespace current]::elementend $id] \ + -characterdatacommand [list [namespace current]::chdata $id] \ + -ignorewhitespace 0 + } else { + + # This resets the actual XML parser. Not sure this is actually needed. + $wrapper($id,parser) reset + + # Unfortunately it also removes all our callbacks and options. + if {$wrapper($id,class) eq "expat"} { + $wrapper($id,parser) configure \ + -final 0 \ + -reportempty 1 \ + -elementstartcommand [list [namespace current]::elementstart $id] \ + -elementendcommand [list [namespace current]::elementend $id] \ + -characterdatacommand [list [namespace current]::chdata $id] \ + -ignorewhitespace 1 \ + -defaultexpandinternalentities 0 + } else { + $wrapper($id,parser) configure \ + -final 0 \ + -reportempty 1 \ + -elementstartcommand [list [namespace current]::elementstart $id] \ + -elementendcommand [list [namespace current]::elementend $id] \ + -characterdatacommand [list [namespace current]::chdata $id] \ + -errorcommand [list [namespace current]::xmlerror $id] \ + -ignorewhitespace 1 \ + -defaultexpandinternalentities 0 + } + } + + # Cleanup internal state vars. + array unset wrapper $id,tree,* + + # Reset also our internal wrapper to its initial position. + set wrapper($id,level) 0 + set wrapper($id,levelonetag) "" + set wrapper($id,tree,2) [list] + + set wrapper($id,refcount) 0 + set wrapper($id,stack) "" +} + +# wrapper::xmlerror -- +# +# Callback from the XML parser when error received. Resets wrapper, +# and makes a 'streamend' command callback. +# +# Arguments: +# id: the wrapper id. +# +# Results: +# none. + +proc wrapper::xmlerror {id args} { + variable wrapper + + uplevel #0 $wrapper($id,errorcmd) $args +} + +# wrapper::createxml -- +# +# Creates raw xml data from a hierarchical list of xml code. +# This proc gets called recursively for each child. +# It makes also internal entity replacements on character data. +# Mixed elements aren't treated correctly generally. +# +# Arguments: +# xmllist a list of xml code in the format described in the header. +# +# Results: +# raw xml data. + +proc wrapper::createxml {xmllist} { + + # Extract the XML data items. + foreach {tag attrlist isempty chdata childlist} $xmllist { break } + set attrlist [xmlcrypt $attrlist] + set rawxml "<$tag" + foreach {attr value} $attrlist { + append rawxml " $attr='$value'" + } + if {$isempty} { + append rawxml "/>" + } else { + append rawxml ">" + + # Call ourselves recursively for each child element. + # There is an arbitrary choice here where childs are put before PCDATA. + foreach child $childlist { + append rawxml [createxml $child] + } + + # Make standard entity replacements. + if {[string length $chdata]} { + append rawxml [xmlcrypt $chdata] + } + append rawxml "" + } + return $rawxml +} + +# wrapper::formatxml, formattag -- +# +# Creates formatted raw xml data from a xml list. + +proc wrapper::formatxml {xmllist args} { + variable tabs + variable nl + variable prefix + + array set argsA { + -prefix "" + } + array set argsA $args + set prefix $argsA(-prefix) + set nl "" + set tabs "" + formattag $xmllist +} + +proc wrapper::formattag {xmllist} { + variable tabs + variable nl + variable prefix + + foreach {tag attrlist isempty chdata childlist} $xmllist { break } + set attrlist [xmlcrypt $attrlist] + set rawxml "$nl$prefix$tabs<$tag" + foreach {attr value} $attrlist { + append rawxml " $attr='$value'" + } + set nl "\n" + if {$isempty} { + append rawxml "/>" + } else { + append rawxml ">" + if {[llength $childlist]} { + append tabs "\t" + foreach child $childlist { + append rawxml [formattag $child] + } + set tabs [string range $tabs 0 end-1] + append rawxml "$nl$prefix$tabs" + } else { + if {[string length $chdata]} { + append rawxml [xmlcrypt $chdata] + } + append rawxml "" + } + } + return $rawxml +} + +# wrapper::createtag -- +# +# Build an element list given the tag and the args. +# +# Arguments: +# tagname: the name of this element. +# args: +# -empty 0|1 Is this an empty tag? If $chdata +# and $subtags are empty, then whether +# to make the tag empty or not is decided +# here. (default: 1) +# -attrlist {attr1 value1 attr2 value2 ..} Vars is a list +# consisting of attr/value pairs, as shown. +# -chdata $chdata ChData of tag (default: ""). +# -subtags {$subchilds $subchilds ...} is a list containing xmldata +# of $tagname's subtags. (default: no sub-tags) +# +# Results: +# a list suitable for wrapper::createxml. + +proc wrapper::createtag {tagname args} { + variable xmldefaults + + # Fill in the defaults. + array set xmlarr $xmldefaults + + # Override the defults with actual values. + if {[llength $args]} { + array set xmlarr $args + } + if {[string length $xmlarr(-chdata)] || [llength $xmlarr(-subtags)]} { + set xmlarr(-isempty) 0 + } + + # Build sub elements list. + set sublist [list] + foreach child $xmlarr(-subtags) { + lappend sublist $child + } + set xmllist [list $tagname $xmlarr(-attrlist) $xmlarr(-isempty) \ + $xmlarr(-chdata) $sublist] + return $xmllist +} + +# wrapper::validxmllist -- +# +# Makes a primitive check to see if this is a valid xmllist. + +proc wrapper::validxmllist {xmllist} { + return [expr ([llength $xmllist] == 5) ? 1 : 0] +} + +# wrapper::getattr -- +# +# This proc returns the value of 'attrname' from 'attrlist'. +# +# Arguments: +# attrlist: a list of key value pairs for the attributes. +# attrname: the name of the attribute which value we query. +# +# Results: +# value of the attribute or empty. + +proc wrapper::getattr {attrlist attrname} { + + foreach {attr val} $attrlist { + if {[string equal $attr $attrname]} { + return $val + } + } + return +} + +proc wrapper::getattribute {xmllist attrname} { + + foreach {attr val} [lindex $xmllist 1] { + if {[string equal $attr $attrname]} { + return $val + } + } + return +} + +proc wrapper::isattr {attrlist attrname} { + + foreach {attr val} $attrlist { + if {[string equal $attr $attrname]} { + return 1 + } + } + return 0 +} + +proc wrapper::isattribute {xmllist attrname} { + + foreach {attr val} [lindex $xmllist 1] { + if {[string equal $attr $attrname]} { + return 1 + } + } + return 0 +} + +proc wrapper::setattr {attrlist attrname value} { + + array set attrArr $attrlist + set attrArr($attrname) $value + return [array get attrArr] +} + +# wrapper::gettag, getattrlist, getisempty, getcdata, getchildren -- +# +# Accessor functions for 'xmllist'. +# {tag attrlist isempty cdata {grandchild1 grandchild2 ...}} +# +# Arguments: +# xmllist: an xml hierarchical list. +# +# Results: +# list of childrens if any. + +proc wrapper::gettag {xmllist} { + return [lindex $xmllist 0] +} + +proc wrapper::getattrlist {xmllist} { + return [lindex $xmllist 1] +} + +proc wrapper::getisempty {xmllist} { + return [lindex $xmllist 2] +} + +proc wrapper::getcdata {xmllist} { + return [lindex $xmllist 3] +} + +proc wrapper::getchildren {xmllist} { + return [lindex $xmllist 4] +} + +proc wrapper::splitxml {xmllist tagVar attrVar cdataVar childVar} { + + foreach {tag attr empty cdata children} $xmllist break + uplevel 1 [list set $tagVar $tag] + uplevel 1 [list set $attrVar $attr] + uplevel 1 [list set $cdataVar $cdata] + uplevel 1 [list set $childVar $children] +} + +proc wrapper::getchildswithtag {xmllist tag} { + + set clist [list] + foreach celem [lindex $xmllist 4] { + if {[string equal [lindex $celem 0] $tag]} { + lappend clist $celem + } + } + return $clist +} + +proc wrapper::getfirstchildwithtag {xmllist tag} { + + set c [list] + foreach celem [lindex $xmllist 4] { + if {[string equal [lindex $celem 0] $tag]} { + set c $celem + break + } + } + return $c +} + +proc wrapper::havechildtag {xmllist tag} { + return [llength [getfirstchildwithtag $xmllist $tag]] +} + +proc wrapper::getfirstchildwithxmlns {xmllist ns} { + + set c [list] + foreach celem [lindex $xmllist 4] { + unset -nocomplain attr + array set attr [lindex $celem 1] + if {[info exists attr(xmlns)] && [string equal $attr(xmlns) $ns]} { + set c $celem + break + } + } + return $c +} + +proc wrapper::getchildswithtagandxmlns {xmllist tag ns} { + + set clist [list] + foreach celem [lindex $xmllist 4] { + if {[string equal [lindex $celem 0] $tag]} { + unset -nocomplain attr + array set attr [lindex $celem 1] + if {[info exists attr(xmlns)] && [string equal $attr(xmlns) $ns]} { + lappend clist $celem + } + } + } + return $clist +} + +proc wrapper::getfirstchild {xmllist tag ns} { + + set elem [list] + foreach celem [lindex $xmllist 4] { + if {[string equal [lindex $celem 0] $tag]} { + unset -nocomplain attr + array set attr [lindex $celem 1] + if {[info exists attr(xmlns)] && [string equal $attr(xmlns) $ns]} { + set elem $celem + break + } + } + } + return $elem +} + +proc wrapper::getfromchilds {childs tag} { + + set clist [list] + foreach celem $childs { + if {[string equal [lindex $celem 0] $tag]} { + lappend clist $celem + } + } + return $clist +} + +proc wrapper::deletefromchilds {childs tag} { + + set clist [list] + foreach celem $childs { + if {![string equal [lindex $celem 0] $tag]} { + lappend clist $celem + } + } + return $clist +} + +proc wrapper::getnamespacefromchilds {childs tag ns} { + + set clist [list] + foreach celem $childs { + if {[string equal [lindex $celem 0] $tag]} { + unset -nocomplain attr + array set attr [lindex $celem 1] + if {[info exists attr(xmlns)] && [string equal $attr(xmlns) $ns]} { + lappend clist $celem + break + } + } + } + return $clist +} + +# wrapper::getchilddeep -- +# +# Searches recursively for the first child with matching tags and +# optionally matching xmlns attributes. +# +# Arguments: +# xmllist: an xml hierarchical list. +# specs: {{tag ?xmlns?} {tag ?xmlns?} ...} +# +# Results: +# first found matching child element or empty if not found + +proc wrapper::getchilddeep {xmllist specs} { + + set xlist $xmllist + + foreach cspec $specs { + set tag [lindex $cspec 0] + set xmlns [lindex $cspec 1] + set match 0 + + foreach c [lindex $xlist 4] { + if {[string equal $tag [lindex $c 0]]} { + if {[string length $xmlns]} { + array unset attr + array set attr [lindex $c 1] + if {[info exists attr(xmlns)] && \ + [string equal $xmlns $attr(xmlns)]} { + set xlist $c + set match 1 + break + } else { + # tag matched but not xmlns; go for next child. + continue + } + } + set xlist $c + set match 1 + break + } + } + # No matches found. + if {!$match} { + return + } + } + return $xlist +} + +proc wrapper::setattrlist {xmllist attrlist} { + return [lreplace $xmllist 1 1 $attrlist] +} + +proc wrapper::setcdata {xmllist cdata} { + return [lreplace $xmllist 3 3 $cdata] +} + +proc wrapper::setchildlist {xmllist childlist} { + return [lreplace $xmllist 4 4 $childlist] +} + +# wrapper::setchildwithtag -- +# +# Replaces any element with same tag. +# If not there it will be added. +# xmllist must be nonempty. + +proc wrapper::setchildwithtag {xmllist elem} { + set tag [lindex $elem 0] + set clist [list] + foreach c [lindex $xmllist 4] { + if {[lindex $c 0] ne $tag} { + lappend clist $c + } + } + lappend clist $elem + # IMPORTANT: + lset xmllist 2 0 + return [lreplace $xmllist 4 4 $clist] +} + +# wrapper::deletechildswithtag -- +# +# Deletes any element with tag. +# xmllist must be nonempty. + +proc wrapper::deletechildswithtag {xmllist tag} { + set clist [list] + foreach c [lindex $xmllist 4] { + if {[lindex $c 0] ne $tag} { + lappend clist $c + } + } + return [lreplace $xmllist 4 4 $clist] +} + +# wrapper::xmlcrypt -- +# +# Makes standard XML entity replacements. +# +# Arguments: +# chdata: character data. +# +# Results: +# chdata with XML standard entities replaced. + +proc wrapper::xmlcrypt {chdata} { + + # RFC 3454 (STRINGPREP): + # C.2.1 ASCII control characters + # 0000-001F; [CONTROL CHARACTERS] + # 007F; DELETE + + return [string map {& & < < > > \" " ' ' + \x00 " " \x01 " " \x02 " " \x03 " " + \x04 " " \x05 " " \x06 " " \x07 " " + \x08 " " \x0B " " + \x0C " " \x0E " " \x0F " " + \x10 " " \x11 " " \x12 " " \x13 " " + \x14 " " \x15 " " \x16 " " \x17 " " + \x18 " " \x19 " " \x1A " " \x1B " " + \x1C " " \x1D " " \x1E " " \x1F " " + \x7F " "} $chdata] +} + +# wrapper::xmldecrypt -- +# +# Replaces the XML standard entities with real characters. +# +# Arguments: +# chdata: character data. +# +# Results: +# chdata without any XML standard entities. + +proc wrapper::xmldecrypt {chdata} { + + return [string map { + {&} {&} {<} {<} {>} {>} {"} {"} {'} {'}} $chdata] + #'" +} + +# wrapper::parse_xmllist_to_array -- +# +# Takes a hierarchical list of xml data and parses the character data +# into array elements. The array key of each element is constructed as: +# rootTag_subTag_subSubTag. +# Repetitative elements are not parsed correctly. +# Mixed elements of chdata and tags are not allowed. +# This is typically called without a 'key' argument. +# +# Arguments: +# xmllist: a hierarchical list of xml data as defined above. +# arrName: +# key: (optional) the rootTag, typically only used internally. +# +# Results: +# none. Array elements filled. + +proc wrapper::parse_xmllist_to_array {xmllist arrName {key {}}} { + + upvar #0 $arrName locArr + + # Return if empty element. + if {[lindex $xmllist 2]} { + return + } + if {[string length $key]} { + set und {_} + } else { + set und {} + } + + set childs [lindex $xmllist 4] + if {[llength $childs]} { + foreach c $childs { + set newkey "${key}${und}[lindex $c 0]" + + # Call ourselves recursively. + parse_xmllist_to_array $c $arrName $newkey + } + } else { + + # This is a leaf of the tree structure. + set locArr($key) [lindex $xmllist 3] + } + return +} + +#------------------------------------------------------------------------------- +package provide wrapper 1.2 + diff --git a/lib/log/log.tcl b/lib/log/log.tcl new file mode 100644 index 0000000..a9f42ae --- /dev/null +++ b/lib/log/log.tcl @@ -0,0 +1,851 @@ +# log.tcl -- +# +# Tcl implementation of a general logging facility +# (Reaped from Pool_Base and modified to fit into tcllib) +# +# Copyright (c) 2001 by ActiveState Tool Corp. +# See the file license.terms. + +package require Tcl 8 +package provide log 1.2 + +# ### ### ### ######### ######### ######### + +namespace eval ::log { + namespace export levels lv2longform lv2color lv2priority + namespace export lv2cmd lv2channel lvCompare + namespace export lvSuppress lvSuppressLE lvIsSuppressed + namespace export lvCmd lvCmdForall + namespace export lvChannel lvChannelForall lvColor lvColorForall + namespace export log logMsg logError + + # The known log-levels. + + variable levels [list \ + emergency \ + alert \ + critical \ + error \ + warning \ + notice \ + info \ + debug] + + # Array mapping from all unique prefixes for log levels to their + # corresponding long form. + + # *future* Use a procedure from 'textutil' to calculate the + # prefixes and to fill the map. + + variable levelMap + array set levelMap { + a alert + al alert + ale alert + aler alert + alert alert + c critical + cr critical + cri critical + crit critical + criti critical + critic critical + critica critical + critical critical + d debug + de debug + deb debug + debu debug + debug debug + em emergency + eme emergency + emer emergency + emerg emergency + emerge emergency + emergen emergency + emergenc emergency + emergency emergency + er error + err error + erro error + error error + i info + in info + inf info + info info + n notice + no notice + not notice + noti notice + notic notice + notice notice + w warning + wa warning + war warning + warn warning + warni warning + warnin warning + warning warning + } + + # Map from log-levels to the commands to execute when a message + # with that level arrives in the system. The standard command for + # all levels is '::log::Puts' which writes the message to either + # stdout or stderr, depending on the level. The decision about the + # channel is stored in another map and modifiable by the user of + # the package. + + variable cmdMap + array set cmdMap {} + + variable lv + foreach lv $levels {set cmdMap($lv) ::log::Puts} + unset lv + + # Map from log-levels to the channels ::log::Puts shall write + # messages with that level to. The map can be queried and changed + # by the user. + + variable channelMap + array set channelMap { + emergency stderr + alert stderr + critical stderr + error stderr + warning stdout + notice stdout + info stdout + debug stdout + } + + # Graphical user interfaces may want to colorize messages based + # upon their level. The following array stores a map from levels + # to colors. The map can be queried and changed by the user. + + variable colorMap + array set colorMap { + emergency red + alert red + critical red + error red + warning yellow + notice seagreen + info {} + debug lightsteelblue + } + + # To allow an easy comparison of the relative importance of a + # level the following array maps from levels to a numerical + # priority. The higher the number the more important the + # level. The user cannot change this map (for now). This package + # uses the priorities to allow the user to supress messages based + # upon their levels. + + variable priorityMap + array set priorityMap { + emergency 7 + alert 6 + critical 5 + error 4 + warning 3 + notice 2 + info 1 + debug 0 + } + + # The following array is internal and holds the information about + # which levels are suppressed, i.e. may not be written. + # + # 0 - messages with with level are written out. + # 1 - messages with this level are suppressed. + + variable suppressed + array set suppressed { + emergency 0 + alert 0 + critical 0 + error 0 + warning 0 + notice 0 + info 0 + debug 0 + } + + # Internal static information. Map from levels to a string of + # spaces. The number of spaces in each string is just enough to + # make all level names together with their string of the same + # length. + + variable fill + array set fill { + emergency "" alert " " critical " " error " " + warning " " notice " " info " " debug " " + } +} + + +# log::levels -- +# +# Retrieves the names of all known levels. +# +# Arguments: +# None. +# +# Side Effects: +# None. +# +# Results: +# A list containing the names of all known levels, +# alphabetically sorted. + +proc ::log::levels {} { + variable levels + return [lsort $levels] +} + +# log::lv2longform -- +# +# Converts any unique abbreviation of a level name to the full +# level name. +# +# Arguments: +# level The prefix of a level name to convert. +# +# Side Effects: +# None. +# +# Results: +# Returns the full name to the specified abbreviation or an +# error. + +proc ::log::lv2longform {level} { + variable levelMap + + if {[info exists levelMap($level)]} { + return $levelMap($level) + } + + return -code error "\"$level\" is no unique abbreviation of a level name" +} + +# log::lv2color -- +# +# Converts any level name including unique abbreviations to the +# corresponding color. +# +# Arguments: +# level The level to convert into a color. +# +# Side Effects: +# None. +# +# Results: +# The name of a color or an error. + +proc ::log::lv2color {level} { + variable colorMap + set level [lv2longform $level] + return $colorMap($level) +} + +# log::lv2priority -- +# +# Converts any level name including unique abbreviations to the +# corresponding priority. +# +# Arguments: +# level The level to convert into a priority. +# +# Side Effects: +# None. +# +# Results: +# The numerical priority of the level or an error. + +proc ::log::lv2priority {level} { + variable priorityMap + set level [lv2longform $level] + return $priorityMap($level) +} + +# log::lv2cmd -- +# +# Converts any level name including unique abbreviations to the +# command prefix used to write messages with that level. +# +# Arguments: +# level The level to convert into a command prefix. +# +# Side Effects: +# None. +# +# Results: +# A string containing a command prefix or an error. + +proc ::log::lv2cmd {level} { + variable cmdMap + set level [lv2longform $level] + return $cmdMap($level) +} + +# log::lv2channel -- +# +# Converts any level name including unique abbreviations to the +# channel used by ::log::Puts to write messages with that level. +# +# Arguments: +# level The level to convert into a channel. +# +# Side Effects: +# None. +# +# Results: +# A string containing a channel handle or an error. + +proc ::log::lv2channel {level} { + variable channelMap + set level [lv2longform $level] + return $channelMap($level) +} + +# log::lvCompare -- +# +# Compares two levels (including unique abbreviations) with +# respect to their priority. This command can be used by the +# -command option of lsort. +# +# Arguments: +# level1 The first of the levels to compare. +# level2 The second of the levels to compare. +# +# Side Effects: +# None. +# +# Results: +# One of -1, 0 or 1 or an error. A result of -1 signals that +# level1 is of less priority than level2. 0 signals that both +# levels have the same priority. 1 signals that level1 has +# higher priority than level2. + +proc ::log::lvCompare {level1 level2} { + variable priorityMap + + set level1 $priorityMap([lv2longform $level1]) + set level2 $priorityMap([lv2longform $level2]) + + if {$level1 < $level2} { + return -1 + } elseif {$level1 > $level2} { + return 1 + } else { + return 0 + } +} + +# log::lvSuppress -- +# +# (Un)suppresses the output of messages having the specified +# level. Unique abbreviations for the level are allowed here +# too. +# +# Arguments: +# level The name of the level to suppress or +# unsuppress. Unique abbreviations are allowed +# too. +# suppress Boolean flag. Optional. Defaults to the value +# 1, which means to suppress the level. The +# value 0 on the other hand unsuppresses the +# level. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::lvSuppress {level {suppress 1}} { + variable suppressed + set level [lv2longform $level] + + switch -exact -- $suppress { + 0 - 1 {} default { + return -code error "\"$suppress\" is not a member of \{0, 1\}" + } + } + + set suppressed($level) $suppress + return +} + +# log::lvSuppressLE -- +# +# (Un)suppresses the output of messages having the specified +# level or one of lesser priority. Unique abbreviations for the +# level are allowed here too. +# +# Arguments: +# level The name of the level to suppress or +# unsuppress. Unique abbreviations are allowed +# too. +# suppress Boolean flag. Optional. Defaults to the value +# 1, which means to suppress the specified +# levels. The value 0 on the other hand +# unsuppresses the levels. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::lvSuppressLE {level {suppress 1}} { + variable suppressed + variable levels + variable priorityMap + + set level [lv2longform $level] + + switch -exact -- $suppress { + 0 - 1 {} default { + return -code error "\"$suppress\" is not a member of \{0, 1\}" + } + } + + set prio [lv2priority $level] + + foreach l $levels { + if {$priorityMap($l) <= $prio} { + set suppressed($l) $suppress + } + } + return +} + +# log::lvIsSuppressed -- +# +# Asks the package wether the specified level is currently +# suppressed. Unique abbreviations of level names are allowed. +# +# Arguments: +# level The level to query. +# +# Side Effects: +# None. +# +# Results: +# None. + +proc ::log::lvIsSuppressed {level} { + variable suppressed + set level [lv2longform $level] + return $suppressed($level) +} + +# log::lvCmd -- +# +# Defines for the specified level with which command to write +# the messages having this level. Unique abbreviations of level +# names are allowed. The command is actually a command prefix +# and this facility will append 2 arguments before calling it, +# the level of the message and the message itself, in this +# order. +# +# Arguments: +# level The level the command prefix is for. +# cmd The command prefix to use for the specified level. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::lvCmd {level cmd} { + variable cmdMap + set level [lv2longform $level] + set cmdMap($level) $cmd + return +} + +# log::lvCmdForall -- +# +# Defines for all known levels with which command to write the +# messages having this level. The command is actually a command +# prefix and this facility will append 2 arguments before +# calling it, the level of the message and the message itself, +# in this order. +# +# Arguments: +# cmd The command prefix to use for all levels. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::lvCmdForall {cmd} { + variable cmdMap + variable levels + + foreach l $levels { + set cmdMap($l) $cmd + } + return +} + +# log::lvChannel -- +# +# Defines for the specified level into which channel ::log::Puts +# (the standard command) shall write the messages having this +# level. Unique abbreviations of level names are allowed. The +# command is actually a command prefix and this facility will +# append 2 arguments before calling it, the level of the message +# and the message itself, in this order. +# +# Arguments: +# level The level the channel is for. +# chan The channel to use for the specified level. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::lvChannel {level chan} { + variable channelMap + set level [lv2longform $level] + set channelMap($level) $chan + return +} + +# log::lvChannelForall -- +# +# Defines for all known levels with which which channel +# ::log::Puts (the standard command) shall write the messages +# having this level. The command is actually a command prefix +# and this facility will append 2 arguments before calling it, +# the level of the message and the message itself, in this +# order. +# +# Arguments: +# chan The channel to use for all levels. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::lvChannelForall {chan} { + variable channelMap + variable levels + + foreach l $levels { + set channelMap($l) $chan + } + return +} + +# log::lvColor -- +# +# Defines for the specified level the color to return for it in +# a call to ::log::lv2color. Unique abbreviations of level names +# are allowed. +# +# Arguments: +# level The level the color is for. +# color The color to use for the specified level. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::lvColor {level color} { + variable colorMap + set level [lv2longform $level] + set colorMap($level) $color + return +} + +# log::lvColorForall -- +# +# Defines for all known levels the color to return for it in a +# call to ::log::lv2color. Unique abbreviations of level names +# are allowed. +# +# Arguments: +# color The color to use for all levels. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::lvColorForall {color} { + variable colorMap + variable levels + + foreach l $levels { + set colorMap($l) $color + } + return +} + +# log::logarray -- +# +# Similar to parray, except that the contents of the array +# printed out through the log system instead of directly +# to stdout. +# +# See also 'log::log' for a general explanation +# +# Arguments: +# level The level of the message. +# arrayvar The name of the array varaibe to dump +# pattern Optional pattern to restrict the dump +# to certain elements in the array. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::logarray {level arrayvar {pattern *}} { + variable cmdMap + + if {[lvIsSuppressed $level]} { + # Ignore messages for suppressed levels. + return + } + + set level [lv2longform $level] + + set cmd $cmdMap($level) + if {$cmd == {}} { + # Ignore messages for levels without a command + return + } + + upvar 1 $arrayvar array + if {![array exists array]} { + error "\"$arrayvar\" isn't an array" + } + set maxl 0 + foreach name [lsort [array names array $pattern]] { + if {[string length $name] > $maxl} { + set maxl [string length $name] + } + } + set maxl [expr {$maxl + [string length $arrayvar] + 2}] + foreach name [lsort [array names array $pattern]] { + set nameString [format %s(%s) $arrayvar $name] + + eval [linsert $cmd end $level \ + [format "%-*s = %s" $maxl $nameString $array($name)]] + } + return +} + +# log::loghex -- +# +# Like 'log::log', except that the logged data is assumed to +# be binary and is logged as a block of hex numbers. +# +# See also 'log::log' for a general explanation +# +# Arguments: +# level The level of the message. +# text Message printed before the hex block +# data Binary data to show as hex. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::loghex {level text data} { + variable cmdMap + + if {[lvIsSuppressed $level]} { + # Ignore messages for suppressed levels. + return + } + + set level [lv2longform $level] + + set cmd $cmdMap($level) + if {$cmd == {}} { + # Ignore messages for levels without a command + return + } + + # Format the messages and print them. + + set len [string length $data] + + eval [linsert $cmd end $level "$text ($len bytes):"] + + set address "" + set hexnums "" + set ascii "" + + for {set i 0} {$i < $len} {incr i} { + set v [string index $data $i] + binary scan $v H2 hex + binary scan $v c num + set num [expr {($num + 0x100) % 0x100}] + + set text . + if {$num > 31} {set text $v} + + if {($i % 16) == 0} { + if {$address != ""} { + eval [linsert $cmd end $level [format "%4s %-48s |%s|" $address $hexnums $ascii]] + set address "" + set hexnums "" + set ascii "" + } + append address [format "%04d" $i] + } + append hexnums "$hex " + append ascii $text + } + if {$address != ""} { + eval [linsert $cmd end $level [format "%4s %-48s |%s|" $address $hexnums $ascii]] + } + eval [linsert $cmd end $level ""] + return +} + +# log::log -- +# +# Log a message according to the specifications for commands, +# channels and suppression. In other words: The command will do +# nothing if the specified level is suppressed. If it is not +# suppressed the actual logging is delegated to the specified +# command. If there is no command specified for the level the +# message won't be logged. The standard command ::log::Puts will +# write the message to the channel specified for the given +# level. If no channel is specified for the level the message +# won't be logged. Unique abbreviations of level names are +# allowed. Errors in the actual logging command are *not* +# catched, but propagated to the caller, as they may indicate +# misconfigurations of the log facility or errors in the callers +# code itself. +# +# Arguments: +# level The level of the message. +# text The message to log. +# +# Side Effects: +# See above. +# +# Results: +# None. + +proc ::log::log {level text} { + variable cmdMap + + if {[lvIsSuppressed $level]} { + # Ignore messages for suppressed levels. + return + } + + set level [lv2longform $level] + + set cmd $cmdMap($level) + if {$cmd == {}} { + # Ignore messages for levels without a command + return + } + + # Delegate actual logging to the command. + # Handle multi-line messages correctly. + + foreach line [split $text \n] { + eval [linsert $cmd end $level $line] + } + return +} + +# log::logMsg -- +# +# Convenience wrapper around ::log::log. Equivalent to +# '::log::log info text'. +# +# Arguments: +# text The message to log. +# +# Side Effects: +# See ::log::log. +# +# Results: +# None. + +proc ::log::logMsg {text} { + log info $text +} + +# log::logError -- +# +# Convenience wrapper around ::log::log. Equivalent to +# '::log::log error text'. +# +# Arguments: +# text The message to log. +# +# Side Effects: +# See ::log::log. +# +# Results: +# None. + +proc ::log::logError {text} { + log error $text +} + + +# log::Puts -- +# +# Standard log command, writing messages and levels to +# user-specified channels. Assumes that the supression checks +# were done by the caller. Expects full level names, +# abbreviations are *not allowed*. +# +# Arguments: +# level The level of the message. +# text The message to log. +# +# Side Effects: +# Writes into channels. +# +# Results: +# None. + +proc ::log::Puts {level text} { + variable channelMap + variable fill + + set chan $channelMap($level) + if {$chan == {}} { + # Ignore levels without channel. + return + } + + puts $chan "$level$fill($level) $text" + return +} + +# ### ### ### ######### ######### ######### +## Initialization code. Disable logging for the lower levels by +## default. + +## log::lvSuppressLE emergency +log::lvSuppressLE warning diff --git a/lib/log/logger.tcl b/lib/log/logger.tcl new file mode 100644 index 0000000..7e69481 --- /dev/null +++ b/lib/log/logger.tcl @@ -0,0 +1,1206 @@ +# logger.tcl -- +# +# Tcl implementation of a general logging facility. +# +# Copyright (c) 2003 by David N. Welton +# Copyright (c) 2004-2007 by Michael Schlenker +# Copyright (c) 2006 by Andreas Kupries +# +# See the file license.terms. + +# The logger package provides an 'object oriented' log facility that +# lets you have trees of services, that inherit from one another. +# This is accomplished through the use of Tcl namespaces. + + +package require Tcl 8.2 +package provide logger 0.8 + +namespace eval ::logger { + namespace eval tree {} + namespace export init enable disable services servicecmd import + + # The active services. + variable services {} + + # The log 'levels'. + variable levels [list debug info notice warn error critical alert emergency] + + # The default global log level used for new logging services + variable enabled "debug" + + # Tcl return codes (in numeric order) + variable RETURN_CODES [list "ok" "error" "return" "break" "continue"] +} + +# ::logger::_nsExists -- +# +# Workaround for missing namespace exists in Tcl 8.2 and 8.3. +# + +if {[package vcompare [package provide Tcl] 8.4] < 0} { + proc ::logger::_nsExists {ns} { + expr {![catch {namespace parent $ns}]} + } +} else { + proc ::logger::_nsExists {ns} { + namespace exists $ns + } +} + +# ::logger::_cmdPrefixExists -- +# +# Utility function to check if a given callback prefix exists, +# this should catch all oddities in prefix names, including spaces, +# glob patterns, non normalized namespaces etc. +# +# Arguments: +# prefix - The command prefix to check +# +# Results: +# 1 or 0 for yes or no +# +proc ::logger::_cmdPrefixExists {prefix} { + set cmd [lindex $prefix 0] + set full [namespace eval :: namespace which [list $cmd]] + if {[string equal $full ""]} {return 0} else {return 1} + # normalize namespaces + set ns [namespace qualifiers $cmd] + set cmd ${ns}::[namespace tail $cmd] + set matches [::info commands ${ns}::*] + if {[lsearch -exact $matches $cmd] != -1} {return 1} + return 0 +} + +# ::logger::walk -- +# +# Walk namespaces, starting in 'start', and evaluate 'code' in +# them. +# +# Arguments: +# start - namespace to start in. +# code - code to execute in namespaces walked. +# +# Side Effects: +# Side effects of code executed. +# +# Results: +# None. + +proc ::logger::walk { start code } { + set children [namespace children $start] + foreach c $children { + logger::walk $c $code + namespace eval $c $code + } +} + +proc ::logger::init {service} { + variable levels + variable services + variable enabled + + # We create a 'tree' namespace to house all the services, so + # they are in a 'safe' namespace sandbox, and won't overwrite + # any commands. + namespace eval tree::${service} { + variable service + variable levels + variable oldname + variable enabled + } + + lappend services $service + + set [namespace current]::tree::${service}::service $service + set [namespace current]::tree::${service}::levels $levels + set [namespace current]::tree::${service}::oldname $service + set [namespace current]::tree::${service}::enabled $enabled + + namespace eval tree::${service} { + # Callback to use when the service in question is shut down. + variable delcallback [namespace current]::no-op + + # Callback when the loglevel is changed + variable levelchangecallback [namespace current]::no-op + + # State variable to decide when to call levelcallback + variable inSetLevel 0 + + # The currently configured levelcommands + variable lvlcmds + array set lvlcmds {} + + # List of procedures registered via the trace command + variable traceList "" + + # Flag indicating whether or not tracing is currently enabled + variable tracingEnabled 0 + + # We use this to disable a service completely. In Tcl 8.4 + # or greater, by using this, disabled log calls are a + # no-op! + + proc no-op args {} + + + proc stdoutcmd {level text} { + variable service + puts "\[[clock format [clock seconds]]\] \[$service\] \[$level\] \'$text\'" + } + + proc stderrcmd {level text} { + variable service + puts stderr "\[[clock format [clock seconds]]\] \[$service\] \[$level\] \'$text\'" + } + + + # setlevel -- + # + # This command differs from enable and disable in that + # it disables all the levels below that selected, and + # then enables all levels above it, which enable/disable + # do not do. + # + # Arguments: + # lv - the level, as defined in $levels. + # + # Side Effects: + # Runs disable for the level, and then enable, in order + # to ensure that all levels are set correctly. + # + # Results: + # None. + + + proc setlevel {lv} { + variable inSetLevel 1 + set oldlvl [currentloglevel] + + # do not allow enable and disable to do recursion + if {[catch { + disable $lv 0 + set newlvl [enable $lv 0] + } msg] == 1} { + return -code error -errorcode $::errorCode $msg + } + # do the recursion here + logger::walk [namespace current] [list setlevel $lv] + + set inSetLevel 0 + lvlchangewrapper $oldlvl $newlvl + return + } + + # enable -- + # + # Enable a particular 'level', and above, for the + # service, and its 'children'. + # + # Arguments: + # lv - the level, as defined in $levels. + # + # Side Effects: + # Enables logging for the particular level, and all + # above it (those more important). It also walks + # through all services that are 'children' and enables + # them at the same level or above. + # + # Results: + # None. + + proc enable {lv {recursion 1}} { + variable levels + set lvnum [lsearch -exact $levels $lv] + if { $lvnum == -1 } { + return -code error "Invalid level '$lv' - levels are $levels" + } + + variable enabled + set newlevel $enabled + set elnum [lsearch -exact $levels $enabled] + if {($elnum == -1) || ($elnum > $lvnum)} { + set newlevel $lv + } + + variable service + while { $lvnum < [llength $levels] } { + interp alias {} [namespace current]::[lindex $levels $lvnum] \ + {} [namespace current]::[lindex $levels $lvnum]cmd + incr lvnum + } + + if {$recursion} { + logger::walk [namespace current] [list enable $lv] + } + lvlchangewrapper $enabled $newlevel + set enabled $newlevel + } + + # disable -- + # + # Disable a particular 'level', and below, for the + # service, and its 'children'. + # + # Arguments: + # lv - the level, as defined in $levels. + # + # Side Effects: + # Disables logging for the particular level, and all + # below it (those less important). It also walks + # through all services that are 'children' and disables + # them at the same level or below. + # + # Results: + # None. + + proc disable {lv {recursion 1}} { + variable levels + set lvnum [lsearch -exact $levels $lv] + if { $lvnum == -1 } { + return -code error "Invalid level '$lv' - levels are $levels" + } + + variable enabled + set newlevel $enabled + set elnum [lsearch -exact $levels $enabled] + if {($elnum > -1) && ($elnum <= $lvnum)} { + if {$lvnum+1 >= [llength $levels]} { + set newlevel "none" + } else { + set newlevel [lindex $levels [expr {$lvnum+1}]] + } + } + + while { $lvnum >= 0 } { + + interp alias {} [namespace current]::[lindex $levels $lvnum] {} \ + [namespace current]::no-op + incr lvnum -1 + } + if {$recursion} { + logger::walk [namespace current] [list disable $lv] + } + lvlchangewrapper $enabled $newlevel + set enabled $newlevel + } + + # currentloglevel -- + # + # Get the currently enabled log level for this service. + # + # Arguments: + # none + # + # Side Effects: + # none + # + # Results: + # current log level + # + + proc currentloglevel {} { + variable enabled + return $enabled + } + + # lvlchangeproc -- + # + # Set or introspect a callback for when the logger instance + # changes its loglevel. + # + # Arguments: + # cmd - the Tcl command to call, it is called with two parameters, old and new log level. + # or none for introspection + # + # Side Effects: + # None. + # + # Results: + # If no arguments are given return the current callback cmd. + + proc lvlchangeproc {args} { + variable levelchangecallback + + switch -exact -- [llength [::info level 0]] { + 1 {return $levelchangecallback} + 2 { + if {[::logger::_cmdPrefixExists [lindex $args 0]]} { + set levelchangecallback [lindex $args 0] + } else { + return -code error "Invalid cmd '[lindex $args 0]' - does not exist" + } + } + default { + return -code error "Wrong # of arguments. Usage: \${log}::lvlchangeproc ?cmd?" + } + } + } + + proc lvlchangewrapper {old new} { + variable inSetLevel + + # we are called after disable and enable are finished + if {$inSetLevel} {return} + + # no action if level does not change + if {[string equal $old $new]} {return} + + variable levelchangecallback + # no action if levelchangecallback isn't a valid command + if {[::logger::_cmdPrefixExists $levelchangecallback]} { + catch { + uplevel \#0 [linsert $levelchangecallback end $old $new] + } + } + } + + # logproc -- + # + # Command used to create a procedure that is executed to + # perform the logging. This could write to disk, out to + # the network, or something else. + # If two arguments are given, use an existing command. + # If three arguments are given, create a proc. + # + # Arguments: + # lv - the level to log, which must be one of $levels. + # args - either zero, one or two arguments. + # if zero this returns the current command registered + # if one, this is a cmd name that is called for this level + # if two, these are an argument and proc body + # + # Side Effects: + # Creates a logging command to take care of the details + # of logging an event. + # + # Results: + # If called with zero length args, returns the name of the currently + # configured logging procedure. + # + # + + proc logproc {lv args} { + variable levels + variable lvlcmds + + set lvnum [lsearch -exact $levels $lv] + if { ($lvnum == -1) && ($lv != "trace") } { + return -code error "Invalid level '$lv' - levels are $levels" + } + switch -exact -- [llength $args] { + 0 { + return $lvlcmds($lv) + } + 1 { + set cmd [lindex $args 0] + if {[string equal "[namespace current]::${lv}cmd" $cmd]} {return} + if {[llength [::info commands $cmd]]} { + proc ${lv}cmd {args} "uplevel 1 \[list $cmd \[lindex \$args end\]\]" + } else { + return -code error "Invalid cmd '$cmd' - does not exist" + } + set lvlcmds($lv) $cmd + } + 2 { + foreach {arg body} $args {break} + proc ${lv}cmd {args} "_setservicename \$args; + set val \[${lv}customcmd \[lindex \$args end\]\] ; + _restoreservice; set val" + proc ${lv}customcmd $arg $body + set lvlcmds($lv) [namespace current]::${lv}customcmd + } + default { + return -code error "Usage: \${log}::logproc level ?cmd?\nor \${log}::logproc level argname body" + } + } + } + + + # delproc -- + # + # Set or introspect a callback for when the logger instance + # is deleted. + # + # Arguments: + # cmd - the Tcl command to call. + # or none for introspection + # + # Side Effects: + # None. + # + # Results: + # If no arguments are given return the current callback cmd. + + proc delproc {args} { + variable delcallback + + switch -exact -- [llength [::info level 0]] { + 1 {return $delcallback} + 2 { if {[::logger::_cmdPrefixExists [lindex $args 0]]} { + set delcallback [lindex $args 0] + } else { + return -code error "Invalid cmd '[lindex $args 0]' - does not exist" + } + } + default { + return -code error "Wrong # of arguments. Usage: \${log}::delproc ?cmd?" + } + } + } + + + # delete -- + # + # Delete the namespace and its children. + + proc delete {} { + variable delcallback + variable service + + logger::walk [namespace current] delete + if {[::logger::_cmdPrefixExists $delcallback]} { + uplevel \#0 [lrange $delcallback 0 end] + } + # clean up the global services list + set idx [lsearch -exact [logger::services] $service] + if {$idx !=-1} { + set ::logger::services [lreplace [logger::services] $idx $idx] + } + + namespace delete [namespace current] + + } + + # services -- + # + # Return all child services + + proc services {} { + variable service + + set children [list] + foreach srv [logger::services] { + if {[string match "${service}::*" $srv]} { + lappend children $srv + } + } + return $children + } + + # servicename -- + # + # Return the name of the service + + proc servicename {} { + variable service + return $service + } + + proc _setservicename {arg} { + variable service + variable oldname + if {[llength $arg] <= 1} { + return + } else { + set oldname $service + set service [lindex $arg end-1] + } + } + + proc _restoreservice {} { + variable service + variable oldname + set service $oldname + return + } + + proc trace { action args } { + variable service + + # Allow other boolean values (true, false, yes, no, 0, 1) to be used + # as synonymns for "on" and "off". + + if {[string is boolean $action]} { + set xaction [expr {($action && 1) ? "on" : "off"}] + } else { + set xaction $action + } + + # Check for required arguments for actions/subcommands and dispatch + # to the appropriate procedure. + + switch -- $xaction { + "status" { + return [uplevel 1 [list logger::_trace_status $service $args]] + } + "on" { + if {[llength $args]} { + return -code error "wrong # args: should be \"trace on\"" + } + return [logger::_trace_on $service] + } + "off" { + if {[llength $args]} { + return -code error "wrong # args: should be \"trace off\"" + } + return [logger::_trace_off $service] + } + "add" { + if {![llength $args]} { + return -code error \ + "wrong # args: should be \"trace add ?-ns? ...\"" + } + return [uplevel 1 [list ::logger::_trace_add $service $args]] + } + "remove" { + if {![llength $args]} { + return -code error \ + "wrong # args: should be \"trace remove ?-ns? ...\"" + } + return [uplevel 1 [list ::logger::_trace_remove $service $args]] + } + + default { + return -code error \ + "Invalid action \"$action\": must be status, add, remove,\ + on, or off" + } + } + } + + # Walk the parent service namespaces to see first, if they + # exist, and if any are enabled, and then, as a + # consequence, enable this one + # too. + + enable $enabled + variable parent [namespace parent] + while {[string compare $parent "::logger::tree"]} { + # If the 'enabled' variable doesn't exist, create the + # whole thing. + if { ! [::info exists ${parent}::enabled] } { + + logger::init [string range $parent 16 end] + } + set enabled [set ${parent}::enabled] + enable $enabled + set parent [namespace parent $parent] + } + } + + # Now create the commands for different levels. + + namespace eval tree::${service} { + set parent [namespace parent] + + # We 'inherit' the commands from the parents. This + # means that, if you want to share the same methods with + # children, they should be instantiated after the parent's + # methods have been defined. + if {[string compare $parent "::logger::tree"]} { + foreach lvl [::logger::levels] { + # OPTIMIZE: do not allow multiple aliases in the hierarchy + # they can always be replaced by more efficient + # direct aliases to the target procs. + interp alias {} [namespace current]::${lvl}cmd {} ${parent}::${lvl}cmd $service + } + # inherit the starting loglevel of the parent service + setlevel [${parent}::currentloglevel] + + } else { + foreach lvl [concat [::logger::levels] "trace"] { + proc ${lvl}cmd {args} "_setservicename \$args ; + set val \[stdoutcmd $lvl \[lindex \$args end\]\] ; + _restoreservice; set val" + set lvlcmds($lvl) [namespace current]::${lvl}cmd + } + } + } + + + return ::logger::tree::${service} +} + +# ::logger::services -- +# +# Returns a list of all active services. +# +# Arguments: +# None. +# +# Side Effects: +# None. +# +# Results: +# List of active services. + +proc ::logger::services {} { + variable services + return $services +} + +# ::logger::enable -- +# +# Global enable for a certain level. NOTE - this implementation +# isn't terribly effective at the moment, because it might hit +# children before their parents, who will then walk down the +# tree attempting to disable the children again. +# +# Arguments: +# lv - level above which to enable logging. +# +# Side Effects: +# Enables logging in a given level, and all higher levels. +# +# Results: +# None. + +proc ::logger::enable {lv} { + variable services + if {[catch { + foreach sv $services { + ::logger::tree::${sv}::enable $lv + } + } msg] == 1} { + return -code error -errorcode $::errorCode $msg + } +} + +proc ::logger::disable {lv} { + variable services + if {[catch { + foreach sv $services { + ::logger::tree::${sv}::disable $lv + } + } msg] == 1} { + return -code error -errorcode $::errorCode $msg + } +} + +proc ::logger::setlevel {lv} { + variable services + variable enabled + variable levels + if {[lsearch -exact $levels $lv] == -1} { + return -code error "Invalid level '$lv' - levels are $levels" + } + set enabled $lv + if {[catch { + foreach sv $services { + ::logger::tree::${sv}::setlevel $lv + } + } msg] == 1} { + return -code error -errorcode $::errorCode $msg + } +} + +# ::logger::levels -- +# +# Introspect the available log levels. Provided so a caller does +# not need to know implementation details or code the list +# himself. +# +# Arguments: +# None. +# +# Side Effects: +# None. +# +# Results: +# levels - The list of valid log levels accepted by enable and disable + +proc ::logger::levels {} { + variable levels + return $levels +} + +# ::logger::servicecmd -- +# +# Get the command token for a given service name. +# +# Arguments: +# service - name of the service. +# +# Side Effects: +# none +# +# Results: +# log - namespace token for this service + +proc ::logger::servicecmd {service} { + variable services + if {[lsearch -exact $services $service] == -1} { + return -code error "Service \"$service\" does not exist." + } + return "::logger::tree::${service}" +} + +# ::logger::import -- +# +# Import the logging commands. +# +# Arguments: +# service - name of the service. +# +# Side Effects: +# creates aliases in the target namespace +# +# Results: +# none + +proc ::logger::import {args} { + variable services + + if {[llength $args] == 0 || [llength $args] > 7} { + return -code error "Wrong # of arguments: \"logger::import ?-all?\ + ?-force?\ + ?-prefix prefix? ?-namespace namespace? service\"" + } + + # process options + # + set import_all 0 + set force 0 + set prefix "" + set ns [uplevel 1 namespace current] + while {[llength $args] > 1} { + set opt [lindex $args 0] + set args [lrange $args 1 end] + switch -exact -- $opt { + -all { set import_all 1} + -prefix { set prefix [lindex $args 0] + set args [lrange $args 1 end] + } + -namespace { + set ns [lindex $args 0] + set args [lrange $args 1 end] + } + -force { + set force 1 + } + default { + return -code error "Unknown argument: \"$opt\" :\nUsage:\ + \"logger::import ?-all? ?-force?\ + ?-prefix prefix? ?-namespace namespace? service\"" + } + } + } + + # + # build the list of commands to import + # + + set cmds [logger::levels] + lappend cmds "trace" + if {$import_all} { + lappend cmds setlevel enable disable logproc delproc services + lappend cmds servicename currentloglevel delete + } + + # + # check the service argument + # + + set service [lindex $args 0] + if {[lsearch -exact $services $service] == -1} { + return -code error "Service \"$service\" does not exist." + } + + # + # setup the namespace for the import + # + + set sourcens [logger::servicecmd $service] + set localns [uplevel 1 namespace current] + + if {[string match ::* $ns]} { + set importns $ns + } else { + set importns ${localns}::$ns + } + + # fake namespace exists for Tcl 8.2 - 8.3 + if {![_nsExists $importns]} { + namespace eval $importns {} + } + + + # + # prepare the import + # + + set imports "" + foreach cmd $cmds { + set cmdname ${importns}::${prefix}$cmd + set collision [llength [info commands $cmdname]] + if {$collision && !$force} { + return -code error "can't import command \"$cmdname\": already exists" + } + lappend imports ${importns}::${prefix}$cmd ${sourcens}::${cmd} + } + + # + # and execute the aliasing after checking all is well + # + + foreach {target source} $imports { + proc $target {args} "uplevel 1 \[linsert \$args 0 $source \]" + } +} + +# ::logger::initNamespace -- +# +# Creates a logger for the specified namespace and makes the log +# commands available to said namespace as well. Allows the initial +# setting of a default log level. +# +# Arguments: +# ns - Namespace to initialize, is also the service name, modulo a ::-prefix +# level - Initial log level, optional, defaults to 'warn'. +# +# Side Effects: +# creates aliases in the target namespace +# +# Results: +# none + +proc ::logger::initNamespace {ns {level warn}} { + set service [string trimleft $ns :] + namespace eval $ns [list ::logger::init $service] + namespace eval $ns [list ::logger::import -force -all -namespace log $service] + namespace eval $ns [list log::setlevel $level] + return +} + +# This procedure handles the "logger::trace status" command. Given no +# arguments, returns a list of all procedures that have been registered +# via "logger::trace add". Given one or more procedure names, it will +# return 1 if all were registered, or 0 if any were not. + +proc ::logger::_trace_status { service procList } { + upvar #0 ::logger::tree::${service}::traceList traceList + + # If no procedure names were given, just return the registered list + + if {![llength $procList]} { + return $traceList + } + + # Get caller's namespace for qualifying unqualified procedure names + + set caller_ns [uplevel 1 namespace current] + set caller_ns [string trimright $caller_ns ":"] + + # Search for any specified proc names that are *not* registered + + foreach procName $procList { + # Make sure the procedure namespace is qualified + + if {![string match "::*" $procName]} { + set procName ${caller_ns}::$procName + } + + # Check if the procedure has been registered for tracing + + if {[lsearch -exact $traceList $procName] == -1} { + return 0 + } + } + + return 1 +} + +# This procedure handles the "logger::trace on" command. If tracing +# is turned off, it will enable Tcl trace handlers for all of the procedures +# registered via "logger::trace add". Does nothing if tracing is already +# turned on. + +proc ::logger::_trace_on { service } { + set tcl_version [package provide Tcl] + + if {[package vcompare $tcl_version "8.4"] < 0} { + return -code error \ + "execution tracing is not available in Tcl $tcl_version" + } + + namespace eval ::logger::tree::${service} { + if {!$tracingEnabled} { + set tracingEnabled 1 + ::logger::_enable_traces $service $traceList + } + } + + return 1 +} + +# This procedure handles the "logger::trace off" command. If tracing +# is turned on, it will disable Tcl trace handlers for all of the procedures +# registered via "logger::trace add", leaving them in the list so they +# tracing on all of them can be enabled again with "logger::trace on". +# Does nothing if tracing is already turned off. + +proc ::logger::_trace_off { service } { + namespace eval ::logger::tree::${service} { + if {$tracingEnabled} { + ::logger::_disable_traces $service $traceList + set tracingEnabled 0 + } + } + + return 1 +} + +# This procedure is used by the logger::trace add and remove commands to +# process the arguments in a common fashion. If the -ns switch is given +# first, this procedure will return a list of all existing procedures in +# all of the namespaces given in remaining arguments. Otherwise, each +# argument is taken to be either a pattern for a glob-style search of +# procedure names or, failing that, a namespace, in which case this +# procedure returns a list of all the procedures matching the given +# pattern (or all in the named namespace, if no procedures match). + +proc ::logger::_trace_get_proclist { inputList } { + set procList "" + + if {[string equal [lindex $inputList 0] "-ns"]} { + # Verify that at least one target namespace was supplied + + set inputList [lrange $inputList 1 end] + if {![llength $inputList]} { + return -code error "Must specify at least one namespace target" + } + + # Rebuild the argument list to contain namespace procedures + + foreach namespace $inputList { + # Don't allow tracing of the logger (or child) namespaces + + if {![string match "::logger::*" $namespace]} { + set nsProcList [::info procs ${namespace}::*] + set procList [concat $procList $nsProcList] + } + } + } else { + # Search for procs or namespaces matching each of the specified + # patterns. + + foreach pattern $inputList { + set matches [uplevel 1 ::info proc $pattern] + + if {![llength $matches]} { + if {[uplevel 1 namespace exists $pattern]} { + set matches [::info procs ${pattern}::*] + } + + # Matched procs will be qualified due to above pattern + + set procList [concat $procList $matches] + } elseif {[string match "::*" $pattern]} { + # Patterns were pre-qualified - add them directly + + set procList [concat $procList $matches] + } else { + # Qualify each proc with the namespace it was in + + set ns [uplevel 1 namespace current] + if {$ns == "::"} { + set ns "" + } + foreach proc $matches { + lappend procList ${ns}::$proc + } + } + } + } + + return $procList +} + +# This procedure handles the "logger::trace add" command. If the tracing +# feature is enabled, it will enable the Tcl entry and leave trace handlers +# for each procedure specified that isn't already being traced. Each +# procedure is added to the list of procedures that the logger trace feature +# should log when tracing is enabled. + +proc ::logger::_trace_add { service procList } { + upvar #0 ::logger::tree::${service}::traceList traceList + + # Handle -ns switch and glob search patterns for procedure names + + set procList [uplevel 1 [list logger::_trace_get_proclist $procList]] + + # Enable tracing for each procedure that has not previously been + # specified via logger::trace add. If tracing is off, this will just + # store the name of the procedure for later when tracing is turned on. + + foreach procName $procList { + if {[lsearch -exact $traceList $procName] == -1} { + lappend traceList $procName + ::logger::_enable_traces $service [list $procName] + } + } +} + +# This procedure handles the "logger::trace remove" command. If the tracing +# feature is enabled, it will remove the Tcl entry and leave trace handlers +# for each procedure specified. Each procedure is removed from the list +# of procedures that the logger trace feature should log when tracing is +# enabled. + +proc ::logger::_trace_remove { service procList } { + upvar #0 ::logger::tree::${service}::traceList traceList + + # Handle -ns switch and glob search patterns for procedure names + + set procList [uplevel 1 [list logger::_trace_get_proclist $procList]] + + # Disable tracing for each proc that previously had been specified + # via logger::trace add. If tracing is off, this will just + # remove the name of the procedure from the trace list so that it + # will be excluded when tracing is turned on. + + foreach procName $procList { + set index [lsearch -exact $traceList $procName] + if {$index != -1} { + set traceList [lreplace $traceList $index $index] + ::logger::_disable_traces $service [list $procName] + } + } +} + +# This procedure enables Tcl trace handlers for all procedures specified. +# It is used both to enable Tcl's tracing for a single procedure when +# removed via "logger::trace add", as well as to enable all traces +# via "logger::trace on". + +proc ::logger::_enable_traces { service procList } { + upvar #0 ::logger::tree::${service}::tracingEnabled tracingEnabled + + if {$tracingEnabled} { + foreach procName $procList { + ::trace add execution $procName enter \ + [list ::logger::_trace_enter $service] + ::trace add execution $procName leave \ + [list ::logger::_trace_leave $service] + } + } +} + +# This procedure disables Tcl trace handlers for all procedures specified. +# It is used both to disable Tcl's tracing for a single procedure when +# removed via "logger::trace remove", as well as to disable all traces +# via "logger::trace off". + +proc ::logger::_disable_traces { service procList } { + upvar #0 ::logger::tree::${service}::tracingEnabled tracingEnabled + + if {$tracingEnabled} { + foreach procName $procList { + ::trace remove execution $procName enter \ + [list ::logger::_trace_enter $service] + ::trace remove execution $procName leave \ + [list ::logger::_trace_leave $service] + } + } +} + +######################################################################## +# Trace Handlers +######################################################################## + +# This procedure is invoked upon entry into a procedure being traced +# via "logger::trace add" when tracing is enabled via "logger::trace on" +# to log information about how the procedure was called. + +proc ::logger::_trace_enter { service cmd op } { + # Parse the command + set procName [uplevel 1 namespace origin [lindex $cmd 0]] + set args [lrange $cmd 1 end] + + # Display the message prefix + set callerLvl [expr {[::info level] - 1}] + set calledLvl [::info level] + + lappend message "proc" $procName + lappend message "level" $calledLvl + lappend message "script" [uplevel ::info script] + + # Display the caller information + set caller "" + if {$callerLvl >= 1} { + # Display the name of the caller proc w/prepended namespace + catch { + set callerProcName [lindex [::info level $callerLvl] 0] + set caller [uplevel 2 namespace origin $callerProcName] + } + } + + lappend message "caller" $caller + + # Display the argument names and values + set argSpec [uplevel 1 ::info args $procName] + set argList "" + if {[llength $argSpec]} { + foreach argName $argSpec { + lappend argList $argName + + if {$argName == "args"} { + lappend argList $args + break + } else { + lappend argList [lindex $args 0] + set args [lrange $args 1 end] + } + } + } + + lappend message "procargs" $argList + set message [list $op $message] + + ::logger::tree::${service}::tracecmd $message +} + +# This procedure is invoked upon leaving into a procedure being traced +# via "logger::trace add" when tracing is enabled via "logger::trace on" +# to log information about the result of the procedure call. + +proc ::logger::_trace_leave { service cmd status rc op } { + variable RETURN_CODES + + # Parse the command + set procName [uplevel 1 namespace origin [lindex $cmd 0]] + + # Gather the caller information + set callerLvl [expr {[::info level] - 1}] + set calledLvl [::info level] + + lappend message "proc" $procName "level" $calledLvl + lappend message "script" [uplevel ::info script] + + # Get the name of the proc being returned to w/prepended namespace + set caller "" + catch { + set callerProcName [lindex [::info level $callerLvl] 0] + set caller [uplevel 2 namespace origin $callerProcName] + } + + lappend message "caller" $caller + + # Convert the return code from numeric to verbal + + if {$status < [llength $RETURN_CODES]} { + set status [lindex $RETURN_CODES $status] + } + + lappend message "status" $status + lappend message "result" $rc + + # Display the leave message + + set message [list $op $message] + ::logger::tree::${service}::tracecmd $message + + return 1 +} + diff --git a/lib/log/loggerAppender.tcl b/lib/log/loggerAppender.tcl new file mode 100644 index 0000000..6bbd24a --- /dev/null +++ b/lib/log/loggerAppender.tcl @@ -0,0 +1,449 @@ +##Library Header +# +# $Id: loggerAppender.tcl,v 1.4 2007/02/08 22:09:54 mic42 Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::appender +# +# Purpose: +# collection of appenders for tcllib logger +# +# Author: +# Aamer Akhter / aakhter@cisco.com +# +# Support Alias: +# aakhter@cisco.com +# +# Usage: +# package require logger::appender +# +# Description: +# set of logger templates +# +# Requirements: +# package require logger +# package require md5 +# +# Variables: +# namespace ::loggerExtension:: +# id: CVS ID: keyword extraction +# version: current version of package +# packageDir: directory where package is located +# log: instance log +# +# Notes: +# 1. +# +# Keywords: +# +# +# Category: +# +# +# End of Header + +package require md5 + +namespace eval ::logger::appender { + variable fgcolor + array set fgcolor { + red {31m} + red-bold {1;31m} + black {m} + blue {1m} + green {32m} + yellow {33m} + cyan {36m} + } + + variable levelToColor + array set levelToColor { + debug cyan + info blue + notice black + warn red + error red + critical red-bold + alert red-bold + emergency red-bold + } +} + + + +##Procedure Header +# $Id: loggerAppender.tcl,v 1.4 2007/02/08 22:09:54 mic42 Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::appender::console +# +# Purpose: +# +# +# Synopsis: +# ::logger::appender::console -level -service [options] +# +# Arguments: +# -level +# name of level to fill in as 'priority' in log proc +# -service +# name of service to fill in as 'category' in log proc +# -appenderArgs +# any additional args in list form +# -conversionPattern +# log pattern to use (see genLogProc) +# -procName +# explicitly set the proc name +# -procNameVar +# name of variable to set in the calling context +# variable has name of proc +# +# +# Return Values: +# a runnable command +# +# Description: +# +# +# Examples: +# +# +# Notes: +# 1. +# +# End of Procedure Header + + +proc ::logger::appender::console {args} { + set usage {console + ?-level level? + ?-service service? + ?-appenderArgs appenderArgs? + } + set bargs $args + set conversionPattern {\[%d\] \[%c\] \[%M\] \[%p\] %m} + while {[llength $args] > 1} { + set opt [lindex $args 0] + set args [lrange $args 1 end] + switch -exact -- $opt { + -level { set level [lindex $args 0] + set args [lrange $args 1 end] + } + -service { set service [lindex $args 0] + set args [lrange $args 1 end] + } + -appenderArgs { + set appenderArgs [lindex $args 0] + set args [lrange $args 1 end] + set args [concat $args $appenderArgs] + } + -conversionPattern { + set conversionPattern [lindex $args 0] + set args [lrange $args 1 end] + } + -procName { + set procName [lindex $args 0] + set args [lrange $args 1 end] + } + -procNameVar { + set procNameVar [lindex $args 0] + set args [lrange $args 1 end] + } + default { + return -code error [msgcat::mc "Unknown argument: \"%s\" :\nUsage:\ + %s" $opt $usage] + } + } + } + if {![info exists procName]} { + set procName [genProcName $bargs] + } + if {[info exists procNameVar]} { + upvar $procNameVar myProcNameVar + } + set procText \ + [ ::logger::utils::createLogProc \ + -procName $procName \ + -conversionPattern $conversionPattern \ + -category $service \ + -priority $level ] + set myProcNameVar $procName + return $procText +} + + + +##Procedure Header +# $Id: loggerAppender.tcl,v 1.4 2007/02/08 22:09:54 mic42 Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::appender::colorConsole +# +# Purpose: +# +# +# Synopsis: +# ::logger::appender::console -level -service [options] +# +# Arguments: +# -level +# name of level to fill in as 'priority' in log proc +# -service +# name of service to fill in as 'category' in log proc +# -appenderArgs +# any additional args in list form +# -conversionPattern +# log pattern to use (see genLogProc) +# -procName +# explicitly set the proc name +# -procNameVar +# name of variable to set in the calling context +# variable has name of proc +# +# +# Return Values: +# a runnable command +# +# Description: +# provides colorized logs +# +# Examples: +# +# +# Notes: +# 1. +# +# End of Procedure Header + + +proc ::logger::appender::colorConsole {args} { + variable fgcolor + set usage {console + ?-level level? + ?-service service? + ?-appenderArgs appenderArgs? + } + set bargs $args + set conversionPattern {\[%d\] \[%c\] \[%M\] \[%p\] %m} + upvar 0 ::logger::appender::levelToColor colorMap + while {[llength $args] > 1} { + set opt [lindex $args 0] + set args [lrange $args 1 end] + switch -exact -- $opt { + -level { set level [lindex $args 0] + set args [lrange $args 1 end] + } + -service { set service [lindex $args 0] + set args [lrange $args 1 end] + } + -appenderArgs { + set appenderArgs [lindex $args 0] + set args [lrange $args 1 end] + set args [concat $args $appenderArgs] + } + -conversionPattern { + set conversionPattern [lindex $args 0] + set args [lrange $args 1 end] + } + -procName { + set procName [lindex $args 0] + set args [lrange $args 1 end] + } + -procNameVar { + set procNameVar [lindex $args 0] + set args [lrange $args 1 end] + } + default { + return -code error [msgcat::mc "Unknown argument: \"%s\" :\nUsage:\ + %s" $opt $usage] + } + } + } + if {![info exists procName]} { + set procName [genProcName $bargs] + } + upvar $procNameVar myProcNameVar + if {[info exists level]} { + #apply color + set colorCode $colorMap($level) + append newCPattern {\033\[} $fgcolor($colorCode) $conversionPattern {\033\[0m} + set conversionPattern $newCPattern + } + set procText \ + [ ::logger::utils::createLogProc \ + -procName $procName \ + -conversionPattern $conversionPattern \ + -category $service \ + -priority $level ] + set myProcNameVar $procName + return $procText +} + +##Procedure Header +# $Id: loggerAppender.tcl,v 1.4 2007/02/08 22:09:54 mic42 Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::appender::fileAppend +# +# Purpose: +# +# +# Synopsis: +# ::logger::appender::fileAppend -level -service -outputChannel [options] +# +# Arguments: +# -level +# name of level to fill in as 'priority' in log proc +# -service +# name of service to fill in as 'category' in log proc +# -appenderArgs +# any additional args in list form +# -conversionPattern +# log pattern to use (see genLogProc) +# -procName +# explicitly set the proc name +# -procNameVar +# name of variable to set in the calling context +# variable has name of proc +# -outputChannel +# name of output channel (eg stdout, file handle) +# +# +# Return Values: +# a runnable command +# +# Description: +# +# +# Examples: +# +# +# Notes: +# 1. +# +# End of Procedure Header + + +proc ::logger::appender::fileAppend {args} { + set usage {console + ?-level level? + ?-service service? + ?-outputChannel channel? + ?-appenderArgs appenderArgs? + } + set bargs $args + set conversionPattern {\[%d\] \[%c\] \[%M\] \[%p\] %m} + while {[llength $args] > 1} { + set opt [lindex $args 0] + set args [lrange $args 1 end] + switch -exact -- $opt { + -level { set level [lindex $args 0] + set args [lrange $args 1 end] + } + -service { set service [lindex $args 0] + set args [lrange $args 1 end] + } + -appenderArgs { + set appenderArgs [lindex $args 0] + set args [lrange $args 1 end] + set args [concat $args $appenderArgs] + } + -conversionPattern { + set conversionPattern [lindex $args 0] + set args [lrange $args 1 end] + } + -procName { + set procName [lindex $args 0] + set args [lrange $args 1 end] + } + -procNameVar { + set procNameVar [lindex $args 0] + set args [lrange $args 1 end] + } + -outputChannel { + set outputChannel [lindex $args 0] + set args [lrange $args 1 end] + } + default { + return -code error [msgcat::mc "Unknown argument: \"%s\" :\nUsage:\ + %s" $opt $usage] + } + } + } + if {![info exists procName]} { + set procName [genProcName $bargs] + } + if {[info exists procNameVar]} { + upvar $procNameVar myProcNameVar + } + set procText \ + [ ::logger::utils::createLogProc \ + -procName $procName \ + -conversionPattern $conversionPattern \ + -category $service \ + -outputChannel $outputChannel \ + -priority $level ] + set myProcNameVar $procName + return $procText +} + + + + +##Internal Procedure Header +# $Id: loggerAppender.tcl,v 1.4 2007/02/08 22:09:54 mic42 Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::appender::genProcName +# +# Purpose: +# +# +# Synopsis: +# ::logger::appender::genProcName +# +# Arguments: +# +# string composed of formatting chars (see description) +# +# +# Return Values: +# a runnable command +# +# Description: +# +# +# Examples: +# ::loggerExtension::new param1 +# ::loggerExtension::new param2 +# ::loggerExtension::new param3 +# +# +# Sample Input: +# (Optional) Sample of input to the proc provided by its argument values. +# +# Sample Output: +# (Optional) For procs that output to files, provide +# sample of format of output produced. +# Notes: +# 1. +# +# End of Procedure Header + + +proc ::logger::appender::genProcName {args} { + set name [md5::md5 -hex $args] + return "::logger::appender::logProc-$name" +} + + +package provide logger::appender 1.3 + +# ;;; Local Variables: *** +# ;;; mode: tcl *** +# ;;; End: *** diff --git a/lib/log/loggerUtils.tcl b/lib/log/loggerUtils.tcl new file mode 100644 index 0000000..7c4d859 --- /dev/null +++ b/lib/log/loggerUtils.tcl @@ -0,0 +1,544 @@ +##Library Header +# +# $Id: loggerUtils.tcl,v 1.6 2007/03/20 16:22:16 andreas_kupries Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::utils:: +# +# Purpose: +# an extension to the tcllib logger module +# +# Author: +# Aamer Akhter / aakhter@cisco.com +# +# Support Alias: +# aakhter@cisco.com +# +# Usage: +# package require logger::utils +# +# Description: +# this extension adds template based appenders +# +# Requirements: +# package require logger +# +# Variables: +# namespace ::logger::utils:: +# id: CVS ID: keyword extraction +# version: current version of package +# packageDir: directory where package is located +# log: instance log +# +# Notes: +# 1. +# +# Keywords: +# +# +# Category: +# +# +# End of Header + +package require Tcl 8.4 +package require logger +package require logger::appender +package require msgcat + +namespace eval ::logger::utils { + + variable packageDir [file dirname [info script]] + variable log [logger::init logger::utils] + + logger::import -force -namespace log logger::utils + + # @mdgen OWNER: msgs/*.msg + ::msgcat::mcload [file join $packageDir msgs] +} + +##Internal Procedure Header +# $Id: loggerUtils.tcl,v 1.6 2007/03/20 16:22:16 andreas_kupries Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::utils::createFormatCmd +# +# Purpose: +# +# +# Synopsis: +# ::logger::utils::createFormatCmd +# +# Arguments: +# +# string composed of formatting chars (see description) +# +# +# Return Values: +# a runnable command +# +# Description: +# createFormatCmd translates into an expandable +# command string. +# +# The following are the known substitutions (from log4perl): +# %c category of the logging event +# %C fully qualified name of logging event +# %d current date in yyyy/MM/dd hh:mm:ss +# %H hostname +# %m message to be logged +# %M method where logging event was issued +# %p priority of logging event +# %P pid of current process +# +# +# Examples: +# ::logger::new param1 +# ::logger::new param2 +# ::logger::new param3 +# +# +# Sample Input: +# (Optional) Sample of input to the proc provided by its argument values. +# +# Sample Output: +# (Optional) For procs that output to files, provide +# sample of format of output produced. +# Notes: +# 1. +# +# End of Procedure Header + + +proc ::logger::utils::createFormatCmd {text args} { + variable log + array set opt $args + + regsub -all -- \ + {%P} \ + $text \ + [pid] \ + text + + regsub -all -- \ + {%H} \ + $text \ + [info hostname] \ + text + + + #the %d subst has to happen at the end + regsub -all -- \ + {%d} \ + $text \ + {[clock format [clock seconds] -format {%Y/%m/%d %H:%M:%S}]} \ + text + + if {[info exists opt(-category)]} { + regsub -all -- \ + {%c} \ + $text \ + $opt(-category) \ + text + + regsub -all -- \ + {%C} \ + $text \ + [lindex [split $opt(-category) :: ] 0] \ + text + } + + if {[info exists opt(-priority)]} { + regsub -all -- \ + {%p} \ + $text \ + $opt(-priority) \ + text + } + + return $text +} + + + +##Procedure Header +# $Id: loggerUtils.tcl,v 1.6 2007/03/20 16:22:16 andreas_kupries Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::utils::createLogProc +# +# Purpose: +# +# +# Synopsis: +# ::logger::utils::createLogProc -procName [options] +# +# Arguments: +# -procName +# name of proc to create +# -conversionPattern +# see createFormatCmd for +# -category +# the category (service) +# -priority +# the priority (level) +# -outputChannel +# channel to output on (default stdout) +# +# +# Return Values: +# a runnable command +# +# Description: +# createFormatCmd translates into an expandable +# command string. +# +# The following are the known substitutions (from log4perl): +# %c category of the logging event +# %C fully qualified name of logging event +# %d current date in yyyy/MM/dd hh:mm:ss +# %H hostname +# %m message to be logged +# %M method where logging event was issued +# %p priority of logging event +# %P pid of current process +# +# +# Examples: +# +# +# Sample Input: +# (Optional) Sample of input to the proc provided by its argument values. +# +# Sample Output: +# (Optional) For procs that output to files, provide +# sample of format of output produced. +# Notes: +# 1. +# +# End of Procedure Header + + +proc ::logger::utils::createLogProc {args} { + variable log + array set opt $args + + set formatText "" + set methodText "" + if {[info exists opt(-conversionPattern)]} { + set text $opt(-conversionPattern) + + regsub -all -- \ + {%P} \ + $text \ + [pid] \ + text + + regsub -all -- \ + {%H} \ + $text \ + [info hostname] \ + text + + if {[info exists opt(-category)]} { + regsub -all -- \ + {%c} \ + $text \ + $opt(-category) \ + text + + regsub -all -- \ + {%C} \ + $text \ + [lindex [split $opt(-category) :: ] 0] \ + text + } + + if {[info exists opt(-priority)]} { + regsub -all -- \ + {%p} \ + $text \ + $opt(-priority) \ + text + } + + + if {[regexp {%M} $text]} { + set methodText { + if {[info level] < 2} { + set method "global" + } else { + set method [lindex [info level -1] 0] + } + + } + + regsub -all -- \ + {%M} \ + $text \ + {$method} \ + text + } + + regsub -all -- \ + {%m} \ + $text \ + {$text} \ + text + + regsub -all -- \ + {%d} \ + $text \ + {[clock format [clock seconds] -format {%Y/%m/%d %H:%M:%S}]} \ + text + + } + + if {[info exists opt(-outputChannel)]} { + set outputChannel $opt(-outputChannel) + } else { + set outputChannel stdout + } + + set formatText $text + set outputCommand puts + + set procText { + proc $opt(-procName) {text} { + $methodText + $outputCommand $outputChannel \"$formatText\" + } + } + + set procText [subst $procText] + return $procText +} + + +##Procedure Header +# $Id: loggerUtils.tcl,v 1.6 2007/03/20 16:22:16 andreas_kupries Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::utils::applyAppender +# +# Purpose: +# +# +# Synopsis: +# ::logger::utils::applyAppender -appender [options] +# +# Arguments: +# -service +# -serviceCmd +# name of logger instance to modify +# -serviceCmd takes as input the return of logger::init +# +# -appender +# type of appender to use +# console|colorConsole... +# +# -conversionPattern +# see createLogProc for format +# if not provided the default pattern +# is used: +# {\[%d\] \[%c\] \[%M\] \[%p\] %m} +# +# -levels +# list of levels to apply this appender to +# by default all levels are applied to +# +# Return Values: +# +# +# Description: +# applyAppender will create an appender for the specified +# logger services. If not service is specified then the +# appender will be added as the default appender for +# the specified levels. If no levels are specified, then +# all levels are assumed. +# +# The following are the known substitutions (from log4perl): +# %c category of the logging event +# %C fully qualified name of logging event +# %d current date in yyyy/MM/dd hh:mm:ss +# %H hostname +# %m message to be logged +# %M method where logging event was issued +# %p priority of logging event +# %P pid of current process +# +# +# Examples: +# % set log [logger::init testLog] +# ::logger::tree::testLog +# % logger::utils::applyAppender -appender console -serviceCmd $log +# % ${log}::error "this is error" +# [2005/08/22 10:14:13] [testLog] [global] [error] this is error +# +# +# End of Procedure Header + + +proc ::logger::utils::applyAppender {args} { + set usage {logger::utils::applyAppender + -appender appender + ?-instance? + ?-levels levels? + ?-appenderArgs appenderArgs? + } + set levels [logger::levels] + set appenderArgs {} + set bargs $args + while {[llength $args] > 1} { + set opt [lindex $args 0] + set args [lrange $args 1 end] + switch -exact -- $opt { + -appender { set appender [lindex $args 0] + set args [lrange $args 1 end] + } + -serviceCmd { set serviceCmd [lindex $args 0] + set args [lrange $args 1 end] + } + -service { set serviceCmd [logger::servicecmd [lindex $args 0]] + set args [lrange $args 1 end] + } + -levels { set levels [lindex $args 0] + set args [lrange $args 1 end] + } + -appenderArgs { + set appenderArgs [lindex $args 0] + set args [lrange $args 1 end] + } + default { + return -code error [msgcat::mc "Unknown argument: \"%s\" :\nUsage:\ + %s" $opt $usage] + } + } + } + + set appender ::logger::appender::${appender} + if {[info commands $appender] == {}} { + return -code error [msgcat::mc "could not find appender '%s'" $appender] + } + + #if service is not specified make all future services with this appender + # spec + if {![info exists serviceCmd]} { + set ::logger::utils::autoApplyAppenderArgs $bargs + #add trace + #check to see if trace is already set + if {[lsearch [trace info execution logger::init] \ + {leave ::logger::utils::autoApplyAppender} ] == -1} { + trace add execution ::logger::init leave ::logger::utils::autoApplyAppender + } + return + } + + + #foreach service specified, apply the appender for each of the levels + # specified + foreach srvCmd $serviceCmd { + + foreach lvl $levels { + set procText [$appender -appenderArgs $appenderArgs \ + -level $lvl \ + -service [${srvCmd}::servicename] \ + -procNameVar procName + ] + eval $procText + ${srvCmd}::logproc $lvl $procName + } + } +} + + +##Internal Procedure Header +# $Id: loggerUtils.tcl,v 1.6 2007/03/20 16:22:16 andreas_kupries Exp $ +# Copyright (c) 2005 Cisco Systems, Inc. +# +# Name: +# ::logger::utils::autoApplyAppender +# +# Purpose: +# +# +# Synopsis: +# ::logger::utils::autoApplyAppender +# +# Arguments: +# +# +# +# servicecmd generated by logger:init +# +# +# +# Return Values: +# +# +# Description: +# autoApplyAppender is designed to be added via trace leave +# to logger::init calls +# +# autoApplyAppender will look at preconfigred state (via applyAppender) +# to autocreate appenders for newly created logger instances +# +# Examples: +# logger::utils::applyAppender -appender console +# set log [logger::init applyAppender-3] +# ${log}::error "this is error" +# +# +# Sample Input: +# +# Sample Output: +# +# Notes: +# 1. +# +# End of Procedure Header + + +proc ::logger::utils::autoApplyAppender {command command-string log op args} { + variable autoApplyAppenderArgs + set bAppArgs $autoApplyAppenderArgs + set levels [logger::levels] + set appenderArgs {} + while {[llength $bAppArgs] > 1} { + set opt [lindex $bAppArgs 0] + set bAppArgs [lrange $bAppArgs 1 end] + switch -exact -- $opt { + -appender { set appender [lindex $bAppArgs 0] + set bAppArgs [lrange $bAppArgs 1 end] + } + -levels { set levels [lindex $bAppArgs 0] + set bAppArgs [lrange $bAppArgs 1 end] + } + -appenderArgs { + set appenderArgs [lindex $bAppArgs 0] + set bAppArgs [lrange $bAppArgs 1 end] + } + default { + return -code error [msgcat::mc "Unknown argument: \"%s\" :\nUsage:\ + %s" $opt $usage] + } + } + } + if {![info exists appender]} { + return -code error [msgcat::mc "need to specify -appender"] + } + logger::utils::applyAppender -appender $appender -serviceCmd $log \ + -levels $levels -appenderArgs $appenderArgs + return $log +} + + +package provide logger::utils 1.3 + +# ;;; Local Variables: *** +# ;;; mode: tcl *** +# ;;; End: *** diff --git a/lib/log/loggerperformance b/lib/log/loggerperformance new file mode 100644 index 0000000..d9d9b0b --- /dev/null +++ b/lib/log/loggerperformance @@ -0,0 +1,79 @@ +# -*- tcl -*- +# loggerperformance.tcl + +# $Id: loggerperformance,v 1.2 2004/01/15 06:36:13 andreas_kupries Exp $ + +# This code is for benchmarking the performance of the log tools. + +set auto_path "[file dirname [info script]] $auto_path" +package require logger +package require log + +# Set up logger +set log [logger::init date] + +# Create a custom log routine, so we don't deal with the overhead of +# the default one, which does some system calls itself. + +${log}::logproc notice txt { + puts "$txt" +} + +# Basic output. +proc Test1 {} { + set date [clock format [clock seconds]] + puts "Date is now $date" +} + +# No output at all. This is the benchmark by which 'turned off' log +# systems should be judged. +proc Test2 {} { + set date [clock format [clock seconds]] +} + +# Use logger. +proc Test3 {} { + set date [clock format [clock seconds]] + ${::log}::notice "Date is now $date" +} + +# Use log. +proc Test4 {} { + set date [clock format [clock seconds]] + log::log notice "Date is now $date" +} + +set res1 [time { + Test1 +} 1000] + +set res2 [time { + Test2 +} 1000] + +set res3 [time { + Test3 +} 1000] + +${log}::disable notice + +set res4 [time { + Test3 +} 1000] + +set res5 [time { + Test4 +} 1000] + +log::lvSuppressLE notice + +set res6 [time { + Test4 +} 1000] + +puts "Puts output: $res1" +puts "No output: $res2" +puts "Logger: $res3" +puts "Logger disabled: $res4" +puts "Log: $res5" +puts "Log disabled: $res6" diff --git a/lib/log/msgs/en.msg b/lib/log/msgs/en.msg new file mode 100644 index 0000000..9b6df9e --- /dev/null +++ b/lib/log/msgs/en.msg @@ -0,0 +1,7 @@ +# -*- tcl -*- +package require msgcat +namespace import ::msgcat::* + +mcset en "Unknown argument: \"%s\" :\nUsage: %s" "Unknown argument: \"%s\" :\nUsage: %s" +mcset en "could not find appender '%s'" "could not find appender '%s'" +mcset en "need to specify -appender" "need to specify -appender" diff --git a/lib/log/pkgIndex.tcl b/lib/log/pkgIndex.tcl new file mode 100644 index 0000000..9158b68 --- /dev/null +++ b/lib/log/pkgIndex.tcl @@ -0,0 +1,9 @@ +if {![package vsatisfies [package provide Tcl] 8]} {return} +package ifneeded log 1.2 [list source [file join $dir log.tcl]] + +if {![package vsatisfies [package provide Tcl] 8.2]} {return} +package ifneeded logger 0.8 [list source [file join $dir logger.tcl]] +package ifneeded logger::appender 1.3 [list source [file join $dir loggerAppender.tcl]] + +if {![package vsatisfies [package provide Tcl] 8.4]} {return} +package ifneeded logger::utils 1.3 [list source [file join $dir loggerUtils.tcl]] diff --git a/lib/md5/md5.tcl b/lib/md5/md5.tcl new file mode 100644 index 0000000..418c782 --- /dev/null +++ b/lib/md5/md5.tcl @@ -0,0 +1,454 @@ +################################################## +# +# md5.tcl - MD5 in Tcl +# Author: Don Libes , July 1999 +# Version 1.2.0 +# +# MD5 defined by RFC 1321, "The MD5 Message-Digest Algorithm" +# HMAC defined by RFC 2104, "Keyed-Hashing for Message Authentication" +# +# Most of the comments below come right out of RFC 1321; That's why +# they have such peculiar numbers. In addition, I have retained +# original syntax, bugs in documentation (yes, really), etc. from the +# RFC. All remaining bugs are mine. +# +# HMAC implementation by D. J. Hagberg and +# is based on C code in RFC 2104. +# +# For more info, see: http://expect.nist.gov/md5pure +# +# - Don +# +# Modified by Miguel Sofer to use inlines and simple variables +################################################## + +# @mdgen EXCLUDE: md5c.tcl + +package require Tcl 8.2 +namespace eval ::md5 { +} + +if {![catch {package require Trf 2.0}] && ![catch {::md5 -- test}]} { + # Trf is available, so implement the functionality provided here + # in terms of calls to Trf for speed. + + proc ::md5::md5 {msg} { + string tolower [::hex -mode encode -- [::md5 -- $msg]] + } + + # hmac: hash for message authentication + + # MD5 of Trf and MD5 as defined by this package have slightly + # different results. Trf returns the digest in binary, here we get + # it as hex-string. In the computation of the HMAC the latter + # requires back conversion into binary in some places. With Trf we + # can use omit these. + + proc ::md5::hmac {key text} { + # if key is longer than 64 bytes, reset it to MD5(key). If shorter, + # pad it out with null (\x00) chars. + set keyLen [string length $key] + if {$keyLen > 64} { + #old: set key [binary format H32 [md5 $key]] + set key [::md5 -- $key] + set keyLen [string length $key] + } + + # ensure the key is padded out to 64 chars with nulls. + set padLen [expr {64 - $keyLen}] + append key [binary format "a$padLen" {}] + + # Split apart the key into a list of 16 little-endian words + binary scan $key i16 blocks + + # XOR key with ipad and opad values + set k_ipad {} + set k_opad {} + foreach i $blocks { + append k_ipad [binary format i [expr {$i ^ 0x36363636}]] + append k_opad [binary format i [expr {$i ^ 0x5c5c5c5c}]] + } + + # Perform inner md5, appending its results to the outer key + append k_ipad $text + #old: append k_opad [binary format H* [md5 $k_ipad]] + append k_opad [::md5 -- $k_ipad] + + # Perform outer md5 + #old: md5 $k_opad + string tolower [::hex -mode encode -- [::md5 -- $k_opad]] + } + +} else { + # Without Trf use the all-tcl implementation by Don Libes. + + # T will be inlined after the definition of md5body + + # test md5 + # + # This proc is not necessary during runtime and may be omitted if you + # are simply inserting this file into a production program. + # + proc ::md5::test {} { + foreach {msg expected} { + "" + "d41d8cd98f00b204e9800998ecf8427e" + "a" + "0cc175b9c0f1b6a831c399e269772661" + "abc" + "900150983cd24fb0d6963f7d28e17f72" + "message digest" + "f96b697d7cb7938d525a2f31aaf161d0" + "abcdefghijklmnopqrstuvwxyz" + "c3fcd3d76192e4007dfb496cca67e13b" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + "d174ab98d277d9f5a5611c2c9f419d9f" + "12345678901234567890123456789012345678901234567890123456789012345678901234567890" + "57edf4a22be3c955ac49da2e2107b67a" + } { + puts "testing: md5 \"$msg\"" + set computed [md5 $msg] + puts "expected: $expected" + puts "computed: $computed" + if {0 != [string compare $computed $expected]} { + puts "FAILED" + } else { + puts "SUCCEEDED" + } + } + } + + # time md5 + # + # This proc is not necessary during runtime and may be omitted if you + # are simply inserting this file into a production program. + # + proc ::md5::time {} { + foreach len {10 50 100 500 1000 5000 10000} { + set time [::time {md5 [format %$len.0s ""]} 100] + set msec [lindex $time 0] + puts "input length $len: [expr {$msec/1000}] milliseconds per interation" + } + } + + # + # We just define the body of md5pure::md5 here; later we + # regsub to inline a few function calls for speed + # + + set ::md5::md5body { + + # + # 3.1 Step 1. Append Padding Bits + # + + set msgLen [string length $msg] + + set padLen [expr {56 - $msgLen%64}] + if {$msgLen % 64 > 56} { + incr padLen 64 + } + + # pad even if no padding required + if {$padLen == 0} { + incr padLen 64 + } + + # append single 1b followed by 0b's + append msg [binary format "a$padLen" \200] + + # + # 3.2 Step 2. Append Length + # + + # RFC doesn't say whether to use little- or big-endian + # code demonstrates little-endian + # This step limits our input to size 2^32b or 2^24B + append msg [binary format "i1i1" [expr {8*$msgLen}] 0] + + # + # 3.3 Step 3. Initialize MD Buffer + # + + set A [expr 0x67452301] + set B [expr 0xefcdab89] + set C [expr 0x98badcfe] + set D [expr 0x10325476] + + # + # 3.4 Step 4. Process Message in 16-Word Blocks + # + + # process each 16-word block + # RFC doesn't say whether to use little- or big-endian + # code says little-endian + binary scan $msg i* blocks + + # loop over the message taking 16 blocks at a time + + foreach {X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15} $blocks { + + # Save A as AA, B as BB, C as CC, and D as DD. + set AA $A + set BB $B + set CC $C + set DD $D + + # Round 1. + # Let [abcd k s i] denote the operation + # a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). + # [ABCD 0 7 1] [DABC 1 12 2] [CDAB 2 17 3] [BCDA 3 22 4] + set A [expr {$B + [<<< [expr {$A + [F $B $C $D] + $X0 + $T01}] 7]}] + set D [expr {$A + [<<< [expr {$D + [F $A $B $C] + $X1 + $T02}] 12]}] + set C [expr {$D + [<<< [expr {$C + [F $D $A $B] + $X2 + $T03}] 17]}] + set B [expr {$C + [<<< [expr {$B + [F $C $D $A] + $X3 + $T04}] 22]}] + # [ABCD 4 7 5] [DABC 5 12 6] [CDAB 6 17 7] [BCDA 7 22 8] + set A [expr {$B + [<<< [expr {$A + [F $B $C $D] + $X4 + $T05}] 7]}] + set D [expr {$A + [<<< [expr {$D + [F $A $B $C] + $X5 + $T06}] 12]}] + set C [expr {$D + [<<< [expr {$C + [F $D $A $B] + $X6 + $T07}] 17]}] + set B [expr {$C + [<<< [expr {$B + [F $C $D $A] + $X7 + $T08}] 22]}] + # [ABCD 8 7 9] [DABC 9 12 10] [CDAB 10 17 11] [BCDA 11 22 12] + set A [expr {$B + [<<< [expr {$A + [F $B $C $D] + $X8 + $T09}] 7]}] + set D [expr {$A + [<<< [expr {$D + [F $A $B $C] + $X9 + $T10}] 12]}] + set C [expr {$D + [<<< [expr {$C + [F $D $A $B] + $X10 + $T11}] 17]}] + set B [expr {$C + [<<< [expr {$B + [F $C $D $A] + $X11 + $T12}] 22]}] + # [ABCD 12 7 13] [DABC 13 12 14] [CDAB 14 17 15] [BCDA 15 22 16] + set A [expr {$B + [<<< [expr {$A + [F $B $C $D] + $X12 + $T13}] 7]}] + set D [expr {$A + [<<< [expr {$D + [F $A $B $C] + $X13 + $T14}] 12]}] + set C [expr {$D + [<<< [expr {$C + [F $D $A $B] + $X14 + $T15}] 17]}] + set B [expr {$C + [<<< [expr {$B + [F $C $D $A] + $X15 + $T16}] 22]}] + + # Round 2. + # Let [abcd k s i] denote the operation + # a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). + # Do the following 16 operations. + # [ABCD 1 5 17] [DABC 6 9 18] [CDAB 11 14 19] [BCDA 0 20 20] + set A [expr {$B + [<<< [expr {$A + [G $B $C $D] + $X1 + $T17}] 5]}] + set D [expr {$A + [<<< [expr {$D + [G $A $B $C] + $X6 + $T18}] 9]}] + set C [expr {$D + [<<< [expr {$C + [G $D $A $B] + $X11 + $T19}] 14]}] + set B [expr {$C + [<<< [expr {$B + [G $C $D $A] + $X0 + $T20}] 20]}] + # [ABCD 5 5 21] [DABC 10 9 22] [CDAB 15 14 23] [BCDA 4 20 24] + set A [expr {$B + [<<< [expr {$A + [G $B $C $D] + $X5 + $T21}] 5]}] + set D [expr {$A + [<<< [expr {$D + [G $A $B $C] + $X10 + $T22}] 9]}] + set C [expr {$D + [<<< [expr {$C + [G $D $A $B] + $X15 + $T23}] 14]}] + set B [expr {$C + [<<< [expr {$B + [G $C $D $A] + $X4 + $T24}] 20]}] + # [ABCD 9 5 25] [DABC 14 9 26] [CDAB 3 14 27] [BCDA 8 20 28] + set A [expr {$B + [<<< [expr {$A + [G $B $C $D] + $X9 + $T25}] 5]}] + set D [expr {$A + [<<< [expr {$D + [G $A $B $C] + $X14 + $T26}] 9]}] + set C [expr {$D + [<<< [expr {$C + [G $D $A $B] + $X3 + $T27}] 14]}] + set B [expr {$C + [<<< [expr {$B + [G $C $D $A] + $X8 + $T28}] 20]}] + # [ABCD 13 5 29] [DABC 2 9 30] [CDAB 7 14 31] [BCDA 12 20 32] + set A [expr {$B + [<<< [expr {$A + [G $B $C $D] + $X13 + $T29}] 5]}] + set D [expr {$A + [<<< [expr {$D + [G $A $B $C] + $X2 + $T30}] 9]}] + set C [expr {$D + [<<< [expr {$C + [G $D $A $B] + $X7 + $T31}] 14]}] + set B [expr {$C + [<<< [expr {$B + [G $C $D $A] + $X12 + $T32}] 20]}] + + # Round 3. + # Let [abcd k s t] [sic] denote the operation + # a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). + # Do the following 16 operations. + # [ABCD 5 4 33] [DABC 8 11 34] [CDAB 11 16 35] [BCDA 14 23 36] + set A [expr {$B + [<<< [expr {$A + [H $B $C $D] + $X5 + $T33}] 4]}] + set D [expr {$A + [<<< [expr {$D + [H $A $B $C] + $X8 + $T34}] 11]}] + set C [expr {$D + [<<< [expr {$C + [H $D $A $B] + $X11 + $T35}] 16]}] + set B [expr {$C + [<<< [expr {$B + [H $C $D $A] + $X14 + $T36}] 23]}] + # [ABCD 1 4 37] [DABC 4 11 38] [CDAB 7 16 39] [BCDA 10 23 40] + set A [expr {$B + [<<< [expr {$A + [H $B $C $D] + $X1 + $T37}] 4]}] + set D [expr {$A + [<<< [expr {$D + [H $A $B $C] + $X4 + $T38}] 11]}] + set C [expr {$D + [<<< [expr {$C + [H $D $A $B] + $X7 + $T39}] 16]}] + set B [expr {$C + [<<< [expr {$B + [H $C $D $A] + $X10 + $T40}] 23]}] + # [ABCD 13 4 41] [DABC 0 11 42] [CDAB 3 16 43] [BCDA 6 23 44] + set A [expr {$B + [<<< [expr {$A + [H $B $C $D] + $X13 + $T41}] 4]}] + set D [expr {$A + [<<< [expr {$D + [H $A $B $C] + $X0 + $T42}] 11]}] + set C [expr {$D + [<<< [expr {$C + [H $D $A $B] + $X3 + $T43}] 16]}] + set B [expr {$C + [<<< [expr {$B + [H $C $D $A] + $X6 + $T44}] 23]}] + # [ABCD 9 4 45] [DABC 12 11 46] [CDAB 15 16 47] [BCDA 2 23 48] + set A [expr {$B + [<<< [expr {$A + [H $B $C $D] + $X9 + $T45}] 4]}] + set D [expr {$A + [<<< [expr {$D + [H $A $B $C] + $X12 + $T46}] 11]}] + set C [expr {$D + [<<< [expr {$C + [H $D $A $B] + $X15 + $T47}] 16]}] + set B [expr {$C + [<<< [expr {$B + [H $C $D $A] + $X2 + $T48}] 23]}] + + # Round 4. + # Let [abcd k s t] [sic] denote the operation + # a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). + # Do the following 16 operations. + # [ABCD 0 6 49] [DABC 7 10 50] [CDAB 14 15 51] [BCDA 5 21 52] + set A [expr {$B + [<<< [expr {$A + [I $B $C $D] + $X0 + $T49}] 6]}] + set D [expr {$A + [<<< [expr {$D + [I $A $B $C] + $X7 + $T50}] 10]}] + set C [expr {$D + [<<< [expr {$C + [I $D $A $B] + $X14 + $T51}] 15]}] + set B [expr {$C + [<<< [expr {$B + [I $C $D $A] + $X5 + $T52}] 21]}] + # [ABCD 12 6 53] [DABC 3 10 54] [CDAB 10 15 55] [BCDA 1 21 56] + set A [expr {$B + [<<< [expr {$A + [I $B $C $D] + $X12 + $T53}] 6]}] + set D [expr {$A + [<<< [expr {$D + [I $A $B $C] + $X3 + $T54}] 10]}] + set C [expr {$D + [<<< [expr {$C + [I $D $A $B] + $X10 + $T55}] 15]}] + set B [expr {$C + [<<< [expr {$B + [I $C $D $A] + $X1 + $T56}] 21]}] + # [ABCD 8 6 57] [DABC 15 10 58] [CDAB 6 15 59] [BCDA 13 21 60] + set A [expr {$B + [<<< [expr {$A + [I $B $C $D] + $X8 + $T57}] 6]}] + set D [expr {$A + [<<< [expr {$D + [I $A $B $C] + $X15 + $T58}] 10]}] + set C [expr {$D + [<<< [expr {$C + [I $D $A $B] + $X6 + $T59}] 15]}] + set B [expr {$C + [<<< [expr {$B + [I $C $D $A] + $X13 + $T60}] 21]}] + # [ABCD 4 6 61] [DABC 11 10 62] [CDAB 2 15 63] [BCDA 9 21 64] + set A [expr {$B + [<<< [expr {$A + [I $B $C $D] + $X4 + $T61}] 6]}] + set D [expr {$A + [<<< [expr {$D + [I $A $B $C] + $X11 + $T62}] 10]}] + set C [expr {$D + [<<< [expr {$C + [I $D $A $B] + $X2 + $T63}] 15]}] + set B [expr {$C + [<<< [expr {$B + [I $C $D $A] + $X9 + $T64}] 21]}] + + # Then perform the following additions. (That is increment each + # of the four registers by the value it had before this block + # was started.) + incr A $AA + incr B $BB + incr C $CC + incr D $DD + } + # 3.5 Step 5. Output + + # ... begin with the low-order byte of A, and end with the high-order byte + # of D. + + return [bytes $A][bytes $B][bytes $C][bytes $D] + } + + # + # Here we inline/regsub the functions F, G, H, I and <<< + # + + namespace eval ::md5 { + #proc md5pure::F {x y z} {expr {(($x & $y) | ((~$x) & $z))}} + regsub -all -- {\[ *F +(\$.) +(\$.) +(\$.) *\]} $md5body {((\1 \& \2) | ((~\1) \& \3))} md5body + + #proc md5pure::G {x y z} {expr {(($x & $z) | ($y & (~$z)))}} + regsub -all -- {\[ *G +(\$.) +(\$.) +(\$.) *\]} $md5body {((\1 \& \3) | (\2 \& (~\3)))} md5body + + #proc md5pure::H {x y z} {expr {$x ^ $y ^ $z}} + regsub -all -- {\[ *H +(\$.) +(\$.) +(\$.) *\]} $md5body {(\1 ^ \2 ^ \3)} md5body + + #proc md5pure::I {x y z} {expr {$y ^ ($x | (~$z))}} + regsub -all -- {\[ *I +(\$.) +(\$.) +(\$.) *\]} $md5body {(\2 ^ (\1 | (~\3)))} md5body + + # bitwise left-rotate + if {0} { + proc md5pure::<<< {x i} { + # This works by bitwise-ORing together right piece and left + # piece so that the (original) right piece becomes the left + # piece and vice versa. + # + # The (original) right piece is a simple left shift. + # The (original) left piece should be a simple right shift + # but Tcl does sign extension on right shifts so we + # shift it 1 bit, mask off the sign, and finally shift + # it the rest of the way. + + # expr {($x << $i) | ((($x >> 1) & 0x7fffffff) >> (31-$i))} + + # + # New version, faster when inlining + # We replace inline (computing at compile time): + # R$i -> (32 - $i) + # S$i -> (0x7fffffff >> (31-$i)) + # + + expr { ($x << $i) | (($x >> [set R$i]) & [set S$i])} + } + } + # inline <<< + regsub -all -- {\[ *<<< +\[ *expr +({[^\}]*})\] +([0-9]+) *\]} $md5body {(([set x [expr \1]] << \2) | (($x >> R\2) \& S\2))} md5body + + # now replace the R and S + set map {} + foreach i { + 7 12 17 22 + 5 9 14 20 + 4 11 16 23 + 6 10 15 21 + } { + lappend map R$i [expr {32 - $i}] S$i [expr {0x7fffffff >> (31-$i)}] + } + + # inline the values of T + foreach \ + tName { + T01 T02 T03 T04 T05 T06 T07 T08 T09 T10 + T11 T12 T13 T14 T15 T16 T17 T18 T19 T20 + T21 T22 T23 T24 T25 T26 T27 T28 T29 T30 + T31 T32 T33 T34 T35 T36 T37 T38 T39 T40 + T41 T42 T43 T44 T45 T46 T47 T48 T49 T50 + T51 T52 T53 T54 T55 T56 T57 T58 T59 T60 + T61 T62 T63 T64 } \ + tVal { + 0xd76aa478 0xe8c7b756 0x242070db 0xc1bdceee + 0xf57c0faf 0x4787c62a 0xa8304613 0xfd469501 + 0x698098d8 0x8b44f7af 0xffff5bb1 0x895cd7be + 0x6b901122 0xfd987193 0xa679438e 0x49b40821 + + 0xf61e2562 0xc040b340 0x265e5a51 0xe9b6c7aa + 0xd62f105d 0x2441453 0xd8a1e681 0xe7d3fbc8 + 0x21e1cde6 0xc33707d6 0xf4d50d87 0x455a14ed + 0xa9e3e905 0xfcefa3f8 0x676f02d9 0x8d2a4c8a + + 0xfffa3942 0x8771f681 0x6d9d6122 0xfde5380c + 0xa4beea44 0x4bdecfa9 0xf6bb4b60 0xbebfbc70 + 0x289b7ec6 0xeaa127fa 0xd4ef3085 0x4881d05 + 0xd9d4d039 0xe6db99e5 0x1fa27cf8 0xc4ac5665 + + 0xf4292244 0x432aff97 0xab9423a7 0xfc93a039 + 0x655b59c3 0x8f0ccc92 0xffeff47d 0x85845dd1 + 0x6fa87e4f 0xfe2ce6e0 0xa3014314 0x4e0811a1 + 0xf7537e82 0xbd3af235 0x2ad7d2bb 0xeb86d391 + } { + lappend map \$$tName $tVal + } + set md5body [string map $map $md5body] + + + # Finally, define the proc + proc md5 {msg} $md5body + + # unset auxiliary variables + unset md5body tName tVal map + } + + proc ::md5::byte0 {i} {expr {0xff & $i}} + proc ::md5::byte1 {i} {expr {(0xff00 & $i) >> 8}} + proc ::md5::byte2 {i} {expr {(0xff0000 & $i) >> 16}} + proc ::md5::byte3 {i} {expr {((0xff000000 & $i) >> 24) & 0xff}} + + proc ::md5::bytes {i} { + format %0.2x%0.2x%0.2x%0.2x [byte0 $i] [byte1 $i] [byte2 $i] [byte3 $i] + } + + # hmac: hash for message authentication + proc ::md5::hmac {key text} { + # if key is longer than 64 bytes, reset it to MD5(key). If shorter, + # pad it out with null (\x00) chars. + set keyLen [string length $key] + if {$keyLen > 64} { + set key [binary format H32 [md5 $key]] + set keyLen [string length $key] + } + + # ensure the key is padded out to 64 chars with nulls. + set padLen [expr {64 - $keyLen}] + append key [binary format "a$padLen" {}] + + # Split apart the key into a list of 16 little-endian words + binary scan $key i16 blocks + + # XOR key with ipad and opad values + set k_ipad {} + set k_opad {} + foreach i $blocks { + append k_ipad [binary format i [expr {$i ^ 0x36363636}]] + append k_opad [binary format i [expr {$i ^ 0x5c5c5c5c}]] + } + + # Perform inner md5, appending its results to the outer key + append k_ipad $text + append k_opad [binary format H* [md5 $k_ipad]] + + # Perform outer md5 + md5 $k_opad + } +} + +package provide md5 1.4.4 diff --git a/lib/md5/md5x.tcl b/lib/md5/md5x.tcl new file mode 100644 index 0000000..9b48162 --- /dev/null +++ b/lib/md5/md5x.tcl @@ -0,0 +1,714 @@ +# md5.tcl - Copyright (C) 2003 Pat Thoyts +# +# MD5 defined by RFC 1321, "The MD5 Message-Digest Algorithm" +# HMAC defined by RFC 2104, "Keyed-Hashing for Message Authentication" +# +# This is an implementation of MD5 based upon the example code given in +# RFC 1321 and upon the tcllib MD4 implementation and taking some ideas +# from the earlier tcllib md5 version by Don Libes. +# +# This implementation permits incremental updating of the hash and +# provides support for external compiled implementations either using +# critcl (md5c) or Trf. +# +# ------------------------------------------------------------------------- +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# ------------------------------------------------------------------------- +# +# $Id: md5x.tcl,v 1.17 2006/09/19 23:36:17 andreas_kupries Exp $ + +package require Tcl 8.2; # tcl minimum version + +namespace eval ::md5 { + variable version 2.0.5 + variable rcsid {$Id: md5x.tcl,v 1.17 2006/09/19 23:36:17 andreas_kupries Exp $} + variable accel + array set accel {critcl 0 cryptkit 0 trf 0} + + namespace export md5 hmac MD5Init MD5Update MD5Final + + variable uid + if {![info exists uid]} { + set uid 0 + } +} + +# ------------------------------------------------------------------------- + +# MD5Init -- +# +# Create and initialize an MD5 state variable. This will be +# cleaned up when we call MD5Final +# +proc ::md5::MD5Init {} { + variable accel + variable uid + set token [namespace current]::[incr uid] + upvar #0 $token state + + # RFC1321:3.3 - Initialize MD5 state structure + array set state \ + [list \ + A [expr {0x67452301}] \ + B [expr {0xefcdab89}] \ + C [expr {0x98badcfe}] \ + D [expr {0x10325476}] \ + n 0 i "" ] + if {$accel(cryptkit)} { + cryptkit::cryptCreateContext state(ckctx) CRYPT_UNUSED CRYPT_ALGO_MD5 + } elseif {$accel(trf)} { + set s {} + switch -exact -- $::tcl_platform(platform) { + windows { set s [open NUL w] } + unix { set s [open /dev/null w] } + } + if {$s != {}} { + fconfigure $s -translation binary -buffering none + ::md5 -attach $s -mode write \ + -read-type variable \ + -read-destination [subst $token](trfread) \ + -write-type variable \ + -write-destination [subst $token](trfwrite) + array set state [list trfread 0 trfwrite 0 trf $s] + } + } + return $token +} + +# MD5Update -- +# +# This is called to add more data into the hash. You may call this +# as many times as you require. Note that passing in "ABC" is equivalent +# to passing these letters in as separate calls -- hence this proc +# permits hashing of chunked data +# +# If we have a C-based implementation available, then we will use +# it here in preference to the pure-Tcl implementation. +# +proc ::md5::MD5Update {token data} { + variable accel + upvar #0 $token state + + if {$accel(critcl)} { + if {[info exists state(md5c)]} { + set state(md5c) [md5c $data $state(md5c)] + } else { + set state(md5c) [md5c $data] + } + return + } elseif {[info exists state(ckctx)]} { + if {[string length $data] > 0} { + cryptkit::cryptEncrypt $state(ckctx) $data + } + return + } elseif {[info exists state(trf)]} { + puts -nonewline $state(trf) $data + return + } + + # Update the state values + incr state(n) [string length $data] + append state(i) $data + + # Calculate the hash for any complete blocks + set len [string length $state(i)] + for {set n 0} {($n + 64) <= $len} {} { + MD5Hash $token [string range $state(i) $n [incr n 64]] + } + + # Adjust the state for the blocks completed. + set state(i) [string range $state(i) $n end] + return +} + +# MD5Final -- +# +# This procedure is used to close the current hash and returns the +# hash data. Once this procedure has been called the hash context +# is freed and cannot be used again. +# +# Note that the output is 128 bits represented as binary data. +# +proc ::md5::MD5Final {token} { + upvar #0 $token state + + # Check for either of the C-compiled versions. + if {[info exists state(md5c)]} { + set r $state(md5c) + unset state + return $r + } elseif {[info exists state(ckctx)]} { + cryptkit::cryptEncrypt $state(ckctx) "" + cryptkit::cryptGetAttributeString $state(ckctx) \ + CRYPT_CTXINFO_HASHVALUE r 16 + cryptkit::cryptDestroyContext $state(ckctx) + # If nothing was hashed, we get no r variable set! + if {[info exists r]} { + unset state + return $r + } + } elseif {[info exists state(trf)]} { + close $state(trf) + set r $state(trfwrite) + unset state + return $r + } + + # RFC1321:3.1 - Padding + # + set len [string length $state(i)] + set pad [expr {56 - ($len % 64)}] + if {$len % 64 > 56} { + incr pad 64 + } + if {$pad == 0} { + incr pad 64 + } + append state(i) [binary format a$pad \x80] + + # RFC1321:3.2 - Append length in bits as little-endian wide int. + append state(i) [binary format ii [expr {8 * $state(n)}] 0] + + # Calculate the hash for the remaining block. + set len [string length $state(i)] + for {set n 0} {($n + 64) <= $len} {} { + MD5Hash $token [string range $state(i) $n [incr n 64]] + } + + # RFC1321:3.5 - Output + set r [bytes $state(A)][bytes $state(B)][bytes $state(C)][bytes $state(D)] + unset state + return $r +} + +# ------------------------------------------------------------------------- +# HMAC Hashed Message Authentication (RFC 2104) +# +# hmac = H(K xor opad, H(K xor ipad, text)) +# + +# HMACInit -- +# +# This is equivalent to the MD5Init procedure except that a key is +# added into the algorithm +# +proc ::md5::HMACInit {K} { + + # Key K is adjusted to be 64 bytes long. If K is larger, then use + # the MD5 digest of K and pad this instead. + set len [string length $K] + if {$len > 64} { + set tok [MD5Init] + MD5Update $tok $K + set K [MD5Final $tok] + set len [string length $K] + } + set pad [expr {64 - $len}] + append K [string repeat \0 $pad] + + # Cacluate the padding buffers. + set Ki {} + set Ko {} + binary scan $K i16 Ks + foreach k $Ks { + append Ki [binary format i [expr {$k ^ 0x36363636}]] + append Ko [binary format i [expr {$k ^ 0x5c5c5c5c}]] + } + + set tok [MD5Init] + MD5Update $tok $Ki; # initialize with the inner pad + + # preserve the Ko value for the final stage. + # FRINK: nocheck + set [subst $tok](Ko) $Ko + + return $tok +} + +# HMACUpdate -- +# +# Identical to calling MD5Update +# +proc ::md5::HMACUpdate {token data} { + MD5Update $token $data + return +} + +# HMACFinal -- +# +# This is equivalent to the MD5Final procedure. The hash context is +# closed and the binary representation of the hash result is returned. +# +proc ::md5::HMACFinal {token} { + upvar #0 $token state + + set tok [MD5Init]; # init the outer hashing function + MD5Update $tok $state(Ko); # prepare with the outer pad. + MD5Update $tok [MD5Final $token]; # hash the inner result + return [MD5Final $tok] +} + +# ------------------------------------------------------------------------- +# Description: +# This is the core MD5 algorithm. It is a lot like the MD4 algorithm but +# includes an extra round and a set of constant modifiers throughout. +# +# Note: +# This function body is substituted later on to inline some of the +# procedures and to make is a bit more comprehensible. +# +set ::md5::MD5Hash_body { + variable $token + upvar 0 $token state + + # RFC1321:3.4 - Process Message in 16-Word Blocks + binary scan $msg i* blocks + foreach {X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15} $blocks { + set A $state(A) + set B $state(B) + set C $state(C) + set D $state(D) + + # Round 1 + # Let [abcd k s i] denote the operation + # a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). + # Do the following 16 operations. + # [ABCD 0 7 1] [DABC 1 12 2] [CDAB 2 17 3] [BCDA 3 22 4] + set A [expr {$B + (($A + [F $B $C $D] + $X0 + $T01) <<< 7)}] + set D [expr {$A + (($D + [F $A $B $C] + $X1 + $T02) <<< 12)}] + set C [expr {$D + (($C + [F $D $A $B] + $X2 + $T03) <<< 17)}] + set B [expr {$C + (($B + [F $C $D $A] + $X3 + $T04) <<< 22)}] + # [ABCD 4 7 5] [DABC 5 12 6] [CDAB 6 17 7] [BCDA 7 22 8] + set A [expr {$B + (($A + [F $B $C $D] + $X4 + $T05) <<< 7)}] + set D [expr {$A + (($D + [F $A $B $C] + $X5 + $T06) <<< 12)}] + set C [expr {$D + (($C + [F $D $A $B] + $X6 + $T07) <<< 17)}] + set B [expr {$C + (($B + [F $C $D $A] + $X7 + $T08) <<< 22)}] + # [ABCD 8 7 9] [DABC 9 12 10] [CDAB 10 17 11] [BCDA 11 22 12] + set A [expr {$B + (($A + [F $B $C $D] + $X8 + $T09) <<< 7)}] + set D [expr {$A + (($D + [F $A $B $C] + $X9 + $T10) <<< 12)}] + set C [expr {$D + (($C + [F $D $A $B] + $X10 + $T11) <<< 17)}] + set B [expr {$C + (($B + [F $C $D $A] + $X11 + $T12) <<< 22)}] + # [ABCD 12 7 13] [DABC 13 12 14] [CDAB 14 17 15] [BCDA 15 22 16] + set A [expr {$B + (($A + [F $B $C $D] + $X12 + $T13) <<< 7)}] + set D [expr {$A + (($D + [F $A $B $C] + $X13 + $T14) <<< 12)}] + set C [expr {$D + (($C + [F $D $A $B] + $X14 + $T15) <<< 17)}] + set B [expr {$C + (($B + [F $C $D $A] + $X15 + $T16) <<< 22)}] + + # Round 2. + # Let [abcd k s i] denote the operation + # a = b + ((a + G(b,c,d) + X[k] + Ti) <<< s) + # Do the following 16 operations. + # [ABCD 1 5 17] [DABC 6 9 18] [CDAB 11 14 19] [BCDA 0 20 20] + set A [expr {$B + (($A + [G $B $C $D] + $X1 + $T17) <<< 5)}] + set D [expr {$A + (($D + [G $A $B $C] + $X6 + $T18) <<< 9)}] + set C [expr {$D + (($C + [G $D $A $B] + $X11 + $T19) <<< 14)}] + set B [expr {$C + (($B + [G $C $D $A] + $X0 + $T20) <<< 20)}] + # [ABCD 5 5 21] [DABC 10 9 22] [CDAB 15 14 23] [BCDA 4 20 24] + set A [expr {$B + (($A + [G $B $C $D] + $X5 + $T21) <<< 5)}] + set D [expr {$A + (($D + [G $A $B $C] + $X10 + $T22) <<< 9)}] + set C [expr {$D + (($C + [G $D $A $B] + $X15 + $T23) <<< 14)}] + set B [expr {$C + (($B + [G $C $D $A] + $X4 + $T24) <<< 20)}] + # [ABCD 9 5 25] [DABC 14 9 26] [CDAB 3 14 27] [BCDA 8 20 28] + set A [expr {$B + (($A + [G $B $C $D] + $X9 + $T25) <<< 5)}] + set D [expr {$A + (($D + [G $A $B $C] + $X14 + $T26) <<< 9)}] + set C [expr {$D + (($C + [G $D $A $B] + $X3 + $T27) <<< 14)}] + set B [expr {$C + (($B + [G $C $D $A] + $X8 + $T28) <<< 20)}] + # [ABCD 13 5 29] [DABC 2 9 30] [CDAB 7 14 31] [BCDA 12 20 32] + set A [expr {$B + (($A + [G $B $C $D] + $X13 + $T29) <<< 5)}] + set D [expr {$A + (($D + [G $A $B $C] + $X2 + $T30) <<< 9)}] + set C [expr {$D + (($C + [G $D $A $B] + $X7 + $T31) <<< 14)}] + set B [expr {$C + (($B + [G $C $D $A] + $X12 + $T32) <<< 20)}] + + # Round 3. + # Let [abcd k s i] denote the operation + # a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s) + # Do the following 16 operations. + # [ABCD 5 4 33] [DABC 8 11 34] [CDAB 11 16 35] [BCDA 14 23 36] + set A [expr {$B + (($A + [H $B $C $D] + $X5 + $T33) <<< 4)}] + set D [expr {$A + (($D + [H $A $B $C] + $X8 + $T34) <<< 11)}] + set C [expr {$D + (($C + [H $D $A $B] + $X11 + $T35) <<< 16)}] + set B [expr {$C + (($B + [H $C $D $A] + $X14 + $T36) <<< 23)}] + # [ABCD 1 4 37] [DABC 4 11 38] [CDAB 7 16 39] [BCDA 10 23 40] + set A [expr {$B + (($A + [H $B $C $D] + $X1 + $T37) <<< 4)}] + set D [expr {$A + (($D + [H $A $B $C] + $X4 + $T38) <<< 11)}] + set C [expr {$D + (($C + [H $D $A $B] + $X7 + $T39) <<< 16)}] + set B [expr {$C + (($B + [H $C $D $A] + $X10 + $T40) <<< 23)}] + # [ABCD 13 4 41] [DABC 0 11 42] [CDAB 3 16 43] [BCDA 6 23 44] + set A [expr {$B + (($A + [H $B $C $D] + $X13 + $T41) <<< 4)}] + set D [expr {$A + (($D + [H $A $B $C] + $X0 + $T42) <<< 11)}] + set C [expr {$D + (($C + [H $D $A $B] + $X3 + $T43) <<< 16)}] + set B [expr {$C + (($B + [H $C $D $A] + $X6 + $T44) <<< 23)}] + # [ABCD 9 4 45] [DABC 12 11 46] [CDAB 15 16 47] [BCDA 2 23 48] + set A [expr {$B + (($A + [H $B $C $D] + $X9 + $T45) <<< 4)}] + set D [expr {$A + (($D + [H $A $B $C] + $X12 + $T46) <<< 11)}] + set C [expr {$D + (($C + [H $D $A $B] + $X15 + $T47) <<< 16)}] + set B [expr {$C + (($B + [H $C $D $A] + $X2 + $T48) <<< 23)}] + + # Round 4. + # Let [abcd k s i] denote the operation + # a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s) + # Do the following 16 operations. + # [ABCD 0 6 49] [DABC 7 10 50] [CDAB 14 15 51] [BCDA 5 21 52] + set A [expr {$B + (($A + [I $B $C $D] + $X0 + $T49) <<< 6)}] + set D [expr {$A + (($D + [I $A $B $C] + $X7 + $T50) <<< 10)}] + set C [expr {$D + (($C + [I $D $A $B] + $X14 + $T51) <<< 15)}] + set B [expr {$C + (($B + [I $C $D $A] + $X5 + $T52) <<< 21)}] + # [ABCD 12 6 53] [DABC 3 10 54] [CDAB 10 15 55] [BCDA 1 21 56] + set A [expr {$B + (($A + [I $B $C $D] + $X12 + $T53) <<< 6)}] + set D [expr {$A + (($D + [I $A $B $C] + $X3 + $T54) <<< 10)}] + set C [expr {$D + (($C + [I $D $A $B] + $X10 + $T55) <<< 15)}] + set B [expr {$C + (($B + [I $C $D $A] + $X1 + $T56) <<< 21)}] + # [ABCD 8 6 57] [DABC 15 10 58] [CDAB 6 15 59] [BCDA 13 21 60] + set A [expr {$B + (($A + [I $B $C $D] + $X8 + $T57) <<< 6)}] + set D [expr {$A + (($D + [I $A $B $C] + $X15 + $T58) <<< 10)}] + set C [expr {$D + (($C + [I $D $A $B] + $X6 + $T59) <<< 15)}] + set B [expr {$C + (($B + [I $C $D $A] + $X13 + $T60) <<< 21)}] + # [ABCD 4 6 61] [DABC 11 10 62] [CDAB 2 15 63] [BCDA 9 21 64] + set A [expr {$B + (($A + [I $B $C $D] + $X4 + $T61) <<< 6)}] + set D [expr {$A + (($D + [I $A $B $C] + $X11 + $T62) <<< 10)}] + set C [expr {$D + (($C + [I $D $A $B] + $X2 + $T63) <<< 15)}] + set B [expr {$C + (($B + [I $C $D $A] + $X9 + $T64) <<< 21)}] + + # Then perform the following additions. (That is, increment each + # of the four registers by the value it had before this block + # was started.) + incr state(A) $A + incr state(B) $B + incr state(C) $C + incr state(D) $D + } + + return +} + +proc ::md5::byte {n v} {expr {((0xFF << (8 * $n)) & $v) >> (8 * $n)}} +proc ::md5::bytes {v} { + #format %c%c%c%c [byte 0 $v] [byte 1 $v] [byte 2 $v] [byte 3 $v] + format %c%c%c%c \ + [expr {0xFF & $v}] \ + [expr {(0xFF00 & $v) >> 8}] \ + [expr {(0xFF0000 & $v) >> 16}] \ + [expr {((0xFF000000 & $v) >> 24) & 0xFF}] +} + +# 32bit rotate-left +proc ::md5::<<< {v n} { + return [expr {((($v << $n) \ + | (($v >> (32 - $n)) \ + & (0x7FFFFFFF >> (31 - $n))))) \ + & 0xFFFFFFFF}] +} + +# Convert our <<< pseudo-operator into a procedure call. +regsub -all -line \ + {\[expr {(\$[ABCD]) \+ \(\((.*)\)\s+<<<\s+(\d+)\)}\]} \ + $::md5::MD5Hash_body \ + {[expr {int(\1 + [<<< [expr {\2}] \3])}]} \ + ::md5::MD5Hash_bodyX + +# RFC1321:3.4 - function F +proc ::md5::F {X Y Z} { + return [expr {($X & $Y) | ((~$X) & $Z)}] +} + +# Inline the F function +regsub -all -line \ + {\[F (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \ + $::md5::MD5Hash_bodyX \ + {( (\1 \& \2) | ((~\1) \& \3) )} \ + ::md5::MD5Hash_bodyX + +# RFC1321:3.4 - function G +proc ::md5::G {X Y Z} { + return [expr {(($X & $Z) | ($Y & (~$Z)))}] +} + +# Inline the G function +regsub -all -line \ + {\[G (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \ + $::md5::MD5Hash_bodyX \ + {(((\1 \& \3) | (\2 \& (~\3))))} \ + ::md5::MD5Hash_bodyX + +# RFC1321:3.4 - function H +proc ::md5::H {X Y Z} { + return [expr {$X ^ $Y ^ $Z}] +} + +# Inline the H function +regsub -all -line \ + {\[H (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \ + $::md5::MD5Hash_bodyX \ + {(\1 ^ \2 ^ \3)} \ + ::md5::MD5Hash_bodyX + +# RFC1321:3.4 - function I +proc ::md5::I {X Y Z} { + return [expr {$Y ^ ($X | (~$Z))}] +} + +# Inline the I function +regsub -all -line \ + {\[I (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \ + $::md5::MD5Hash_bodyX \ + {(\2 ^ (\1 | (~\3)))} \ + ::md5::MD5Hash_bodyX + + +# RFC 1321:3.4 step 4: inline the set of constant modifiers. +namespace eval md5 { + foreach tName { + T01 T02 T03 T04 T05 T06 T07 T08 T09 T10 + T11 T12 T13 T14 T15 T16 T17 T18 T19 T20 + T21 T22 T23 T24 T25 T26 T27 T28 T29 T30 + T31 T32 T33 T34 T35 T36 T37 T38 T39 T40 + T41 T42 T43 T44 T45 T46 T47 T48 T49 T50 + T51 T52 T53 T54 T55 T56 T57 T58 T59 T60 + T61 T62 T63 T64 + } tVal { + 0xd76aa478 0xe8c7b756 0x242070db 0xc1bdceee + 0xf57c0faf 0x4787c62a 0xa8304613 0xfd469501 + 0x698098d8 0x8b44f7af 0xffff5bb1 0x895cd7be + 0x6b901122 0xfd987193 0xa679438e 0x49b40821 + + 0xf61e2562 0xc040b340 0x265e5a51 0xe9b6c7aa + 0xd62f105d 0x2441453 0xd8a1e681 0xe7d3fbc8 + 0x21e1cde6 0xc33707d6 0xf4d50d87 0x455a14ed + 0xa9e3e905 0xfcefa3f8 0x676f02d9 0x8d2a4c8a + + 0xfffa3942 0x8771f681 0x6d9d6122 0xfde5380c + 0xa4beea44 0x4bdecfa9 0xf6bb4b60 0xbebfbc70 + 0x289b7ec6 0xeaa127fa 0xd4ef3085 0x4881d05 + 0xd9d4d039 0xe6db99e5 0x1fa27cf8 0xc4ac5665 + + 0xf4292244 0x432aff97 0xab9423a7 0xfc93a039 + 0x655b59c3 0x8f0ccc92 0xffeff47d 0x85845dd1 + 0x6fa87e4f 0xfe2ce6e0 0xa3014314 0x4e0811a1 + 0xf7537e82 0xbd3af235 0x2ad7d2bb 0xeb86d391 + } { + lappend map \$$tName $tVal + } + set ::md5::MD5Hash_bodyX [string map $map $::md5::MD5Hash_bodyX] + unset map +} + +# Define the MD5 hashing procedure with inline functions. +proc ::md5::MD5Hash {token msg} $::md5::MD5Hash_bodyX + +# ------------------------------------------------------------------------- + +if {[package provide Trf] != {}} { + interp alias {} ::md5::Hex {} ::hex -mode encode -- +} else { + proc ::md5::Hex {data} { + binary scan $data H* result + return [string toupper $result] + } +} + +# ------------------------------------------------------------------------- + +# LoadAccelerator -- +# +# This package can make use of a number of compiled extensions to +# accelerate the digest computation. This procedure manages the +# use of these extensions within the package. During normal usage +# this should not be called, but the test package manipulates the +# list of enabled accelerators. +# +proc ::md5::LoadAccelerator {name} { + variable accel + set r 0 + switch -exact -- $name { + critcl { + if {![catch {package require tcllibc}] + || ![catch {package require md5c}]} { + set r [expr {[info command ::md5::md5c] != {}}] + } + } + cryptkit { + if {![catch {package require cryptkit}]} { + set r [expr {![catch {cryptkit::cryptInit}]}] + } + } + trf { + if {![catch {package require Trf}]} { + set r [expr {![catch {::md5 aa} msg]}] + } + } + default { + return -code error "invalid accelerator package:\ + must be one of [join [array names accel] {, }]" + } + } + set accel($name) $r +} + +# ------------------------------------------------------------------------- + +# Description: +# Pop the nth element off a list. Used in options processing. +# +proc ::md5::Pop {varname {nth 0}} { + upvar $varname args + set r [lindex $args $nth] + set args [lreplace $args $nth $nth] + return $r +} + +# ------------------------------------------------------------------------- + +# fileevent handler for chunked file hashing. +# +proc ::md5::Chunk {token channel {chunksize 4096}} { + upvar #0 $token state + + if {[eof $channel]} { + fileevent $channel readable {} + set state(reading) 0 + } + + MD5Update $token [read $channel $chunksize] +} + +# ------------------------------------------------------------------------- + +proc ::md5::md5 {args} { + array set opts {-hex 0 -filename {} -channel {} -chunksize 4096} + while {[string match -* [set option [lindex $args 0]]]} { + switch -glob -- $option { + -hex { set opts(-hex) 1 } + -file* { set opts(-filename) [Pop args 1] } + -channel { set opts(-channel) [Pop args 1] } + -chunksize { set opts(-chunksize) [Pop args 1] } + default { + if {[llength $args] == 1} { break } + if {[string compare $option "--"] == 0} { Pop args; break } + set err [join [lsort [array names opts]] ", "] + return -code error "bad option $option:\ + must be one of $err\nlen: [llength $args]" + } + } + Pop args + } + + if {$opts(-filename) != {}} { + set opts(-channel) [open $opts(-filename) r] + fconfigure $opts(-channel) -translation binary + } + + if {$opts(-channel) == {}} { + + if {[llength $args] != 1} { + return -code error "wrong # args:\ + should be \"md5 ?-hex? -filename file | string\"" + } + set tok [MD5Init] + MD5Update $tok [lindex $args 0] + set r [MD5Final $tok] + + } else { + + set tok [MD5Init] + # FRINK: nocheck + set [subst $tok](reading) 1 + fileevent $opts(-channel) readable \ + [list [namespace origin Chunk] \ + $tok $opts(-channel) $opts(-chunksize)] + vwait [subst $tok](reading) + set r [MD5Final $tok] + + # If we opened the channel - we should close it too. + if {$opts(-filename) != {}} { + close $opts(-channel) + } + } + + if {$opts(-hex)} { + set r [Hex $r] + } + return $r +} + +# ------------------------------------------------------------------------- + +proc ::md5::hmac {args} { + array set opts {-hex 0 -filename {} -channel {} -chunksize 4096} + while {[string match -* [set option [lindex $args 0]]]} { + switch -glob -- $option { + -key { set opts(-key) [Pop args 1] } + -hex { set opts(-hex) 1 } + -file* { set opts(-filename) [Pop args 1] } + -channel { set opts(-channel) [Pop args 1] } + -chunksize { set opts(-chunksize) [Pop args 1] } + default { + if {[llength $args] == 1} { break } + if {[string compare $option "--"] == 0} { Pop args; break } + set err [join [lsort [array names opts]] ", "] + return -code error "bad option $option:\ + must be one of $err" + } + } + Pop args + } + + if {![info exists opts(-key)]} { + return -code error "wrong # args:\ + should be \"hmac ?-hex? -key key -filename file | string\"" + } + + if {$opts(-filename) != {}} { + set opts(-channel) [open $opts(-filename) r] + fconfigure $opts(-channel) -translation binary + } + + if {$opts(-channel) == {}} { + + if {[llength $args] != 1} { + return -code error "wrong # args:\ + should be \"hmac ?-hex? -key key -filename file | string\"" + } + set tok [HMACInit $opts(-key)] + HMACUpdate $tok [lindex $args 0] + set r [HMACFinal $tok] + + } else { + + set tok [HMACInit $opts(-key)] + # FRINK: nocheck + set [subst $tok](reading) 1 + fileevent $opts(-channel) readable \ + [list [namespace origin Chunk] \ + $tok $opts(-channel) $opts(-chunksize)] + vwait [subst $tok](reading) + set r [HMACFinal $tok] + + # If we opened the channel - we should close it too. + if {$opts(-filename) != {}} { + close $opts(-channel) + } + } + + if {$opts(-hex)} { + set r [Hex $r] + } + return $r +} + +# ------------------------------------------------------------------------- + +# Try and load a compiled extension to help. +namespace eval ::md5 { + foreach e {critcl cryptkit trf} { if {[LoadAccelerator $e]} { break } } +} + +package provide md5 $::md5::version + +# ------------------------------------------------------------------------- +# Local Variables: +# mode: tcl +# indent-tabs-mode: nil +# End: + + diff --git a/lib/md5/pkgIndex.tcl b/lib/md5/pkgIndex.tcl new file mode 100644 index 0000000..1c436f0 --- /dev/null +++ b/lib/md5/pkgIndex.tcl @@ -0,0 +1,3 @@ +if {![package vsatisfies [package provide Tcl] 8.2]} {return} +package ifneeded md5 2.0.5 [list source [file join $dir md5x.tcl]] +package ifneeded md5 1.4.4 [list source [file join $dir md5.tcl]] diff --git a/lib/sha1/pkgIndex.tcl b/lib/sha1/pkgIndex.tcl new file mode 100644 index 0000000..297187e --- /dev/null +++ b/lib/sha1/pkgIndex.tcl @@ -0,0 +1,14 @@ +# Tcl package index file, version 1.1 +# This file is generated by the "pkg_mkIndex" command +# and sourced either when an application starts up or +# by a "package unknown" script. It invokes the +# "package ifneeded" command to set up package-related +# information so that packages will be loaded automatically +# in response to "package require" commands. When this +# script is sourced, the variable $dir must contain the +# full path name of this file's directory. + +if {![package vsatisfies [package provide Tcl] 8.2]} {return} +package ifneeded sha256 1.0.2 [list source [file join $dir sha256.tcl]] +package ifneeded sha1 2.0.3 [list source [file join $dir sha1.tcl]] +package ifneeded sha1 1.1.0 [list source [file join $dir sha1v1.tcl]] diff --git a/lib/sha1/sha1.tcl b/lib/sha1/sha1.tcl new file mode 100644 index 0000000..125c8f6 --- /dev/null +++ b/lib/sha1/sha1.tcl @@ -0,0 +1,818 @@ +# sha1.tcl - +# +# Copyright (C) 2001 Don Libes +# Copyright (C) 2003 Pat Thoyts +# +# SHA1 defined by FIPS 180-1, "The SHA1 Message-Digest Algorithm" +# HMAC defined by RFC 2104, "Keyed-Hashing for Message Authentication" +# +# This is an implementation of SHA1 based upon the example code given in +# FIPS 180-1 and upon the tcllib MD4 implementation and taking some ideas +# and methods from the earlier tcllib sha1 version by Don Libes. +# +# This implementation permits incremental updating of the hash and +# provides support for external compiled implementations either using +# critcl (sha1c) or Trf. +# +# ref: http://www.itl.nist.gov/fipspubs/fip180-1.htm +# +# ------------------------------------------------------------------------- +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# ------------------------------------------------------------------------- +# +# $Id: sha1.tcl,v 1.21 2007/05/03 21:41:10 andreas_kupries Exp $ + +# @mdgen EXCLUDE: sha1c.tcl + +package require Tcl 8.2; # tcl minimum version + +namespace eval ::sha1 { + variable version 2.0.3 + variable rcsid {$Id: sha1.tcl,v 1.21 2007/05/03 21:41:10 andreas_kupries Exp $} + + variable accel + array set accel {tcl 0 critcl 0 cryptkit 0 trf 0} + variable loaded {} + variable active + array set active {tcl 0 critcl 0 cryptkit 0 trf 0} + + namespace export sha1 hmac SHA1Init SHA1Update SHA1Final + + variable uid + if {![info exists uid]} { + set uid 0 + } +} + +# ------------------------------------------------------------------------- +# Management of sha1 implementations. + +# LoadAccelerator -- +# +# This package can make use of a number of compiled extensions to +# accelerate the digest computation. This procedure manages the +# use of these extensions within the package. During normal usage +# this should not be called, but the test package manipulates the +# list of enabled accelerators. +# +proc ::sha1::LoadAccelerator {name} { + variable accel + set r 0 + switch -exact -- $name { + tcl { + # Already present (this file) + set r 1 + } + critcl { + if {![catch {package require tcllibc}] + || ![catch {package require sha1c}]} { + set r [expr {[info command ::sha1::sha1c] != {}}] + } + } + cryptkit { + if {![catch {package require cryptkit}]} { + set r [expr {![catch {cryptkit::cryptInit}]}] + } + } + trf { + if {![catch {package require Trf}]} { + set r [expr {![catch {::sha1 aa} msg]}] + } + } + default { + return -code error "invalid accelerator $key:\ + must be one of [join [KnownImplementations] {, }]" + } + } + set accel($name) $r + return $r +} + +# ::sha1::Implementations -- +# +# Determines which implementations are +# present, i.e. loaded. +# +# Arguments: +# None. +# +# Results: +# A list of implementation keys. + +proc ::sha1::Implementations {} { + variable accel + set res {} + foreach n [array names accel] { + if {!$accel($n)} continue + lappend res $n + } + return $res +} + +# ::sha1::KnownImplementations -- +# +# Determines which implementations are known +# as possible implementations. +# +# Arguments: +# None. +# +# Results: +# A list of implementation keys. In the order +# of preference, most prefered first. + +proc ::sha1::KnownImplementations {} { + return {critcl cryptkit trf tcl} +} + +proc ::sha1::Names {} { + return { + critcl {tcllibc based} + cryptkit {cryptkit based} + trf {Trf based} + tcl {pure Tcl} + } +} + +# ::sha1::SwitchTo -- +# +# Activates a loaded named implementation. +# +# Arguments: +# key Name of the implementation to activate. +# +# Results: +# None. + +proc ::sha1::SwitchTo {key} { + variable accel + variable active + variable loaded + + if {[string equal $key $loaded]} { + # No change, nothing to do. + return + } elseif {![string equal $key ""]} { + # Validate the target implementation of the switch. + + if {![info exists accel($key)]} { + return -code error "Unable to activate unknown implementation \"$key\"" + } elseif {![info exists accel($key)] || !$accel($key)} { + return -code error "Unable to activate missing implementation \"$key\"" + } + } + + if {![string equal $loaded ""]} { + set active($loaded) 0 + } + if {![string equal $key ""]} { + set active($key) 1 + } + + # Remember the active implementation, for deactivation by future + # switches. + + set loaded $key + return +} + +# ------------------------------------------------------------------------- + +# SHA1Init -- +# +# Create and initialize an SHA1 state variable. This will be +# cleaned up when we call SHA1Final +# + +proc ::sha1::SHA1Init {} { + variable active + variable uid + set token [namespace current]::[incr uid] + upvar #0 $token state + + # FIPS 180-1: 7 - Initialize the hash state + array set state \ + [list \ + A [expr {int(0x67452301)}] \ + B [expr {int(0xEFCDAB89)}] \ + C [expr {int(0x98BADCFE)}] \ + D [expr {int(0x10325476)}] \ + E [expr {int(0xC3D2E1F0)}] \ + n 0 i "" ] + if {$active(cryptkit)} { + cryptkit::cryptCreateContext state(ckctx) CRYPT_UNUSED CRYPT_ALGO_SHA + } elseif {$active(trf)} { + set s {} + switch -exact -- $::tcl_platform(platform) { + windows { set s [open NUL w] } + unix { set s [open /dev/null w] } + } + if {$s != {}} { + fconfigure $s -translation binary -buffering none + ::sha1 -attach $s -mode write \ + -read-type variable \ + -read-destination [subst $token](trfread) \ + -write-type variable \ + -write-destination [subst $token](trfwrite) + array set state [list trfread 0 trfwrite 0 trf $s] + } + } + return $token +} + +# SHA1Update -- +# +# This is called to add more data into the hash. You may call this +# as many times as you require. Note that passing in "ABC" is equivalent +# to passing these letters in as separate calls -- hence this proc +# permits hashing of chunked data +# +# If we have a C-based implementation available, then we will use +# it here in preference to the pure-Tcl implementation. +# +proc ::sha1::SHA1Update {token data} { + variable active + upvar #0 $token state + + if {$active(critcl)} { + if {[info exists state(sha1c)]} { + set state(sha1c) [sha1c $data $state(sha1c)] + } else { + set state(sha1c) [sha1c $data] + } + return + } elseif {[info exists state(ckctx)]} { + if {[string length $data] > 0} { + cryptkit::cryptEncrypt $state(ckctx) $data + } + return + } elseif {[info exists state(trf)]} { + puts -nonewline $state(trf) $data + return + } + + # Update the state values + incr state(n) [string length $data] + append state(i) $data + + # Calculate the hash for any complete blocks + set len [string length $state(i)] + for {set n 0} {($n + 64) <= $len} {} { + SHA1Transform $token [string range $state(i) $n [incr n 64]] + } + + # Adjust the state for the blocks completed. + set state(i) [string range $state(i) $n end] + return +} + +# SHA1Final -- +# +# This procedure is used to close the current hash and returns the +# hash data. Once this procedure has been called the hash context +# is freed and cannot be used again. +# +# Note that the output is 160 bits represented as binary data. +# +proc ::sha1::SHA1Final {token} { + upvar #0 $token state + + # Check for either of the C-compiled versions. + if {[info exists state(sha1c)]} { + set r $state(sha1c) + unset state + return $r + } elseif {[info exists state(ckctx)]} { + cryptkit::cryptEncrypt $state(ckctx) "" + cryptkit::cryptGetAttributeString $state(ckctx) \ + CRYPT_CTXINFO_HASHVALUE r 20 + cryptkit::cryptDestroyContext $state(ckctx) + # If nothing was hashed, we get no r variable set! + if {[info exists r]} { + unset state + return $r + } + } elseif {[info exists state(trf)]} { + close $state(trf) + set r $state(trfwrite) + unset state + return $r + } + + # Padding + # + set len [string length $state(i)] + set pad [expr {56 - ($len % 64)}] + if {$len % 64 > 56} { + incr pad 64 + } + if {$pad == 0} { + incr pad 64 + } + append state(i) [binary format a$pad \x80] + + # Append length in bits as big-endian wide int. + set dlen [expr {8 * $state(n)}] + append state(i) [binary format II 0 $dlen] + + # Calculate the hash for the remaining block. + set len [string length $state(i)] + for {set n 0} {($n + 64) <= $len} {} { + SHA1Transform $token [string range $state(i) $n [incr n 64]] + } + + # Output + set r [bytes $state(A)][bytes $state(B)][bytes $state(C)][bytes $state(D)][bytes $state(E)] + unset state + return $r +} + +# ------------------------------------------------------------------------- +# HMAC Hashed Message Authentication (RFC 2104) +# +# hmac = H(K xor opad, H(K xor ipad, text)) +# + +# HMACInit -- +# +# This is equivalent to the SHA1Init procedure except that a key is +# added into the algorithm +# +proc ::sha1::HMACInit {K} { + + # Key K is adjusted to be 64 bytes long. If K is larger, then use + # the SHA1 digest of K and pad this instead. + set len [string length $K] + if {$len > 64} { + set tok [SHA1Init] + SHA1Update $tok $K + set K [SHA1Final $tok] + set len [string length $K] + } + set pad [expr {64 - $len}] + append K [string repeat \0 $pad] + + # Cacluate the padding buffers. + set Ki {} + set Ko {} + binary scan $K i16 Ks + foreach k $Ks { + append Ki [binary format i [expr {$k ^ 0x36363636}]] + append Ko [binary format i [expr {$k ^ 0x5c5c5c5c}]] + } + + set tok [SHA1Init] + SHA1Update $tok $Ki; # initialize with the inner pad + + # preserve the Ko value for the final stage. + # FRINK: nocheck + set [subst $tok](Ko) $Ko + + return $tok +} + +# HMACUpdate -- +# +# Identical to calling SHA1Update +# +proc ::sha1::HMACUpdate {token data} { + SHA1Update $token $data + return +} + +# HMACFinal -- +# +# This is equivalent to the SHA1Final procedure. The hash context is +# closed and the binary representation of the hash result is returned. +# +proc ::sha1::HMACFinal {token} { + upvar #0 $token state + + set tok [SHA1Init]; # init the outer hashing function + SHA1Update $tok $state(Ko); # prepare with the outer pad. + SHA1Update $tok [SHA1Final $token]; # hash the inner result + return [SHA1Final $tok] +} + +# ------------------------------------------------------------------------- +# Description: +# This is the core SHA1 algorithm. It is a lot like the MD4 algorithm but +# includes an extra round and a set of constant modifiers throughout. +# +set ::sha1::SHA1Transform_body { + upvar #0 $token state + + # FIPS 180-1: 7a: Process Message in 16-Word Blocks + binary scan $msg I* blocks + set blockLen [llength $blocks] + for {set i 0} {$i < $blockLen} {incr i 16} { + set W [lrange $blocks $i [expr {$i+15}]] + + # FIPS 180-1: 7b: Expand the input into 80 words + # For t = 16 to 79 + # let Wt = (Wt-3 ^ Wt-8 ^ Wt-14 ^ Wt-16) <<< 1 + set t3 12 + set t8 7 + set t14 1 + set t16 -1 + for {set t 16} {$t < 80} {incr t} { + set x [expr {[lindex $W [incr t3]] ^ [lindex $W [incr t8]] ^ \ + [lindex $W [incr t14]] ^ [lindex $W [incr t16]]}] + lappend W [expr {int(($x << 1) | (($x >> 31) & 1))}] + } + + # FIPS 180-1: 7c: Copy hash state. + set A $state(A) + set B $state(B) + set C $state(C) + set D $state(D) + set E $state(E) + + # FIPS 180-1: 7d: Do permutation rounds + # For t = 0 to 79 do + # TEMP = (A<<<5) + ft(B,C,D) + E + Wt + Kt; + # E = D; D = C; C = S30(B); B = A; A = TEMP; + + # Round 1: ft(B,C,D) = (B & C) | (~B & D) ( 0 <= t <= 19) + for {set t 0} {$t < 20} {incr t} { + set TEMP [F1 $A $B $C $D $E [lindex $W $t]] + set E $D + set D $C + set C [rotl32 $B 30] + set B $A + set A $TEMP + } + + # Round 2: ft(B,C,D) = (B ^ C ^ D) ( 20 <= t <= 39) + for {} {$t < 40} {incr t} { + set TEMP [F2 $A $B $C $D $E [lindex $W $t]] + set E $D + set D $C + set C [rotl32 $B 30] + set B $A + set A $TEMP + } + + # Round 3: ft(B,C,D) = ((B & C) | (B & D) | (C & D)) ( 40 <= t <= 59) + for {} {$t < 60} {incr t} { + set TEMP [F3 $A $B $C $D $E [lindex $W $t]] + set E $D + set D $C + set C [rotl32 $B 30] + set B $A + set A $TEMP + } + + # Round 4: ft(B,C,D) = (B ^ C ^ D) ( 60 <= t <= 79) + for {} {$t < 80} {incr t} { + set TEMP [F4 $A $B $C $D $E [lindex $W $t]] + set E $D + set D $C + set C [rotl32 $B 30] + set B $A + set A $TEMP + } + + # Then perform the following additions. (That is, increment each + # of the four registers by the value it had before this block + # was started.) + incr state(A) $A + incr state(B) $B + incr state(C) $C + incr state(D) $D + incr state(E) $E + } + + return +} + +proc ::sha1::F1 {A B C D E W} { + expr {(((($A << 5) & 0xffffffff) | (($A >> 27) & 0x1f)) \ + + ($D ^ ($B & ($C ^ $D))) + $E + $W + 0x5a827999) & 0xffffffff} +} + +proc ::sha1::F2 {A B C D E W} { + expr {(((($A << 5) & 0xffffffff) | (($A >> 27) & 0x1f)) \ + + ($B ^ $C ^ $D) + $E + $W + 0x6ed9eba1) & 0xffffffff} +} + +proc ::sha1::F3 {A B C D E W} { + expr {(((($A << 5) & 0xffffffff)| (($A >> 27) & 0x1f)) \ + + (($B & $C) | ($D & ($B | $C))) + $E + $W + 0x8f1bbcdc) & 0xffffffff} +} + +proc ::sha1::F4 {A B C D E W} { + expr {(((($A << 5) & 0xffffffff)| (($A >> 27) & 0x1f)) \ + + ($B ^ $C ^ $D) + $E + $W + 0xca62c1d6) & 0xffffffff} +} + +proc ::sha1::rotl32 {v n} { + return [expr {((($v << $n) \ + | (($v >> (32 - $n)) \ + & (0x7FFFFFFF >> (31 - $n))))) \ + & 0xFFFFFFFF}] +} + + +# ------------------------------------------------------------------------- +# +# In order to get this code to go as fast as possible while leaving +# the main code readable we can substitute the above function bodies +# into the transform procedure. This inlines the code for us an avoids +# a procedure call overhead within the loops. +# +# We can do some minor tweaking to improve speed on Tcl < 8.5 where we +# know our arithmetic is limited to 64 bits. On > 8.5 we may have +# unconstrained integer arithmetic and must avoid letting it run away. +# + +regsub -all -line \ + {\[F1 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body \ + {[expr {(rotl32($A,5) + ($D ^ ($B \& ($C ^ $D))) + $E + \1 + 0x5a827999) \& 0xffffffff}]} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {\[F2 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp \ + {[expr {(rotl32($A,5) + ($B ^ $C ^ $D) + $E + \1 + 0x6ed9eba1) \& 0xffffffff}]} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {\[F3 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp \ + {[expr {(rotl32($A,5) + (($B \& $C) | ($D \& ($B | $C))) + $E + \1 + 0x8f1bbcdc) \& 0xffffffff}]} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {\[F4 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp \ + {[expr {(rotl32($A,5) + ($B ^ $C ^ $D) + $E + \1 + 0xca62c1d6) \& 0xffffffff}]} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {rotl32\(\$A,5\)} \ + $::sha1::SHA1Transform_body_tmp \ + {((($A << 5) \& 0xffffffff) | (($A >> 27) \& 0x1f))} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {\[rotl32 \$B 30\]} \ + $::sha1::SHA1Transform_body_tmp \ + {[expr {int(($B << 30) | (($B >> 2) \& 0x3fffffff))}]} \ + ::sha1::SHA1Transform_body_tmp +# +# Version 2 avoids a few truncations to 32 bits in non-essential places. +# +regsub -all -line \ + {\[F1 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body \ + {[expr {rotl32($A,5) + ($D ^ ($B \& ($C ^ $D))) + $E + \1 + 0x5a827999}]} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {\[F2 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp2 \ + {[expr {rotl32($A,5) + ($B ^ $C ^ $D) + $E + \1 + 0x6ed9eba1}]} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {\[F3 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp2 \ + {[expr {rotl32($A,5) + (($B \& $C) | ($D \& ($B | $C))) + $E + \1 + 0x8f1bbcdc}]} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {\[F4 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp2 \ + {[expr {rotl32($A,5) + ($B ^ $C ^ $D) + $E + \1 + 0xca62c1d6}]} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {rotl32\(\$A,5\)} \ + $::sha1::SHA1Transform_body_tmp2 \ + {(($A << 5) | (($A >> 27) \& 0x1f))} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {\[rotl32 \$B 30\]} \ + $::sha1::SHA1Transform_body_tmp2 \ + {[expr {($B << 30) | (($B >> 2) \& 0x3fffffff)}]} \ + ::sha1::SHA1Transform_body_tmp2 + +if {[package vsatisfies [package provide Tcl] 8.5]} { + proc ::sha1::SHA1Transform {token msg} $::sha1::SHA1Transform_body_tmp +} else { + proc ::sha1::SHA1Transform {token msg} $::sha1::SHA1Transform_body_tmp2 +} + +unset ::sha1::SHA1Transform_body +unset ::sha1::SHA1Transform_body_tmp +unset ::sha1::SHA1Transform_body_tmp2 + +# ------------------------------------------------------------------------- + +proc ::sha1::byte {n v} {expr {((0xFF << (8 * $n)) & $v) >> (8 * $n)}} +proc ::sha1::bytes {v} { + #format %c%c%c%c [byte 0 $v] [byte 1 $v] [byte 2 $v] [byte 3 $v] + format %c%c%c%c \ + [expr {((0xFF000000 & $v) >> 24) & 0xFF}] \ + [expr {(0xFF0000 & $v) >> 16}] \ + [expr {(0xFF00 & $v) >> 8}] \ + [expr {0xFF & $v}] +} + +# ------------------------------------------------------------------------- + +proc ::sha1::Hex {data} { + binary scan $data H* result + return $result +} + +# ------------------------------------------------------------------------- + +# Description: +# Pop the nth element off a list. Used in options processing. +# +proc ::sha1::Pop {varname {nth 0}} { + upvar $varname args + set r [lindex $args $nth] + set args [lreplace $args $nth $nth] + return $r +} + +# ------------------------------------------------------------------------- + +# fileevent handler for chunked file hashing. +# +proc ::sha1::Chunk {token channel {chunksize 4096}} { + upvar #0 $token state + + if {[eof $channel]} { + fileevent $channel readable {} + set state(reading) 0 + } + + SHA1Update $token [read $channel $chunksize] +} + +# ------------------------------------------------------------------------- + +proc ::sha1::sha1 {args} { + array set opts {-hex 0 -filename {} -channel {} -chunksize 4096} + if {[llength $args] == 1} { + set opts(-hex) 1 + } else { + while {[string match -* [set option [lindex $args 0]]]} { + switch -glob -- $option { + -hex { set opts(-hex) 1 } + -bin { set opts(-hex) 0 } + -file* { set opts(-filename) [Pop args 1] } + -channel { set opts(-channel) [Pop args 1] } + -chunksize { set opts(-chunksize) [Pop args 1] } + default { + if {[llength $args] == 1} { break } + if {[string compare $option "--"] == 0} { Pop args; break } + set err [join [lsort [concat -bin [array names opts]]] ", "] + return -code error "bad option $option:\ + must be one of $err" + } + } + Pop args + } + } + + if {$opts(-filename) != {}} { + set opts(-channel) [open $opts(-filename) r] + fconfigure $opts(-channel) -translation binary + } + + if {$opts(-channel) == {}} { + + if {[llength $args] != 1} { + return -code error "wrong # args:\ + should be \"sha1 ?-hex? -filename file | string\"" + } + set tok [SHA1Init] + SHA1Update $tok [lindex $args 0] + set r [SHA1Final $tok] + + } else { + + set tok [SHA1Init] + # FRINK: nocheck + set [subst $tok](reading) 1 + fileevent $opts(-channel) readable \ + [list [namespace origin Chunk] \ + $tok $opts(-channel) $opts(-chunksize)] + # FRINK: nocheck + vwait [subst $tok](reading) + set r [SHA1Final $tok] + + # If we opened the channel - we should close it too. + if {$opts(-filename) != {}} { + close $opts(-channel) + } + } + + if {$opts(-hex)} { + set r [Hex $r] + } + return $r +} + +# ------------------------------------------------------------------------- + +proc ::sha1::hmac {args} { + array set opts {-hex 1 -filename {} -channel {} -chunksize 4096} + if {[llength $args] != 2} { + while {[string match -* [set option [lindex $args 0]]]} { + switch -glob -- $option { + -key { set opts(-key) [Pop args 1] } + -hex { set opts(-hex) 1 } + -bin { set opts(-hex) 0 } + -file* { set opts(-filename) [Pop args 1] } + -channel { set opts(-channel) [Pop args 1] } + -chunksize { set opts(-chunksize) [Pop args 1] } + default { + if {[llength $args] == 1} { break } + if {[string compare $option "--"] == 0} { Pop args; break } + set err [join [lsort [array names opts]] ", "] + return -code error "bad option $option:\ + must be one of $err" + } + } + Pop args + } + } + + if {[llength $args] == 2} { + set opts(-key) [Pop args] + } + + if {![info exists opts(-key)]} { + return -code error "wrong # args:\ + should be \"hmac ?-hex? -key key -filename file | string\"" + } + + if {$opts(-filename) != {}} { + set opts(-channel) [open $opts(-filename) r] + fconfigure $opts(-channel) -translation binary + } + + if {$opts(-channel) == {}} { + + if {[llength $args] != 1} { + return -code error "wrong # args:\ + should be \"hmac ?-hex? -key key -filename file | string\"" + } + set tok [HMACInit $opts(-key)] + HMACUpdate $tok [lindex $args 0] + set r [HMACFinal $tok] + + } else { + + set tok [HMACInit $opts(-key)] + # FRINK: nocheck + set [subst $tok](reading) 1 + fileevent $opts(-channel) readable \ + [list [namespace origin Chunk] \ + $tok $opts(-channel) $opts(-chunksize)] + # FRINK: nocheck + vwait [subst $tok](reading) + set r [HMACFinal $tok] + + # If we opened the channel - we should close it too. + if {$opts(-filename) != {}} { + close $opts(-channel) + } + } + + if {$opts(-hex)} { + set r [Hex $r] + } + return $r +} + +# ------------------------------------------------------------------------- + +# Try and load a compiled extension to help. +namespace eval ::sha1 { + variable e {} + foreach e [KnownImplementations] { + if {[LoadAccelerator $e]} { + SwitchTo $e + break + } + } + unset e +} + +package provide sha1 $::sha1::version + +# ------------------------------------------------------------------------- +# Local Variables: +# mode: tcl +# indent-tabs-mode: nil +# End: diff --git a/lib/sha1/sha1v1.tcl b/lib/sha1/sha1v1.tcl new file mode 100644 index 0000000..057560d --- /dev/null +++ b/lib/sha1/sha1v1.tcl @@ -0,0 +1,713 @@ +# sha1.tcl - +# +# Copyright (C) 2001 Don Libes +# Copyright (C) 2003 Pat Thoyts +# +# SHA1 defined by FIPS 180-1, "The SHA1 Message-Digest Algorithm" +# HMAC defined by RFC 2104, "Keyed-Hashing for Message Authentication" +# +# This is an implementation of SHA1 based upon the example code given in +# FIPS 180-1 and upon the tcllib MD4 implementation and taking some ideas +# and methods from the earlier tcllib sha1 version by Don Libes. +# +# This implementation permits incremental updating of the hash and +# provides support for external compiled implementations either using +# critcl (sha1c) or Trf. +# +# ref: http://www.itl.nist.gov/fipspubs/fip180-1.htm +# +# ------------------------------------------------------------------------- +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# ------------------------------------------------------------------------- +# +# $Id: sha1v1.tcl,v 1.1 2006/03/12 22:46:13 andreas_kupries Exp $ + +# @mdgen EXCLUDE: sha1c.tcl + +package require Tcl 8.2; # tcl minimum version + +namespace eval ::sha1 { + variable version 1.1.0 + variable rcsid {$Id: sha1v1.tcl,v 1.1 2006/03/12 22:46:13 andreas_kupries Exp $} + variable accel + array set accel {critcl 0 cryptkit 0 trf 0} + + namespace export sha1 hmac SHA1Init SHA1Update SHA1Final + + variable uid + if {![info exists uid]} { + set uid 0 + } +} + +# ------------------------------------------------------------------------- + +# SHA1Init -- +# +# Create and initialize an SHA1 state variable. This will be +# cleaned up when we call SHA1Final +# +proc ::sha1::SHA1Init {} { + variable accel + variable uid + set token [namespace current]::[incr uid] + upvar #0 $token state + + # FIPS 180-1: 7 - Initialize the hash state + array set state \ + [list \ + A [expr {int(0x67452301)}] \ + B [expr {int(0xEFCDAB89)}] \ + C [expr {int(0x98BADCFE)}] \ + D [expr {int(0x10325476)}] \ + E [expr {int(0xC3D2E1F0)}] \ + n 0 i "" ] + if {$accel(cryptkit)} { + cryptkit::cryptCreateContext state(ckctx) CRYPT_UNUSED CRYPT_ALGO_SHA + } elseif {$accel(trf)} { + set s {} + switch -exact -- $::tcl_platform(platform) { + windows { set s [open NUL w] } + unix { set s [open /dev/null w] } + } + if {$s != {}} { + fconfigure $s -translation binary -buffering none + ::sha1 -attach $s -mode write \ + -read-type variable \ + -read-destination [subst $token](trfread) \ + -write-type variable \ + -write-destination [subst $token](trfwrite) + array set state [list trfread 0 trfwrite 0 trf $s] + } + } + return $token +} + +# SHA1Update -- +# +# This is called to add more data into the hash. You may call this +# as many times as you require. Note that passing in "ABC" is equivalent +# to passing these letters in as separate calls -- hence this proc +# permits hashing of chunked data +# +# If we have a C-based implementation available, then we will use +# it here in preference to the pure-Tcl implementation. +# +proc ::sha1::SHA1Update {token data} { + variable accel + upvar #0 $token state + + if {$accel(critcl)} { + if {[info exists state(sha1c)]} { + set state(sha1c) [sha1c $data $state(sha1c)] + } else { + set state(sha1c) [sha1c $data] + } + return + } elseif {[info exists state(ckctx)]} { + if {[string length $data] > 0} { + cryptkit::cryptEncrypt $state(ckctx) $data + } + return + } elseif {[info exists state(trf)]} { + puts -nonewline $state(trf) $data + return + } + + # Update the state values + incr state(n) [string length $data] + append state(i) $data + + # Calculate the hash for any complete blocks + set len [string length $state(i)] + for {set n 0} {($n + 64) <= $len} {} { + SHA1Transform $token [string range $state(i) $n [incr n 64]] + } + + # Adjust the state for the blocks completed. + set state(i) [string range $state(i) $n end] + return +} + +# SHA1Final -- +# +# This procedure is used to close the current hash and returns the +# hash data. Once this procedure has been called the hash context +# is freed and cannot be used again. +# +# Note that the output is 160 bits represented as binary data. +# +proc ::sha1::SHA1Final {token} { + upvar #0 $token state + + # Check for either of the C-compiled versions. + if {[info exists state(sha1c)]} { + set r $state(sha1c) + unset state + return $r + } elseif {[info exists state(ckctx)]} { + cryptkit::cryptEncrypt $state(ckctx) "" + cryptkit::cryptGetAttributeString $state(ckctx) \ + CRYPT_CTXINFO_HASHVALUE r 20 + cryptkit::cryptDestroyContext $state(ckctx) + # If nothing was hashed, we get no r variable set! + if {[info exists r]} { + unset state + return $r + } + } elseif {[info exists state(trf)]} { + close $state(trf) + set r $state(trfwrite) + unset state + return $r + } + + # Padding + # + set len [string length $state(i)] + set pad [expr {56 - ($len % 64)}] + if {$len % 64 > 56} { + incr pad 64 + } + if {$pad == 0} { + incr pad 64 + } + append state(i) [binary format a$pad \x80] + + # Append length in bits as big-endian wide int. + set dlen [expr {8 * $state(n)}] + append state(i) [binary format II 0 $dlen] + + # Calculate the hash for the remaining block. + set len [string length $state(i)] + for {set n 0} {($n + 64) <= $len} {} { + SHA1Transform $token [string range $state(i) $n [incr n 64]] + } + + # Output + set r [bytes $state(A)][bytes $state(B)][bytes $state(C)][bytes $state(D)][bytes $state(E)] + unset state + return $r +} + +# ------------------------------------------------------------------------- +# HMAC Hashed Message Authentication (RFC 2104) +# +# hmac = H(K xor opad, H(K xor ipad, text)) +# + +# HMACInit -- +# +# This is equivalent to the SHA1Init procedure except that a key is +# added into the algorithm +# +proc ::sha1::HMACInit {K} { + + # Key K is adjusted to be 64 bytes long. If K is larger, then use + # the SHA1 digest of K and pad this instead. + set len [string length $K] + if {$len > 64} { + set tok [SHA1Init] + SHA1Update $tok $K + set K [SHA1Final $tok] + set len [string length $K] + } + set pad [expr {64 - $len}] + append K [string repeat \0 $pad] + + # Cacluate the padding buffers. + set Ki {} + set Ko {} + binary scan $K i16 Ks + foreach k $Ks { + append Ki [binary format i [expr {$k ^ 0x36363636}]] + append Ko [binary format i [expr {$k ^ 0x5c5c5c5c}]] + } + + set tok [SHA1Init] + SHA1Update $tok $Ki; # initialize with the inner pad + + # preserve the Ko value for the final stage. + # FRINK: nocheck + set [subst $tok](Ko) $Ko + + return $tok +} + +# HMACUpdate -- +# +# Identical to calling SHA1Update +# +proc ::sha1::HMACUpdate {token data} { + SHA1Update $token $data + return +} + +# HMACFinal -- +# +# This is equivalent to the SHA1Final procedure. The hash context is +# closed and the binary representation of the hash result is returned. +# +proc ::sha1::HMACFinal {token} { + upvar #0 $token state + + set tok [SHA1Init]; # init the outer hashing function + SHA1Update $tok $state(Ko); # prepare with the outer pad. + SHA1Update $tok [SHA1Final $token]; # hash the inner result + return [SHA1Final $tok] +} + +# ------------------------------------------------------------------------- +# Description: +# This is the core SHA1 algorithm. It is a lot like the MD4 algorithm but +# includes an extra round and a set of constant modifiers throughout. +# +set ::sha1::SHA1Transform_body { + upvar #0 $token state + + # FIPS 180-1: 7a: Process Message in 16-Word Blocks + binary scan $msg I* blocks + set blockLen [llength $blocks] + for {set i 0} {$i < $blockLen} {incr i 16} { + set W [lrange $blocks $i [expr {$i+15}]] + + # FIPS 180-1: 7b: Expand the input into 80 words + # For t = 16 to 79 + # let Wt = (Wt-3 ^ Wt-8 ^ Wt-14 ^ Wt-16) <<< 1 + set t3 12 + set t8 7 + set t14 1 + set t16 -1 + for {set t 16} {$t < 80} {incr t} { + set x [expr {[lindex $W [incr t3]] ^ [lindex $W [incr t8]] ^ \ + [lindex $W [incr t14]] ^ [lindex $W [incr t16]]}] + lappend W [expr {int(($x << 1) | (($x >> 31) & 1))}] + } + + # FIPS 180-1: 7c: Copy hash state. + set A $state(A) + set B $state(B) + set C $state(C) + set D $state(D) + set E $state(E) + + # FIPS 180-1: 7d: Do permutation rounds + # For t = 0 to 79 do + # TEMP = (A<<<5) + ft(B,C,D) + E + Wt + Kt; + # E = D; D = C; C = S30(B); B = A; A = TEMP; + + # Round 1: ft(B,C,D) = (B & C) | (~B & D) ( 0 <= t <= 19) + for {set t 0} {$t < 20} {incr t} { + set TEMP [F1 $A $B $C $D $E [lindex $W $t]] + set E $D + set D $C + set C [rotl32 $B 30] + set B $A + set A $TEMP + } + + # Round 2: ft(B,C,D) = (B ^ C ^ D) ( 20 <= t <= 39) + for {} {$t < 40} {incr t} { + set TEMP [F2 $A $B $C $D $E [lindex $W $t]] + set E $D + set D $C + set C [rotl32 $B 30] + set B $A + set A $TEMP + } + + # Round 3: ft(B,C,D) = ((B & C) | (B & D) | (C & D)) ( 40 <= t <= 59) + for {} {$t < 60} {incr t} { + set TEMP [F3 $A $B $C $D $E [lindex $W $t]] + set E $D + set D $C + set C [rotl32 $B 30] + set B $A + set A $TEMP + } + + # Round 4: ft(B,C,D) = (B ^ C ^ D) ( 60 <= t <= 79) + for {} {$t < 80} {incr t} { + set TEMP [F4 $A $B $C $D $E [lindex $W $t]] + set E $D + set D $C + set C [rotl32 $B 30] + set B $A + set A $TEMP + } + + # Then perform the following additions. (That is, increment each + # of the four registers by the value it had before this block + # was started.) + incr state(A) $A + incr state(B) $B + incr state(C) $C + incr state(D) $D + incr state(E) $E + } + + return +} + +proc ::sha1::F1 {A B C D E W} { + expr {(((($A << 5) & 0xffffffff) | (($A >> 27) & 0x1f)) \ + + ($D ^ ($B & ($C ^ $D))) + $E + $W + 0x5a827999) & 0xffffffff} +} + +proc ::sha1::F2 {A B C D E W} { + expr {(((($A << 5) & 0xffffffff) | (($A >> 27) & 0x1f)) \ + + ($B ^ $C ^ $D) + $E + $W + 0x6ed9eba1) & 0xffffffff} +} + +proc ::sha1::F3 {A B C D E W} { + expr {(((($A << 5) & 0xffffffff)| (($A >> 27) & 0x1f)) \ + + (($B & $C) | ($D & ($B | $C))) + $E + $W + 0x8f1bbcdc) & 0xffffffff} +} + +proc ::sha1::F4 {A B C D E W} { + expr {(((($A << 5) & 0xffffffff)| (($A >> 27) & 0x1f)) \ + + ($B ^ $C ^ $D) + $E + $W + 0xca62c1d6) & 0xffffffff} +} + +proc ::sha1::rotl32 {v n} { + return [expr {((($v << $n) \ + | (($v >> (32 - $n)) \ + & (0x7FFFFFFF >> (31 - $n))))) \ + & 0xFFFFFFFF}] +} + + +# ------------------------------------------------------------------------- +# +# In order to get this code to go as fast as possible while leaving +# the main code readable we can substitute the above function bodies +# into the transform procedure. This inlines the code for us an avoids +# a procedure call overhead within the loops. +# +# We can do some minor tweaking to improve speed on Tcl < 8.5 where we +# know our arithmetic is limited to 64 bits. On > 8.5 we may have +# unconstrained integer arithmetic and must avoid letting it run away. +# + +regsub -all -line \ + {\[F1 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body \ + {[expr {(rotl32($A,5) + ($D ^ ($B \& ($C ^ $D))) + $E + \1 + 0x5a827999) \& 0xffffffff}]} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {\[F2 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp \ + {[expr {(rotl32($A,5) + ($B ^ $C ^ $D) + $E + \1 + 0x6ed9eba1) \& 0xffffffff}]} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {\[F3 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp \ + {[expr {(rotl32($A,5) + (($B \& $C) | ($D \& ($B | $C))) + $E + \1 + 0x8f1bbcdc) \& 0xffffffff}]} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {\[F4 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp \ + {[expr {(rotl32($A,5) + ($B ^ $C ^ $D) + $E + \1 + 0xca62c1d6) \& 0xffffffff}]} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {rotl32\(\$A,5\)} \ + $::sha1::SHA1Transform_body_tmp \ + {((($A << 5) \& 0xffffffff) | (($A >> 27) \& 0x1f))} \ + ::sha1::SHA1Transform_body_tmp + +regsub -all -line \ + {\[rotl32 \$B 30\]} \ + $::sha1::SHA1Transform_body_tmp \ + {[expr {int(($B << 30) | (($B >> 2) \& 0x3fffffff))}]} \ + ::sha1::SHA1Transform_body_tmp +# +# Version 2 avoids a few truncations to 32 bits in non-essential places. +# +regsub -all -line \ + {\[F1 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body \ + {[expr {rotl32($A,5) + ($D ^ ($B \& ($C ^ $D))) + $E + \1 + 0x5a827999}]} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {\[F2 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp2 \ + {[expr {rotl32($A,5) + ($B ^ $C ^ $D) + $E + \1 + 0x6ed9eba1}]} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {\[F3 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp2 \ + {[expr {rotl32($A,5) + (($B \& $C) | ($D \& ($B | $C))) + $E + \1 + 0x8f1bbcdc}]} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {\[F4 \$A \$B \$C \$D \$E (\[.*?\])\]} \ + $::sha1::SHA1Transform_body_tmp2 \ + {[expr {rotl32($A,5) + ($B ^ $C ^ $D) + $E + \1 + 0xca62c1d6}]} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {rotl32\(\$A,5\)} \ + $::sha1::SHA1Transform_body_tmp2 \ + {(($A << 5) | (($A >> 27) \& 0x1f))} \ + ::sha1::SHA1Transform_body_tmp2 + +regsub -all -line \ + {\[rotl32 \$B 30\]} \ + $::sha1::SHA1Transform_body_tmp2 \ + {[expr {($B << 30) | (($B >> 2) \& 0x3fffffff)}]} \ + ::sha1::SHA1Transform_body_tmp2 + +if {[package vsatisfies [package provide Tcl] 8.5]} { + proc ::sha1::SHA1Transform {token msg} $::sha1::SHA1Transform_body_tmp +} else { + proc ::sha1::SHA1Transform {token msg} $::sha1::SHA1Transform_body_tmp2 +} + +unset ::sha1::SHA1Transform_body_tmp +unset ::sha1::SHA1Transform_body_tmp2 + +# ------------------------------------------------------------------------- + +proc ::sha1::byte {n v} {expr {((0xFF << (8 * $n)) & $v) >> (8 * $n)}} +proc ::sha1::bytes {v} { + #format %c%c%c%c [byte 0 $v] [byte 1 $v] [byte 2 $v] [byte 3 $v] + format %c%c%c%c \ + [expr {((0xFF000000 & $v) >> 24) & 0xFF}] \ + [expr {(0xFF0000 & $v) >> 16}] \ + [expr {(0xFF00 & $v) >> 8}] \ + [expr {0xFF & $v}] +} + +# ------------------------------------------------------------------------- + +proc ::sha1::Hex {data} { + binary scan $data H* result + return $result +} + +# ------------------------------------------------------------------------- + +# LoadAccelerator -- +# +# This package can make use of a number of compiled extensions to +# accelerate the digest computation. This procedure manages the +# use of these extensions within the package. During normal usage +# this should not be called, but the test package manipulates the +# list of enabled accelerators. +# +proc ::sha1::LoadAccelerator {name} { + variable accel + set r 0 + switch -exact -- $name { + critcl { + if {![catch {package require tcllibc}] + || ![catch {package require sha1c}]} { + set r [expr {[info command ::sha1::sha1c] != {}}] + } + } + cryptkit { + if {![catch {package require cryptkit}]} { + set r [expr {![catch {cryptkit::cryptInit}]}] + } + } + trf { + if {![catch {package require Trf}]} { + set r [expr {![catch {::sha1 aa} msg]}] + } + } + default { + return -code error "invalid accelerator package:\ + must be one of [join [array names accel] {, }]" + } + } + set accel($name) $r +} + +# ------------------------------------------------------------------------- + +# Description: +# Pop the nth element off a list. Used in options processing. +# +proc ::sha1::Pop {varname {nth 0}} { + upvar $varname args + set r [lindex $args $nth] + set args [lreplace $args $nth $nth] + return $r +} + +# ------------------------------------------------------------------------- + +# fileevent handler for chunked file hashing. +# +proc ::sha1::Chunk {token channel {chunksize 4096}} { + upvar #0 $token state + + if {[eof $channel]} { + fileevent $channel readable {} + set state(reading) 0 + } + + SHA1Update $token [read $channel $chunksize] +} + +# ------------------------------------------------------------------------- + +proc ::sha1::sha1 {args} { + array set opts {-hex 0 -filename {} -channel {} -chunksize 4096} + if {[llength $args] == 1} { + set opts(-hex) 1 + } else { + while {[string match -* [set option [lindex $args 0]]]} { + switch -glob -- $option { + -hex { set opts(-hex) 1 } + -bin { set opts(-hex) 0 } + -file* { set opts(-filename) [Pop args 1] } + -channel { set opts(-channel) [Pop args 1] } + -chunksize { set opts(-chunksize) [Pop args 1] } + default { + if {[llength $args] == 1} { break } + if {[string compare $option "--"] == 0} { Pop args; break } + set err [join [lsort [concat -bin [array names opts]]] ", "] + return -code error "bad option $option:\ + must be one of $err" + } + } + Pop args + } + } + + if {$opts(-filename) != {}} { + set opts(-channel) [open $opts(-filename) r] + fconfigure $opts(-channel) -translation binary + } + + if {$opts(-channel) == {}} { + + if {[llength $args] != 1} { + return -code error "wrong # args:\ + should be \"sha1 ?-hex? -filename file | string\"" + } + set tok [SHA1Init] + SHA1Update $tok [lindex $args 0] + set r [SHA1Final $tok] + + } else { + + set tok [SHA1Init] + # FRINK: nocheck + set [subst $tok](reading) 1 + fileevent $opts(-channel) readable \ + [list [namespace origin Chunk] \ + $tok $opts(-channel) $opts(-chunksize)] + # FRINK: nocheck + vwait [subst $tok](reading) + set r [SHA1Final $tok] + + # If we opened the channel - we should close it too. + if {$opts(-filename) != {}} { + close $opts(-channel) + } + } + + if {$opts(-hex)} { + set r [Hex $r] + } + return $r +} + +# ------------------------------------------------------------------------- + +proc ::sha1::hmac {args} { + array set opts {-hex 1 -filename {} -channel {} -chunksize 4096} + if {[llength $args] != 2} { + while {[string match -* [set option [lindex $args 0]]]} { + switch -glob -- $option { + -key { set opts(-key) [Pop args 1] } + -hex { set opts(-hex) 1 } + -bin { set opts(-hex) 0 } + -file* { set opts(-filename) [Pop args 1] } + -channel { set opts(-channel) [Pop args 1] } + -chunksize { set opts(-chunksize) [Pop args 1] } + default { + if {[llength $args] == 1} { break } + if {[string compare $option "--"] == 0} { Pop args; break } + set err [join [lsort [array names opts]] ", "] + return -code error "bad option $option:\ + must be one of $err" + } + } + Pop args + } + } + + if {[llength $args] == 2} { + set opts(-key) [Pop args] + } + + if {![info exists opts(-key)]} { + return -code error "wrong # args:\ + should be \"hmac ?-hex? -key key -filename file | string\"" + } + + if {$opts(-filename) != {}} { + set opts(-channel) [open $opts(-filename) r] + fconfigure $opts(-channel) -translation binary + } + + if {$opts(-channel) == {}} { + + if {[llength $args] != 1} { + return -code error "wrong # args:\ + should be \"hmac ?-hex? -key key -filename file | string\"" + } + set tok [HMACInit $opts(-key)] + HMACUpdate $tok [lindex $args 0] + set r [HMACFinal $tok] + + } else { + + set tok [HMACInit $opts(-key)] + # FRINK: nocheck + set [subst $tok](reading) 1 + fileevent $opts(-channel) readable \ + [list [namespace origin Chunk] \ + $tok $opts(-channel) $opts(-chunksize)] + # FRINK: nocheck + vwait [subst $tok](reading) + set r [HMACFinal $tok] + + # If we opened the channel - we should close it too. + if {$opts(-filename) != {}} { + close $opts(-channel) + } + } + + if {$opts(-hex)} { + set r [Hex $r] + } + return $r +} + +# ------------------------------------------------------------------------- + +# Try and load a compiled extension to help. +namespace eval ::sha1 { + foreach e {critcl cryptkit trf} { if {[LoadAccelerator $e]} { break } } +} + +package provide sha1 $::sha1::version + +# ------------------------------------------------------------------------- +# Local Variables: +# mode: tcl +# indent-tabs-mode: nil +# End: + + diff --git a/lib/tclxml3.1/pkgIndex.tcl b/lib/tclxml3.1/pkgIndex.tcl new file mode 100644 index 0000000..4c7428d --- /dev/null +++ b/lib/tclxml3.1/pkgIndex.tcl @@ -0,0 +1,97 @@ +# Tcl package index file - handcrafted +# +# $Id: pkgIndex.tcl.in,v 1.13 2003/12/03 20:06:34 balls Exp $ + +package ifneeded xml::c 3.1 [list load [file join $dir Tclxml30.dll]] +package ifneeded xml::tcl 3.1 [list source [file join $dir xml__tcl.tcl]] +package ifneeded sgmlparser 1.0 [list source [file join $dir sgmlparser.tcl]] +package ifneeded xpath 1.0 [list source [file join $dir xpath.tcl]] +package ifneeded xmldep 1.0 [list source [file join $dir xmldep.tcl]] + +# The C parsers are provided through their own packages and indices, +# and thus do not have to be listed here. This index may require them +# in certain places, but does not provide them. This is part of the +# work refactoring the build system of TclXML to create clean +# packages, and not require a jumble (jungle?) of things in one Makefile. +# +#package ifneeded xml::expat 3.1 [list load [file join $dir @expat_TCL_LIB_FILE@]] +#package ifneeded xml::xerces 2.0 [list load [file join $dir @xerces_TCL_LIB_FILE@]] +#package ifneeded xml::libxml2 3.1 [list load [file join $dir @TclXML_libxml2_LIB_FILE@]] + +namespace eval ::xml {} + +# Requesting a specific package means we want it to be the default parser class. +# This is achieved by loading it last. + +# expat and libxml2 packages must have xml::c package loaded +package ifneeded expat 3.1 { + package require xml::c 3.1 + package require xmldefs + package require xml::tclparser 3.1 + catch {package require xml::libxml2 3.1} + package require xml::expat 3.1 + package provide expat 3.1 +} +package ifneeded libxml2 3.1 { + package require xml::c 3.1 + package require xmldefs + package require xml::tclparser 3.1 + catch {package require xml::expat 3.1} + package require xml::libxml2 3.1 + package provide libxml2 3.1 +} + +# tclparser works with either xml::c or xml::tcl +package ifneeded tclparser 3.1 { + if {[catch {package require xml::c 3.1}]} { + # No point in trying to load expat or libxml2 + package require xml::tcl 3.1 + package require xmldefs + package require xml::tclparser 3.1 + } else { + package require xmldefs + catch {package require xml::expat 3.1} + catch {package require xml::libxml2 3.1} + package require xml::tclparser + } + package provide tclparser 3.1 +} + +# use tcl only (mainly for testing) +package ifneeded puretclparser 3.1 { + package require xml::tcl 3.1 + package require xmldefs + package require xml::tclparser 3.1 + package provide puretclparser 3.1 +} + +# Requesting the generic package leaves the choice of default parser automatic + +package ifneeded xml 3.1 { + if {[catch {package require xml::c 3.1}]} { + package require xml::tcl 3.1 + package require xmldefs + # Only choice is tclparser + package require xml::tclparser 3.1 + } else { + package require xmldefs + package require xml::tclparser 3.1 + # libxml2 is favoured since it provides more features + catch {package require xml::expat 3.1} + catch {package require xml::libxml2 3.1} + } + package provide xml 3.1 +} + +if {[info tclversion] <= 8.0} { + package ifneeded sgml 1.9 [list source [file join $dir sgml-8.0.tcl]] + package ifneeded xmldefs 3.1 [list source [file join $dir xml-8.0.tcl]] + package ifneeded xml::tclparser 3.1 [list source [file join $dir tclparser-8.0.tcl]] +} else { + package ifneeded sgml 1.9 [list source [file join $dir sgml-8.1.tcl]] + package ifneeded xmldefs 3.1 [list source [file join $dir xml-8.1.tcl]] + package ifneeded xml::tclparser 3.1 [list source [file join $dir tclparser-8.1.tcl]] +} + + + diff --git a/lib/tclxml3.1/sgml-8.1.tcl b/lib/tclxml3.1/sgml-8.1.tcl new file mode 100644 index 0000000..5e65bf8 --- /dev/null +++ b/lib/tclxml3.1/sgml-8.1.tcl @@ -0,0 +1,143 @@ +# sgml-8.1.tcl -- +# +# This file provides generic parsing services for SGML-based +# languages, namely HTML and XML. +# This file supports Tcl 8.1 characters and regular expressions. +# +# NB. It is a misnomer. There is no support for parsing +# arbitrary SGML as such. +# +# Copyright (c) 1998-2003 Zveno Pty Ltd +# http://www.zveno.com/ +# +# See the file "LICENSE" in this distribution for information on usage and +# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# $Id: sgml-8.1.tcl,v 1.7 2003/12/09 04:43:15 balls Exp $ + +package require Tcl 8.1 + +package provide sgml 1.9 + +namespace eval sgml { + + # Convenience routine + proc cl x { + return "\[$x\]" + } + + # Define various regular expressions + + # Character classes + variable Char \t\n\r\ -\uD7FF\uE000-\uFFFD\u10000-\u10FFFF + variable BaseChar \u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0100-\u0131\u0134-\u013E\u0141-\u0148\u014A-\u017E\u0180-\u01C3\u01CD-\u01F0\u01F4-\u01F5\u01FA-\u0217\u0250-\u02A8\u02BB-\u02C1\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03D6\u03DA\u03DC\u03DE\u03E0\u03E2-\u03F3\u0401-\u040C\u040E-\u044F\u0451-\u045C\u045E-\u0481\u0490-\u04C4\u04C7-\u04C8\u04CB-\u04CC\u04D0-\u04EB\u04EE-\u04F5\u04F8-\u04F9\u0531-\u0556\u0559\u0561-\u0586\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0641-\u064A\u0671-\u06B7\u06BA-\u06BE\u06C0-\u06CE\u06D0-\u06D3\u06D5\u06E5-\u06E6\u0905-\u0939\u093D\u0958-\u0961\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8B\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD\u0AE0\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B36-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB5\u0BB7-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CDE\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D60-\u0D61\u0E01-\u0E2E\u0E30\u0E32-\u0E33\u0E40-\u0E45\u0E81-\u0E82\u0E84\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EAE\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0F40-\u0F47\u0F49-\u0F69\u10A0-\u10C5\u10D0-\u10F6\u1100\u1102-\u1103\u1105-\u1107\u1109\u110B-\u110C\u110E-\u1112\u113C\u113E\u1140\u114C\u114E\u1150\u1154-\u1155\u1159\u115F-\u1161\u1163\u1165\u1167\u1169\u116D-\u116E\u1172-\u1173\u1175\u119E\u11A8\u11AB\u11AE-\u11AF\u11B7-\u11B8\u11BA\u11BC-\u11C2\u11EB\u11F0\u11F9\u1E00-\u1E9B\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2126\u212A-\u212B\u212E\u2180-\u2182\u3041-\u3094\u30A1-\u30FA\u3105-\u312C\uAC00-\uD7A3 + variable Ideographic \u4E00-\u9FA5\u3007\u3021-\u3029 + variable CombiningChar \u0300-\u0345\u0360-\u0361\u0483-\u0486\u0591-\u05A1\u05A3-\u05B9\u05BB-\u05BD\u05BF\u05C1-\u05C2\u05C4\u064B-\u0652\u0670\u06D6-\u06DC\u06DD-\u06DF\u06E0-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0901-\u0903\u093C\u093E-\u094C\u094D\u0951-\u0954\u0962-\u0963\u0981-\u0983\u09BC\u09BE\u09BF\u09C0-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7\u09E2-\u09E3\u0A02\u0A3C\u0A3E\u0A3F\u0A40-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D\u0B56-\u0B57\u0B82-\u0B83\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56\u0C82-\u0C83\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86-\u0F8B\u0F90-\u0F95\u0F97\u0F99-\u0FAD\u0FB1-\u0FB7\u0FB9\u20D0-\u20DC\u20E1\u302A-\u302F\u3099\u309A + variable Digit \u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE7-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29 + variable Extender \u00B7\u02D0\u02D1\u0387\u0640\u0E46\u0EC6\u3005\u3031-\u3035\u309D-\u309E\u30FC-\u30FE + variable Letter $BaseChar|$Ideographic + + # white space + variable Wsp " \t\r\n" + variable noWsp [cl ^$Wsp] + + # Various XML names + variable NameChar \[-$Letter$Digit._:$CombiningChar$Extender\] + variable Name \[_:$BaseChar$Ideographic\]$NameChar* + variable Names ${Name}(?:$Wsp$Name)* + variable Nmtoken $NameChar+ + variable Nmtokens ${Nmtoken}(?:$Wsp$Nmtoken)* + + # table of predefined entities for XML + + variable EntityPredef + array set EntityPredef { + lt < gt > amp & quot \" apos ' + } + +} + +# These regular expressions are defined here once for better performance + +namespace eval sgml { + variable Wsp + + # Watch out for case-sensitivity + + set attlist_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*(#REQUIRED|#IMPLIED) + set attlist_enum_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*\\(([cl ^)]*)\\)[cl $Wsp]*("([cl ^")]*)")? ;# " + set attlist_fixed_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*(#FIXED)[cl $Wsp]*([cl ^$Wsp]+) + + set param_entity_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*([cl ^"$Wsp]*)[cl $Wsp]*"([cl ^"]*)" + + set notation_exp [cl $Wsp]*([cl ^$Wsp]+)[cl $Wsp]*(.*) + +} + +### Utility procedures + +# sgml::noop -- +# +# A do-nothing proc +# +# Arguments: +# args arguments +# +# Results: +# Nothing. + +proc sgml::noop args { + return 0 +} + +# sgml::identity -- +# +# Identity function. +# +# Arguments: +# a arbitrary argument +# +# Results: +# $a + +proc sgml::identity a { + return $a +} + +# sgml::Error -- +# +# Throw an error +# +# Arguments: +# args arguments +# +# Results: +# Error return condition. + +proc sgml::Error args { + uplevel return -code error [list $args] +} + +### Following procedures are based on html_library + +# sgml::zapWhite -- +# +# Convert multiple white space into a single space. +# +# Arguments: +# data plain text +# +# Results: +# As above + +proc sgml::zapWhite data { + regsub -all "\[ \t\r\n\]+" $data { } data + return $data +} + +proc sgml::Boolean value { + regsub {1|true|yes|on} $value 1 value + regsub {0|false|no|off} $value 0 value + return $value +} + diff --git a/lib/tclxml3.1/sgmlparser.tcl b/lib/tclxml3.1/sgmlparser.tcl new file mode 100644 index 0000000..72776d9 --- /dev/null +++ b/lib/tclxml3.1/sgmlparser.tcl @@ -0,0 +1,2816 @@ +# sgmlparser.tcl -- +# +# This file provides the generic part of a parser for SGML-based +# languages, namely HTML and XML. +# +# NB. It is a misnomer. There is no support for parsing +# arbitrary SGML as such. +# +# See sgml.tcl for variable definitions. +# +# Copyright (c) 1998-2003 Zveno Pty Ltd +# http://www.zveno.com/ +# +# See the file "LICENSE" in this distribution for information on usage and +# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# $Id: sgmlparser.tcl,v 1.32 2003/12/09 04:43:15 balls Exp $ + +package require sgml 1.9 + +package require uri 1.1 + +package provide sgmlparser 1.0 + +namespace eval sgml { + namespace export tokenise parseEvent + + namespace export parseDTD + + # NB. Most namespace variables are defined in sgml-8.[01].tcl + # to account for differences between versions of Tcl. + # This especially includes the regular expressions used. + + variable ParseEventNum + if {![info exists ParseEventNum]} { + set ParseEventNum 0 + } + variable ParseDTDnum + if {![info exists ParseDTDNum]} { + set ParseDTDNum 0 + } + + variable declExpr [cl $::sgml::Wsp]*([cl ^$::sgml::Wsp]+)[cl $::sgml::Wsp]*([cl ^>]*) + variable EntityExpr [cl $::sgml::Wsp]*(%[cl $::sgml::Wsp])?[cl $::sgml::Wsp]*($::sgml::Name)[cl $::sgml::Wsp]+(.*) + + #variable MarkupDeclExpr <([cl ^$::sgml::Wsp>]+)[cl $::sgml::Wsp]*([cl ^$::sgml::Wsp]+)[cl $::sgml::Wsp]*([cl ^>]*)> + #variable MarkupDeclSub "} {\\1} {\\2} {\\3} {" + variable MarkupDeclExpr <[cl $::sgml::Wsp]*([cl ^$::sgml::Wsp>]+)[cl $::sgml::Wsp]*([cl ^>]*)> + variable MarkupDeclSub "\} {\\1} {\\2} \{" + + variable ExternalEntityExpr ^(PUBLIC|SYSTEM)[cl $::sgml::Wsp]+("|')(.*?)\\2([cl $::sgml::Wsp]+("|')(.*?)\\2)?([cl $::sgml::Wsp]+NDATA[cl $::sgml::Wsp]+($::xml::Name))?\$ + + variable StdOptions + array set StdOptions [list \ + -elementstartcommand [namespace current]::noop \ + -elementendcommand [namespace current]::noop \ + -characterdatacommand [namespace current]::noop \ + -processinginstructioncommand [namespace current]::noop \ + -externalentitycommand {} \ + -xmldeclcommand [namespace current]::noop \ + -doctypecommand [namespace current]::noop \ + -commentcommand [namespace current]::noop \ + -entitydeclcommand [namespace current]::noop \ + -unparsedentitydeclcommand [namespace current]::noop \ + -parameterentitydeclcommand [namespace current]::noop \ + -notationdeclcommand [namespace current]::noop \ + -elementdeclcommand [namespace current]::noop \ + -attlistdeclcommand [namespace current]::noop \ + -paramentityparsing 1 \ + -defaultexpandinternalentities 1 \ + -startdoctypedeclcommand [namespace current]::noop \ + -enddoctypedeclcommand [namespace current]::noop \ + -entityreferencecommand {} \ + -warningcommand [namespace current]::noop \ + -errorcommand [namespace current]::Error \ + -final 1 \ + -validate 0 \ + -baseuri {} \ + -name {} \ + -cmd {} \ + -emptyelement [namespace current]::EmptyElement \ + -parseattributelistcommand [namespace current]::noop \ + -parseentitydeclcommand [namespace current]::noop \ + -normalize 1 \ + -internaldtd {} \ + -reportempty 0 \ + -ignorewhitespace 0 \ + ] +} + +# sgml::tokenise -- +# +# Transform the given HTML/XML text into a Tcl list. +# +# Arguments: +# sgml text to tokenize +# elemExpr RE to recognise tags +# elemSub transform for matched tags +# args options +# +# Valid Options: +# -internaldtdvariable +# -final boolean True if no more data is to be supplied +# -statevariable varName Name of a variable used to store info +# +# Results: +# Returns a Tcl list representing the document. + +proc sgml::tokenise {sgml elemExpr elemSub args} { + array set options {-final 1} + array set options $args + set options(-final) [Boolean $options(-final)] + + # If the data is not final then there must be a variable to store + # unused data. + if {!$options(-final) && ![info exists options(-statevariable)]} { + return -code error {option "-statevariable" required if not final} + } + + # Pre-process stage + # + # Extract the internal DTD subset, if any + + catch {upvar #0 $options(-internaldtdvariable) dtd} + if {[regexp {]*$)} [lindex $sgml end] x text rest]} { + set sgml [lreplace $sgml end end $text] + # Mats: unmatched stuff means that it is chopped off. Cache it for next round. + set state(leftover) $rest + } + + ## Patch from bug report #596959, Marshall Rose + # This patch was for use when the caller of this function used lrange + # 5 -end on thresult. This is no longer the case [PT] + #if {[string compare [lindex $sgml 4] ""]} { + # set sgml [linsert $sgml 0 {} {} {} {} {}] + #} + + } else { + + # Performance note (Tcl 8.0): + # In this case, no conversion to list object is performed + + # Mats: This fails if not -final and $sgml is chopped off right in a tag. + regsub -all $elemExpr $sgml $elemSub sgml + set sgml "{} {} {} \{$sgml\}" + } + + return $sgml + +} + +# sgml::parseEvent -- +# +# Produces an event stream for a XML/HTML document, +# given the Tcl list format returned by tokenise. +# +# This procedure checks that the document is well-formed, +# and throws an error if the document is found to be not +# well formed. Warnings are passed via the -warningcommand script. +# +# The procedure only check for well-formedness, +# no DTD is required. However, facilities are provided for entity expansion. +# +# Arguments: +# sgml Instance data, as a Tcl list. +# args option/value pairs +# +# Valid Options: +# -final Indicates end of document data +# -validate Boolean to enable validation +# -baseuri URL for resolving relative URLs +# -elementstartcommand Called when an element starts +# -elementendcommand Called when an element ends +# -characterdatacommand Called when character data occurs +# -entityreferencecommand Called when an entity reference occurs +# -processinginstructioncommand Called when a PI occurs +# -externalentitycommand Called for an external entity reference +# +# -xmldeclcommand Called when the XML declaration occurs +# -doctypecommand Called when the document type declaration occurs +# -commentcommand Called when a comment occurs +# -entitydeclcommand Called when a parsed entity is declared +# -unparsedentitydeclcommand Called when an unparsed external entity is declared +# -parameterentitydeclcommand Called when a parameter entity is declared +# -notationdeclcommand Called when a notation is declared +# -elementdeclcommand Called when an element is declared +# -attlistdeclcommand Called when an attribute list is declared +# -paramentityparsing Boolean to enable/disable parameter entity substitution +# -defaultexpandinternalentities Boolean to enable/disable expansion of entities declared in internal DTD subset +# +# -startdoctypedeclcommand Called when the Doc Type declaration starts (see also -doctypecommand) +# -enddoctypedeclcommand Called when the Doc Type declaration ends (see also -doctypecommand) +# +# -errorcommand Script to evaluate for a fatal error +# -warningcommand Script to evaluate for a reportable warning +# -statevariable global state variable +# -normalize whether to normalize names +# -reportempty whether to include an indication of empty elements +# -ignorewhitespace whether to automatically strip whitespace +# +# Results: +# The various callback scripts are invoked. +# Returns empty string. +# +# BUGS: +# If command options are set to empty string then they should not be invoked. + +proc sgml::parseEvent {sgml args} { + variable Wsp + variable noWsp + variable Nmtoken + variable Name + variable ParseEventNum + variable StdOptions + + array set options [array get StdOptions] + catch {array set options $args} + + # Mats: + # If the data is not final then there must be a variable to persistently store the parse state. + if {!$options(-final) && ![info exists options(-statevariable)]} { + return -code error {option "-statevariable" required if not final} + } + + foreach {opt value} [array get options *command] { + if {[string compare $opt "-externalentitycommand"] && ![string length $value]} { + set options($opt) [namespace current]::noop + } + } + + if {![info exists options(-statevariable)]} { + set options(-statevariable) [namespace current]::ParseEvent[incr ParseEventNum] + } + if {![info exists options(entities)]} { + set options(entities) [namespace current]::Entities$ParseEventNum + array set $options(entities) [array get [namespace current]::EntityPredef] + } + if {![info exists options(extentities)]} { + set options(extentities) [namespace current]::ExtEntities$ParseEventNum + } + if {![info exists options(parameterentities)]} { + set options(parameterentities) [namespace current]::ParamEntities$ParseEventNum + } + if {![info exists options(externalparameterentities)]} { + set options(externalparameterentities) [namespace current]::ExtParamEntities$ParseEventNum + } + if {![info exists options(elementdecls)]} { + set options(elementdecls) [namespace current]::ElementDecls$ParseEventNum + } + if {![info exists options(attlistdecls)]} { + set options(attlistdecls) [namespace current]::AttListDecls$ParseEventNum + } + if {![info exists options(notationdecls)]} { + set options(notationdecls) [namespace current]::NotationDecls$ParseEventNum + } + if {![info exists options(namespaces)]} { + set options(namespaces) [namespace current]::Namespaces$ParseEventNum + } + + # For backward-compatibility + catch {set options(-baseuri) $options(-baseurl)} + + # Choose an external entity resolver + + if {![string length $options(-externalentitycommand)]} { + if {$options(-validate)} { + set options(-externalentitycommand) [namespace code ResolveEntity] + } else { + set options(-externalentitycommand) [namespace code noop] + } + } + + upvar #0 $options(-statevariable) state + upvar #0 $options(entities) entities + + # Mats: + # The problem is that the state is not maintained when -final 0 ! + # I've switched back to an older version here. + + if {![info exists state(line)]} { + # Initialise the state variable + array set state { + mode normal + haveXMLDecl 0 + haveDocElement 0 + inDTD 0 + context {} + stack {} + line 0 + defaultNS {} + defaultNSURI {} + } + } + + foreach {tag close param text} $sgml { + + # Keep track of lines in the input + incr state(line) [regsub -all \n $param {} discard] + incr state(line) [regsub -all \n $text {} discard] + + # If the current mode is cdata or comment then we must undo what the + # regsub has done to reconstitute the data + + set empty {} + switch $state(mode) { + comment { + # This had "[string length $param] && " as a guard - + # can't remember why :-( + if {[regexp ([cl ^-]*)--\$ $tag discard comm1]} { + # end of comment (in tag) + set tag {} + set close {} + set state(mode) normal + DeProtect1 $options(-commentcommand) $state(commentdata)<$comm1 + unset state(commentdata) + } elseif {[regexp ([cl ^-]*)--\$ $param discard comm1]} { + # end of comment (in attributes) + DeProtect1 $options(-commentcommand) $state(commentdata)<$close$tag>$comm1 + unset state(commentdata) + set tag {} + set param {} + set close {} + set state(mode) normal + } elseif {[regexp ([cl ^-]*)-->(.*) $text discard comm1 text]} { + # end of comment (in text) + DeProtect1 $options(-commentcommand) $state(commentdata)<$close$tag$param>$comm1 + unset state(commentdata) + set tag {} + set param {} + set close {} + set state(mode) normal + } else { + # comment continues + append state(commentdata) <$close$tag$param>$text + continue + } + } + cdata { + if {[string length $param] && [regexp ([cl ^\]]*)\]\][cl $Wsp]*\$ $tag discard cdata1]} { + # end of CDATA (in tag) + PCDATA [array get options] $state(cdata)<[subst -nocommand -novariable $close$cdata1] + set text [subst -novariable -nocommand $text] + set tag {} + unset state(cdata) + set state(mode) normal + } elseif {[regexp ([cl ^\]]*)\]\][cl $Wsp]*\$ $param discard cdata1]} { + # end of CDATA (in attributes) + PCDATA [array get options] $state(cdata)<[subst -nocommand -novariable $close$tag$cdata1] + set text [subst -novariable -nocommand $text] + set tag {} + set param {} + unset state(cdata) + set state(mode) normal + } elseif {[regexp (.*)\]\][cl $Wsp]*>(.*) $text discard cdata1 text]} { + # end of CDATA (in text) + PCDATA [array get options] $state(cdata)<[subst -nocommand -novariable $close$tag$param>$cdata1] + set text [subst -novariable -nocommand $text] + set tag {} + set param {} + set close {} + unset state(cdata) + set state(mode) normal + } else { + # CDATA continues + append state(cdata) [subst -nocommand -novariable <$close$tag$param>$text] + continue + } + } + continue { + # We're skipping elements looking for the close tag + switch -glob -- [string length $tag],[regexp {^\?|!.*} $tag],$close { + 0,* { + continue + } + *,0, { + if {![string compare $tag $state(continue:tag)]} { + set empty [uplevel #0 $options(-emptyelement) [list $tag $param $empty]] + if {![string length $empty]} { + incr state(continue:level) + } + } + continue + } + *,0,/ { + if {![string compare $tag $state(continue:tag)]} { + incr state(continue:level) -1 + } + if {!$state(continue:level)} { + unset state(continue:tag) + unset state(continue:level) + set state(mode) {} + } + } + default { + continue + } + } + } + default { + # The trailing slash on empty elements can't be automatically separated out + # in the RE, so we must do it here. + regexp (.*)(/)[cl $Wsp]*$ $param discard param empty + } + } + + # default: normal mode + + # Bug: if the attribute list has a right angle bracket then the empty + # element marker will not be seen + + set empty [uplevel #0 $options(-emptyelement) [list $tag $param $empty]] + + switch -glob -- [string length $tag],[regexp {^\?|!.*} $tag],$close,$empty { + + 0,0,, { + # Ignore empty tag - dealt with non-normal mode above + } + *,0,, { + + # Start tag for an element. + + # Check if the internal DTD entity is in an attribute value + regsub -all &xml:intdtd\; $param \[$options(-internaldtd)\] param + + set code [catch {ParseEvent:ElementOpen $tag $param [array get options]} msg] + set state(haveDocElement) 1 + switch $code { + 0 {# OK} + 3 { + # break + return {} + } + 4 { + # continue + # Remember this tag and look for its close + set state(continue:tag) $tag + set state(continue:level) 1 + set state(mode) continue + continue + } + default { + return -code $code -errorinfo $::errorInfo $msg + } + } + + } + + *,0,/, { + + # End tag for an element. + + set code [catch {ParseEvent:ElementClose $tag [array get options]} msg] + switch $code { + 0 {# OK} + 3 { + # break + return {} + } + 4 { + # continue + # skip sibling nodes + set state(continue:tag) [lindex $state(stack) end] + set state(continue:level) 1 + set state(mode) continue + continue + } + default { + return -code $code -errorinfo $::errorInfo $msg + } + } + + } + + *,0,,/ { + + # Empty element + + # The trailing slash sneaks through into the param variable + regsub -all /[cl $::sgml::Wsp]*\$ $param {} param + + set code [catch {ParseEvent:ElementOpen $tag $param [array get options] -empty 1} msg] + set state(haveDocElement) 1 + switch $code { + 0 {# OK} + 3 { + # break + return {} + } + 4 { + # continue + # Pretty useless since it closes straightaway + } + default { + return -code $code -errorinfo $::errorInfo $msg + } + } + set code [catch {ParseEvent:ElementClose $tag [array get options] -empty 1} msg] + switch $code { + 0 {# OK} + 3 { + # break + return {} + } + 4 { + # continue + # skip sibling nodes + set state(continue:tag) [lindex $state(stack) end] + set state(continue:level) 1 + set state(mode) continue + continue + } + default { + return -code $code -errorinfo $::errorInfo $msg + } + } + + } + + *,1,* { + # Processing instructions or XML declaration + switch -glob -- $tag { + + {\?xml} { + # XML Declaration + if {$state(haveXMLDecl)} { + uplevel #0 $options(-errorcommand) [list illegalcharacter "unexpected characters \"<$tag\" around line $state(line)"] + } elseif {![regexp {\?$} $param]} { + uplevel #0 $options(-errorcommand) [list missingcharacters "XML Declaration missing characters \"?>\" around line $state(line)"] + } else { + + # We can do the parsing in one step with Tcl 8.1 RE's + # This has the benefit of performing better WF checking + + set adv_re [format {^[%s]*version[%s]*=[%s]*("|')(-+|[a-zA-Z0-9_.:]+)\1([%s]+encoding[%s]*=[%s]*("|')([A-Za-z][-A-Za-z0-9._]*)\4)?([%s]*standalone[%s]*=[%s]*("|')(yes|no)\7)?[%s]*\?$} $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp] + + if {[catch {regexp $adv_re $param discard delimiter version discard delimiter encoding discard delimiter standalone} matches]} { + # Otherwise we must fallback to 8.0. + # This won't detect certain well-formedness errors + + # Get the version number + if {[regexp [format {[%s]*version[%s]*=[%s]*"(-+|[a-zA-Z0-9_.:]+)"[%s]*} $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp] $param discard version] || [regexp [format {[%s]*version[%s]*=[%s]*'(-+|[a-zA-Z0-9_.:]+)'[%s]*} $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp] $param discard version]} { + if {[string compare $version "1.0"]} { + # Should we support future versions? + # At least 1.X? + uplevel #0 $options(-errorcommand) [list versionincompatibility "document XML version \"$version\" is incompatible with XML version 1.0"] + } + } else { + uplevel #0 $options(-errorcommand) [list missingversion "XML Declaration missing version information around line $state(line)"] + } + + # Get the encoding declaration + set encoding {} + regexp [format {[%s]*encoding[%s]*=[%s]*"([A-Za-z]([A-Za-z0-9._]|-)*)"[%s]*} $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp] $param discard encoding + regexp [format {[%s]*encoding[%s]*=[%s]*'([A-Za-z]([A-Za-z0-9._]|-)*)'[%s]*} $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp] $param discard encoding + + # Get the standalone declaration + set standalone {} + regexp [format {[%s]*standalone[%s]*=[%s]*"(yes|no)"[%s]*} $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp] $param discard standalone + regexp [format {[%s]*standalone[%s]*=[%s]*'(yes|no)'[%s]*} $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp $::sgml::Wsp] $param discard standalone + + # Invoke the callback + uplevel #0 $options(-xmldeclcommand) [list $version $encoding $standalone] + + } elseif {$matches == 0} { + uplevel #0 $options(-errorcommand) [list illformeddeclaration "XML Declaration not well-formed around line $state(line)"] + } else { + + # Invoke the callback + uplevel #0 $options(-xmldeclcommand) [list $version $encoding $standalone] + + } + + } + + } + + {\?*} { + # Processing instruction + set tag [string range $tag 1 end] + if {[regsub {\?$} $tag {} tag]} { + if {[string length [string trim $param]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$param\" in processing instruction around line $state(line)"] + } + } elseif {![regexp ^$Name\$ $tag]} { + uplevel #0 $options(-errorcommand) [list illegalcharacter "illegal character in processing instruction target \"$tag\""] + } elseif {[regexp {[xX][mM][lL]} $tag]} { + uplevel #0 $options(-errorcommand) [list illegalcharacters "characters \"xml\" not permitted in processing instruction target \"$tag\""] + } elseif {![regsub {\?$} $param {} param]} { + uplevel #0 $options(-errorcommand) [list missingquestion "PI: expected '?' character around line $state(line)"] + } + set code [catch {uplevel #0 $options(-processinginstructioncommand) [list $tag [string trimleft $param]]} msg] + switch $code { + 0 {# OK} + 3 { + # break + return {} + } + 4 { + # continue + # skip sibling nodes + set state(continue:tag) [lindex $state(stack) end] + set state(continue:level) 1 + set state(mode) continue + continue + } + default { + return -code $code -errorinfo $::errorInfo $msg + } + } + } + + !DOCTYPE { + # External entity reference + # This should move into xml.tcl + # Parse the params supplied. Looking for Name, ExternalID and MarkupDecl + set matched [regexp ^[cl $Wsp]*($Name)[cl $Wsp]*(.*) $param x state(doc_name) param] + set state(doc_name) [Normalize $state(doc_name) $options(-normalize)] + set externalID {} + set pubidlit {} + set systemlit {} + set externalID {} + if {[regexp -nocase ^[cl $Wsp]*(SYSTEM|PUBLIC)(.*) $param x id param]} { + switch [string toupper $id] { + SYSTEM { + if {[regexp ^[cl $Wsp]+"([cl ^"]*)"(.*) $param x systemlit param] || [regexp ^[cl $Wsp]+'([cl ^']*)'(.*) $param x systemlit param]} { + set externalID [list SYSTEM $systemlit] ;# " + } else { + uplevel #0 $options(-errorcommand) {syntaxerror {syntax error: SYSTEM identifier not followed by literal}} + } + } + PUBLIC { + if {[regexp ^[cl $Wsp]+"([cl ^"]*)"(.*) $param x pubidlit param] || [regexp ^[cl $Wsp]+'([cl ^']*)'(.*) $param x pubidlit param]} { + if {[regexp ^[cl $Wsp]+"([cl ^"]*)"(.*) $param x systemlit param] || [regexp ^[cl $Wsp]+'([cl ^']*)'(.*) $param x systemlit param]} { + set externalID [list PUBLIC $pubidlit $systemlit] + } else { + uplevel #0 $options(-errorcommand) [list syntaxerror "syntax error: PUBLIC identifier not followed by system literal around line $state(line)"] + } + } else { + uplevel #0 $options(-errorcommand) [list syntaxerror "syntax error: PUBLIC identifier not followed by literal around line $state(line)"] + } + } + } + if {[regexp -nocase ^[cl $Wsp]+NDATA[cl $Wsp]+($Name)(.*) $param x notation param]} { + lappend externalID $notation + } + } + + set state(inDTD) 1 + + ParseEvent:DocTypeDecl [array get options] $state(doc_name) $pubidlit $systemlit $options(-internaldtd) + + set state(inDTD) 0 + + } + + !--* { + + # Start of a comment + # See if it ends in the same tag, otherwise change the + # parsing mode + + regexp {!--(.*)} $tag discard comm1 + if {[regexp ([cl ^-]*)--[cl $Wsp]*\$ $comm1 discard comm1_1]} { + # processed comment (end in tag) + uplevel #0 $options(-commentcommand) [list $comm1_1] + } elseif {[regexp ([cl ^-]*)--[cl $Wsp]*\$ $param discard comm2]} { + # processed comment (end in attributes) + uplevel #0 $options(-commentcommand) [list $comm1$comm2] + } elseif {[regexp ([cl ^-]*)-->(.*) $text discard comm2 text]} { + # processed comment (end in text) + uplevel #0 $options(-commentcommand) [list $comm1$param$empty>$comm2] + } else { + # start of comment + set state(mode) comment + set state(commentdata) "$comm1$param$empty>$text" + continue + } + } + + {!\[CDATA\[*} { + + regexp {!\[CDATA\[(.*)} $tag discard cdata1 + if {[regexp {(.*)]]$} $cdata1 discard cdata2]} { + # processed CDATA (end in tag) + PCDATA [array get options] [subst -novariable -nocommand $cdata2] + set text [subst -novariable -nocommand $text] + } elseif {[regexp {(.*)]]$} $param discard cdata2]} { + # processed CDATA (end in attribute) + # Backslashes in param are quoted at this stage + PCDATA [array get options] $cdata1[subst -novariable -nocommand $cdata2] + set text [subst -novariable -nocommand $text] + } elseif {[regexp {(.*)]]>(.*)} $text discard cdata2 text]} { + # processed CDATA (end in text) + # Backslashes in param and text are quoted at this stage + PCDATA [array get options] $cdata1[subst -novariable -nocommand $param]$empty>[subst -novariable -nocommand $cdata2] + set text [subst -novariable -nocommand $text] + } else { + # start CDATA + set state(cdata) "$cdata1$param>$text" + set state(mode) cdata + continue + } + + } + + !ELEMENT - + !ATTLIST - + !ENTITY - + !NOTATION { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "[string range $tag 1 end] declaration not expected in document instance around line $state(line)"] + } + + default { + uplevel #0 $options(-errorcommand) [list unknowninstruction "unknown processing instruction \"<$tag>\" around line $state(line)"] + } + } + } + *,1,* - + *,0,/,/ { + # Syntax error + uplevel #0 $options(-errorcommand) [list syntaxerror "syntax error: closed/empty tag: tag $tag param $param empty $empty close $close around line $state(line)"] + } + } + + # Mats: we could have been reset from any of the callbacks! + if {![info exists state(haveDocElement)]} { + return {} + } + + # Process character data + if {$state(haveDocElement) && [llength $state(stack)]} { + + # Check if the internal DTD entity is in the text + regsub -all &xml:intdtd\; $text \[$options(-internaldtd)\] text + + # Look for entity references + if {([array size entities] || \ + [string length $options(-entityreferencecommand)]) && \ + $options(-defaultexpandinternalentities) && \ + [regexp {&[^;]+;} $text]} { + + # protect Tcl specials + # NB. braces and backslashes may already be protected + regsub -all {\\({|}|\\)} $text {\1} text + regsub -all {([][$\\{}])} $text {\\\1} text + + # Mark entity references + regsub -all {&([^;]+);} $text [format {%s; %s {\1} ; %s %s} \}\} [namespace code [list Entity [array get options] $options(-entityreferencecommand) [namespace code [list PCDATA [array get options]]] $options(entities)]] [namespace code [list DeProtect [namespace code [list PCDATA [array get options]]]]] \{\{] text + set text "uplevel #0 [namespace code [list DeProtect1 [namespace code [list PCDATA [array get options]]]]] {{$text}}" + eval $text + } else { + + # Restore protected special characters + regsub -all {\\([][{}\\])} $text {\1} text + PCDATA [array get options] $text + } + } elseif {[string length [string trim $text]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$text\" in document prolog around line $state(line)"] + } + + } + + # If this is the end of the document, close all open containers + if {$options(-final) && [llength $state(stack)]} { + eval $options(-errorcommand) [list unclosedelement "element [lindex $state(stack) end] remains unclosed around line $state(line)"] + } + + return {} +} + +# sgml::DeProtect -- +# +# Invoke given command after removing protecting backslashes +# from given text. +# +# Arguments: +# cmd Command to invoke +# text Text to deprotect +# +# Results: +# Depends on command + +proc sgml::DeProtect1 {cmd text} { + if {[string compare {} $text]} { + regsub -all {\\([]$[{}\\])} $text {\1} text + uplevel #0 $cmd [list $text] + } +} +proc sgml::DeProtect {cmd text} { + set text [lindex $text 0] + if {[string compare {} $text]} { + regsub -all {\\([]$[{}\\])} $text {\1} text + uplevel #0 $cmd [list $text] + } +} + +# sgml::ParserDelete -- +# +# Free all memory associated with parser +# +# Arguments: +# var global state array +# +# Results: +# Variables unset + +proc sgml::ParserDelete var { + upvar #0 $var state + + if {![info exists state]} { + return -code error "unknown parser" + } + + catch {unset $state(entities)} + catch {unset $state(parameterentities)} + catch {unset $state(elementdecls)} + catch {unset $state(attlistdecls)} + catch {unset $state(notationdecls)} + catch {unset $state(namespaces)} + + unset state + + return {} +} + +# sgml::ParseEvent:ElementOpen -- +# +# Start of an element. +# +# Arguments: +# tag Element name +# attr Attribute list +# opts Options +# args further configuration options +# +# Options: +# -empty boolean +# indicates whether the element was an empty element +# +# Results: +# Modify state and invoke callback + +proc sgml::ParseEvent:ElementOpen {tag attr opts args} { + variable Name + variable Wsp + + array set options $opts + upvar #0 $options(-statevariable) state + array set cfg {-empty 0} + array set cfg $args + set handleEmpty 0 + + if {$options(-normalize)} { + set tag [string toupper $tag] + } + + # Update state + lappend state(stack) $tag + + # Parse attribute list into a key-value representation + if {[string compare $options(-parseattributelistcommand) {}]} { + if {[catch {uplevel #0 $options(-parseattributelistcommand) [list $opts $attr]} attr]} { + if {[string compare [lindex $attr 0] "unterminated attribute value"]} { + uplevel #0 $options(-errorcommand) [list unterminatedattribute "$attr around line $state(line)"] + set attr {} + } else { + + # It is most likely that a ">" character was in an attribute value. + # This manifests itself by ">" appearing in the element's text. + # In this case the callback should return a three element list; + # the message "unterminated attribute value", the attribute list it + # did manage to parse and the remainder of the attribute list. + + foreach {msg attlist brokenattr} $attr break + + upvar text elemText + if {[string first > $elemText] >= 0} { + + # Now piece the attribute list back together + regexp [cl $Wsp]*($Name)[cl $Wsp]*=[cl $Wsp]*("|')(.*) $brokenattr discard attname delimiter attvalue + regexp (.*)>([cl ^>]*)\$ $elemText discard remattlist elemText + regexp ([cl ^$delimiter]*)${delimiter}(.*) $remattlist discard remattvalue remattlist + + # Gotcha: watch out for empty element syntax + if {[string match */ [string trimright $remattlist]]} { + set remattlist [string range $remattlist 0 end-1] + set handleEmpty 1 + set cfg(-empty) 1 + } + + append attvalue >$remattvalue + lappend attlist $attname $attvalue + + # Complete parsing the attribute list + if {[catch {uplevel #0 $options(-parseattributelistcommand) [list $options(-statevariable) $remattlist]} attr]} { + uplevel #0 $options(-errorcommand) [list unterminatedattribute "$attr around line $state(line)"] + set attr {} + set attlist {} + } else { + eval lappend attlist $attr + } + + set attr $attlist + + } else { + uplevel #0 $options(-errorcommand) [list unterminatedattribute "$attr around line $state(line)"] + set attr {} + } + } + } + } + + set empty {} + if {$cfg(-empty) && $options(-reportempty)} { + set empty {-empty 1} + } + + # Check for namespace declarations + upvar #0 $options(namespaces) namespaces + set nsdecls {} + if {[llength $attr]} { + array set attrlist $attr + foreach {attrName attrValue} [array get attrlist xmlns*] { + unset attrlist($attrName) + set colon [set prefix {}] + if {[regexp {^xmlns(:(.+))?$} $attrName discard colon prefix]} { + switch -glob [string length $colon],[string length $prefix] { + 0,0 { + # default NS declaration + lappend state(defaultNSURI) $attrValue + lappend state(defaultNS) [llength $state(stack)] + lappend nsdecls $attrValue {} + } + 0,* { + # Huh? + } + *,0 { + # Error + uplevel #0 $state(-warningcommand) "no prefix specified for namespace URI \"$attrValue\" in element \"$tag\"" + } + default { + set namespaces($prefix,[llength $state(stack)]) $attrValue + lappend nsdecls $attrValue $prefix + } + } + } + } + if {[llength $nsdecls]} { + set nsdecls [list -namespacedecls $nsdecls] + } + set attr [array get attrlist] + } + + # Check whether this element has an expanded name + set ns {} + if {[regexp {([^:]+):(.*)$} $tag discard prefix tag]} { + set nsspec [lsort -dictionary -decreasing [array names namespaces $prefix,*]] + if {[llength $nsspec]} { + set nsuri $namespaces([lindex $nsspec 0]) + set ns [list -namespace $nsuri] + } else { + uplevel #0 $options(-errorcommand) [list namespaceundeclared "no namespace declared for prefix \"$prefix\" in element $tag"] + } + } elseif {[llength $state(defaultNSURI)]} { + set ns [list -namespace [lindex $state(defaultNSURI) end]] + } + + # Invoke callback + set code [catch {uplevel #0 $options(-elementstartcommand) [list $tag $attr] $empty $ns $nsdecls} msg] + + # Sometimes empty elements must be handled here (see above) + if {$code == 0 && $handleEmpty} { + ParseEvent:ElementClose $tag $opts -empty 1 + } + + return -code $code -errorinfo $::errorInfo $msg +} + +# sgml::ParseEvent:ElementClose -- +# +# End of an element. +# +# Arguments: +# tag Element name +# opts Options +# args further configuration options +# +# Options: +# -empty boolean +# indicates whether the element as an empty element +# +# Results: +# Modify state and invoke callback + +proc sgml::ParseEvent:ElementClose {tag opts args} { + array set options $opts + upvar #0 $options(-statevariable) state + array set cfg {-empty 0} + array set cfg $args + + # WF check + if {[string compare $tag [lindex $state(stack) end]]} { + uplevel #0 $options(-errorcommand) [list illegalendtag "end tag \"$tag\" does not match open element \"[lindex $state(stack) end]\" around line $state(line)"] + return + } + + # Check whether this element has an expanded name + upvar #0 $options(namespaces) namespaces + set ns {} + if {[regexp {([^:]+):(.*)$} $tag discard prefix tag]} { + set nsuri $namespaces([lindex [lsort -dictionary -decreasing [array names namespaces $prefix,*]] 0]) + set ns [list -namespace $nsuri] + } elseif {[llength $state(defaultNSURI)]} { + set ns [list -namespace [lindex $state(defaultNSURI) end]] + } + + # Pop namespace stacks, if any + if {[llength $state(defaultNS)]} { + if {[llength $state(stack)] == [lindex $state(defaultNS) end]} { + set state(defaultNS) [lreplace $state(defaultNS) end end] + } + } + foreach nsspec [array names namespaces *,[llength $state(stack)]] { + unset namespaces($nsspec) + } + + # Update state + set state(stack) [lreplace $state(stack) end end] + + set empty {} + if {$cfg(-empty) && $options(-reportempty)} { + set empty {-empty 1} + } + + # Invoke callback + # Mats: Shall be same as sgml::ParseEvent:ElementOpen to handle exceptions in callback. + set code [catch {uplevel #0 $options(-elementendcommand) [list $tag] $empty $ns} msg] + return -code $code -errorinfo $::errorInfo $msg +} + +# sgml::PCDATA -- +# +# Process PCDATA before passing to application +# +# Arguments: +# opts options +# pcdata Character data to be processed +# +# Results: +# Checks that characters are legal, +# checks -ignorewhitespace setting. + +proc sgml::PCDATA {opts pcdata} { + array set options $opts + + if {$options(-ignorewhitespace) && \ + ![string length [string trim $pcdata]]} { + return {} + } + + if {![regexp ^[cl $::sgml::Char]*\$ $pcdata]} { + upvar \#0 $options(-statevariable) state + uplevel \#0 $options(-errorcommand) [list illegalcharacters "illegal, non-Unicode characters found in text \"$pcdata\" around line $state(line)"] + } + + uplevel \#0 $options(-characterdatacommand) [list $pcdata] +} + +# sgml::Normalize -- +# +# Perform name normalization if required +# +# Arguments: +# name name to normalize +# req normalization required +# +# Results: +# Name returned as upper-case if normalization required + +proc sgml::Normalize {name req} { + if {$req} { + return [string toupper $name] + } else { + return $name + } +} + +# sgml::Entity -- +# +# Resolve XML entity references (syntax: &xxx;). +# +# Arguments: +# opts options +# entityrefcmd application callback for entity references +# pcdatacmd application callback for character data +# entities name of array containing entity definitions. +# ref entity reference (the "xxx" bit) +# +# Results: +# Returns substitution text for given entity. + +proc sgml::Entity {opts entityrefcmd pcdatacmd entities ref} { + array set options $opts + upvar #0 $options(-statevariable) state + + if {![string length $entities]} { + set entities [namespace current]::EntityPredef + } + + switch -glob -- $ref { + %* { + # Parameter entity - not recognised outside of a DTD + } + #x* { + # Character entity - hex + if {[catch {format %c [scan [string range $ref 2 end] %x tmp; set tmp]} char]} { + return -code error "malformed character entity \"$ref\"" + } + uplevel #0 $pcdatacmd [list $char] + + return {} + + } + #* { + # Character entity - decimal + if {[catch {format %c [scan [string range $ref 1 end] %d tmp; set tmp]} char]} { + return -code error "malformed character entity \"$ref\"" + } + uplevel #0 $pcdatacmd [list $char] + + return {} + + } + default { + # General entity + upvar #0 $entities map + if {[info exists map($ref)]} { + + if {![regexp {<|&} $map($ref)]} { + + # Simple text replacement - optimise + uplevel #0 $pcdatacmd [list $map($ref)] + + return {} + + } + + # Otherwise an additional round of parsing is required. + # This only applies to XML, since HTML doesn't have general entities + + # Must parse the replacement text for start & end tags, etc + # This text must be self-contained: balanced closing tags, and so on + + set tokenised [tokenise $map($ref) $::xml::tokExpr $::xml::substExpr] + set options(-final) 0 + eval parseEvent [list $tokenised] [array get options] + + return {} + + } elseif {[string compare $entityrefcmd "::sgml::noop"]} { + + set result [uplevel #0 $entityrefcmd [list $ref]] + + if {[string length $result]} { + uplevel #0 $pcdatacmd [list $result] + } + + return {} + + } else { + + # Reconstitute entity reference + + uplevel #0 $options(-errorcommand) [list illegalentity "undefined entity reference \"$ref\""] + + return {} + + } + } + } + + # If all else fails leave the entity reference untouched + uplevel #0 $pcdatacmd [list &$ref\;] + + return {} +} + +#################################### +# +# DTD parser for SGML (XML). +# +# This DTD actually only handles XML DTDs. Other language's +# DTD's, such as HTML, must be written in terms of a XML DTD. +# +#################################### + +# sgml::ParseEvent:DocTypeDecl -- +# +# Entry point for DTD parsing +# +# Arguments: +# opts configuration options +# docEl document element name +# pubId public identifier +# sysId system identifier (a URI) +# intSSet internal DTD subset + +proc sgml::ParseEvent:DocTypeDecl {opts docEl pubId sysId intSSet} { + array set options {} + array set options $opts + + set code [catch {uplevel #0 $options(-doctypecommand) [list $docEl $pubId $sysId $intSSet]} err] + switch $code { + 3 { + # break + return {} + } + 0 - + 4 { + # continue + } + default { + return -code $code $err + } + } + + # Otherwise we'll parse the DTD and report it piecemeal + + # The internal DTD subset is processed first (XML 2.8) + # During this stage, parameter entities are only allowed + # between markup declarations + + ParseDTD:Internal [array get options] $intSSet + + # The external DTD subset is processed last (XML 2.8) + # During this stage, parameter entities may occur anywhere + + # We must resolve the external identifier to obtain the + # DTD data. The application may supply its own resolver. + + if {[string length $pubId] || [string length $sysId]} { + uplevel #0 $options(-externalentitycommand) [list $options(-cmd) $options(-baseuri) $sysId $pubId] + } + + return {} +} + +# sgml::ParseDTD:Internal -- +# +# Parse the internal DTD subset. +# +# Parameter entities are only allowed between markup declarations. +# +# Arguments: +# opts configuration options +# dtd DTD data +# +# Results: +# Markup declarations parsed may cause callback invocation + +proc sgml::ParseDTD:Internal {opts dtd} { + variable MarkupDeclExpr + variable MarkupDeclSub + + array set options {} + array set options $opts + + upvar #0 $options(-statevariable) state + upvar #0 $options(parameterentities) PEnts + upvar #0 $options(externalparameterentities) ExtPEnts + + # Bug 583947: remove comments before further processing + regsub -all {} $dtd {} dtd + + # Tokenize the DTD + + # Protect Tcl special characters + regsub -all {([{}\\])} $dtd {\\\1} dtd + + regsub -all $MarkupDeclExpr $dtd $MarkupDeclSub dtd + + # Entities may have angle brackets in their replacement + # text, which breaks the RE processing. So, we must + # use a similar technique to processing doc instances + # to rebuild the declarations from the pieces + + set mode {} ;# normal + set delimiter {} + set name {} + set param {} + + set state(inInternalDTD) 1 + + # Process the tokens + foreach {decl value text} [lrange "{} {} \{$dtd\}" 3 end] { + + # Keep track of line numbers + incr state(line) [regsub -all \n $text {} discard] + + ParseDTD:EntityMode [array get options] mode replText decl value text $delimiter $name $param + + ParseDTD:ProcessMarkupDecl [array get options] decl value delimiter name mode replText text param + + # There may be parameter entity references between markup decls + + if {[regexp {%.*;} $text]} { + + # Protect Tcl special characters + regsub -all {([{}\\])} $text {\\\1} text + + regsub -all %($::sgml::Name)\; $text "\} {\\1} \{" text + + set PElist "\{$text\}" + set PElist [lreplace $PElist end end] + foreach {text entref} $PElist { + if {[string length [string trim $text]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text in internal DTD subset around line $state(line)"] + } + + # Expand parameter entity and recursively parse + # BUG: no checks yet for recursive entity references + + if {[info exists PEnts($entref)]} { + set externalParser [$options(-cmd) entityparser] + $externalParser parse $PEnts($entref) -dtdsubset internal + } elseif {[info exists ExtPEnts($entref)]} { + set externalParser [$options(-cmd) entityparser] + $externalParser parse $ExtPEnts($entref) -dtdsubset external + #$externalParser free + } else { + uplevel #0 $options(-errorcommand) [list illegalreference "reference to undeclared parameter entity \"$entref\""] + } + } + + } + + } + + return {} +} + +# sgml::ParseDTD:EntityMode -- +# +# Perform special processing for various parser modes +# +# Arguments: +# opts configuration options +# modeVar pass-by-reference mode variable +# replTextVar pass-by-ref +# declVar pass-by-ref +# valueVar pass-by-ref +# textVar pass-by-ref +# delimiter delimiter currently in force +# name +# param +# +# Results: +# Depends on current mode + +proc sgml::ParseDTD:EntityMode {opts modeVar replTextVar declVar valueVar textVar delimiter name param} { + upvar 1 $modeVar mode + upvar 1 $replTextVar replText + upvar 1 $declVar decl + upvar 1 $valueVar value + upvar 1 $textVar text + array set options $opts + + switch $mode { + {} { + # Pass through to normal processing section + } + entity { + # Look for closing delimiter + if {[regexp ([cl ^$delimiter]*)${delimiter}(.*) $decl discard val1 remainder]} { + append replText <$val1 + DTD:ENTITY [array get options] $name [string trim $param] $delimiter$replText$delimiter + set decl / + set text $remainder\ $value>$text + set value {} + set mode {} + } elseif {[regexp ([cl ^$delimiter]*)${delimiter}(.*) $value discard val2 remainder]} { + append replText <$decl\ $val2 + DTD:ENTITY [array get options] $name [string trim $param] $delimiter$replText$delimiter + set decl / + set text $remainder>$text + set value {} + set mode {} + } elseif {[regexp ([cl ^$delimiter]*)${delimiter}(.*) $text discard val3 remainder]} { + append replText <$decl\ $value>$val3 + DTD:ENTITY [array get options] $name [string trim $param] $delimiter$replText$delimiter + set decl / + set text $remainder + set value {} + set mode {} + } else { + + # Remain in entity mode + append replText <$decl\ $value>$text + return -code continue + + } + } + + ignore { + upvar #0 $options(-statevariable) state + + if {[regexp {]](.*)$} $decl discard remainder]} { + set state(condSections) [lreplace $state(condSections) end end] + set decl $remainder + set mode {} + } elseif {[regexp {]](.*)$} $value discard remainder]} { + set state(condSections) [lreplace $state(condSections) end end] + regexp <[cl $::sgml::Wsp]*($::sgml::Name)(.*) $remainder discard decl value + set mode {} + } elseif {[regexp {]]>(.*)$} $text discard remainder]} { + set state(condSections) [lreplace $state(condSections) end end] + set decl / + set value {} + set text $remainder + #regexp <[cl $::sgml::Wsp]*($::sgml::Name)([cl ^>]*)>(.*) $remainder discard decl value text + set mode {} + } else { + set decl / + } + + } + + comment { + # Look for closing comment delimiter + + upvar #0 $options(-statevariable) state + + if {[regexp (.*?)--(.*)\$ $decl discard data1 remainder]} { + } elseif {[regexp (.*?)--(.*)\$ $value discard data1 remainder]} { + } elseif {[regexp (.*?)--(.*)\$ $text discard data1 remainder]} { + } else { + # comment continues + append state(commentdata) <$decl\ $value>$text + set decl / + set value {} + set text {} + } + } + + } + + return {} +} + +# sgml::ParseDTD:ProcessMarkupDecl -- +# +# Process a single markup declaration +# +# Arguments: +# opts configuration options +# declVar pass-by-ref +# valueVar pass-by-ref +# delimiterVar pass-by-ref for current delimiter in force +# nameVar pass-by-ref +# modeVar pass-by-ref for current parser mode +# replTextVar pass-by-ref +# textVar pass-by-ref +# paramVar pass-by-ref +# +# Results: +# Depends on markup declaration. May change parser mode + +proc sgml::ParseDTD:ProcessMarkupDecl {opts declVar valueVar delimiterVar nameVar modeVar replTextVar textVar paramVar} { + upvar 1 $modeVar mode + upvar 1 $replTextVar replText + upvar 1 $textVar text + upvar 1 $declVar decl + upvar 1 $valueVar value + upvar 1 $nameVar name + upvar 1 $delimiterVar delimiter + upvar 1 $paramVar param + + variable declExpr + variable ExternalEntityExpr + + array set options $opts + upvar #0 $options(-statevariable) state + + switch -glob -- $decl { + + / { + # continuation from entity processing + } + + !ELEMENT { + # Element declaration + if {[regexp $declExpr $value discard tag cmodel]} { + DTD:ELEMENT [array get options] $tag $cmodel + } else { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "malformed element declaration around line $state(line)"] + } + } + + !ATTLIST { + # Attribute list declaration + variable declExpr + if {[regexp $declExpr $value discard tag attdefns]} { + if {[catch {DTD:ATTLIST [array get options] $tag $attdefns} err]} { + #puts stderr "Stack trace: $::errorInfo\n***\n" + # Atttribute parsing has bugs at the moment + #return -code error "$err around line $state(line)" + return {} + } + } else { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "malformed attribute list declaration around line $state(line)"] + } + } + + !ENTITY { + # Entity declaration + variable EntityExpr + + if {[regexp $EntityExpr $value discard param name value]} { + + # Entity replacement text may have a '>' character. + # In this case, the real delimiter will be in the following + # text. This is complicated by the possibility of there + # being several '<','>' pairs in the replacement text. + # At this point, we are searching for the matching quote delimiter. + + if {[regexp $ExternalEntityExpr $value]} { + DTD:ENTITY [array get options] $name [string trim $param] $value + } elseif {[regexp ("|')(.*?)\\1(.*) $value discard delimiter replText value]} { + + if {[string length [string trim $value]]} { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "malformed entity declaration around line $state(line)"] + } else { + DTD:ENTITY [array get options] $name [string trim $param] $delimiter$replText$delimiter + } + } elseif {[regexp ("|')(.*) $value discard delimiter replText]} { + append replText >$text + set text {} + set mode entity + } else { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "no delimiter for entity declaration around line $state(line)"] + } + + } else { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "malformed entity declaration around line $state(line)"] + } + } + + !NOTATION { + # Notation declaration + if {[regexp $declExpr param discard tag notation]} { + DTD:ENTITY [array get options] $tag $notation + } else { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "malformed entity declaration around line $state(line)"] + } + } + + !--* { + # Start of a comment + + if {[regexp !--(.*?)--\$ $decl discard data]} { + if {[string length [string trim $value]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$value\""] + } + uplevel #0 $options(-commentcommand) [list $data] + set decl / + set value {} + } elseif {[regexp -- ^(.*?)--\$ $value discard data2]} { + regexp !--(.*)\$ $decl discard data1 + uplevel #0 $options(-commentcommand) [list $data1\ $data2] + set decl / + set value {} + } elseif {[regexp (.*?)-->(.*)\$ $text discard data3 remainder]} { + regexp !--(.*)\$ $decl discard data1 + uplevel #0 $options(-commentcommand) [list $data1\ $value>$data3] + set decl / + set value {} + set text $remainder + } else { + regexp !--(.*)\$ $decl discard data1 + set state(commentdata) $data1\ $value>$text + set decl / + set value {} + set text {} + set mode comment + } + } + + !*INCLUDE* - + !*IGNORE* { + if {$state(inInternalDTD)} { + uplevel #0 $options(-errorcommand) [list illegalsection "conditional section not permitted in internal DTD subset around line $state(line)"] + } + + if {[regexp {^!\[INCLUDE\[(.*)} $decl discard remainder]} { + # Push conditional section stack, popped by ]]> sequence + + if {[regexp {(.*?)]]$} $remainder discard r2]} { + # section closed immediately + if {[string length [string trim $r2]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$r2\" in conditional section"] + } + } elseif {[regexp {(.*?)]](.*)} $value discard r2 r3]} { + # section closed immediately + if {[string length [string trim $r2]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$r2\" in conditional section"] + } + if {[string length [string trim $r3]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$r3\" in conditional section"] + } + } else { + + lappend state(condSections) INCLUDE + + set parser [$options(-cmd) entityparser] + $parser parse $remainder\ $value> -dtdsubset external + #$parser free + + if {[regexp {(.*?)]]>(.*)} $text discard t1 t2]} { + if {[string length [string trim $t1]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$t1\""] + } + if {![llength $state(condSections)]} { + uplevel #0 $options(-errorcommand) [list illegalsection "extraneous conditional section close"] + } + set state(condSections) [lreplace $state(condSections) end end] + set text $t2 + } + + } + } elseif {[regexp {^!\[IGNORE\[(.*)} $decl discard remainder]} { + # Set ignore mode. Still need a stack + set mode ignore + + if {[regexp {(.*?)]]$} $remainder discard r2]} { + # section closed immediately + if {[string length [string trim $r2]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$r2\" in conditional section"] + } + } elseif {[regexp {(.*?)]](.*)} $value discard r2 r3]} { + # section closed immediately + if {[string length [string trim $r2]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$r2\" in conditional section"] + } + if {[string length [string trim $r3]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$r3\" in conditional section"] + } + } else { + + lappend state(condSections) IGNORE + + if {[regexp {(.*?)]]>(.*)} $text discard t1 t2]} { + if {[string length [string trim $t1]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$t1\""] + } + if {![llength $state(condSections)]} { + uplevel #0 $options(-errorcommand) [list illegalsection "extraneous conditional section close"] + } + set state(condSections) [lreplace $state(condSections) end end] + set text $t2 + } + + } + } else { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "illegal markup declaration \"$decl\" around line $state(line)"] + } + + } + + default { + if {[regexp {^\?(.*)} $decl discard target]} { + # Processing instruction + } else { + uplevel #0 $options(-errorcommand) [list illegaldeclaration "illegal markup declaration \"$decl\""] + } + } + } + + return {} +} + +# sgml::ParseDTD:External -- +# +# Parse the external DTD subset. +# +# Parameter entities are allowed anywhere. +# +# Arguments: +# opts configuration options +# dtd DTD data +# +# Results: +# Markup declarations parsed may cause callback invocation + +proc sgml::ParseDTD:External {opts dtd} { + variable MarkupDeclExpr + variable MarkupDeclSub + variable declExpr + + array set options $opts + upvar #0 $options(parameterentities) PEnts + upvar #0 $options(externalparameterentities) ExtPEnts + upvar #0 $options(-statevariable) state + + # As with the internal DTD subset, watch out for + # entities with angle brackets + set mode {} ;# normal + set delimiter {} + set name {} + set param {} + + set oldState 0 + catch {set oldState $state(inInternalDTD)} + set state(inInternalDTD) 0 + + # Initialise conditional section stack + if {![info exists state(condSections)]} { + set state(condSections) {} + } + set startCondSectionDepth [llength $state(condSections)] + + while {[string length $dtd]} { + set progress 0 + set PEref {} + if {![string compare $mode "ignore"]} { + set progress 1 + if {[regexp {]]>(.*)} $dtd discard dtd]} { + set remainder {} + set mode {} ;# normal + set state(condSections) [lreplace $state(condSections) end end] + continue + } else { + uplevel #0 $options(-errorcommand) [list missingdelimiter "IGNORE conditional section closing delimiter not found"] + } + } elseif {[regexp ^(.*?)%($::sgml::Name)\;(.*)\$ $dtd discard data PEref remainder]} { + set progress 1 + } else { + set data $dtd + set dtd {} + set remainder {} + } + + # Tokenize the DTD (so far) + + # Protect Tcl special characters + regsub -all {([{}\\])} $data {\\\1} dataP + + set n [regsub -all $MarkupDeclExpr $dataP $MarkupDeclSub dataP] + + if {$n} { + set progress 1 + # All but the last markup declaration should have no text + set dataP [lrange "{} {} \{$dataP\}" 3 end] + if {[llength $dataP] > 3} { + foreach {decl value text} [lrange $dataP 0 [expr [llength $dataP] - 4]] { + ParseDTD:EntityMode [array get options] mode replText decl value text $delimiter $name $param + ParseDTD:ProcessMarkupDecl [array get options] decl value delimiter name mode repltextVar text param + + if {[string length [string trim $text]]} { + # check for conditional section close + if {[regexp {]]>(.*)$} $text discard text]} { + if {[string length [string trim $text]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$text\""] + } + if {![llength $state(condSections)]} { + uplevel #0 $options(-errorcommand) [list illegalsection "extraneous conditional section close"] + } + set state(condSections) [lreplace $state(condSections) end end] + if {![string compare $mode "ignore"]} { + set mode {} ;# normal + } + } else { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$text\""] + } + } + } + } + # Do the last declaration + foreach {decl value text} [lrange $dataP [expr [llength $dataP] - 3] end] { + ParseDTD:EntityMode [array get options] mode replText decl value text $delimiter $name $param + ParseDTD:ProcessMarkupDecl [array get options] decl value delimiter name mode repltextVar text param + } + } + + # Now expand the PE reference, if any + switch -glob $mode,[string length $PEref],$n { + ignore,0,* { + set dtd $text + } + ignore,*,* { + set dtd $text$remainder + } + *,0,0 { + set dtd $data + } + *,0,* { + set dtd $text + } + *,*,0 { + if {[catch {append data $PEnts($PEref)}]} { + if {[info exists ExtPEnts($PEref)]} { + set externalParser [$options(-cmd) entityparser] + $externalParser parse $ExtPEnts($PEref) -dtdsubset external + #$externalParser free + } else { + uplevel #0 $options(-errorcommand) [list entityundeclared "parameter entity \"$PEref\" not declared"] + } + } + set dtd $data$remainder + } + default { + if {[catch {append text $PEnts($PEref)}]} { + if {[info exists ExtPEnts($PEref)]} { + set externalParser [$options(-cmd) entityparser] + $externalParser parse $ExtPEnts($PEref) -dtdsubset external + #$externalParser free + } else { + uplevel #0 $options(-errorcommand) [list entityundeclared "parameter entity \"$PEref\" not declared"] + } + } + set dtd $text$remainder + } + } + + # Check whether a conditional section has been terminated + if {[regexp {^(.*?)]]>(.*)$} $dtd discard t1 t2]} { + if {![regexp <.*> $t1]} { + if {[string length [string trim $t1]]} { + uplevel #0 $options(-errorcommand) [list unexpectedtext "unexpected text \"$t1\""] + } + if {![llength $state(condSections)]} { + uplevel #0 $options(-errorcommand) [list illegalsection "extraneous conditional section close"] + } + set state(condSections) [lreplace $state(condSections) end end] + if {![string compare $mode "ignore"]} { + set mode {} ;# normal + } + set dtd $t2 + set progress 1 + } + } + + if {!$progress} { + # No parameter entity references were found and + # the text does not contain a well-formed markup declaration + # Avoid going into an infinite loop + upvar #0 $options(-errorcommand) [list syntaxerror "external entity does not contain well-formed markup declaration"] + break + } + } + + set state(inInternalDTD) $oldState + + # Check that conditional sections have been closed properly + if {[llength $state(condSections)] > $startCondSectionDepth} { + uplevel #0 $options(-errorcommand) [list syntaxerror "[lindex $state(condSections) end] conditional section not closed"] + } + if {[llength $state(condSections)] < $startCondSectionDepth} { + uplevel #0 $options(-errorcommand) [list syntaxerror "too many conditional section closures"] + } + + return {} +} + +# Procedures for handling the various declarative elements in a DTD. +# New elements may be added by creating a procedure of the form +# parse:DTD:_element_ + +# For each of these procedures, the various regular expressions they use +# are created outside of the proc to avoid overhead at runtime + +# sgml::DTD:ELEMENT -- +# +# defines an element. +# +# The content model for the element is stored in the contentmodel array, +# indexed by the element name. The content model is parsed into the +# following list form: +# +# {} Content model is EMPTY. +# Indicated by an empty list. +# * Content model is ANY. +# Indicated by an asterix. +# {ELEMENT ...} +# Content model is element-only. +# {MIXED {element1 element2 ...}} +# Content model is mixed (PCDATA and elements). +# The second element of the list contains the +# elements that may occur. #PCDATA is assumed +# (ie. the list is normalised). +# +# Arguments: +# opts configuration options +# name element GI +# modspec unparsed content model specification + +proc sgml::DTD:ELEMENT {opts name modspec} { + variable Wsp + array set options $opts + + upvar #0 $options(elementdecls) elements + + if {$options(-validate) && [info exists elements($name)]} { + eval $options(-errorcommand) [list elementdeclared "element \"$name\" already declared"] + } else { + switch -- $modspec { + EMPTY { + set elements($name) {} + uplevel #0 $options(-elementdeclcommand) $name {{}} + } + ANY { + set elements($name) * + uplevel #0 $options(-elementdeclcommand) $name * + } + default { + # Don't parse the content model for now, + # just pass the model to the application + if {0 && [regexp [format {^\([%s]*#PCDATA[%s]*(\|([^)]+))?[%s]*\)*[%s]*$} $Wsp $Wsp $Wsp $Wsp] discard discard mtoks]} { + set cm($name) [list MIXED [split $mtoks |]] + } elseif {0} { + if {[catch {CModelParse $state(state) $value} result]} { + eval $options(-errorcommand) [list element? $result] + } else { + set cm($id) [list ELEMENT $result] + } + } else { + set elements($name) $modspec + uplevel #0 $options(-elementdeclcommand) $name [list $modspec] + } + } + } + } +} + +# sgml::CModelParse -- +# +# Parse an element content model (non-mixed). +# A syntax tree is constructed. +# A transition table is built next. +# +# This is going to need alot of work! +# +# Arguments: +# state state array variable +# value the content model data +# +# Results: +# A Tcl list representing the content model. + +proc sgml::CModelParse {state value} { + upvar #0 $state var + + # First build syntax tree + set syntaxTree [CModelMakeSyntaxTree $state $value] + + # Build transition table + set transitionTable [CModelMakeTransitionTable $state $syntaxTree] + + return [list $syntaxTree $transitionTable] +} + +# sgml::CModelMakeSyntaxTree -- +# +# Construct a syntax tree for the regular expression. +# +# Syntax tree is represented as a Tcl list: +# rep {:choice|:seq {{rep list1} {rep list2} ...}} +# where: rep is repetition character, *, + or ?. {} for no repetition +# listN is nested expression or Name +# +# Arguments: +# spec Element specification +# +# Results: +# Syntax tree for element spec as nested Tcl list. +# +# Examples: +# (memo) +# {} {:seq {{} memo}} +# (front, body, back?) +# {} {:seq {{} front} {{} body} {? back}} +# (head, (p | list | note)*, div2*) +# {} {:seq {{} head} {* {:choice {{} p} {{} list} {{} note}}} {* div2}} +# (p | a | ul)+ +# + {:choice {{} p} {{} a} {{} ul}} + +proc sgml::CModelMakeSyntaxTree {state spec} { + upvar #0 $state var + variable Wsp + variable name + + # Translate the spec into a Tcl list. + + # None of the Tcl special characters are allowed in a content model spec. + if {[regexp {\$|\[|\]|\{|\}} $spec]} { + return -code error "illegal characters in specification" + } + + regsub -all [format {(%s)[%s]*(\?|\*|\+)?[%s]*(,|\|)?} $name $Wsp $Wsp] $spec [format {%sCModelSTname %s {\1} {\2} {\3}} \n $state] spec + regsub -all {\(} $spec "\nCModelSTopenParen $state " spec + regsub -all [format {\)[%s]*(\?|\*|\+)?[%s]*(,|\|)?} $Wsp $Wsp] $spec [format {%sCModelSTcloseParen %s {\1} {\2}} \n $state] spec + + array set var {stack {} state start} + eval $spec + + # Peel off the outer seq, its redundant + return [lindex [lindex $var(stack) 1] 0] +} + +# sgml::CModelSTname -- +# +# Processes a name in a content model spec. +# +# Arguments: +# state state array variable +# name name specified +# rep repetition operator +# cs choice or sequence delimiter +# +# Results: +# See CModelSTcp. + +proc sgml::CModelSTname {state name rep cs args} { + if {[llength $args]} { + return -code error "syntax error in specification: \"$args\"" + } + + CModelSTcp $state $name $rep $cs +} + +# sgml::CModelSTcp -- +# +# Process a content particle. +# +# Arguments: +# state state array variable +# name name specified +# rep repetition operator +# cs choice or sequence delimiter +# +# Results: +# The content particle is added to the current group. + +proc sgml::CModelSTcp {state cp rep cs} { + upvar #0 $state var + + switch -glob -- [lindex $var(state) end]=$cs { + start= { + set var(state) [lreplace $var(state) end end end] + # Add (dummy) grouping, either choice or sequence will do + CModelSTcsSet $state , + CModelSTcpAdd $state $cp $rep + } + :choice= - + :seq= { + set var(state) [lreplace $var(state) end end end] + CModelSTcpAdd $state $cp $rep + } + start=| - + start=, { + set var(state) [lreplace $var(state) end end [expr {$cs == "," ? ":seq" : ":choice"}]] + CModelSTcsSet $state $cs + CModelSTcpAdd $state $cp $rep + } + :choice=| - + :seq=, { + CModelSTcpAdd $state $cp $rep + } + :choice=, - + :seq=| { + return -code error "syntax error in specification: incorrect delimiter after \"$cp\", should be \"[expr {$cs == "," ? "|" : ","}]\"" + } + end=* { + return -code error "syntax error in specification: no delimiter before \"$cp\"" + } + default { + return -code error "syntax error" + } + } + +} + +# sgml::CModelSTcsSet -- +# +# Start a choice or sequence on the stack. +# +# Arguments: +# state state array +# cs choice oir sequence +# +# Results: +# state is modified: end element of state is appended. + +proc sgml::CModelSTcsSet {state cs} { + upvar #0 $state var + + set cs [expr {$cs == "," ? ":seq" : ":choice"}] + + if {[llength $var(stack)]} { + set var(stack) [lreplace $var(stack) end end $cs] + } else { + set var(stack) [list $cs {}] + } +} + +# sgml::CModelSTcpAdd -- +# +# Append a content particle to the top of the stack. +# +# Arguments: +# state state array +# cp content particle +# rep repetition +# +# Results: +# state is modified: end element of state is appended. + +proc sgml::CModelSTcpAdd {state cp rep} { + upvar #0 $state var + + if {[llength $var(stack)]} { + set top [lindex $var(stack) end] + lappend top [list $rep $cp] + set var(stack) [lreplace $var(stack) end end $top] + } else { + set var(stack) [list $rep $cp] + } +} + +# sgml::CModelSTopenParen -- +# +# Processes a '(' in a content model spec. +# +# Arguments: +# state state array +# +# Results: +# Pushes stack in state array. + +proc sgml::CModelSTopenParen {state args} { + upvar #0 $state var + + if {[llength $args]} { + return -code error "syntax error in specification: \"$args\"" + } + + lappend var(state) start + lappend var(stack) [list {} {}] +} + +# sgml::CModelSTcloseParen -- +# +# Processes a ')' in a content model spec. +# +# Arguments: +# state state array +# rep repetition +# cs choice or sequence delimiter +# +# Results: +# Stack is popped, and former top of stack is appended to previous element. + +proc sgml::CModelSTcloseParen {state rep cs args} { + upvar #0 $state var + + if {[llength $args]} { + return -code error "syntax error in specification: \"$args\"" + } + + set cp [lindex $var(stack) end] + set var(stack) [lreplace $var(stack) end end] + set var(state) [lreplace $var(state) end end] + CModelSTcp $state $cp $rep $cs +} + +# sgml::CModelMakeTransitionTable -- +# +# Given a content model's syntax tree, constructs +# the transition table for the regular expression. +# +# See "Compilers, Principles, Techniques, and Tools", +# Aho, Sethi and Ullman. Section 3.9, algorithm 3.5. +# +# Arguments: +# state state array variable +# st syntax tree +# +# Results: +# The transition table is returned, as a key/value Tcl list. + +proc sgml::CModelMakeTransitionTable {state st} { + upvar #0 $state var + + # Construct nullable, firstpos and lastpos functions + array set var {number 0} + foreach {nullable firstpos lastpos} [ \ + TraverseDepth1st $state $st { + # Evaluated for leaf nodes + # Compute nullable(n) + # Compute firstpos(n) + # Compute lastpos(n) + set nullable [nullable leaf $rep $name] + set firstpos [list {} $var(number)] + set lastpos [list {} $var(number)] + set var(pos:$var(number)) $name + } { + # Evaluated for nonterminal nodes + # Compute nullable, firstpos, lastpos + set firstpos [firstpos $cs $firstpos $nullable] + set lastpos [lastpos $cs $lastpos $nullable] + set nullable [nullable nonterm $rep $cs $nullable] + } \ + ] break + + set accepting [incr var(number)] + set var(pos:$accepting) # + + # var(pos:N) maps from position to symbol. + # Construct reverse map for convenience. + # NB. A symbol may appear in more than one position. + # var is about to be reset, so use different arrays. + + foreach {pos symbol} [array get var pos:*] { + set pos [lindex [split $pos :] 1] + set pos2symbol($pos) $symbol + lappend sym2pos($symbol) $pos + } + + # Construct the followpos functions + catch {unset var} + followpos $state $st $firstpos $lastpos + + # Construct transition table + # Dstates is [union $marked $unmarked] + set unmarked [list [lindex $firstpos 1]] + while {[llength $unmarked]} { + set T [lindex $unmarked 0] + lappend marked $T + set unmarked [lrange $unmarked 1 end] + + # Find which input symbols occur in T + set symbols {} + foreach pos $T { + if {$pos != $accepting && [lsearch $symbols $pos2symbol($pos)] < 0} { + lappend symbols $pos2symbol($pos) + } + } + foreach a $symbols { + set U {} + foreach pos $sym2pos($a) { + if {[lsearch $T $pos] >= 0} { + # add followpos($pos) + if {$var($pos) == {}} { + lappend U $accepting + } else { + eval lappend U $var($pos) + } + } + } + set U [makeSet $U] + if {[llength $U] && [lsearch $marked $U] < 0 && [lsearch $unmarked $U] < 0} { + lappend unmarked $U + } + set Dtran($T,$a) $U + } + + } + + return [list [array get Dtran] [array get sym2pos] $accepting] +} + +# sgml::followpos -- +# +# Compute the followpos function, using the already computed +# firstpos and lastpos. +# +# Arguments: +# state array variable to store followpos functions +# st syntax tree +# firstpos firstpos functions for the syntax tree +# lastpos lastpos functions +# +# Results: +# followpos functions for each leaf node, in name/value format + +proc sgml::followpos {state st firstpos lastpos} { + upvar #0 $state var + + switch -- [lindex [lindex $st 1] 0] { + :seq { + for {set i 1} {$i < [llength [lindex $st 1]]} {incr i} { + followpos $state [lindex [lindex $st 1] $i] \ + [lindex [lindex $firstpos 0] [expr $i - 1]] \ + [lindex [lindex $lastpos 0] [expr $i - 1]] + foreach pos [lindex [lindex [lindex $lastpos 0] [expr $i - 1]] 1] { + eval lappend var($pos) [lindex [lindex [lindex $firstpos 0] $i] 1] + set var($pos) [makeSet $var($pos)] + } + } + } + :choice { + for {set i 1} {$i < [llength [lindex $st 1]]} {incr i} { + followpos $state [lindex [lindex $st 1] $i] \ + [lindex [lindex $firstpos 0] [expr $i - 1]] \ + [lindex [lindex $lastpos 0] [expr $i - 1]] + } + } + default { + # No action at leaf nodes + } + } + + switch -- [lindex $st 0] { + ? { + # We having nothing to do here ! Doing the same as + # for * effectively converts this qualifier into the other. + } + * { + foreach pos [lindex $lastpos 1] { + eval lappend var($pos) [lindex $firstpos 1] + set var($pos) [makeSet $var($pos)] + } + } + } + +} + +# sgml::TraverseDepth1st -- +# +# Perform depth-first traversal of a tree. +# A new tree is constructed, with each node computed by f. +# +# Arguments: +# state state array variable +# t The tree to traverse, a Tcl list +# leaf Evaluated at a leaf node +# nonTerm Evaluated at a nonterminal node +# +# Results: +# A new tree is returned. + +proc sgml::TraverseDepth1st {state t leaf nonTerm} { + upvar #0 $state var + + set nullable {} + set firstpos {} + set lastpos {} + + switch -- [lindex [lindex $t 1] 0] { + :seq - + :choice { + set rep [lindex $t 0] + set cs [lindex [lindex $t 1] 0] + + foreach child [lrange [lindex $t 1] 1 end] { + foreach {childNullable childFirstpos childLastpos} \ + [TraverseDepth1st $state $child $leaf $nonTerm] break + lappend nullable $childNullable + lappend firstpos $childFirstpos + lappend lastpos $childLastpos + } + + eval $nonTerm + } + default { + incr var(number) + set rep [lindex [lindex $t 0] 0] + set name [lindex [lindex $t 1] 0] + eval $leaf + } + } + + return [list $nullable $firstpos $lastpos] +} + +# sgml::firstpos -- +# +# Computes the firstpos function for a nonterminal node. +# +# Arguments: +# cs node type, choice or sequence +# firstpos firstpos functions for the subtree +# nullable nullable functions for the subtree +# +# Results: +# firstpos function for this node is returned. + +proc sgml::firstpos {cs firstpos nullable} { + switch -- $cs { + :seq { + set result [lindex [lindex $firstpos 0] 1] + for {set i 0} {$i < [llength $nullable]} {incr i} { + if {[lindex [lindex $nullable $i] 1]} { + eval lappend result [lindex [lindex $firstpos [expr $i + 1]] 1] + } else { + break + } + } + } + :choice { + foreach child $firstpos { + eval lappend result $child + } + } + } + + return [list $firstpos [makeSet $result]] +} + +# sgml::lastpos -- +# +# Computes the lastpos function for a nonterminal node. +# Same as firstpos, only logic is reversed +# +# Arguments: +# cs node type, choice or sequence +# lastpos lastpos functions for the subtree +# nullable nullable functions forthe subtree +# +# Results: +# lastpos function for this node is returned. + +proc sgml::lastpos {cs lastpos nullable} { + switch -- $cs { + :seq { + set result [lindex [lindex $lastpos end] 1] + for {set i [expr [llength $nullable] - 1]} {$i >= 0} {incr i -1} { + if {[lindex [lindex $nullable $i] 1]} { + eval lappend result [lindex [lindex $lastpos $i] 1] + } else { + break + } + } + } + :choice { + foreach child $lastpos { + eval lappend result $child + } + } + } + + return [list $lastpos [makeSet $result]] +} + +# sgml::makeSet -- +# +# Turn a list into a set, ie. remove duplicates. +# +# Arguments: +# s a list +# +# Results: +# A set is returned, which is a list with duplicates removed. + +proc sgml::makeSet s { + foreach r $s { + if {[llength $r]} { + set unique($r) {} + } + } + return [array names unique] +} + +# sgml::nullable -- +# +# Compute the nullable function for a node. +# +# Arguments: +# nodeType leaf or nonterminal +# rep repetition applying to this node +# name leaf node: symbol for this node, nonterm node: choice or seq node +# subtree nonterm node: nullable functions for the subtree +# +# Results: +# Returns nullable function for this branch of the tree. + +proc sgml::nullable {nodeType rep name {subtree {}}} { + switch -glob -- $rep:$nodeType { + :leaf - + +:leaf { + return [list {} 0] + } + \\*:leaf - + \\?:leaf { + return [list {} 1] + } + \\*:nonterm - + \\?:nonterm { + return [list $subtree 1] + } + :nonterm - + +:nonterm { + switch -- $name { + :choice { + set result 0 + foreach child $subtree { + set result [expr $result || [lindex $child 1]] + } + } + :seq { + set result 1 + foreach child $subtree { + set result [expr $result && [lindex $child 1]] + } + } + } + return [list $subtree $result] + } + } +} + +# sgml::DTD:ATTLIST -- +# +# defines an attribute list. +# +# Arguments: +# opts configuration opions +# name Element GI +# attspec unparsed attribute definitions +# +# Results: +# Attribute list variables are modified. + +proc sgml::DTD:ATTLIST {opts name attspec} { + variable attlist_exp + variable attlist_enum_exp + variable attlist_fixed_exp + + array set options $opts + + # Parse the attribute list. If it were regular, could just use foreach, + # but some attributes may have values. + regsub -all {([][$\\])} $attspec {\\\1} attspec + regsub -all $attlist_exp $attspec "\}\nDTDAttribute {$options(-attlistdeclcommand)} $name $options(attlistdecls) {\\1} {\\2} {\\3} {} \{" attspec + regsub -all $attlist_enum_exp $attspec "\}\nDTDAttribute {$options(-attlistdeclcommand)} $name $options(attlistdecls) {\\1} {\\2} {} {\\4} \{" attspec + regsub -all $attlist_fixed_exp $attspec "\}\nDTDAttribute {$options(-attlistdeclcommand)} $name $options(attlistdecls) {\\1} {\\2} {\\3} {\\4} \{" attspec + + eval "noop \{$attspec\}" + + return {} +} + +# sgml::DTDAttribute -- +# +# Parse definition of a single attribute. +# +# Arguments: +# callback attribute defn callback +# name element name +# var array variable +# att attribute name +# type type of this attribute +# default default value of the attribute +# value other information +# text other text (should be empty) +# +# Results: +# Attribute defn added to array, unless it already exists + +proc sgml::DTDAttribute args { + # BUG: Some problems with parameter passing - deal with it later + foreach {callback name var att type default value text} $args break + + upvar #0 $var atts + + if {[string length [string trim $text]]} { + return -code error "unexpected text \"$text\" in attribute definition" + } + + # What about overridden attribute defns? + # A non-validating app may want to know about them + # (eg. an editor) + if {![info exists atts($name/$att)]} { + set atts($name/$att) [list $type $default $value] + uplevel #0 $callback [list $name $att $type $default $value] + } + + return {} +} + +# sgml::DTD:ENTITY -- +# +# declaration. +# +# Callbacks: +# -entitydeclcommand for general entity declaration +# -unparsedentitydeclcommand for unparsed external entity declaration +# -parameterentitydeclcommand for parameter entity declaration +# +# Arguments: +# opts configuration options +# name name of entity being defined +# param whether a parameter entity is being defined +# value unparsed replacement text +# +# Results: +# Modifies the caller's entities array variable + +proc sgml::DTD:ENTITY {opts name param value} { + + array set options $opts + + if {[string compare % $param]} { + # Entity declaration - general or external + upvar #0 $options(entities) ents + upvar #0 $options(extentities) externals + + if {[info exists ents($name)] || [info exists externals($name)]} { + eval $options(-warningcommand) entity [list "entity \"$name\" already declared"] + } else { + if {[catch {uplevel #0 $options(-parseentitydeclcommand) [list $value]} value]} { + return -code error "unable to parse entity declaration due to \"$value\"" + } + switch -glob [lindex $value 0],[lindex $value 3] { + internal, { + set ents($name) [EntitySubst [array get options] [lindex $value 1]] + uplevel #0 $options(-entitydeclcommand) [list $name $ents($name)] + } + internal,* { + return -code error "unexpected NDATA declaration" + } + external, { + set externals($name) [lrange $value 1 2] + uplevel #0 $options(-entitydeclcommand) [eval list $name [lrange $value 1 2]] + } + external,* { + set externals($name) [lrange $value 1 3] + uplevel #0 $options(-unparsedentitydeclcommand) [eval list $name [lrange $value 1 3]] + } + default { + return -code error "internal error: unexpected parser state" + } + } + } + } else { + # Parameter entity declaration + upvar #0 $options(parameterentities) PEnts + upvar #0 $options(externalparameterentities) ExtPEnts + + if {[info exists PEnts($name)] || [info exists ExtPEnts($name)]} { + eval $options(-warningcommand) parameterentity [list "parameter entity \"$name\" already declared"] + } else { + if {[catch {uplevel #0 $options(-parseentitydeclcommand) [list $value]} value]} { + return -code error "unable to parse parameter entity declaration due to \"$value\"" + } + if {[string length [lindex $value 3]]} { + return -code error "NDATA illegal in parameter entity declaration" + } + switch [lindex $value 0] { + internal { + # Substitute character references and PEs (XML: 4.5) + set value [EntitySubst [array get options] [lindex $value 1]] + + set PEnts($name) $value + uplevel #0 $options(-parameterentitydeclcommand) [list $name $value] + } + external - + default { + # Get the replacement text now. + # Could wait until the first reference, but easier + # to just do it now. + + set token [uri::geturl [uri::resolve $options(-baseuri) [lindex $value 1]]] + + set ExtPEnts($name) [lindex [array get $token data] 1] + uplevel #0 $options(-parameterentitydeclcommand) [eval list $name [lrange $value 1 2]] + } + } + } + } +} + +# sgml::EntitySubst -- +# +# Perform entity substitution on an entity replacement text. +# This differs slightly from other substitution procedures, +# because only parameter and character entity substitution +# is performed, not general entities. +# See XML Rec. section 4.5. +# +# Arguments: +# opts configuration options +# value Literal entity value +# +# Results: +# Expanded replacement text + +proc sgml::EntitySubst {opts value} { + array set options $opts + + # Protect Tcl special characters + regsub -all {([{}\\])} $value {\\\1} value + + # Find entity references + regsub -all (&#\[0-9\]+|&#x\[0-9a-fA-F\]+|%${::sgml::Name})\; $value "\[EntitySubstValue [list $options(parameterentities)] {\\1}\]" value + + set result [subst $value] + + return $result +} + +# sgml::EntitySubstValue -- +# +# Handle a single character or parameter entity substitution +# +# Arguments: +# PEvar array variable containing PE declarations +# ref character or parameter entity reference +# +# Results: +# Replacement text + +proc sgml::EntitySubstValue {PEvar ref} { + switch -glob -- $ref { + &#x* { + scan [string range $ref 3 end] %x hex + return [format %c $hex] + } + &#* { + return [format %c [string range $ref 2 end]] + } + %* { + upvar #0 $PEvar PEs + set ref [string range $ref 1 end] + if {[info exists PEs($ref)]} { + return $PEs($ref) + } else { + return -code error "parameter entity \"$ref\" not declared" + } + } + default { + return -code error "internal error - unexpected entity reference" + } + } + return {} +} + +# sgml::DTD:NOTATION -- +# +# Process notation declaration +# +# Arguments: +# opts configuration options +# name notation name +# value unparsed notation spec + +proc sgml::DTD:NOTATION {opts name value} { + return {} + + variable notation_exp + upvar opts state + + if {[regexp $notation_exp $value x scheme data] == 2} { + } else { + eval $state(-errorcommand) [list notationvalue "notation value \"$value\" incorrectly specified"] + } +} + +# sgml::ResolveEntity -- +# +# Default entity resolution routine +# +# Arguments: +# cmd command of parent parser +# base base URL for relative URLs +# sysId system identifier +# pubId public identifier + +proc sgml::ResolveEntity {cmd base sysId pubId} { + variable ParseEventNum + + if {[catch {uri::resolve $base $sysId} url]} { + return -code error "unable to resolve system identifier \"$sysId\"" + } + if {[catch {uri::geturl $url} token]} { + return -code error "unable to retrieve external entity \"$url\" for system identifier \"$sysId\"" + } + + upvar #0 $token data + + set parser [uplevel #0 $cmd entityparser] + + set body {} + catch {set body $data(body)} + catch {set body $data(data)} + if {[string length $body]} { + uplevel #0 $parser parse [list $body] -dtdsubset external + } + $parser free + + return {} +} diff --git a/lib/tclxml3.1/tclparser-8.1.tcl b/lib/tclxml3.1/tclparser-8.1.tcl new file mode 100644 index 0000000..727727c --- /dev/null +++ b/lib/tclxml3.1/tclparser-8.1.tcl @@ -0,0 +1,612 @@ +# tclparser-8.1.tcl -- +# +# This file provides a Tcl implementation of a XML parser. +# This file supports Tcl 8.1. +# +# See xml-8.[01].tcl for definitions of character sets and +# regular expressions. +# +# Copyright (c) 1998-2003 Zveno Pty Ltd +# http://www.zveno.com/ +# +# See the file "LICENSE" in this distribution for information on usage and +# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# $Id: tclparser-8.1.tcl,v 1.26 2004/08/14 07:41:11 balls Exp $ + +package require Tcl 8.1 + +package provide xml::tclparser 3.1 + +package require xmldefs 3.1 + +package require sgmlparser 1.0 + +namespace eval xml::tclparser { + + namespace export create createexternal externalentity parse configure get delete + + # Tokenising expressions + + variable tokExpr $::xml::tokExpr + variable substExpr $::xml::substExpr + + # Register this parser class + + ::xml::parserclass create tcl \ + -createcommand [namespace code create] \ + -createentityparsercommand [namespace code createentityparser] \ + -parsecommand [namespace code parse] \ + -configurecommand [namespace code configure] \ + -deletecommand [namespace code delete] \ + -resetcommand [namespace code reset] +} + +# xml::tclparser::create -- +# +# Creates XML parser object. +# +# Arguments: +# name unique identifier for this instance +# +# Results: +# The state variable is initialised. + +proc xml::tclparser::create name { + + # Initialise state variable + upvar \#0 [namespace current]::$name parser + array set parser [list -name $name \ + -cmd [uplevel 3 namespace current]::$name \ + -final 1 \ + -validate 0 \ + -statevariable [namespace current]::$name \ + -baseuri {} \ + internaldtd {} \ + entities [namespace current]::Entities$name \ + extentities [namespace current]::ExtEntities$name \ + parameterentities [namespace current]::PEntities$name \ + externalparameterentities [namespace current]::ExtPEntities$name \ + elementdecls [namespace current]::ElDecls$name \ + attlistdecls [namespace current]::AttlistDecls$name \ + notationdecls [namespace current]::NotDecls$name \ + depth 0 \ + leftover {} \ + ] + + # Initialise entities with predefined set + array set [namespace current]::Entities$name [array get ::sgml::EntityPredef] + + return $parser(-cmd) +} + +# xml::tclparser::createentityparser -- +# +# Creates XML parser object for an entity. +# +# Arguments: +# name name for the new parser +# parent name of parent parser +# +# Results: +# The state variable is initialised. + +proc xml::tclparser::createentityparser {parent name} { + upvar #0 [namespace current]::$parent p + + # Initialise state variable + upvar \#0 [namespace current]::$name external + array set external [array get p] + + regsub $parent $p(-cmd) {} parentns + + array set external [list -name $name \ + -cmd $parentns$name \ + -statevariable [namespace current]::$name \ + internaldtd {} \ + line 0 \ + ] + incr external(depth) + + return $external(-cmd) +} + +# xml::tclparser::configure -- +# +# Configures a XML parser object. +# +# Arguments: +# name unique identifier for this instance +# args option name/value pairs +# +# Results: +# May change values of config options + +proc xml::tclparser::configure {name args} { + upvar \#0 [namespace current]::$name parser + + # BUG: very crude, no checks for illegal args + # Mats: Should be synced with sgmlparser.tcl + set options {-elementstartcommand -elementendcommand \ + -characterdatacommand -processinginstructioncommand \ + -externalentitycommand -xmldeclcommand \ + -doctypecommand -commentcommand \ + -entitydeclcommand -unparsedentitydeclcommand \ + -parameterentitydeclcommand -notationdeclcommand \ + -elementdeclcommand -attlistdeclcommand \ + -paramentityparsing -defaultexpandinternalentities \ + -startdoctypedeclcommand -enddoctypedeclcommand \ + -entityreferencecommand -warningcommand \ + -defaultcommand -unknownencodingcommand -notstandalonecommand \ + -startcdatasectioncommand -endcdatasectioncommand \ + -errorcommand -final \ + -validate -baseuri -baseurl \ + -name -cmd -emptyelement \ + -parseattributelistcommand -parseentitydeclcommand \ + -normalize -internaldtd -dtdsubset \ + -reportempty -ignorewhitespace \ + -reportempty \ + } + set usage [join $options ", "] + regsub -all -- - $options {} options + set pat ^-([join $options |])$ + foreach {flag value} $args { + if {[regexp $pat $flag]} { + # Validate numbers + if {[info exists parser($flag)] && \ + [string is integer -strict $parser($flag)] && \ + ![string is integer -strict $value]} { + return -code error "Bad value for $flag ($value), must be integer" + } + set parser($flag) $value + } else { + return -code error "Unknown option $flag, can be: $usage" + } + } + + # Backward-compatibility: -baseuri is a synonym for -baseurl + catch {set parser(-baseuri) $parser(-baseurl)} + + return {} +} + +# xml::tclparser::parse -- +# +# Parses document instance data +# +# Arguments: +# name parser object +# xml data +# args configuration options +# +# Results: +# Callbacks are invoked + +proc xml::tclparser::parse {name xml args} { + + array set options $args + upvar \#0 [namespace current]::$name parser + variable tokExpr + variable substExpr + + # Mats: + if {[llength $args]} { + eval {configure $name} $args + } + + set parseOptions [list \ + -emptyelement [namespace code ParseEmpty] \ + -parseattributelistcommand [namespace code ParseAttrs] \ + -parseentitydeclcommand [namespace code ParseEntity] \ + -normalize 0] + eval lappend parseOptions \ + [array get parser -*command] \ + [array get parser -reportempty] \ + [array get parser -ignorewhitespace] \ + [array get parser -name] \ + [array get parser -cmd] \ + [array get parser -baseuri] \ + [array get parser -validate] \ + [array get parser -final] \ + [array get parser -defaultexpandinternalentities] \ + [array get parser entities] \ + [array get parser extentities] \ + [array get parser parameterentities] \ + [array get parser externalparameterentities] \ + [array get parser elementdecls] \ + [array get parser attlistdecls] \ + [array get parser notationdecls] + + # Mats: + # If -final 0 we also need to maintain the state with a -statevariable ! + if {!$parser(-final)} { + eval lappend parseOptions [array get parser -statevariable] + } + + set dtdsubset no + catch {set dtdsubset $options(-dtdsubset)} + switch -- $dtdsubset { + internal { + # Bypass normal parsing + lappend parseOptions -statevariable $parser(-statevariable) + array set intOptions [array get ::sgml::StdOptions] + array set intOptions $parseOptions + ::sgml::ParseDTD:Internal [array get intOptions] $xml + return {} + } + external { + # Bypass normal parsing + lappend parseOptions -statevariable $parser(-statevariable) + array set intOptions [array get ::sgml::StdOptions] + array set intOptions $parseOptions + ::sgml::ParseDTD:External [array get intOptions] $xml + return {} + } + default { + # Pass through to normal processing + } + } + + lappend tokenOptions \ + -internaldtdvariable [namespace current]::${name}(internaldtd) + + # Mats: If -final 0 we also need to maintain the state with a -statevariable ! + if {!$parser(-final)} { + eval lappend tokenOptions [array get parser -statevariable] \ + [array get parser -final] + } + + # Mats: + # Why not the first four? Just padding? Lrange undos \n interp. + # It is necessary to have the first four as well if chopped off in + # middle of pcdata. + set tokenised [lrange \ + [eval {::sgml::tokenise $xml $tokExpr $substExpr} $tokenOptions] \ + 0 end] + + lappend parseOptions -internaldtd [list $parser(internaldtd)] + eval ::sgml::parseEvent [list $tokenised] $parseOptions + + return {} +} + +# xml::tclparser::ParseEmpty -- Tcl 8.1+ version +# +# Used by parser to determine whether an element is empty. +# This is usually dead easy in XML, but as always not quite. +# Have to watch out for empty element syntax +# +# Arguments: +# tag element name +# attr attribute list (raw) +# e End tag delimiter. +# +# Results: +# Return value of e + +proc xml::tclparser::ParseEmpty {tag attr e} { + switch -glob [string length $e],[regexp "/[::xml::cl $::xml::Wsp]*$" $attr] { + 0,0 { + return {} + } + 0,* { + return / + } + default { + return $e + } + } +} + +# xml::tclparser::ParseAttrs -- Tcl 8.1+ version +# +# Parse element attributes. +# +# There are two forms for name-value pairs: +# +# name="value" +# name='value' +# +# Arguments: +# opts parser options +# attrs attribute string given in a tag +# +# Results: +# Returns a Tcl list representing the name-value pairs in the +# attribute string +# +# A ">" occurring in the attribute list causes problems when parsing +# the XML. This manifests itself by an unterminated attribute value +# and a ">" appearing the element text. +# In this case return a three element list; +# the message "unterminated attribute value", the attribute list it +# did manage to parse and the remainder of the attribute list. + +proc xml::tclparser::ParseAttrs {opts attrs} { + + set result {} + + while {[string length [string trim $attrs]]} { + if {[regexp [::sgml::cl $::xml::Wsp]*($::xml::Name)[::sgml::cl $::xml::Wsp]*=[::sgml::cl $::xml::Wsp]*("|')([::sgml::cl ^<]*?)\\2(.*) $attrs discard attrName delimiter value attrs]} { + lappend result $attrName [NormalizeAttValue $opts $value] + } elseif {[regexp [::sgml::cl $::xml::Wsp]*$::xml::Name[::sgml::cl $::xml::Wsp]*=[::sgml::cl $::xml::Wsp]*("|')[::sgml::cl ^<]*\$ $attrs]} { + return -code error [list {unterminated attribute value} $result $attrs] + } else { + return -code error "invalid attribute list" + } + } + + return $result +} + +# xml::tclparser::NormalizeAttValue -- +# +# Perform attribute value normalisation. This involves: +# . character references are appended to the value +# . entity references are recursively processed and replacement value appended +# . whitespace characters cause a space to be appended +# . other characters appended as-is +# +# Arguments: +# opts parser options +# value unparsed attribute value +# +# Results: +# Normalised value returned. + +proc xml::tclparser::NormalizeAttValue {opts value} { + + # sgmlparser already has backslashes protected + # Protect Tcl specials + regsub -all {([][$])} $value {\\\1} value + + # Deal with white space + regsub -all "\[$::xml::Wsp\]" $value { } value + + # Find entity refs + regsub -all {&([^;]+);} $value {[NormalizeAttValue:DeRef $opts {\1}]} value + + return [subst $value] +} + +# xml::tclparser::NormalizeAttValue:DeRef -- +# +# Handler to normalize attribute values +# +# Arguments: +# opts parser options +# ref entity reference +# +# Results: +# Returns character + +proc xml::tclparser::NormalizeAttValue:DeRef {opts ref} { + + switch -glob -- $ref { + #x* { + scan [string range $ref 2 end] %x value + set char [format %c $value] + # Check that the char is legal for XML + if {[regexp [format {^[%s]$} $::xml::Char] $char]} { + return $char + } else { + return -code error "illegal character" + } + } + #* { + scan [string range $ref 1 end] %d value + set char [format %c $value] + # Check that the char is legal for XML + if {[regexp [format {^[%s]$} $::xml::Char] $char]} { + return $char + } else { + return -code error "illegal character" + } + } + lt - + gt - + amp - + quot - + apos { + array set map {lt < gt > amp & quot \" apos '} + return $map($ref) + } + default { + # A general entity. Must resolve to a text value - no element structure. + + array set options $opts + upvar #0 $options(entities) map + + if {[info exists map($ref)]} { + + if {[regexp < $map($ref)]} { + return -code error "illegal character \"<\" in attribute value" + } + + if {![regexp & $map($ref)]} { + # Simple text replacement + return $map($ref) + } + + # There are entity references in the replacement text. + # Can't use child entity parser since must catch element structures + + return [NormalizeAttValue $opts $map($ref)] + + } elseif {[string compare $options(-entityreferencecommand) "::sgml::noop"]} { + + set result [uplevel #0 $options(-entityreferencecommand) [list $ref]] + + return $result + + } else { + return -code error "unable to resolve entity reference \"$ref\"" + } + } + } +} + +# xml::tclparser::ParseEntity -- +# +# Parse general entity declaration +# +# Arguments: +# data text to parse +# +# Results: +# Tcl list containing entity declaration + +proc xml::tclparser::ParseEntity data { + set data [string trim $data] + if {[regexp $::sgml::ExternalEntityExpr $data discard type delimiter1 id1 discard delimiter2 id2 optNDATA ndata]} { + switch $type { + PUBLIC { + return [list external $id2 $id1 $ndata] + } + SYSTEM { + return [list external $id1 {} $ndata] + } + } + } elseif {[regexp {^("|')(.*?)\1$} $data discard delimiter value]} { + return [list internal $value] + } else { + return -code error "badly formed entity declaration" + } +} + +# xml::tclparser::delete -- +# +# Destroy parser data +# +# Arguments: +# name parser object +# +# Results: +# Parser data structure destroyed + +proc xml::tclparser::delete name { + upvar \#0 [namespace current]::$name parser + catch {::sgml::ParserDelete $parser(-statevariable)} + catch {unset parser} + return {} +} + +# xml::tclparser::get -- +# +# Retrieve additional information from the parser +# +# Arguments: +# name parser object +# method info to retrieve +# args additional arguments for method +# +# Results: +# Depends on method + +proc xml::tclparser::get {name method args} { + upvar #0 [namespace current]::$name parser + + switch -- $method { + + elementdecl { + switch [llength $args] { + + 0 { + # Return all element declarations + upvar #0 $parser(elementdecls) elements + return [array get elements] + } + + 1 { + # Return specific element declaration + upvar #0 $parser(elementdecls) elements + if {[info exists elements([lindex $args 0])]} { + return [array get elements [lindex $args 0]] + } else { + return -code error "element \"[lindex $args 0]\" not declared" + } + } + + default { + return -code error "wrong number of arguments: should be \"elementdecl ?element?\"" + } + } + } + + attlist { + if {[llength $args] != 1} { + return -code error "wrong number of arguments: should be \"get attlist element\"" + } + + upvar #0 $parser(attlistdecls) + + return {} + } + + entitydecl { + } + + parameterentitydecl { + } + + notationdecl { + } + + default { + return -code error "unknown method \"$method\"" + } + } + + return {} +} + +# xml::tclparser::ExternalEntity -- +# +# Resolve and parse external entity +# +# Arguments: +# name parser object +# base base URL +# sys system identifier +# pub public identifier +# +# Results: +# External entity is fetched and parsed + +proc xml::tclparser::ExternalEntity {name base sys pub} { +} + +# xml::tclparser:: -- +# +# Reset a parser instance, ready to parse another document +# +# Arguments: +# name parser object +# +# Results: +# Variables unset + +proc xml::tclparser::reset {name} { + upvar \#0 [namespace current]::$name parser + + # Has this parser object been properly initialised? + if {![info exists parser] || \ + ![info exists parser(-name)]} { + return [create $name] + } + + array set parser { + -final 1 + depth 0 + leftover {} + } + + foreach var {Entities ExtEntities PEntities ExtPEntities ElDecls AttlistDecls NotDecls} { + catch {unset [namespace current]::${var}$name} + } + + # Initialise entities with predefined set + array set [namespace current]::Entities$name [array get ::sgml::EntityPredef] + + return {} +} diff --git a/lib/tclxml3.1/xml-8.1.tcl b/lib/tclxml3.1/xml-8.1.tcl new file mode 100644 index 0000000..501c12a --- /dev/null +++ b/lib/tclxml3.1/xml-8.1.tcl @@ -0,0 +1,133 @@ +# xml.tcl -- +# +# This file provides generic XML services for all implementations. +# This file supports Tcl 8.1 regular expressions. +# +# See tclparser.tcl for the Tcl implementation of a XML parser. +# +# Copyright (c) 1998-2004 Zveno Pty Ltd +# http://www.zveno.com/ +# +# See the file "LICENSE" in this distribution for information on usage and +# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# $Id: xml-8.1.tcl,v 1.16 2004/08/14 07:41:11 balls Exp $ + +package require Tcl 8.1 + +package provide xmldefs 3.1 + +package require sgml 1.8 + +namespace eval xml { + + namespace export qnamesplit + + # Convenience routine + proc cl x { + return "\[$x\]" + } + + # Define various regular expressions + + # Characters + variable Char $::sgml::Char + + # white space + variable Wsp " \t\r\n" + variable allWsp [cl $Wsp]* + variable noWsp [cl ^$Wsp] + + # Various XML names and tokens + + variable NameChar $::sgml::NameChar + variable Name $::sgml::Name + variable Names $::sgml::Names + variable Nmtoken $::sgml::Nmtoken + variable Nmtokens $::sgml::Nmtokens + + # XML Namespaces names + + # NCName ::= Name - ':' + variable NCName $::sgml::Name + regsub -all : $NCName {} NCName + variable QName (${NCName}:)?$NCName ;# (Prefix ':')? LocalPart + + # The definition of the Namespace URI for XML Namespaces themselves. + # The prefix 'xml' is automatically bound to this URI. + variable xmlnsNS http://www.w3.org/XML/1998/namespace + + # table of predefined entities + + variable EntityPredef + array set EntityPredef { + lt < gt > amp & quot \" apos ' + } + + # Expressions for pulling things apart + variable tokExpr <(/?)([::xml::cl ^$::xml::Wsp>/]+)([::xml::cl $::xml::Wsp]*[::xml::cl ^>]*)> + variable substExpr "\}\n{\\2} {\\1} {\\3} \{" + +} + +### +### Exported procedures +### + +# xml::qnamesplit -- +# +# Split a QName into its constituent parts: +# the XML Namespace prefix and the Local-name +# +# Arguments: +# qname XML Qualified Name (see XML Namespaces [6]) +# +# Results: +# Returns prefix and local-name as a Tcl list. +# Error condition returned if the prefix or local-name +# are not valid NCNames (XML Name) + +proc xml::qnamesplit qname { + variable NCName + variable Name + + set prefix {} + set localname $qname + if {[regexp : $qname]} { + if {![regexp ^($NCName)?:($NCName)\$ $qname discard prefix localname]} { + return -code error "name \"$qname\" is not a valid QName" + } + } elseif {![regexp ^$Name\$ $qname]} { + return -code error "name \"$qname\" is not a valid Name" + } + + return [list $prefix $localname] +} + +### +### General utility procedures +### + +# xml::noop -- +# +# A do-nothing proc + +proc xml::noop args {} + +### Following procedures are based on html_library + +# xml::zapWhite -- +# +# Convert multiple white space into a single space. +# +# Arguments: +# data plain text +# +# Results: +# As above + +proc xml::zapWhite data { + regsub -all "\[ \t\r\n\]+" $data { } data + return $data +} + diff --git a/lib/tclxml3.1/xml__tcl.tcl b/lib/tclxml3.1/xml__tcl.tcl new file mode 100644 index 0000000..ef9948b --- /dev/null +++ b/lib/tclxml3.1/xml__tcl.tcl @@ -0,0 +1,270 @@ +# xml__tcl.tcl -- +# +# This file provides a Tcl implementation of the parser +# class support found in ../tclxml.c. It is only used +# when the C implementation is not installed (for some reason). +# +# Copyright (c) 2000-2004 Zveno Pty Ltd +# http://www.zveno.com/ +# +# See the file "LICENSE" in this distribution for information on usage and +# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# $Id: xml__tcl.tcl,v 1.15 2004/08/14 07:41:11 balls Exp $ + +package provide xml::tcl 3.1 + +namespace eval xml { + namespace export configure parser parserclass + + # Parser implementation classes + variable classes + array set classes {} + + # Default parser class + variable default {} + + # Counter for generating unique names + variable counter 0 +} + +# xml::configure -- +# +# Configure the xml package +# +# Arguments: +# None +# +# Results: +# None (not yet implemented) + +proc xml::configure args {} + +# xml::parserclass -- +# +# Implements the xml::parserclass command for managing +# parser implementations. +# +# Arguments: +# method subcommand +# args method arguments +# +# Results: +# Depends on method + +proc xml::parserclass {method args} { + variable classes + variable default + + switch -- $method { + + create { + if {[llength $args] < 1} { + return -code error "wrong number of arguments, should be xml::parserclass create name ?args?" + } + + set name [lindex $args 0] + if {[llength [lrange $args 1 end]] % 2} { + return -code error "missing value for option \"[lindex $args end]\"" + } + array set classes [list $name [list \ + -createcommand [namespace current]::noop \ + -createentityparsercommand [namespace current]::noop \ + -parsecommand [namespace current]::noop \ + -configurecommand [namespace current]::noop \ + -getcommand [namespace current]::noop \ + -deletecommand [namespace current]::noop \ + ]] + # BUG: we're not checking that the arguments are kosher + set classes($name) [lrange $args 1 end] + set default $name + } + + destroy { + if {[llength $args] < 1} { + return -code error "wrong number of arguments, should be xml::parserclass destroy name" + } + + if {[info exists classes([lindex $args 0])]} { + unset classes([lindex $args 0]) + } else { + return -code error "no such parser class \"[lindex $args 0]\"" + } + } + + info { + if {[llength $args] < 1} { + return -code error "wrong number of arguments, should be xml::parserclass info method" + } + + switch -- [lindex $args 0] { + names { + return [array names classes] + } + default { + return $default + } + } + } + + default { + return -code error "unknown method \"$method\"" + } + } + + return {} +} + +# xml::parser -- +# +# Create a parser object instance +# +# Arguments: +# args optional name, configuration options +# +# Results: +# Returns object name. Parser instance created. + +proc xml::parser args { + variable classes + variable default + + if {[llength $args] < 1} { + # Create unique name, no options + set parserName [FindUniqueName] + } else { + if {[string index [lindex $args 0] 0] == "-"} { + # Create unique name, have options + set parserName [FindUniqueName] + } else { + # Given name, optional options + set parserName [lindex $args 0] + set args [lrange $args 1 end] + } + } + + array set options [list \ + -parser $default + ] + array set options $args + + if {![info exists classes($options(-parser))]} { + return -code error "no such parser class \"$options(-parser)\"" + } + + # Now create the parser instance command and data structure + # The command must be created in the caller's namespace + uplevel 1 [list proc $parserName {method args} "eval [namespace current]::ParserCmd [list $parserName] \[list \$method\] \$args"] + upvar #0 [namespace current]::$parserName data + array set data [list class $options(-parser)] + + array set classinfo $classes($options(-parser)) + if {[string compare $classinfo(-createcommand) ""]} { + eval $classinfo(-createcommand) [list $parserName] + } + if {[string compare $classinfo(-configurecommand) ""] && \ + [llength $args]} { + eval $classinfo(-configurecommand) [list $parserName] $args + } + + return $parserName +} + +# xml::FindUniqueName -- +# +# Generate unique object name +# +# Arguments: +# None +# +# Results: +# Returns string. + +proc xml::FindUniqueName {} { + variable counter + return xmlparser[incr counter] +} + +# xml::ParserCmd -- +# +# Implements parser object command +# +# Arguments: +# name object reference +# method subcommand +# args method arguments +# +# Results: +# Depends on method + +proc xml::ParserCmd {name method args} { + variable classes + upvar #0 [namespace current]::$name data + + array set classinfo $classes($data(class)) + + switch -- $method { + + configure { + # BUG: We're not checking for legal options + array set data $args + eval $classinfo(-configurecommand) [list $name] $args + return {} + } + + cget { + return $data([lindex $args 0]) + } + + entityparser { + set new [FindUniqueName] + + upvar #0 [namespace current]::$name parent + upvar #0 [namespace current]::$new data + array set data [array get parent] + + uplevel 1 [list proc $new {method args} "eval [namespace current]::ParserCmd [list $new] \[list \$method\] \$args"] + + return [eval $classinfo(-createentityparsercommand) [list $name $new] $args] + } + + free { + eval $classinfo(-deletecommand) [list $name] + unset data + uplevel 1 [list rename $name {}] + } + + get { + eval $classinfo(-getcommand) [list $name] $args + } + + parse { + if {[llength $args] < 1} { + return -code error "wrong number of arguments, should be $name parse xml ?options?" + } + eval $classinfo(-parsecommand) [list $name] $args + } + + reset { + eval $classinfo(-resetcommand) [list $name] + } + + default { + return -code error "unknown method" + } + } + + return {} +} + +# xml::noop -- +# +# Do nothing utility proc +# +# Arguments: +# args whatever +# +# Results: +# Nothing happens + +proc xml::noop args {} diff --git a/lib/tclxml3.1/xmldep.tcl b/lib/tclxml3.1/xmldep.tcl new file mode 100644 index 0000000..7f1c404 --- /dev/null +++ b/lib/tclxml3.1/xmldep.tcl @@ -0,0 +1,179 @@ +# xmldep.tcl -- +# +# Find the dependencies in an XML document. +# Supports external entities and XSL include/import. +# +# TODO: +# XInclude +# +# Copyright (c) 2001-2003 Zveno Pty Ltd +# http://www.zveno.com/ +# +# See the file "LICENSE" in this distribution for information on usage and +# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# $Id: xmldep.tcl,v 1.3 2003/12/09 04:43:15 balls Exp $ + +package require xml + +package provide xml::dep 1.0 + +namespace eval xml::dep { + namespace export depend + + variable extEntities + array set extEntities {} + + variable XSLTNS http://www.w3.org/1999/XSL/Transform +} + +# xml::dep::depend -- +# +# Find the resources which an XML document +# depends on. The document is parsed +# sequentially, rather than using DOM, for efficiency. +# +# TODO: +# Asynchronous parsing. +# +# Arguments: +# xml XML document entity +# args configuration options +# +# Results: +# Returns list of resource (system) identifiers + +proc xml::dep::depend {xml args} { + variable resources + variable entities + + set resources {} + catch {unset entities} + array set entities {} + + set p [xml::parser \ + -elementstartcommand [namespace code ElStart] \ + -doctypecommand [namespace code DocTypeDecl] \ + -entitydeclcommand [namespace code EntityDecl] \ + -entityreferencecommand [namespace code EntityReference] \ + -validate 1 \ + ] + if {[llength $args]} { + eval [list $p] configure $args + } + $p parse $xml + + return $resources +} + +# xml::dep::ElStart -- +# +# Process start element +# +# Arguments: +# name tag name +# atlist attribute list +# args options +# +# Results: +# May add to resources list + +proc xml::dep::ElStart {name atlist args} { + variable XSLTNS + variable resources + + array set opts { + -namespace {} + } + array set opts $args + + switch -- $opts(-namespace) \ + $XSLTNS { + switch $name { + import - + include { + array set attr { + href {} + } + array set attr $atlist + + if {[string length $attr(href)]} { + if {[lsearch $resources $attr(href)] < 0} { + lappend resources $attr(href) + } + } + + } + } + } +} + +# xml::dep::DocTypeDecl -- +# +# Process Document Type Declaration +# +# Arguments: +# name Document element +# pubid Public identifier +# sysid System identifier +# dtd Internal DTD Subset +# +# Results: +# Resource added to list + +proc xml::dep::DocTypeDecl {name pubid sysid dtd} { + variable resources + + puts stderr [list DocTypeDecl $name $pubid $sysid dtd] + + if {[string length $sysid] && \ + [lsearch $resources $sysid] < 0} { + lappend resources $sysid + } + + return {} +} + +# xml::dep::EntityDecl -- +# +# Process entity declaration, looking for external entity +# +# Arguments: +# name entity name +# sysid system identifier +# pubid public identifier or repl. text +# +# Results: +# Store external entity info for later reference + +proc xml::dep::EntityDecl {name sysid pubid} { + variable extEntities + + puts stderr [list EntityDecl $name $sysid $pubid] + + set extEntities($name) $sysid +} + +# xml::dep::EntityReference -- +# +# Process entity reference +# +# Arguments: +# name entity name +# +# Results: +# May add to resources list + +proc xml::dep::EntityReference name { + variable extEntities + variable resources + + puts stderr [list EntityReference $name] + + if {[info exists extEntities($name)] && \ + [lsearch $resources $extEntities($name)] < 0} { + lappend resources $extEntities($name) + } + +} + diff --git a/lib/tclxml3.1/xpath.tcl b/lib/tclxml3.1/xpath.tcl new file mode 100644 index 0000000..7d248aa --- /dev/null +++ b/lib/tclxml3.1/xpath.tcl @@ -0,0 +1,362 @@ +# xpath.tcl -- +# +# Provides an XPath parser for Tcl, +# plus various support procedures +# +# Copyright (c) 2000-2003 Zveno Pty Ltd +# +# See the file "LICENSE" in this distribution for information on usage and +# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# $Id: xpath.tcl,v 1.8 2003/12/09 04:43:15 balls Exp $ + +package provide xpath 1.0 + +# We need the XML package for definition of Names +package require xml + +namespace eval xpath { + namespace export split join createnode + + variable axes { + ancestor + ancestor-or-self + attribute + child + descendant + descendant-or-self + following + following-sibling + namespace + parent + preceding + preceding-sibling + self + } + + variable nodeTypes { + comment + text + processing-instruction + node + } + + # NB. QName has parens for prefix + + variable nodetestExpr ^(${::xml::QName})${::xml::allWsp}(\\(${::xml::allWsp}(("|')(.*?)\\5)?${::xml::allWsp}\\))?${::xml::allWsp}(.*) + + variable nodetestExpr2 ((($::xml::QName)${::xml::allWsp}(\\(${::xml::allWsp}(("|')(.*?)\\7)?${::xml::allWsp}\\))?)|${::xml::allWsp}(\\*))${::xml::allWsp}(.*) +} + +# xpath::split -- +# +# Parse an XPath location path +# +# Arguments: +# locpath location path +# +# Results: +# A Tcl list representing the location path. +# The list has the form: {{axis node-test {predicate predicate ...}} ...} +# Where each list item is a location step. + +proc xpath::split locpath { + set leftover {} + + set result [InnerSplit $locpath leftover] + + if {[string length [string trim $leftover]]} { + return -code error "unexpected text \"$leftover\"" + } + + return $result +} + +proc xpath::InnerSplit {locpath leftoverVar} { + upvar $leftoverVar leftover + + variable axes + variable nodetestExpr + variable nodetestExpr2 + + # First determine whether we have an absolute location path + if {[regexp {^/(.*)} $locpath discard locpath]} { + set path {{}} + } else { + set path {} + } + + while {[string length [string trimleft $locpath]]} { + if {[regexp {^\.\.(.*)} $locpath discard locpath]} { + # .. abbreviation + set axis parent + set nodetest * + } elseif {[regexp {^/(.*)} $locpath discard locpath]} { + # // abbreviation + set axis descendant-or-self + if {[regexp ^$nodetestExpr2 [string trimleft $locpath] discard discard discard nodetest discard typetest discard discard literal wildcard locpath]} { + set nodetest [ResolveWildcard $nodetest $typetest $wildcard $literal] + } else { + set leftover $locpath + return $path + } + } elseif {[regexp ^\\.${::xml::allWsp}(.*) $locpath discard locpath]} { + # . abbreviation + set axis self + set nodetest * + } elseif {[regexp ^@($::xml::QName)${::xml::allWsp}=${::xml::allWsp}"(\[^"\])"(.*) $locpath discard attrName discard attrValue locpath]} { + # @ abbreviation + set axis attribute + set nodetest $attrName + } elseif {[regexp ^@($::xml::QName)${::xml::allWsp}=${::xml::allWsp}'(\[^'\])'(.*) $locpath discard attrName discard attrValue locpath]} { + # @ abbreviation + set axis attribute + set nodetest $attrName + } elseif {[regexp ^@($::xml::QName)(.*) $locpath discard attrName discard2 locpath]} { + # @ abbreviation + set axis attribute + set nodetest $attrName + } elseif {[regexp ^((${::xml::QName})${::xml::allWsp}::${::xml::allWsp})?\\*(.*) $locpath discard discard axis discard locpath]} { + # wildcard specified + set nodetest * + if {![string length $axis]} { + set axis child + } + } elseif {[regexp ^((${::xml::QName})${::xml::allWsp}::${::xml::allWsp})?$nodetestExpr2 $locpath discard discard axis discard discard discard nodetest discard typetest discard discard literal wildcard locpath]} { + # nodetest, with or without axis + if {![string length $axis]} { + set axis child + } + set nodetest [ResolveWildcard $nodetest $typetest $wildcard $literal] + } else { + set leftover $locpath + return $path + } + + # ParsePredicates + set predicates {} + set locpath [string trimleft $locpath] + while {[regexp {^\[(.*)} $locpath discard locpath]} { + if {[regexp {^([0-9]+)(\].*)} [string trim $locpath] discard posn locpath]} { + set predicate [list = {function position {}} [list number $posn]] + } else { + set leftover2 {} + set predicate [ParseExpr $locpath leftover2] + set locpath $leftover2 + unset leftover2 + } + + if {[regexp {^\](.*)} [string trimleft $locpath] discard locpath]} { + lappend predicates $predicate + } else { + return -code error "unexpected text in predicate \"$locpath\"" + } + } + + set axis [string trim $axis] + set nodetest [string trim $nodetest] + + # This step completed + if {[lsearch $axes $axis] < 0} { + return -code error "invalid axis \"$axis\"" + } + lappend path [list $axis $nodetest $predicates] + + # Move to next step + + if {[string length $locpath] && ![regexp ^/(.*) $locpath discard locpath]} { + set leftover $locpath + return $path + } + + } + + return $path +} + +# xpath::ParseExpr -- +# +# Parse one expression in a predicate +# +# Arguments: +# locpath location path to parse +# leftoverVar Name of variable in which to store remaining path +# +# Results: +# Returns parsed expression as a Tcl list + +proc xpath::ParseExpr {locpath leftoverVar} { + upvar $leftoverVar leftover + variable nodeTypes + + set expr {} + set mode expr + set stack {} + + while {[string index [string trimleft $locpath] 0] != "\]"} { + set locpath [string trimleft $locpath] + switch $mode { + expr { + # We're looking for a term + if {[regexp ^-(.*) $locpath discard locpath]} { + # UnaryExpr + lappend stack "-" + } elseif {[regexp ^\\\$({$::xml::QName})(.*) $locpath discard varname discard locpath]} { + # VariableReference + lappend stack [list varRef $varname] + set mode term + } elseif {[regexp {^\((.*)} $locpath discard locpath]} { + # Start grouping + set leftover2 {} + lappend stack [list group [ParseExpr $locpath leftover2]] + set locpath $leftover2 + unset leftover2 + + if {[regexp {^\)(.*)} [string trimleft $locpath] discard locpath]} { + set mode term + } else { + return -code error "unexpected text \"$locpath\", expected \")\"" + } + + } elseif {[regexp {^"([^"]*)"(.*)} $locpath discard literal locpath]} { + # Literal (" delimited) + lappend stack [list literal $literal] + set mode term + } elseif {[regexp {^'([^']*)'(.*)} $locpath discard literal locpath]} { + # Literal (' delimited) + lappend stack [list literal $literal] + set mode term + } elseif {[regexp {^([0-9]+(\.[0-9]+)?)(.*)} $locpath discard number discard locpath]} { + # Number + lappend stack [list number $number] + set mode term + } elseif {[regexp {^(\.[0-9]+)(.*)} $locpath discard number locpath]} { + # Number + lappend stack [list number $number] + set mode term + } elseif {[regexp ^(${::xml::QName})\\(${::xml::allWsp}(.*) $locpath discard functionName discard locpath]} { + # Function call start or abbreviated node-type test + + if {[lsearch $nodeTypes $functionName] >= 0} { + # Looking like a node-type test + if {[regexp ^\\)${::xml::allWsp}(.*) $locpath discard locpath]} { + lappend stack [list path [list child [list $functionName ()] {}]] + set mode term + } else { + return -code error "invalid node-type test \"$functionName\"" + } + } else { + if {[regexp ^\\)${::xml::allWsp}(.*) $locpath discard locpath]} { + set parameters {} + } else { + set leftover2 {} + set parameters [ParseExpr $locpath leftover2] + set locpath $leftover2 + unset leftover2 + while {[regexp {^,(.*)} $locpath discard locpath]} { + set leftover2 {} + lappend parameters [ParseExpr $locpath leftover2] + set locpath $leftover2 + unset leftover2 + } + + if {![regexp ^\\)${::xml::allWsp}(.*) [string trimleft $locpath] discard locpath]} { + return -code error "unexpected text \"locpath\" - expected \")\"" + } + } + + lappend stack [list function $functionName $parameters] + set mode term + } + + } else { + # LocationPath + set leftover2 {} + lappend stack [list path [InnerSplit $locpath leftover2]] + set locpath $leftover2 + unset leftover2 + set mode term + } + } + term { + # We're looking for an expression operator + if {[regexp ^-(.*) $locpath discard locpath]} { + # UnaryExpr + set stack [linsert $stack 0 expr "-"] + set mode expr + } elseif {[regexp ^(and|or|\\=|!\\=|<|>|<\\=|>\\=|\\||\\+|\\-|\\*|div|mod)(.*) $locpath discard exprtype locpath]} { + # AndExpr, OrExpr, EqualityExpr, RelationalExpr or UnionExpr + set stack [linsert $stack 0 $exprtype] + set mode expr + } else { + return -code error "unexpected text \"$locpath\", expecting operator" + } + } + default { + # Should never be here! + return -code error "internal error" + } + } + } + + set leftover $locpath + return $stack +} + +# xpath::ResolveWildcard -- + +proc xpath::ResolveWildcard {nodetest typetest wildcard literal} { + variable nodeTypes + + switch -glob -- [string length $nodetest],[string length $typetest],[string length $wildcard],[string length $literal] { + 0,0,0,* { + return -code error "bad location step (nothing parsed)" + } + 0,0,* { + # Name wildcard specified + return * + } + *,0,0,* { + # Element type test - nothing to do + return $nodetest + } + *,0,*,* { + # Internal error? + return -code error "bad location step (found both nodetest and wildcard)" + } + *,*,0,0 { + # Node type test + if {[lsearch $nodeTypes $nodetest] < 0} { + return -code error "unknown node type \"$typetest\"" + } + return [list $nodetest $typetest] + } + *,*,0,* { + # Node type test + if {[lsearch $nodeTypes $nodetest] < 0} { + return -code error "unknown node type \"$typetest\"" + } + return [list $nodetest $literal] + } + default { + # Internal error? + return -code error "bad location step" + } + } +} + +# xpath::join -- +# +# Reconstitute an XPath location path from a +# Tcl list representation. +# +# Arguments: +# spath split path +# +# Results: +# Returns an Xpath location path + +proc xpath::join spath { + return -code error "not yet implemented" +} + diff --git a/lib/tdom/pkgIndex.tcl b/lib/tdom/pkgIndex.tcl new file mode 100644 index 0000000..5e92b90 --- /dev/null +++ b/lib/tdom/pkgIndex.tcl @@ -0,0 +1,6 @@ +# Only relevant on Windows-x86 +if {[string compare $::tcl_platform(platform) "windows"]} { return } +if {[string compare $::tcl_platform(machine) "intel"]} { return } +package ifneeded tdom 0.8.3 \ + "load [list [file join $dir win32-ix86 tdom083.dll]];\ + source [list [file join $dir tdom.tcl]]" diff --git a/lib/tdom/tdom.tcl b/lib/tdom/tdom.tcl new file mode 100644 index 0000000..569a11e --- /dev/null +++ b/lib/tdom/tdom.tcl @@ -0,0 +1,911 @@ +#---------------------------------------------------------------------------- +# Copyright (c) 1999 Jochen Loewer (loewerj@hotmail.com) +#---------------------------------------------------------------------------- +# +# $Id: tdom.tcl,v 1.19 2005/01/11 15:57:19 rolf Exp $ +# +# +# The higher level functions of tDOM written in plain Tcl. +# +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.1 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +# License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is tDOM. +# +# The Initial Developer of the Original Code is Jochen Loewer +# Portions created by Jochen Loewer are Copyright (C) 1998, 1999 +# Jochen Loewer. All Rights Reserved. +# +# Contributor(s): +# Rolf Ade (rolf@pointsman.de): 'fake' nodelists/live childNodes +# +# written by Jochen Loewer +# April, 1999 +# +#---------------------------------------------------------------------------- + +package require tdom + +#---------------------------------------------------------------------------- +# setup namespaces for additional Tcl level methods, etc. +# +#---------------------------------------------------------------------------- +namespace eval ::dom { + namespace eval domDoc { + } + namespace eval domNode { + } + namespace eval DOMImplementation { + } + namespace eval xpathFunc { + } + namespace eval xpathFuncHelper { + } +} + +namespace eval ::tDOM { + variable extRefHandlerDebug 0 + variable useForeignDTD "" + + namespace export xmlOpenFile xmlReadFile extRefHandler baseURL +} + +#---------------------------------------------------------------------------- +# hasFeature (DOMImplementation method) +# +# +# @in url the URL, where to get the XML document +# +# @return document object +# @exception XML parse errors, ... +# +#---------------------------------------------------------------------------- +proc ::dom::DOMImplementation::hasFeature { dom feature {version ""} } { + + switch $feature { + xml - + XML { + if {($version == "") || ($version == "1.0")} { + return 1 + } + } + } + return 0 + +} + +#---------------------------------------------------------------------------- +# load (DOMImplementation method) +# +# requests a XML document via http using the given URL and +# builds up a DOM tree in memory returning the document object +# +# +# @in url the URL, where to get the XML document +# +# @return document object +# @exception XML parse errors, ... +# +#---------------------------------------------------------------------------- +proc ::dom::DOMImplementation::load { dom url } { + + error "Sorry, load method not implemented yet!" + +} + +#---------------------------------------------------------------------------- +# isa (docDoc method, for [incr tcl] compatibility) +# +# +# @in className +# +# @return 1 iff inherits from the given class +# +#---------------------------------------------------------------------------- +proc ::dom::domDoc::isa { doc className } { + + if {$className == "domDoc"} { + return 1 + } + return 0 +} + +#---------------------------------------------------------------------------- +# info (domDoc method, for [incr tcl] compatibility) +# +# +# @in subcommand +# @in args +# +#---------------------------------------------------------------------------- +proc ::dom::domDoc::info { doc subcommand args } { + + switch $subcommand { + class { + return "domDoc" + } + inherit { + return "" + } + heritage { + return "domDoc {}" + } + default { + error "domDoc::info subcommand $subcommand not yet implemented!" + } + } +} + +#---------------------------------------------------------------------------- +# importNode (domDoc method) +# +# Document Object Model (Core) Level 2 method +# +# +# @in subcommand +# @in args +# +#---------------------------------------------------------------------------- +proc ::dom::domDoc::importNode { doc importedNode deep } { + + if {$deep || ($deep == "-deep")} { + set node [$importedNode cloneNode -deep] + } else { + set node [$importedNode cloneNode] + } + return $node +} + +#---------------------------------------------------------------------------- +# isa (domNode method, for [incr tcl] compatibility) +# +# +# @in className +# +# @return 1 iff inherits from the given class +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::isa { doc className } { + + if {$className == "domNode"} { + return 1 + } + return 0 +} + +#---------------------------------------------------------------------------- +# info (domNode method, for [incr tcl] compatibility) +# +# +# @in subcommand +# @in args +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::info { doc subcommand args } { + + switch $subcommand { + class { + return "domNode" + } + inherit { + return "" + } + heritage { + return "domNode {}" + } + default { + error "domNode::info subcommand $subcommand not yet implemented!" + } + } +} + +#---------------------------------------------------------------------------- +# isWithin (domNode method) +# +# tests, whether a node object is nested below another tag +# +# +# @in tagName the nodeName of an elment node +# +# @return 1 iff node is nested below a element with nodeName tagName +# 0 otherwise +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::isWithin { node tagName } { + + while {[$node parentNode] != ""} { + set node [$node parentNode] + if {[$node nodeName] == $tagName} { + return 1 + } + } + return 0 +} + +#---------------------------------------------------------------------------- +# tagName (domNode method) +# +# same a nodeName for element interface +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::tagName { node } { + + if {[$node nodeType] == "ELEMENT_NODE"} { + return [$node nodeName] + } + return -code error "NOT_SUPPORTED_ERR not an element!" +} + +#---------------------------------------------------------------------------- +# simpleTranslate (domNode method) +# +# applies simple translation rules similar to Cost's simple +# translations to a node +# +# +# @in output_var +# @in trans_specs +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::simpleTranslate { node output_var trans_specs } { + + upvar $output_var output + + if {[$node nodeType] == "TEXT_NODE"} { + append output [cgiQuote [$node nodeValue]] + return + } + set found 0 + + foreach {match action} $trans_specs { + + if {[catch { + if {!$found && ([$node selectNode self::$match] != "") } { + set found 1 + } + } err]} { + if {![string match "NodeSet expected for parent axis!" $err]} { + error $err + } + } + if {$found && ($action != "-")} { + set stop 0 + foreach {type value} $action { + switch $type { + prefix { append output [subst $value] } + tag { append output <$value> } + start { append output [eval $value] } + stop { set stop 1 } + } + } + if {!$stop} { + foreach child [$node childNodes] { + simpleTranslate $child output $trans_specs + } + } + foreach {type value} $action { + switch $type { + suffix { append output [subst $value] } + end { append output [eval $value] } + tag { append output } + } + } + return + } + } + foreach child [$node childNodes] { + simpleTranslate $child output $trans_specs + } +} + +#---------------------------------------------------------------------------- +# a DOM conformant 'live' childNodes +# +# @return a 'nodelist' object (it is just the normal node) +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::childNodesLive { node } { + + return $node +} + +#---------------------------------------------------------------------------- +# item method on a 'nodelist' object +# +# @return a 'nodelist' object (it is just a normal +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::item { nodeListNode index } { + + return [lindex [$nodeListNode childNodes] $index] +} + +#---------------------------------------------------------------------------- +# length method on a 'nodelist' object +# +# @return a 'nodelist' object (it is just a normal +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::length { nodeListNode } { + + return [llength [$nodeListNode childNodes]] +} + +#---------------------------------------------------------------------------- +# appendData on a 'CharacterData' object +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::appendData { node arg } { + + set type [$node nodeType] + if {($type != "TEXT_NODE") && ($type != "CDATA_SECTION_NODE") && + ($type != "COMMENT_NODE") + } { + return -code error "NOT_SUPPORTED_ERR: node is not a cdata node" + } + set oldValue [$node nodeValue] + $node nodeValue [append oldValue $arg] +} + +#---------------------------------------------------------------------------- +# deleteData on a 'CharacterData' object +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::deleteData { node offset count } { + + set type [$node nodeType] + if {($type != "TEXT_NODE") && ($type != "CDATA_SECTION_NODE") && + ($type != "COMMENT_NODE") + } { + return -code error "NOT_SUPPORTED_ERR: node is not a cdata node" + } + incr offset -1 + set before [string range [$node nodeValue] 0 $offset] + incr offset + incr offset $count + set after [string range [$node nodeValue] $offset end] + $node nodeValue [append before $after] +} + +#---------------------------------------------------------------------------- +# insertData on a 'CharacterData' object +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::insertData { node offset arg } { + + set type [$node nodeType] + if {($type != "TEXT_NODE") && ($type != "CDATA_SECTION_NODE") && + ($type != "COMMENT_NODE") + } { + return -code error "NOT_SUPPORTED_ERR: node is not a cdata node" + } + incr offset -1 + set before [string range [$node nodeValue] 0 $offset] + incr offset + set after [string range [$node nodeValue] $offset end] + $node nodeValue [append before $arg $after] +} + +#---------------------------------------------------------------------------- +# replaceData on a 'CharacterData' object +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::replaceData { node offset count arg } { + + set type [$node nodeType] + if {($type != "TEXT_NODE") && ($type != "CDATA_SECTION_NODE") && + ($type != "COMMENT_NODE") + } { + return -code error "NOT_SUPPORTED_ERR: node is not a cdata node" + } + incr offset -1 + set before [string range [$node nodeValue] 0 $offset] + incr offset + incr offset $count + set after [string range [$node nodeValue] $offset end] + $node nodeValue [append before $arg $after] +} + +#---------------------------------------------------------------------------- +# substringData on a 'CharacterData' object +# +# @return part of the node value (text) +# +#---------------------------------------------------------------------------- +proc ::dom::domNode::substringData { node offset count } { + + set type [$node nodeType] + if {($type != "TEXT_NODE") && ($type != "CDATA_SECTION_NODE") && + ($type != "COMMENT_NODE") + } { + return -code error "NOT_SUPPORTED_ERR: node is not a cdata node" + } + set endOffset [expr $offset + $count - 1] + return [string range [$node nodeValue] $offset $endOffset] +} + +#---------------------------------------------------------------------------- +# coerce2number +# +#---------------------------------------------------------------------------- +proc ::dom::xpathFuncHelper::coerce2number { type value } { + switch $type { + empty { return 0 } + number - + string { return $value } + attrvalues { return [lindex $value 0] } + nodes { return [[lindex $value 0] selectNodes number()] } + attrnodes { return [lindex $value 1] } + } +} + +#---------------------------------------------------------------------------- +# coerce2string +# +#---------------------------------------------------------------------------- +proc ::dom::xpathFuncHelper::coerce2string { type value } { + switch $type { + empty { return "" } + number - + string { return $value } + attrvalues { return [lindex $value 0] } + nodes { return [[lindex $value 0] selectNodes string()] } + attrnodes { return [lindex $value 1] } + } +} + +#---------------------------------------------------------------------------- +# function-available +# +#---------------------------------------------------------------------------- +proc ::dom::xpathFunc::function-available { ctxNode pos + nodeListType nodeList args} { + + if {[llength $args] != 2} { + error "function-available(): wrong # of args!" + } + foreach { arg1Typ arg1Value } $args break + set str [::dom::xpathFuncHelper::coerce2string $arg1Typ $arg1Value ] + switch $str { + boolean - + ceiling - + concat - + contains - + count - + current - + document - + element-available - + false - + floor - + format-number - + generate-id - + id - + key - + last - + lang - + local-name - + name - + namespace-uri - + normalize-space - + not - + number - + position - + round - + starts-with - + string - + string-length - + substring - + substring-after - + substring-before - + sum - + translate - + true - + unparsed-entity-uri { + return [list bool true] + } + default { + set TclXpathFuncs [info procs ::dom::xpathFunc::*] + if {[lsearch -exact $TclXpathFuncs $str] != -1} { + return [list bool true] + } else { + return [list bool false] + } + } + } +} + +#---------------------------------------------------------------------------- +# element-available +# +# This is not strictly correct. The XSLT namespace may be bound +# to another prefix (and the prefix 'xsl' may be bound to another +# namespace). Since the expression context isn't available at the +# moment at tcl coded XPath functions, this couldn't be done better +# than this "works in the 'normal' cases" version. +#---------------------------------------------------------------------------- +proc ::dom::xpathFunc::element-available { ctxNode pos + nodeListType nodeList args} { + + if {[llength $args] != 2} { + error "element-available(): wrong # of args!" + } + foreach { arg1Typ arg1Value } $args break + set str [::dom::xpathFuncHelper::coerce2string $arg1Typ $arg1Value ] + switch $str { + xsl:stylesheet - + xsl:transform - + xsl:include - + xsl:import - + xsl:strip-space - + xsl:preserve-space - + xsl:template - + xsl:apply-templates - + xsl:apply-imports - + xsl:call-template - + xsl:element - + xsl:attribute - + xsl:attribute-set - + xsl:text - + xsl:processing-instruction - + xsl:comment - + xsl:copy - + xsl:value-of - + xsl:number - + xsl:for-each - + xsl:if - + xsl:choose - + xsl:when - + xsl:otherwise - + xsl:sort - + xsl:variable - + xsl:param - + xsl:copy-of - + xsl:with-param - + xsl:key - + xsl:message - + xsl:decimal-format - + xsl:namespace-alias - + xsl:output - + xsl:fallback { + return [list bool true] + } + default { + return [list bool false] + } + } +} + +#---------------------------------------------------------------------------- +# system-property +# +# This is not strictly correct. The XSLT namespace may be bound +# to another prefix (and the prefix 'xsl' may be bound to another +# namespace). Since the expression context isn't available at the +# moment at tcl coded XPath functions, this couldn't be done better +# than this "works in the 'normal' cases" version. +#---------------------------------------------------------------------------- +proc ::dom::xpathFunc::system-property { ctxNode pos + nodeListType nodeList args } { + + if {[llength $args] != 2} { + error "system-property(): wrong # of args!" + } + foreach { arg1Typ arg1Value } $args break + set str [::dom::xpathFuncHelper::coerce2string $arg1Typ $arg1Value ] + switch $str { + xsl:version { + return [list number 1.0] + } + xsl:vendor { + return [list string "Jochen Loewer (loewerj@hotmail.com), Rolf Ade (rolf@pointsman.de) et. al."] + } + xsl:vendor-url { + return [list string "http://www.tdom.org"] + } + default { + return [list string ""] + } + } +} + +#---------------------------------------------------------------------------- +# IANAEncoding2TclEncoding +# +#---------------------------------------------------------------------------- + +# As of version 8.3.4 tcl supports +# cp860 cp861 cp862 cp863 tis-620 cp864 cp865 cp866 gb12345 cp949 +# cp950 cp869 dingbats ksc5601 macCentEuro cp874 macUkraine jis0201 +# gb2312 euc-cn euc-jp iso8859-10 macThai jis0208 iso2022-jp +# macIceland iso2022 iso8859-13 iso8859-14 jis0212 iso8859-15 cp737 +# iso8859-16 big5 euc-kr macRomania macTurkish gb1988 iso2022-kr +# macGreek ascii cp437 macRoman iso8859-1 iso8859-2 iso8859-3 ebcdic +# macCroatian koi8-r iso8859-4 iso8859-5 cp1250 macCyrillic iso8859-6 +# cp1251 koi8-u macDingbats iso8859-7 cp1252 iso8859-8 cp1253 +# iso8859-9 cp1254 cp1255 cp850 cp1256 cp932 identity cp1257 cp852 +# macJapan cp1258 shiftjis utf-8 cp855 cp936 symbol cp775 unicode +# cp857 +# +# Just add more mappings (and mail them to the tDOM mailing list, please). + +proc tDOM::IANAEncoding2TclEncoding {IANAName} { + + # First the most widespread encodings with there + # preferred MIME name, to speed lookup in this + # usual cases. Later the official names and the + # aliases. + # + # For "official names for character sets that may be + # used in the Internet" see + # http://www.iana.org/assignments/character-sets + # (that's the source for the encoding names below) + # + # Matching is case-insensitive + + switch [string tolower $IANAName] { + "us-ascii" {return ascii} + "utf-8" {return utf-8} + "utf-16" {return unicode; # not sure about this} + "iso-8859-1" {return iso8859-1} + "iso-8859-2" {return iso8859-2} + "iso-8859-3" {return iso8859-3} + "iso-8859-4" {return iso8859-4} + "iso-8859-5" {return iso8859-5} + "iso-8859-6" {return iso8859-6} + "iso-8859-7" {return iso8859-7} + "iso-8859-8" {return iso8859-8} + "iso-8859-9" {return iso8859-9} + "iso-8859-10" {return iso8859-10} + "iso-8859-13" {return iso8859-13} + "iso-8859-14" {return iso8859-14} + "iso-8859-15" {return iso8859-15} + "iso-8859-16" {return iso8859-16} + "iso-2022-kr" {return iso2022-kr} + "euc-kr" {return euc-kr} + "iso-2022-jp" {return iso2022-jp} + "koi8-r" {return koi8-r} + "shift_jis" {return shiftjis} + "euc-jp" {return euc-jp} + "gb2312" {return gb2312} + "big5" {return big5} + "cp866" {return cp866} + "cp1250" {return cp1250} + "cp1253" {return cp1253} + "cp1254" {return cp1254} + "cp1255" {return cp1255} + "cp1256" {return cp1256} + "cp1257" {return cp1257} + + "windows-1251" - + "cp1251" {return cp1251} + + "windows-1252" - + "cp1252" {return cp1252} + + "iso_8859-1:1987" - + "iso-ir-100" - + "iso_8859-1" - + "latin1" - + "l1" - + "ibm819" - + "cp819" - + "csisolatin1" {return iso8859-1} + + "iso_8859-2:1987" - + "iso-ir-101" - + "iso_8859-2" - + "iso-8859-2" - + "latin2" - + "l2" - + "csisolatin2" {return iso8859-2} + + "iso_8859-5:1988" - + "iso-ir-144" - + "iso_8859-5" - + "iso-8859-5" - + "cyrillic" - + "csisolatincyrillic" {return iso8859-5} + + "ms_kanji" - + "csshiftjis" {return shiftjis} + + "csiso2022kr" {return iso2022-kr} + + "ibm866" - + "csibm866" {return cp866} + + default { + # There are much more encoding names out there + # It's only laziness, that let me stop here. + error "Unrecognized encoding name '$IANAName'" + } + } +} + +#---------------------------------------------------------------------------- +# xmlOpenFile +# +#---------------------------------------------------------------------------- +proc tDOM::xmlOpenFile {filename {encodingString {}}} { + + set fd [open $filename] + + if {$encodingString != {}} { + upvar $encodingString encString + } + + # The autodetection of the encoding follows + # XML Recomendation, Appendix F + + fconfigure $fd -encoding binary + if {![binary scan [read $fd 4] "H8" firstBytes]} { + # very short (< 4 Bytes) file + seek $fd 0 start + set encString UTF-8 + return $fd + } + + # First check for BOM + switch [string range $firstBytes 0 3] { + "feff" - + "fffe" { + # feff: UTF-16, big-endian BOM + # ffef: UTF-16, little-endian BOM + seek $fd 0 start + set encString UTF-16 + fconfigure $fd -encoding identity + return $fd + } + } + + # If the entity has a XML Declaration, the first four characters + # must be "" $head] + if {$closeIndex == -1} { + error "Weird XML data or not XML data at all" + } + + seek $fd 0 start + set xmlDeclaration [read $fd [expr {$closeIndex + 5}]] + # extract the encoding information + set pattern {^[^>]+encoding=[\x20\x9\xd\xa]*["']([^ "']+)['"]} + # emacs: " + if {![regexp $pattern $head - encStr]} { + # Probably something like . + # Without encoding declaration this must be UTF-8 + set encoding utf-8 + set encString UTF-8 + } else { + set encoding [IANAEncoding2TclEncoding $encStr] + set encString $encStr + } + } + "0000003c" - + "0000003c" - + "3c000000" - + "00003c00" { + # UCS-4 + error "UCS-4 not supported" + } + "003c003f" - + "3c003f00" { + # UTF-16, big-endian, no BOM + # UTF-16, little-endian, no BOM + seek $fd 0 start + set encoding identity + set encString UTF-16 + } + "4c6fa794" { + # EBCDIC in some flavor + error "EBCDIC not supported" + } + default { + # UTF-8 without an encoding declaration + seek $fd 0 start + set encoding identity + set encString "UTF-8" + } + } + fconfigure $fd -encoding $encoding + return $fd +} + +#---------------------------------------------------------------------------- +# xmlReadFile +# +#---------------------------------------------------------------------------- +proc tDOM::xmlReadFile {filename {encodingString {}}} { + + if {$encodingString != {}} { + upvar $encodingString encString + } + + set fd [xmlOpenFile $filename encString] + set data [read $fd [file size $filename]] + close $fd + return $data +} + +#---------------------------------------------------------------------------- +# extRefHandler +# +# A very simple external entity resolver, included for convenience. +# Depends on the tcllib package uri and resolves only file URLs. +# +#---------------------------------------------------------------------------- + +if {![catch {package require uri}]} { + proc tDOM::extRefHandler {base systemId publicId} { + variable extRefHandlerDebug + variable useForeignDTD + + if {$extRefHandlerDebug} { + puts stderr "tDOM::extRefHandler called with:" + puts stderr "\tbase: '$base'" + puts stderr "\tsystemId: '$systemId'" + puts stderr "\tpublicId: '$publicId'" + } + if {$systemId == ""} { + if {$useForeignDTD != ""} { + set systemId $useForeignDTD + } else { + error "::tDOM::useForeignDTD does\ + not point to the foreign DTD" + } + } + set absolutURI [uri::resolve $base $systemId] + array set uriData [uri::split $absolutURI] + switch $uriData(scheme) { + file { + return [list string $absolutURI [xmlReadFile $uriData(path)]] + } + default { + error "can only handle file URI's" + } + } + } +} + +#---------------------------------------------------------------------------- +# baseURL +# +# A simple convenience proc which returns an absolute URL for a given +# filename. +# +#---------------------------------------------------------------------------- + +proc tDOM::baseURL {path} { + switch [file pathtype $path] { + "relative" { + return "file://[pwd]/$path" + } + default { + return "file://$path" + } + } +} + +# EOF diff --git a/lib/tdom/win32-ix86/tdom083.dll b/lib/tdom/win32-ix86/tdom083.dll new file mode 100644 index 0000000000000000000000000000000000000000..a7e6fb493ba7b72cd1d1e2729dad8ba39efc3ab1 GIT binary patch literal 611328 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�(MvVlEwh2eXC)wQyFDR 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%0D3o)WXu zndhx&GQpaRt(;EeH5;G5fQwvEp}!?uc_fxM-Usq`2(0%gIg8UEzGu;_DNd?W+DRj? zT7_&~I@Y3*qhTdHxA|Xc=L@PnC$M(L6rSfyV!1i6u@<_^iDFlItHIIMGeb6Aklztw z!=A}sF`;5mCBITRQ5o)mB((cs^`IkKHw&)IOsP;zp zfrA_pvEZEIk%7#sDq$UR!CEjDS?uP-fNkaW*lDpcOaZT@ZgRgmD^P!dckR zDQ2o!n$=<|npNO7rkm%lo{`J*KljOQg)9Bf&+uPAv48!1{`J#15j{->8GO~H#UoP? zOSr*Os#F@zYDKAvR6|OKyV{71oJ4JzMJ`cPg8$PK`0Ls{Q>fNEe?5%ET0QI0gFCPm zhvkTr8m=L-8XSCsz6o@68{C4u*xEzkDjyr*EFBH`<~6`3+Qa`C#!Q(FQ-1?1tL!6l zdKT7X4H&H+9Ijz_O|yxWSCO6E$Lu(Zk0~ChZ*^7M%^_c@^31eRwN8gXXDW z9bak2y$NI8p4Dn{;T2BCcbOTkmn2q8S_uLsR!Z80rz%!S64l+Jw)+GY`7AzNDom7$ zphn_3HDS#pF-yc85i_K>9LY?LY#{#p>yvFRwU*A6_a2P$K>Si6y+SJqog_4pSTRJb z7qXtQ7c1NBfW^BH3w)TTaGKSw3^UqO&kfU21f`mDGlQ3EC_`$b*9sOlFr{lmz=hNO*G%rLVm-{Qp zwXa1kH^F_{=I?@!cL2QgC|>j#c`pRvS`O1-HPyQtf-S+;a7~PTR2d#dzE9&H2|iM3*hp2PHSm-gf$V8Wd~*=) z_C%^Z-~bLp_H$wVPll~DleLWIG7px5d#?)lI-6kY2ph1Md2tB+e*zuF?0}D!DwdEy zM+prj^pns|LN^J`)Ee|Ola(;U3QuBvr!mZ&Da;(P(%3>gisj6l)yy2R!q`@3&Msz- zSQSC6iExIQlfukNW9A4A*NB;;F>{1<5w=CFIVM&d%O$5V8AhzoXJU09vF?~yb!@%7 z0Zq0wTzhAqd>~}zotDqY5hX<_t)wYch%V|YjT8xvo(Ds(E&RN$u=D!i`3(bongB0P zu=2zzFT!$()#1d-A;M;DBV)1`Wcv_a&I#hqvtZn*tdCVutwMZKUu{B+Vo@c~8fJDT zaeHt0xI^HyjDZ_51z%{kI)@C)VmzXiWTV%@jot*(wv9Cw_NoWS)g4t&ke5G;N1hs5 zMZtV&M81G=o{}2!Apev38kv(CuDO-$R}2FO7VjLr?EbZIE3t3pX#AIj>M zP+E6{^13gS*u$aBo(`op5(v319ZK%MwsGfs972|-htfPJl;_2vM6V2GdTl7xn?kuh zceNKG-6vx5y*f4Ip_c|HPhP2HFV;lpmcyZTIUVYkh$}Qq=}^a13AIf9P|rxArt$K1 zO;`8d?L`6aFS02$Qc+Bge5NN~4y1-W#PdC*f7d>5VAT^L+1v8hl}a#APvDQA0Yw%v zo*GJd#Zb=ELrHIftcy7uIT7pI;ZV*_BWd(4!aBVCFD#XqaRn<+&VhNf@Gol+ZUAf8 zLgcay#9 +# +# $Header: /cvsroot/tls/tls/tls.tcl,v 1.10 2008/03/19 02:34:21 patthoyts Exp $ +# +namespace eval tls { + variable logcmd tclLog + variable debug 0 + + # Default flags passed to tls::import + variable defaults {} + + # Maps UID to Server Socket + variable srvmap + variable srvuid 0 + + # Over-ride this if you are using a different socket command + variable socketCmd + if {![info exists socketCmd]} { + set socketCmd [info command ::socket] + } +} + +proc tls::initlib {dir dll} { + # Package index cd's into the package directory for loading. + # Irrelevant to unixoids, but for Windows this enables the OS to find + # the dependent DLL's in the CWD, where they may be. + set cwd [pwd] + catch {cd $dir} + set res [catch {uplevel #0 [list load [file join [pwd] $dll]]} err] + catch {cd $cwd} + if {$res} { + namespace eval [namespace parent] {namespace delete tls} + return -code $res $err + } + rename tls::initlib {} +} + +# +# Backwards compatibility, also used to set the default +# context options +# +proc tls::init {args} { + variable defaults + + set defaults $args +} +# +# Helper function - behaves exactly as the native socket command. +# +proc tls::socket {args} { + variable socketCmd + variable defaults + set idx [lsearch $args -server] + if {$idx != -1} { + set server 1 + set callback [lindex $args [expr {$idx+1}]] + set args [lreplace $args $idx [expr {$idx+1}]] + + set usage "wrong # args: should be \"tls::socket -server command ?options? port\"" + set options "-cadir, -cafile, -certfile, -cipher, -command, -keyfile, -myaddr, -password, -request, -require, -ssl2, -ssl3, or -tls1" + } else { + set server 0 + + set usage "wrong # args: should be \"tls::socket ?options? host port\"" + set options "-async, -cadir, -cafile, -certfile, -cipher, -command, -keyfile, -myaddr, -myport, -password, -request, -require, -ssl2, -ssl3, or -tls1" + } + set argc [llength $args] + set sopts {} + set iopts [concat [list -server $server] $defaults] ;# Import options + + for {set idx 0} {$idx < $argc} {incr idx} { + set arg [lindex $args $idx] + switch -glob -- $server,$arg { + 0,-async {lappend sopts $arg} + 0,-myport - + *,-myaddr {lappend sopts $arg [lindex $args [incr idx]]} + *,-cadir - + *,-cafile - + *,-certfile - + *,-cipher - + *,-command - + *,-keyfile - + *,-password - + *,-request - + *,-require - + *,-ssl2 - + *,-ssl3 - + *,-tls1 {lappend iopts $arg [lindex $args [incr idx]]} + -* {return -code error "bad option \"$arg\": must be one of $options"} + default {break} + } + } + if {$server} { + if {($idx + 1) != $argc} { + return -code error $usage + } + set uid [incr ::tls::srvuid] + + set port [lindex $args [expr {$argc-1}]] + lappend sopts $port + #set sopts [linsert $sopts 0 -server $callback] + set sopts [linsert $sopts 0 -server [list tls::_accept $iopts $callback]] + #set sopts [linsert $sopts 0 -server [list tls::_accept $uid $callback]] + } else { + if {($idx + 2) != $argc} { + return -code error $usage + } + set host [lindex $args [expr {$argc-2}]] + set port [lindex $args [expr {$argc-1}]] + lappend sopts $host $port + } + # + # Create TCP/IP socket + # + set chan [eval $socketCmd $sopts] + if {!$server && [catch { + # + # Push SSL layer onto socket + # + eval [list tls::import] $chan $iopts + } err]} { + set info ${::errorInfo} + catch {close $chan} + return -code error -errorinfo $info $err + } + return $chan +} + +# tls::_accept -- +# +# This is the actual accept that TLS sockets use, which then calls +# the callback registered by tls::socket. +# +# Arguments: +# iopts tls::import opts +# callback server callback to invoke +# chan socket channel to accept/deny +# ipaddr calling IP address +# port calling port +# +# Results: +# Returns an error if the callback throws one. +# +proc tls::_accept { iopts callback chan ipaddr port } { + log 2 [list tls::_accept $iopts $callback $chan $ipaddr $port] + + set chan [eval [list tls::import $chan] $iopts] + + lappend callback $chan $ipaddr $port + if {[catch { + uplevel #0 $callback + } err]} { + log 1 "tls::_accept error: ${::errorInfo}" + close $chan + error $err $::errorInfo $::errorCode + } else { + log 2 "tls::_accept - called \"$callback\" succeeded" + } +} +# +# Sample callback for hooking: - +# +# error +# verify +# info +# +proc tls::callback {option args} { + variable debug + + #log 2 [concat $option $args] + + switch -- $option { + "error" { + foreach {chan msg} $args break + + log 0 "TLS/$chan: error: $msg" + } + "verify" { + # poor man's lassign + foreach {chan depth cert rc err} $args break + + array set c $cert + + if {$rc != "1"} { + log 1 "TLS/$chan: verify/$depth: Bad Cert: $err (rc = $rc)" + } else { + log 2 "TLS/$chan: verify/$depth: $c(subject)" + } + if {$debug > 0} { + return 1; # FORCE OK + } else { + return $rc + } + } + "info" { + # poor man's lassign + foreach {chan major minor state msg} $args break + + if {$msg != ""} { + append state ": $msg" + } + # For tracing + upvar #0 tls::$chan cb + set cb($major) $minor + + log 2 "TLS/$chan: $major/$minor: $state" + } + default { + return -code error "bad option \"$option\":\ + must be one of error, info, or verify" + } + } +} + +proc tls::xhandshake {chan} { + upvar #0 tls::$chan cb + + if {[info exists cb(handshake)] && \ + $cb(handshake) == "done"} { + return 1 + } + while {1} { + vwait tls::${chan}(handshake) + if {![info exists cb(handshake)]} { + return 0 + } + if {$cb(handshake) == "done"} { + return 1 + } + } +} + +proc tls::password {} { + log 0 "TLS/Password: did you forget to set your passwd!" + # Return the worlds best kept secret password. + return "secret" +} + +proc tls::log {level msg} { + variable debug + variable logcmd + + if {$level > $debug || $logcmd == ""} { + return + } + set cmd $logcmd + lappend cmd $msg + uplevel #0 $cmd +} diff --git a/lib/tls/win32-ix86/tls16.dll b/lib/tls/win32-ix86/tls16.dll new file mode 100644 index 0000000000000000000000000000000000000000..6f4268dfdfc00cea5a0d62341fb407ff6d85d55f GIT binary patch literal 715776 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 z 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

GSJ*9r;!y(jDU0{uinMflnJS*V{=_4D)kDKx6T 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 z

we4z`5?YAclQIDhPxO3qI<>_ItVS+C&4ECi=7ycB;6PLzo6tWZcY2nl^5m&6x9 zR5eyl!`rRrF=|CM1qACp7)@J0|A;jD7}nU1bMXfYTbudQwa=|afU|f4Z2&!Rg^V6N zD@B{Fg-#(2lMU8njXl|DP1f3zP1fWPd$M_;vJ_Wv0+$Su%YT9a9yZvMBSk?(Ta!)p z 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 l&FlW!4 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 ?arg? +# +# clear ?pattern? +# Stops the specified widgets (defaults to all) from showing tooltips +# +# delay ?millisecs? +# Query or set the delay. The delay is in milliseconds and must +# be at least 50. Returns the delay. +# +# disable OR off +# Disables all tooltips. +# +# enable OR on +# Enables tooltips for defined widgets. +# +# ?-index index? ?-item id? ?message? +# If -index is specified, then is assumed to be a menu +# and the index represents what index into the menu (either the +# numerical index or the label) to associate the tooltip message with. +# Tooltips do not appear for disabled menu items. +# If message is {}, then the tooltip for that widget is removed. +# The widget must exist prior to calling tooltip. The current +# tooltip message for is returned, if any. +# +# RETURNS: varies (see methods above) +# +# NAMESPACE & STATE +# The namespace tooltip is used. +# Control toplevel name via ::tooltip::wname. +# +# EXAMPLE USAGE: +# tooltip .button "A Button" +# tooltip .menu -index "Load" "Loads a file" +# +#------------------------------------------------------------------------ + +namespace eval ::tooltip { + namespace export -clear tooltip + variable tooltip + variable G + + array set G { + enabled 1 + fade 1 + FADESTEP 0.2 + FADEID {} + DELAY 500 + AFTERID {} + LAST -1 + TOPLEVEL .__tooltip__ + } + if {[tk windowingsystem] eq "x11"} { + set G(fade) 0 ; # don't fade by default on X11 + } + # The extra ::hide call in is necessary to catch moving to + # child widgets where the event won't be generated + bind Tooltip [namespace code { + #tooltip::hide + variable tooltip + variable G + set G(LAST) -1 + if {$G(enabled) && [info exists tooltip(%W)]} { + set G(AFTERID) \ + [after $G(DELAY) [namespace code [list show %W $tooltip(%W) cursor]]] + } + }] + + bind Menu <> [namespace code { menuMotion %W }] + bind Tooltip [namespace code [list hide 1]] ; # fade ok + bind Tooltip [namespace code hide] + bind Tooltip [namespace code hide] +} + +proc ::tooltip::tooltip {w args} { + variable tooltip + variable G + switch -- $w { + clear { + if {[llength $args]==0} { set args .* } + clear $args + } + delay { + if {[llength $args]} { + if {![string is integer -strict $args] || $args<50} { + return -code error "tooltip delay must be an\ + integer greater than 50 (delay is in millisecs)" + } + return [set G(DELAY) $args] + } else { + return $G(DELAY) + } + } + fade { + if {[llength $args]} { + set G(fade) [string is true -strict [lindex $args 0]] + } + return $G(fade) + } + off - disable { + set G(enabled) 0 + hide + } + on - enable { + set G(enabled) 1 + } + default { + set i $w + if {[llength $args]} { + set i [uplevel 1 [namespace code "register [list $w] $args"]] + } + set b $G(TOPLEVEL) + if {![winfo exists $b]} { + toplevel $b -class Tooltip + if {[tk windowingsystem] eq "aqua"} { + ::tk::unsupported::MacWindowStyle style $b help none + } else { + wm overrideredirect $b 1 + } + catch {wm attributes $b -topmost 1} + # avoid the blink issue with 1 to <1 alpha on Windows + catch {wm attributes $b -alpha 0.99} + wm positionfrom $b program + wm withdraw $b + label $b.label -highlightthickness 0 -relief solid -bd 1 \ + -background lightyellow -fg black + pack $b.label -ipadx 1 + } + if {[info exists tooltip($i)]} { return $tooltip($i) } + } + } +} + +proc ::tooltip::register {w args} { + variable tooltip + set key [lindex $args 0] + while {[string match -* $key]} { + switch -- $key { + -index { + if {[catch {$w entrycget 1 -label}]} { + return -code error "widget \"$w\" does not seem to be a\ + menu, which is required for the -index switch" + } + set index [lindex $args 1] + set args [lreplace $args 0 1] + } + -item { + set namedItem [lindex $args 1] + if {[catch {$w find withtag $namedItem} item]} { + return -code error "widget \"$w\" is not a canvas, or item\ + \"$namedItem\" does not exist in the canvas" + } + if {[llength $item] > 1} { + return -code error "item \"$namedItem\" specifies more\ + than one item on the canvas" + } + set args [lreplace $args 0 1] + } + -tag { + set tag [lindex $args 1] + set r [catch {lsearch -exact [$w tag names] $tag} ndx] + if {$r || $ndx == -1} { + return -code error "widget \"$w\" is not a text widget or\ + \"$tag\" is not a text tag" + } + set args [lreplace $args 0 1] + } + default { + return -code error "unknown option \"$key\":\ + should be -index or -item" + } + } + set key [lindex $args 0] + } + if {[llength $args] != 1} { + return -code error "wrong # args: should be \"tooltip widget\ + ?-index index? ?-item item? ?-tag tag? message\"" + } + if {$key eq ""} { + clear $w + } else { + if {![winfo exists $w]} { + return -code error "bad window path name \"$w\"" + } + if {[info exists index]} { + set tooltip($w,$index) $key + return $w,$index + } elseif {[info exists item]} { + set tooltip($w,$item) $key + enableCanvas $w $item + return $w,$item + } elseif {[info exists tag]} { + set tooltip($w,t_$tag) $key + enableTag $w $tag + return $w,$tag + } else { + set tooltip($w) $key + bindtags $w [linsert [bindtags $w] end "Tooltip"] + return $w + } + } +} + +proc ::tooltip::clear {{pattern .*}} { + variable tooltip + # cache the current widget at pointer + set ptrw [winfo containing [winfo pointerx .] [winfo pointery .]] + foreach w [array names tooltip $pattern] { + unset tooltip($w) + if {[winfo exists $w]} { + set tags [bindtags $w] + if {[set i [lsearch -exact $tags "Tooltip"]] != -1} { + bindtags $w [lreplace $tags $i $i] + } + ## We don't remove TooltipMenu because there + ## might be other indices that use it + + # Withdraw the tooltip if we clear the current contained item + if {$ptrw eq $w} { hide } + } + } +} + +proc ::tooltip::show {w msg {i {}}} { + if {![winfo exists $w]} { return } + + # Use string match to allow that the help will be shown when + # the pointer is in any child of the desired widget + if {([winfo class $w] ne "Menu") + && ![string match $w* [eval [list winfo containing] \ + [winfo pointerxy $w]]]} { + return + } + + variable G + + after cancel $G(FADEID) + set b $G(TOPLEVEL) + # Use late-binding msgcat (lazy translation) to support programs + # that allow on-the-fly l10n changes + $b.label configure -text [::msgcat::mc $msg] -justify left + update idletasks + set screenw [winfo screenwidth $w] + set screenh [winfo screenheight $w] + set reqw [winfo reqwidth $b] + set reqh [winfo reqheight $b] + # When adjusting for being on the screen boundary, check that we are + # near the "edge" already, as Tk handles multiple monitors oddly + if {$i eq "cursor"} { + set y [expr {[winfo pointery $w]+20}] + if {($y < $screenh) && ($y+$reqh) > $screenh} { + set y [expr {[winfo pointery $w]-$reqh-5}] + } + } elseif {$i ne ""} { + set y [expr {[winfo rooty $w]+[winfo vrooty $w]+[$w yposition $i]+25}] + if {($y < $screenh) && ($y+$reqh) > $screenh} { + # show above if we would be offscreen + set y [expr {[winfo rooty $w]+[$w yposition $i]-$reqh-5}] + } + } else { + set y [expr {[winfo rooty $w]+[winfo vrooty $w]+[winfo height $w]+5}] + if {($y < $screenh) && ($y+$reqh) > $screenh} { + # show above if we would be offscreen + set y [expr {[winfo rooty $w]-$reqh-5}] + } + } + if {$i eq "cursor"} { + set x [winfo pointerx $w] + } else { + set x [expr {[winfo rootx $w]+[winfo vrootx $w]+ + ([winfo width $w]-$reqw)/2}] + } + # only readjust when we would appear right on the screen edge + if {$x<0 && ($x+$reqw)>0} { + set x 0 + } elseif {($x < $screenw) && ($x+$reqw) > $screenw} { + set x [expr {$screenw-$reqw}] + } + if {[tk windowingsystem] eq "aqua"} { + set focus [focus] + } + # avoid the blink issue with 1 to <1 alpha on Windows, watch half-fading + catch {wm attributes $b -alpha 0.99} + wm geometry $b +$x+$y + wm deiconify $b + raise $b + if {[tk windowingsystem] eq "aqua" && $focus ne ""} { + # Aqua's help window steals focus on display + after idle [list focus -force $focus] + } +} + +proc ::tooltip::menuMotion {w} { + variable G + + if {$G(enabled)} { + variable tooltip + + # Menu events come from a funny path, map to the real path. + set m [string map {"#" "."} [winfo name $w]] + set cur [$w index active] + + # The next two lines (all uses of LAST) are necessary until the + # <> event is properly coded for Unix/(Windows)? + if {$cur == $G(LAST)} return + set G(LAST) $cur + # a little inlining - this is :hide + after cancel $G(AFTERID) + catch {wm withdraw $G(TOPLEVEL)} + if {[info exists tooltip($m,$cur)] || \ + (![catch {$w entrycget $cur -label} cur] && \ + [info exists tooltip($m,$cur)])} { + set G(AFTERID) [after $G(DELAY) \ + [namespace code [list show $w $tooltip($m,$cur) cursor]]] + } + } +} + +proc ::tooltip::hide {{fadeOk 0}} { + variable G + + after cancel $G(AFTERID) + after cancel $G(FADEID) + if {$fadeOk && $G(fade)} { + fade $G(TOPLEVEL) $G(FADESTEP) + } else { + catch {wm withdraw $G(TOPLEVEL)} + } +} + +proc ::tooltip::fade {w step} { + if {[catch {wm attributes $w -alpha} alpha] || $alpha <= 0.0} { + catch { wm withdraw $w } + catch { wm attributes $w -alpha 0.99 } + } else { + variable G + wm attributes $w -alpha [expr {$alpha-$step}] + set G(FADEID) [after 50 [namespace code [list fade $w $step]]] + } +} + +proc ::tooltip::wname {{w {}}} { + variable G + if {[llength [info level 0]] > 1} { + # $w specified + if {$w ne $G(TOPLEVEL)} { + hide + destroy $G(TOPLEVEL) + set G(TOPLEVEL) $w + } + } + return $G(TOPLEVEL) +} + +proc ::tooltip::itemTip {w args} { + variable tooltip + variable G + + set G(LAST) -1 + set item [$w find withtag current] + if {$G(enabled) && [info exists tooltip($w,$item)]} { + set G(AFTERID) [after $G(DELAY) \ + [namespace code [list show $w $tooltip($w,$item) cursor]]] + } +} + +proc ::tooltip::enableCanvas {w args} { + $w bind all +[namespace code [list itemTip $w]] + $w bind all +[namespace code [list hide 1]] ; # fade ok + $w bind all +[namespace code hide] + $w bind all +[namespace code hide] +} + +proc ::tooltip::tagTip {w tag} { + variable tooltip + variable G + set G(LAST) -1 + if {$G(enabled) && [info exists tooltip($w,t_$tag)]} { + set G(AFTERID) [after $G(DELAY) \ + [namespace code [list show $w $tooltip($w,t_$tag) cursor]]] + } +} + +proc ::tooltip::enableTag {w tag} { + $w tag bind $tag +[namespace code [list tagTip $w $tag]] + $w tag bind $tag +[namespace code [list hide 1]] ; # fade ok + $w tag bind $tag +[namespace code hide] + $w tag bind $tag +[namespace code hide] +} diff --git a/lib/udp1.0.9/pkgIndex.tcl b/lib/udp1.0.9/pkgIndex.tcl new file mode 100644 index 0000000..e15a0ad --- /dev/null +++ b/lib/udp1.0.9/pkgIndex.tcl @@ -0,0 +1,4 @@ +# We only have a win32-intel binary at the moment +if {[string compare $::tcl_platform(platform) "windows"]} { return } +if {[string compare $::tcl_platform(machine) "intel"]} { return } +package ifneeded udp 1.0.9 [list load [file join $dir win32-ix86 udp109.dll]] diff --git a/lib/udp1.0.9/win32-ix86/udp109.dll b/lib/udp1.0.9/win32-ix86/udp109.dll new file mode 100644 index 0000000000000000000000000000000000000000..8cdf1769aa04dd6182b8ed939c3e17ed08754cd4 GIT binary patch literal 15360 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 literal 0 HcmV?d00001 diff --git a/lib/uri/pkgIndex.tcl b/lib/uri/pkgIndex.tcl new file mode 100644 index 0000000..c17f1a1 --- /dev/null +++ b/lib/uri/pkgIndex.tcl @@ -0,0 +1,6 @@ +if {![package vsatisfies [package provide Tcl] 8.2]} { + # FRINK: nocheck + return +} +package ifneeded uri 1.2.1 [list source [file join $dir uri.tcl]] +package ifneeded uri::urn 1.0.2 [list source [file join $dir urn-scheme.tcl]] diff --git a/lib/uri/uri.tcl b/lib/uri/uri.tcl new file mode 100644 index 0000000..9e0344c --- /dev/null +++ b/lib/uri/uri.tcl @@ -0,0 +1,1034 @@ +# uri.tcl -- +# +# URI parsing and fetch +# +# Copyright (c) 2000 Zveno Pty Ltd +# Copyright (c) 2006 Pierre DAVID +# Copyright (c) 2006 Andreas Kupries +# Steve Ball, http://www.zveno.com/ +# Derived from urls.tcl by Andreas Kupries +# +# TODO: +# Handle www-url-encoding details +# +# CVS: $Id: uri.tcl,v 1.35 2007/01/11 19:35:23 andreas_kupries Exp $ + +package require Tcl 8.2 + +namespace eval ::uri { + + namespace export split join + namespace export resolve isrelative + namespace export geturl + namespace export canonicalize + namespace export register + + variable file:counter 0 + + # extend these variable in the coming namespaces + variable schemes {} + variable schemePattern "" + variable url "" + variable url2part + array set url2part {} + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # basic regular expressions used in URL syntax. + + namespace eval basic { + variable loAlpha {[a-z]} + variable hiAlpha {[A-Z]} + variable digit {[0-9]} + variable alpha {[a-zA-Z]} + variable safe {[$_.+-]} + variable extra {[!*'(,)]} + # danger in next pattern, order important for [] + variable national {[][|\}\{\^~`]} + variable punctuation {[<>#%"]} ;#" fake emacs hilit + variable reserved {[;/?:@&=]} + variable hex {[0-9A-Fa-f]} + variable alphaDigit {[A-Za-z0-9]} + variable alphaDigitMinus {[A-Za-z0-9-]} + + # next is + variable unsafe {[][<>"#%\{\}|\\^~`]} ;#" emacs hilit + variable escape "%${hex}${hex}" + + # unreserved = alpha | digit | safe | extra + # xchar = unreserved | reserved | escape + + variable unreserved {[a-zA-Z0-9$_.+!*'(,)-]} + variable uChar "(${unreserved}|${escape})" + variable xCharN {[a-zA-Z0-9$_.+!*'(,);/?:@&=-]} + variable xChar "(${xCharN}|${escape})" + variable digits "${digit}+" + + variable toplabel \ + "(${alpha}${alphaDigitMinus}*${alphaDigit}|${alpha})" + variable domainlabel \ + "(${alphaDigit}${alphaDigitMinus}*${alphaDigit}|${alphaDigit})" + + variable hostname \ + "((${domainlabel}\\.)*${toplabel})" + variable hostnumber \ + "(${digits}\\.${digits}\\.${digits}\\.${digits})" + + variable host "(${hostname}|${hostnumber})" + + variable port $digits + variable hostOrPort "${host}(:${port})?" + + variable usrCharN {[a-zA-Z0-9$_.+!*'(,);?&=-]} + variable usrChar "(${usrCharN}|${escape})" + variable user "${usrChar}*" + variable password $user + variable login "(${user}(:${password})?@)?${hostOrPort}" + } ;# basic {} +} + + +# ::uri::register -- +# +# Register a scheme (and aliases) in the package. The command +# creates a namespace below "::uri" with the same name as the +# scheme and executes the script declaring the pattern variables +# for this scheme in the new namespace. At last it updates the +# uri variables keeping track of overall scheme information. +# +# The script has to declare at least the variable "schemepart", +# the pattern for an url of the registered scheme after the +# scheme declaration. Not declaring this variable is an error. +# +# Arguments: +# schemeList Name of the scheme to register, plus aliases +# script Script declaring the scheme patterns +# +# Results: +# None. + +proc ::uri::register {schemeList script} { + variable schemes + variable schemePattern + variable url + variable url2part + + # Check scheme and its aliases for existence. + foreach scheme $schemeList { + if {[lsearch -exact $schemes $scheme] >= 0} { + return -code error \ + "trying to register scheme (\"$scheme\") which is already known" + } + } + + # Get the main scheme + set scheme [lindex $schemeList 0] + + if {[catch {namespace eval $scheme $script} msg]} { + catch {namespace delete $scheme} + return -code error \ + "error while evaluating scheme script: $msg" + } + + if {![info exists ${scheme}::schemepart]} { + namespace delete $scheme + return -code error \ + "Variable \"schemepart\" is missing." + } + + # Now we can extend the variables which keep track of the registered schemes. + + eval [linsert $schemeList 0 lappend schemes] + set schemePattern "([::join $schemes |]):" + + foreach s $schemeList { + # FRINK: nocheck + set url2part($s) "${s}:[set ${scheme}::schemepart]" + # FRINK: nocheck + append url "(${s}:[set ${scheme}::schemepart])|" + } + set url [string trimright $url |] + return +} + +# ::uri::split -- +# +# Splits the given into its constituents. +# +# Arguments: +# url the URL to split +# +# Results: +# Tcl list containing constituents, suitable for 'array set'. + +proc ::uri::split {url {defaultscheme http}} { + + set url [string trim $url] + set scheme {} + + # RFC 1738: scheme = 1*[ lowalpha | digit | "+" | "-" | "." ] + regexp -- {^([a-z0-9+.-][a-z0-9+.-]*):} $url dummy scheme + + if {$scheme == {}} { + set scheme $defaultscheme + } + + # ease maintenance: dynamic dispatch, able to handle all schemes + # added in future! + + if {[::info procs Split[string totitle $scheme]] == {}} { + error "unknown scheme '$scheme' in '$url'" + } + + regsub -- "^${scheme}:" $url {} url + + set parts(scheme) $scheme + array set parts [Split[string totitle $scheme] $url] + + # should decode all encoded characters! + + return [array get parts] +} + +proc ::uri::SplitFtp {url} { + # @c Splits the given ftp- into its constituents. + # @a url: The url to split, without! scheme specification. + # @r List containing the constituents, suitable for 'array set'. + + # general syntax: + # //:@://...//;type= + # + # additional rules: + # + # : are optional, detectable by presence of @. + # is optional too. + # + # "//" [ [":" ] "@"] [":" ] "/" + # "/" ..."/" "/" [";type=" ] + + upvar \#0 [namespace current]::ftp::typepart ftptype + + array set parts {user {} pwd {} host {} port {} path {} type {}} + + # slash off possible type specification + + if {[regexp -indices -- "${ftptype}$" $url dummy ftype]} { + + set from [lindex $ftype 0] + set to [lindex $ftype 1] + + set parts(type) [string range $url $from $to] + + set from [lindex $dummy 0] + set url [string replace $url $from end] + } + + # Handle user, password, host and port + + if {[string match "//*" $url]} { + set url [string range $url 2 end] + + array set parts [GetUPHP url] + } + + set parts(path) [string trimleft $url /] + + return [array get parts] +} + +proc ::uri::JoinFtp args { + array set components { + user {} pwd {} host {} port {} + path {} type {} + } + array set components $args + + set userPwd {} + if {[string length $components(user)] || [string length $components(pwd)]} { + set userPwd $components(user)[expr {[string length $components(pwd)] ? ":$components(pwd)" : {}}]@ + } + + set port {} + if {[string length $components(port)]} { + set port :$components(port) + } + + set type {} + if {[string length $components(type)]} { + set type \;type=$components(type) + } + + return ftp://${userPwd}$components(host)${port}/[string trimleft $components(path) /]$type +} + +proc ::uri::SplitHttps {url} { + return [SplitHttp $url] +} + +proc ::uri::SplitHttp {url} { + # @c Splits the given http- into its constituents. + # @a url: The url to split, without! scheme specification. + # @r List containing the constituents, suitable for 'array set'. + + # general syntax: + # //:/? + # + # where and are as described in Section 3.1. If : + # is omitted, the port defaults to 80. No user name or password is + # allowed. is an HTTP selector, and is a query + # string. The is optional, as is the and its + # preceding "?". If neither nor is present, the "/" + # may also be omitted. + # + # Within the and components, "/", ";", "?" are + # reserved. The "/" character may be used within HTTP to designate a + # hierarchical structure. + # + # path == "/" ..."/" "/" ["#" ] + + upvar #0 [namespace current]::http::search search + upvar #0 [namespace current]::http::segment segment + + array set parts {host {} port {} path {} query {}} + + set searchPattern "\\?(${search})\$" + set fragmentPattern "#(${segment})\$" + + # slash off possible query. the 'search' regexp, while official, + # is not good enough. We have apparently lots of urls in the wild + # which contain unquoted urls with queries in a query. The RE + # finds the embedded query, not the actual one. Using string first + # now instead of a RE + + if {[set pos [string first ? $url]] >= 0} { + incr pos + set parts(query) [string range $url $pos end] + incr pos -1 + set url [string replace $url $pos end] + } + + # slash off possible fragment + + if {[regexp -indices -- $fragmentPattern $url match fragment]} { + set from [lindex $fragment 0] + set to [lindex $fragment 1] + + set parts(fragment) [string range $url $from $to] + + set url [string replace $url [lindex $match 0] end] + } + + if {[string match "//*" $url]} { + set url [string range $url 2 end] + + array set parts [GetUPHP url] + } + + set parts(path) [string trimleft $url /] + + return [array get parts] +} + +proc ::uri::JoinHttp {args} { + return [eval [linsert $args 0 ::uri::JoinHttpInner http 80]] +} + +proc ::uri::JoinHttps {args} { + return [eval [linsert $args 0 ::uri::JoinHttpInner https 443]] +} + +proc ::uri::JoinHttpInner {scheme defport args} { + array set components {host {} path {} query {}} + set components(port) $defport + array set components $args + + set port {} + if {[string length $components(port)] && $components(port) != $defport} { + set port :$components(port) + } + + set query {} + if {[string length $components(query)]} { + set query ?$components(query) + } + + regsub -- {^/} $components(path) {} components(path) + + if { [info exists components(fragment)] && $components(fragment) != "" } { + set components(fragment) "#$components(fragment)" + } else { + set components(fragment) "" + } + + return $scheme://$components(host)$port/$components(path)$components(fragment)$query +} + +proc ::uri::SplitFile {url} { + # @c Splits the given file- into its constituents. + # @a url: The url to split, without! scheme specification. + # @r List containing the constituents, suitable for 'array set'. + + upvar #0 [namespace current]::basic::hostname hostname + upvar #0 [namespace current]::basic::hostnumber hostnumber + + if {[string match "//*" $url]} { + set url [string range $url 2 end] + + set hostPattern "^($hostname|$hostnumber)" + switch -exact -- $::tcl_platform(platform) { + windows { + # Catch drive letter + append hostPattern :? + } + default { + # Proceed as usual + } + } + + if {[regexp -indices -- $hostPattern $url match host]} { + set fh [lindex $host 0] + set th [lindex $host 1] + + set parts(host) [string range $url $fh $th] + + set matchEnd [lindex $match 1] + incr matchEnd + + set url [string range $url $matchEnd end] + } + } + + set parts(path) $url + + return [array get parts] +} + +proc ::uri::JoinFile args { + array set components { + host {} port {} path {} + } + array set components $args + + switch -exact -- $::tcl_platform(platform) { + windows { + if {[string length $components(host)]} { + return file://$components(host):$components(path) + } else { + return file://$components(path) + } + } + default { + return file://$components(host)$components(path) + } + } +} + +proc ::uri::SplitMailto {url} { + # @c Splits the given mailto- into its constituents. + # @a url: The url to split, without! scheme specification. + # @r List containing the constituents, suitable for 'array set'. + + if {[string match "*@*" $url]} { + set url [::split $url @] + return [list user [lindex $url 0] host [lindex $url 1]] + } else { + return [list user $url] + } +} + +proc ::uri::JoinMailto args { + array set components { + user {} host {} + } + array set components $args + + return mailto:$components(user)@$components(host) +} + +proc ::uri::SplitNews {url} { + if { [string first @ $url] >= 0 } { + return [list message-id $url] + } else { + return [list newsgroup-name $url] + } +} + +proc ::uri::JoinNews args { + array set components { + message-id {} newsgroup-name {} + } + array set components $args + return news:$components(message-id)$components(newsgroup-name) +} + +proc ::uri::SplitLdaps {url} { + ::uri::SplitLdap $url +} + +proc ::uri::SplitLdap {url} { + # @c Splits the given Ldap- into its constituents. + # @a url: The url to split, without! scheme specification. + # @r List containing the constituents, suitable for 'array set'. + + # general syntax: + # //:/???? + # + # where and are as described in Section 5 of RFC 1738. + # No user name or password is allowed. + # If omitted, the port defaults to 389 for ldap, 636 for ldaps + # is the base DN for the search + # is a comma separated list of attributes description + # is either "base", "one" or "sub". + # is a RFC 2254 filter specification + # are documented in RFC 2255 + # + + array set parts {host {} port {} dn {} attrs {} scope {} filter {} extensions {}} + + # host port dn attrs scope filter extns + set re {//([^:?/]+)(?::([0-9]+))?(?:/([^?]+)(?:\?([^?]*)(?:\?(base|one|sub)?(?:\?([^?]*)(?:\?(.*))?)?)?)?)?} + + if {! [regexp $re $url match parts(host) parts(port) \ + parts(dn) parts(attrs) parts(scope) parts(filter) \ + parts(extensions)]} then { + return -code error "unable to match URL \"$url\"" + } + + set parts(attrs) [::split $parts(attrs) ","] + + return [array get parts] +} + +proc ::uri::JoinLdap {args} { + return [eval [linsert $args 0 ::uri::JoinLdapInner ldap 389]] +} + +proc ::uri::JoinLdaps {args} { + return [eval [linsert $args 0 ::uri::JoinLdapInner ldaps 636]] +} + +proc ::uri::JoinLdapInner {scheme defport args} { + array set components {host {} port {} dn {} attrs {} scope {} filter {} extensions {}} + set components(port) $defport + array set components $args + + set port {} + if {[string length $components(port)] && $components(port) != $defport} { + set port :$components(port) + } + + set url "$scheme://$components(host)$port" + + set components(attrs) [::join $components(attrs) ","] + + set s "" + foreach c {dn attrs scope filter extensions} { + if {[string equal $c "dn"]} then { + append s "/" + } else { + append s "?" + } + if {! [string equal $components($c) ""]} then { + append url "${s}$components($c)" + set s "" + } + } + + return $url +} + +proc ::uri::GetUPHP {urlvar} { + # @c Parse user, password host and port out of the url stored in + # @c variable . + # @d Side effect: The extracted information is removed from the given url. + # @r List containing the extracted information in a format suitable for + # @r 'array set'. + # @a urlvar: Name of the variable containing the url to parse. + + upvar \#0 [namespace current]::basic::user user + upvar \#0 [namespace current]::basic::password password + upvar \#0 [namespace current]::basic::hostname hostname + upvar \#0 [namespace current]::basic::hostnumber hostnumber + upvar \#0 [namespace current]::basic::port port + + upvar $urlvar url + + array set parts {user {} pwd {} host {} port {}} + + # syntax + # "//" [ [":" ] "@"] [":" ] "/" + # "//" already cut off by caller + + set upPattern "^(${user})(:(${password}))?@" + + if {[regexp -indices -- $upPattern $url match theUser c d thePassword]} { + set fu [lindex $theUser 0] + set tu [lindex $theUser 1] + + set fp [lindex $thePassword 0] + set tp [lindex $thePassword 1] + + set parts(user) [string range $url $fu $tu] + set parts(pwd) [string range $url $fp $tp] + + set matchEnd [lindex $match 1] + incr matchEnd + + set url [string range $url $matchEnd end] + } + + set hpPattern "^($hostname|$hostnumber)(:($port))?" + + if {[regexp -indices -- $hpPattern $url match theHost c d e f g h thePort]} { + set fh [lindex $theHost 0] + set th [lindex $theHost 1] + + set fp [lindex $thePort 0] + set tp [lindex $thePort 1] + + set parts(host) [string range $url $fh $th] + set parts(port) [string range $url $fp $tp] + + set matchEnd [lindex $match 1] + incr matchEnd + + set url [string range $url $matchEnd end] + } + + return [array get parts] +} + +proc ::uri::GetHostPort {urlvar} { + # @c Parse host and port out of the url stored in variable . + # @d Side effect: The extracted information is removed from the given url. + # @r List containing the extracted information in a format suitable for + # @r 'array set'. + # @a urlvar: Name of the variable containing the url to parse. + + upvar #0 [namespace current]::basic::hostname hostname + upvar #0 [namespace current]::basic::hostnumber hostnumber + upvar #0 [namespace current]::basic::port port + + upvar $urlvar url + + set pattern "^(${hostname}|${hostnumber})(:(${port}))?" + + if {[regexp -indices -- $pattern $url match host c d e f g h thePort]} { + set fromHost [lindex $host 0] + set toHost [lindex $host 1] + + set fromPort [lindex $thePort 0] + set toPort [lindex $thePort 1] + + set parts(host) [string range $url $fromHost $toHost] + set parts(port) [string range $url $fromPort $toPort] + + set matchEnd [lindex $match 1] + incr matchEnd + + set url [string range $url $matchEnd end] + } + + return [array get parts] +} + +# ::uri::resolve -- +# +# Resolve an arbitrary URL, given a base URL +# +# Arguments: +# base base URL (absolute) +# url arbitrary URL +# +# Results: +# Returns a URL + +proc ::uri::resolve {base url} { + if {[string length $url]} { + if {[isrelative $url]} { + + array set baseparts [split $base] + + switch -- $baseparts(scheme) { + http - + https - + ftp - + file { + array set relparts [split $url] + if { [string match /* $url] } { + catch { set baseparts(path) $relparts(path) } + } elseif { [string match */ $baseparts(path)] } { + set baseparts(path) "$baseparts(path)$relparts(path)" + } else { + if { [string length $relparts(path)] > 0 } { + set path [lreplace [::split $baseparts(path) /] end end] + set baseparts(path) "[::join $path /]/$relparts(path)" + } + } + catch { set baseparts(query) $relparts(query) } + catch { set baseparts(fragment) $relparts(fragment) } + return [eval [linsert [array get baseparts] 0 join]] + } + default { + return -code error "unable to resolve relative URL \"$url\"" + } + } + + } else { + return $url + } + } else { + return $base + } +} + +# ::uri::isrelative -- +# +# Determines whether a URL is absolute or relative +# +# Arguments: +# url URL to check +# +# Results: +# Returns 1 if the URL is relative, 0 otherwise + +proc ::uri::isrelative url { + return [expr {![regexp -- {^[a-z0-9+-.][a-z0-9+-.]*:} $url]}] +} + +# ::uri::geturl -- +# +# Fetch the data from an arbitrary URL. +# +# This package provides a handler for the file: +# scheme, since this conflicts with the file command. +# +# Arguments: +# url address of data resource +# args configuration options +# +# Results: +# Depends on scheme + +proc ::uri::geturl {url args} { + array set urlparts [split $url] + + switch -- $urlparts(scheme) { + file { + return [eval [linsert $args 0 file_geturl $url]] + } + default { + # Load a geturl package for the scheme first and only if + # that fails the scheme package itself. This prevents + # cyclic dependencies between packages. + if {[catch {package require $urlparts(scheme)::geturl}]} { + package require $urlparts(scheme) + } + return [eval [linsert $args 0 $urlparts(scheme)::geturl $url]] + } + } +} + +# ::uri::file_geturl -- +# +# geturl implementation for file: scheme +# +# TODO: +# This is an initial, basic implementation. +# Eventually want to support all options for geturl. +# +# Arguments: +# url URL to fetch +# args configuration options +# +# Results: +# Returns data from file + +proc ::uri::file_geturl {url args} { + variable file:counter + + set var [namespace current]::file[incr file:counter] + upvar #0 $var state + array set state {data {}} + + array set parts [split $url] + + set ch [open $parts(path)] + # Could determine text/binary from file extension, + # except on Macintosh + # fconfigure $ch -translation binary + set state(data) [read $ch] + close $ch + + return $var +} + +# ::uri::join -- +# +# Format a URL +# +# Arguments: +# args components, key-value format +# +# Results: +# A URL + +proc ::uri::join args { + array set components $args + + return [eval [linsert $args 0 Join[string totitle $components(scheme)]]] +} + +# ::uri::canonicalize -- +# +# Canonicalize a URL +# +# Acknowledgements: +# Andreas Kupries +# +# Arguments: +# uri URI (which contains a path component) +# +# Results: +# The canonical form of the URI + +proc ::uri::canonicalize uri { + + # Make uri canonical with respect to dots (path changing commands) + # + # Remove single dots (.) => pwd not changing + # Remove double dots (..) => gobble previous segment of path + # + # Fixes for this command: + # + # * Ignore any url which cannot be split into components by this + # module. Just assume that such urls do not have a path to + # canonicalize. + # + # * Ignore any url which could be split into components, but does + # not have a path component. + # + # In the text above 'ignore' means + # 'return the url unchanged to the caller'. + + if {[catch {array set u [::uri::split $uri]}]} { + return $uri + } + if {![info exists u(path)]} { + return $uri + } + + set uri $u(path) + + # Remove leading "./" "../" "/.." (and "/../") + regsub -all -- {^(\./)+} $uri {} uri + regsub -all -- {^/(\.\./)+} $uri {/} uri + regsub -all -- {^(\.\./)+} $uri {} uri + + # Remove inner /./ and /../ + while {[regsub -all -- {/\./} $uri {/} uri]} {} + while {[regsub -all -- {/[^/]+/\.\./} $uri {/} uri]} {} + while {[regsub -all -- {^[^/]+/\.\./} $uri {} uri]} {} + # Munge trailing /.. + while {[regsub -all -- {/[^/]+/\.\.} $uri {/} uri]} {} + if { $uri == ".." } { set uri "/" } + + set u(path) $uri + set uri [eval [linsert [array get u] 0 ::uri::join]] + + return $uri +} + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# regular expressions covering various url schemes + +# Currently known URL schemes: +# +# (RFC 1738) +# ------------------------------------------------ +# scheme basic syntax of scheme specific part +# ------------------------------------------------ +# ftp //:@://...//;type= +# +# http //:/? +# +# gopher //:/ +# %09 +# %09%09 +# +# mailto +# news +# +# nntp //:// +# telnet //:@:/ +# wais //:/ +# //:/? +# //:/// +# file /// +# prospero //:/;= +# ------------------------------------------------ +# +# (RFC 2111) +# ------------------------------------------------ +# scheme basic syntax of scheme specific part +# ------------------------------------------------ +# mid message-id +# message-id/content-id +# cid content-id +# ------------------------------------------------ +# +# (RFC 2255) +# ------------------------------------------------ +# scheme basic syntax of scheme specific part +# ------------------------------------------------ +# ldap //:/???? +# ------------------------------------------------ + +# FTP +uri::register ftp { + variable escape [set [namespace parent [namespace current]]::basic::escape] + variable login [set [namespace parent [namespace current]]::basic::login] + + variable charN {[a-zA-Z0-9$_.+!*'(,)?:@&=-]} + variable char "(${charN}|${escape})" + variable segment "${char}*" + variable path "${segment}(/${segment})*" + + variable type {[AaDdIi]} + variable typepart ";type=(${type})" + variable schemepart \ + "//${login}(/${path}(${typepart})?)?" + + variable url "ftp:${schemepart}" +} + +# FILE +uri::register file { + variable host [set [namespace parent [namespace current]]::basic::host] + variable path [set [namespace parent [namespace current]]::ftp::path] + + variable schemepart "//(${host}|localhost)?/${path}" + variable url "file:${schemepart}" +} + +# HTTP +uri::register http { + variable escape \ + [set [namespace parent [namespace current]]::basic::escape] + variable hostOrPort \ + [set [namespace parent [namespace current]]::basic::hostOrPort] + + variable charN {[a-zA-Z0-9$_.+!*'(,);:@&=-]} + variable char "($charN|${escape})" + variable segment "${char}*" + + variable path "${segment}(/${segment})*" + variable search $segment + variable schemepart \ + "//${hostOrPort}(/${path}(\\?${search})?)?" + + variable url "http:${schemepart}" +} + +# GOPHER +uri::register gopher { + variable xChar \ + [set [namespace parent [namespace current]]::basic::xChar] + variable hostOrPort \ + [set [namespace parent [namespace current]]::basic::hostOrPort] + variable search \ + [set [namespace parent [namespace current]]::http::search] + + variable type $xChar + variable selector "$xChar*" + variable string $selector + variable schemepart \ + "//${hostOrPort}(/(${type}(${selector}(%09${search}(%09${string})?)?)?)?)?" + variable url "gopher:${schemepart}" +} + +# MAILTO +uri::register mailto { + variable xChar [set [namespace parent [namespace current]]::basic::xChar] + variable host [set [namespace parent [namespace current]]::basic::host] + + variable schemepart "$xChar+(@${host})?" + variable url "mailto:${schemepart}" +} + +# NEWS +uri::register news { + variable escape [set [namespace parent [namespace current]]::basic::escape] + variable alpha [set [namespace parent [namespace current]]::basic::alpha] + variable host [set [namespace parent [namespace current]]::basic::host] + + variable aCharN {[a-zA-Z0-9$_.+!*'(,);/?:&=-]} + variable aChar "($aCharN|${escape})" + variable gChar {[a-zA-Z0-9$_.+-]} + variable newsgroup-name "${alpha}${gChar}*" + variable message-id "${aChar}+@${host}" + variable schemepart "\\*|${newsgroup-name}|${message-id}" + variable url "news:${schemepart}" +} + +# WAIS +uri::register wais { + variable uChar \ + [set [namespace parent [namespace current]]::basic::xChar] + variable hostOrPort \ + [set [namespace parent [namespace current]]::basic::hostOrPort] + variable search \ + [set [namespace parent [namespace current]]::http::search] + + variable db "${uChar}*" + variable type "${uChar}*" + variable path "${uChar}*" + + variable database "//${hostOrPort}/${db}" + variable index "//${hostOrPort}/${db}\\?${search}" + variable doc "//${hostOrPort}/${db}/${type}/${path}" + + #variable schemepart "${doc}|${index}|${database}" + + variable schemepart \ + "//${hostOrPort}/${db}((\\?${search})|(/${type}/${path}))?" + + variable url "wais:${schemepart}" +} + +# PROSPERO +uri::register prospero { + variable escape \ + [set [namespace parent [namespace current]]::basic::escape] + variable hostOrPort \ + [set [namespace parent [namespace current]]::basic::hostOrPort] + variable path \ + [set [namespace parent [namespace current]]::ftp::path] + + variable charN {[a-zA-Z0-9$_.+!*'(,)?:@&-]} + variable char "(${charN}|$escape)" + + variable fieldname "${char}*" + variable fieldvalue "${char}*" + variable fieldspec ";${fieldname}=${fieldvalue}" + + variable schemepart "//${hostOrPort}/${path}(${fieldspec})*" + variable url "prospero:$schemepart" +} + +# LDAP +uri::register ldap { + variable hostOrPort \ + [set [namespace parent [namespace current]]::basic::hostOrPort] + + # very crude parsing + variable dn {[^?]*} + variable attrs {[^?]*} + variable scope "base|one|sub" + variable filter {[^?]*} + # extensions are not handled yet + + variable schemepart "//${hostOrPort}(/${dn}(\?${attrs}(\?(${scope})(\?${filter})?)?)?)?" + variable url "ldap:$schemepart" +} + +package provide uri 1.2.1 diff --git a/lib/uri/urn-scheme.tcl b/lib/uri/urn-scheme.tcl new file mode 100644 index 0000000..0819dde --- /dev/null +++ b/lib/uri/urn-scheme.tcl @@ -0,0 +1,136 @@ +# urn-scheme.tcl - Copyright (C) 2001 Pat Thoyts +# +# extend the uri package to deal with URN (RFC 2141) +# see http://www.normos.org/ietf/rfc/rfc2141.txt +# +# Released under the tcllib license. +# +# $Id: urn-scheme.tcl,v 1.11 2005/09/28 04:51:24 andreas_kupries Exp $ +# ------------------------------------------------------------------------- + +package require uri 1.1.2 + +namespace eval ::uri {} +namespace eval ::uri::urn { + variable version 1.0.2 +} + +# ------------------------------------------------------------------------- + +# Description: +# Called by uri::split with a url to split into its parts. +# +proc ::uri::SplitUrn {uri} { + #@c Split the given uri into then URN component parts + #@a uri: the URI to split without it's scheme part. + #@r List of the component parts suitable for 'array set' + + upvar \#0 [namespace current]::urn::URNpart pattern + array set parts {nid {} nss {}} + if {[regexp -- ^$pattern $uri -> parts(nid) parts(nss)]} { + return [array get parts] + } else { + error "invalid urn syntax: \"$uri\" could not be parsed" + } +} + + +# ------------------------------------------------------------------------- + +proc ::uri::JoinUrn args { + #@c Join the parts of a URN scheme URI + #@a list of nid value nss value + #@r a valid string representation for your URI + variable urn::NIDpart + + array set parts [list nid {} nss {}] + array set parts $args + if {! [regexp -- ^$NIDpart$ $parts(nid)]} { + error "invalid urn: nid is invalid" + } + set url "urn:$parts(nid):[urn::quote $parts(nss)]" + return $url +} + +# ------------------------------------------------------------------------- + +# Quote the disallowed characters according to the RFC for URN scheme. +# ref: RFC2141 sec2.2 +proc ::uri::urn::quote {url} { + variable trans + + set ndx 0 + set result "" + while {[regexp -indices -- "\[^$trans\]" $url r]} { + set ndx [lindex $r 0] + scan [string index $url $ndx] %c chr + set rep %[format %.2X $chr] + if {[string match $rep %00]} { + error "invalid character: character $chr is not allowed" + } + + incr ndx -1 + append result [string range $url 0 $ndx] $rep + incr ndx 2 + set url [string range $url $ndx end] + } + append result $url + return $result +} + +# ------------------------------------------------------------------------- +# Perform the reverse of urn::quote. + +if { [package vcompare [package provide Tcl] 8.3] < 0 } { + # Before Tcl 8.3 we do not have 'regexp -start'. We simulate it by + # using 'string range' and adjusting the match results. + + proc ::uri::urn::unquote {url} { + set result "" + set start 0 + while {[regexp -indices {%[0-9a-fA-F]{2}} [string range $url $start end] match]} { + foreach {first last} $match break + incr first $start ; # Make the indices relative to the true string. + incr last $start ; # I.e. undo the effect of the 'string range' on match results. + append result [string range $url $start [expr {$first - 1}]] + append result [format %c 0x[string range $url [incr first] $last]] + set start [incr last] + } + append result [string range $url $start end] + return $result + } +} else { + proc ::uri::urn::unquote {url} { + set result "" + set start 0 + while {[regexp -start $start -indices {%[0-9a-fA-F]{2}} $url match]} { + foreach {first last} $match break + append result [string range $url $start [expr {$first - 1}]] + append result [format %c 0x[string range $url [incr first] $last]] + set start [incr last] + } + append result [string range $url $start end] + return $result + } +} + +# ------------------------------------------------------------------------- + +::uri::register {urn URN} { + variable NIDpart {[a-zA-Z0-9][a-zA-Z0-9-]{0,31}} + variable esc {%[0-9a-fA-F]{2}} + variable trans {a-zA-Z0-9$_.+!*'(,):=@;-} + variable NSSpart "($esc|\[$trans\])+" + variable URNpart "($NIDpart):($NSSpart)" + variable schemepart $URNpart + variable url "urn:$NIDpart:$NSSpart" +} + +# ------------------------------------------------------------------------- + +package provide uri::urn $::uri::urn::version + +# ------------------------------------------------------------------------- +# Local Variables: +# indent-tabs-mode: nil +# End: diff --git a/lib/uuid/pkgIndex.tcl b/lib/uuid/pkgIndex.tcl new file mode 100644 index 0000000..94f7d14 --- /dev/null +++ b/lib/uuid/pkgIndex.tcl @@ -0,0 +1,8 @@ +# pkgIndex.tcl - +# +# uuid package index file +# +# $Id: pkgIndex.tcl,v 1.2 2005/09/30 05:36:39 andreas_kupries Exp $ + +if {![package vsatisfies [package provide Tcl] 8.2]} {return} +package ifneeded uuid 1.0.1 [list source [file join $dir uuid.tcl]] diff --git a/lib/uuid/uuid.tcl b/lib/uuid/uuid.tcl new file mode 100644 index 0000000..35a5ca5 --- /dev/null +++ b/lib/uuid/uuid.tcl @@ -0,0 +1,216 @@ +# uuid.tcl - Copyright (C) 2004 Pat Thoyts +# +# UUIDs are 128 bit values that attempt to be unique in time and space. +# +# Reference: +# http://www.opengroup.org/dce/info/draft-leach-uuids-guids-01.txt +# +# uuid: scheme: +# http://www.globecom.net/ietf/draft/draft-kindel-uuid-uri-00.html +# +# Usage: uuid::uuid generate +# uuid::uuid equal $idA $idB + +namespace eval uuid { + variable version 1.0.1 + variable accel + array set accel {critcl 0} + + namespace export uuid + + variable uid + if {![info exists uid]} { + set uid 1 + } + + if {[package vcompare [package provide Tcl] 8.4] < 0} { + package require struct::list + interp alias {} ::uuid::lset {} ::struct::list::lset + } + + proc K {a b} {set a} +} + +# Generates a binary UUID as per the draft spec. We generate a pseudo-random +# type uuid (type 4). See section 3.4 +# +proc ::uuid::generate_tcl {} { + package require md5 2 + variable uid + + set tok [md5::MD5Init] + md5::MD5Update $tok [clock seconds]; # timestamp + md5::MD5Update $tok [clock clicks]; # system incrementing counter + md5::MD5Update $tok [incr uid]; # package incrementing counter + md5::MD5Update $tok [info hostname]; # spatial unique id (poor) + md5::MD5Update $tok [pid]; # additional entropy + md5::MD5Update $tok [array get ::tcl_platform] + + # More spatial information -- better than hostname. + # bug 1150714: opening a server socket may raise a warning messagebox + # with WinXP firewall, using ipconfig will return all IP addresses + # including ipv6 ones if available. ipconfig is OK on win98+ + if {[string equal $::tcl_platform(platform) "windows"]} { + catch {exec ipconfig} config + md5::MD5Update $tok $config + } else { + catch { + set s [socket -server void -myaddr [info hostname] 0] + K [fconfigure $s -sockname] [close $s] + } r + md5::MD5Update $tok $r + } + + if {[package provide Tk] != {}} { + md5::MD5Update $tok [winfo pointerxy .] + md5::MD5Update $tok [winfo id .] + } + + set r [md5::MD5Final $tok] + binary scan $r c* r + + # 3.4: set uuid versioning fields + lset r 8 [expr {([lindex $r 8] & 0x7F) | 0x40}] + lset r 6 [expr {([lindex $r 6] & 0x0F) | 0x40}] + + return [binary format c* $r] +} + +if {[string equal $tcl_platform(platform) "windows"] + && [package provide critcl] != {}} { + namespace eval uuid { + critcl::ccode { + #define WIN32_LEAN_AND_MEAN + #define STRICT + #include + #include + typedef long (__stdcall *LPFNUUIDCREATE)(UUID *); + typedef const unsigned char cu_char; + } + critcl::cproc generate_c {Tcl_Interp* interp} ok { + HRESULT hr = S_OK; + int r = TCL_OK; + UUID uuid = {0}; + HMODULE hLib; + LPFNUUIDCREATE lpfnUuidCreate = NULL; + + hLib = LoadLibrary(_T("rpcrt4.dll")); + if (hLib) + lpfnUuidCreate = (LPFNUUIDCREATE) + GetProcAddress(hLib, "UuidCreate"); + if (lpfnUuidCreate) { + Tcl_Obj *obj; + lpfnUuidCreate(&uuid); + obj = Tcl_NewByteArrayObj((cu_char *)&uuid, sizeof(uuid)); + Tcl_SetObjResult(interp, obj); + } else { + Tcl_SetResult(interp, "error: failed to create a guid", + TCL_STATIC); + r = TCL_ERROR; + } + return r; + } + } +} + +# Convert a binary uuid into its string representation. +# +proc ::uuid::tostring {uuid} { + binary scan $uuid H* s + foreach {a b} {0 7 8 11 12 15 16 19 20 end} { + append r [string range $s $a $b] - + } + return [string tolower [string trimright $r -]] +} + +# Convert a string representation of a uuid into its binary format. +# +proc ::uuid::fromstring {uuid} { + return [binary format H* [string map {- {}} $uuid]] +} + +# Compare two uuids for equality. +# +proc ::uuid::equal {left right} { + set l [fromstring $left] + set r [fromstring $right] + return [string equal $l $r] +} + +# Call our generate uuid implementation +proc ::uuid::generate {} { + variable accel + if {$accel(critcl)} { + return [generate_c] + } else { + return [generate_tcl] + } +} + +# uuid generate -> string rep of a new uuid +# uuid equal uuid1 uuid2 +# +proc uuid::uuid {cmd args} { + switch -exact -- $cmd { + generate { + if {[llength $args] != 0} { + return -code error "wrong # args:\ + should be \"uuid generate\"" + } + return [tostring [generate]] + } + equal { + if {[llength $args] != 2} { + return -code error "wrong \# args:\ + should be \"uuid equal uuid1 uuid2\"" + } + return [eval [linsert $args 0 equal]] + } + default { + return -code error "bad option \"$cmd\":\ + must be generate or equal" + } + } +} + +# ------------------------------------------------------------------------- + +# LoadAccelerator -- +# +# This package can make use of a number of compiled extensions to +# accelerate the digest computation. This procedure manages the +# use of these extensions within the package. During normal usage +# this should not be called, but the test package manipulates the +# list of enabled accelerators. +# +proc ::uuid::LoadAccelerator {name} { + variable accel + set r 0 + switch -exact -- $name { + critcl { + if {![catch {package require tcllibc}]} { + set r [expr {[info command ::uuid::generate_c] != {}}] + } + } + default { + return -code error "invalid accelerator package:\ + must be one of [join [array names accel] {, }]" + } + } + set accel($name) $r +} + +# ------------------------------------------------------------------------- + +# Try and load a compiled extension to help. +namespace eval ::uuid { + foreach e {critcl} { if {[LoadAccelerator $e]} { break } } +} + +package provide uuid $::uuid::version + +# ------------------------------------------------------------------------- +# Local variables: +# mode: tcl +# indent-tabs-mode: nil +# End: diff --git a/main.tcl b/main.tcl new file mode 100644 index 0000000..f6e700c --- /dev/null +++ b/main.tcl @@ -0,0 +1,13 @@ +# Starkit setup. +# +package require Tcl 8.4 +package require starkit +if {[starkit::startup] ne "sourced"} { + set f [file join $starkit::topdir bin [lindex $argv 0].tcl] + if {[file exists $f]} { + set argv [lrange $argv 1 end] + source $f + } else { + source [file join $starkit::topdir bin bullfrog.tcl] + } +} diff --git a/nokit.tcl b/nokit.tcl new file mode 100644 index 0000000..bf81a24 --- /dev/null +++ b/nokit.tcl @@ -0,0 +1,7 @@ +set auto_path [concat [file join [file dirname [info script]] lib] $auto_path] +if {[lindex $argv 0] eq "irc"} { + set argv [lrange $argv 1 end] + source [file join [file dirname [info script]] bin irc.tcl] +} else { + source [file join [file dirname [info script]] bin demo.tcl] +} diff --git a/tclkit.ico b/tclkit.ico new file mode 100644 index 0000000000000000000000000000000000000000..d7850ff2bb91c81ce20e8f46ecc70ce76d3b976e GIT binary patch literal 10134 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