Zarutian: An start of an SAMv2 library for Tcl
# I, Zarutian, hereby put this code into the International public domain. package require Tcl 8.5 package provide i2p/sam2 0.2 # 0.1 was refactored into 0.2 # 0.2 isnt spec implementation finished namespace eval i2p {} namespace eval i2p::sam2 { variable defaults { bridge {host localhost port none} style STREAM} variable session_counter 0 variable sessions # public below proc new_session_for_streams {params} { if {![dict exists $params accept_callback]} { error "an accept_callback must be provided" } set i [next_session_nr] variable sessions dict set sessions $i accept_callback [dict get $params accept_callback] dict set params style STREAM dict set params session_id $i return [new_session $params] } proc new_session_for_datagrams {params} { if {![dict exists $params recieve_callback]} { error "an recive_callback must be provided" } set i [next_session_nr] variable sessions dict set sessions $i datagram_recieve_callback [dict get $params recieve_callback] dict set params style DATAGRAM dict set params session_id $i return [new_session $params] } proc new_session_for_rawgrams {params} { if {![dict exists $params recieve_callback]} { error "an recive_callback must be provided" } set i [next_session_nr] variable sessions dict set sessions $i rawgram_recieve_callback [dict get $params recieve_callback] dict set params style RAW dict set params session_id $i return [new_session $params] } proc new_session {params} { if {[dict exists $params session_id]} { set i [dict get $params session_id] } else { set i [next_session_nr] } variable defaults if {![dict exists $params style]} { error "an style must be provided" } if {![dict exists $params bridge]} { dict set params bridge [dict get $defaults bridge] } if {![dict exists $params bridge host]} { dict set params bridge host [dict get $defaults bridge host] } if {![dict exists $params bridge port]} { dict set params bridge port [dict get $defaults bridge port] } if {![dict exists $params stream]} { dict set params stream [dict get $defaults stream] } if {![dict exists $params condition_callback} { error "an condition_callback must be provided" } if {![dict exists $params my_name]} { error "my_name must be provided" } variable sessions dict set sessions $i condition_callback [dict get $params condition_callback] dict set sessions $i my_name [dict get $params my_name] dict set sessions $i style [dict get $params style] dict set sessions $i stream_counter 0 dict set sessions $i stage 1 dict set sessions $i socket [set sock [socket -async [dict get $params bridge host] [dict get $params bridge port]]] fconfigure $sock -buffering none -translation binary -encoding binary -blocking no fileevent $sock readable [list i2p::sam2::raw_session_recive $i] raw_session_send $sock "HELLO VERSION MIN=2.0 MAX=2.0\n" return $i } proc new_stream {session_id destination callback} { variable sessions if {![string equal "STREAM" [dict get $sessions $session_id style]]} { error "cant open an new stream in session $session_id as its style isnt STREAM" } set sock [dict get $sessions $session_id socket] set i [expr {[dict get $sessions $session_id stream_counter] + 1}] dict set sessions $session_id stream_counter $i set p "STREAM CONNECT ID=" append p $i append p " DESTINATION=" append p $destination append p "\n" raw_session_send $sock $p dict set sessions $session_id streams $i destination $destination dict set sessions $session_id streams $i callback $callback dict set sessions $session_id streams $i status connecting dict set sessions $session_id streams $i ready_to_send no return $i } proc send_on_stream {session_id stream_id data} { variable sessions if {![string equal "STREAM" [dict get $sessions $session_id style]]} { error "cant send on stream $stream_id in session $session_id as its style isnt STREAM" } set sock [dict get $sessions $session_id socket] if {[dict get $sessions $session_id streams $stream_id ready_to_send]} { set p "STREAM SEND ID=" append p $stream_id append p " SIZE=" if {[string length $data] <= 32768} { append p [string length $data] append p "\n" append p $data dict set sessions $sessions_id streams $stream_id more2send no } else { append p 32768 append p "\n" append p [string range $data 0 32767] dict append sessions $session_id streams $stream_id out_buffer [string range $data 32768 end] dict set sessions $session_id streams $stream_id more2send yes } raw_session_send $sock $p } else { dict append sessions $session_id streams $stream_id out_buffer $data dict set sessions $session_id streams $stream_id more2send yes } } proc close_stream {session_id stream_id} { variable sessions if {![string equal "STREAM" [dict get $sessions $session_id style]]} { error "cant close stream $stream_id in session $session_id as its style isnt STREAM" } set sock [dict get $sessions $session_id socket] if {[dict get $sessions $session_id streams $stream_id more2send]} { dict set sessions $session_id streams $stream_id closeAfterAllIsSent yes } else { raw_session_send $sock "STREAM CLOSE ID=[set stream_id]\n" } } proc send_rawgram {session_id destination data} { variable sessions if {![string equal "RAW" [dict get $sessions $session_id style]]} { error "cant send rawgram via session $session_id as its style isnt RAW" } set sock [dict get $sessions $session_id socket] set p "RAW SEND " append p "DESTINATION=" append p $destination append p " SIZE=" append p [string length $data] append p "\n" append p $data raw_session_send $sock $p } proc send_datagram {session_id destination data} { variable sessionscant close stream $stream_ if {![string equal "DATAGRAM" [dict get $sessions $session_id style]]} { error "cant send datagram via session $session_id as its style isnt DATAGRAM" } set sock [dict get $sessions $session_id socket] set p "DATAGRAM SEND " append p "DESTINATION=" append p $destination append p " SIZE=" append p [string length $data] append p "\n" append p $data raw_session_send $sock $p } # internal below proc invoke_condition_callback {args} { variable sessions lassign $args session_id after 0 [list {*}[dict get $sessions $session_id condition_callback] {*}$args] return } proc get_pairs {line} { dict set out MESSAGE "" foreach pair [split [string range $line 14 end] " "] { set tmp [string first "=" $pair] set key [string range $pair 0 [expr {$tmp - 1}]] set value [string range $pair [expr {$tmp + 1}] end] if {[string equal $key "MESSAGE"]} { set tmp [expr {[string first "MESSAGE=" $line] + 8}] dict set out MESSAGE [string range $line $tmp end] } elseif {[string equal $key "DESTINATION"]} { # cant recall if DESTINATION needed any special handling like MESSAGE -Zarutian dict set out DESTINATION $value } else { dict set out $key $value } } return $out } proc consume_one_line_of_rawbuffer {session_id} { variable sessions set buffer [dict get $sessions $session_id raw_buffer] dict set sessions $session_id raw_buffer [string range $buffer [expr {[string first "\n" $buffer] + 1} end] return } proc next_session_nr {} { variable session_counter return session[incr session_counter] } proc raw_session_send {socket data} { catch { puts -nonewline $socket $data; flush $socket } } proc raw_session_recive {session_id} { variable sessions set sock [dict get $sessions $session_id socket] if {[eof $sock]} { close $sock invoke_condition_callback $session_id bridge connection closed return } dict append sessions $session_id raw_buffer [set buffer [read $sock]] if {[string first "\n" $buffer] != -1} { switch -exact -- [lindex $buffer 0] { "HELLO" { hello_recive $session_id } "SESSION" { session_recive $session_id } "STREAM" { stream_recive $session_id } "DATAGRAM" { datagram_recive $session_id } "RAW" { rawgram_recive $session_id } "NAMING" { naming_recive $session_id } "DEST" { dest_recive $session_id } default { invoke_condition_callback $session_id bridge protocol unknown_message_type return } } } return } proc hello_recive {session_id} { variable sessions set buffer [dict get $sessions $session_id raw_buffer] set line [lindex [split $buffer "\n"] 0] if {[string equal "REPLY" [lindex $line 1]]} { set params [get_pairs [string range $line 12 end]] if {![dict exists $params RESULT]} { invoke_condition_callback $session_id bridge connection missingHelloReplyParam RESULT return } if {![dict exists $params VERSION]} { invoke_condition_callback $session_id bridge connection missingHelloReplyParam VERSION return } if {[string equal [dict get $params RESULT] "NOVERSION"]} { invoke_condition_callback $session_id bridge connection noversion return } if {![string equal [dict get $params RESULT] "OK"]} { invoke_condition_callback $session_id bridge connection resultNotOk return } if {![string equal [dict get $params VERSION] "2.0"]} { invoke_condition_callback $session_id bridge connection versionNot2.0 return } #--- consume_one_line_of_raw_buffer $session_id set p "SESSION CREATE STYLE=" append p [string toupper [dict get $sessions $session_id style]] append p " DESTINATION=" append p [dict get $sessions $session_id my_name] append p "\n" raw_session_send $sock $p dict set sessions $session_id stage 2 } return } proc session_recive {session_id} { variable sessions set buffer [dict get $sessions $session_id raw_buffer] set line [lindex [split $buffer "\n"] 0] if {[string equal [lindex $line 1] "STATUS"]} { set params [get_pairs [string range $line 15 end]] if {![dict exists $params RESULT]} { invoke_condition_callback $session_id bridge session statusParamMissing RESULT return } if {![dict exists $params DESTINATION]} { invoke_condition_callback $session_id bridge session statusParamMissing DESTINATION return } if {![string equal "OK" [dict get $params RESULT]]} { if {![dict exists $params MESSAGE]} { dict set params MESSAGE "" } consume_one_line_of_rawbuffer $session_id invoke_condition_callback $session_id bridge session statusNotOk [dict get $params RESULT] [dict get $params MESSAGE] } else { consume_one_line_of_rawbuffer $session_id dict set sessions $session_id stage 3 } return } } proc stream_recive {session_id} { variable sessions set sock [dict get $sessions $session_id socket] set buffer [dict get $sessions $session_id raw_buffer] set line [lindex [split $buffer "\n"] 0] switch -exact -- [lindex $line 1] { "STATUS" { set params [get_pairs [string range $line 14 end]] if {![dict exists $params RESULT]} { invoke_condition_callback $session_id bridge stream statusParamMissing RESULT return } if {![dict exists $params ID]} {} if {![dict exists $params MESSAGE]} { dict set params MESSAGE "" } if {$done != 2} { after 0 [list {*}[dict get $sessions $session_id condition_callback] bridge protocol statusInvalidParams $session_id] return } if {[string equal $RESULT "OK"]} { set p "STREAM RECEIVE ID=" append p $ID append p " LIMIT=NONE\n" raw_send $sock $p } after 0 [list {*}[dict get $sessions $session_id streams $ID callback] connected $RESULT $MESSAGE] dict set sessions $session_id raw_buffer [string range $buffer [expr {[string first "\n" $buffer] + 1}] end] return } "CONNECTED" { set done 0 foreach pair [split [string range $line 14 end] " "] { set tmp [string first "=" $pair] set key [string range $pair 0 [expr {$tmp - 1}]] set value [string range $pair [expr {$tmp + 1}] end] if {[string equal $key "DESTINATION"]} { incr done; set DESTINATION $value } if {[string equal $key "ID"]} { incr done; set ID $value } } if {$done != 2} { after 0 [list {*}[dict get $sessions $session_id condition_callback] bridge protocol connectedInvalidParams $session_id] return } set p "STREAM RECEIVE ID=" append p $ID append p " LIMIT=NONE\n" raw_send $sock $p after 0 [list {*}[dict get $sessions condition_callback] stream connected $ID $DESTINATION] dict set sessions $session_id raw_buffer [string range $buffer [expr {[string first "\n" $buffer] + 1}] end] return } "SEND" { set done 0 foreach pair [split [string range $line 14 end] " "] { set tmp [string first "=" $pair] set key [string range $pair 0 [expr {$tmp - 1}]] set value [string range $pair [expr {$tmp + 1}] end] if {[string equal $key "RESULT"]} { incr done; set RESULT $value } if {[string equal $key "ID"]} { incr done; set ID $value } if {[string equal $key "STATE"]} { incr done; set STATE $value } if {$done != 3} { after 0 [list {*}[dict get $sessions $session_id condition_callback] bridge protocol sendInvalidParams $session_id] return } if {[string equal $RESULT "FAILED"]} { after 0 [list {*}[dict get $sessions $session_id streams $ID callback] sendFail] } if {[string equal $STATE "BUFFER_FULL"]} { dict set sessions $session_id streams $ID ready_to_send no } elseif {[string equal $STATE "READY"]} { dict set sessions $session_id streams $ID ready_to_send yes send_next $session_id $ID } dict set sessions $session_id raw_buffer [string range $buffer [expr {[string first "\n" $buffer] + 1}] end] return } "READY_TO_SEND" { set done 0 foreach pair [split [string range $line 14 end] " "] { set tmp [string first "=" $pair] set key [string range $pair 0 [expr {$tmp - 1}]] set value [string range $pair [expr {$tmp + 1}] end] if {[string equal $key "ID"]} { incr done; set ID $value } } if {$done != 1} { after 0 [list {*}[dict get $sessions $session_id condition_callback] bridge protocol ReadyToSendInvalidParams $session_id] return } send_next $session_id $ID dict set sessions $session_id raw_buffer [string range $buffer [expr {[string first "\n" $buffer] + 1}] end] return } "CLOSED" { } "RECEIVED" {} } return } proc datagram_recive {session_id} { variable sessions set sock [dict get $sessions $session_id socket] set buffer [dict get $sessions $session_id raw_buffer] set line [lindex [split $buffer "\n"] 0] if {[string equal "RECEIVED" [lindex $line 1]]} { } return } proc rawgram_recive {session_id} { variable sessions set sock [dict get $sessions $session_id socket] set buffer [dict get $sessions $session_id raw_buffer] set line [lindex [split $buffer "\n"] 0] if {[string equal "RECEIVED" [lindex $line 1]]} { } return } proc naming_recive {session_id} {} proc dest_recive {session_id} {} proc send_next {session_id stream_id} { variable sessions set sock [dict get $sessions $session_id socket] if {[dict get $sessions $session_id streams $stream_id more2send]} { set p "STREAM SEND ID=" append p $ID append p " SIZE=" if {[string length [dict get $sessions $session_id streams $stream_id out_buffer]] <= 32768} { append p [string length [dict get $sessions $session_id streams $stream_id out_buffer]] append p "\n" append p [dict get $sessions $session_id streams $stream_id out_buffer] dict set sessions $sessions_id streams $stream_id more2send no } else { append p 32768 append p "\n" append p [string range [dict get $sessions $session_id streams $stream_id out_buffer] 0 32767] dict set sessions $session_id streams $stream_id out_buffer \ [string range [dict get $sessions $session_id streams $stream_id out_buffer] 32768 end] dict set sessions $session_id streams $stream_id more2send yes } raw_send $sock $p } else { if {[dict get $sessions $session_id streams $stream_id closeAfterAllIsSent]} { raw_send $sock "STREAM CLOSE ID=[set stream_id]\n" } } } }